Skip to content

Causal graphs are 62% fragmented and audit-graphs cannot see it #183

Description

@realmarcin

scripts/audit_causal_graphs.py reports DANGLING_EDGE (an edge naming a node that does not exist) and ORPHAN_NODE (a node no edge references at all). Neither fires when every node has at least one edge but the graph still splits into mutually unreachable components — which is the common case here, because a node picks up an edge to a neighbour long before it is wired back toward the trait node.

So the audit-graphs gate is green while most of the corpus is fragmented.

Measured

Over the full corpus on 2026-07-30 — 353 causal graphs, 4,136 nodes, treating edges as undirected for the connectivity test:

  • 220 graphs (62%) have more than one connected component.
  • 1,264 nodes (30%) sit outside their graph's largest component.

Worst cases, all with 12 nodes stranded outside the main body:

trait nodes components
physiology/lithotrophic.yaml 19 7
environment/temperature_range_very_low.yaml 15 7
environment/ph_delta_mid2.yaml 15 7
morphology/black_pigmented.yaml 18 6
environment/temperature_range_mid4.yaml 17 6
environment/temperature_delta_high.yaml 15 6

The generated environment/physiology traits dominate the tail.

This is a content gap — missing edges — not a schema defect. That is precisely why nothing currently reports it.

Two independent pieces of work

1. Add a connectivity defect to the audit. A FRAGMENTED_GRAPH and/or UNREACHABLE_FROM_TRAIT check alongside the existing two, reported in reports/causal_graph_audit.tsv.

It must land non-blocking first. At 62% it would fail just qc on day one, so shipping it as a hard gate is not an option until the backfill is well advanced.

A design decision to make explicitly: is the invariant "the graph has exactly one component", or the stronger "every node is reachable from the trait node"? The second is what actually matters for interpretation — a well-connected subgraph that never links back to the trait is still useless — but it is also the harder bar.

2. Backfill the missing edges, per trait, with real evidence. This is deep-research work (research-causal-graphs / deep-research-trait), not something to mass-generate: every added edge needs a reference and a snippet matching the existing ones. Mass-generating plausible edges would defeat the point of the graphs.

Worked example already in hand

data/traits/metabolism/cellulolysis.yaml was fixed as the first instance and is where this was found. Its graph split into 4 components with 9 of 14 nodes unreachable from the trait node. The repair added 7 evidence-backed edges (cellulose → cellobiose, cellobiohydrolase → cellobiose, endoglucanase / cellobiohydrolase / beta_glucosidase part_of cellulase, cellulosome → enables → trait, cellulolytic_genes → encodes → cellulase), grounded cellobiose to CHEBI:17057, and retyped cellulosome from CELLULAR_LOCALIZATION to GENE_OR_PROTEIN (it is a complex, not a location; its GO:0043263 grounding is unchanged).

Its own deferred remainder, worth carrying forward as the template for what "done" looks like per trait: the LPMO / CBM / transport / phosphorolysis branches, splitting cellobiohydrolase by chain end, and replacing the generic catabolite-repression edge with taxon-specific cip-cel evidence.

Reproducing the numbers

Load each causal_graphs block, build an undirected adjacency over nodes[].node_id from edges[].subject/object, and count components per graph. Recorded here so the baseline can be re-measured after any backfill batch.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions