biolink: migrate the AI-curated corpus to Biolink 4.4.4 (qualifier model) - #72
Draft
andrewsu wants to merge 2 commits into
Draft
biolink: migrate the AI-curated corpus to Biolink 4.4.4 (qualifier model)#72andrewsu wants to merge 2 commits into
andrewsu wants to merge 2 commits into
Conversation
…del) docs/biolink_version_decision.md recorded the strategy as "re-curated from scratch in the latest Biolink version -- storage IS the current vocabulary", and closed #19 and #54 as moot on that basis. The premise did not hold: the curator was handed the legacy 67-predicate enum (AGENTS.md S3, .claude/commands/curate.md) and QC Layer 3 enforced it, so forwardfilled records came out in the legacy vocabulary. Measured against Biolink 4.4.4, 323 of 558 edges (58%) in the 135 AI-curated records used predicates Biolink had already removed. #69 built a drift monitor -- it detects this, it does not fix it. Pin + status. New src/drugmechdb/schema/biolink_predicate_status.yaml: the pinned release, each enum value's status (current / legacy_only / deprecated), and the replacement each removed predicate maps to. One source of truth, read by both the gate and the migration. Schema (additive). PathEdge gains three optional slots -- object_aspect_qualifier, object_direction_qualifier, qualified_predicate -- plus ObjectAspectEnum / ObjectDirectionEnum mirroring Biolink's. Legacy records carry none of them and are unaffected. Layer 3 is era-aware. The enum deliberately spans both eras so the 4,846 legacy records still validate; which era a record MAY use is profile- dependent, mirroring qc.py's own split. `legacy` accepts any enum member, `ai_curated` only `status: current`, and affects/regulates must carry the qualifiers that hold their polarity. Failures name the exact replacement. qc.py now propagates a forced --profile to Layer 3, which would otherwise re-detect and override it. Records migrated, not re-curated: scripts/migrate_predicates.py applied 324 rewrites across 132 files. The transform is line-based, so formatting and comments survive and the diff is only the changed edges. Polarity follows the qualifier. structural_quality.py reads the sign from object_direction_qualifier (synthesizing a <predicate>|<direction> lexicon entry per record) and keys domain/range constraints on affects|<aspect>. Net polarity on the migrated corpus is coherent=124/135, and type_violation coverage went 9 -> 11, so the checks sharpened rather than silently lapsing. Two defects found on the way: * sonnet/outputs/P13.yaml used `binds`, which is not in the enum -- it fails Layer 3 on main today. AGENTS.md advertised `binds` and `affects` while the enum contained neither. `binds` is deprecated in 4.4.4, so it maps to `directly physically interacts with`; the contract list is corrected. * The status file's replacements initially pointed at predicates absent from the enum (preventative for condition, contraindicated in, ...), which would have migrated a record straight into a Layer 3 failure. Those five are now enum members with polarity and type constraints, and a new test keeps the mapping closed over the enum. Three predicates carry needs_human_decision and are never auto-rewritten: `affects risk for` (4.4.4 splits it by direction) and `increases/decreases response to` (no `response` aspect exists). All are legacy-corpus-only. kb/paths/ is untouched -- it keeps its vocabulary until forwardfill replaces it. #54 is therefore not moot (its rules are implemented here) and #19 is live again for a legacy publish. Verified: kb/paths Layers 1+3 PASS (Layer 2's 230 failures are pre-existing -- confirmed by stashing and re-running on unmodified main); all 135 AI-curated records PASS Layers 1-3 as ai_curated; migrate_predicates.py --check reports 0 remaining; drift monitor shows 0 missing / 0 stale polarity entries; full test suite 188 passed, 9 skipped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The static site's data layer had one documented swap point (SOURCE in dmdb-data.js) and was still serving 12 hand-written sample rows. Nothing in the repo built the JSON it wanted for the live path, so the site could not show the corpus it exists to show. scripts/build_site_data.py builds it from the agentic run outputs -- not kb/paths, which carries no per-edge evidence. 135 records across 4 runs (pilot, opus, sonnet, phase-3 eval) covering 59 distinct path ids, since the same drug/disease pair is curated repeatedly for model comparison; a record's site id is namespaced by run. The builder joins each cited PMID to its references_cache frontmatter for the paper credential block (85% of evidence items; the pilot run's sources are not cached anywhere in the repo), derives source_type and source_tier, and generates graph.summary as the node chain along the record's longest path -- flagged as generated in a `site` block, since real records carry no summary. Wiring: SOURCE flipped to "live"; live getRecord now honours an id (it ignored the argument before); browse links each result to record.html?id=... instead of all pointing at one hardcoded record, gains a Curation run facet, and shows N/M edges sourced per row. Qualifier rendering. After the Biolink 4.4.4 migration an edge's polarity lives in object_direction_qualifier, and `affects`/`regulates` are neutral words -- so colouring edges by matching /decrease|increase/ against the predicate name now reads every migrated edge as neutral. record.html and pathograph.js read the qualifier first and fall back to the name for legacy-vocabulary edges, and both render the full form (`affects activity (decreased)`). Verified: a node harness drove dmdb-data.js against all 135 records over a local server -- no dangling edges, every node has id/name/label, every evidence item has a snippet and source_type. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
andrewsu
marked this pull request as draft
September 4, 2026 04:36
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
docs/biolink_version_decision.mdrecorded the strategy as "re-curated from scratch in the latestBiolink version… storage is the current vocabulary," and closed #19 and #54 as moot on that
basis. The premise did not hold. The curator was handed the legacy 67-predicate enum
(
AGENTS.md§3,.claude/commands/curate.md) and QC Layer 3 enforced it, so forwardfilled recordscame out in the legacy vocabulary.
Measured against Biolink 4.4.4 (latest release, 2026-08-10) with the repo's own drift monitor:
kb/paths, 4,846 records)#69 built a drift monitor — read-only, dormant, human-PR-only. It detects this; it does not fix it.
What this does
Migrates the AI-curated corpus to Biolink 4.4.4 and makes the gate hold new curation there.
kb/paths/is untouched — it keeps its vocabulary until forwardfill replaces it.Pin + status. New
src/drugmechdb/schema/biolink_predicate_status.yaml: the pinned release(4.4.4), each enum value's status (
current/legacy_only/deprecated), and the replacementeach removed predicate maps to. One source of truth, read by both the gate and the migration.
Schema (additive).
PathEdgegains three optional slots —object_aspect_qualifier,object_direction_qualifier,qualified_predicate— plusObjectAspectEnum/ObjectDirectionEnummirroring Biolink's. Legacy records carry none of them and are unaffected.
Layer 3 is now era-aware. The enum deliberately spans both eras (legacy records still validate).
Which era a record may use is profile-dependent, mirroring qc.py's own split:
legacyaccepts anyenum member,
ai_curatedaccepts onlystatus: current— andaffects/regulatesmust carry thequalifiers that hold their polarity. Failures name the exact replacement.
Records migrated, not re-curated:
scripts/migrate_predicates.pyapplied 324 rewrites across132 files (line-based, so formatting/comments survive and the diff is only the changed edges).
Polarity follows the qualifier.
structural_quality.pyreads the sign fromobject_direction_qualifier(synthesizing a<predicate>|<direction>lexicon entry per record), anddomain/range constraints key on
affects|<aspect>. Net polarity on the migrated corpus:coherent=124 / 135, unchanged in character — and
type_violationcoverage went 9 → 11, so thechecks got slightly sharper rather than silently lapsing.
Two defects found on the way
sonnet/outputs/P13.yamlusedbinds, which was not in the enum — it fails Layer 3 today, onmain. Cause:
AGENTS.mdadvertisedbindsandaffectsin its example list while the enumcontained neither.
bindsturns out to be deprecated in 4.4.4, so it is mapped todirectly physically interacts with; the contract list is corrected.(
preventative for condition,contraindicated in, …), which would have migrated a legacy recordstraight into a Layer 3 failure. Those five are now enum members with polarity and type
constraints, and
test_every_replacement_points_at_a_current_predicatekeeps the mapping closed.Left for a human (never auto-rewritten)
Three predicates carry
needs_human_decision: true—affects risk for(4.4.4 splits it bydirection; needs a per-edge reading) and
increases/decreases response to(noresponseaspectexists). All three are legacy-corpus-only.
Issue status
kb/paths/as current Biolink still needstranslation.
translate-at-publish policy.
Verification
just qconkb/paths/(4,846): Layers 1 + 3 PASS. Layer 2's 230 failures are pre-existing —confirmed by stashing this branch and re-running on unmodified main.
ai_curated).scripts/migrate_predicates.py --check: 0 edges remaining.🤖 Generated with Claude Code
Second commit (
site:) is the browse layer — it builds the site JSON from the run outputs andteaches the record page and graph renderer to read polarity from the qualifier, which they otherwise
lose after this migration. Happy to split it into its own PR if you'd rather review them apart.