Migrate the 185 mechanical microbe-domain edges (#301, part 1 of 2) - #328
Conversation
67 METPO object properties inherit rdfs:domain METPO:1000525 (microbe) via METPO:2000001. OWL domain is an inference rule, not a constraint, so a causal-graph edge using one entails that its subject IS a microbe, and CausalNodeTypeEnum has no organism member. This moves the 185 edges whose target was already decided or is upstream: 62 any subject -> TRAIT object -> METPO:2007700 confers (v8) 103 activity subject `produces` X -> RO:0002234 has output (RO) 12 TRAIT uses carbon source -> METPO:2007806 has carbon source 8 TRAIT uses energy source -> METPO:2007807 has energy source Every edge keeps its direction; this pass only relabels and re-grounds, which is what keeps a 130-file change reviewable. The 181 that need the other 11 v9 terms follow in part 2. Family B takes RO rather than a new METPO term because biolink gives has output domain 'biological process or activity', which the BIOLOGICAL_PROCESS, PATHWAY and MOLECULAR_FUNCTION subjects satisfy. Its mapping row is gated to exactly those three, so a protein-subject edge is refused rather than silently re-acquiring the domain violation this migration removes. The script keys targets on (subject, object, predicate_id), NOT on the endpoint pair. Two corpus cases make the pair unsafe and the guard caught both before anything was written: - ecology/biosafety_level_1.yaml has `low_pathogen_hazard --produces--> bsl1_trait` (a target) and `low_pathogen_hazard --qualifies for--> bsl1_trait` (UNGROUNDED, not a target) on the same endpoints. Keying by pair would have rewritten the ungrounded edge too. - physiology/chemoheterotrophic.yaml has both `uses energy source` and `uses carbon source` between the same two nodes, taking DIFFERENT targets, so one key cannot carry both. Also widened the confers mapping row to admit CAPACITY: one family-A edge has a CAPACITY subject, which the v8 gate did not list. Baseline: MICROBE_DOMAIN_ON_NONORGANISM 366 -> 181, matching the round-trip plan in proposals/metpo_traitmech_v9 exactly. Evidence-snippet baseline unchanged at 2737 with 0 new, as expected when no edge is reversed. validate-strict 477 files / 0 errors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Claude finished @realmarcin's task in 6m 34s —— View job ReviewI verified the migration mechanically rather than taking the numbers on faith. The data pass is clean; one bookkeeping defect. What I confirmed
🟡 The
|
There was a problem hiding this comment.
Migration itself verified clean: the 185 removed baseline rows are exactly METPO:2000202x165 + METPO:2000006x12 + METPO:2000010x8 with zero rows added; the data diff touches only the 370 predicate/predicate_id lines and nothing else (no snippet, reference or node line anywhere); all 20 family-D subjects really are TRAIT; RO:0002234 and the two v9 ids/labels all check out; and the biosafety_level_1.yaml guard case is real (line 112, same endpoints, still ungrounded and untouched).
Requesting changes for one bookkeeping defect. mappings/predicate_grounding.tsv:102 gains CAPACITY in subject_types while its own note still reads 'the 9 node types actually observed across the 145 migrated edges' - the cell now lists 10 and the corpus has 207. The same claim is stale in the artifact that goes upstream, proposals/metpo_traitmech_v8/metpo_proposal_properties_robot.tsv:3, whose 'Subject types observed' enumeration omits CAPACITY. That enumeration is the domain evidence a METPO curator uses to decide what METPO:2007700 admits, and this PR is what made it wrong.
Also worth folding in (non-blocking): reports/predicate_grounding_residual.tsv drops 'has output' from 4 to 3 because data/traits/upper/biological_process.yaml:129 now passes the new gate, but no predicate_id was written for it - so the committed report and the corpus disagree at the same commit. A 'just ground-predicates --apply' closes that. And there is no history/ record for a 130-file data/traits change (advisory only).
Full detail in #328 (comment)
…328 review) The confers mapping row gained CAPACITY in subject_types but its note still said "9 node types across 145 migrated edges" - the cell listed 10 and the corpus had 207. A row that contradicts itself is worse than one merely out of date, since that note is what a curator reads to decide whether widening it again is allowed. Now says 10 types across 207 edges and records where CAPACITY entered. The same claim was stale in the artifact that actually goes upstream: proposals/metpo_traitmech_v8's METPO:2007700 row carried "covers 145 causal-graph edges across 114 trait records" with a subject-type enumeration that omitted CAPACITY. That enumeration is the domain evidence a METPO curator uses to decide what the term admits, and this PR is what made it wrong. Updated to 207 edges / 172 records with the full 10-type breakdown, and it names #328 as the cause so the v8 narrative stays historically readable. Also ran ground-predicates --apply, which the new has output row made newly applicable to one edge: upper/biological_process.yaml's `molecular_function --has output--> chemical_output`, whose own description already cited RO:0002234 but which carried no predicate_id. The committed residual report had begun describing it as groundable while the data left it bare; report and corpus now agree at the same commit. The two edges reported blocked_by_node_type are the gates working as intended - an EXPERIMENTAL_FACTOR subject for has output and a BIOLOGICAL_PROCESS object for confers are both correctly refused. No history/records entry, per the advisory; that convention gap is tracked in #325. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Both fixed, and the 🔵 residual one was the more useful catch. 🟡 The self-contradicting
|
|
Claude finished @realmarcin's task in 4m 12s —— View job Re-review of the fix commit (
|
There was a problem hiding this comment.
Both requested changes verified against the corpus, not just the commit message.
The confers row now agrees with itself (10 types / 207 edges) and records that CAPACITY entered via #328. I counted independently: METPO:2007700 appears 207 times across 172 files under data/traits/**, an exact match for the v8 proposal's updated traits_addressed. The new subject-type enumeration sums to 207, and its delta against the old one (BIOLOGICAL_PROCESS +58, CHEMICAL +2, QUALITY +1, CAPACITY +1) is exactly the 62 family-A edges this PR migrated. CAPACITY 1 is exact: twelve files contain both a CAPACITY node and a confers edge, and only cell_width_small.yaml's rod_diameter_control is actually a confers subject.
The residual/corpus disagreement is closed - biological_process.yaml:136 now carries predicate_id RO:0002234, its subject node is MOLECULAR_FUNCTION so the new gate holds on its own terms, and upper/biological_process.yaml has dropped out of the report's has-output example list, so the 3 blocked edges are genuinely blocked rather than one being silently miscounted.
Outside those three files the commit touches only pages/**, all of it the regenerated timestamp line. The 181 baseline is untouched. All checks green.
Minor, non-blocking: the PR comment says 'the two remaining blocked_by_node_type rows' but the report has three (causally upstream of 13, confers 3, has output 3) - the two named are edge examples. Nothing committed is wrong.
The 🟡 is the same failure #328 had, and the reviewer is right that #327 set the opposite precedent. Adding the `has input` row made upper/biological_process.yaml's `molecular_function --has input--> chemical_input` edge LOOK mapped, so it dropped out of the residual report, but nothing grounded it -- leaving it in neither the grounded set nor the backlog, with the next `ground-predicates --apply` silently touching a file this PR had not. Grounded to RO:0002233, which its own evidence note already named ("Has input [RO:0002233]") and which its sibling has-output edge has carried all along. Curation event appended by the tool. Both 🔵s recorded as decisions in the curation event rather than left as omissions, which is what the reviewer asked for: - The node is typed BIOLOGICAL_PROCESS, not MOLECULAR_FUNCTION. The latter would have grounded to GO:0008776 via node_grounding.tsv, but this trait's own research report asks for a process label plus explicit reaction edges for the SLP step, and that is exactly what this is. - The step carries has-output atp but deliberately not has-output acetate. The existing acetyl_phosphate is-converted-to acetate edge already states that half of the reaction on the chemical chain; asserting it again at the process level is the duplication at two levels of description that #303 criticised in dissimilatory_iron_reduction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…) (#339) * Model the acetate kinase step instead of a chemical producing ATP (#331) `acetyl_phosphate --produces--> atp` had a CHEMICAL subject on METPO:2007800, whose definition is that the ACTIVITY OR STATE of the subject brings the object into existence. A chemical has neither in that sense. RO:0001001 (derives into) did not fit either, and #327 said why: ATP's matter comes mostly from ADP and only the phosphoryl group transfers, so it fails that relation's inherits-the-significant-portion- of-the-matter test. That is why it was filed rather than swept into #327. The resolution was already written in the edge's own description -- "The acetate kinase step generates ATP" -- and in its evidence, which says ACK converts acetyl phosphate to acetate yielding ATP. The process was named but not modelled. Added it as a BIOLOGICAL_PROCESS node and split the edge into `acetate_kinase_step has input acetyl_phosphate` (RO:0002233) and `acetate_kinase_step has output atp` (RO:0002234), both range-correct. Wired on BOTH sides deliberately: atp had exactly one edge, so replacing it without an input edge would have orphaned the new node and atp together. audit-graphs confirms no new findings. This was the last CHEMICAL-subject `produces` edge, so METPO:2007800's subject_types gate drops CHEMICAL and now matches its definition exactly. Added a `has input` mapping row (RO:0002233), gated to the three activity node types for the same reason `has output` is. Evidence-snippet baseline moves 2737 -> 2738, and the delta is exactly explainable: the pre-existing MISSING_SNIPPET re-keys onto the new has-output edge, plus one genuinely new finding because the has-input edge reuses the same evidence, which carries notes but no verbatim snippet. A snippet cannot be invented -- the schema specifies a verbatim quote and the source text is not to hand -- so it joins the known backlog rather than being fabricated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Ground the has-input edge the new mapping row exposed (PR #339 review) The 🟡 is the same failure #328 had, and the reviewer is right that #327 set the opposite precedent. Adding the `has input` row made upper/biological_process.yaml's `molecular_function --has input--> chemical_input` edge LOOK mapped, so it dropped out of the residual report, but nothing grounded it -- leaving it in neither the grounded set nor the backlog, with the next `ground-predicates --apply` silently touching a file this PR had not. Grounded to RO:0002233, which its own evidence note already named ("Has input [RO:0002233]") and which its sibling has-output edge has carried all along. Curation event appended by the tool. Both 🔵s recorded as decisions in the curation event rather than left as omissions, which is what the reviewer asked for: - The node is typed BIOLOGICAL_PROCESS, not MOLECULAR_FUNCTION. The latter would have grounded to GO:0008776 via node_grounding.tsv, but this trait's own research report asks for a process label plus explicit reaction edges for the SLP step, and that is exactly what this is. - The step carries has-output atp but deliberately not has-output acetate. The existing acetyl_phosphate is-converted-to acetate edge already states that half of the reaction on the chemical chain; asserting it again at the process level is the duplication at two levels of description that #303 criticised in dissimilatory_iron_reduction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Say what write_validated_trait actually does to formatting (#322) The comment claimed that re-running the helper over an existing file "produces a byte-identical diff". Measured against the corpus: it holds for 127 of 477 files. The other 350 are reformatted, because safe_dump re-wraps long strings at its own width and drops hand-written quoting. The claim is not harmless. It is what a bulk-script author reads before deciding how to rewrite N files, and believing it means burying the real change in reflow churn across every long string in every file touched -- the difference between a reviewable migration and an unreviewable one. Both the #323 and #328 migrations edit raw lines instead, and this issue exists because the claim was checked rather than trusted. Corrected to state the real behaviour with the measured number, to say plainly that it matters for BULK use and not for a single record the helper already owns, and to note that making the claim true would mean normalising all 477 files once behind a round-trip test -- a large one-time reformat, which is the part of #322 still open. Three tests pin the two causes (re-wrapping, dropped quoting) and the case where the round trip DOES hold, so the claim cannot quietly return. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Bind the round-trip tests to the helper's real options (PR #343 review) The finding is exact and it is this PR's own thesis one level up. The tests asserted on yaml.safe_dump against a re-declared DUMP_OPTS dict, with nothing tying it to the helper. Adding width=4096 to the real options would have stopped the re-wrapping, falsified both the comment's stated cause and its 350/127 counts, and left every test passing -- because they were testing PyYAML's defaults, not write_validated_trait. Fixed structurally rather than by adding another assertion: the options are now EMIT_OPTS at module scope in write_validated.py and imported by the tests, so there is no copy to drift. The helper composes them with its yaml_kwargs exactly as before. Rewritten to exercise the real thing: - a hand-edited corpus file written through write_validated_trait to a tmp_path comes back with different bytes - the helper's OWN output round-trips byte-identically, which is the boundary that makes it safe for one record and unsafe for a bulk pass - the documented 127/350 split is asserted against the live corpus, so the numbers in the comment cannot go stale as a snapshot -- which is how the original claim came to mislead in the first place. It will fail loudly when #322's normalisation lands, which is the right moment to update them. Canaried by injecting width=4096 into the REAL options: the split test fails. The first test still passes there, because quoting differences alone keep some files reformatted -- worth knowing that the split test is the one carrying that guarantee. The corpus walk is lru_cached; three tests need it and each pass parses all 477 records (20s -> 7s). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Bind the split to the emit path, and stop the docstring over-claiming (PR #343 round 2) The 🟡 is a fair hit: that test's docstring said "change them so the reformatting stops and this fails", and I had ALREADY measured that it does not -- injecting width=4096 leaves it passing, because dropped quoting alone still reformats some files. I reported that honestly in the PR comment and then left the docstring asserting the opposite, which is worse than not having measured it. Corrected to say what is true: this one demonstrates the behaviour end to end, and the corpus-split test is what carries the guarantee. The 🔵 was the same class as the original finding, one step smaller: _split() rebuilt `yaml.safe_dump(doc, **EMIT_OPTS)` itself, so it bound to the options but not to how they are COMPOSED with yaml_kwargs. A change to the composition would have gone unnoticed. Extracted emit_trait_yaml() as the single emit path, used by write_validated_trait and imported by the tests, so there is nothing left to re-implement. Canaried the composition specifically this time, not just the options dict: injecting width into the composition expression fails the split test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
… (#344) * Normalise the corpus so the round-trip claim becomes enforceable (#322) write_validated_trait's comment claimed a byte-identical round trip. #343 corrected the claim to match reality -- it held for 127 of 477 records. This makes the claim TRUE instead, which is the half #322 left open. 350 records are rewritten through the helper's own emission path. No data changes; only formatting. safe_dump re-wraps long strings at its own width and drops hand-written quoting, so the diff is entirely those two things. Verified three independent ways rather than trusted: - The normaliser is TWO-PASS (#324's lesson): it computes and semantically checks every file before writing any, and aborts on the first mismatch rather than leaving the corpus half-normalised. 0 problems across 477. - Every rewritten file was re-parsed and compared to its pre-write document; a formatting pass that changed data would have failed there. - Independently, every file's parsed document was compared against `git show HEAD:` afterwards. 477 compared, 0 semantic differences. The strongest evidence is what did NOT change: pages/, reports/ and conf/ are byte-identical, because every generator and audit parses the YAML. Identical data in, identical artifacts out. The tests flip from documenting the gap to enforcing its absence. What asserted a 127/350 split now asserts that NO record fails to round trip, and the end-to-end test asserts equality rather than difference. Canaried by re-quoting a single scalar by hand: the suite fails. The payoff is that bulk scripts can use this helper again. #323, #328 and #341 all hand-rolled raw-line editors specifically to avoid the reflow churn, and that workaround is no longer necessary. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Re-trigger CI The initial push produced no workflow runs at all despite the diff matching several paths: filters (data/traits/**, src/traitmech/**.py, tests/**.py). Close/reopen did not trigger them either. Empty commit to fire a synchronize event. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Re-trigger CI after the Actions outage This PR was opened at 19:24Z on 2026-08-06, during a critical GitHub Actions incident (stspg.io/rcz3fcm83sff) in which webhook triggers were throttled to ~15% and many push and pull_request events never created workflow runs. It received none, so it has sat CLEAN-looking and entirely unverified -- which is the state #346's detector now reports on every merge to main, and it flagged this PR within a minute of landing. The incident is resolved. Empty commit to fire a synchronize event now that they are being delivered again. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Pin the corpus total, and fix prose the flip contradicted (PR #344 review) Both 🟡 are fair and the first is a regression I introduced when flipping the test. _split() skips anything that will not parse. The old assertion was `(len(same), len(changed)) == (127, 350)`, which pinned the total at 477, so a skipped record failed it. `assert not changed` does not -- a record edited into invalid YAML would drop silently out of the guard and the test would pass. Now asserts len(same) == len(TRAITS) as well. Canaried by appending invalid YAML to a record: it fails, and names the count. The second is the stale-prose-beside-updated-code mistake I have now made several times in this session. The module docstring still opened with the claim being "false for most of the corpus" three lines above saying it is now true, and test_the_helpers_own_output_round_trips still described the helper as "unsafe for a bulk rewrite" -- the exact opposite of what this PR establishes and of write_validated.py's own comment. Rewritten: the docstring is now past-tense about the pre-#322 state, and that test is described as what it actually asserts, the emitter's idempotence, which is a different property from the corpus being in that form. The 🔵 about a `str` representer emitting block scalars for the ~50 curation-history prose blocks is a good idea and is filed as #347 rather than taken here: it changes the emitted format, so it would mean re-normalising all 477 records inside a PR already under review, and the round-trip test would then enforce whichever form is chosen -- a one-way decision worth making deliberately. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…351) * Retype six mis-typed dispositions, and route six real states (#334) The recommendation going in was to check node TYPES before touching predicates, because that has been the actual defect three times running (#328's assay_measurement, #331's unmodelled acetate kinase step, #330's negated node name). It holds again: half of the STATE/CAPACITY objects here are not states at all. Their own descriptions give them away -- "Capacity of an organism to grow and survive under high hydrostatic pressure", "Ability to grow when external phosphate is unavailable", "Detoxification-mediated tolerance of arsenic oxyanions". Those describe what an organism CAN do, which is what a TRAIT is. Five nodes retyped (one carries two in-edges), six edges then take METPO:2007700 confers unchanged, because with the object correctly a TRAIT the existing relation already fits. The other six objects ARE genuine states -- a proton gradient, a community composition, an internal microoxic environment -- so retyping them would be wrong. They take RO:0002326 (contributes to), which fits because the subject genuinely contributes to the OCCURRENCE OR GENERATION of the object. That is biolink's actual wording, and it is the same test the motive-force edges FAILED in #341, where the subject powers a machine it does not generate. Getting the same relation right here and wrong there is the distinction, not an inconsistency. ENABLES_RANGE_VIOLATION: 28 -> 16. Retyping to TRAIT adds reachability anchors, so audit-graphs improved: 1535 -> 1521 findings, none new. Also widened the confers gate for ORGANELLE, which appeared once buoyancy became a TRAIT (gas_vesicle confers buoyancy) -- listed rather than left open so each new subject kind stays an explicit decision. The #322 round-trip guard caught the hand-built curation events again; 11 records re-emitted through the helper. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Ground the new TRAIT anchors, and correct my audit claim (PR #351 review) The sharpest point is one I should have caught: "audit-graphs improved 1535 -> 1521" was ANCHOR ACCOUNTING, not connectivity. pressure_optimum's 5-node island and ploidy's 4-node island stopped being reported UNREACHABLE_FROM_TRAIT without a single edge changing -- they just acquired an ungrounded anchor inside them. FRAGMENTED_GRAPH still says components=2 for both, and is still 218 overall. The islands are intact. Making a metric fall without improving the graph is exactly what I should be catching rather than causing, and the PR body said the opposite. Two of the retyped nodes duplicated traits that already exist and were left ungrounded, which the playbook requires of every TRAIT row (462 of 482 TRAIT nodes carry one, so ungrounded is the exception here): - hhp_tolerance IS piezotolerant, already traitmech:000003 -> grounded to it - growth_external_ph_5_5_9 is the same concept as its own record's ph_delta_high_trait -> grounded to METPO:1000478, which makes the duplication explicit rather than leaving two unrelated-looking anchors The playbook still told curators this case was unsettled. It now gives the rule the retyping followed -- a node described as a capacity to, an ability to, or a tolerance of is a disposition, so retype and use confers, and ground it -- plus the separate row for genuine states, with the occurrence-or-generation test that distinguishes them from #341's motive-force edges. Four edge descriptions still said "enables" under the new predicate; reworded. Filed #352 for the 🔵: salt tolerance, low-pH tolerance, oxygen tolerance and growth at 4C are the same concept still typed CAPACITY, surviving only because their in-edges did not happen to violate the range -- an unrelated fact. That wants the typing rule as its trigger, not a range-violation sweep. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Repair a description my own reword mangled (PR #351 round 2) The reword script matched on a 60-character prefix and substituted into it, which turned "proper polar growth and normal branch spacing" into "proper powth and normal branch spacing" in branched_shaped.yaml. Repaired, and swept every edge this PR touched for the same signature -- that is the only one. A regex that edits prose it does not parse was the wrong tool for four descriptions; doing them by hand would have been faster and safer. CURATION_PLAYBOOK.md still said "33 pre-existing edges" for ENABLES_RANGE_VIOLATION. It is 16 after this PR -- and the page is where a curator learns whether their edge shape is allowed, so a stale count there misstates how settled the area is. Third stale-prose spot in this PR, which is the same pattern #350 ended on. Added GROUND_CAUSAL_NODES events for the two groundings, per the 🔵. They were a real change to the records, not just metadata: an ungrounded TRAIT node still counts as a reachability anchor, which is how the audit number moved without the graph changing. Folded the other 🔵 into #352: nothing detects two nodes sharing a grounding within one graph, which is the machine-readable signature of exactly the duplication that issue is about, and would have found the ph_delta_high case without anyone reading descriptions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
First of the two migrations planned in
proposals/metpo_traitmech_v9(#326).What moved
67 METPO object properties inherit
rdfs:domain METPO:1000525(microbe) viaMETPO:2000001. OWL domain is an inference rule, not a constraint, so a causal-graphedge using one entails its subject is a microbe — and
CausalNodeTypeEnumhas noorganism member. This migrates the 185 edges whose target was already decided or is
upstream:
METPO:2007700confersproducesXRO:0002234has outputuses carbon sourceMETPO:2007806has carbon sourceuses energy sourceMETPO:2007807has energy sourceEvery edge keeps its direction. This pass only relabels and re-grounds — that is what
keeps a 130-file change reviewable, and it's why the evidence-snippet baseline is
untouched (a reversal changes an edge's baseline key; nothing here does).
Family B takes an RO term rather than a new METPO one because biolink gives
has outputdomain: biological process or activity, which itsBIOLOGICAL_PROCESS/PATHWAY/MOLECULAR_FUNCTIONsubjects satisfy. Its mapping row is gated to exactly those three,so a protein-subject edge is refused rather than silently re-acquiring the very domain
violation this removes.
The guard earned its keep
The script keys targets on (subject, object, predicate_id), not the endpoint pair.
Two corpus cases make the pair unsafe, and the count guard caught both before anything
was written:
ecology/biosafety_level_1.yamlhaslow_pathogen_hazard --produces--> bsl1_trait(a target) and
low_pathogen_hazard --qualifies for--> bsl1_trait(ungrounded, not a target) on the same endpoints. Keying by pair would have
rewritten the ungrounded edge too — a silent corruption.
physiology/chemoheterotrophic.yamlhas bothuses energy sourceanduses carbon sourcebetween the same two nodes, taking different targets, so one key cannotcarry both.
Both were canaried after the fix: the ungrounded edge is untouched, and the two
same-endpoint edges get their correct distinct targets.
The proof it landed
MICROBE_DOMAIN_ON_NONORGANISM366 → 181 — exactly the number the v9 round-trip planpredicted, which is the point of having stated it.
What was checked
just validate-strict— 477 files, 0 errors.just qc— green, every ratchet atblocking: 0.uv run pytest— 420 passed. Ruff clean.no-reversal pass.
has carbon sourceobjects are12/12 CHEMICAL;
has energy sourceis 2 CHEMICAL + 6 ENVIRONMENTAL_FACTOR, which is whyits
object_typesis wider than its sibling's.confersrow to admitCAPACITY— one family-A edge has a CAPACITYsubject, which the v8 gate did not list and which would otherwise have been refused.
Scope
Does not touch the 181 edges needing the other 11 v9 terms (enzyme, transport, and the
rest of
produces) — that's part 2, taking the baseline 181 → 0. TheMETPO:20078xxids remain placeholders until upstream mints them.🤖 Generated with Claude Code