Key the snippet baseline by occurrence count, not by array index (#270) - #290
Conversation
The baseline keyed on (file, locator, defect, detail), and locator embeds the evidence-array index. 114 baselined findings sat at index >= 1, so deleting a duplicate evidence item renumbered the survivors and turned unchanged findings into new ones — `just qc` failing on a change that strictly improved the corpus. The remedy a curator reaches for is --write-baseline, which re-freezes anything genuinely new in the same PR. That is how a ratchet rots. detail went too, for the same reason at a different granularity: it carries the full snippet for ELLIPTICAL/UNSUPPORTIVE and the DOI for MISSING, so retyping a still-elliptical snippet or correcting the DOI on a still-snippet-less reference also flipped the key. audit_causal_graphs.py learned this first and keys on only the leading fragment of its detail; the docstring claimed "same shape" and this was the one place it wasn't. Dropping both collapses findings onto shared keys, which is why #267 deferred the obvious fix: set membership would then let a THIRD missing snippet match a baselined pair and pass silently, trading a false positive for a false negative inside the integrity mechanism. So the baseline is read as a COUNT per key — "two of these were accepted" — and only occurrences in excess are new. Fewer than baselined is an improvement and passes. Canaried on the record the issue names. Deleting biofilm_formation's evidence[0] so evidence[1] renumbers: 2736 findings, 0 new, exit 0 — the case that used to fail. Adding a third snippet-less item: 2738 findings, 1 new, exit 1. The baseline file format is unchanged, so no regeneration: load_baseline aggregates the existing rows by key. 348 tests pass (6 new), `just qc` exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Claude finished @realmarcin's task in 2m 54s —— View job ReviewThe count-keyed baseline is the right call, and the reasoning about why the naive index-strip was wrong is correct. One hole, in the defect this audit most needs to hold. 🟡 Dropping
|
There was a problem hiding this comment.
Count-keyed baseline is the right call. One hole: dropping detail from _key disarms the ratchet on REUSED_SNIPPET, whose locator ({graph_id}:*) carries no index and whose magnitude lives entirely in detail — a baselined "3 evidence items share one snippet" now silently covers "9 evidence items share one snippet". That is the failure audit_causal_graphs.py:220-230 explicitly reversed for its own whole-graph defect, and it is the defect #183's backfill most directly worsens. Full detail in the review comment.
Dropping detail from the key cost nothing on the index problem for per-item
defects, and disarmed the ratchet on the one aggregate this audit emits.
REUSED_SNIPPET's locator is `{graph_id}:*` with no index, and its magnitude
lives entirely in detail — so a graph going from 3 shared snippets to 9 is one
finding either way, same key, occurrence count unchanged, and it passed. All 13
baselined rows were exposed; gc_content is already at 5.
The naive repair fails the other way: putting the count back in the key makes
3 -> 2, an improvement, an unbaselined finding that exits 1 — the exact rot #270
was about. So the magnitude is ratcheted as a VALUE. load_baseline records the
worst accepted per key; compare() flags a finding whose magnitude exceeds it
even when the count does not.
Scoped to REUSED_SNIPPET deliberately. UNSUPPORTIVE_SNIPPET's detail also leads
with an integer, but that one is a character count where larger is better, so
ratcheting it would flag a snippet growing from 6 chars to 10 as a regression.
MAGNITUDE_DEFECTS says which, and why.
Canaried against the real baseline: 3 -> 9 flagged, 3 -> 2 and 3 -> 3 pass.
352 tests pass (4 new), `just qc` exits 0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Claude finished @realmarcin's task in 2m 26s —— View job ReviewRe-reviewed after One defect remains, plus the two 🔵s from the last pass that are still open. 🟡 The baseline-currency test still uses the semantics this PR abandoned
new = [r for r in audit() if _key(r) not in baseline]
Concretely: add a third snippet-less reference to a file with two baselined at the same key, and 🔵
|
Four review items, one of them a hole the previous commit opened.
The baseline-currency test — the only one comparing the committed baseline to
the live corpus — still read `_key(r) not in baseline`. Against the new dict
that tests key PRESENCE, so it silently stopped checking both the occurrence
count and the magnitude: a third snippet-less reference, or a graph growing from
5 shared snippets to 50, would fail `just qc` while the test passed. Now uses
compare(), so it asserts what qc enforces.
Magnitudes were keyed per _key(), and REUSED_SNIPPET's locator is `{graph_id}:*`
— so every reused snippet in one graph shared a key and a per-key max let the
smaller of an uneven pair grow up to the larger unnoticed.
trophic_type_classification_axes already carries two such rows, equal today. The
snippet itself is now the discriminator.
compare() took the baseline as an argument while reading magnitudes from a
module global, which the tests exposed by having to clear and poke it by hand.
load_baseline returns a Baseline NamedTuple carrying both maps, so compare() is
a pure function of its arguments and the tests construct a baseline instead.
The module docstring still claimed "same shape as audit_causal_graphs.py". It
is not, and the divergence is the point: that shape is set membership on a
4-tuple carrying the index and a detail fragment, which is exactly what rotted
here. Now says what this does and why. compare() also documents that for a count
excess it names the LAST row at a key rather than the new one — the occurrences
are interchangeable by construction, so no better answer exists.
353 tests pass (1 new), `just qc` exits 0, and the real-baseline canary still
flags 3 -> 9 while passing 3 -> 2.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Claude finished @realmarcin's task in 2m 42s —— View job ReviewRe-reviewed after
No 🔴 or 🟡. One 🔵 below, which is a tradeoff to document rather than a defect to fix. 🔵 The magnitude branch is an identity check for any snippet it hasn't seen
That is a false positive on a finding that got neither better nor worse, and the PR's own thesis is that a false positive is what sends a curator to I don't think the obvious remedy is free. Falling back to the per-key max when Two smaller things in the same neighbourhood:
Checked and clean: Not verified: · branch |
Editing a REUSED_SNIPPET's shared quote produces an unseen magnitude key, so its baselined value reads as 0 and any count beats it — a rewording reported as new when nothing got worse. The obvious fallback to the graph's per-key max would restore the sheltering #291 removed, so this fails closed and is tracked rather than patched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Claude finished @realmarcin's task in 2m 8s —— View job ReviewRe-reviewed after I re-checked the whole mechanism rather than just the delta. No 🔴 or 🟡. Two 🔵 nits below, both narrow and neither blocking. What I verified this pass
🔵
|
Closes #270, deferred out of #267 because getting it right needed more than the obvious fix.
The bug
The baseline keyed on
(file, locator, defect, detail), andlocatorembeds the evidence-array index. 114 baselined findings sat at index ≥ 1, so deleting a duplicate evidence item renumbered the survivors and turned unchanged findings into new ones —just qcfailing on a change that strictly improved the corpus. The remedy a curator reaches for is--write-baseline, which re-freezes anything genuinely new in the same PR. That is how a ratchet rots.detailhad the same sensitivity one level down: it carries the full snippet forELLIPTICAL/UNSUPPORTIVEand the DOI forMISSING, so retyping a still-elliptical snippet or correcting the DOI on a still-snippet-less reference also flipped the key.audit_causal_graphs.pylearned this first and keys on only the leading fragment of its detail; this script's docstring claimed "same shape as audit_causal_graphs.py" and the key was the one place it wasn't.Why the obvious fix was wrong, and what this does instead
Stripping the index collapses
evidence[0]andevidence[1]onto one key — so under set membership a third missing snippet atevidence[2]would match a baselined key and pass silently. That trades a false positive for a false negative inside the integrity mechanism, which is why #267 deferred it rather than patching it at the tail of a merging PR.So the baseline is read as a count per key: "two of these were accepted." Only occurrences in excess of that are new; fewer than baselined is an improvement and passes.
Canaried on the record the issue names
The first is the case #270 is about; the second is the false negative the naive fix would have introduced. Both are pinned by tests, along with the key still separating files, locators and defects.
No baseline regeneration. The file format is unchanged —
load_baselineaggregates the existing rows by key — so the diff is code and tests only.Verification
🤖 Generated with Claude Code