Backfill 3 evidence-backed edges, connecting 2 fragmented graphs (#183) - #294
Conversation
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>
|
Claude finished @realmarcin's task in 3m 29s —— View job Review complete — approved
Everything I can check locally is correct and internally consistent. No 🔴, no 🟡. Grounding is right, including the one that reads wrong in the PR body. The description says the acceptor edges are grounded " The connectivity claim holds. 🔵 Suggestions (optional)
AdvisoryNo record under What I could not verifyI have no network egress in this job — One detail argues for the claim: Scope discipline is right: 3 of 221, framed as a method canary, with the ratchet re-frozen tighter. |
There was a problem hiding this comment.
Review — approve
Everything I can check locally is correct and internally consistent. No 🔴, no 🟡.
Grounding is right, including the one that reads wrong in the PR body. The description says the acceptor edges are grounded "METPO:2000008 to match the 13 existing uses electron donor edges" — which sounds like a donor/acceptor inversion. It isn't: mappings/predicate_grounding.tsv:6 maps uses electron acceptor → METPO:2000008 "uses as electron acceptor" (skos:exactMatch), and the 13 donor edges use METPO:2000009. What is being borrowed is the TRAIT → CHEMICAL shape, not the CURIE. biolink:participates_in likewise matches predicate_grounding.tsv:16 and the biolink:located_in already in that graph.
The connectivity claim holds. audit_causal_graphs.py computes reachability undirected, so persulfide_dioxygenase → oxidative_sulfur_metabolism does reattach that branch even though the trait node is a sink. Two independent signals say the audit was actually re-run rather than the baseline hand-edited: conf/causal_graph_audit_baseline.tsv and reports/causal_graph_audit.tsv drop the same 6 rows, and the baseline diff is +0/-6 — it strictly tightens, no new findings smuggled in.
🔵 Suggestions (optional)
-
data/traits/metabolism/dissimilatory_iron_reduction.yaml:148— the node isdissolved Fe(III)-organic-matter complex(ambient OM-complexed Fe), but the snippet is about microbially produced chelators and electron shuttles. Those are adjacent, not identical, and the shuttle half of the quote describes reduction of oxides not in contact — a different mechanism from "chelated Fe(III) is the acceptor pool". The edge is defensible; thedescriptionoverstates how directly the quote lands. -
data/traits/morphology/sulfur_globule.yaml:135— the quote's scope is heterotrophic bacteria and human mitochondria, while this trait's canonical examples are Thiomargarita / Beggiatoa. It supports the genericPDO participates in oxidative sulfur metabolismclaim fine, since that node is generic, but it is not evidence about globule-forming organisms. -
METPO:2000008/:2000009are declared with an organism domain (docs/CURATION_PLAYBOOK.md, "Chemical-use relations"), yet the subject here is a TRAIT node. That is exactly what the 13 existing donor edges already do, so this PR is consistent with practice — but it is a corpus-wide modelling question worth its own issue rather than something to fix here.
Advisory
No record under history/ for either edit. curation_history in both YAMLs is thorough, so nothing is lost, but history/README.md's own worked example is literally --issue .../183 --summary "Connect fragmented cellulolysis causal graph" — this is the case it was written for.
What I could not verify
I have no network egress in this job, so I could not re-fetch PMID:15518832 or PMID:35653932 and check the three snippets character-for-character. Given that verbatim-ness is the PR's central claim, that gap is worth naming rather than glossing.
One detail argues for the claim: sulfide: quinone oxidoreductase with a space after the colon looks like a typo, but it is the house style of the group that publishes the SQR/PDO sulfide-oxidation work, and a curator paraphrasing or tidying would have normalised it to sulfide:quinone. Preserving an oddity is the fingerprint of a real copy. That is an inference, not a check — a reviewer with network access should still diff all three against efetch output.
Scope discipline is right: 3 of 221, framed as a method canary, with the ratchet re-frozen tighter. Approving.
…ts (#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>
|
Claude finished @realmarcin's task in 5m 5s —— View job Re-review after
|
There was a problem hiding this comment.
Re-review after 0370ee76 — approve
The second commit does what the prior review's 🔵 1 and 🔵 2 asked, and does it in the right place. Both edges stand; only the prose narrowed. 🔵 3 (organism-domain METPO terms on a TRAIT subject) is correctly deferred to its own issue rather than patched here.
Two things I could newly verify this run that the last one couldn't:
- The audit was genuinely re-run.
just audit-graphsreproducesreports/causal_graph_audit.tsvbyte-for-byte —git statusis clean after — and reportsbaselined: 1535, new: 0. The 218/1317 in the PR body is real, not transcribed. - Both history records are schema-valid.
just validate-historypasses over all three records. Required fields present,session.idequals each filename stem, and thedetailspattern that rejects the scaffolder placeholder is satisfied.
Putting the scope caveats in evidence[].notes rather than only in the commit message is the right call. A commit message is not reachable from the record, and the failure mode being guarded against — a true quote attached to a slightly wider claim — is invisible to audit-snippets by construction, so prose in the record is the only place it can live.
🔵 Suggestion (optional)
session.timestamp: '2026-08-05T00:00:00Z' in both new records is a midnight placeholder; the commit is dated 2026-08-05T06:05Z. The schema calls that field "ISO-8601 UTC start time", and the scaffolded dumbbell_shaped record carries a real one (23:09:03Z). Same for the 183a01/183a02 shortids, which are sequential where the schema's stated rationale wants them unguessable — the collision argument is about two agents on one slug, so nothing breaks here, but a future hand-written 183a03 for the same slug is exactly the case the random id was meant to rule out. Both follow from hand-writing rather than scaffolding, already tracked in #296; worth naming only as the concrete cost of that gap.
What I could not verify
No network egress again — curl and WebFetch are both denied in this job, so I could not efetch PMID:15518832 or PMID:35653932 and diff the three snippets character-for-character. Since verbatim-ness is this PR's central claim, that remains unchecked by a reviewer, and I would rather name it twice than let it read as settled.
What I can say from local evidence: neither PMID appears anywhere else in the repo, including in either trait's own research report — so there is no ECHOES_RESEARCH_REPORT exposure and no in-repo text the snippets could have been lifted from. Both quotes also read as the genuine article on content grounds (the Geobacter-versus-Shewanella/Geothrix contrast is the structural signature of the Lovley 2004 Adv Microb Physiol abstract). That is corroboration, not verification. A reviewer with network access should still run the diff.
CI is green across qc, validate-strict, pr-sanity, vendored-sync, and Curation history. No 🔴, no 🟡. Approving.
* Let new-history write a record without a claw checkout (#296) `just new-history` was gated on _require-claw, so on a machine without a culturebotai-claw checkout no record got written at all. Nothing about writing one actually needs claw: the schema is already vendored at src/traitmech/schema/history.yaml, which is what `just validate-history` and CI check against. Claw stays PREFERRED — the recipe tries it first, so the canonical scaffolder keeps producing the canonical shape across the four Mech repos — and scripts/new_history_record.py fills the gap. A correction worth recording, since it is why this issue exists. history/README asserted that "anyone writing curation records has claw checked out". That is an assumption, not a guarantee: it fails for a fresh clone, for CI (claw is private), and for a contributor outside the fleet. It also did not hold in practice for a duller reason — claw IS present on this machine, and #294's two records were still hand-written, because the recipe was gated and assuming the gate would fail was cheaper than testing it. A fallback removes the incentive. The interface is claw's exactly, and running both is how that got fixed rather than asserted. The first version defaulted --kind, renamed --actor-name to --tool and required --details; invoking the recipe on a machine that HAS claw surfaced all three at once. A fallback taking different arguments is the same trap in a new form. Parity is now byte-identical apart from the id's hash suffix and one deliberate divergence: a bare `--issue 296` is expanded to a full URL, because the schema declares those `range: uri` and the pre-existing committed record carries URLs, while claw passes the string through. Documented as a divergence, and the README carries a runnable parity check rather than a claim. Also fixed a crash the parity harness found: --history-root outside the repo made the final relative_to() raise after the record was already written. 367 tests pass (14 new), `just qc` exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Fix three defects the parity harness could not see (#296) Review found three, and the common thread is that the README's parity check could not reach any of them: it pinned --history-root and exercised only --kind record with --details supplied. Widened accordingly. The kind directory was hardcoded to records/. The layout is history/<kind-dir>/<slug>/ — history/infrastructure/curation-history/ is a live example — so `--kind infrastructure` wrote to history/records/ under the fallback and history/infrastructure/ under claw. The schema does not constrain the path, so the misplaced record validated clean and nothing noticed. This PR is itself an infrastructure change, so a record scaffolded for it would have gone to the wrong place. The default --details placeholder evaded the guard built to catch it. The vendored schema carries `pattern: '^(?!TODO: replace this placeholder)'` and history/README promises an unfilled record cannot slip through. The fallback wrote "TODO: fill in the substance of this change." — a different string the negative lookahead permits — so an unfilled record was committable. The near-miss was load-bearing rather than careless: emitting claw's real string would have made validate() delete the file. Now emits claw's placeholder byte-for-byte and skips validation only when --details was omitted, which is the write-then-edit workflow claw implements; the record then fails validate-history exactly as documented, pinned by a test that asserts the failure. --force plus a validation failure destroyed the record it was correcting: the script wrote the target, validated, then unlinked on failure, in a directory whose whole policy is append-only. Now writes a scratch file, validates that, and moves it into place. Canaried by forcing a rewrite that fails validation — the original comes back byte-identical. The scratch file keeps a .yaml suffix, because linkml-validate picks its loader from the extension and refuses a .tmp outright. 372 tests pass (5 new), `just qc` exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Order sections as the schema declares, and verify KIND_DIRS instead of guessing (#296) Four review items, and the second is the one worth reading. `sections` was appended after `details`, while the schema declares it between `outcome` and `summary` and the one committed record carrying it agrees. That put the DOCUMENTED invocation on the divergent path — history/README's headline example passes `--sections causal_graphs,grounding` — and the parity harness could not see it, because the harness did not pass `--sections`. It does now, and the diff is again only the id hash and the deliberate links expansion. KIND_DIRS was six values of which four were inferred, unevenly: mappings and reports pluralised, schema and other not. That asymmetry was the tell. Verified two ways rather than argued: the table is byte-identical to claw's kg_microbe_history/scaffold.py, and scaffolding all six kinds through claw produces exactly those six directories. Cited in the comment, and the key check now parses permissible_values out of the vendored schema instead of comparing two hand-copied literals — the old test could not have noticed the schema growing a kind. With --details omitted, validation was skipped ENTIRELY rather than just for the placeholder, so `--timestamp nonsense` wrote a record and exited 0. Now validates a copy with `details` substituted, keeping every other check. Writing that test immediately found a scratch file leaking on the failure path, so the write-validate-move is wrapped in try/finally. history/README said every kind but record/schema *needs* an explicit --path. Neither scaffolder enforces that — both derive <target-root>/<slug>.yaml for any kind — so it now says what happens if you rely on it: a target path that does not exist. 375 tests pass (3 new), `just qc` exits 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Two independent reviews converged on the same defect: RO:0002327 `enables` has `range: biological process or activity` (data/raw/biolink-model.yaml:5099), so pointing it at a TRAIT node trades the METPO domain violation for an RO range violation. Verified — the claim is correct. It is not, however, introduced here: 149 `enables` edges already had TRAIT objects on main, and this PR took that to 164. Gating the `enables` row would block all 164 and fail `just qc`, so the range question is filed as #302 (with #303 for the donor/acceptor distinction these 15 edges collapsed) rather than solved in a PR about 15 edges. What changes here is the over-claim: - CURATION_PLAYBOOK.md no longer prescribes `<chemical> enables <trait>` as the general remedy for all 66 predicates in the family. That block would have been the reference for #301's 366 edges and the #183 backfill. It now states the range caveat, points at #302/#303, and recommends the range-correct form available today — chemical to the graph's *process* node. - The `blocked_by_node_type` claim is future-tense: the reversal removed every edge carrying those labels, so no residual row exists to go looking for. Also from review: - 14 trait files get a REVERSE_CAUSAL_EDGE_DIRECTION curation_history event; they were rewritten with no provenance. The dissimilatory_iron_reduction event explicitly supersedes the #294 entry that still describes those two edges in their old direction and grounding — added as a correcting event rather than editing history in place. - load_mapping now fails if CausalNodeTypeEnum ever defines NONE, which would let the sentinel silently shadow a real node type — the failure class the surrounding code deliberately makes fatal. Test added. - conf/evidence_snippet_baseline.tsv restored to canonical _key sort order; it was re-keyed in place, which would have produced spurious diff noise on the next --write-baseline. Still exactly 3 rows changed vs main. just qc exit 0; 380 tests pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
First increment of #183's content half, and the establishment of a repeatable method. The detection half shipped in #185/#227; what remained was 220 fragmented graphs and one worked example.
What was disconnected, and why these edges
metabolism/dissimilatory_iron_reduction— the pairdissolved_fe3_om_complex → solid_fe3_mineralhad no path to the trait. It was a free-standing geochemical rate comparison sitting beside a respiration graph. Both are pools of the very acceptor the organism reduces, sodir_traitnow uses each as an electron acceptor, groundedMETPO:2000008to match the 13 existinguses electron donoredges of the same TRAIT → CHEMICAL shape.morphology/sulfur_globule—persulfide_dioxygenase → sulfitewas stranded: 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 nowparticipates inoxidative_sulfur_metabolism.The snippets are the point
#247 established that a deep-research report is not a snippet source — its evidence text paraphrases, so which of quote-or-paraphrase you are copying is unknowable without opening the paper. #267 made that enforceable. So this backfill could not take the obvious shortcut.
Each snippet was pulled from the PubMed abstract via
efetchand checked character-for-character against it in the same run:dir_trait → solid_fe3_mineralPMID:15518832dir_trait → dissolved_fe3_om_complexPMID:15518832persulfide_dioxygenase → oxidative_sulfur_metabolismPMID:35653932That constraint turns out to be load-bearing rather than ceremonial. An edge added with a reference and no snippet is schema-valid, and is exactly what 63% of existing evidence items do — but a new edge is a new locator, so
audit-snippetssees a newMISSING_SNIPPETandqcexits 1. The ratchet from #267 makes the honest path the only path. That interaction was predicted in #247 and this is the first time it has actually bitten.Numbers
The causal-graph baseline is regenerated so the ratchet tightens rather than tolerating the old count — per #183's own instruction to re-freeze after each batch.
pages/is regenerated because the page stamp derives from the corpus (#228), so appending curation history rebuilds all 489.Scope, stated plainly
3 of 221. This is deliberately small: it is the canary for a method, not the campaign. What it demonstrates is that a fragmented graph can be repaired with genuinely verbatim evidence at roughly one PubMed lookup per edge, and that the two ratchets (
audit-graphs,audit-snippets) between them make a lazy repair impossible to land. Scaling it is per-trait research work and wants its own sequence of PRs.🤖 Generated with Claude Code