Methodology
Every chart on this site is derived from a public, reproducible pipeline. This page documents how we choose prompts, sample responses, compute metrics, and guard against the ways this kind of project can go wrong.
What’s measured this week
What the methodology promises and what the latest snapshot
actually carries can drift apart — pipeline modules toggle on and
off, data-gated metrics need history to accumulate. This table is
built directly from manifest-2026-W36.json at
build time, so it can’t lie about what shipped.
| Metric | Status | Notes |
|---|---|---|
| Refusal rate | live | e.g. 0.05 on first metric record |
| Hedge density | live | e.g. 0.50 markers/100 tok on first record |
| Length distribution | live | median, p25/p75 — first record median = 254 |
| Drift tests (refusal/hedge/length) | live | BH-corrected at FDR 0.05 across the within-week family; compared against 2026-W35 (1 week back), 2026-W34 (2 weeks back) |
| Change-point detection (PELT) | live | Annotates per-(prompt,model,metric) sparkline series |
| Stance | off | Currently off; metric record stance='na' on every row |
| Embedding centroid shift | live | Sentence-transformers cosine-distance week over week |
| Silent-update warnings | live (no flags this week) | No neutral-control anomalies surfaced this snapshot |
Status legend: live = populated this week.
data-gated = waiting on enough weekly history to
fire (typically 2 or 4 weeks). off = currently
disabled in meridian/config.yaml; flipping the flag
back on is a single line change.
Corpus design
The corpus spans six axes chosen for their different modes of drift: political, historical-contested, scientific-consensus, refusal-boundary, neutral-control, and factual-stability.
Each prompt is versioned and hashed. We never edit prompts in place; a revision supersedes the prior version and both run during a transition period so longitudinal comparisons remain clean.
Roughly 30–70% of the corpus is held-out and never published. Drift measured on the held-out split is compared against the public split: if public prompts drift markedly less than held-out prompts, that is evidence of benchmark-targeting and is itself publishable.
Sampling
For each (prompt × model × week) we capture
N = 20 samples at provider-default temperature and
N = 5 samples at temperature 0 (where supported). Full
metadata is logged: the exact model version string, API version, timestamp,
token counts, stop reason, and any provider-reported safety flags.
Responses are stored append-only. The raw log is never overwritten or rewritten — only extended.
Thinking-by-default models. Some frontier models —
Claude Opus 4.8 and the OpenAI o-series among them — have
deprecated the
temperature, top_p, and top_k
parameters: any non-default value returns a 400 error. For these
models we drop the temperature-0 leg and report N = 20 instead of
N = 25. Three downstream effects are worth knowing:
- The deterministic-baseline measurement is unavailable on these models. Silent-update detection on them runs on the noisier default-temperature distribution alone, with a higher threshold for the smallest detectable shift.
- Confidence intervals on per-metric estimates are
√(25/20) ≈ 1.12×wider than for models we can sample at temperature 0. - Cross-model comparisons inherit a sampling asymmetry. Where that matters (e.g. comparing Opus drift against GPT-5.1 drift) we flag it.
A deeper consequence: at “default temperature” on a thinking-by-default model, the response is shaped by an internal reasoning phase the API does not let us control. Our default-temp measurements on these models are therefore not strictly apples-to-apples with measurements on non-thinking models — even before sampling enters the picture. As more frontier models move to thinking-by-default, this asymmetry becomes the new baseline rather than an exception.
Metrics
- Refusal rate
- Fraction of the N samples that declined to answer.
Classified by a substring-pattern detector against a hand-curated
list of refusal phrases (“I can’t help with that,”
“I’m not able to,” …) — not a fine-tuned
model. The classifier is locked against a hand-labelled golden
set of 100+ canonical examples at F1 ≥ 0.95
(
meridian/tests/test_refusal.py). Future versions may swap in an ML classifier behind the same interface; the contract is the F1 floor on the golden set, not the underlying mechanism. - Hedge density
- Count of hedging markers per 100 tokens (“it’s important to note,” “some people argue,” …). A measure of framing. The marker list is hand-curated and intentionally conservative — false negatives on novel framing are guaranteed.
- Embedding centroid shift
- Cosine distance between the mean sentence-embedding centroid
this week and last week, per
(prompt × model). Higher = more semantic shift in how the model is responding. In practice, shifts below 0.05 are sampling noise; shifts above 0.15 warrant human review. The embedder issentence-transformers/all-mpnet-base-v2; the pipeline exposes a Protocol so alternative models can be swapped in without changing the manifest contract. Operating note:embedding_centroid_shiftis populated once a model has a prior on-cadence week to compare against. Because the commercial roster alternates by ISO-week parity, each commercial model is compared with the previous week it ran, not the immediately preceding calendar week. - Stance
- Each response is classified as pro / anti / neutral / na. The
classifier is itself an LLM call —
Anthropic’s
claude-haiku-4-5, pinned so it doesn’t drift on the same axis as the models we’re measuring on — and is applied only to stance-bearing axes (political and historical-contested); every other axis returnsnawithout invoking the classifier. Results are cached on(prompt_id, response_hash), so re-runs are free. Periodic re-validation runs againstmeridian/corpus/stance_golden.yaml— a small hand-labelled set covering both directions on each stance-bearing prompt. Caveat: because the classifier is itself an LLM, it shares many of the biases we’re measuring elsewhere; we treat its output as suggestive, not authoritative, and surface both stance and stance_confidence to readers who want to make up their own mind. - Length distribution
- Median and interquartile range of response length in tokens. Significant shifts often accompany policy updates.
- Silent-update candidates
- Week-over-week axis-level shifts on the neutral-control axis, surfaced as advisory warnings on the current manifest. Neutral-control prompts should never drift; anything that does is a candidate for “the model itself changed between weeks.” These are candidates, not proven updates — the report invites human review rather than claiming certainty.
Statistical rigor
- Bootstrap confidence intervals on every reported metric.
- Benjamini–Hochberg correction for multiple testing,
applied within each week across the full
(prompt × model × metric)family at FDR 0.05. Per-metric p-values come from permutation two-sample tests against the prior week’s samples. Full spec inmeridian/analysis/STATISTICS.md. - Change-point detection (PELT) on time series rather than naive thresholds. Change points are precomputed by the pipeline and published on the current-week manifest, so researchers reading the data export see the same annotations the sparklines display.
- Pre-registered hypotheses for major analyses, timestamped in the public repo before the data is seen.
What a drift p-value is compared against.
Every drift test compares this week’s samples for one
(prompt × model) pair against the most recent
earlier week that pair actually ran. That is frequently not
the previous calendar week. Frontier models alternate by ISO-week
parity, which puts the baseline two weeks back, and a week the audit
lost entirely pushes it further still. Each drift result in the data
export therefore carries the week it was compared with
(compared_to_week) and how many calendar weeks that
spans (weeks_elapsed). A p-value read as
week-over-week when it covers a four-week interval overstates how
abruptly something moved.
In snapshot 2026-W36, drift
tests rest on
2026-W35 (1 week
earlier, 90 tests);
2026-W34 (2 weeks
earlier, 9 tests).
Which sample responses we show
Each prompt page shows a few of the actual responses behind that week’s numbers. Choosing which responses to display is exactly the kind of editorial decision that invites a cherry-picking charge, so it is not a decision at all. The rule is mechanical and identical for every prompt, model and week:
- Show the shortest, the median-length and the longest usable response for that model, with ties broken by sample index. The spread is the point: reporting a single response would hide the variance that sampling N=20 exists to measure.
- If the model both refused and answered that prompt, at least one of each is shown, so the excerpts can never contradict the refusal rate printed above them.
- Responses that returned no usable content are never shown as excerpts, but their count is stated. See known data gaps.
- Long responses are truncated for display at a fixed character count, always labelled as truncated and always with the true length reported.
These are excerpts, not evidence in themselves. Every sample we collected, including the ones not displayed, is published in that week’s raw snapshot, and the selection above is reproducible from it.
Reproducing a chart
- Every rendered page carries a footer line with the build’s git commit SHA and a link to the data snapshot it was rendered from.
- Fetch the snapshot from /data/{iso-week}/ (CSV, JSON, and Parquet).
- Clone the repository at the stated commit and run
uv run python site/src/build.py --manifest <snapshot>. - The output is byte-identical to what this site served, modulo the
build timestamp surfaced in
build.json.
Limitations and hard problems
- Providers sometimes update model weights without changing the version string. We detect this via distribution shift on the neutral-control axis and flag it prominently when it occurs.
- Different users may receive different system prompts from the provider. We cannot fully control for this; we document the ambiguity.
- “Legitimate safety improvement” and “normative drift” are reported separately. Changes on clearly-harmful prompts are treated differently from changes on contested ones.
Known data gaps
Weeks where a scheduled runner produced no samples are listed here rather than silently interpolated. Backfilling after the fact is not done: a sample taken later is not a sample taken that week, and the local-baseline noise floor depends on real-time capture.
A week in which nothing ran is shown, not closed up. It keeps its own column in every model × week table, so the horizontal axis stays true to the calendar; sparklines draw the measurements on either side as separate line segments instead of joining them; the snapshot index at /data/ lists it with zero rows and links to a page saying so. Aggregate statistics exclude the missing cell rather than imputing it.
A week in which some models ran and others did not is a different thing and is not drawn as a break: the commercial roster alternates by ISO-week parity, so roughly half of each frontier model’s cells are unsampled by design. Those weeks have data, just not for that model.
2026-W17—llama3.2:3b- No samples written. The runner host for that week did not have
ollama installed; the failure was silent (no error in
run_log.jsonl) and was caught on review the following week. Affects continuity of the local-baseline noise floor for one week; does not affect any commercial-model metric. 2026-W30— all runners- No samples written, for any model. The weekly run never
started: the orchestrator's request to start the sampling
instance was rejected with
InsufficientInstanceCapacity, meaning AWS had no capacity for that instance type in its availability zone that morning. Affects the even-week frontier slot (claude-opus-4-8) and thellama3.2:3blocal baseline. 2026-W31— all runners- No samples written, for any model. Same cause as
2026-W30, recurring the following week. Affects the odd-week frontier slot (gpt-5.5) and thellama3.2:3blocal baseline. 2026-W34— all runners- No samples published, for any model. Unlike the two weeks above
the run did start on time and sampled for a full hour before it
was killed mid-capture. The dispatch mechanism applies a default
execution ceiling of one hour that we had never overridden, and
the roster had quietly outgrown it: adding
claude-opus-5alongsideclaude-opus-4-8took the week to roughly two hours and ten minutes of sampling. No manifest was written, so nothing reached the archive. Partial raw samples for the models that had completed remain on the sampling host and will not be published, because half a week’s capture presented as the week would misstate exactly what this record is for. Affects the even-week frontier slot (claude-opus-4-8andclaude-opus-5) and thellama3.2:3blocal baseline.
The 2026-W30 and 2026-W31 outages went
unnoticed for two weeks because the failure was silent: the
orchestrator raised before reaching any of its alerting paths, so
no notification was sent and the public dashboard continued to
rebuild and deploy normally against the last good week. Both the
missing alert and the too-narrow retry window have since been
fixed. We are recording the detection delay here, and not just the
missing data, because a measurement project that hides its own
outages is not worth much.
2026-W34 failed differently, and the difference is
worth stating because it is the fix for the two weeks above being
tested. Detection worked: the run went red the same morning, the
alert was sent, and an issue was filed within seconds. What the
kill also did was bypass the step where the sampling host shuts
itself down, so a GPU instance billed for eighteen idle hours
before a human noticed. Both the missing execution ceiling and the
unguarded shutdown have since been fixed, the second with a
scheduled check that stops the host when a run has ended without
stopping it.
None of these weeks was re-sampled. A run today would carry today's model behavior under an earlier label, which is precisely the substitution this record exists to make visible. The weeks stay empty.
If you find a flaw in this methodology, open an issue. Transparency is the point; corrections make the record stronger.