Skip to content

Causal-graph connectivity: detect graphs that are islands, and connect cellulolysis - #185

Merged
realmarcin merged 4 commits into
mainfrom
fix/causal-graph-connectivity
Jul 31, 2026
Merged

Causal-graph connectivity: detect graphs that are islands, and connect cellulolysis#185
realmarcin merged 4 commits into
mainfrom
fix/causal-graph-connectivity

Conversation

@realmarcin

Copy link
Copy Markdown
Contributor

Summary

audit-graphs could not tell a causal graph from a pile of disconnected fragments. It flags DANGLING_EDGE (an edge naming a node that does not exist) and ORPHAN_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:

Defect Meaning Severity
UNREACHABLE_FROM_TRAIT node has edges, but sits in an island with no undirected path to any TRAIT node WARN
NO_TRAIT_NODE graph has 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".

It ships as a ratchet, not a gate. audit-graphs gates just qc in 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. Instead conf/causal_graph_audit_baseline.tsv freezes today's findings and --fail-on defaults to new: 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 burndown

Verified 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.yaml was 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 retypes cellulosome CELLULAR_LOCALIZATIONGENE_OR_PROTEIN — it is a protein complex, not a location, and the schema's GENE_OR_PROTEIN explicitly covers complexes. GO:0043263 was 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 qc exits 0
  • 145 tests pass; 6 new tests cover the island case, NO_TRAIT_NODE, undirected reachability, non-double-reporting against ORPHAN_NODE, baseline suppression, and severity assignment
  • the 3 pre-existing audit tests are unchanged and still pass
  • each commit is independently green, so history bisects cleanly

Notes for review

  • The TSV gains a severity column. Nothing in the repo parses that file, so no consumer needed updating.
  • Commit order matters: the baseline was generated after the cellulolysis fix and correctly excludes it. Landing these out of order would freeze the old fragmentation into the baseline.
  • NEXT_TASKS.md item 5 is reconciled in ff1da9ea.

🤖 Generated with Claude Code

realmarcin and others added 2 commits July 30, 2026 23:27
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>
@realmarcin

Copy link
Copy Markdown
Contributor Author

Review

Self-review of #185. Three commits: a trait fix, the audit change, and a NEXT_TASKS.md reconciliation.

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

  • Undirected reachability is the right invariant. A directed walk would flag cellulase -enables-> trait graphs that are correctly modelled. The test test_reachability_is_undirected locks this in, which is exactly the assumption most likely to get "fixed" by a future contributor who hasn't hit the case.
  • Not double-reporting ORPHAN_NODE as UNREACHABLE_FROM_TRAIT is handled deliberately (nid not in referenced guard) and tested. Without it every zero-edge node would produce two findings.
  • Dangling edges don't build adjacency. A phantom node can't fabricate a path between two real islands. Subtle and correct.
  • The trait fix is genuinely evidence-backed. Every new edge has a citation plus a verbatim snippet, and every predicate reuses an existing grounding from mappings/predicate_grounding.tsv — no new vocabulary smuggled in.

Blocking

1. reports/causal_graph_audit.tsv is byte-identical to conf/causal_graph_audit_baseline.tsv.

Both blobs hash to 6d593a9b…. We are committing the same 1315-line generated file twice, and because the report is regenerated on every run, just qc now dirties the working tree every time it is run locally. Before this PR the report was a 1-line header, so the churn was invisible.

Pick one:

  • gitignore reports/causal_graph_audit.tsv (it is a build artifact; the baseline is the reviewed input), or
  • keep committing it and accept a 1315-line diff on every corpus change.

I lean toward the first. The baseline is the file that deserves review; the report is output.

2. Commit ff1da9ea has a misleading scope.

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 audit(). Nothing tests main()'s exit codes across --fail-on new|error|any. This matters because the first cut of this PR shipped with --fail-on defaulting to error, which meant new WARN findings were reported and then ignored — the ratchet silently did not ratchet. It was caught by running it by hand. A test asserting "clean → 0, injected island → 1" would have caught it, and would stop a future default change from quietly disarming the gate.

4. _key() collapses every DANGLING_EDGE in a graph to one baseline key. The node fragment is only extracted when detail starts with node_id=; dangling-edge details start with subject=/object=, so they all key to ("file", "graph", "DANGLING_EDGE", ""). Baselining one dangling edge would suppress every other dangling edge in that same graph. Impact is currently zero (there are none), but it is a latent hole in an ERROR-severity check.

5. --write-baseline will happily freeze ERROR-severity defects. It writes every finding, so running it while a real DANGLING_EDGE or NO_TRAIT_NODE exists permanently suppresses a genuine error. It should refuse, or baseline only WARN-severity findings, or at minimum print a loud warning.

6. The 1314 backfill. Tracked as NEXT_TASKS.md item 5.2; filing an issue so it is visible outside that file. This PR fixes 1 of 220 traits.


Notes, not defects

  • Commit order matters: the baseline was generated after the cellulolysis fix and correctly excludes it. Landing these out of order would freeze the old fragmentation in.
  • The baseline keys on file path, so moving or renaming a trait file un-suppresses all of its findings. Correct-by-default (it forces a re-look) but worth knowing.
  • Splitting cellobiohydrolase by chain end stays blocked on EC being absent from the LinkML prefix map.

realmarcin and others added 2 commits July 30, 2026 23:37
…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>
@realmarcin
realmarcin force-pushed the fix/causal-graph-connectivity branch from ff1da9e to 6a84c13 Compare July 31, 2026 06:37
@realmarcin

Copy link
Copy Markdown
Contributor Author

Review follow-up: one finding retracted, everything else addressed

Retracted — blocking item 1 was wrong

I flagged committing reports/causal_graph_audit.tsv as a problem because it is byte-identical to the new baseline and dirties the tree on every just qc. I checked the repo convention before acting on it, and the convention says otherwise: reports/graph_completeness_audit.tsv is tracked at 354 lines, and every sibling audit commits its report the same way. Gitignoring this one would have made it the odd one out.

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

ff1da9ea3b548def, reworded (force-push, branch is unmerged and solely mine). The old message claimed only "reconcile item 5" while carrying 140 insertions across sections 1, 2, 4, 5 and 6. The new message states plainly which part is mine and which is a concurrent session's unrelated working-tree sweep (vendored-sync/claw topology, METPO upstream, web design #151), so anyone auditing the commit knows those sections arrived unreviewed by me rather than as part of this change.

Fixed — the three follow-ups, in 6a84c13e

I filed these as #186/#187/#188 and then decided not to merge known defects in the very check this PR introduces, so they are fixed here and closed by the commit:

Left open

#189 — the 1314-node backfill across 219 graphs. Real content work, one trait at a time with sources; not something to sweep into this PR.

State

152 tests (up from 145), just qc exits 0, baseline byte-unchanged, working tree clean.

@realmarcin
realmarcin merged commit bc39812 into main Jul 31, 2026
3 checks passed
@realmarcin
realmarcin deleted the fix/causal-graph-connectivity branch July 31, 2026 06:39
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.

1 participant