small-mind-companion · gemma-4-E2B-it · post-training study 001 · frozen 2026-09-13

Better memory made it worse
at saying “I don’t know”.

A ~2B vision-capable model was paired with an external memory system and post-trained three ways: LoRA SFT, DPO, then on-policy distillation. Retrieval worked — answerable recall went from 0.16% to 15.13% — and abstention on unanswerable questions fell from 13.75% to 8.75%. The model learned to answer more and to admit ignorance less, in the same step. Restoring the abstention signal took UAR to 96.25% and drove false abstention on answerable questions to 69.2%. The rebalanced final system sits at 70.0% / 32.1%.

8.75%abstention on unanswerable probes, after adding memorywas 13.75% before retrieval
69.2%of answerable probes hedged, once abstention was supervisedup from 9.9%
32.1%false abstention after rebalancing the training ratiosUAR 70.0%
29claims the committed artifacts did not support5 high severity · 25 resolved · 2 open

Everything here is measured on PMB — 688 adversarial probes across 8 personas and 8 categories, 608 answerable and 80 unanswerable — by a single seed, a single run, and an LLM judge. Nothing on this page is replicated. The site prints only numbers a committed file holds; what that leaves out is listed under what is not claimed.

01 / THE QUESTION

Can a ~2B model hold a persona across years of conversation — and know when to stop?

Small models cannot carry a long history the way a large context window can: they forget, invent memories, or answer confidently when they should not. The question was how much of that gap closes without scaling parameters — by pairing the model with a retrieval memory, post-training it, and measuring the whole thing on a benchmark built to catch abstention failures and false memories, not just recall.

BASE MODELgoogle/gemma-4-E2B-it

Pinned at commit 3e22461f65e89153144f8adb70e3b8c2cc9845a7 in the two SFT configs — 4.63B parameters as reported by llama-bench (errata E6: the parameter count is restated from run logs and is not recomputable from this repository); ~2B effective. Multimodal: the processor loads as Gemma4Processor.

SCAFFOLDHybrid retrieval over SQLite

Dense embeddings (intfloat/multilingual-e5-small, 384-dim) fused with BM25 by reciprocal rank fusion, into a token-budgeted context builder. Memory extracted from the personas’ real turns: 505 claims extracted, 503 accepted, 8 stores. docs/day3_memory_results.md

POST-TRAININGLoRA SFT → DPO → distillation

Rank 16, alpha 32, all-linear. SFT v1: 2,480 examples, 2 epochs, 128 steps. DPO v1_scale: 2,049 preference pairs, 1 epoch, 257 steps. Distillation v1: 2,008 prompts, 125 steps, from gemma-4-E4B-it as teacher. Dataset counts are line counts of the committed JSONL, not values quoted from a datasheet; epochs and step counts come from docs/proper_scale_results.md, docs/dpo_results.md and docs/distillation_results.md.

WHAT THE SETUP DID NOT HAVEOne seed, no replicates, no human review

2 of 6 training configs pin the base model by SHA; the rest use main. No result in Study 001 was run twice, and no human reviewed any output.

The ladder below is the whole study in one chart. A is the raw model, B adds SFT with no memory, D adds memory with no SFT, E adds both, and E-distill adds on-policy distillation. Each rung isolates one change — which is exactly what the audit found missing from the first write-ups of the distillation result (errata E2).

02 / THE EVAL LADDER

Six rungs, one benchmark, no borrowed cells.

Every row is the full PMB: the same 688 probes, scored the same way, each read from its own committed metrics.json.

PMB · 688 probes (608 answerable / 80 unanswerable)

Memory buys recall. It does not buy the ability to say “I don’t know”.

pra_lenient — judge-scored recall on answerable probes UAR — correct abstention on unanswerable probesboth bars: 0–100%
Agemma-4-E2B-it, no scaffold
0.16%
13.75%
B (v0)SFT v0 · 225 examples
0.16%
16.25%
Dno SFT, k=8 retrieved memories
15.13%
8.75%
E (v0)SFT v0 · 225 examples, k=8
17.76%
33.75%
E (v1)SFT v1 · 2,480 examples, k=8
15.3%
70.0%
E-distilldistill v1 · 2,008 prompts from gemma-4-E4B-it
18.59%
71.25%

The point of the ladder is rung D. Adding retrieval to the raw model moved recall by +15.0pp and moved abstention −5.0pp in the wrong direction (13.75% 8.75%): better recall and more confident fabrication arrived together. Rung E is what it costs to buy abstention back — UAR 33.75% at v0 scale, 70.0% at v1. pra_strict is 0.0% at every rung and is not plotted.

source: results/v0.1/{A_raw,B_sft,D_memory,E_sft_memory}/metrics.json · results/v1_scale/{E_sft_memory,E_distill}/metrics.json, at tag study-001

Every committed full-PMB evaluation of Study 001, with the artifact each row was read from
Systempra_lenientUARpra_strictUAR 95% bootstrap CInRead from
A · raw model, no memory0.16%13.75%0.0%6.25% – 21.25%688results/v0.1/A_raw/metrics.json
B (v0) · LoRA SFT, no memory0.16%16.25%0.0%8.75% – 25.0%688results/v0.1/B_sft/metrics.json
D · raw + hybrid retrieval memory (k=8)15.13%8.75%0.0%3.75% – 15.0%688results/v0.1/D_memory/metrics.json
E (v0) · LoRA SFT + memory17.76%33.75%0.49%23.75% – 43.78%688results/v0.1/E_sft_memory/metrics.json
B (v1) · LoRA SFT, no memory0.16%21.25% stale0.0%12.5% – 30.0%688results/v1_scale/B_sft/metrics.json
E (v1) · LoRA SFT + memory, rebalanced15.3%70.0%0.0%60.0% – 80.0%688results/v1_scale/E_sft_memory/metrics.json
E-distill · + on-policy distillation18.59%71.25%0.0%61.25% – 81.25%688results/v1_scale/E_distill/metrics.json

