v1.108.278 — `exact` must mean exact, and a guardrail must not be its own baseline
Two fixes, both cases of a measurement reporting a grade it did not make.
identity_type said exact for a match only normalisation made (#458)
_tokenize folds case, strips leading underscores and drops punctuation, and the identity channel's tokenized comparison ran against that folded form. So for the query _State, a pytest fixture named state and the class literally named _State both scored identity: 50.0, identity_type: "exact". The channel could not separate them, the tie fell through to BM25, and the shorter name with a docstring won by 0.355 points out of ~58 — a test fixture outranking the source symbol it tests, on the single highest-confidence query a caller can send.
A match that needed normalisation now scores 40.0 and reports identity_type: "normalized". Literal stays 50.0; prefix (30.0) and segment (20.0) are untouched, so this uses the tiering mechanism that already existed.
⚠ Case folding alone is still exact, and that boundary was the decision. raw_lower has been case-folded since the channel arrived, so making case load-bearing would change the answer for every caller who types getuser for getUser — a behaviour change with no defect behind it. A caller passing only tokenized terms and no raw query also keeps exact: with no raw spelling there is nothing to be literal about, and grading it down would report a distinction that was never measured, which is the defect rather than the fix.
⚠⚠ The first end-to-end test passed against the broken code. BM25 normalises by document length, and the real _State is a large class that scored below the two-line fixture on every lexical field (name 6.996 vs 8.012, signature 6.153 vs 7.389, summary 0.0 vs 5.992). A small synthetic class wins on lexical signals alone, so the ordering assertion held with and without the identity tier — green, asserting exactly the right thing, and testing nothing. The corpus now gives the class a long docstring of words unrelated to the query: length without a match, which is what the real class's own prose does.
A schema-budget guardrail asserted a file against copies of itself (test-only)
test_the_core_compact_schema_budget_is_unchanged read three numbers out of benchmarks/schema_baseline.json and asserted they equalled three copies of themselves written into the test. Both sides were the same frozen artifact, so it pinned the artifact and never the surface: the baseline was captured in 2026-07 and the tool surface drifted underneath it release after release while the assertion stayed green. It failed for the first time when the capture was re-run — firing on the one event that proves nothing regressed, and silent through every event it existed for.
Removed rather than re-pinned, because re-pinning restores exactly that. tests/test_schema_budget.py already holds the 5% drift ceiling against a live _build_tools_list() and the §10 <=4000 ceiling recomputed from the live build, which is the check written to catch a breach before the baseline is regenerated. tests/test_schema_baseline_transcription.py now fails if any baseline value returns to tests/ or benchmarks/, prose included — two of the five sites this was written for were docstrings, and a stale number in a comment survives longest precisely because nothing executes it.
Verification
7799 passed, 9 skipped, 0 failed · ruff check src/ clean · all 9 CI jobs green at 35eeb2d · 3.13 CI-env reproduce 7793 passed / 15 skipped, the same 7808 total.
⚠ The documented 3.13 reproduce command under-collects by 105 tests and reports a clean pass while doing it. uv run --python 3.13 python -m pytest tests/ -q misses three watcher files gated on pytest.importorskip("watchfiles"), an optional extra that CI installs and that command does not. Use --group dev --extra watch. Corrected in CLAUDE.md; the compare-totals-never-passed-counts convention is what caught it.
Not in this release: #447, the install-pack archive confinement fix by @elfrost, which is reviewed, green and waiting only on a CLA. A release is never blocked on an open issue; it rides the next one.