Skip to content

Stop the encodes row claiming provenance it does not have (#342) - #350

Merged
realmarcin merged 5 commits into
mainfrom
fix/342-local-source
Aug 7, 2026
Merged

Stop the encodes row claiming provenance it does not have (#342)#350
realmarcin merged 5 commits into
mainfrom
fix/342-local-source

Conversation

@realmarcin

Copy link
Copy Markdown
Contributor

mappings/predicate_grounding.tsv grounded encodes to biolink:encodes with
source=biolink and a note claiming an "exact label match against biolink slot". The
pinned biolink-model.yaml (4.4.0) has no such slot — the gene-to-product slot is
has gene product — so the CURIE resolved to nothing upstream.

The sweep answers the issue's open question

All 13 biolink: CURIEs used in the mapping table, checked against the pinned model:

biolink:encodes is the only one with no backing slot. The other twelve resolve
(produces, located_in, part_of, catalyzes, interacts_with, …). That bounds the
problem to one row.

What changes, and what doesn't

The coinage stays. A gene cluster → protein complex edge doesn't fit
has gene product's range: gene product mixin, so repointing would trade a missing term
for a wrong one.

The claim changes: source=local rather than biolink, and predicate_id
skos:closeMatch rather than exactMatch — there's no upstream term to be exactly equal
to
. local joins METPO/biolink/RO/RDFS in the column's vocabulary.

The real defect was that nothing noticed

The signal had been sitting in reports/biolink_coverage.tsv the whole time — the only
applied biolink: CURIE with both backing columns empty. A report nobody consults is
not a check.
audit-biolink-curies is that check, and it's in qc.

Deliberately narrow: it does not require the corpus label to match a slot name.
Most labels are synonyms — generates, yields, forms and six others all ground to
biolink:produces — so requiring that would flag correct rows. It requires only that the
CURIE names a real slot, and exempts rows declaring source=local, which keeps a
coinage legitimate while forcing it to say so.

What was checked

  • Canaried: setting the row back to source=biolink makes the gate fail and name it.
  • Six tests, including that a synonym label passes and a local row is exempt.
  • audit-justfile-paths caught the unstaged script, and audit-qc-paths confirms the
    filter still covers what qc reads.
  • just qc green · 467 tests pass · ruff clean.

Closes #342.

🤖 Generated with Claude Code

mappings/predicate_grounding.tsv grounded `encodes` to biolink:encodes
with source=biolink and a note claiming an "exact label match against
biolink slot". The pinned data/raw/biolink-model.yaml (4.4.0) has no such
slot -- the gene-to-product slot is `has gene product` -- so the CURIE
resolved to nothing upstream.

Swept all 13 biolink: CURIEs used in the mapping table against the model.
biolink:encodes is the ONLY one with no backing slot; the other twelve
resolve. That answers the question the issue asked and bounds the problem
to one row.

The row keeps the coinage, because a gene CLUSTER to protein COMPLEX edge
does not fit `has gene product`'s range of gene product mixin --
repointing would trade a missing term for a wrong one. What changes is
the claim: source=local rather than biolink, and predicate_id
skos:closeMatch rather than exactMatch, since there is no upstream term
to be exactly equal to. `local` joins METPO/biolink/RO/RDFS in the
column's vocabulary.

The real defect was not the coinage but that nothing noticed. The signal
had been sitting in reports/biolink_coverage.tsv all along as the only
applied biolink: CURIE with both backing columns empty -- a report nobody
consults is not a check. audit-biolink-curies is that check, and is in qc.

Deliberately narrow: it does NOT require the corpus LABEL to match a slot
name. Most labels are synonyms -- generates, yields, forms and six others
all ground to biolink:produces -- so requiring that would flag correct
rows. It requires only that the CURIE names a real slot, and exempts rows
declaring source=local, which keeps a coinage legitimate while making it
say so.

Canaried by setting the row back to source=biolink: the gate fails and
names it. Six tests, including that a synonym label passes and that a
local row is exempt.

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

culturebot-reviewer Bot commented Aug 7, 2026

Copy link
Copy Markdown

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


Review — PR #350

  • Read repo conventions (docs/GROUNDING_POLICY.md, docs/CURATION_PLAYBOOK.md, proposal cohorts)
  • Verified the sweep claim against the pinned model
  • Reviewed scripts/audit_biolink_curies.py + tests + qc wiring
  • Reviewed the reports/biolink_coverage.tsv regeneration
  • Verdict

The central factual claim checks out. I swept all 13 distinct biolink: CURIEs in mappings/predicate_grounding.tsv against data/raw/biolink-model.yaml: twelve resolve to top-level slots, biolink:encodes is the only one absent, and has gene product is at data/raw/biolink-model.yaml:3421 as described. No occurrence of an encodes slot under any spelling. The gate's deliberate refusal to require label↔slot-name equality is the right call and test_a_synonym_label_is_not_required_to_match_a_slot pins the case that would have made a naive version wrong.


🟡 The disclaimer doesn't travel with the CURIE

source=local lives in one column of one TSV. It is not serialized into the corpus, and scripts/ground_causal_predicates.py:165 discards it outright (curie, _src, subj_ok, obj_ok = mapping[pred]). Meanwhile biolink:encodes is a live predicate_id in 8 trait files:

data/traits/morphology/{white_pigmented,intracellular_inclusion,green_pigmented}.yaml
data/traits/metabolism/{cellulolysis,methanogenesis}.yaml
data/traits/physiology/{carboxydotrophic,methanotrophic}.yaml
data/traits/environment/ph_range_high.yaml

Anything consuming those files sees a CURIE in the biolink: namespace that expands to https://w3id.org/biolink/vocab/encodes and resolves to nothing, with no signal anywhere in the data that it is a coinage. The PR is right that the edge is sound and that repointing to has gene product would trade a missing term for a wrong range — but those aren't the only two options, and the repo already established the third:

label CURIE source note
confers METPO:2007700 METPO "Proposed in proposals/metpo_traitmech_v8 (placeholder id, not yet minted upstream)"
has electron donor METPO:2007701 METPO placeholder id
has carbon source METPO:2007806 METPO "Proposed in proposals/metpo_traitmech_v9 (placeholder id)"

Three predicates with no adequate upstream term, all handled by minting a placeholder METPO id and filing it in a cohort. encodes is the only one minted into a namespace TraitMech doesn't own. Applying the existing pattern — placeholder METPO:20078xx, add to a cohort, repoint the 8 edges — makes source=METPO true, keeps the semantics the PR correctly wants to preserve, and leaves the new gate with nothing to exempt.

Fix this →

🟡 Closes #342 contradicts the row's own note

The PR body says Closes #342; the note it writes into mappings/predicate_grounding.tsv:28 says "Proposing it upstream is the open half of TraitMech#342." Both can't be right. Either drop the Closes keyword or split the upstream-proposal half into a follow-up issue and reference that in the note — otherwise the tracking pointer in the mapping table points at a closed issue.

🔵 The exemption is keyed to an unvalidated free-text column

unbacked() skips any row whose source is the literal local (scripts/audit_biolink_curies.py:57-58). Nothing validates that column's vocabulary, so a future unbacked CURIE is silenced by typing five characters into a TSV cell — the same shape of failure the PR is fixing, one level up. The repo's other blocking gate keys its escapes to specific identifiers (exceptions: in conf/id_label_targets.yaml, per justfile:817-818). An allow-list keyed to the CURIE would make a second coinage an explicit decision.

🔵 biolink_coverage.tsv isn't in the staleness gate

The regeneration picked up 549 lines of drift unrelated to this change (produces METPO:2000202METPO:2007800, defines residual→applied), which means it had been stale for some time. audit-derived-reports (justfile:486-530) covers predicate_grounding_residual.tsv, node_grounding_residual.tsv, causal_graph_audit.tsv and predicate_domain_audit.tsv — not this one. The committed copy is consistent with the current mapping table now; nothing stops it drifting again. Given the PR's own thesis, that's the natural follow-through.

(No history/ record needed — no data/traits/** change here.)

I couldn't run just qc or the test suite in this environment (uv run isn't permitted here), so the green-CI claims are taken on the author's word and CI's.


Verdict: request changes — on the first item. The diagnosis and the gate are good work; the resolution stops one step short of the repo's own established remedy for exactly this situation.
· branch fix/342-local-source

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

Diagnosis and the gate are sound — I verified all 13 biolink CURIEs against the pinned model and biolink:encodes is indeed the only one with no backing slot. But the resolution stops one step short: source=local lives in a TSV column that is never serialized into the corpus and is discarded by ground_causal_predicates.py:165, while biolink:encodes remains a live predicate_id in 8 trait files, expanding to an IRI that resolves to nothing. The repo already has the remedy for a predicate with no adequate upstream term — a placeholder METPO id in a proposal cohort (confers/METPO:2007700, has electron donor/METPO:2007701, has carbon source/METPO:2007806). Applying it here makes the source column true and leaves the new gate nothing to exempt. Also: 'Closes #342' contradicts the row note's 'the open half of #342'. Full review in the linked comment.

…review)

The 🟡 is right and it changes the fix rather than refining it. source=local
lived in one column of one TSV, ground_causal_predicates discards it
outright, and the unqualified `biolink:encodes` was what got written into
the corpus -- so a reader of a trait record saw an apparently-upstream
term and the disclaimer was nowhere near them. Annotating provenance at a
layer the data does not carry is not fixing the claim.

Minted METPO:2007813 `encodes` into the v9 cohort instead, and migrated
all 10 edges across 8 records. That removes the false claim from the DATA
rather than describing it elsewhere, and puts the term in the same
pending-upstream state as the other 13 v9 predicates instead of a
category of its own.

RO:0002205 (has gene product) was considered and rejected on the same
grounds the coinage existed for: it relates a GENE to a gene product,
while every corpus edge relates a gene CLUSTER or OPERON to a protein
COMPLEX or a biosynthetic PROCESS. Recorded as skos:closeMatch at medium
confidence -- the granularity differs on both sides, and saying `high`
would overstate it.

audit-biolink-curies stays. It now passes with no exemptions at all,
which is a better resting state than passing because one row opted out,
and it still catches the next unbacked CURIE.

Worth recording: the #322 round-trip guard caught this commit. Inserting
curation events as hand-built YAML left 8 records non-canonical, the
guard failed, and re-emitting them through the helper fixed it -- which
is exactly the workflow #322 was for, on the first bulk edit after it
landed.

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

culturebot-reviewer Bot commented Aug 7, 2026

Copy link
Copy Markdown

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


Review — PR #350 (re-review after 0593a256)

  • Read repo conventions and the prior review
  • Verify the minted METPO id and its cohort entry
  • Verify the 8 trait YAMLs were repointed
  • Re-check the gate, tests, and derived reports
  • Verdict

The 🟡 from the last round is resolved properly. METPO:2007813 is free (grep confirms 20078002007812 are the only occupants of the block), the cohort rows are in both the ROBOT template and the SSSOM file, and all 10 edges across the 8 records now carry it — no predicate_id: biolink:encodes survives in data/traits/**. I checked the new subject_types/object_types columns against the actual graph: every subject node is GENE_OR_PROTEIN, and the objects are exactly GENE_OR_PROTEIN / BIOLOGICAL_PROCESS / ORGANELLE, so the declared domain is true rather than aspirational. skos:closeMatch at medium against RO:0002205 is the right call and the right confidence. The Closes #342 contradiction is gone with the old note.

Two things the second commit left behind.


🟡 reports/biolink_coverage.tsv still reports the CURIE this PR removed

reports/biolink_coverage.tsv:28:

applied	encodes	biolink:encodes			

scripts/check_biolink_coverage.py derives current_curie from mappings/predicate_grounding.tsv, and that row now reads METPO:2007813. The first commit regenerated the report (that's where the 549 lines of drift came from, including producesMETPO:2007800 two lines above); the second commit changed the mapping and didn't. So the committed report asserts an applied biolink: CURIE that no longer appears anywhere in the corpus.

That is the same shape of defect the PR exists to fix, in the same file the PR names as the signal nobody read — and audit-derived-reports (justfile:486) covers predicate_grounding_residual.tsv, node_grounding_residual.tsv, causal_graph_audit.tsv and predicate_domain_audit.tsv, not this one, so nothing in qc will catch it. just check-biolink-coverage and commit the result.

Fix this →

🟡 The gate still documents the remedy the PR abandoned

scripts/audit_biolink_curies.py was written for the source=local resolution and wasn't updated when that resolution was replaced. Its docstring still says:

A local coinage is legitimate when nothing upstream fits — encodes is kept precisely because a gene CLUSTER to protein COMPLEX edge does not fit has gene product's range.

encodes was not kept. And the failure message (scripts/audit_biolink_curies.py:88-91) tells the next contributor who trips this gate to "keep the coinage and set source=local" — which is exactly what this PR decided not to do. justfile:94-103 carries the same text.

No row uses source=local any more (METPO 55, biolink 28, RO 23, RDFS 3), so LOCAL_SOURCE is now an unexercised escape hatch — and it's still keyed to an unvalidated free-text column, so it can silence a future unbacked CURIE with five characters in a TSV cell. Either drop the exemption and let the gate say "mint a placeholder METPO id in a cohort, as encodes did" — which is what the repo actually does — or keep it and rewrite the prose so it isn't advertising a rejected remedy. test_a_local_coinage_is_exempt should follow whichever way that goes.

Fix this →

🔵 The edge count says 9 records; it's 8

metpo_proposal_properties_robot.tsv says "covers 10 causal-graph edges across 9 trait records" and the mapping note says "written into 9 trait files". The 10 edges live in 8 files — carboxydotrophic and methanotrophic hold two each (confirmed against origin/main). Worth correcting in the ROBOT row specifically, since that one is submitted upstream.


Advisory, per repo convention: the 8 data/traits/** edits carry in-file curation_history entries but no record under history/records/. Not blocking.

I can't run uv in this environment, so just qc / test-suite results are taken from the author and CI. Note that neither would catch the first item — no gate covers biolink_coverage.tsv.

Verdict: request changes — the substantive fix is right this time; what's left is the derived report and the gate's own prose disagreeing with the decision the PR made.
· branch fix/342-local-source

…d 2)

Both 🔵s are the PR's own thesis pointed at the PR.

The exemption was `source == "local"`, a free-text cell. A future
unbacked CURIE could have been silenced by typing five characters into a
TSV -- the same shape of failure this gate exists to catch, one level up,
and exactly what the reviewer said. Now an explicit ALLOWED_UNBACKED set
keyed to the CURIE, following conf/id_label_targets.yaml's `exceptions:`
convention. It is EMPTY, because #342's only coinage was minted as
METPO:2007813 rather than exempted, so adding an entry is a code change
someone reviews rather than a cell edit. Two tests: source=local alone no
longer exempts, and an explicitly allowed CURIE does.

reports/biolink_coverage.tsv is now in audit-derived-reports. It had
drifted -- regenerating it moved 549 lines unrelated to this change
(produces METPO:2000202 -> METPO:2007800, defines residual -> applied) --
and it is the report that recorded biolink:encodes as unbacked for
however long that was true. A stale report nobody reads is even less of a
check than a current one, which is the argument this PR is built on.

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

@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 METPO minting is right and I verified it: METPO:2007813 is free in the 20078xx block, both cohort files carry it, all 10 encodes edges across 8 records are repointed, and the new subject_types/object_types columns match the actual node types (all subjects GENE_OR_PROTEIN; objects exactly GENE_OR_PROTEIN/BIOLOGICAL_PROCESS/ORGANELLE). closeMatch at medium against RO:0002205 is the right call. Two leftovers from the second commit: (1) reports/biolink_coverage.tsv:28 still reads 'applied encodes biolink:encodes' - it derives current_curie from the mapping table, was regenerated in commit 1 and not in commit 2, and no gate covers it, so the repo now ships a derived report asserting an applied CURIE that exists nowhere in the corpus; (2) scripts/audit_biolink_curies.py and justfile:94-103 still document the abandoned source=local remedy - the docstring says encodes 'is kept' as a coinage, and the failure message tells the next contributor to set source=local, which is exactly what this PR decided against. Also a 9-vs-8 record count in the ROBOT row that goes upstream. Full detail in the linked comment.

@realmarcin

Copy link
Copy Markdown
Contributor Author

All four taken, and the first changed the fix rather than refining it.

🟡 The disclaimer not travelling with the CURIE

You're right, and it's the argument that matters: source=local lived in one column of one TSV, ground_causal_predicates discards it outright, and the unqualified biolink:encodes was what got written into the corpus. Annotating provenance at a layer the data doesn't carry isn't fixing the claim.

Minted METPO:2007813 into the v9 cohort and migrated all 10 edges across 8 records. That removes the false claim from the data, and puts the term in the same pending-upstream state as the other 13 v9 predicates rather than a category of its own.

RO:0002205 was rejected for the reason the coinage existed: it relates a gene to a gene product, while every corpus edge relates a gene cluster/operon to a protein complex or a biosynthetic process. Recorded as skos:closeMatch at medium confidence — granularity differs on both sides and high would overstate it.

🟡 Closes #342 contradicting the note

Resolved by the above: there's no longer an "open half". The term has a home in v9, pending upstream mint like everything else there.

🔵 Free-text exemption

Right, and it was this PR's thesis one level up. Now an explicit ALLOWED_UNBACKED set keyed to the CURIE, following conf/id_label_targets.yaml's exceptions: convention — and empty, since minting removed the only user. Adding an entry is a code change someone reviews, not a cell edit. Two tests: source=local alone no longer exempts; an explicitly allowed CURIE does.

🔵 biolink_coverage.tsv not gated

Added to audit-derived-reports. Your point about the 549 lines of unrelated drift is the clinching one — that's the report that recorded biolink:encodes as unbacked for however long it was true. A stale report nobody reads is even less of a check than a current one, which is the argument this whole PR rests on.

One more thing worth reporting

The #322 round-trip guard caught this commit. Inserting curation events as hand-built YAML left 8 records non-canonical; the guard failed, and re-emitting them through the helper fixed it — the first bulk edit after #322 landed, doing exactly what it was for.

just qc green · just pr-sanity clean · 468 tests pass.

@culturebot-reviewer

culturebot-reviewer Bot commented Aug 7, 2026

Copy link
Copy Markdown

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


Review — PR #350 (round 3, after 73aa19f8)

  • Read repo conventions and prior review rounds
  • Verify reports/biolink_coverage.tsv regeneration + its new gate
  • Verify the ALLOWED_UNBACKED rework and its prose/tests
  • Check the 9-vs-8 record count in the ROBOT row
  • Verdict

Two of the three leftovers are resolved and I verified both. reports/biolink_coverage.tsv:28 now reads applied encodes METPO:2007813 with both backing columns empty, which is what check_biolink_coverage.py produces — METPO:2007813 appears in no biolink exact_mappings and no slot or alias is spelled encodes. Row counts line up exactly (678 = header + 109 applied + 568 residual, against a 110-line mapping table and a 569-line residual), so the file is internally consistent, not just patched at one line. The new audit-derived-reports block (justfile:598-624) compares the committed copy against a fresh run and emits the right just check-biolink-coverage hint, matching the pattern the other four reports already use. ALLOWED_UNBACKED is keyed to the CURIE, is empty, and the two tests pin both directions.


🟡 The gate's failure message still tells contributors to do the thing the tests prove doesn't work

The docstring was rewritten (scripts/audit_biolink_curies.py:21-24 now says the coinage was minted rather than exempted). The failure message was not:

scripts/audit_biolink_curies.py:97-100

"Either repoint to a real slot, or -- if nothing upstream fits -- keep
 the coinage and set source=local so the row stops claiming biolink
 provenance it does not have (#342)."

unbacked() no longer looks at source at all except to echo it — and tests/test_audit_biolink_curies.py:64-66 asserts that a source=local row is flagged. So the only text a contributor sees when this gate fires directs them to an edit that leaves the gate red, for a column value that now appears in zero of the 109 rows (METPO 55, biolink 28, RO 23, RDFS 3). Same text at justfile:101-103: "A local coinage stays legitimate … provided the row says source=local."

The message should say what the repo actually did: repoint to a real slot, or mint a placeholder METPO:20078xx in a proposal cohort the way encodes was — and note that ALLOWED_UNBACKED is the escape, and it's a code edit.

Fix this →

🔵 The 9-vs-8 count is unchanged from last round

proposals/metpo_traitmech_v9/metpo_proposal_properties_robot.tsv:16 still reads "covers 10 causal-graph edges across 9 trait records", and mappings/predicate_grounding.tsv:28 still says "written into 9 trait files". Counted directly: grep -c "predicate_id: METPO:2007813" gives 10 edges across 8 files (carboxydotrophic and methanotrophic carry two each). The ROBOT row is the one that goes upstream to METPO.

🔵 The gate reads the mapping table, not the corpus

unbacked() only opens mappings/predicate_grounding.tsv. That is the right chokepoint for anything ground_causal_predicates.py writes, but this PR itself hand-edited 10 predicate_id values in 8 trait YAMLs — a biolink: CURIE introduced that way, without a mapping row, is invisible to the gate. Since the corpus is where #342 actually did its damage, sweeping data/traits/** for predicate_id: biolink:* and checking those against the model too would close the loop. Optional; the writer path is covered.


Advisory, per repo convention: the 8 data/traits/** edits carry in-file curation_history entries but no record under history/records/. Not blocking.

uv run isn't permitted in this environment, so just qc / the 468-test count are taken from the author and CI. Note that nothing in qc would catch the 🟡 — it's prose, and the gate passes.

Verdict: request changes — the substance is right and verified; what's left is the gate telling the next contributor to apply the remedy this PR rejected.
· branch fix/342-local-source

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

Round 3. The two 🟡s from last round are fixed and I verified both: reports/biolink_coverage.tsv:28 now reads METPO:2007813 with both backing columns correctly empty and row counts consistent with the mapping/residual sources (109 applied + 568 residual), and the new audit-derived-reports block gates it the same way the other four reports are gated. ALLOWED_UNBACKED is keyed to the CURIE, empty, and both directions are tested.

One 🟡 left: the gate's failure message (scripts/audit_biolink_curies.py:97-100) and the recipe comment (justfile:101-103) still tell contributors to 'keep the coinage and set source=local'. unbacked() no longer reads that column, tests/test_audit_biolink_curies.py:64 asserts a source=local row IS flagged, and no row in the 109-row mapping table uses that value any more. The docstring was rewritten; the message a failing contributor actually sees was not, and following it leaves the gate red. It should point at the remedy the repo used: repoint, or mint a placeholder METPO:20078xx in a cohort as encodes did.

Also unchanged from last round: the ROBOT row still says '9 trait records' where it is 8 (10 edges, confirmed by grep), and that row goes upstream. Full detail in the linked comment.

…350 round 3)

The 🟡 is a good catch and neatly framed: the failure message told
contributors to set source=local, which the tests added LAST round prove
no longer exempts anything. It also could not be caught by qc, being
prose in a passing gate. Rewritten to name what actually works -- repoint
to a real slot, or MINT it as #342 did -- and to say plainly that
source=local does not exempt and the escape is ALLOWED_UNBACKED, keyed to
the CURIE.

The gate read only the mapping table. That is not the only route into the
corpus: ground_causal_predicates writes from the table, but a curator can
type a predicate_id straight into a record, and #342's whole point is
that the CURIE in the RECORD is what a reader believes. It now checks
both, reporting an example file per offending CURIE so it can be found.

Canaried by typing biolink:not_a_slot into a record: exit 1, CURIE and
file named. The first attempt at that canary was a no-op -- it targeted
RO:0002327 in crispr_cas_system.yaml, which #323 had already migrated
away -- and reported a false pass until I checked the substitution
actually happened.

Also fixed the 9-vs-8 record count in both the v9 row and the mapping
note, and guarded relative_to() against a traits dir outside the repo,
which the new tests hit immediately (the same guard audit_causal_graphs
already carries).

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

Copy link
Copy Markdown
Contributor Author

All three taken.

🟡 The message contradicting the code

Good catch, and neatly framed — the failure message told contributors to set source=local, which the tests I added last round prove no longer exempts anything. And as you say, qc can't catch it: it's prose in a passing gate.

Rewritten to name what actually works — repoint to a real slot, or mint it as #342 did — and to state plainly that source=local does not exempt, the escape being ALLOWED_UNBACKED keyed to the CURIE.

🔵 The gate reading the mapping table, not the corpus

Right, and it undercuts the PR's own argument. ground_causal_predicates writes from the table, but a curator can type a predicate_id straight into a record — and #342's whole point is that the CURIE in the record is what a reader believes.

It now checks both, reporting an example file per offending CURIE.

Canaried by typing biolink:not_a_slot into a record: exit 1, CURIE and file named. Worth reporting that my first attempt at that canary was a no-op — it targeted RO:0002327 in crispr_cas_system.yaml, which #323 had already migrated away, so the substitution never happened and it reported a false pass. Checking that the edit landed is what caught it.

🔵 The 9-vs-8 count

Fixed in both the v9 row and the mapping note.

One incidental

The new corpus tests immediately hit relative_to(REPO_ROOT) raising for a fixture dir outside the repo — guarded the same way audit_causal_graphs already does.

just qc green · 471 tests pass · ruff clean · audit-biolink-curies reports 0 unbacked in both the mapping table and the corpus.

@culturebot-reviewer

culturebot-reviewer Bot commented Aug 7, 2026

Copy link
Copy Markdown

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


Review — PR #350 (round 4, after b6d65a20)

  • Read repo conventions and prior review rounds
  • Verify the failure-message / justfile prose rewrite
  • Verify the new corpus sweep + tests
  • Check the 9→8 record count fix
  • Verdict

The corpus half is real and I verified it end to end. corpus_biolink_curies sweeps data/traits/** for predicate_id: biolink:*; the 12 CURIEs it finds (associated_with, catalyzes, causes, consumes, develops_into, has_part, interacts_with, located_in, occurs_in, part_of, participates_in, produces) all name real top-level slots in the pinned 4.4.0 model, and no predicate_id: biolink:encodes survives. There are no prose false positives — every match in the corpus is an actual edge field, and the 8 records carry 10 METPO:2007813 edges, which is the count the ROBOT row and the mapping note now both give. reports/biolink_coverage.tsv:28 reads applied encodes METPO:2007813, consistent with the mapping table. The relative_to guard is the right one to copy from audit_causal_graphs.

One thing from round 3 landed in only one of the two places it was reported.


🟡 The recipe comment still carries the remedy the tests reject

The script's failure message was rewritten correctly (scripts/audit_biolink_curies.py:129-136 now says "MINT it, as #342 did … Setting source=local does NOT exempt a row"). justfile:100-103 was not — it hasn't been touched since commit da14f39c, across two rounds that named it with line numbers:

# Does NOT require the LABEL to match a slot name: most labels are synonyms
# (generates, yields and seven others all ground to biolink:produces). A local
# coinage stays legitimate when nothing upstream fits, provided the row says
# source=local rather than claiming biolink provenance.

unbacked() no longer reads that column except to echo it, tests/test_audit_biolink_curies.py:58-66 asserts a source=local row is flagged, and zero of the 109 mapping rows use that value (METPO 55, biolink 28, RO 23, RDFS 3). git log origin/main..HEAD -- justfile confirms the file was last touched in round 2, so the round-3 reply's "Rewritten" covers the script only.

Same paragraph also asserts biolink_coverage.tsv holds "the only applied biolink: CURIE with both backing columns empty" in the present tense, which this PR made false — that's the sentence the rewrite would naturally fix too.

Fix this →

🔵 The corpus sweep regexes YAML in a file that already imports yaml

scripts/audit_biolink_curies.py:103 matches predicate_id:\s*(biolink:\S+) against raw text, so a quoted value (predicate_id: "biolink:foo") slips past. Nothing in the corpus is quoted today, so this is a latent gap rather than a live one — but the writer path is the one that decides quoting, and it isn't the one being audited here.

Relatedly, the tests cover corpus_biolink_curies (which returns all biolink CURIEs) but not the slot-filtering in main() at line 117-118 — test_the_example_file_is_reported_so_it_can_be_found uses biolink:produces, which is backed and would be filtered out. The canary the author ran covers it manually; a fixture-level test would keep it covered.


Advisory, per repo convention: the 8 data/traits/** edits carry in-file curation_history entries but no record under history/records/. Not blocking, and unchanged from prior rounds.

uv run isn't permitted in this environment, so just qc and the 471-test count are taken from the author and CI. Nothing in qc would catch the 🟡 — it's a comment in a passing recipe, which is the same class of defect this PR was opened to fix.

Verdict: request changes — one line-item, mechanical: the recipe comment saying the opposite of what the code and tests do.
· branch fix/342-local-source

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

Round 4. The corpus sweep is right and I verified it: all 12 biolink: CURIEs still written as a predicate_id in data/traits/** name real top-level slots in the pinned 4.4.0 model, no biolink:encodes survives, there are no prose false positives in the regex sweep, and the 9-vs-8 count is corrected in both the ROBOT row and the mapping note (10 edges across 8 files, confirmed). reports/biolink_coverage.tsv:28 is consistent with the mapping table.

One 🟡 left, and it is the same one from round 3, applied in only one of the two places it was reported. scripts/audit_biolink_curies.py:129-136 was rewritten correctly. justfile:100-103 was not — git log origin/main..HEAD -- justfile shows the file untouched since round 2, and it still reads 'A local coinage stays legitimate when nothing upstream fits, provided the row says source=local rather than claiming biolink provenance.' unbacked() no longer reads that column, tests/test_audit_biolink_curies.py:58 asserts a source=local row IS flagged, and no row in the 109-row mapping table uses that value. The same paragraph also still claims in the present tense that biolink_coverage.tsv holds 'the only applied biolink: CURIE with both backing columns empty', which this PR made false.

Mechanical fix — mirror the wording already in the script. Full detail in the linked comment.

…round 4)

The 🟡 is the same defect a third time in this PR: I corrected the
remedy in the script's failure message last round and left it in the
justfile recipe comment. As the reviewer notes, qc cannot catch it --
prose in a passing recipe, which is the class this PR was opened over.
Corrected, and while there, the comment now also states that the gate
checks the corpus, which it had not said either.

The corpus sweep regexed YAML in a module that already imports yaml. Now
parses causal_graphs[].edges[].predicate_id properly. That is not just
tidiness: a text scan matches a CURIE quoted inside curation_history
prose, and several records now contain exactly that BECAUSE they describe
this issue -- methanogenesis.yaml and intracellular_inclusion.yaml both
mention biolink:encodes in their history. A regex would have reported
those as live groundings the moment the phrasing shifted. A test pins it.

Switching to parsing broke two of my own tests, correctly: their fixtures
used a flat `edges:` list rather than the real
causal_graphs[].edges[] shape, so they would have passed through the
parsing version unseen and proved nothing. Fixtures rebuilt in the real
structure.

Re-canaried after the change: a CURIE typed into a record still exits 1
and names the file.

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

Copy link
Copy Markdown
Contributor Author

Both taken — and the 🟡 is the same defect for the third time in this PR, which is worth naming rather than glossing.

🟡 The recipe comment

I corrected the remedy in the script's failure message last round and left it in the justfile comment. You're right that qc can't catch it — prose in a passing recipe, which is the class this PR was opened over. Corrected, and while there the comment now also says the gate checks the corpus, which it hadn't mentioned either.

The pattern across this PR: the claim lived in three places (mapping note, script message, recipe comment) and I fixed them one review round at a time. The lesson isn't "be more careful" — it's that a claim repeated in three places will go stale in at least one, which is an argument for the minting fix over the annotation fix I started with.

🔵 Regexing YAML

Fixed, and it wasn't only tidiness. A text scan matches a CURIE quoted inside curation_history prose — and several records now contain exactly that, because they describe this issue: methanogenesis.yaml and intracellular_inclusion.yaml both mention biolink:encodes in their history. A regex would have reported those as live groundings the moment the phrasing shifted. There's now a test for it.

Switching to parsing broke two of my own tests, correctly: their fixtures used a flat edges: list rather than the real causal_graphs[].edges[] shape, so they'd have passed through the parsing version unseen and proved nothing. Fixtures rebuilt in the real structure.

Re-canaried after the change: a CURIE typed into a record still exits 1 and names the file.

just qc green · 472 tests pass · ruff clean.

@culturebot-reviewer

culturebot-reviewer Bot commented Aug 7, 2026

Copy link
Copy Markdown

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


Review — PR #350 (round 5, after 46e19892)

  • Verify the justfile comment rewrite
  • Verify the YAML-parsing corpus sweep + its tests
  • Confirm the corpus sweep's coverage is unchanged by the rewrite
  • Re-verify the minting, counts, and derived report
  • Verdict

Both round-4 items are resolved, and the parsing change is a real improvement rather than a like-for-like swap.

justfile:100-109 now mirrors the script: it names minting as the remedy, says plainly that source=local does not exempt, points at ALLOWED_UNBACKED as a reviewed code-level escape, and mentions the corpus half — which it hadn't. The biolink_coverage.tsv sentence at justfile:96-97 is now past tense ("was already ... and nothing read it"), so it no longer asserts something this PR made false.

I checked the parse rewrite doesn't narrow coverage. Every predicate_id: biolink:* in the corpus — 377 occurrences, 12 distinct CURIEs — sits at one indent level, and all 353 edges: keys in the corpus are nested under causal_graphs:, with no edges: anywhere else and every trait file a top-level mapping. So causal_graphs[].edges[] reaches exactly what the regex reached. All 12 name real top-level slots in the pinned 4.4.0 model; no predicate_id: biolink:encodes survives. The _traits fixture rewrite is the right call — the old flat edges: fixtures would indeed have passed through the parsing version unseen, and test_a_curie_only_mentioned_in_prose_is_not_flagged pins the case parsing exists for.

Re-verified the substance is intact: mappings/predicate_grounding.tsv:28METPO:2007813, reports/biolink_coverage.tsv:28 consistent with it, ROBOT row and mapping note both say 8 records, and grep gives 10 edges across 8 files.


🔵 The prose-false-positive justification is stronger than the facts

scripts/audit_biolink_curies.py:108-111 says a text scan "would match a CURIE quoted inside curation_history prose — which several records now contain." The records do mention biolink:encodes in their history (7 files), but none in the form the old regex required: predicate_id:\s*(biolink:\S+) needs the literal predicate_id: token immediately before, and no prose mention has one — including via the \s*-spans-newlines path, since no line in data/traits/** ends in predicate_id:. So the risk was latent, not live, which is what the PR comment's "the moment the phrasing shifted" concedes but this code comment doesn't. Worth a word change, given the class of defect three rounds of this PR have been about.

🔵 A file the sweep can't parse is silently counted as clean

scripts/audit_biolink_curies.py:104-107 drops any file on yaml.YAMLError or a non-mapping root with a bare continue, and the gate then prints unbacked corpus CURIEs: 0 for a corpus it only partly read. validate-strict runs earlier in qc so nothing gets through in practice — but "the check silently didn't check" is this PR's own subject. A collected list of skipped files in the summary line would cost two lines.

🔵 main()'s corpus filter is still untested (carried from round 4)

The tests exercise corpus_biolink_curies, which returns all biolink CURIEs; the slot-filtering at scripts/audit_biolink_curies.py:129-130 — the part that decides pass or fail — has no fixture-level test. test_the_example_file_is_reported_so_it_can_be_found uses biolink:produces, which main() would filter out. The author's canary covers it manually.


Advisory, per repo convention: the 8 data/traits/** edits carry in-file curation_history entries but no record under history/records/. Unchanged from prior rounds, not blocking. This commit touches no trait data.

uv run isn't permitted in this environment, so just qc and the 472-test count are taken from the author and CI.

Verdict: approve. The three remaining items are all optional; the gate, the minting, and the prose now agree with each other.
· branch fix/342-local-source

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

Round 5. Both round-4 items are resolved and I verified both. justfile:100-109 now mirrors the script's failure message — minting as the remedy, source=local explicitly non-exempting, ALLOWED_UNBACKED as the reviewed escape — and the biolink_coverage.tsv sentence is past tense, so it no longer asserts what this PR made false.

The parse rewrite doesn't narrow coverage: all 377 predicate_id: biolink:* occurrences (12 distinct CURIEs) sit at one indent level, all 353 edges: keys in the corpus are nested under causal_graphs:, and every trait file is a top-level mapping — so causal_graphs[].edges[] reaches exactly what the regex reached. All 12 name real slots in the pinned 4.4.0 model; no biolink:encodes predicate_id survives. The fixture rebuild into the real structure is the right call, since the old flat edges: fixtures would have passed through the parsing version unseen.

Substance re-verified: mapping row -> METPO:2007813, reports/biolink_coverage.tsv:28 consistent with it, ROBOT row and mapping note both say 8 records, and grep gives 10 edges across 8 files.

Three optional items left, none blocking: the prose-false-positive comment at scripts/audit_biolink_curies.py:108-111 overstates the case (no prose mention is in the predicate_id: biolink:... form the old regex needed, and no line in data/traits/** ends in predicate_id:, so the risk was latent rather than live); an unparseable file is silently counted clean at lines 104-107, which validate-strict covers in practice but which is this PR's own subject; and main()'s slot filter still has no fixture-level test. Detail in the linked comment.

@realmarcin
realmarcin merged commit c5ccabe into main Aug 7, 2026
8 checks passed
@realmarcin
realmarcin deleted the fix/342-local-source branch August 7, 2026 06:46
realmarcin added a commit that referenced this pull request Aug 7, 2026
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>
realmarcin added a commit that referenced this pull request Aug 7, 2026
…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>
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.

biolink:encodes is a local coinage with no slot in the pinned biolink model

1 participant