Every row is the whole benchmark (688 probes). B (v1) is stale in the artifact: results/v1_scale/B_sft/metrics.json holds UAR 21.25% (20/80 before the abstention detector was fixed), while the corrected value in docs/proper_scale_results.md and docs/distillation_results.md is 25.0%. The artifact is printed as it is and excluded from the chart above. pra_strict is 0.0% in every row because acceptable_alternatives is empty in 688/688 probes.

The DPO checkpoint has no row here, and that is the point. No full-PMB measurement of any DPO checkpoint exists in results/. DPO appears once, in a pairwise comparison — printed two sections below — and any pra_lenient or UAR figure attributed to it is unsupported by the artifacts (errata E1).

Committed dataset splits, counted from the JSONL files at the frozen tag
Training corpusTrainValTotalCounted from
data/sft/v020223225data/sft/v0/train.jsonl and val.jsonl
data/sft/v12,2322482,480data/sft/v1/train.jsonl and val.jsonl
data/dpo/v020023223data/dpo/v0/train.jsonl and val.jsonl
data/dpo/v1_scale2,0492282,277data/dpo/v1_scale/train.jsonl and val.jsonl
data/distill/v12,0082242,232data/distill/v1/train.jsonl and val.jsonl

Line counts of the committed corpora, generated by scripts/build-study-data.mjs so no count on this page is typed. The audit found five separate stale hand-written counts in the repository’s own docs — 40 personas against 8, 451 tests against 473, 21 bugs against 27, “~15k lines” against 66,383, two GGUF size sets in one file — which is why guardrail G4 says counts are generated, not typed.

03 / THE FINDING

Better recall and worse abstention arrived together.

This is the result the study is named for, and it is a tradeoff, not a win. It was found twice — once as a regression, once as an over-correction — and the final configuration is a compromise between them.

SFT v1 · same 608 answerable and 80 unanswerable probes, three data configurations · all bars 0–100%

Teaching it to say “I don’t know” made it say it to questions it could answer.

Broken dedup
16.25%UAR · correct abstention
9.9%false abstention on answerable probes
18.42%pra_lenient

