/**
 * llmindex.io — methodology page: full pipeline detail, live sample items, references
 * Author:  Simon-Pierre Boucher
 * Contact: contact@spboucher.ai
 * License: Proprietary — © Simon-Pierre Boucher, all rights reserved
 *
 * Sample items are rendered from documentation-only seeds ("docs:<template>")
 * that are never used in scored runs — templates are public by design, scored
 * instantiations are not.
 */
import {
  CONTAMINATION_DELTA_FLOOR,
  DOMAINS,
  DOMAIN_WEIGHTS,
  DUEL_DOMAINS,
  INDEX_VERSION,
  IRT_HYPERPARAMS,
  SUBMETRIC_WEIGHTS,
  THETA_SCALE,
} from '@llmindex/scoring';
import { TEMPLATES, createRng } from '@llmindex/items';

export const revalidate = 3600;

const PILLARS: Array<{ title: string; body: string }> = [
  {
    title: '1 · IRT-based scoring (2PL)',
    body: 'Every item carries a fitted difficulty (b) and discrimination (a): P(correct) = σ(a·(θ−b)). Ability θ is a MAP estimate with priors θ~N(0,1), b~N(0,1.5), log a~N(0,0.5), fitted by a missing-aware numpy optimizer; θ standard errors come from the Fisher information. Items solved by everyone (or no one) carry zero ranking information — items with a<0.3 or |b|>3 are auto-flagged for retirement after every refit. Raw accuracy is never the score.',
  },
  {
    title: '2 · Dynamic item generation',
    body: 'Items are instantiated from versioned template generators with seeded value substitution, paraphrase rotation and structural perturbation — fresh for every scored batch, so no fixed test set exists to memorize. A frozen anchor stream (≤20% of any run) is kept identical across runs; the accuracy gap anchor−fresh is published per model as contamination_delta and maps to a resistance sub-metric via clamp01(1 − Δ/0.2).',
  },
  {
    title: '3 · Difficulty engineered for discrimination',
    body: 'Item information peaks where a model has ~50% success probability. Generators therefore stack proven difficulty knobs: 6-8-step dependent arithmetic chains (errors compound multiplicatively), chained sub-problems whose answers feed forward, provably-inert distractor clauses, counterfactual rules (base-7/8/9/11/13 arithmetic), interior-rank deduction with decoy entities, nested control-flow traces, and constraint stacking — each knob re-randomized per run.',
  },
  {
    title: '4 · Agentic: simulated tool-calling',
    body: 'Home-made mock environments (support-desk triage under policy, treasury ledger with overdraft pre-funding, dependency-ordered deployments) present a tool catalog salted with distractor tools. A deterministic simulator computes the unique correct call sequence; grading is canonical-JSON equality of the emitted sequence — binary, no judges, no partial credit. A dedicated context-load family buries the relevant records among hundreds of near-miss decoys (same customer/wrong region, right region/wrong status), making prompt length itself the difficulty knob.',
  },
  {
    title: '5 · Terminal: simulated shell, exact prediction',
    body: 'No shell ever executes. A closed, unambiguous POSIX subset (fixed-string grep, cut, byte-order C-locale sort, integer-only awk aggregation, head/tail) is simulated in TypeScript over generated CSV data; models predict exact stdout, final file trees after mv/cp/rm/cd sequences with relative paths, and && / || short-circuit execution traces with exit codes. Locale-dependent, GNU/BSD-divergent and float-formatting constructs are excluded by design, so every answer is unambiguous.',
  },
  {
    title: '6 · Vision OCR under clutter',
    body: 'Generated SVG scenes — rasterized server-side — embed target codes among rotated decoy codes, noise strokes and low-contrast patches, plus rendered mini-tables requiring grounded arithmetic. The glyph set excludes visually ambiguous characters (0/O, 1/l/I, 5/S, 8/B), so difficulty comes from clutter and selection, never unfair ambiguity. Text-only models skip the domain; their Global Index renormalizes.',
  },
  {
    title: '7 · Judged duels + Bradley-Terry (writing, safety, SVG design)',
    body: 'Open-ended domains use pairwise duels: both models answer the same generated task (constrained creative writing; delicate gray-zone situations; reproducing a real-world logo in raw SVG from memory). A 3-judge cross-provider panel rates each duel with recorded position swaps; a model never judges its own duel; empty responses never count as wins. Verdicts feed a Bradley-Terry fit (MM algorithm, ties as half-wins, Fisher-information SEs), standardized to the same θ scale as IRT domains. Judge panel agreement is published with every run.',
  },
  {
    title: '8 · Robust answer extraction (measured, not guessed)',
    body: 'Extraction never confounds formatting with ability: an ordered cascade accepts "ANSWER: x" in any markdown wrapping, FINAL ANSWER variants, LaTeX \\boxed{}, and fenced code blocks for JSON/multi-line answers; numbers are normalized across thousands separators, currency signs and units; hyphen/space orthography variants are unified. Truncated completions (finish_reason=length) are unscored — never counted as wrong — and every model gets a 16k-token completion budget so reasoning models can finish thinking.',
  },
  {
    title: '9 · Consistency, calibration, cost',
    body: 'Each scored item is also sampled k times at the model\'s default temperature; the share of samples agreeing with the modal answer is the consistency sub-metric. Every item demands a 0-100 confidence line; calibration = 1 − ECE over 10 bins. Latency p50 and measured cost per 1k items are published but NEVER blended into quality — they live on a separate Pareto frontier.',
  },
];

