Detect the typing inconsistency instead of guessing at seven records (#352) - #353
Conversation
…352) #352 asks for a sweep. Doing it by hand first showed why a sweep is the wrong deliverable: each candidate CAPACITY node turned out to duplicate its OWN record's trait -- salt_tolerance beside METPO:1000625, low_ph_tolerance beside METPO:1000232, growth_at_4c beside METPO:1000618 -- so retyping them to TRAIT without merging would recreate exactly the ungrounded-duplicate-anchor problem #351's review caught. That is seven modelling decisions, not a mechanical pass. So this adds the two checks that make the class visible and non-growing, in the audit that already has the ratchet machinery: DUPLICATE_GROUNDING two nodes in one graph with the same grounding -- the machine-readable signature of one concept modelled twice DISPOSITION_MISTYPED a CAPACITY or STATE node whose own DESCRIPTION reads as a disposition ("capacity to", "ability to", "tolerance of"), which is a TRAIT Both found more than my hand sweep did, which is the argument for building them rather than doing one pass: - DUPLICATE_GROUNDING found catalase/catalase_function sharing GO:0004096 and urease/urease_function sharing GO:0009039. Neither was known; both are a protein node and a function node modelling one thing. Only the ph_delta_high case from #351 was on anyone's list. - DISPOSITION_MISTYPED found 8 where I had found 6, including oxygen_tolerance -- my pattern wanted "capacity to" and the record says "Capacity OF A CELL to" -- and elongation_capacity in sphere_shaped. Anchored on the DESCRIPTION rather than the label on purpose: the label is usually just the concept name ("buoyancy"), while the description is where the disposition shows. All 11 baselined, since each needs a per-record decision. Canaried: a new CAPACITY node described as a disposition exits 1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Claude finished @realmarcin's task in 4m 21s —— View job Review: PR #353Verified CI (qc, pytest, pr-sanity, vendored-sync all green), reproduced the 11 findings against the corpus by hand, and confirmed all 8 🟡 blob = f"{n.get('label') or ''} {n.get('description') or ''}"The comment at line 149 says the regex is "deliberately anchored on the phrasing a curator writes, not on the label", the PR body repeats it, and the finding text hard-codes
Neither behaviour is covered: 🟡
That is the identical fail-open the comment 20 lines below at 🔵 The heuristic's precision leans on an accident. 🔵 🔵 Both new blocks land between the Note, not a defect: the baseline's The framing is right — the two checks did find |
There was a problem hiding this comment.
Two issues in the detection code; details in the review comment.
- The DISPOSITION_MISTYPED check scans label+description concatenated (scripts/audit_causal_graphs.py:262), contradicting both the comment at :149 and the finding text 'description reads as a disposition', and creating cross-field phantom matches.
- DUPLICATE_GROUNDING's detail leads with 'grounding=...', so _key's leading-token discriminator makes 2->3 nodes sharing a grounding invisible to the ratchet -- the same fail-open the FRAGMENTED_GRAPH comment at :275 documents and fixes.
All 8 DISPOSITION_MISTYPED hits verified as true positives, and the baseline regeneration tightens rather than relaxes the ratchet.
…#353 review) Four findings, all correct. The docstring said the check reads the description "rather than the label", and the code read both. Now description-only, which is what the rationale actually argues for -- a label is usually just the concept name. DUPLICATE_GROUNDING did not ratchet on growth. _key takes the leading whitespace-delimited token of detail as the baseline discriminator, and the detail led with the CURIE, so a THIRD node joining the same grounding kept the key stable and baselining two would have silently forgiven three. Now leads with the node count, which is exactly the reasoning FRAGMENTED_GRAPH records a few lines below and which I had read without applying. A test pins that two and three nodes key differently. The pattern's precision was accidental. ph_optimum.yaml's "Capacity of cytoplasmic buffers (e.g. ...) to absorb pH fluctuations" is a genuine reservoir CAPACITY and escaped only because the dots in "e.g." stopped the character class -- a right answer for a reason an unrelated cleanup would erase, and under --fail-on new that is CI failing with a wrong diagnosis. The capacity/ability arms now require the capacity to be ORGANISM-scoped: bare, or of a cell/organism/bacterium/microbe. Verified it still excludes the reservoir case WITH THE PARENTHETICAL REMOVED, and still catches all eight real ones. Documented why QUALITY is out of scope: carboxydotrophic's oxygen_tolerance is "Ability of an ENZYME to function in the presence of O2", a property of a protein rather than something an organism can do, while the same node_id typed CAPACITY in oxygen_preference IS organism-scoped and is flagged. Widening would need a distinction this heuristic does not make. Both blocks moved above the FRAGMENTED_GRAPH rationale comment, which they had been inserted into the middle of. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
All four correct. Two were me not applying reasoning that was already written a few lines away. 🟡 Reading the label while claiming not toFixed — description-only, which is what the rationale actually argues for. 🟡
|
|
Claude finished @realmarcin's task in 2m 58s —— View job Review: PR #353 (re-review of
|
There was a problem hiding this comment.
Both 🟡 from the last round are fixed and pinned by non-vacuous tests, and I traced the regex fix by hand rather than trusting the commit message — the reservoir case in ph_optimum.yaml now escapes for a principled reason (organism-scoping) rather than an accident of "e.g." punctuation, verified with the parenthetical removed.
One new 🟡, introduced by the DUPLICATE_GROUNDING fix: leading the detail with nodes=N closed the 2→3 fail-open but opened its mirror. Unlike FRAGMENTED_GRAPH, where the component count is unique per graph, DUPLICATE_GROUNDING can raise several findings in one graph — and every pair-shaped one now keys as nodes=2, so two distinct duplicated groundings in one graph collapse onto a single baseline key and baselining one silently suppresses the other. That is the exact failure _key's own docstring warns about for DANGLING_EDGE. Not triggered today (the three findings are in three files), but reachable from the current baseline without any new file.
Both properties are available at once: lead with a single token carrying the count and the CURIE — f"nodes={len(ids)}:{g} share grounding: ...". Details in the comment.
…und 2) Leading the detail with the count closed one fail-open and opened its mirror. _key takes the leading whitespace-delimited token, so with `nodes=2` in front, two DIFFERENT groundings each duplicated across 2 nodes in the same graph collapse onto one key -- freezing one silently forgives the other. The previous round had the same bug pointing the other way. Only one part varying is not enough; both are now in the token: `nodes=2;grounding=METPO:1000478`. A test writes two 2-node groundings in one graph and asserts two distinct keys. Confirmed it fails (1 == 2) against the count-only form and passes here, so it pins the fix rather than restating it. Widened the disposition scope nouns with strain|species|isolate|archae*, which the review offered as cheap and are ordinary in this corpus. Findings unchanged at 8 + 3. Baseline and report regenerated: the discriminator changed, so the three DUPLICATE_GROUNDING keys are rewritten. Same 1532 findings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Right, and it's the same bug pointing the other way — I closed one fail-open by opening its mirror.
A test writes two 2-node groundings into one graph and asserts two distinct keys. It fails against the count-only form ( Worth recording how close this came to shipping green: when I checked the fix I hit a stale- 🔵 Scope nounsTaken — added Findings unchanged at 8 + 3. Baseline and report regenerated — the discriminator changed, so the three
|
|
Claude finished @realmarcin's task in 4m 7s —— View job Review: PR #353 (re-review of
|
There was a problem hiding this comment.
The round-2 discriminator issue is fixed and the fix is the right shape: nodes=2;grounding=METPO:1000478 puts both the count and the CURIE in _key's leading token, so 2->3 re-keys and two distinct groundings in one graph stay on distinct keys. test_two_groundings_of_equal_size_do_not_collide is non-vacuous — it asserts two findings AND two distinct keys, which the count-only form fails at 1 != 2.
Verified independently rather than from the commit message: just audit-graphs reports 1532 findings, 0 new, 0 blocking, 8 DISPOSITION_MISTYPED + 3 DUPLICATE_GROUNDING, zero ERROR rows; the report blob is byte-identical to the baseline and qc's staleness check is green, so the committed report is what the current code produces. 1521 -> 1532 is exactly +11.
The scope-noun widening is a no-op on today's corpus, so it is defensive rather than a behaviour change. I also checked every corpus description matching the pattern for false negatives from the tightening: the near-misses are out of scope by node_type, not by luck (spore_germination_trait is already TRAIT, protein_stability is MOLECULAR_FUNCTION, pathogenicity is BIOLOGICAL_PROCESS, the tolerance-of/to hits are TRAIT nodes or edge descriptions). The only CAPACITY among them, ph_optimum.yaml:77, escapes because 'cytoplasmic buffers' fails the scope group — the principled reason, not the parenthetical.
No data/traits/** change, so no history/ record is owed.
Three suggestions in the comment, none blocking: 'species' is used chemically 56 times in this corpus and is the one scope noun with a plausible collision; a partial 3->2 fix will now re-key and report as new (the same trade FRAGMENTED_GRAPH already makes); and the docstring's '1541 findings' is now 1532.
* Check that every workflow which SHOULD have run, ran (#348) audit-pr-checks can only see TOTAL silence, and the reason is structural: claude-code-review.yml fires on pull_request with no paths: filter and records a run even when its if: gates skip the job, so nearly every PR here has at least one qualifying event. qc, pytest and validate-strict could all be mute and it would stay green. This is the stronger property #348 asked for -- each PR-triggered workflow checked by name. THE REQUIRED SET IS DERIVED, NOT DECLARED: every workflow in .github/workflows with a pull_request: trigger, read from the files. #252 rejected a hand-maintained list for audit-qc-paths because a declaration drifts the moment someone adds a workflow, and the argument applies here unchanged. A test pins that adding a workflow file grows the set with no second edit. A paths: FILTER IS NOT A MISSING RUN. Five of the eight PR-triggered workflows are filtered, so the filters are evaluated against the PR's own changed files and only an unfiltered-or-matching workflow is expected. That evaluation is the hard part, and also the payoff: a paths: regression -- the class #184, #200, #250 and #252 all belong to -- surfaces here as "expected, did not run". Two things I got wrong while writing it, both pinned by tests: PyYAML resolves the unquoted key `on:` to the BOOLEAN True under YAML 1.1, so doc["on"] is absent in every workflow in this repo. Reading only "on" would have found zero required workflows and exited 0 -- a check that passes because it looked at nothing, the exact vacuous green it exists to catch. Invisible without a test, so there is one asserting the trap itself. The matcher anchored only the end, so `data/traits/**` would have matched `vendor/data/traits/x.yaml`. Now fullmatch. GitHub's `*` also does not cross a slash where fnmatch's does, which is why this is hand-rolled rather than fnmatch. Filter syntax the matcher does not implement ([], !, +, ?) is reported as UNSUPPORTED and never guessed at, since the value of the check rests on "expected, did not run" meaning something. PRs past GitHub's 300-file path-filter evaluation limit are skipped AND NAMED, per #346's rule that a PR vanishing from both counts makes the output read as coverage it did not have. Wired into pr-checks-present.yaml with if: always(), so a total silence does not suppress the partial-silence report -- they answer different questions. CANARY (live, against this repo, before wiring it into CI). 8 workflows parsed; PR #353's 4 changed files predicted 5 expected workflows, all 5 matched against real API data, and curation-history, label-correspondence and validate-strict were correctly filtered out. Negative control: deleting qc's run from the fetched data reports exactly ['.github/workflows/qc.yaml'], so the green was not vacuous. Not exercised: a PR with >300 files, and unsupported filter syntax, both of which exist only in tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fetch at the PR head, survive pagination, decline branch filters (#354 review) All three correct, and the first two were reachable today. PAGINATION. `gh api --paginate -q` applies the jq filter to EACH PAGE and concatenates, so past GitHub's default per_page=30 json.loads raises "Extra data" -- taking down collect() for every OTHER open PR, and making the >300-file skip branch unreachable because the fetch died at 31. Reproduced live against PR #311 (346 changed files): JSONDecodeError: Extra data: line 2 column 1 (char 7050) gh 2.97 refuses --slurp together with --jq, so the fix is --slurp plus extraction in Python. Same PR now returns 346 filenames. HEAD vs MAIN. GitHub dispatches pull_request events from the workflow files AT THE PR HEAD; this process has whatever it was checked out at, which for pr-checks-present.yaml is main. They disagree exactly when a PR touches .github/workflows -- and every filtered workflow here lists its own file in its paths:, so a PR DELETING one matches the filter and main's copy expects a run GitHub correctly never made. A false "expected, did not run" is what makes this check stop meaning anything, so collect() now fetches the head's copy per PR. A fetch failure is a REFUSAL: the PR is skipped and named rather than judged against the wrong ref, because falling back to main would silently reintroduce the bug. Writing that test found a bug the review did not: `pr.get("workflows") or workflows` falls back when the head list is EMPTY, so a PR deleting all the workflows would have every one of them reported missing. Now keyed on `in`, not truthiness. BRANCH FILTERS. `branches:`/`branches-ignore:` restrict which base a PR must target and this audit never reads the base, so unmodelled they would make every PR against another base a false offender. Routed through the existing UNSUPPORTED escape hatch. `types:` gets a sharper rule rather than a blanket refusal: every PR head arrives via `opened` (the first) or `synchronize` (any later push), so a types: containing both is predictable and one that does not is declined -- which keeps claude-code-review.yml's `types: [opened, synchronize]` in the set. Also: an unparseable workflow is now NAMED rather than silently dropped (it would shrink the required set invisibly), and _glob_to_regex is lru_cached. CANARY, live, after the rewrite. 2 open PRs, 8 workflows fetched AT EACH HEAD, 5 expected and 5 matched on both. Pagination exercised for real on PR #311's 346 files. Negative control: deleting pytest's run from the fetched data reports exactly ['.github/workflows/pytest.yaml']. Still not exercised live: the >300-file skip and unsupported syntax, both tests-only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
… retypes (#352) (#360) * Burn down the 11 disposition-typing findings, half of which were not retypes (#352) #353 shipped the detection and baselined what it found. This is the burn-down: DISPOSITION_MISTYPED 8 -> 0, DUPLICATE_GROUNDING 3 -> 0. ONLY FOUR OF THE EIGHT WERE MISTYPES. #352 framed the fix as "sweep CAPACITY nodes matching the disposition pattern and retype them", and for four nodes that is exactly right - salt tolerance, salt-tolerance breadth, oxygen tolerance and low-pH tolerance are dispositions, now TRAIT and grounded METPO:1000622 / 1000622 / 1000609 / 1003008. For the other four, retyping would have been wrong, and the thing that exposes it is the grounding. Every TRAIT node in the corpus is grounded, and the only correct grounding for each of those four is the term its OWN record already carries. Grounding them that way trades a DISPOSITION_MISTYPED for a DUPLICATE_GROUNDING and calls it progress. What it actually means is the node RESTATES its anchor: - ph_homeostasis_capacity "Capacity to balance and maintain cytoplasmic pH" against cytoplasmic_ph_homeostasis, IN THE SAME GRAPH, already BIOLOGICAL_PROCESS and grounded GO:0051453. - elongation_capacity "Capacity of a cell to elongate into a rod via sidewall growth" against lateral_elongation "Sidewall growth mode that lengthens rods" - both already carried `reduced in -> sphere_shaped_trait`. - loss_sporulation_capacity IS METPO:1000872, the record's own term. Collapsed to low_spo0a_activity -causes-> non_spore_forming_trait, which is the shape loss_sporulation_genes already uses in that graph. - growth_at_4c IS METPO:1000618, and it is a leaf hanging off the node grounded to it. That is #352's third bullet read strictly. It warns that an UNGROUNDED new TRAIT node becomes a free anchor, which suggests requiring a grounding fixes it. Requiring one does something better: it separates a real mistype from a restatement. CAPACITY IS NOT VESTIGIAL, which #352 left open. 24 nodes carry it; these 8 leave 16, and the survivors are a different sense - reducing_power (a pool of reductants), cytoplasmic_buffering_capacity (a reservoir), swimming_velocity (a rate), metabolic_versatility (a breadth). A buffer has a capacity; so does a battery; neither is something an organism can DO. Same two-senses shape `reduces` recorded in predicate_grounding.tsv, and it is why #353's heuristic is organism-scoped rather than matching bare "capacity to". THE 3 DUPLICATE_GROUNDINGS. growth_external_ph_5_5_9 shared METPO:1000478 with ph_delta_high_trait, but the two say different things: an ABSOLUTE external range (~5.5-9.0) versus a BREADTH (~5-9 pH UNITS), which is what a pH delta is. Regrounded to METPO:1000332 (pH range). catalase and urease shared their function node's GO term; GO:0004096 is catalase ACTIVITY, and a protein is not its activity - the graphs already say so correctly with catalase -enables-> catalase_function. Dropped from the proteins, kept on the functions. Note this is NOT a corpus-wide sweep: 72 GENE_OR_PROTEIN nodes are grounded to GO terms and that shorthand is fine where no separate function node exists; only these two graphs model both. NOT AN IMPROVEMENT IN CONNECTIVITY, and saying so before anyone asks. UNREACHABLE_FROM_TRAIT fell 1303 -> 1296, but FRAGMENTED_GRAPH is flat at 218 and the islands are intact. Every one of the seven is explained by a retype adding an anchor INSIDE an existing island - in oxygen_preference, superoxide_dismutase and two others stopped being reported because oxygen_tolerance, sitting in their island, became a TRAIT. Filed as #359: the metric moves for two different reasons and only one is progress, and this is the second time it has invited the wrong claim. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Regenerate biolink_coverage.tsv for the merged edges (#352) Two consequences of the merges that I regenerated the report too early to capture: `reduced in` drops 3 -> 2 residual because elongation_capacity's duplicate edge to sphere_shaped_trait was deduplicated into lateral_elongation's, and `has capability` disappears entirely because its only corpus edge was psychrotolerant_trait -> growth_at_4c, the leaf that restated its own parent. Both are the migration working as intended, not collateral: an ungrounded predicate with no remaining edges should stop appearing in a coverage report. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Correct two of the four claimed retypes to merges after review (#352) Review of the first burn-down pass (#360) found that 2 of the 4 nodes it retyped were restatements, caught by the SAME test the other restatements failed: the grounding chosen for each contradicted an edge or a definition the graph already had. salt_tolerance_breadth was retyped and grounded METPO:1000622 (halotolerant) while keeping its `is a -> nacl_delta` edge. nacl_delta is METPO:1000335, a delta (sub 1000532/1000534), whereas 1000622 is a halophily preference (sub 1000629) - so the graph asserted halotolerant sub NaCl-delta, a subsumption METPO does not have. That is the absolute-vs-breadth distinction this migration already insists on for pH, missed for salt. Merged into nacl_delta. oxygen_tolerance was grounded METPO:1000609 (aerotolerant), which METPO defines as "does NOT use O2 for growth but tolerates its presence" - false of the obligate aerobes the node also covers - and which is itself sub METPO:1000601, whose own definition is "oxygen requirements OR TOLERANCE for growth". So it was a sixth child phenotype in a graph that wires the other four in with `is a` and left this one unlinked. Merged into oxygen_preference_trait. The migration is now 2 retypes, 6 merges, 3 regroundings. THE MERGE IS A REAL CONNECTIVITY GAIN AND THE RETYPE WAS NOT, which is the distinction #359 is about. oxygen_preference FRAGMENTED_GRAPH goes components=3 of 14 (sizes 8, 4, 2) -> components=2 of 13 (sizes 11, 2): the ROS-defence island now attaches to the trait via detoxifying_enzyme_expression and superoxide_dismutase, instead of merely sitting beside a new anchor. The retype left that finding unchanged and the PR body said so. Note UNREACHABLE_FROM_TRAIT is 1296 either way - the count alone cannot tell the two apart, which is exactly #359's point. Also fixes the audit trail. The first pass logged all 11 events as RETYPE_CAUSAL_NODE, including the 7 merges and regroundings; an audit trail that calls a merge a retype cannot answer the question it exists to answer. The script now emits MERGE / DROP / REGROUND / UNGROUND_CAUSAL_NODE and appends the curation_history entry itself via record_curation_event rather than leaving it to be hand-written after the fact. Its timestamp is fixed rather than wall-clock because pages/ derives "Corpus as of" from the latest curation_history entry (#228). just qc green (audit-graphs and audit-snippets both 0 new) - 519 tests pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Merge the last two claimed retypes: none of the eight were retypes (#352) Third review round on #360. Both remaining retypes are mis-grounded, so the migration is now 0 retypes, 8 merges, 3 regroundings. salt_tolerance was grounded METPO:1000622 (halotolerant) on the reasoning that the record's own METPO:1000625 (slightly halophilic) makes the term "distinct". Verified against data/raw/metpo.owl: 1000622 and 1000625 are DIRECT SIBLINGS under 1000629, and 1000622 means "tolerates high salt but DOES NOT REQUIRE it for growth" while 1000625 means the organism "REQUIRES low to moderate salt for optimal growth". The node asserted of this record the negation of what the record's own term says. Merged into slightly_halophilic_trait. low_ph_tolerance was grounded METPO:1003008 (acidotolerant), defined as tolerating acid "WHILE MAINTAINING OPTIMAL GROWTH NEAR NEUTRAL pH" — which excludes the acidophiles this generic pH-delta record covers. A grounding narrower than the node it labels is a false claim about every organism in the excluded part. Merged into ph_delta_trait. THE TEST THAT FAILED FOUR TIMES was "is this term distinct from the record's own?". A sibling term is maximally distinct and still wrong. The right question is whether the term is COMPATIBLE with the record and no NARROWER than the node. Written into the playbook, because the wrong test is the intuitive one. CORRECTING AN OVERCLAIM OF MY OWN in the same pass: I first wrote that merging salt_tolerance attaches the ectoine island. Measurement says otherwise — the node was already in the trait's component via osmoprotectant_transport -> compatible_solutes -> osmotic_stress, so that merge changes no structure. It is a correctness fix, and saying more would be this migration committing the exact error it was written to catch. MEASURED, NOT ASSERTED, across all eight: retyping changed component structure in ZERO of the eight graphs merging improves three — oxygen_preference 3 components -> 2, ph_delta 3 -> 2, ph_delta_low 5 -> 4 and leaves five unchanged as pure deduplication UNREACHABLE_FROM_TRAIT reads 1296 under either fix. That is #359's point, and the playbook now points at reports/causal_graph_connectivity.tsv rather than the finding counts. Playbook also updated because its canonical "disposition" example WAS salt_tolerance's own description, which this pass merges away. RETYPE kept as an empty table rather than deleted: "we looked and there were none" and "we never modelled retypes" are different claims, and only one is true. 519 tests pass - ruff clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Correct two claims the data no longer supports (#360 review) Both are claims, not data — but one of them sits in curation_history, whose only job is to be true, in the pass that rewrote the audit trail so a merge would stop being logged as a retype. "filed as a proposal" was false. Nothing under proposals/ covers a generic salt-tolerance disposition and this PR adds nothing there. The first half of the sentence is right and stays: METPO genuinely has no term to reground to, since halotolerant and acidotolerant are the only candidate labels and each already anchors its own record. Now cites issue #364, which proposes a tolerance axis distinct from the preference axis, and says plainly that nothing under proposals/ exists yet. The playbook's CAPACITY table listed two TRAIT examples that are both nodes THIS PR DELETED. Last round caught the row using salt_tolerance's description; the fix swapped in oxygen_tolerance's, which acd4a12 merged away one commit earlier, alongside "Ability to grow at 4 C" which is growth_at_4c, dropped in f60cb99. So the table told a curator "this sense becomes TRAIT" using nodes that did not become TRAIT, thirteen lines above a sentence saying #352 retyped nothing. Rewritten so the column says what to DO rather than what to type: a disposition reading makes a node a CANDIDATE, and the grounding step decides. The examples are kept and labelled as what they are — all three merged or dropped, none survived — which is a better lesson than a list of nodes that did become traits, since there are none. Also stopped citing reports/causal_graph_connectivity.tsv in the present tense: it arrives with #363, not on this branch. The hand-measured numbers stay, attributed as hand-measured. 519 tests pass - ruff clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
#352 asks for a sweep. Doing it by hand first showed why a sweep is the wrong
deliverable.
Every candidate
CAPACITYnode turns out to duplicate its own record's trait —salt_tolerancebesideMETPO:1000625,low_ph_tolerancebesideMETPO:1000232,growth_at_4cbesideMETPO:1000618. Retyping them toTRAITwithout merging wouldrecreate precisely the ungrounded-duplicate-anchor problem #351's review caught. That's
seven modelling decisions, not a mechanical pass.
So: two checks, in the audit that already has the ratchet machinery
DUPLICATE_GROUNDINGgrounding— the machine-readable signature of one concept modelled twiceDISPOSITION_MISTYPEDCAPACITY/STATEnode whose own description reads as a disposition ("capacity to", "ability to", "tolerance of") — which is aTRAITBoth found more than my hand sweep did
Which is the argument for building them rather than doing one pass:
DUPLICATE_GROUNDINGfoundcatalase/catalase_functionsharingGO:0004096andurease/urease_functionsharingGO:0009039. Neither was known — both are aprotein node and a function node modelling one thing. Only the
ph_delta_highcase fromRetype six mis-typed dispositions, and route six real states (#334) #351 was on anyone's list.
DISPOSITION_MISTYPEDfound 8 where I'd found 6 — includingoxygen_tolerance(my pattern wanted "capacity to"; the record says "Capacity of a cell to") and
elongation_capacityinsphere_shaped.yaml.Anchored on the description, not the label, on purpose: the label is usually just the
concept name (
buoyancy) while the description is where the disposition shows.What was checked
and non-growing rather than guessing.
CAPACITYnode described as a disposition exits 1.collapse onto each other as
'', and that a genuine state ("Transmembrane protongradient…") is not flagged.
just qcgreen · 478 tests pass · ruff clean.Addresses the detection half of #352; the retyping/merging burn-down stays open.
🤖 Generated with Claude Code