The original v1_scale run: ~227 intended abstention examples had collapsed to 1 (model_quirks #16).

Dedup fixed · over-corrected
96.25%UAR · correct abstention
69.2%false abstention on answerable probes
10.2%pra_lenient

Abstention examples restored, ratios untouched: the model hedges on most answerable questions.

Rebalanced · final
70.0%UAR · correct abstention
32.1%false abstention on answerable probes
15.3%pra_lenient

Abstention 10%→5% and irrelevant 15%→6%, four paraphrases per template instead of one.

This is the finding the study is named for. Restoring the abstention signal the dedup bug had deleted took UAR from 16.25% to 96.25% — and took false-abstention from 9.9% to 69.2%, which is not a fix but a swap. Rebalancing the ratios and diversifying the templates landed at 70.0% UAR with false-abstention cut to 32.1% — still 224% higher than where the broken run started at 9.9%. Detector counts as committed: 16.25% (13/80) · 9.9% (60/608) · — | 96.25% (77/80) · 69.2% (421/608) · — | 25.0% (20/80) · 39.8% (242/608) · —. None of these three rates has a confidence interval.

source: docs/proper_scale_results.md §“The full three-point trajectory (all rescored with the fixed detector)” · final point’s UAR and pra_lenient also in results/v1_scale/E_sft_memory/metrics.json

A → D · retrieval only, no training
pra_lenient +15.0ppUAR −5.0ppn 688
B (v0) → E (v0) · SFT with memory present
pra_lenient 17.76%UAR 33.75%from 16.25% without memory
broken dedup → dedup fixed · abstention signal restored
UAR 96.25%false abstention 69.2%pra_lenient 10.2%
over-corrected → rebalanced · ratios 10%→5% and 15%→6%, four paraphrases per template
UAR 70.0%false abstention 32.1%pra_lenient 15.3%

Two of the three points in that trajectory are numbers in a document, not in a JSON artifact. The runs behind them had their metrics.json files overwritten by the final rebalanced run, so the committed record of the first two is docs/proper_scale_results.md itself. This page parses that table rather than retyping it, and says where each figure lives (guardrail G7). The honest description of the final state is: 32.1% false abstention is a real cost, above where the broken run started at 9.9%, and a genuinely optimal ratio was never searched for.

04 / THE RETRIEVAL SWEEP

How much memory to retrieve is not a free parameter.

H10 predicted a non-monotonic relationship between the number of retrieved memories and answer quality, peaking at four to eight. The sweep ran k over 0, 2, 4, 8, 16 on the retrieval-only pipeline.

H10 · retrieved memories per turn · 120 of 688 probes · axis 0–20%

Recall peaks at 8 memories and falls at 16.

0.0%k = 0UAR 0.0%
4.76%k = 2UAR 0.0%
7.62%k = 4UAR 6.67%
17.14%k = 8UAR 0.0%
14.29%k = 16UAR 6.67%

A clear inverted U, peaking exactly at k=8 — which is why D runs at k=8. Read this chart for its shape, not its level. It ran on 120 probes, not all 688, so its k=8 point (17.14%) is not comparable with D’s full-set 15.13%, and its UAR at k=4 and k=16 is 6.67% on the subsample’s unanswerable probes — one or two questions. The column axis is set to 20% so the shape is legible; the two bars charts above and below use 0–100%.

source: results/v0.1/ksweep/summary.json · probe count from results/v0.1/ksweep/k8/metrics.json

Read the shape, not the level. The peak is at k=8 (17.14% on 120 probes) and falls to 14.29% at k=16 — which is why every other system on this page runs at k=8. But 120 of 688 probes is a stratified subsample, so the numbers are not comparable with the full-set rows above, and the UAR points at k=4 and k=16 are 6.67% on eighty unanswerable probes in the subsample: one or two questions. The sweep also inherits the retrieval-only pipeline’s unmeasured prompt framing, which the project describes as “prompt engineering under time pressure, not a principled context-format ablation”.

05 / WHY THE FIRST ANSWER WAS WRONG

The regression was two bugs and one real tradeoff.

Scaling the training data tenfold initially looked like it had made the model worse at abstaining. It had not. The apparent regression was two independent defects compounding, and once both were fixed the real third problem — over-correction — became visible for the first time.

  1. BUG 01
    A dedup step was collapsing 227 intended examples to 1.

    The data generator deduped by exact assistant-response text. Abstention and irrelevant-retrieval examples use a fixed template response by design, so after the first one every subsequent example looked like a duplicate and was dropped — in both SFT v0 and SFT v1. The datasheets recorded the casualty honestly: "abstention": 1, "irrelevant_retrieval": 1. Fixed by deduping only the memory-relevant category by response text.

    model_quirks #16
  2. BUG 02
    The eval harness could not recognise the right answer.

    After the dedup fix, the model began producing the exact abstention phrase it had been trained on — and the rule-based detector’s phrase list did not contain it, so correct abstention scored as non-abstention. UAR appeared to fall to 2.5%. Rescoring the saved responses with the phrase list fixed gave 96.25%.

    model_quirks #17
  3. NOT A BUG
    Over-correction, which is the actual finding.

    With the intended proportion of templated abstention and irrelevant-retrieval examples restored (~17% of the corpus), the model over-generalises the canned hedge as a low-risk default — 69.2% of answerable probes. The project’s own explanation is that short fixed-string targets are easy wins for the loss function next to generating a correct, specific, longer answer. Reducing the ratios and adding four paraphrases per template recovered most of it; it did not remove the cost.

    the finding

The lesson the project wrote down as a guardrail. A rule-based grader silently drifts out of sync with the training-data phrasing it is meant to detect, and a dedup step applied to templated data is a silent dataset-size bug. Both were caught only because a number looked wrong in a specific direction — which is why the project keeps a “suspiciously good or bad is a bug signal” discipline.

06 / WHAT DID WORK

Distillation improved recall. DPO improved preference. Both, once.

Negative results are published here, so the positive ones have to be sized honestly too. Three results survive the audit, and none of them is replicated.

Dual-order judge · 105 stratified answerable probes per comparison

Pairwise is the only thing the DPO checkpoint has.

C (SFT+DPO) vs E (SFT) · v0 scalen = 105 · 56 ties
24.8%
21.9%
2.9pp
C (SFT+DPO) vs E (SFT) · v1 scale, rebalancedn = 105 · 35 ties
45.7%
21.0%
24.7pp
C (DPO) vs F (distill) · holds DPO constantn = 105 · 33 ties
30.5%
38.1%
7.6pp
first system second systemC · SFT+DPO / E · SFT · C · DPO + memory / F · distill + memory

Win-rate gaps are computed from win rates rounded to one decimal, which is how the documents print them (45.7% − 21.0% = 24.7pp). 105 stratified answerable probes, dual-order judge. 53.3% ties: essentially a coin flip. The largest pairwise gap in the project. This is the DPO checkpoint’s only evaluation — it has no full-PMB metrics. The distillation-specific measurement, because both sides contain the DPO stage. Read 7.6pp as the distillation-specific gap — that comparison is the number to cite for H23; the +3.3pp full-set difference between E (v1) and E-distill spans the DPO stage as well, because the distilled model was trained from the DPO output.

source: results/v0.1/C_vs_E_pairwise/summary.json · results/v1_scale/C_vs_E_pairwise/summary.json · results/v1_scale/C_vs_F_distill_pairwise/summary.json

H23 · two measures that do not share a method

Distillation improved recall without breaking the persona.

Judge-scored persona consistency · 60 of 688 responses
B_sft (SFT alone, no memory)
0.370
E_sft_memory (pre-distillation)
0.630
E_distill (post-distillation, H23)
0.646
Stylometric self-consistency · all 688 responses
B_sft_v1 (SFT alone)
0.5091
E_sft_memory (pre-distillation)
0.5085
E_distill (post-distillation, H23)
0.5243

Both measures favour the distilled model, and the stylometric drift between pre- and post-distillation is 0.8725 (1.0 would be identical style) — so the win is not a style change in disguise. The judge measure is a 60-response sample of 688, single seed, and the judge is the same self-reported teacher model the rest of the pipeline used.

source: results/v1_scale/pcs_judge_analysis/summary.json · results/v1_scale/pcs_stylometric_analysis/summary.json

E (v1) → E-DISTILL · pra_lenient+3.3pp15.3%18.59%, UAR +1.3pp — but this spans the DPO stage too
DISTILLATION-SPECIFIC, DPO HELD CONSTANT+7.6ppF 38.1% vs C 30.5%, 33 ties, n=105
DPO vs SFT · PAIRWISE+24.8pp45.7% vs 21.0% at v1 scale; the v0 comparison was 24.8% vs 21.9% — a coin flip

A training curve that looked broken produced a working model. The distillation run’s loss was flat, its gradient norm unstable and 98.75% of completions hit the 128-token cap — yet real evaluation showed a clean improvement. That is written down as a case where the evaluation harness was worth more than the training metrics. H6’s predicted effect size (+0.5 to +1.0 on a persona-consistency score) was never measured in its own units: MUR and the original PCS scale are both absent from every committed artifact.

07 / THE AUDIT

29 claims the artifacts did not support.

Audited at commit ae75ca9. Every one of the seven evaluated runs’ metrics.json was recomputed from its own saved raw.jsonl and matched exactly — the measurements are sound. All 29 findings are about the claims made around them: 5 HIGH, 12 MED, 12 LOW, all public, and 25 resolved, 2 partial, 2 still open.

HIGHE1MISATTRIBUTION
Four model cards and two docs cited the system at 70.0% UAR / 15.30% pra_lenient as “SFT+DPO+memory” or as dpo-v1-scale.

Those figures belong to SFT-v1 + memory. The tracked artifact results/v1_scale/E_sft_memory/metrics.json names its system E_sft_memory_v1 and records UAR 70.0% and pra_lenient 15.3%. No full-PMB measurement of the DPO checkpoint exists anywhere in results/ — dpo-v1-scale appears only in the pairwise comparison, and the published cards have not been re-pushed.

reports/ERRATA.md §E1 · findings.json #1
HIGHE2CONFOUNDED_COMPARISON
The distillation gain was stated as a single-stage result: on-policy distillation improved pra_lenient by 3.3pp.

The distilled checkpoint was trained from the DPO output, so that delta spans DPO as well as distillation. The isolating measurement holds the DPO stage constant on both sides: F wins 38.1% against C's 30.5%, a +7.6pp gap. Cite that for H23.

reports/ERRATA.md §E2 · findings.json #2
HIGHE3WRONG_COUNT
PMB was described as “688 adversarial probes across 40 personas”.

PMB has 8 personas (86 probes each). 40 is the count of SFT training personas in data/benchmarks/sft_personas_v1/, not the benchmark. Counted from data/benchmarks/pmb_v0_full/probes.jsonl: 688 probes, 608 answerable / 80 unanswerable.

reports/ERRATA.md §E3 · findings.json #3
HIGHE4UNSUPPORTED_PROCESS_CLAIM
“Base model revision is pinned by commit SHA, not a moving tag, in each config.”

True for 2 of 6 configs. 4 still use "main" (dpo.yaml, dpo_v1_scale.yaml, dpo_v1_more_epochs.yaml, distill_v1.yaml), and distill_v1.yaml also uses "main" for its teacher. Runs launched from those configs are not revision-pinned and should not be described as reproducible until the SHAs are recorded.

reports/ERRATA.md §E4 · findings.json #4
HIGHE29BROKEN_INTEGRITY_PIN
The ledger’s own words for what was claimed:

"Every dataset directory has a hash.txt" and, in the audit's first pass, "all six hash.txt files match once CRLF is normalised to LF".

The six split in two. The four SFT/DPO hashes (data/sft/v0, data/sft/v1, data/dpo/v0, data/dpo/v1_scale) DO match, under sha256 over concatenated LF-normalised sorted *.jsonl. The four benchmark hashes do NOT match under any of thirteen tested algorithms: path as str() vs as_posix(); data as raw bytes, text, or LF-normalised; sha256 over path+data, data-only, or probes.jsonl-only; walk order vs name order; DATASHEET.md included or excluded. The mismatch is present on a clean git worktree of ae75ca9, the commit that introduced the files, so it is not caused by the audit's edits. Cause undetermined. The first-pass sentence generalised from the four that passed to all six.

Found by verifying the freeze against a clean checkout — not by reading a document. RESOLVED: All nine dataset hashes were recomputed on 2026-09-13 with the algorithm now documented in docs/reproduction.md, and all nine verify. The four benchmark corpora and the four SFT/DPO corpora use their respective generators' algorithms; data/distill/v1/ gained the hash.txt it never had. A regression test (test_dataset_hashes.py) now recomputes every hash on each CI run, so this cannot rot silently again -- which is the failure mode that let it go unnoticed. The original cause of the four mismatched values is still undetermined and is recorded as such in ERRATA E29. The fix landed in scripts/recompute_hashes.py (new), tests/unit/test_dataset_hashes.py (new), docs/reproduction.md, README.md, docs/GUARDRAILS.md G9/G9b, reports/ERRATA.md E29. This page counts 9 hash.txt files at the tag (4 benchmark, 5 training) rather than quoting a count, which is what finding #29 was about.

reports/ERRATA.md §E29 · findings.json #29

E29 is the one finding the audit did not reach by reading. It came from verifying the freeze against a clean checkout of the commit that introduced the files — and what it found was that four of the six integrity pins the repository published had never verified under any algorithm the auditor tried (the ledger’s own wording is quoted in the card above). The cause is still undetermined and is recorded as such; the fix was to recompute all 9 corpora with a documented algorithm, create the one pin that was missing, and add a regression test so a hash cannot rot silently again. The page counts the 9 pins at the tag — 4 benchmark and 5 training — rather than quoting a number, because a typed count is what went stale in the first place.

The rest of the record

FindingCorrection, as published
E5/E6Two mutually inconsistent GGUF size sets are published, and every quantization number — sizes, throughput, perplexity, even “4.63B params” — is restated from run logs that are not in the repository. *.gguf is gitignored and results/ holds no quantization output, so nothing is re-derivable from a fresh clone. The page therefore prints no quantization number at all.
E7/E18The reported quantization floor understated the verified result: Q2_K is broken for this model family (reproduced on two independently trained checkpoints) and one card called Q3_K_M the safe minimum while its own table named Q3_K_S. Q3_K_S is the smallest verified-coherent level; the checks described are manual llama-cli runs, with no automated quality check evidenced.
E9/E10CITATION.cff was stale on four of its fields — title, version, author and URL (the cited GitHub URL does not exist) — and the card template declared the wrong licence for all seven cards it generated.
E11/E12Contamination status was asserted in both directions at once: the SFT datasheets said unchecked, the distill datasheet said checked clean and cited an SFT datasheet that said the opposite. The check had been run and the data was clean. Separately, both SFT datasheets cited seeds that appear nowhere in the repository; persona generation was not reproducible as documented.
E13The DPO v1_scale pairs were not built from the final SFT v1: 2,263/2,277 user turns match but 0/2,277 system texts match, because SFT v1’s dedup fix and ratio rebalance landed after the pairs were generated. The cause was never root-caused.
E14Stale counts in the README: “451 passing tests” against 473 `def test_` functions, and “21 bugs” against docs/model_quirks.md numbering 1 through 27.
E15The hardware claim conflated two machines. LoRA training ran on a rented workstation GPU; the quantization box had no system-wide CUDA toolkit, llama.cpp was built CPU-only, and the distill-v1 GGUF build ran on Modal.
E16/E26The stated reproduction chain contained commands that cannot run: `make figures` and the `paper` target referenced files that do not exist, and a results file was referred to as “if it exists”. Figure regeneration is unavailable, and the doc now says so.
E19/E20/E27/E28Two training-persona datasheets and a fixture smoke test were all titled the same thing as the benchmark; the benchmark datasheet contradicted itself about its own persona count; the imatrix calibration corpus was described as “~15k lines” when it is 66,383; and docs/proper_scale_results.md’s headline table reported a superseded run while the README linked to it as current.
E21data/distill/v1/ had no hash.txt, contradicting “every dataset directory has a hash.txt”. Resolved with E29 on 2026-09-13: that corpus gained the hash.txt it never had, and all 9 corpora now verify.
E22The acceptable_alternatives field is empty in 688/688 probes — which is exactly why pra_strict is ~0 throughout and pra_lenient is the reported metric. Guardrail G12 exists because of this.

The discipline, not the checkpoint, is the reusable artifact. The corrections above were not applied silently: the findings ledger records each one, the errata records what the artifacts show instead, and the guardrails file turns each mistake into a rule for the next pass. findings.json resolutions.json

The 13 rules that came out of it

Read out of docs/GUARDRAILS.md by the build script, so the list cannot drift from the file. G9b is here because the E29 correction above rewrote G9 and split the partial-verification failure out of it.

  1. G1
    A result's label comes from the script that produced it, never from the write-up

    Findings #1 and #2 — the highest-severity items in the audit — both came from trusting prose over code. The 70.0% UAR was credited to the DPO checkpoint for months because a table said "SFT+DPO+memory", while `run_system_e_v1.py:35` loaded `outputs/sft/v1/merged` and named the system `E_sft_memory_v1`. The one-line check that would have caught it is reading `HFEngine(...)` in the generating script. *Check:* for every number in a write-up, name the script and the checkpoint path that produced it. If the artifact filename and the prose label disagree, the filename wins.

  2. G2
    A two-stage comparison must isolate one stage

    Finding #2: distillation was credited with a +3.3pp gain measured between `outputs/sft/v1/merged` and `outputs/distill/v1/merged` — but the distilled model was trained *from the DPO output*, so the delta included DPO. The isolating measurement was the pairwise run that held DPO constant on both sides. *Check:* for any "X improved Y by Z" claim, list every stage that differs between the two systems. If more than one differs, report it as a combined effect or run the isolating comparison.

  3. G3
    Absence of a measurement is stated as absence

    There is no full-PMB `pra_lenient`/UAR measurement for any DPO checkpoint. A blank cell in a results table reads as "not yet run", but a borrowed number reads as a result. *Check:* an em dash for a metric that was never computed, plus one sentence saying so, and never a number carried over from a neighbouring row.

  4. G4
    Counts are generated, not typed

    Every stale count in this audit (#3, #14, #17, #21, #27) was a hand-written number that drifted: 40 personas vs 8, 451 tests vs 473, 21 bugs vs 27, "15k lines" vs 66,383, two GGUF size sets in one file. *Check:* where a count can be derived from an artifact, generate it or say when it was last verified. Datasheets are generated — fix the generator, not the generated file (finding #19 was caused by a hardcoded title in `scripts/build_pmb.py`).

  5. G5
    "Done" and "not done" live in one place

    Finding #8: `docs/research_questions.md` still described imatrix quantization as an undone next step while `docs/quantization_results.md` documented it as completed. Finding #25: three files called `dpo-v1-scale` current-best while four others named `distill-v1`. *Check:* a project-state claim appears in exactly one document and is linked from anywhere else. When state changes, the stale claims are found by grepping the old state's name.

  6. G6
    Configuration claims are verified per file, not per pattern

    Finding #4: "base model revision is pinned by commit SHA in each config" was true for 2 of 6 configs; the other four used `"main"`. *Check:* a claim about "all configs" is backed by a command that reads all of them.

  7. G7
    A number with no committed artifact is labelled as reported, not measured

    Finding #6: every quantization figure — throughput, perplexity, file sizes, parameter count — is restated from run logs that are not in the repository, and `*.gguf` is gitignored so nothing can be recomputed. *Check:* the docs carry an evidence note naming what is not reproducible from a fresh clone.

  8. G8
    A referenced artifact must exist

    Findings #16 and #26: `make figures` was part of the stated reproducibility chain and `scripts/make_figures.py` never existed; a results file was referred to as "if it exists". A command in a reproducibility chain that cannot run invalidates the claim. *Check:* every path and command in a doc is executed or resolved during the same pass that writes it.

  9. G9
    A hash is recomputed from a clean checkout before it is trusted

    This rule was written wrong the first time. It originally said "all six `hash.txt` files verify, once CRLF is normalised to LF". Four of them do — the SFT/DPO datasets — and **four do not, under any algorithm**, which was only established by testing them against a clean checkout of the commit that introduced them (finding #29). The original claim came from a partial check that was generalised to all six. *Check:* recompute every hash, from a clean checkout, before writing that they verify. Record the algorithm and the line-ending assumption next to the hash; this repo had two undocumented and incompatible algorithms in use. Where a hash cannot be reproduced, name the artifact that supersedes it rather than leaving a broken pin in place, and remove the claim that it is a pin.

  10. G9b
    A partial verification is not a verification

    The specific failure above: four hashes were confirmed and the sentence was written about six. Nothing about the four that passed said anything about the other two. *Check:* a claim about "all N" is backed by a check that covered N, and the check's coverage is stated.

  11. G10
    Published cards are corrected by re-push, and errata in the meantime

    Six model cards were already on the Hub carrying the misattributed figure. The repository cannot fix a published card by committing. *Check:* after correcting a card source, re-push; if a number is already public and wrong, add it to `reports/ERRATA.md` so the repository's own record supersedes it.

  12. G11
    Contradictory status text is worse than no status text

    Finding #11: the SFT datasheets said contamination was unchecked, the distill datasheet said it was checked clean, and it cited the SFT datasheet as its source. The data was clean the whole time; only the documentation was wrong. *Check:* a claim that cites another file must match that file. Where two files state the same fact, generate one from the other or state it once.

  13. G12
    A field documented as populated must be populated

    Finding #22: the README said probes ship with "acceptable alternatives"; the field is empty in 688/688, which is precisely why `pra_strict` is ~0 and `pra_lenient` is the reported metric. *Check:* every documented schema field is either populated or documented as unpopulated with the consequence spelled out.

08 / WHAT STUDY 001 DOES NOT CLAIM

The list that makes the rest of the page readable.

Taken verbatim from the freeze manifest’s own not_claimed array, with the page’s additional limits below it. A reader who skips everything else on this site should read this section.

NOT CLAIMED · reports/data/study-001-freeze.json
  • No full-PMB pra_lenient or UAR measurement exists for any DPO checkpoint; dpo-v1-scale appears only in pairwise comparisons.
  • No quantization figure in this repository is backed by a committed artifact (*.gguf is gitignored and results/ holds no quantization output).
  • data/distill/v1/ has no hash.txt, so its contents are not integrity-pinned here. — stale: 9 hash.txt files exist at the tag, including that one. See the discrepancy table below.
  • The acceptable_alternatives field is unpopulated in 688/688 PMB probes, which is why pra_strict is ~0 throughout and pra_lenient is the reported metric.
  • Every result is a single seed and a single run. Nothing here is replicated.
  • No human evaluation exists; no reviewer log or teacher transcript was retained, so the teacher identity is self-reported and unverifiable from this repository.

The repository’s canonical study definitions carry the up-to-date version of this list; this page prints the frozen manifest’s copy verbatim and flags the one item that has gone stale rather than silently editing either.

NOT SHOWN BY THIS PAGE’S OWN CHARTS
  • pra_lenient has no bootstrap confidence interval anywhere: the harness computes intervals only for pra_strict and UAR, and pra_strict is 0.0% in every system because the benchmark’s acceptable_alternatives field is empty.
  • MUR (memory utilisation rate), the metric H5 was written against, is 0 in every committed metrics.json — defined, never measured. It is reported here as 0.0, which is what the files hold, and it should not be read as a finding.
  • The k-sweep ran on 120 of the 688 probes (a stratified subsample), not the full benchmark, so its k=8 point (17.14%) is not comparable with D’s full-set 15.13%.
  • The pairwise comparisons used 105-probe subsamples of answerable probes, not the full set.
  • Judge-scored persona consistency sampled 60 of 688 responses per system.
  • The 2B + scaffold vs 8B crossover (H16/H17) — Study 001’s own thesis question — was never run.
  • H7 (alignment tax) was never tested: no IFEval-style instruction-following benchmark accompanies the DPO result.

Where the site and the repository disagree, the artifact wins, and the disagreement is printed. 7 were found while building this page — the last two by checking the manifest against the tree rather than trusting it:

QuantityWhat the prose saysWhat the artifact holdsWhat this site does
D · raw + memory, pra_lenientdocs/STUDIES.md and README.md print 15.10%15.13% — results/v0.1/D_memory/metrics.json holds 0.1513157894736842The artifact wins. The page prints 15.13% everywhere, so nothing on this site repeats the 15.10% figure.
B (v1) · SFT v1 alone, UARdocs/proper_scale_results.md’s corrected pass and docs/distillation_results.md print 25.0% (20/80)21.25% — results/v1_scale/B_sft/metrics.json holds 0.2125, which is 20/80 pre-rescoringThe artifact is shown in the table as it is and labelled stale; it is excluded from the ladder chart. scripts/generate_linkedin_figures.py states the same thing about this file.
C-vs-E at v1 scale, pairwise gapdocs/proper_scale_results.md prints 39.0% vs 20.0% (19.0pp) in its headline table45.7% vs 21.0% (+24.7pp) — results/v1_scale/C_vs_E_pairwise/summary.jsonThe artifact wins, and it matches the same document’s later corrected section: the headline table is the superseded bugged run.
Study 001’s own freeze committhe freeze manifest’s own git_commit field reads f2735aathe study-001 tag resolves at 4d9e5a0 — resolved with git, not read from the manifestThe manifest is written from `git rev-parse HEAD` at freeze time, so it lags a commit that only repaired hashes and docs. This page prints the commit the tag actually points at, shows the manifest’s own value beside it, and never treats the manifest field as evidence of the tag’s state.
false-abstention ratedocs/proper_scale_results.md, with the second and third points also in scripts/generate_linkedin_figures.pyno results/**/metrics.json records a false-abstention rate at allThe rates are parsed from the committed document by scripts/build-study-data.mjs and labelled as document-sourced, not artifact-sourced. Only the final point’s UAR and pra_lenient are also in a JSON artifact.
the manifest’s not_claimed listit still says data/distill/v1/hash.txt has no hash.txtdata/distill/v1/hash.txt exists at the tag — 9 hash.txt files do (4 benchmark, 5 training)The list is typed text in the manifest and is not regenerated from the tree, so it went stale when the hash repair created that file. This page prints the list verbatim and this row beside it. Item 3 of 6.
a guardrail citation with no guardrailthe E29 corrections cite “guardrail G13”, “G13” in reports/ERRATA.md and in the two new scriptsdocs/GUARDRAILS.md defines 13 rules and no G13/G13: the rules are G1, G2, G3, G4, G5, G6, G7, G8, G9, G9b, G10, G11, G12The rule describing that mistake already exists as G9b, “A partial verification is not a verification” — it was added by the same correction. This page reads the rules out of the guardrails file, so it prints G9b.

The stale SFT-v1-only row is the one case where this page prints a number the project itself considers corrected: the artifact is shown as it is, flagged, and left out of the chart, because a site about publishing corrected results should not quietly launder one.

09 / THE FREEZE

76 artifacts, pinned by SHA-256, and a check that fails if one moves.

Study 001 stopped changing on 2026-09-13. Its evidence is hash-pinned at the tag study-001; the manifest is regenerated and compared by scripts/freeze_study_001.py --check, which fails on any drift.

benchmark11files
training_data14files
eval_results21files
eval_raw7files
report10files
config6files
public_surface7files
PINNED ARTIFACTS76across 7 groups
HASH METHODSHA-256sha256 over LF-normalised bytes
LIVING DOCUMENTS6recorded for provenance, excluded from the check
INTEGRITY PINS9dataset hash.txt files at the tag — 4 benchmark, 5 training
CI ON THIS COMMITsuccessCI run #114 on 4d9e5a0 — lint, format, types, tests and the freeze check. Verified against the GitHub Actions API, not read from the tag.
CLAIMS MATRIXnot yet landedthe repository’s README references make validate make validate — freeze + hashes + recomputed metrics + claims matrix. The script behind that target is not in the frozen tag, so this page does not claim it exists; it will be reported here when it lands.

The living documents are excluded on purpose. The errata grows as mistakes are found and the guardrails gain rules; hashing them into the enforced set would mean every future correction “breaks the freeze”, which is backwards. The hash is computed over LF-normalised bytes so it verifies on a fresh clone — the naive sha256sum fails on all of them because the working tree is CRLF, which is the mistake guardrail G9 exists to prevent.

TAG RESOLVES AT4d9e5a09c67fa0cef688309dc4a3cc556353c073
MANIFEST RECORDSf2735aa11f89fdd6b3646b9a8cc7d38fb03d8ae9one commit behind — the freeze writes the field from HEAD at freeze time, and a later commit that only repaired hashes and docs did not re-take it

The commit is resolved with git, not read from the manifest. A generated field is not evidence of the state it names: the manifest’s own git_commit is stale here, and taking it at face value would have put the wrong commit on this page. Both values are shown.

THE REPOSITORY HAS MOVED PAST THIS FREEZEmain is at d8b5a32; the tag study-001 is at 4d9e5a0

This page reports the tag, because that is what a freeze is: the evidence a reader lands on cannot change underneath them. But the audit ledger kept going after the freeze, and the difference is material to any count a reader takes from this page:

  • audit findings: 29 at the tag, 30 on main — the ledger itself is a living document, so it was expected to grow
  • resolutions: 25 resolved at the tag, 26 on main
  • 12 tracked paths differ between the two — 0 of them under results/ or data/, which is the set that would move a number on this page

No number on this page comes from main. Every figure is read at the tag, and this banner is generated by the build script: when the maintainer re-takes the freeze and moves the tag, it disappears on the next npm run data without a code change.

Current errata on main

The freeze was re-taken, and each re-take is logged

The freeze was re-taken. Each entry records what moved and why it did not change a Study 001 finding. An unrecorded re-freeze makes the manifest meaningless, so this list is part of the artifact.

DateWhat movedWhy it did not change a finding
2026-09-13hf_readmes/generate_cards.pyFormatting only (one long line wrapped by black to satisfy E501; no values touched). Verified after the change that the generator still emits the corrected card content: no '70.0%' UAR on the DPO card, 'not measured' in the DPO eval table, and '15.30% / 70.0%' attributed to SFT+memory. No Study 001 finding depends on this file's byte content, only on the cards it produces.
2026-09-13README.mdCorrected to name the four hash.txt files that verify and to point at this manifest as the authoritative pin for the benchmark corpora (finding #29). A correction to a claim about the artifacts, not a change to any artifact.
10 / SOURCES

Every number on this page, and the file it came from.

No figure above was typed by hand. scripts/build-study-data.mjs reads each artifact out of the frozen study-001 tag with git show and writes src/studies/001/study-001.json, which the page and its charts import. The one dataset that has no JSON artifact is parsed from the committed document that holds it, and says so.

Path in small-mind-companionWhat was read out of it
results/v0.1/A_raw/metrics.jsonA: pra_lenient 0.16%, UAR 13.75%, per-category breakdown
results/v0.1/B_sft/metrics.jsonB (v0): pra_lenient 0.16%, UAR 16.25%
results/v0.1/D_memory/metrics.jsonD: pra_lenient 15.13%, UAR 8.75%
results/v0.1/E_sft_memory/metrics.jsonE (v0): pra_lenient 17.76%, UAR 33.75%
results/v1_scale/B_sft/metrics.jsonB (v1): UAR 21.25% — labelled stale on this page
results/v1_scale/E_sft_memory/metrics.jsonE (v1): pra_lenient 15.3%, UAR 70.0%
results/v1_scale/E_distill/metrics.jsonE-distill: pra_lenient 18.59%, UAR 71.25%
results/v0.1/ksweep/summary.jsonk-sweep pra_lenient and UAR at k = 0, 2, 4, 8, 16
results/v0.1/ksweep/k8/metrics.jsonk-sweep probe count (120)
results/v0.1/C_vs_E_pairwise/summary.jsonC-vs-E win rates and ties at v0 scale
results/v1_scale/C_vs_E_pairwise/summary.jsonC-vs-E win rates and ties at v1 scale
results/v1_scale/C_vs_F_distill_pairwise/summary.jsonF-vs-C win rates and ties — the distillation-specific measurement
results/v1_scale/pcs_judge_analysis/summary.jsonjudge-scored persona consistency for B, E and E-distill
results/v1_scale/pcs_stylometric_analysis/summary.jsonstylometric self-consistency and pre/post-distillation drift
data/benchmarks/pmb_v0_full/probes.jsonlprobe, persona and category counts (688 probes, 8 personas, 608/80 split)
docs/proper_scale_results.mdthe false-abstention trajectory (9.9% → 69.2% → 32.1%), its UAR and pra_lenient companions, and the detector numerator/denominator counts
reports/data/study-001-freeze.json76 frozen artifacts, 7 groups, the hash method, and the verbatim not-claimed list
reports/audits/001-claim-audit/findings.json29 findings (5 HIGH), their category counts, and the verbatim claim/actual text of every HIGH finding — including #29, BROKEN_INTEGRITY_PIN
reports/audits/001-claim-audit/resolutions.jsonresolution status (25 resolved, 2 partial, 2 open) and the verbatim resolution note and fix locations
docs/GUARDRAILS.mdall 13 rules, parsed rather than typed — including G1, G2, G3, G4, G5, G6, G7, G8, G9, G9b, G10, G11, G12
git ls-tree / git grep at the tag9 dataset hash.txt files (4 benchmark / 5 training), and the check that the manifest’s not_claimed list and the repository’s guardrail citations still hold
configs/training/*.yamlwhich configs pin the base model revision (2 of 6)
11 / WHAT COMES NEXT

The experiment that answers this study’s own question was never run.

Study 001 asked whether a small model plus a scaffold can substitute for scale. It measured the scaffold, the post-training and the tradeoff, but it never ran the 2B + scaffold against an 8B with no memory — the crossover script is written and unrun, and it needs a GPU. That, and replication of every headline number, is Study 002.

STUDY 002 · STATUSIN PROGRESS · NO RESULTS YET

Nothing in Study 002 has been run. Each candidate is pre-registered before it spends GPU time, and its results will be reported against the frozen Study 001 checkpoints rather than in place of them.