const REFERENCES: Array<{ label: string; url: string }> = [
  { label: 'metabench — sparse IRT benchmark distillation (ICLR 2025)', url: 'https://openreview.net/forum?id=4T33izzFpK' },
  { label: 'tinyBenchmarks — 100-item IRT evaluation (ICML 2024)', url: 'https://arxiv.org/abs/2402.14992' },
  { label: 'ATLAS — Fisher-information adaptive testing for LLMs', url: 'https://arxiv.org/abs/2511.04689' },
  { label: 'GSM-Symbolic — template perturbation & memorization gaps (Apple)', url: 'https://arxiv.org/abs/2410.05229' },
  { label: 'GSM-IC — irrelevant-context distractors (ICML 2023)', url: 'https://arxiv.org/abs/2302.00093' },
  { label: 'Reasoning or Reciting? — counterfactual rule perturbation (NAACL 2024)', url: 'https://arxiv.org/abs/2307.02477' },
  { label: 'Faith and Fate — compositional depth limits (NeurIPS 2023)', url: 'https://arxiv.org/abs/2305.18654' },
  { label: 'ZebraLogic — constraint-satisfaction scaling curse (ICML 2025)', url: 'https://arxiv.org/abs/2502.01100' },
  { label: 'GSM-Infinite — computational-graph difficulty scaling (ICML 2025)', url: 'https://arxiv.org/abs/2502.05252' },
  { label: 'R-Horizon — chained-problem discrimination amplification', url: 'https://arxiv.org/abs/2510.08189' },
  { label: 'IFEval — verifiable instruction constraints', url: 'https://arxiv.org/abs/2311.07911' },
  { label: 'MMLU-Pro — distractor design & discrimination (NeurIPS 2024)', url: 'https://arxiv.org/abs/2406.01574' },
  { label: 'Humanity\'s Last Exam — frontier-failure item filtering (Nature 2025)', url: 'https://www.nature.com/articles/s41586-025-09962-4' },
  { label: 'NPHardEval — monthly regenerated complexity-class items', url: 'https://arxiv.org/abs/2312.14890' },
  { label: 'Can We Trust IRT for AI Evaluation? — small-N caveats', url: 'https://arxiv.org/html/2607.15190v1' },
  { label: 'τ-bench / τ²-bench — policy-constrained agents, pass^k (Sierra)', url: 'https://arxiv.org/pdf/2406.12045' },
  { label: 'BFCL v3 — AST/state-based tool-call grading (Berkeley)', url: 'https://gorilla.cs.berkeley.edu/blogs/13_bfcl_v3_multi_turn.html' },
  { label: 'GAIA — normalized exact-match agent grading', url: 'https://huggingface.co/papers/2311.12983' },
  { label: 'Establishing Best Practices for Rigorous Agentic Benchmarks (ABC)', url: 'https://arxiv.org/abs/2507.02825' },
  { label: 'SWE-Bench+ — leakage & weak-test inflation audit', url: 'https://arxiv.org/pdf/2410.06992' },
  { label: 'Terminal-Bench 2.0 — containerized terminal tasks', url: 'https://www.tbench.ai/' },
  { label: 'The Pilot/ForgeCode cheating audit (UPenn) — why we grade by simulation', url: 'https://debugml.github.io/cheating-agents/' },
  { label: 'CRUXEval — output-prediction evaluation paradigm', url: 'https://arxiv.org/abs/2401.03065' },
  { label: 'InterCode — execution-graded interactive shell tasks', url: 'https://arxiv.org/abs/2306.14898' },
  { label: 'Smoosh — mechanized POSIX shell semantics (why we whitelist)', url: 'https://arxiv.org/abs/1907.05308' },
  { label: 'LiveBench — procedural regeneration against contamination', url: 'https://arxiv.org/abs/2406.19314' },
  { label: 'Math-Verify — extraction leniency reshuffles leaderboards (HF)', url: 'https://huggingface.co/blog/math_verify_leaderboard' },
  { label: 'xFinder — regex extraction is only ~74% accurate', url: 'https://arxiv.org/abs/2405.11874' },
  { label: 'ReasonIF — reasoning models ignore format instructions', url: 'https://arxiv.org/abs/2510.15211' },
  { label: 'FormatSpread — 76-point spreads from formatting alone', url: 'https://arxiv.org/abs/2310.11324' },
  { label: 'Just Ask for Calibration — verbalized confidence (EMNLP 2023)', url: 'https://aclanthology.org/2023.emnlp-main.330/' },
  { label: 'WebArena-Verified — checker misalignment fixes', url: 'https://openreview.net/forum?id=94tlGxmqkN' },
];

