Skip to content

Constrain predicate groundings by node type (#236) - #239

Merged
realmarcin merged 4 commits into
mainfrom
feat/predicate-node-type-constraints
Aug 4, 2026
Merged

Constrain predicate groundings by node type (#236)#239
realmarcin merged 4 commits into
mainfrom
feat/predicate-node-type-constraints

Conversation

@realmarcin

Copy link
Copy Markdown
Contributor

Closes #236. The precondition for fanning out the grounding loop.

What #235 established

An exact label match is not sufficient to ground a predicate, and nothing in the repo could have caught it. causally upstream of IS the label of RO:0002411, so it passed the id↔label gate — but RO defines the relation over occurrents, and all 13 corpus edges carrying it connect material entities. The gate compares a CURIE to its label and never looks at the edge; predicate_id is an unbound string in the schema; nothing in qc inspects domain or range.

The mechanism

mappings/predicate_grounding.tsv gains subject_types and object_types|-separated CausalNodeTypeEnum names, or * for any. All 99 existing rows are */* explicitly rather than by omission, so the file states its own permissiveness instead of relying on a missing column.

ground_causal_predicates.py builds a node_id → node_type map per graph and refuses edges outside the declared types. They stay in the residual — a wrong grounding is worse than a missing one — and are reported under their own heading, because folding them into the plain residual would read as "no CURIE known", which is the opposite of the situation.

It demonstrably blocks the mistake it was built for

causally upstream of is restored to the mapping with a process constraint. It grounds zero edges, deliberately: the CURIE is right, the corpus usage is not, and recording that is more useful than leaving 13 edges silently residual with no explanation.

blocked by node-type constraint: 13
  causally upstream of (CHEMICAL->CHEMICAL)         6
  causally upstream of (GENE_OR_PROTEIN->CHEMICAL)  6
  causally upstream of (GENE_OR_PROTEIN->CAPACITY)  1

Exactly the three shapes the #235 review identified.

Tests

Six new, plus the nine existing ones updated for the wider signature: blocked outside the types, grounded inside them, object type alone can block, unconstrained rows still ground anything, a node with no declared type cannot satisfy a constraint, and both * and empty parse as "any".

No corpus change

0 newly grounded, residual unchanged at 1221, just qc exits 0, 232 tests pass.

Whether to constrain the regulation relations — RO:0002213 is 27/206 process→process (13%), RO:0002212 is 9/102 (8%) — is the separate curation decision #236 records. It now has a mechanism to express, which it did not before.

🤖 Generated with Claude Code

#235 established that an exact label match is not sufficient to ground a
predicate, and that nothing in the repo could have caught it. `causally
upstream of` IS the label of RO:0002411, so it passed the id↔label gate — but
RO defines the relation over occurrents, and all 13 corpus edges carrying it
connect material entities. The gate compares a CURIE to its label and never
looks at the edge; predicate_id is an unbound string in the schema; nothing
in qc inspects domain or range.

mappings/predicate_grounding.tsv gains subject_types and object_types:
`|`-separated CausalNodeTypeEnum names, or `*` for any. All 99 existing rows
are `*`/`*` explicitly rather than by omission, so the file states its own
permissiveness instead of relying on a missing column.

ground_causal_predicates.py builds a node_id->node_type map per graph and
refuses edges outside the declared types. They stay in the residual — a wrong
grounding is worse than a missing one — and are reported under their own
heading with the shape that failed, because folding them into the plain
residual would read as "no CURIE known", which is the opposite of the truth.

`causally upstream of` is restored to the mapping WITH a process constraint.
It grounds zero edges, deliberately: the CURIE is right, the corpus usage is
not, and recording that is more useful than leaving 13 edges silently
residual with no explanation. Running it now prints exactly the three shapes
the #235 review identified:

  causally upstream of (CHEMICAL->CHEMICAL)         6
  causally upstream of (GENE_OR_PROTEIN->CHEMICAL)  6
  causally upstream of (GENE_OR_PROTEIN->CAPACITY)  1

So the mechanism demonstrably blocks the exact mistake it was built for.

Six new tests plus the nine existing ones updated for the wider signature:
blocked outside the types, grounded inside them, object type alone can block,
unconstrained rows still ground anything, a node with no declared type cannot
satisfy a constraint, and both `*` and empty parse as "any".

No corpus change: 0 newly grounded, residual unchanged at 1221. Whether to
constrain the regulation relations — RO:0002213 is 13% process->process,
RO:0002212 is 8% — is the separate curation decision #236 records, and now
has a mechanism to express.

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

culturebot-reviewer Bot commented Aug 4, 2026

Copy link
Copy Markdown

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


Review — PR #239

The mechanism itself is right, and the RO:0002411 constraint is semantically sound (GO molecular_function sits under occurrent in the BFO alignment, so all three permitted types are occurrents; the three names used are real CausalNodeTypeEnum values). Making all 99 existing rows explicitly * rather than relying on a missing column is the correct call, and the test for "a node with no declared type cannot satisfy a constraint" is the one people usually forget.

The problem is that the block is only visible on stderr, which does not survive the run. Every durable artifact still says "no CURIE known" — the thing the code comment at scripts/ground_causal_predicates.py:237 explicitly says would be wrong.

🟡 The residual TSV does not distinguish blocked rows from unmapped ones

ground_edges_in_doc puts blocked edges into residual (line 131), and the writer at scripts/ground_causal_predicates.py:222 emits only predicate_label, edge_count, example_files. So reports/predicate_grounding_residual.tsv:12 keeps reading:

causally upstream of	13	data/traits/metabolism/propionic_acid_fermentation.yaml|…

indistinguishable from the ~1200 genuinely unmapped labels. That file is the durable, committed, staleness-gated one, and it is a documented input contract: .claude-pr/.claude/commands/ground-or-propose-metpo.md:32 names those exact three columns and treats every row as a grounding-or-proposal candidate. A curator or that workflow reading the file sees a frequency-ranked label with no CURIE and does the obvious thing — which is the loop #235 closed. A status column (unmapped / blocked_by_node_type) on the residual writer, plus a regenerated reports/predicate_grounding_residual.tsv, closes it. Fix this →

🟡 NEXT_TASKS.md §9 still queues causally upstream of and still asserts the gate this PR disproves

NEXT_TASKS.md:520-527 lists causally upstream of | 13 | RO:0002411 | — as a ready-to-apply backfill row, and lines 539-542 state that each mapping row "is verified by the existing blocking label-correspondence gate … so a wrong CURIE fails CI rather than landing silently." That is exactly the belief #235 falsified, in the standing backlog doc, unedited by the PR that exists to correct it. Two sentences in §9 recording that this row is deliberately blocked by domain/range would prevent someone acting on the table. Fix this →

🟡 Nothing validates that the type names are real CausalNodeTypeEnum values

_types() (scripts/ground_causal_predicates.py:53) accepts any string. BIOLOGICAL_PROCCESS would block 100% of that row's edges, and the failure is indistinguishable from the row not existing — the edges just land in the residual, where by the point above they look merely unmapped. The enum is right there at src/traitmech/schema/traitmech.yaml:485; having load_mapping raise on an unknown name costs a few lines and is the difference between "the file states its own constraints" and "the file states something that may or may not be a constraint." Prospective, not a present data error — the one constrained row is correct today. Fix this →

🔵 One-way gate, not an invariant

The type check is reached only for edges with an empty predicate_id — an already-grounded edge returns at line 125 before it. The corpus has zero RO:0002411 today so nothing is currently wrong, but a violating grounding applied by hand, or by a future script, is never surfaced. A qc check reading the same two columns over grounded edges would turn this into a ratchet. Reasonable as follow-up, not for this PR.


Not verified: I could not execute just test, just qc, or the dry-run — uv/just/python invocations are outside this job's allowed tools, so the "232 tests pass, 0 newly grounded, residual unchanged at 1221" claims are unchecked by me. Everything above is static reading. The committed reports/predicate_grounding_residual.tsv is consistent with "residual unchanged," which is at least indirect support.

Also worth a glance: check_biolink_coverage.py reads both the mapping and the residual, so causally upstream of will now appear twice in reports/biolink_coverage.tsv — once applied, once residual. That report is not in the staleness gate, so it will just drift; mentioning it only so it isn't a surprise later.

No history/ record is needed here — no data/traits/** file changes.

Requesting changes on the three 🟡 items. None of them touch the corpus; they're all about making the block legible outside a terminal that has since scrolled.
· branch feat/predicate-node-type-constraints

@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.

Mechanism is right and the RO:0002411 constraint is semantically sound. Requesting changes on three points, all about the block being visible only on stderr: (1) the residual TSV does not distinguish blocked rows from unmapped ones, and that file is a documented input contract for the ground-or-propose-metpo workflow; (2) NEXT_TASKS.md section 9 still queues 'causally upstream of' for backfill and still asserts the label-correspondence gate is sufficient; (3) subject_types/object_types values are never validated against CausalNodeTypeEnum, so a typo silently blocks every edge. Details in the review comment.

All three 🟡s were about the same gap — the constraint fired, but only into a
stderr summary that scrolls away.

The residual TSV is the durable work queue, and it listed a blocked label
identically to an unmapped one. A curator reading "causally upstream of, 13"
would try to map it again, which is exactly the mistake the constraint just
prevented. Added `status` and `blocked_by` columns: blocked rows now read
`blocked_by_node_type` / `RO:0002411`.

Nothing validated the type names, so a typo like BIOLOGICAL_PROCES would
silently block every edge the row could ground. The permissible values are now
read from CausalNodeTypeEnum in the schema — rather than hardcoded, so a
schema change cannot leave this file quietly disagreeing — and an unknown name
raises. A constraint nobody can see is worse than no constraint.

NEXT_TASKS section 9 still queued `causally upstream of` as a free win and
still credited label-correspondence with making the loop safe, which this work
disproved. It now strikes that row, says why an exact label match was not
sufficient, and points at the type columns as the thing that actually checks
it — including the caveat that rows left at */* are unconstrained, so a new
mapping is only as safe as the constraint written beside it.

233 tests.

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

culturebot-reviewer Bot commented Aug 4, 2026

Copy link
Copy Markdown

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


Review — PR #239 (re-review after 2a9d21cc)

Two of the three 🟡s are properly closed. The residual TSV now carries status/blocked_by and reports/predicate_grounding_residual.tsv:12 reads causally upstream of 13 blocked_by_node_type RO:0002411 — the durable artifact finally says what happened. _node_type_values() reading CausalNodeTypeEnum out of the schema rather than hardcoding it is the better version of what I asked for; a schema rename can't leave the mapping file quietly disagreeing. And NEXT_TASKS.md §9 now strikes the row and replaces the label-correspondence claim with the caveat that */* rows are unconstrained, which is the part someone would otherwise trip on.

Two things left, both small.

🟡 The consuming workflow's input contract was not updated

.claude/commands/ground-or-propose-metpo.md is the tracked file that drives the grounding loop, and it is untouched by this PR:

  • Line 32 still declares the columns as predicate_label, edge_count, example_files — the status column that carries the whole point of this change is absent from the contract.
  • Lines 20–22 still name causally upstream of in the head of the worklist, and step 1 (line 47) says to filter by min-freq/category/label and rank by frequency — no mention of skipping blocked rows.

So an agent running /ground-or-propose-metpo predicates min-freq 2 still gets a 13-edge label near the head with no instruction to leave it alone. The TSV now tells the truth; the doc that tells the reader how to consume it does not. Adding status to the column list and one line in step 1 — "skip rows with status: blocked_by_node_type; the CURIE is known and deliberately withheld" — is the whole fix. (The .claude-pr/ copy is a job artifact, not tracked; only the .claude/ one needs editing.) Fix this →

🟡 blocked_labels recovers the label by string-splitting a display key

scripts/ground_causal_predicates.py:253:

blocked_labels = {shape.split(" (")[0] for shape in blocked_total}

blocked_total is keyed by the rendered shape f"{pred} ({s_type}->{o_type})" (line 154), and the label is recovered by splitting on the first " (". Predicate labels containing " (" exist in the corpus today — reports/predicate_grounding_residual.tsv:477 is positively influences (saturating). Give that label (or any sibling) a type constraint and:

  • its blocked shape truncates to positively influences, so the real row is written unmapped with an empty blocked_by — the precise misreport the two new columns exist to prevent; and
  • if positively influences is itself residual-and-unmapped, it matches the truncated key, is_blocked is true, and line 259 does mapping["positively influences"][0]KeyError, failing the staleness gate in just qc for a reason that points nowhere near the cause.

The counter is doing double duty as data and as display text. Returning the label alongside the shape — key blocked on (pred, shape), or return a separate blocked_labels set from ground_edges_in_doc — removes the parse entirely. [Fix this →](https://claude.ai/code?q=On%20branch%20feat%2Fpredicate-node-type-constraints%3A%20in%20scripts%2Fground_causal_predicates.py%2C%20line%20253%20recovers%20a%20predicate%20label%20from%20the%20blocked%20counter%20key%20via%20shape.split(%22%20(%22)%5B0%5D%2C%20but%20the%20counter%20key%20is%20a%20display%20string%20%60pred%20(SUBJ-%3EOBJ)%60%20and%20predicate%20labels%20can%20themselves%20contain%20%22%20(%22%20-%20e.g.%20%60positively%20influences%20(saturating)%60.%20That%20truncates%20the%20label%2C%20mislabels%20a%20blocked%20row%20as%20unmapped%2C%20and%20can%20KeyError%20on%20mapping%5Blabel%5D%5B0%5D.%20Key%20the%20blocked%20counter%20on%20(pred%2C%20shape)%20or%20return%20a%20separate%20set%20of%20blocked%20labels%20from%20ground_edges_in_doc%2C%20and%20add%20a%20test%20with%20a%20parenthesised%20predicate%20label.&repo=CultureBotAI/TraitMech)

Checked and clear

check_biolink_coverage.py:71 reads the residual with csv.DictReader, so the two new columns don't break it — the applied/residual double-listing of causally upstream of I flagged last time is still cosmetic drift in an ungated report. Header order in mappings/predicate_grounding.tsv matches what load_mapping reads by name. The blockedmapping[label][0] lookup is safe for every label that isn't hit by the splitting issue above, since blocked implies mapped.

Not verified: uv/just/python are still outside this job's allowed tools, so "233 tests" and the regenerated residual being byte-identical to what the generator produces are unchecked by me — the file's shape (571 lines, header + 570 labels, the blocked row correctly populated) is consistent with the claim, but just qc is the thing that actually proves it.

No history/ record needed — no data/traits/** changes.

Requesting changes on the two 🟡s. Neither touches the corpus.
· branch feat/predicate-node-type-constraints

@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 of the three earlier points are closed: the residual TSV now carries status/blocked_by and reads 'causally upstream of 13 blocked_by_node_type RO:0002411', and the enum values are read from CausalNodeTypeEnum rather than hardcoded. NEXT_TASKS section 9 is corrected.

Two left: (1) .claude/commands/ground-or-propose-metpo.md still documents the old three-column contract and still names 'causally upstream of' in the head of the worklist, with no instruction to skip blocked rows — the consuming workflow is the loop this PR exists to close; (2) ground_causal_predicates.py:253 recovers the predicate label by splitting the display key on ' (', and labels containing ' (' exist in the corpus today ('positively influences (saturating)'), so a constrained one would be mislabeled 'unmapped' or KeyError in the staleness gate. Details in the review comment.

blocked_total was keyed by the rendered string f"{pred} ({s}->{o})" and the
label recovered with split(" (")[0]. That truncates any label containing " (",
and the corpus has one — `positively influences (saturating)`. Give it a
constraint and the residual TSV would mark `positively influences` blocked
while the real row read `unmapped`: the wrong row flagged, the right one
silently wrong. Keyed by the (label, shape) tuple now, with a test using that
exact corpus label.

.claude/commands/ground-or-propose-metpo.md is the tracked file that drives
this loop, and it was worse than out of date — line 64 explicitly instructed
`causally upstream of`→`RO:0002411`, the mistake this PR exists to prevent,
as a "straight to RO" freebie. Removed, with the reason stated.

Also updated there: the residual column list now includes status/blocked_by;
step 1 gains "skip every row whose status is blocked_by_node_type" with the
explanation that the CURIE is known and deliberately withheld; and the
head-of-worklist example no longer advertises `causally upstream of`.

Restored `causally upstream of or within`→`RO:0002418`, which I had removed as
collateral because it shared a line — it is a different term and dropping it
was not intended. It carries the same hazard, so the whole RO list now says
these are processual relations and each needs subject_types/object_types
rather than */*.

234 tests.

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

culturebot-reviewer Bot commented Aug 4, 2026

Copy link
Copy Markdown

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


Review — PR #239 (re-review after f408ee09)

Both 🟡s from the last round are closed, and closed well.

blocked is now keyed on the (label, shape) tuple (scripts/ground_causal_predicates.py:158), so blocked_labels at line 257 is a set comprehension over real labels rather than a parse of display text — the failure mode is gone rather than guarded, which is the better fix. The test uses positively influences (saturating), the actual corpus label at reports/predicate_grounding_residual.tsv:477, not a synthetic one. And .claude/commands/ground-or-propose-metpo.md now lists status/blocked_by in the column contract, carries the skip rule with the reason attached, and drops causally upstream of from the head-of-worklist example.

You also found something I missed: line 64 of that doc instructed causally upstream ofRO:0002411 as a no-research freebie. That was the live source of the error, not just documentation lagging it. Removing it is the more important half of that fix.

One new thing, introduced by this commit.

🟡 The new blanket domain claim in the RO list is wrong for about half the list

.claude/commands/ground-or-propose-metpo.md:61-63 now reads:

These are all processual relations — RO defines them over occurrents — so give each one subject_types/object_types in the mapping rather than leaving */*.

True for the regulation/causal subset — RO:0002213, RO:0002212, RO:0002211, RO:0002418, RO:0002629, RO:0002630. Not true for the rest of the same list:

  • enablesRO:0002327 — domain is material entity, range is the function. Your own mapping row says so: mappings/predicate_grounding.tsv:9 notes it is "common in causal-graph node→activity edges", i.e. GENE_OR_PROTEINMOLECULAR_FUNCTION.
  • part ofBFO:0000050 / has partBFO:0000051 — top-level parthood, deliberately domain-neutral over continuants and occurrents.
  • producesRO:0003000 — range is a material entity. mappings/predicate_grounding.tsv:24 calls it "the material-entity→product slot" in as many words.
  • RO:0002233/RO:0002234 (has input/output) — domain process, range continuant.

The instruction that follows ("give each one subject_types/object_types") is right; the premise handed to whoever acts on it is not. An agent reading "RO defines them over occurrents" and constraining enables to BIOLOGICAL_PROCESS|PATHWAY|MOLECULAR_FUNCTION on the subject side blocks 100% of the correct edges — and that constraint passes the new _node_type_values check, because the names are real. It is the #235 failure shape with the sign flipped: a plausible domain assumption applied without looking at what the relation is actually defined over, in the file that tells the next agent how to write domain constraints.

Scoping the sentence to the causal/regulatory group and saying explicitly that the participation and parthood relations are not occurrent-to-occurrent — check each one — is the whole fix. Fix this →

Checked and clear

  • test_unknown_node_type_name_is_fatal (tests/test_ground_causal_predicates.py:236) matches on the typo string, so it fails for the right reason.
  • mapping[label][0] if is_blocked else "" (line 263) — is_blocked now implies the label came out of blocked_total, which implies mapped. No KeyError path left.
  • Residual TSV header order matches the column list the command doc declares; reports/predicate_grounding_residual.tsv:12 still reads causally upstream of 13 blocked_by_node_type RO:0002411.
  • check_biolink_coverage.py:71 uses csv.DictReader — unaffected by the two columns. The applied/residual double-listing is still cosmetic drift in an ungated report.
  • The three types on mappings/predicate_grounding.tsv:101 are real CausalNodeTypeEnum values (src/traitmech/schema/traitmech.yaml:485-506) and are all occurrents, so the RO:0002411 constraint is sound.
  • Restoring causally upstream of or withinRO:0002418 is correct — different term, same occurrent domain, genuinely covered by the processual claim.

Not verified: uv/just/python are outside this job's allowed tools, so "234 tests" and the residual being byte-identical to what the generator emits are unchecked by me. just qc regenerates and diffs (justfile:377-390), so CI is the thing that actually proves the second one.

No history/ record needed — no data/traits/** changes.

Requesting changes on the one 🟡. It is a doc sentence, not corpus data, and it is the last thing outstanding.
· branch feat/predicate-node-type-constraints

@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.

Both prior 🟡s are properly closed: the blocked counter is keyed on (label, shape) so the split-on-' (' failure mode is removed rather than guarded, and the command doc now carries status/blocked_by plus the skip rule. Good catch on line 64 of that doc having actively instructed the RO:0002411 mapping — that was the live source of the error, not just lagging documentation.

One new 🟡, introduced by this commit: the sentence added at .claude/commands/ground-or-propose-metpo.md:61-63 says 'These are all processual relations — RO defines them over occurrents' about a list that includes enables (RO:0002327, domain material entity — your own mapping row at predicate_grounding.tsv:9 says 'node→activity edges'), part of / has part (BFO:0000050/51, domain-neutral), produces (RO:0003000, range material entity — predicate_grounding.tsv:24 calls it 'the material-entity→product slot'), and has input/output (RO:0002233/4, range continuant). An agent following that premise would constrain enables to process-only subject types and block 100% of its correct edges — and that constraint passes the new enum check, because the names are real. It is the #235 shape with the sign flipped, in the file that tells the next agent how to write domain constraints. Scope the processual claim to the regulation/causal subset and say the participation and parthood relations must be checked individually.

Details in the review comment. No corpus change involved.

"These are all processual relations" was true for the regulation and causal
subset and wrong for the rest of the same list. Checked against RO rather than
re-asserted: `enables` (RO:0002327) is "c enables p iff c is capable of p" —
material entity to process; `produces` (RO:0003000) is defined "where a and b
are material entities"; `part of`/`has part` (BFO) are deliberately
domain-neutral. The repo's own mapping notes said as much for two of them.

Replaced with the rule that actually generalises: write the type columns for
every row and read each relation's own domain and range to decide what they
are, with the four cases spelled out so the next reader does not have to
re-derive them. A wrong blanket rule in the file that drives the loop is worse
than none — it would have been applied to `enables` and `produces` on the next
run.

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

culturebot-reviewer Bot commented Aug 4, 2026

Copy link
Copy Markdown

Claude finished @realmarcin's task in 2m 39s —— View job


Review — PR #239 (re-review after b275c35c)

The last 🟡 is closed, and closed by checking rather than re-asserting. .claude/commands/ground-or-propose-metpo.md:62-71 now replaces the blanket "these are all processual relations" with the rule that actually generalises — write the type columns for every row, and read each relation's own domain and range to decide what they are — followed by the four cases spelled out:

  • regulation/causal (RO:0002211/2/3, RO:0002418, RO:0002629/30) → occurrents ✓
  • enables (RO:0002327) "c enables p iff c is capable of p" → material entity → process ✓
  • produces (RO:0003000) "where a and b are material entities"
  • part of/has part (BFO) deliberately domain-neutral ✓

That is the correct reading of each, and it matches what this repo's own mapping notes already said for two of them. Spelling out the cases rather than just deleting the wrong sentence is the better version — the next reader gets the four answers instead of a warning that there are no answers.

Re-checked from the earlier rounds — all still hold

  • blocked keyed on (label, shape) (scripts/ground_causal_predicates.py:154); blocked_labels at line 257 is a set comprehension over real labels. positively influences (saturating) is still a live corpus label (reports/predicate_grounding_residual.tsv:477) and the test uses it.
  • mapping[label][0] if is_blocked else "" (line 263) — is_blocked ⟸ present in blocked_total ⟸ present in mapping. No KeyError path.
  • reports/predicate_grounding_residual.tsv:12 reads causally upstream of 13 blocked_by_node_type RO:0002411; header order matches the column contract the command doc declares at line 33.
  • mappings/predicate_grounding.tsv:101 — 9 fields, aligned with the 9-column header (predicate_id column included), the embedded doubled-quotes in notes are a single quoted field on one line, and the three type names are real CausalNodeTypeEnum values.
  • The only two Python consumers of either TSV (check_biolink_coverage.py:73 and ground_causal_predicates.py) use csv.DictReader; rename_predicate_labels.py only names the file in a docstring. Nothing reads either file positionally, so the added columns break no consumer.
  • NEXT_TASKS.md §9 strikes the row and replaces the label-correspondence claim with the */*-rows-are-unconstrained caveat.

🔵 Still open as follow-up, not for this PR

The type check is reached only for edges with an empty predicate_id — an already-grounded edge returns at line 125 first. A hand-applied or future-script-applied violating grounding is never surfaced. A qc check reading the same two columns over grounded edges would make this a ratchet rather than a one-way gate. Worth an issue; it is out of scope here.

Not verified: uv run/just/python invocations are outside this job's allowed tools, so "234 tests pass", "0 newly grounded", and the residual being byte-identical to what the generator emits are unchecked by me. just qc regenerates into a tmpdir and diffs (justfile:377-390), so CI proves the last one. Everything above is static reading of HEAD, not the working tree — the job's checkout has an unrelated local revert of .claude/commands/ground-or-propose-metpo.md, which is a harness artifact and not part of the PR.

No history/ record needed — no data/traits/** changes.

Approving.
· branch feat/predicate-node-type-constraints

@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.

The last 🟡 is closed, and closed by checking rather than re-asserting. The blanket 'these are all processual relations' is replaced with the rule that generalises — write the type columns for every row and read each relation's own domain and range — plus the four cases spelled out: regulation/causal over occurrents, enables material entity → process, produces material-entity-to-material-entity, part of / has part domain-neutral. Each is a correct reading, and matches what this repo's own mapping notes already said for two of them. Giving the next reader the four answers rather than just deleting the wrong sentence is the better fix.

Re-checked and holding: blocked keyed on (label, shape) with the real corpus label 'positively influences (saturating)' in the test; mapping[label][0] guarded by is_blocked with no KeyError path; residual TSV row 12 reads 'causally upstream of 13 blocked_by_node_type RO:0002411' and its header matches the command doc's column contract; the mapping row is 9 fields against a 9-column header with the quoted notes field intact; the three type names are real CausalNodeTypeEnum values. Both Python consumers of either TSV use csv.DictReader and nothing reads them positionally, so the added columns break no consumer.

One 🔵 for follow-up, not this PR: the type check is reached only for edges with an empty predicate_id, so a hand-applied violating grounding is never surfaced. A qc check over grounded edges reading the same two columns would turn this into a ratchet.

Not verified: uv/just/python are outside this job's allowed tools, so '234 tests pass' and the residual being byte-identical to the generator's output are unchecked by me — just qc regenerates and diffs, so CI proves the latter. No history/ record needed; no data/traits/** changes.

@realmarcin
realmarcin merged commit d1cdba9 into main Aug 4, 2026
6 checks passed
@realmarcin
realmarcin deleted the feat/predicate-node-type-constraints branch August 4, 2026 04:55
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.

Grounding a predicate is not checkable by label alone: mappings need a node_type constraint

1 participant