Skip to content

Check that evidence snippets are verbatim, instead of assuming it (#247) - #267

Merged
realmarcin merged 3 commits into
mainfrom
fix/evidence-snippet-verbatim
Aug 5, 2026
Merged

Check that evidence snippets are verbatim, instead of assuming it (#247)#267
realmarcin merged 3 commits into
mainfrom
fix/evidence-snippet-verbatim

Conversation

@realmarcin

Copy link
Copy Markdown
Contributor

Closes #247. Settles the decision it asked for, and ships the check that makes it enforceable.

The decision: verbatim stands

#247 offered three ways out — re-fetch the source, add a separate verbatim field to reports, or relax the schema. Re-fetch. A report's evidence text goes in notes:, which is where reasoning about a source belongs and carries no verbatim requirement; the DOI transfers, the quote does not. Relaxing snippet: was never attractive: the anti-hallucination check is the reason the field is worded as it is.

That is now written into docs/CURATION_PLAYBOOK.md, because pasting the report's evidence column is otherwise the obvious way to work through #183's backfill, and nine reports state outright that their evidence text "closely paraphrases or quotes" the source — so which of the two you copied is unknowable without opening the paper.

The enforcement gap was the real finding

The requirement was documented; nothing checked it. Enforcement was a human PR reviewer, so the corpus already carried violations nobody had logged. Across 4,089 evidence items on 3,402 edges:

defect n
MISSING_SNIPPET 2,586 a reference with no quote at all — 63%
UNSUPPORTIVE_SNIPPET 71 host, toxins, growth
ELLIPTICAL_SNIPPET 60 stitched, not contiguous — ERROR
REUSED_SNIPPET 13 one phrase across 3+ edges of a graph
ECHOES_RESEARCH_REPORT 7 text also in the trait's own report answer

The REUSED_SNIPPET set includes virulence factors on three edges of animal_pathogen — the literal example the playbook names as the thing to avoid. The ECHOES set includes cellulolysis, the worked example #183 holds up as the template for what "done" looks like.

Worth stating plainly: 63% of evidence items assert a mechanism on a bare DOI. snippet: is schema-optional so those are valid records, but burning that down is the same job as #183's backfill, on the same edges.

A ratchet, so it lands green

conf/evidence_snippet_baseline.tsv freezes today's 2,737 findings and only new ones fail — the same shape as audit-graphs (#185). It joins qc passing, and the backfill cannot make it worse. Canaried: injecting one elliptical snippet takes it to exit 1; removing it returns to 0.

Direction was the hard part, and hand-checking found what reading didn't

A curated snippet appearing in a report proves nothing on its own. Two confounds, each found by inspecting actual findings:

  • 257 findings — naive whole-file match. Every curator-written snippet matched its own reflection: the pipeline feeds a trait's existing evidence into the prompt as evidence_summary, and the provider echoes the whole prompt twice.
  • 41 findings — after trimming to the answer with research_answer() (reused from The per-trait research block never renders: the renderer and the research pipeline disagree on the filename #233). arsenic_tolerant still flagged, and I checked it: its quote was handed to the provider in the prompt, so the answer restating it says nothing about where the YAML got it.
  • 7 findings — after also subtracting what the provider was shown. What survives is text in the provider's own answer that was never given to it.

Both confounds are pinned by tests, since either regression silently re-inflates the count. The finding is worded as a prompt to verify, not a verdict — a report may legitimately quote the same sentence you did.

Verification

310 tests pass  (17 new)
just qc         exit 0  (audit-snippets now a member)

Also added _safe_rel so the audit runs against a fixture tree instead of raising outside the repo root.

🤖 Generated with Claude Code

EvidenceItem.snippet is specified as a verbatim quote, and CURATION_PLAYBOOK
sharpens that to contiguous, no ellipsis, no paraphrase, diversified across
edges. Nothing in this repo checked any of it — enforcement was a human PR
reviewer — so the corpus already carried violations nobody had logged.

What the corpus actually holds, across 4089 evidence items on 3402 edges:

  MISSING_SNIPPET         2586   a reference with no quote at all — 63%
  UNSUPPORTIVE_SNIPPET      71   'host', 'toxins', 'growth'
  ELLIPTICAL_SNIPPET        60   stitched, not contiguous [ERROR]
  REUSED_SNIPPET            13   one phrase across 3+ edges of a graph
  ECHOES_RESEARCH_REPORT     7   text taken from the trait's own report

That REUSED_SNIPPET set includes 'virulence factors' on three edges of
animal_pathogen — the literal example the playbook names.

The decision #247 asked for: verbatim stands. A report's evidence text goes in
notes:, which is where reasoning about a source belongs and has no verbatim
requirement; the DOI transfers, the quote does not. Documented in the playbook,
because the shortcut is otherwise the obvious way to work #183's backfill.

Landed as a ratchet against conf/evidence_snippet_baseline.tsv, the same shape
as audit-graphs: today's 2737 findings never fail, anything new exits 1. So it
joins qc green while the backfill cannot make it worse. Canaried: injecting one
elliptical snippet takes it to exit 1, removing it returns to 0.

ECHOES_RESEARCH_REPORT was the hard one, and verification mattered — direction
is not observable from a match alone. Two confounds, both found by hand-checking
findings rather than by reading code:

  257 findings  naive whole-file match. Every curator-written snippet matched
                its own reflection, because the pipeline feeds existing evidence
                into the prompt and the provider echoes the prompt twice.
   41 findings  after trimming to the answer via research_answer() (#233).
                arsenic_tolerant still flagged: its quote was handed to the
                provider as evidence_summary, so the answer restating it says
                nothing about where the YAML got it.
    7 findings  after also subtracting what the provider was shown.

Both are pinned by tests, since either regression silently re-inflates the
count. Also added _safe_rel so the audit runs against a fixture tree rather than
raising outside the repo root.

310 tests pass, `just qc` exits 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@culturebot-reviewer

culturebot-reviewer Bot commented Aug 5, 2026

Copy link
Copy Markdown

Claude finished @realmarcin's task in 6m 4s —— View job


Review

  • Read repo conventions (README, docs/, justfile)
  • Read scripts/audit_evidence_snippets.py + tests
  • Spot-check findings against the corpus and the research reports
  • Check CI (pytest / qc / pr-sanity all green on this head)
  • Verdict

The audit itself is sound. iter_evidence covers both places EvidenceItem is used in the schema (TraitRecord.evidence line 219, CausalEdge.evidence line 398) and nothing else, so coverage is complete. The direction handling is the part that could have been quietly wrong, and it isn't: evidence_summary is the only template_variable that carries curated snippet text (checked against a real report's front matter), and edge-level snippets are never in the prompt at all, so the subtraction is doing what the docstring claims. I confirmed the detector is not vacuous — cellulolysis_cellulase_systems:cellobiohydrolase->cellobiose[0] is a verbatim lift of the falcon report's evidence column, ellipsis and all (research/traits/metabolism/cellulolysis-deep-research-falcon.md:292), which is exactly the failure mode #247 describes.

One thing needs changing.


🟡 The new tracked report is outside the staleness gate that exists for exactly this case

reports/evidence_snippet_audit.tsv is committed, and just qc rewrites it (audit-snippets runs at justfile:680, before audit-derived-reports). That is the precise situation justfile:382-390 says is forced: because an earlier step in the same run refreshes the working-tree copy, comparing the working tree "would always pass while a stale committed copy sailed through — confirmed by appending a bogus row and running what qc runs." causal_graph_audit.tsv gets a git show HEAD: comparison for that reason (justfile:437-473); the new report gets nothing.

So a stale committed copy of this file is unobservable. test_the_committed_baseline_matches_the_corpus doesn't cover it — it asserts findings ⊆ baseline, which is the growing direction, while staleness from a burn-down shrinks the set. That is the same shape as the incident quoted at justfile:368: cellobiose sat in a committed report for weeks, off by one row.

Two ways out, either fine:

  • add it to audit-derived-reports alongside cga, same git show HEAD: basis and the same "exit code is a ratchet verdict, not a generation error" caveat; or
  • don't commit it. It is byte-identical to conf/evidence_snippet_baseline.tsv today (md5 4891832e…), so as committed it is 2,738 duplicated lines whose only distinct value is being a snapshot — and a snapshot nothing keeps current is the thing Derived grounding residual reports drift from the corpus with nothing to catch it #214 was filed about.

Fix this →


🔵 Smaller things

  • _fold (scripts/audit_evidence_snippets.py:110) strips punctuation without re-collapsing whitespace, so a standalone or leaves a double space in the folded snippet and the same sentence in a report folds to a single space — no match. The snippets most worth checking against a source (the elliptical ones) are the ones this weakens. cellulolysis matched only because the report carried the identical ellipsis. re.sub(r"\s+", " ", …).strip() after the strip closes it.
  • --fail-on error is a valid choice but the help text only documents new and any (audit_evidence_snippets.py:289-290).
  • REUSED_SNIPPET's detail says "N edges share one snippet" but counts evidence items, so two items on one edge with the same snippet would report as two edges. No corpus row is currently of that shape — I checked the flagged graphs — so this is wording, not a wrong number today.
  • print(f"=== evidence snippet audit: baseline written ===") (line 300) has no placeholders; just lint would flag F541 under the default ruff F set.

Not defects, noted

  • The cellulolysis and ECHOES/ELLIPTICAL findings are real evidence-integrity defects, but they pre-date this PR and freezing them is the point of a ratchet. Worth saying that cellulolysis is Causal graphs are 62% fragmented and audit-graphs cannot see it #183's worked example, so it is a good first burn-down target rather than something to leave in the baseline indefinitely.
  • No data/traits/** change here, so no history/ record is expected.
  • The PR's numbers check out: 2,737 findings + header = 2,738 lines, defect counts match the table, and the test file does contribute 17 tests (13 functions, one parametrized ×4).
    · branch fix/evidence-snippet-verbatim

@culturebot-reviewer culturebot-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The audit is sound — schema coverage is complete (both EvidenceItem use sites), and the direction handling that could have been quietly wrong isn't: evidence_summary is the only template_variable carrying curated snippet text, and edge snippets are never in the prompt. Confirmed the ECHOES detector is not vacuous: cellulolysis_cellulase_systems:cellobiohydrolase->cellobiose[0] is a verbatim lift of the falcon report's evidence column, ellipsis and all.

One 🟡: reports/evidence_snippet_audit.tsv is tracked and rewritten by audit-snippets earlier in the same qc run, but is not in audit-derived-reports — which is exactly the situation justfile:382-390 says forces a 'git show HEAD:' comparison, because a working-tree comparison lets a stale committed copy sail through. The baseline test does not cover it (findings ⊆ baseline is the growing direction; burn-down staleness shrinks). Either wire it into audit-derived-reports next to the cga block, or stop tracking it — it is byte-identical to conf/evidence_snippet_baseline.tsv.

Smaller: _fold strips punctuation without re-collapsing whitespace, so a standalone ellipsis leaves a double space and weakens matching on exactly the snippets most worth checking; --fail-on error is undocumented in the help; REUSED_SNIPPET's detail says 'edges' but counts evidence items; F541 on line 300.

Full detail in the review comment on the PR.

…268, #269)

reports/evidence_snippet_audit.tsv was tracked and rewritten by audit-snippets
during the same `just qc` run that would have to check it — the exact situation
the justfile already documents as unobservable, since an earlier step refreshes
the working tree and a stale committed copy sails through. It was also
byte-identical to conf/evidence_snippet_baseline.tsv (md5 4891832e), so it added
2,738 duplicated lines whose only distinct value was being a snapshot, which is
what #214 was filed about. Untracked and gitignored, with a test asserting it
stays that way. The baseline remains tracked; it is the artifact with meaning.

_fold stripped punctuation without re-collapsing whitespace, so a standalone "…"
or " — " left a double space where the report's prose folded to one, and the
substring test failed on exactly the snippets most worth checking against a
source. `cellulolysis` matched only because the report carried the identical
ellipsis. Now an elliptical snippet matches prose without one, pinned by a test.

Also: --fail-on error was undocumented in the help though it is a valid choice;
REUSED_SNIPPET said "N edges" while counting evidence items, which differ when
one edge carries two; and a placeholder-free f-string would trip ruff F541.

The detail rewording changed 13 baseline keys, so the baseline is regenerated —
13 lines, same 2737 findings.

Canaried again after the changes: injecting one elliptical snippet exits 1,
removing it returns to 0. 312 tests pass, `just qc` exits 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@culturebot-reviewer

culturebot-reviewer Bot commented Aug 5, 2026

Copy link
Copy Markdown

Claude finished @realmarcin's task in 5m 6s —— View job


Review

  • Read the new commit against the prior CHANGES_REQUESTED
  • Re-read scripts/audit_evidence_snippets.py + tests
  • Check the fold fix actually changes what it claims
  • Check CI on this head
  • Verdict

The blocking 🟡 is resolved, and by the cleaner of the two routes I offered: reports/evidence_snippet_audit.tsv is untracked, gitignored with the reason written down, and pinned by test_the_audit_report_is_not_tracked so it cannot drift back. I checked for dangling references — nothing outside DEFAULT_REPORT and the test names the path, so audit-derived-reports needed no change. All four 🔵s landed. The baseline diff is exactly what the commit message claims: 26 lines = 13 REUSED_SNIPPET rows reworded, which is all 13 of them, and the total is still 2,737 findings + header. CI is green on this head (pytest, qc, pr-sanity, vendored-sync).

Approving. Two things below are worth doing; neither should hold the merge.


🔵 The baseline key embeds an array index, which the sibling ratchet deliberately avoids

_key (scripts/audit_evidence_snippets.py:274) is (file, locator, defect, detail), and locator carries the evidence-array index — evidence[1], graph:subj->obj[1]. 114 baselined findings sit at index ≥ 1, so removing or prepending an evidence item renumbers the survivors and turns an unchanged finding into a new one.

Concretely, data/traits/ecology/biofilm_formation.yaml:16-22 has two record-level items, both baselined as MISSING_SNIPPET. Delete item 0 as a duplicate DOI and item 1 becomes evidence[0]; its key is not in the baseline, so just qc exits 1 on a change that strictly improved the corpus. The remedy a curator will reach for is --write-baseline, which re-freezes anything genuinely new in the same PR — that is how a ratchet rots.

audit_causal_graphs.py:220-247 carries a 17-line comment about getting this exact thing wrong: it keys on graph_id plus only the leading fragment of detail, because the full string "blocked a PR" on an edit that changed nothing real. This script's docstring says "same shape as audit_causal_graphs.py"; the key is the one place it isn't. detail has the same sensitivity — the full snippet text for ELLIPTICAL/UNSUPPORTIVE, the DOI for MISSING — so retyping a still-elliptical snippet, or correcting the DOI on a still-snippet-less reference, also flips the key.

It fails closed in every case I could construct, which is why this is 🔵 and not blocking, and the mainline #183 move (adding a snippet to an existing item) only removes findings. But the fix is a few lines and the postmortem is already written next door. Fix this →

🔵 The "edges" → "evidence items" correction landed in the code but not where a curator reads it

The finding text is fixed, but the module docstring still says "the same snippet on several edges of one graph" (audit_evidence_snippets.py:23-24) and the playbook table still says "one snippet on 3+ edges of a graph" (docs/CURATION_PLAYBOOK.md, the REUSED_SNIPPET row). Those are the two places someone goes to find out what the defect means. Fix this →

Minor, in the same test: test_the_audit_report_is_not_tracked(tmp_path) takes an unused fixture, and subprocess.run has no check=True — if git fails for any reason stdout is empty and the assertion passes vacuously.

Noted, not defects

  • The fold fix changes nothing on the corpus: 7 ECHOES and 60 ELLIPTICAL before and after, and the whole 26-line baseline diff is the REUSED rewording. So no elliptical snippet was in fact being missed — the correctness argument rests on test_fold_collapses_whitespace_left_by_stripped_punctuation, which is a fine place for it to rest, but the PR body reads as though a real detection gap was closed and it wasn't.
  • cellulolysis_cellulase_systems:cellobiohydrolase->cellobiose[0] is still baselined, and it is Causal graphs are 62% fragmented and audit-graphs cannot see it #183's worked example. Good first burn-down target rather than something to leave frozen.
  • No data/traits/** change, so no history/ record expected.

· branch fix/evidence-snippet-verbatim

@culturebot-reviewer culturebot-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The blocking issue is resolved, by the cleaner route: reports/evidence_snippet_audit.tsv is untracked, gitignored with the reason recorded, and pinned by a test. No dangling references, so audit-derived-reports needed no change. All four smaller items landed. Baseline diff is exactly the 13 REUSED_SNIPPET rewordings; still 2,737 findings. CI green on this head.

Two 🔵s, neither blocking. (1) _key embeds the evidence-array index and the full detail string, so deleting or prepending an evidence item renumbers survivors and fails qc on a strict improvement — 114 baseline rows sit at index >= 1, and audit_causal_graphs.py:220-247 carries a written postmortem of this exact pattern. It fails closed in every case I could construct, hence not blocking. (2) The 'edges' -> 'evidence items' correction landed in the finding text but not in the module docstring (line 23) or the playbook's defect table.

Also worth knowing: the fold fix changes nothing on the corpus — 7 ECHOES and 60 ELLIPTICAL before and after — so it is pinned by its unit test, not by a detection delta.

Full detail in the review comment.

#271)

The REUSED_SNIPPET finding text was corrected to count evidence items rather
than edges, since one edge can carry several. The correction missed the two
places someone goes to learn what the defect means: the module docstring and the
playbook's defect table.

Deferring #270 rather than fixing it here, deliberately. The baseline key embeds
an evidence-array index, so renumbering can fail qc on a change that improved
the corpus. The obvious fix — strip the index — collapses evidence[0] and
evidence[1] to one key, so a THIRD missing snippet would match a baselined key
and pass silently. Trading a false positive for a false negative in the
integrity mechanism itself needs per-key occurrence counts and its own tests,
not a tail-end edit. It fails closed today, and #183's mainline move only
removes findings.

312 tests pass, `just qc` exits 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@culturebot-reviewer

culturebot-reviewer Bot commented Aug 5, 2026

Copy link
Copy Markdown

Claude finished @realmarcin's task in 1m 31s —— View job


Review

ad4ffd94 is docs + docstring only, and it closes the second 🔵 in both places I named: the module docstring (scripts/audit_evidence_snippets.py:23-26) and the playbook's defect table (docs/CURATION_PLAYBOOK.md:323). The wording now matches what the code measures — counts[snippet] += 1 is per evidence item, not per edge — and the inline comment at the REUSED_SNIPPET construction site says why. I grepped for stragglers; the only remaining "edges" is the ### Evidence on edges heading, which is correct.

The #270 deferral is right, and for the reason given. I checked the argument rather than taking it: stripping [n] from the locator makes evidence[0] and evidence[1] on one file collapse to a single baseline key, so with two MISSING_SNIPPET items baselined, adding a third would match an existing key and pass silently. That converts a false positive that fails closed into a false negative in the integrity mechanism itself — strictly worse than the bug it fixes. A correct fix needs per-key occurrence counts (baseline stores n, fail when observed > baselined), which is a schema change to the TSV plus its own tests. Not a tail-end edit to this PR.

CI is green on this head — pytest, qc, pr-sanity, vendored-sync.

Approving.


🔵 Still open, non-blocking

The one minor from last round didn't land: test_the_audit_report_is_not_tracked(tmp_path) (tests/test_audit_evidence_snippets.py:227) takes an unused fixture, and its subprocess.run has no check=True — if git fails for any reason, stdout is empty and assert out == "" passes vacuously, so the test that pins the untracked-report decision would go quiet exactly when it can't tell. Two-line fix. Fix this →

Noted, not defects

  • cellulolysis_cellulase_systems:cellobiohydrolase->cellobiose[0] is still baselined and is Causal graphs are 62% fragmented and audit-graphs cannot see it #183's worked example — a good first burn-down target rather than something to leave frozen.
  • No data/traits/** change, so no history/ record expected.
  • I hold no write access and can't submit a formal GitHub review, so this comment is the review. The prior APPROVED review stands; nothing in ad4ffd94 changes it.
    · branch fix/evidence-snippet-verbatim

@realmarcin
realmarcin merged commit f4841c7 into main Aug 5, 2026
5 checks passed
@realmarcin
realmarcin deleted the fix/evidence-snippet-verbatim branch August 5, 2026 01:36
realmarcin added a commit that referenced this pull request Aug 5, 2026
The previous commit closed sections 7 and 8 and opened section 5 — the same
header-without-body failure, now in the section the header calls the largest
remaining item.

Three numbers were wrong against conf/causal_graph_audit_baseline.tsv, and the
section still described the pre-#220 audit:

  files with >=1 UNREACHABLE_FROM_TRAIT   said 219    is 220
  UNREACHABLE_FROM_TRAIT findings         said 1314   is 1321
  morphology FRAGMENTED_GRAPH rows        said 46     is 47
  baseline total                          said 1314   is 1541

The 219-vs-220 discrepancy the section explained at length as a real one-file
gap is gone: #220 closed in #227, and FRAGMENTED_GRAPH now catches a graph
splitting into components that each carry their own TRAIT node — the
dumbbell_shaped case. So the two measures now agree at 220, and the section says
why rather than still explaining the old gap.

Also records what the header could not: backfill progress is 1 of 220, and that
one — cellulolysis — is simultaneously #183's worked example and a member of
#267's ECHOES_RESEARCH_REPORT set, so it demonstrates both the target and the
shortcut to avoid.

Every figure re-derived from the tracked baseline rather than carried forward.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
realmarcin added a commit that referenced this pull request Aug 5, 2026
* Reconcile NEXT_TASKS.md (2026-08-05)

The file was written before this session and claimed the Edison sweep was "being
re-run"; it completed in #241. Reconciled against what actually merged.

Eight of the fifteen issues it listed are closed (#192, #193, #203, #205, #208,
#214, #218, #220). Sixteen are open, and nearly all the new ones come from the
reviews of the five PRs that landed since — which is the review loop working
rather than a backlog blowing out.

Recommends #252 next: third recurrence of one bug class (#184, #200, #250), the
gate machinery is fresh from #272, and docs/WORKFLOW_CONVENTIONS.md currently
has to say "nothing checks this invariant". Names #270 as runner-up and says
plainly what is NOT actionable as a next item — #183's backfill is a campaign,
not a PR, and the vendored-file items want the hub repo.

Adds section 10 for the evidence-snippet audit landed in #267, with the frozen
backlog's shape, since the 2,586 missing snippets are the same edges #183's
backfill has to touch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Reconcile the section bodies, not just the headings they sit under (#283)

The first pass changed section headings and the header block and stopped there,
so the file asserted both halves of three contradictions: section 8's heading
said the sweep was complete while its body said "is under way" and displayed
`manifest ok rows with a missing artifact: 331` as a live figure, and section 7's
pending table listed six issues the header's own closed list named on the same
screen.

Section 8 is rewritten to what happened: 353/353 tracked, the manifest's 714
rows as a spend record with all 13 failures later succeeding, the clean --verify
output including the malformed-CURIE scan #242 added, and the five issues the
sweep left behind (#244, #245, #246, #248, #249) which the header table routes
here but which appeared nowhere in the section. It now carries #244's caveat
that --verify is narrower than its clean output suggests.

Section 7's table is the current residuals — #191, #197, #198, #209, #217, #252,
#275 — with the closed ones listed as closed rather than pending.

Checked mechanically rather than by eye, since the file is 600 lines: zero table
rows name a closed issue, and every open issue appears somewhere. That check
found #283 itself missing and it is now listed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Reconcile section 5, where the header sends readers for #183 (#283)

The previous commit closed sections 7 and 8 and opened section 5 — the same
header-without-body failure, now in the section the header calls the largest
remaining item.

Three numbers were wrong against conf/causal_graph_audit_baseline.tsv, and the
section still described the pre-#220 audit:

  files with >=1 UNREACHABLE_FROM_TRAIT   said 219    is 220
  UNREACHABLE_FROM_TRAIT findings         said 1314   is 1321
  morphology FRAGMENTED_GRAPH rows        said 46     is 47
  baseline total                          said 1314   is 1541

The 219-vs-220 discrepancy the section explained at length as a real one-file
gap is gone: #220 closed in #227, and FRAGMENTED_GRAPH now catches a graph
splitting into components that each carry their own TRAIT node — the
dumbbell_shaped case. So the two measures now agree at 220, and the section says
why rather than still explaining the old gap.

Also records what the header could not: backfill progress is 1 of 220, and that
one — cellulolysis — is simultaneously #183's worked example and a member of
#267's ECHOES_RESEARCH_REPORT set, so it demonstrates both the target and the
shortcut to avoid.

Every figure re-derived from the tracked baseline rather than carried forward.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Reconcile the paragraphs the tables sit under, and drop a false example (#283, #284)

Fourth round on the same defect class, so this pass swept the file mechanically
instead of reacting to the sections named.

Section 5's opening still said "the audit-graphs gate does not catch graph
fragmentation" and "nothing currently reports it", three lines under a heading
saying DETECTION DONE. audit_causal_graphs.py reports it 220 times. Rewritten to
past tense, with FRAGMENTED_GRAPH added to the list of what the audit now emits.

The dumbbell_shaped rationale I added last round is FALSE, and the reviewer
checked it against the tree rather than taking it. That record declares exactly
one TRAIT node and carries 7 UNREACHABLE_FROM_TRAIT rows, so reachability
catches it fine. I had propagated it from audit_causal_graphs.py's own comment
without verifying — now filed as #284. The blind spot FRAGMENTED_GRAPH closes is
real, but no record exhibits it: a sweep for fragmented graphs with >1 TRAIT
node and zero unreachable rows returns 0, and the two measures cover the
identical 220 files. Stated abstractly, with that fact said out loud.

Section 8 said "8 fail:1 rows" 22 lines above its own correct 13.

Section 7's prose still had #217 as a page with "nowhere to live" and #218 as
unimplemented, under a table listing both as landed. #217 now says what actually
remains — where the page lives, not whether it exists.

Swept for the rest: the only other stale claim was #216's "still unproven at the
time of writing", which is proven now — claude-review has run to completion on
every PR through #282 without cancelling itself.

Mechanically checked after: zero table rows name a closed issue, every open
issue appears somewhere, and #284 is listed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Fix the backfill arithmetic and a bold span I broke (#283)

"1 of 220" was wrong in the direction that matters. cellulolysis.yaml has ZERO
rows in conf/causal_graph_audit_baseline.tsv — it was repaired before the
baseline was frozen — so the 220 there are all still to do, and "1 of 220" read
as 219 remaining, contradicting the table twenty lines below.

That also explains a discrepancy the file could not previously account for:
#183's 2026-07-30 measurement says 220 fragmented graphs and today's baseline
says 220, with one file fixed in between. Both are right because the 2026-07-30
figure was already post-cellulolysis. The corpus was 221. Now stated where the
measurement is quoted, so the next reader does not have to re-derive it.

The previous commit also dropped an opening ** while rewriting the paragraph
above it, leaving an unmatched closer that GFM renders literally. Checked the
whole file for the same shape rather than just that line — every other bold span
is balanced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Revert my own retraction: the dumbbell_shaped example was right (#283)

Round 4 "corrected" section 5 by retracting the dumbbell_shaped worked example
as fabricated, and filed #284 against audit_causal_graphs.py's comment on the
same reasoning. Both were wrong, and the repo had written proof I did not read.

v_shaped_daughters WAS typed TRAIT. Both components of
dumbbell_shaped_snapping_division were therefore reachable-from-a-trait, the
graph reported clean, and #227 retyped that node to QUALITY in the same change
that added FRAGMENTED_GRAPH. Recorded in
history/records/dumbbell_shaped/2026-08-03T230903Z-claude-code-90a277.yaml
(linking issues: #220, prs: #227) and in the record's own curation_history.

So the 7 UNREACHABLE_FROM_TRAIT rows I cited as disproof are the fix working.
The example was accurate for the data as it stood; it is historical now, not
invented. Section 5 says that, and says to read history/ alongside the record,
because the current node types alone tell the opposite story.

#284 is closed with the correction rather than left as a wrong record.

The failure was checking the current corpus and stopping. history/ exists to
explain why present data looks as it does, and I did not open it — which is
also why the "no live instance" sweep is now stated as #227 having removed the
one instance, rather than as the check guarding against nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
realmarcin added a commit that referenced this pull request Aug 5, 2026
… (#290)

* Key the snippet baseline by occurrence count, not by array index (#270)

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>

* Ratchet REUSED_SNIPPET's magnitude as a value, not as identity (#291)

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>

* Make the baseline a value, and per-snippet rather than per-graph (#291)

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>

* Document the magnitude key's cost (#292)

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>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
realmarcin added a commit that referenced this pull request Aug 5, 2026
… (#294)

* Backfill 3 evidence-backed edges, connecting 2 fragmented graphs (#183)

The detection half of #183 shipped in #185/#227; this is the first increment of
the content half, and establishes the method.

metabolism/dissimilatory_iron_reduction: the Fe(III)-pool pair
(dissolved_fe3_om_complex -> solid_fe3_mineral) sat with no path to the trait —
a free-standing geochemical rate comparison beside a respiration graph. Both are
pools of the acceptor the organism reduces, so dir_trait now uses each as an
electron acceptor, grounded METPO:2000008 to match the 13 existing
uses-electron-donor edges of the same TRAIT -> CHEMICAL shape.

morphology/sulfur_globule: persulfide_dioxygenase -> sulfite was stranded as a
correct enzymatic fact with nothing tying it to globule formation. PDO acts
downstream of SQR in the same sulfide-oxidation pathway the rest of the graph
models, so it now participates in oxidative_sulfur_metabolism.

THE SNIPPETS ARE THE POINT. #247 established that a research report is not a
snippet source — its evidence text paraphrases, so what it quotes is unknowable
without opening the paper — and #267 made that enforceable, which means the
backfill cannot take the shortcut. Each snippet here was pulled from the PubMed
abstract via efetch and checked character-for-character against it in the same
run: PMID:15518832 for both iron edges, PMID:35653932 for the sulfur edge.

That constraint is load-bearing rather than ceremonial. An edge added with a
reference and no snippet is schema-valid and is what 63% of existing evidence
items do — but it creates a new locator, so audit-snippets sees a new
MISSING_SNIPPET and qc exits 1. The ratchet makes the honest path the only path.

  FRAGMENTED_GRAPH        220 -> 218
  UNREACHABLE_FROM_TRAIT  1321 -> 1317
  audit-snippets          0 new findings
  backfill                1 -> 3 of 221

Baseline regenerated so the ratchet tightens rather than tolerating the old
count. pages/ regenerated: the stamp derives from the corpus, so appending
curation history rebuilds all 489.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Narrow two claims to what their quotes establish, and record both edits (#297, #296)

Review found that both snippets support their edges less directly than the
surrounding prose claimed. Both edges stand; the wording did not.

The dissolved Fe(III) quote is about chelators the organism SECRETES, and its
second half describes reduction of oxides not in contact with the cell — a
different mechanism from "chelated Fe(III) is an acceptor pool", while the node
is ambient Fe(III)-organic-matter complex. The description no longer says
"chelator-solubilised", and the notes now say the quote supports the pool's
existence rather than its origin.

The PDO quote covers heterotrophic bacteria and human mitochondria, not the
Thiomargarita/Beggiatoa exemplars of this trait. It supports the generic pathway
claim its object node makes, and the notes now say so and no more.

Both caveats are written into the record rather than into a commit message,
because that is the failure mode #183's backfill will most easily repeat at
scale: a true quote attached to a slightly wider claim. Verbatim-ness does not
protect against it — it is exactly what verbatim-ness cannot see. Tracked in
#297.

Also added the history/ records the review flagged as missing. history/README's
own worked example is this issue, and the one existing record is what let the
#282 review catch a false retraction the current data alone could not explain.
Written by hand because `just new-history` requires a claw checkout that is not
always present — tracked in #296.

353 tests pass, `just qc` exits 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Report evidence snippets paraphrase, but EvidenceItem.snippet requires a verbatim quote

1 participant