export default function MethodologyPage() {
  const samples = TEMPLATES.map((t) => {
    const seed = `docs:${t.id}`;
    const item = t.render(createRng(seed), seed);
    return { template: t, item };
  });
  const byDomain = new Map<string, typeof samples>();
  for (const s of samples) {
    byDomain.set(s.item.domain, [...(byDomain.get(s.item.domain) ?? []), s]);
  }

  return (
    <div className="space-y-10">
      <div className="space-y-2">
        <h1 className="text-2xl font-bold text-zinc-900 sm:text-3xl">Methodology</h1>
        <p className="max-w-3xl text-sm leading-relaxed text-zinc-600">
          Index version {INDEX_VERSION}. Everything below is reproducible: template generators are
          public, every displayed number traces to an immutable score run with stored raw
          responses, and the machine-readable configuration is served at{' '}
          <a href="/api/v1/methodology" className="underline hover:text-zinc-900">
            /api/v1/methodology
          </a>
          . Scored item instantiations and answer keys never leave the server.
        </p>
      </div>

      <section className="grid gap-4 md:grid-cols-2">
        {PILLARS.map((p) => (
          <div key={p.title} className="rounded-xl border border-zinc-200 p-5">
            <h2 className="mb-2 font-semibold text-zinc-900">{p.title}</h2>
            <p className="text-sm leading-relaxed text-zinc-600">{p.body}</p>
          </div>
        ))}
      </section>

      <section className="space-y-4">
        <h2 className="text-xl font-semibold text-zinc-900">Scoring mathematics</h2>
        <div className="grid gap-4 md:grid-cols-2">
          <div className="rounded-xl border border-zinc-200 bg-white p-5">
            <h3 className="mb-2 text-sm font-semibold text-zinc-900">Item response model (2PL)</h3>
            <pre className="overflow-x-auto rounded-lg bg-zinc-100 p-3 text-xs text-zinc-700">{`P(correct | θ, a, b) = 1 / (1 + exp(−a·(θ − b)))

MAP objective (per domain, missing-aware):
  L = Σ observed [ x·log P + (1−x)·log(1−P) ]
      − θ²/2σθ²  − b²/2σb²  − (log a)²/2σloga²
  priors: θ ~ N(0,1) · b ~ N(0,1.5) · log a ~ N(0,0.5)

Optimizer: Adam, warm-started from accuracy logits,
θ recentered each step (scale pinned by priors),
max 500 iterations, tolerance 1e-6.`}</pre>
          </div>
          <div className="rounded-xl border border-zinc-200 bg-white p-5">
            <h3 className="mb-2 text-sm font-semibold text-zinc-900">Uncertainty & display scale</h3>
            <pre className="overflow-x-auto rounded-lg bg-zinc-100 p-3 text-xs text-zinc-700">{`SE(θ) = 1 / √( Σᵢ aᵢ²·P·(1−P) + 1/σθ² )
        (Fisher information + prior precision)

Domain composite (all terms in [0,1]):
  C = w_acc·σ(θ) + w_con·consistency
    + w_cal·calibration + w_res·resistance
  weights renormalize over measured terms

Domain score  = 1000·C, CI via delta method
                through the accuracy term
Global Index  = 1000·Σ_d w_d·C_d (equal w_d,
                renormalized over covered domains)
Global CI     = per-domain half-widths combined
                in quadrature (independent fits)`}</pre>
          </div>
          <div className="rounded-xl border border-zinc-200 bg-white p-5">
            <h3 className="mb-2 text-sm font-semibold text-zinc-900">Bradley-Terry (duel domains)</h3>
            <pre className="overflow-x-auto rounded-lg bg-zinc-100 p-3 text-xs text-zinc-700">{`P(i beats j) = pᵢ / (pᵢ + pⱼ)
ties → half-win to each side
MM update:  pᵢ ← Wᵢ / Σⱼ Nᵢⱼ/(pᵢ+pⱼ)
damping: +0.1 phantom win per pair (finite
strengths for isolated models)
SE from Fisher info: Iᵢᵢ = Σⱼ Nᵢⱼ·pᵢⱼ·(1−pᵢⱼ)
log-strengths standardized → same θ scale
as IRT domains before rescaling.`}</pre>
          </div>
          <div className="rounded-xl border border-zinc-200 bg-white p-5">
            <h3 className="mb-2 text-sm font-semibold text-zinc-900">Sub-metric definitions</h3>
            <pre className="overflow-x-auto rounded-lg bg-zinc-100 p-3 text-xs text-zinc-700">{`consistency = mean over items of
  (# samples agreeing with modal answer / k)
  k samples at the model's default temperature

calibration = 1 − ECE (10 equal-width bins over
  the model's self-reported 0-100 confidence)

contamination_delta = acc(anchors) − acc(fresh)
resistance = clamp01(1 − Δ/0.2)
  (a 20-point memorization gap ⇒ zero credit)

latency_p50, cost_per_1k_items: measured per
call, published, NEVER blended into quality.`}</pre>
          </div>
        </div>
      </section>

      <section className="space-y-3">
        <h2 className="text-xl font-semibold text-zinc-900">Item lifecycle & bank hygiene</h2>
        <ol className="list-decimal space-y-2 pl-5 text-sm leading-relaxed text-zinc-600">
          <li><span className="font-medium text-zinc-800">Generation</span> — every scored batch draws a fresh batch seed; each item derives from <code>seed:domain:index</code> through a deterministic PRNG (mulberry32 over an FNV-1a hash), so a batch is fully reproducible from its recorded seed yet unpredictable in advance.</li>
          <li><span className="font-medium text-zinc-800">Anchors</span> — positions below the anchor fraction (≤20%, capped in code) derive from a <em>fixed</em> seed stream (<code>anchor:domain:index</code>) — byte-identical across every run, forever. They exist only to measure longitudinal drift and the memorization gap.</li>
          <li><span className="font-medium text-zinc-800">Administration</span> — scored sample at temperature 0 with a 16,384-token completion budget; k additional samples at the model&apos;s default temperature for consistency. Full audit row per call: exact request params, raw response, tokens, measured latency, measured cost.</li>
          <li><span className="font-medium text-zinc-800">Grading</span> — mechanical only: normalized exact match, numeric tolerance (1e-6 relative), canonical-JSON equality for call sequences, exact multi-line match for terminal output, constraint-checker stacks for instruction following. No LLM ever grades a scored item.</li>
          <li><span className="font-medium text-zinc-800">Retirement</span> — after every refit, items with discrimination a &lt; {IRT_HYPERPARAMS.minDiscrimination} or |b| &gt; {IRT_HYPERPARAMS.maxAbsDifficultyLogits} logits are flagged <code>flagged_for_retirement</code>; flagged parameter regions are reviewed before the next run. Dead items never silently dilute the index.</li>
          <li><span className="font-medium text-zinc-800">Degradation</span> — a batch with &gt;2% failed calls is marked <code>degraded</code>, shown as such, and excluded from every fit until re-run. Truncated completions are unscored, never counted wrong.</li>
        </ol>
      </section>

      <section className="space-y-3">
        <h2 className="text-xl font-semibold text-zinc-900">Judge protocol (duel domains) — full detail</h2>
        <ul className="list-disc space-y-2 pl-5 text-sm leading-relaxed text-zinc-600">
          <li><span className="font-medium text-zinc-800">Panel</span>: three judge models from at least two different providers, configured explicitly (never hardcoded); a model never judges a duel it participates in — ineligible judges are excluded per-duel, and a duel with fewer than two eligible judges is skipped entirely.</li>
          <li><span className="font-medium text-zinc-800">Position swap</span>: judges alternate which response they see first; the swap is recorded on every verdict row and verdicts are un-swapped before aggregation, so position bias is both mitigated and measurable.</li>
          <li><span className="font-medium text-zinc-800">Verdicts</span>: judges output a single-token verdict (1 / 2 / TIE) at temperature 0; unparseable verdicts count conservatively as ties; empty or degenerate model responses can never win.</li>
          <li><span className="font-medium text-zinc-800">Anti-verbosity</span>: the judge prompt explicitly forbids rewarding length or style over substance; for SVG duels judges evaluate geometric fidelity, brand-color accuracy and vector cleanliness of the code.</li>
          <li><span className="font-medium text-zinc-800">Published diagnostics</span>: per run — judged-verdict count, pair count, and full-panel agreement rate, stored in the run&apos;s immutable <code>fit_diagnostics</code>.</li>
        </ul>
      </section>

      <section className="space-y-3">
        <h2 className="text-xl font-semibold text-zinc-900">Known limitations (read this too)</h2>
        <ul className="list-disc space-y-2 pl-5 text-sm leading-relaxed text-zinc-600">
          <li><span className="font-medium text-zinc-800">IRT with few examinees</span>: item-parameter recovery degrades below ~100 models. We deliberately rank a wide roster (135 models incl. small anchors) to stabilize the scale, and CIs are published precisely so you never over-read a 10-point gap.</li>
          <li><span className="font-medium text-zinc-800">Judge subjectivity</span>: duel domains inherit judge preferences even with a cross-provider panel, swaps and agreement reporting. Duel scores are best read comparatively, not absolutely.</li>
          <li><span className="font-medium text-zinc-800">Provider variance</span>: models are reached through OpenRouter routing; latency (and occasionally behavior) can vary by upstream provider. Latency is reported as measured p50 per run, not a hardware-normalized figure.</li>
          <li><span className="font-medium text-zinc-800">Template scope</span>: generators cover targeted, mechanically-gradeable slices of each capability — deliberately narrow and deep rather than broad and judge-dependent. The template list is public; judge for yourself what is and isn&apos;t measured.</li>
          <li><span className="font-medium text-zinc-800">Single-turn agentic</span>: v0.2 grades planned call sequences in one shot; multi-turn interactive episodes with injected tool errors are on the roadmap.</li>
        </ul>
      </section>

      <section className="space-y-3">
        <h2 className="text-xl font-semibold text-zinc-900">The pipeline, end to end</h2>
        <ol className="list-decimal space-y-2 pl-5 text-sm leading-relaxed text-zinc-600">
          <li>Model catalog and per-token pricing sync daily from the OpenRouter /models endpoint; the ranked roster is explicit configuration.</li>
          <li>Per model, per graded domain: a fresh seeded batch (30 items, ≤20% anchors) is generated; every item is asked at temperature 0 (scored) plus k consistency samples at default temperature; every call stores exact request params, raw response, tokens, measured latency and cost.</li>
          <li>Answers are extracted by the lenient cascade; truncated completions are unscored; batches with &gt;2% failed calls are flagged degraded and excluded from fits until re-run.</li>
          <li>After every model completes, the 2PL fit re-runs over all complete same-version batches; Bradley-Terry re-fits the duel domains; scores publish with 95% CIs and the leaderboard updates live.</li>
          <li>Items with a&lt;{IRT_HYPERPARAMS.minDiscrimination} or |b|&gt;{IRT_HYPERPARAMS.maxAbsDifficultyLogits} logits are auto-flagged for retirement; every methodology change bumps the semver index version with a public changelog.</li>
        </ol>
      </section>

      <section className="space-y-4">
        <h2 className="text-xl font-semibold text-zinc-900">Real sample questions, per domain</h2>
        <p className="text-sm text-zinc-500">
          Rendered live from the public template generators with documentation-only seeds (never
          used in scoring — scored runs draw fresh seeds every time). Reference answers shown where
          the item is mechanically graded.
        </p>
        {[...byDomain.entries()].map(([domain, list]) => (
          <div key={domain} className="space-y-2">
            <h3 className="text-sm font-semibold uppercase tracking-wide text-emerald-600">
              {domain.replaceAll('_', ' ')}
            </h3>
            {list.map(({ template, item }) => (
              <details key={template.id} className="rounded-xl border border-zinc-200">
                <summary className="cursor-pointer select-none px-4 py-3 text-sm text-zinc-800 hover:bg-zinc-50">
                  <code className="text-emerald-700">{template.id}</code>
                  <span className="ml-2 text-xs text-zinc-500">{template.description}</span>
                </summary>
                <div className="space-y-3 border-t border-zinc-200 p-4">
                  {item.svg && (
                    <div
                      className="max-w-md overflow-hidden rounded-lg border border-zinc-300"
                      dangerouslySetInnerHTML={{ __html: item.svg }}
                    />
                  )}
                  <pre className="max-h-96 overflow-auto whitespace-pre-wrap rounded-lg bg-zinc-100 p-3 text-xs leading-relaxed text-zinc-700">
                    {item.prompt}
                  </pre>
                  <p className="text-xs text-zinc-500">
                    grading: <code>{item.grading}</code>
                    {item.answerKey && item.grading !== 'constraints' && (
                      <>
                        {' · '}reference answer (docs seed only):{' '}
                        <code className="break-all text-zinc-700">
                          {item.answerKey.slice(0, 300)}
                          {item.answerKey.length > 300 ? '…' : ''}
                        </code>
                      </>
                    )}
                    {item.grading === 'constraints' && (
                      <>
                        {' · '}graded by mechanical constraint checkers:{' '}
                        <code className="break-all text-zinc-700">{item.answerKey}</code>
                      </>
                    )}
                  </p>
                </div>
              </details>
            ))}
          </div>
        ))}
        <div className="rounded-xl border border-zinc-200 p-4 text-sm text-zinc-600">
          <span className="font-semibold text-zinc-800">Duel domains</span> —{' '}
          {DUEL_DOMAINS.map((d) => d.replaceAll('_', ' ')).join(', ')}: open-ended generated tasks
          (constrained writing briefs; gray-zone assistance scenarios; &ldquo;reproduce the
          &lt;brand&gt; logo in raw SVG from memory&rdquo;) judged pairwise by a 3-judge
          cross-provider panel and aggregated with Bradley-Terry. Judge prompts are public in the
          repository; they contain no secret rubrics.
        </div>
      </section>

      <section className="space-y-3">
        <h2 className="text-xl font-semibold text-zinc-900">Weights (v{INDEX_VERSION}) — and why</h2>
        <p className="max-w-3xl text-sm leading-relaxed text-zinc-600">
          Domain weights are <span className="text-zinc-800">equal by design</span>: absent a
          task-utility function, any unequal weighting is an editorial value judgment; the
          maximum-entropy prior is the only non-arbitrary default, and per-domain scores are always
          published so you can re-weight for your own use case. Sub-metric weights favor the latent
          ability estimate (accuracy_irt {SUBMETRIC_WEIGHTS.accuracy_irt}) with robustness
          corrections for answer stability ({SUBMETRIC_WEIGHTS.consistency}) and template
          memorization ({SUBMETRIC_WEIGHTS.contamination_resistance}), and a smaller calibration
          term ({SUBMETRIC_WEIGHTS.calibration}) reflecting its higher measurement noise at current
          sample sizes. Latency and cost are excluded from quality entirely — Pareto frontier only.
        </p>
        <div className="grid gap-6 md:grid-cols-2">
          <div>
            <h3 className="mb-2 text-sm uppercase tracking-wide text-zinc-500">Domain weights (Global Index)</h3>
            <ul className="space-y-1 text-sm text-zinc-700">
              {DOMAINS.map((d) => (
                <li key={d} className="flex justify-between border-b border-zinc-100 py-1">
                  <span className="capitalize">{d.replaceAll('_', ' ')}</span>
                  <span className="tabular-nums">{DOMAIN_WEIGHTS[d].toFixed(4)}</span>
                </li>
              ))}
            </ul>
          </div>
          <div>
            <h3 className="mb-2 text-sm uppercase tracking-wide text-zinc-500">Sub-metric weights (domain composite)</h3>
            <ul className="space-y-1 text-sm text-zinc-700">
              {Object.entries(SUBMETRIC_WEIGHTS).map(([k, w]) => (
                <li key={k} className="flex justify-between border-b border-zinc-100 py-1">
                  <span>{k}</span>
                  <span className="tabular-nums">{w.toFixed(2)}</span>
                </li>
              ))}
            </ul>
            <p className="mt-3 text-xs text-zinc-500">
              Missing sub-metrics renormalize (a domain measured on accuracy alone neither gains
              nor loses). θ rescale: {THETA_SCALE.center} + {THETA_SCALE.slope}·θ; contamination
              floor Δ={CONTAMINATION_DELTA_FLOOR}.
            </p>
          </div>
        </div>
      </section>

      <section className="space-y-2">
        <h2 className="text-xl font-semibold text-zinc-900">IRT hyperparameters</h2>
        <pre className="overflow-x-auto rounded-xl border border-zinc-200 bg-zinc-50 p-4 text-xs text-zinc-700">
          {JSON.stringify(IRT_HYPERPARAMS, null, 2)}
        </pre>
      </section>

      <section className="space-y-3">
        <h2 className="text-xl font-semibold text-zinc-900">Versioning, reproducibility &amp; audit trail</h2>
        <ul className="list-disc space-y-2 pl-5 text-sm leading-relaxed text-zinc-600">
          <li>Every methodology change bumps the semver <code>INDEX_VERSION</code> with an entry in the public changelog; scores from different versions are never mixed in one fit.</li>
          <li>Every displayed number traces to an immutable <code>score_runs</code> row: item-set hash, model set, index version, fit diagnostics, timestamps. Historical runs are never edited.</li>
          <li>Every model call stores its exact request parameters, raw response, token usage, measured latency and measured cost — a ranking without stored raw responses would be invalid by our own rules.</li>
          <li>Weights, hyperparameters and domain definitions live in exactly one source file and are served machine-readable at <a className="underline hover:text-zinc-900" href="/api/v1/methodology">/api/v1/methodology</a>.</li>
          <li>Batches are reproducible from their recorded seeds; anchors are byte-stable across all runs.</li>
        </ul>
      </section>

      <section className="space-y-3">
        <h2 className="text-xl font-semibold text-zinc-900">References &amp; influences</h2>
        <p className="max-w-3xl text-sm text-zinc-600">
          The design draws on the following research — as inspiration and evidence, not as source
          material: every environment, template, item and grader here is original and home-made.
        </p>
        <ul className="grid gap-1.5 text-sm sm:grid-cols-2">
          {REFERENCES.map((r) => (
            <li key={r.url}>
              <a
                href={r.url}
                target="_blank"
                rel="noreferrer"
                className="text-zinc-600 underline decoration-zinc-300 hover:text-emerald-600"
              >
                {r.label}
              </a>
            </li>
          ))}
        </ul>
      </section>

      <section className="rounded-xl border border-zinc-200 bg-white p-5">
        <h2 className="mb-2 text-xl font-semibold text-zinc-900">Contact &amp; corrections</h2>
        <p className="text-sm leading-relaxed text-zinc-600">
          Methodology questions, disputes about a score, retirement appeals for an item family, or
          requests to add a model:{' '}
          <span className="font-medium text-zinc-800">Simon-Pierre Boucher</span> ·{' '}
          <a className="underline hover:text-emerald-600" href="mailto:contact@spboucher.ai">
            contact@spboucher.ai
          </a>
          . Substantiated corrections are applied in a new score run — never by editing history —
          and acknowledged in the changelog.
        </p>
      </section>
    </div>
  );
}
