Skip to content

Check the whole enables range, not just TRAIT objects (#315) - #335

Merged
realmarcin merged 2 commits into
mainfrom
feat/315-inherited-range-check
Aug 6, 2026
Merged

Check the whole enables range, not just TRAIT objects (#315)#335
realmarcin merged 2 commits into
mainfrom
feat/315-inherited-range-check

Conversation

@realmarcin

Copy link
Copy Markdown
Contributor

#315 asked for the inherited-range half of the METPO:2000001 family, to match the
domain half. Reconciling it first showed that check would be both empty and redundant
and pointed at a real gap somewhere else.

Why the literal request was the wrong target

The domain test flags any use of those 67 predicates unconditionally, because no causal
node type can satisfy a microbe domain. A range test on the same family can therefore only
re-flag a subset of the same edges. And after #301, the corpus has zero of them. So the
literal ask would have added code that can never fire.

The real gap was in the check that already existed

#302 framed its problem as "a TRAIT is a disposition, not a process", and the code matched
that framing literally:

if pid == ENABLES and node_type.get(obj) == "TRAIT":

But biolink declares enables range 'biological process or activity'. The test was
checking one member of the complement instead of the range itself. Widening it surfaces
33 edges that stayed invisible through the entire #301/#302/#303 effort:

object node_type edges example
GENE_OR_PROTEIN 13 mreB --enables--> rod_complex
STATE 8 phototrophy_trait --enables--> proton_motive_force
QUALITY 4 rod_complex_rotation --enables--> even_pg_distribution
CAPACITY 4 compatible_solute_accumulation --enables--> hhp_tolerance
CHEMICAL 3 pfa_operon --enables--> omega3_pufa
CELLULAR_LOCALIZATION 1 scy --enables--> apical_polarisome

ENABLES_RANGE_ON_TRAIT is renamed ENABLES_RANGE_VIOLATION and now tests membership
of the three activity node types.

On reintroducing --fail-on new

The 33 mix several idioms and need per-edge biological judgement, so they are baselined
and tracked in #334 rather than swept. This is exactly the case the ratchet machinery
was deliberately kept for in #327 — a class that has never been clean.

The distinction is enforced, not just asserted: MICROBE_DOMAIN_ON_NONORGANISM has
zero rows in the baseline, so a regression there is new and still fails. The justfile
comment spells out that a baseline is for a class that has never been clean, never for one
that has, and says not to add rows to excuse a regression.

What was checked

Canaried all three paths rather than reasoning about them:

scenario expected got
clean corpus pass exit 0
injected microbe-domain edge fail exit 1
injected enables --> QUALITY edge fail exit 1

Plus: just qc green · 423 tests pass (two new — a QUALITY object flags,
PATHWAY/MOLECULAR_FUNCTION stay clean) · ruff clean · baseline contains exactly 33 rows,
0 of them domain findings.

Closes #315.

🤖 Generated with Claude Code

#315 asked for the inherited-RANGE half of the METPO:2000001 family, to
match the domain half. Reconciling it first showed that check would be
both empty and redundant: the domain test flags ANY use of those 67
predicates unconditionally (no causal node type can satisfy a microbe
domain), so a range test on the same family can only re-flag a subset of
the same edges -- and after #301 the corpus has zero of them.

The real range gap was elsewhere, in the check that already existed.
#302 framed its problem as "a TRAIT is a disposition, not a process" and
the code matched that framing literally: object_type == "TRAIT". But
biolink declares enables range 'biological process or activity', so the
test was checking one member of the complement instead of the range.
Widening it to the range itself surfaces 33 edges that were invisible
through the whole #301/#302/#303 effort -- objects typed GENE_OR_PROTEIN
(13), STATE (8), QUALITY (4), CAPACITY (4), CHEMICAL (3) and
CELLULAR_LOCALIZATION (1).

ENABLES_RANGE_ON_TRAIT is therefore renamed ENABLES_RANGE_VIOLATION and
now tests membership of the three activity node types.

The 33 mix several idioms and need per-edge biological judgement, so they
are baselined and tracked in #334 rather than swept. That is the case the
ratchet machinery was deliberately kept for in #327, and the recipe goes
back to --fail-on new.

The distinction matters and is enforced, not just asserted:
MICROBE_DOMAIN_ON_NONORGANISM has ZERO rows in the baseline, so a
regression there is `new` and still fails. Canaried all three paths -- a
clean corpus passes, an injected microbe-domain edge fails, and an
injected enables->QUALITY edge fails.

Two new tests cover the widened range: a QUALITY object flags, and
PATHWAY/MOLECULAR_FUNCTION objects stay clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@culturebot-reviewer

culturebot-reviewer Bot commented Aug 6, 2026

Copy link
Copy Markdown

Claude finished @realmarcin's task in 3m 52s —— View job


Review — PR #335

  • Read repo conventions (README, docs/CURATION_PLAYBOOK.md)
  • Read the diff
  • Verified the range widening against data/raw/biolink-model.yaml and CausalNodeTypeEnum
  • Checked the baseline / ratchet logic
  • Posted verdict (--request-changes)

The widening itself checks out. biolink enables is domain: physical entity / range: biological process or activity with exact_mappings: RO:0002327 (data/raw/biolink-model.yaml:5099), and pathway is_a biological process (:8586), so ACTIVITY_NODE_TYPES = {BIOLOGICAL_PROCESS, PATHWAY, MOLECULAR_FUNCTION} is the correct partition of CausalNodeTypeEnum — no other member of that enum is an occurrent. The baseline is 33 rows, all ENABLES_RANGE_VIOLATION, zero MICROBE_DOMAIN_ON_NONORGANISM, so the claim that a domain regression is still new and still fails holds. _key gives each edge its own discriminator, so a new violating edge in an already-baselined file still blocks.

Two documentation defects, both the same class: the change was reasoned through in the justfile comment but the two places a human actually reads were left asserting the opposite.

🟡 docs/CURATION_PLAYBOOK.md:154-161 — the curator-facing rule is now wrong

Do not use enables for a TRAIT object.The count is now 0, and the audit (just audit-predicate-domains) fails any new one.

After this PR the rule is "the object must be BIOLOGICAL_PROCESS, PATHWAY or MOLECULAR_FUNCTION" and the count is 33. A curator following the playbook as written will produce <gene> enables <protein complex> — 13 of the 33 baselined rows are exactly that shape — and hit a CI failure the playbook does not explain, with no replacement predicate offered for the non-TRAIT shapes the way the v8 table offers one for TRAIT. :126-127 also still says the gate runs --fail-on any inside just qc. Fix this →

🟡 scripts/audit_predicate_domains.py:33-44, :51-64 — module docstring contradicts what ships

Both classes are now BURNED DOWN and the check runs as a hard gate … so the count is 0 and just audit-predicate-domains passes --fail-on any. No baseline file is tracked any more.do not reintroduce a baseline to make a new violation pass

A baseline file is added in this PR and the gate is --fail-on new. The per-defect block at the top was rewritten; this body was not — so the file now argues against its own configuration. The nuanced version already exists in the justfile; it belongs here, since this is the file the next person opens. Usage at :62 also still advertises the bare invocation, which now exits 1 — worth a line on why the argparse default stays any while the gate passes new. Fix this →

🔵 The class distinction is asserted more strongly than it is enforced

The description says it is "enforced, not just asserted" — but the enforcement is only that the baseline currently happens to have zero domain rows. --write-baseline freezes every WARN, so one run of it would silently freeze a domain regression, which is precisely the failure mode the justfile comment warns against in prose. The structural mechanism already exists and the comment at :99-101 points at it: promote MICROBE_DOMAIN_ON_NONORGANISM to ERROR and --write-baseline refuses to freeze it (:278-287).

Not blocking

No data/traits/** content changed, so no history/ record is expected here.

I could not run just qc or pytest — command approval was denied in this environment — so the 423-tests/exit-code table in the description is unverified by me. It is consistent with the code as read: the two new tests are well-formed, and _write returns the shared traits dir so the PATHWAY/MOLECULAR_FUNCTION loop does exercise both files.
· branch

@culturebot-reviewer culturebot-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two stale-documentation defects, no code defect.

The widening itself checks out: biolink enables is domain physical entity / range biological process or activity with exact_mappings: RO:0002327 (data/raw/biolink-model.yaml:5099), pathway is_a biological process (:8586), so ACTIVITY_NODE_TYPES = {BIOLOGICAL_PROCESS, PATHWAY, MOLECULAR_FUNCTION} is the correct partition of CausalNodeTypeEnum. The baseline is 33 rows, all ENABLES_RANGE_VIOLATION, zero MICROBE_DOMAIN_ON_NONORGANISM — so the claim that a domain regression is still new and still fails holds.

What needs changing:

docs/CURATION_PLAYBOOK.md:154-161 still states the rule as "Do not use enables for a TRAIT object … The count is now 0, and the audit fails any new one." After this PR the rule is "the object must be BIOLOGICAL_PROCESS, PATHWAY or MOLECULAR_FUNCTION", and the count is 33. A curator following the playbook will write <gene> enables <protein complex> — 13 of the 33 baselined rows are exactly that shape — and hit a CI failure the playbook does not explain, with no replacement predicate offered for the non-TRAIT shapes. Line 126-127 also still says the gate runs --fail-on any inside just qc. This is the curator-facing contract; the justfile comment was updated carefully but this was not.

scripts/audit_predicate_domains.py:33-44 and :51-64 contradict what the PR ships: "Both classes are now BURNED DOWN and the check runs as a hard gate", "the count is 0 and just audit-predicate-domains passes --fail-on any. No baseline file is tracked any more", and "do not reintroduce a baseline to make a new violation pass". A baseline file is added in this PR and the gate is --fail-on new. The per-defect block at the top was rewritten; this body was not. The nuanced version already exists in the justfile — it belongs here, since this is the file the next person opens. Worth a line in Usage too on why the argparse default stays any while the gate passes new (the bare invocation at :62 now exits 1).

Suggestion, optional: the PR says the class distinction is "enforced, not just asserted", but the enforcement is only that the baseline happens to have zero domain rows — a single --write-baseline run would freeze a domain regression, since it freezes every WARN. The mechanism to make that structural already exists and the comment at :99-101 points at it: promote MICROBE_DOMAIN_ON_NONORGANISM to ERROR, and --write-baseline refuses to freeze it (:278-287).

Note: I could not run just qc or pytest in this environment (command approval denied), so the test/exit-code claims in the description are unverified by me; they are consistent with the code as read.

… review)

The 🔵 was the important one: the description claimed the domain/range
class distinction was "enforced, not just asserted", but the only
enforcement was that the baseline happened to contain zero domain rows.
--write-baseline freezes every WARN, so a single run intended to
re-freeze the ENABLES_RANGE_VIOLATION backlog would have silently
swallowed a domain regression -- exactly the failure the justfile comment
warns against in prose.

Now structural, using the mechanism that already existed:
MICROBE_DOMAIN_ON_NONORGANISM is ERROR severity, and --write-baseline
refuses to freeze an ERROR. Canaried: with a domain regression present it
prints "Refusing to write baseline" and writes nothing.
ENABLES_RANGE_VIOLATION stays WARN because its 33 edges must remain
baselineable until #334 burns them down; promote it the same way then.

That severity change correctly broke three tests that had encoded the
old contract. Rewritten, plus a new
test_domain_class_is_error_so_it_cannot_be_baselined pinning the
guarantee so it cannot regress to a convention again.

Two staleness findings, both fair, and both the same failure I made
before: updating a table and leaving the prose beside it.

- The module docstring still said "No baseline file is tracked any more"
  and "do not reintroduce a baseline", while this PR adds one. It now
  explains the two classes are at different stages and why, mirroring the
  justfile reasoning, since that file is what the next person opens.
  The --fail-on and Usage blocks now say the argparse default stays `any`
  (so a stray baseline cannot weaken an ad-hoc run) while the recipe
  passes `new`, and that a bare invocation therefore exits 1 today.
- docs/CURATION_PLAYBOOK.md still stated the enables rule as "not a TRAIT
  object" with count 0 and --fail-on any. A curator following it would
  write `<gene> enables <protein complex>` -- 13 of the 33 baselined rows
  are that exact shape -- and hit a CI failure the page did not explain.
  It now states the rule as the range, gives the count and the gate mode,
  and adds a table of what to write instead for each object type,
  including "not settled -- see #334" where that is the honest answer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@realmarcin

Copy link
Copy Markdown
Contributor Author

All three fixed. The 🔵 was the most valuable — you were right that my "enforced, not just asserted" claim was only true by accident.

🔵 The class distinction is now structural

The only enforcement was that the baseline happened to contain zero domain rows. --write-baseline freezes every WARN, so one run intended to re-freeze the ENABLES_RANGE_VIOLATION backlog would have silently swallowed a domain regression — precisely what the justfile comment warns against in prose.

Took your suggestion and used the mechanism that already existed: MICROBE_DOMAIN_ON_NONORGANISM is now ERROR, and --write-baseline refuses to freeze an ERROR. Canaried with a domain regression present:

Refusing to write baseline: 1 ERROR-severity finding(s) present.
  MICROBE_DOMAIN_ON_NONORGANISM  ...rrna_operon_copy_number.yaml  cd2--METPO:2000202-->rrna_copy_trait

…and no baseline written. ENABLES_RANGE_VIOLATION stays WARN because its 33 must remain baselineable until #334 burns them down — promote it the same way then.

That change correctly broke three tests encoding the old contract. Rewritten, plus test_domain_class_is_error_so_it_cannot_be_baselined pinning the guarantee so it can't quietly regress to a convention again.

🟡 Module docstring contradicting its own config

Fair, and it's the same mistake I made in #329 — updating a table and leaving the prose beside it. It now explains that the two classes are at different stages and why, mirroring the justfile reasoning since this is the file the next person opens. The --fail-on/Usage blocks now state that the argparse default stays any (so a stray baseline can't weaken an ad-hoc run) while the recipe passes new, and that a bare invocation therefore exits 1 while #334 stands.

🟡 Playbook

Also fair, and the consequence you identified is the real cost: a curator following it would write <gene> enables <protein complex>13 of the 33 baselined rows are that exact shape — and hit a CI failure the page didn't explain, with no replacement offered.

New subsection states the rule as the range, gives the count and gate mode, and adds a table of what to write instead per object type — confers, has output, derives into, decreases — with "not settled — see #334" where that's the honest answer rather than inventing guidance. Plus the fallback that usually works: point enables at the graph's process node rather than the entity.

Re-verified

just qc green · 424 tests pass · ruff clean · baseline still exactly 33 rows, 0 of them domain findings.

@culturebot-reviewer

culturebot-reviewer Bot commented Aug 6, 2026

Copy link
Copy Markdown

Claude finished @realmarcin's task in 3m 15s —— View job


Re-review — PR #335 (follow-up commit f312863a)

  • Read the new commit
  • Verify 🔵 — class distinction now structural
  • Verify 🟡 — module docstring
  • Verify 🟡 — curation playbook
  • Check the rewritten tests
  • Verify the four CURIEs the new playbook table tells curators to use
  • Posted verdict (--approve)

All three prior findings are addressed, and the 🔵 was addressed properly rather than by rewording the description. SEVERITY["MICROBE_DOMAIN_ON_NONORGANISM"] = ERROR (scripts/audit_predicate_domains.py:120) plugs into the pre-existing refusal at :300-308, and test_domain_class_is_error_so_it_cannot_be_baselined (tests/…:307-322) asserts exit 1, the stderr message, and not baseline.exists() — so it pins the write-nothing behaviour, not just the exit code. Retargeting test_write_baseline_freezes_then_passes and test_default_fail_on_is_any onto ENABLES_ON_QUALITY is the right fix: those tests are about the ratchet, and the ratchet's baselineable class is now the WARN one.

I checked the four replacement predicates in the new playbook table against mappings/predicate_grounding.tsv, since a plausible-but-wrong CURIE in curator-facing guidance is the worst thing that can ship here. All four resolve and the gates agree with the shapes the table recommends: confers/METPO:2007700 (:102, object_types is TRAIT), has output/RO:0002234 (:105, object unconstrained), derives into/RO:0001001 (:108, CHEMICALCHEMICAL), decreases/RO:0002212 (:61, and consistent with the sense-split from #330/#333). The two not settled — see #334 rows are the honest answer rather than invented guidance.

Baseline verified independently: 33 rows, grep -c MICROBE_DOMAIN_ON_NONORGANISM → 0, grep -c object_type=TRAIT → 0. That last one matters because it means the older paragraph's "the count is now 0" is still literally true for the TRAIT sub-case.

Two 🔵s, nothing blocking.

🔵 docs/CURATION_PLAYBOOK.md:175-178 — a paragraph got orphaned by the insertion point

The new ### enables needs a process-or-activity object heading was inserted at :150, but the microbe-domain section it split did not end at :148. This paragraph:

The organism-subject form stays valid at the assertion site — <organism> METPO:2000006 CHEBI:17234 … It is only the causal-graph reuse that is wrong.

is about METPO:2000006 (uses carbon source) and belonged to the domain rule; on origin/main it sat directly after :148. It now reads as a caveat on the enables rule, where a reader can take it as licensing <organism> enables … at the assertion site. Moving it (and the **Do not use enables for a TRAIT object** block at :180-187, which the new general section supersedes and which now states the special case after the general one) above the new ### heading would restore both narratives.

🔵 The structural guarantee is real for the accidental path, and the docstring says so precisely — the playbook overstates it

--write-baseline refuses ERROR, so the accidental path is closed, which is what the docstring claims (:35-38, "even by accident") and what the new test pins. But partition (:245-255) still forgives any baselined finding under --fail-on new regardless of severity, so a hand-added domain row in conf/predicate_domain_audit_baseline.tsv would still pass just qc. The justfile at :113-117 covers that path in prose only ("Do NOT add rows…"), which was accurate before and is now the weaker half of the story it doesn't mention.

docs/CURATION_PLAYBOOK.md:128-129 states the strong form without the scope: "a new one fails CI and cannot be baselined away." Either make it true — have partition always block ERROR findings, baselined or not, which also lets you delete the convention from the justfile — or scope the sentence to match the docstring's wording. Fix this →

Not blocking

No data/traits/** content changed, so no history/ record is expected.

As last time I could not run just qc or pytest — command approval was denied in this environment — so the 424-tests claim is unverified by me. It is consistent with the code as read: _run_cli is defined below its first use but only called at test time, the ERROR check in main() precedes any baseline mkdir/open so not baseline.exists() will hold, and ENABLES_ON_QUALITY produces exactly one WARN finding, which is what the two retargeted ratchet tests need.
· branch

@culturebot-reviewer culturebot-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All three prior findings are addressed, and the 🔵 was addressed properly rather than by rewording the description. SEVERITY["MICROBE_DOMAIN_ON_NONORGANISM"] = ERROR plugs into the pre-existing refusal at scripts/audit_predicate_domains.py:300-308, and the new test pins exit 1, the stderr message, and not baseline.exists() — the write-nothing behaviour, not just the exit code. Retargeting the two ratchet tests onto ENABLES_ON_QUALITY is right: they are about the ratchet, and the baselineable class is now the WARN one.

I checked the four replacement predicates in the new playbook table against mappings/predicate_grounding.tsv, since a plausible-but-wrong CURIE in curator-facing guidance is the worst thing that can ship here. All four resolve and the gates agree with the recommended shapes: confers/METPO:2007700 (object_types TRAIT), has output/RO:0002234 (object unconstrained), derives into/RO:0001001 (CHEMICAL→CHEMICAL), decreases/RO:0002212 (consistent with the #330/#333 sense-split). The two 'not settled — see #334' rows are the honest answer rather than invented guidance. Baseline verified: 33 rows, 0 domain findings, 0 TRAIT-object rows — so the older paragraph's 'the count is now 0' is still literally true for the TRAIT sub-case.

Two optional 🔵s, neither blocking:

  1. docs/CURATION_PLAYBOOK.md:175-178 — the new ### enables needs a process-or-activity object heading was inserted at :150, but the microbe-domain section it split did not end at :148. The 'organism-subject form stays valid at the assertion site' paragraph is about METPO:2000006 and on origin/main sat directly after :148; it now reads as a caveat on the enables rule, where it can be taken as licensing <organism> enables …. Same for the 'Do not use enables for a TRAIT object' block at :180-187, which the new general section supersedes and which now states the special case after the general one. Moving both above the new heading restores the two narratives.

  2. The structural guarantee is real for the accidental path and the docstring scopes it precisely ('even by accident'). But partition() at :245-255 still forgives any baselined finding under --fail-on new regardless of severity, so a hand-added domain row would still pass just qc — the justfile at :113-117 covers that path in prose only. docs/CURATION_PLAYBOOK.md:128-129 states the strong form without the scope: 'a new one fails CI and cannot be baselined away.' Either make it true (have partition always block ERROR findings, baselined or not, which also lets you delete the convention from the justfile) or scope that sentence to match the docstring.

Not blocking: no data/traits/** content changed, so no history/ record is expected.

As last time I could not run just qc or pytest in this environment (command approval denied), so the 424-tests claim is unverified by me. It is consistent with the code as read: _run_cli is defined below its first use but only called at test time, the ERROR check in main() precedes any baseline mkdir/open so not baseline.exists() holds, and ENABLES_ON_QUALITY produces exactly one WARN finding, which is what the two retargeted ratchet tests need.

@realmarcin
realmarcin merged commit 084cba4 into main Aug 6, 2026
5 checks passed
@realmarcin
realmarcin deleted the feat/315-inherited-range-check branch August 6, 2026 07:30
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.

Predicate domain audit checks the inherited domain but not the inherited range

1 participant