Skip to content

biolink: migrate the AI-curated corpus to Biolink 4.4.4 (qualifier model) - #72

Draft
andrewsu wants to merge 2 commits into
mainfrom
biolink-4.4.4-migration
Draft

biolink: migrate the AI-curated corpus to Biolink 4.4.4 (qualifier model)#72
andrewsu wants to merge 2 commits into
mainfrom
biolink-4.4.4-migration

Conversation

@andrewsu

@andrewsu andrewsu commented Sep 4, 2026

Copy link
Copy Markdown
Member

Why

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 §3, .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 (latest release, 2026-08-10) with the repo's own drift monitor:

removed predicates in use affected edges
AI-curated (135 records, 4 runs) 7 distinct 323 / 558 = 58%
Legacy (kb/paths, 4,846 records) 23 distinct 16,873 / 32,641 = 52%

#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 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 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: legacy accepts any
enum member, ai_curated accepts only status: current — and affects/regulates must carry the
qualifiers that hold their polarity. Failures name the exact replacement.

Records migrated, not re-curated: scripts/migrate_predicates.py applied 324 rewrites across
132 files (line-based, so formatting/comments survive and the diff is only the changed edges).

-  - key: decreases activity of      +  - key: affects
                                     +    object_aspect_qualifier: activity
                                     +    object_direction_qualifier: decreased

Polarity follows the qualifier. structural_quality.py reads the sign from
object_direction_qualifier (synthesizing a <predicate>|<direction> lexicon entry per record), and
domain/range constraints key on affects|<aspect>. Net polarity on the migrated corpus:
coherent=124 / 135, unchanged in character — and type_violation coverage went 9 → 11, so the
checks got slightly sharper rather than silently lapsing.

Two defects found on the way

  • sonnet/outputs/P13.yaml used binds, which was not in the enum — it fails Layer 3 today, on
    main. Cause: AGENTS.md advertised binds and affects in its example list while the enum
    contained neither. binds turns out to be deprecated in 4.4.4, so it is mapped to
    directly physically interacts with; the contract list is corrected.
  • The status file's replacements initially pointed at predicates not in the enum
    (preventative for condition, contraindicated in, …), which would have migrated a legacy record
    straight into a Layer 3 failure. Those five are now enum members with polarity and type
    constraints, and test_every_replacement_points_at_a_current_predicate keeps the mapping closed.

Left for a human (never auto-rewritten)

Three predicates carry needs_human_decision: trueaffects risk for (4.4.4 splits it by
direction; needs a per-edge reading) and increases/decreases response to (no response aspect
exists). All three are legacy-corpus-only.

Issue status

Verification

  • just qc on kb/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.
  • QC Layers 1–3 on all 135 AI-curated records: PASS (profile ai_curated).
  • scripts/migrate_predicates.py --check: 0 edges remaining.
  • Drift monitor vs 4.4.4: polarity lexicon 0 missing / 0 stale entries.
  • Full test suite: 188 passed, 9 skipped, 0 failed (baseline on main: 182 passed, 5 failed).

🤖 Generated with Claude Code


Second commit (site:) is the browse layer — it builds the site JSON from the run outputs and
teaches 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.

andrewsu and others added 2 commits September 3, 2026 17:25
…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
andrewsu marked this pull request as draft September 4, 2026 04:36
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.

[P1] Publish-time translation to current Biolink (qualifier remap)

1 participant