Causal-graph connectivity: detect graphs that are islands, and connect cellulolysis - #185
Conversation
Deep-research audit (Edison/PaperQA3 + Codex, independently) found data/traits/metabolism/cellulolysis.yaml split into four disconnected components, with 9 of its 14 nodes unreachable from the trait node. The cellulose substrate node never reached the trait, and the cellulosome and carbon-catabolite-repression clusters were fully orphaned. The graph described fourteen facts rather than one mechanism. Add 7 evidence-backed edges to merge it into a single connected graph (14 nodes, 10 -> 17 edges, all nodes now reachable from the trait): cellulose -is hydrolyzed to-> cellobiose cellobiohydrolase -produces-> cellobiose endoglucanase/cellobiohydrolase/beta_glucosidase -part of-> cellulase cellulosome -enables-> cellulolysis_trait cellulolytic_genes -encodes-> cellulase Every edge carries edge-level evidence with a verbatim snippet, and every predicate reuses a grounding already in mappings/predicate_grounding.tsv (METPO:2000013, METPO:2000202, biolink:part_of, RO:0002327, biolink:encodes) — no new vocabulary. Also ground cellobiose (CHEBI:17057, previously label-only) and retype cellulosome CELLULAR_LOCALIZATION -> GENE_OR_PROTEIN: it is a protein complex, not a location, and the schema's GENE_OR_PROTEIN explicitly covers complexes. The GO:0043263 grounding was already correct and is unchanged. All nine existing CURIEs were re-resolved against OLS and all five existing citations against Crossref; all were live and correctly used. Deferred to follow-up (recorded in curation_history): LPMO/CBM/transport/ phosphorolysis branches, splitting cellobiohydrolase by chain end (blocked — EC is absent from the LinkML prefix map), and replacing the generic CCR edge with taxon-specific C. cellulolyticum cip-cel evidence. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
audit-graphs only flagged DANGLING_EDGE and ORPHAN_NODE. ORPHAN_NODE
catches a node with zero edges, so a well-connected cluster that never
reaches the trait passed clean — a graph could be four separate islands
and report no findings. That is the failure mode of bulk enrichment
passes, which append a cluster of nodes and edges without wiring them
into the existing cascade.
Add two defects:
UNREACHABLE_FROM_TRAIT a node with edges, but in an island with no
undirected path to any TRAIT node. [WARN]
NO_TRAIT_NODE no TRAIT node to anchor reachability. [ERROR]
Reachability is deliberately undirected. Curated predicates legitimately
mix direction (cellulase -enables-> trait points inward, trait -produces->
glucose points outward), so a directed walk would flag correctly-modelled
graphs. The question is "is this one graph or several", not "does
causality flow one way".
The check finds 1314 unreachable nodes across 219 of 353 graphs (32% of
all nodes). Since audit-graphs gates `just qc` in CI, landing this as a
blocking error would break the build on day one and the realistic response
would be to switch it off. So it ships as a ratchet instead: --write-baseline
freezes today's findings into conf/causal_graph_audit_baseline.tsv, and
--fail-on defaults to "new" — anything not in the baseline fails, while
baselined findings never do. The corpus cannot get more fragmented than it
is today, but today's backlog does not block. --fail-on error is the looser
report-only mode; --fail-on any ignores the baseline entirely, for after the
burndown.
The TSV gains a `severity` column; nothing parses that file, so no consumer
needed updating.
Verified in both directions on the real corpus: clean exits 0, an injected
two-node island exits 1 and names both nodes. Adds six tests covering the
island case, NO_TRAIT_NODE, undirected reachability, non-double-reporting
against ORPHAN_NODE, baseline suppression, and severity assignment. The
three pre-existing tests are unchanged and still pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ReviewSelf-review of #185. Three commits: a trait fix, the audit change, and a Overall: the detection logic is sound and well-tested, and the ratchet design is the right call for a check that starts with 1314 findings. But there are two packaging problems that should be fixed before merge, and four follow-ups worth filing. What works
Blocking1. Both blobs hash to Pick one:
I lean toward the first. The baseline is the file that deserves review; the report is output. 2. Commit Its message says "Reconcile NEXT_TASKS.md item 5 against what shipped", and item 5 is genuinely mine. But the commit carries 140 insertions across sections 1, 2, 4, 5 and 6 — the bulk of it an unrelated reconciliation (vendored-sync/claw topology, METPO upstream status, the web-design review) authored by a different session that had left it uncommitted in the working tree. Only a small part of that commit is what the message describes. That content looks accurate and worth keeping, but it should not land under a message that describes a different change. The message needs to disclose the full scope. Follow-ups (non-blocking, filing separately)3. The ratchet itself has no test. All six new tests exercise 4. 5. 6. The 1314 backfill. Tracked as Notes, not defects
|
…ee sweep Two things land together here, and the second is not mine. Mine — section 5 (causal-graph connectivity): item 1 (add a connectivity defect to audit-graphs) has now shipped, so mark it done and record the two decisions it left open — the invariant is "every node reachable from the trait node", and reachability is undirected. Note that it landed as a ratchet rather than a plain non-blocking warning, and that the cellulolysis fix is committed rather than uncommitted, leaving the backfill at 1 of 220 traits. Records that splitting cellobiohydrolase by chain end is blocked on EC being absent from the LinkML prefix map. Not mine — the rest of the file. A concurrent session had left an unrelated reconciliation uncommitted in the working tree: the header date, section 2 (vendored-sync / claw topology, the abandoned CultureMech->claw relocation), section 4 (METPO upstream status), and section 6 (web design review #151). That work is accurate and worth keeping, so it is carried here rather than discarded, but it was authored elsewhere and is unreviewed by me. Anyone auditing this commit should treat those sections as a separate change that happened to share a working tree. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Addresses three review findings on #185, all in the check this PR adds. Closes #186 — the ratchet had no test. Every test covered audit(), the pure defect finder; nothing covered the blocking decision. That is precisely where this PR's first cut was broken: --fail-on defaulted to "error", so a new WARN island was reported and then ignored, and the ratchet silently did not ratchet. Extract that decision into partition(findings, baseline, fail_on) and test all three modes, including the case that regressed. Closes #187 — _key() only extracted a discriminator when detail started with "node_id=", so every DANGLING_EDGE in a graph collapsed onto one baseline key and freezing one would suppress the rest. Take the leading fragment of detail whatever its shape. Existing UNREACHABLE_FROM_TRAIT keys are unchanged, so the committed baseline still matches all 1314 rows. Closes #188 — --write-baseline wrote every finding, so running it while a DANGLING_EDGE or NO_TRAIT_NODE existed froze a real ERROR and kept the gate green forever after. It now refuses, names the offenders, and exits 1; on success it writes only WARN-severity rows. The baseline parks the known backlog, it is not a suppression channel for structural defects. 152 tests pass, just qc exits 0, and the committed baseline is byte-unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ff1da9e to
6a84c13
Compare
Review follow-up: one finding retracted, everything else addressedRetracted — blocking item 1 was wrongI flagged committing The byte-identical hash is also coincidental rather than structural — the baseline is a frozen snapshot of the report at the moment it was written, and the two diverge as soon as any trait changes. No change made. My read was wrong, not the repo's. Fixed — blocking item 2
Fixed — the three follow-ups, in
|
Summary
audit-graphscould not tell a causal graph from a pile of disconnected fragments. It flagsDANGLING_EDGE(an edge naming a node that does not exist) andORPHAN_NODE(a node no edge references), but neither fires when every node has some edge and the graph still splits into mutually unreachable islands. A graph in four pieces reported zero findings.That is not hypothetical. Across the corpus, 1314 nodes in 219 of 353 graphs (32% of all nodes) cannot be reached from their own trait node, and the audit called all of it clean.
This PR adds the detection, and fixes the first trait.
1. Detection (
b88effb7)Two new defects:
UNREACHABLE_FROM_TRAITTRAITnodeNO_TRAIT_NODETRAITnode to anchor reachabilityReachability is deliberately undirected. Curated predicates legitimately mix direction —
cellulase -enables-> traitpoints inward,trait -produces-> glucosepoints outward — so a directed walk would flag correctly-modelled graphs. The question is "is this one graph or several", not "does causality flow one way".It ships as a ratchet, not a gate.
audit-graphsgatesjust qcin CI, so landing 1314 findings as blocking errors would break the build on day one and the realistic response would be to switch the check off. Insteadconf/causal_graph_audit_baseline.tsvfreezes today's findings and--fail-ondefaults tonew: anything not in the baseline fails, baselined findings never do. The corpus cannot get more fragmented than it is today, while the existing backlog does not block anyone.--fail-on error— looser, report-only for new fragmentation--fail-on any— ignores the baseline entirely, for after the burndownVerified in both directions on the real corpus: clean exits 0; an injected two-node island exits 1 and names both nodes.
2. First trait fixed (
fd6ce791)data/traits/metabolism/cellulolysis.yamlwas split into four components with 9 of its 14 nodes unreachable from the trait. The cellulose substrate node never reached the trait; the cellulosome and catabolite-repression clusters were fully orphaned. Seven evidence-backed edges merge it into one graph (10 → 17 edges, all 14 nodes reachable).Also grounds
cellobiose(CHEBI:17057, previously label-only) and retypescellulosomeCELLULAR_LOCALIZATION→GENE_OR_PROTEIN— it is a protein complex, not a location, and the schema'sGENE_OR_PROTEINexplicitly covers complexes.GO:0043263was already correct and is unchanged.Every new edge carries edge-level evidence with a verbatim snippet, and every predicate reuses a grounding already in
mappings/predicate_grounding.tsv— no new vocabulary.Provenance
The cellulolysis defects were found by two independent deep-research passes (Edison/PaperQA3 and Codex) that converged on the same conclusions. All nine pre-existing CURIEs were re-resolved against OLS and all five pre-existing citations against Crossref; every one was live and correctly used, so the graph's problem was structural, not factual.
Testing
just qcexits 0NO_TRAIT_NODE, undirected reachability, non-double-reporting againstORPHAN_NODE, baseline suppression, and severity assignmentNotes for review
severitycolumn. Nothing in the repo parses that file, so no consumer needed updating.NEXT_TASKS.mditem 5 is reconciled inff1da9ea.🤖 Generated with Claude Code