Skip to content

feat: cBioPortal→OMOP showcase — slice-0 concept mapping + slice-1 FK-closed OMOP shape with cross-study identity - #116

Merged
deanban merged 54 commits into
mainfrom
ralph/feat/omop-shape-identity
Jul 22, 2026
Merged

feat: cBioPortal→OMOP showcase — slice-0 concept mapping + slice-1 FK-closed OMOP shape with cross-study identity#116
deanban merged 54 commits into
mainfrom
ralph/feat/omop-shape-identity

Conversation

@deanban

@deanban deanban commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Delivers the cBioPortal → OMOP mapping showcase end to end, in two vertical slices, keeping the SEMA core domain-generic (R29):

  • Slice-0 — concept mapping. Deterministic OncoTree → standard OMOP Condition concept resolution, staged with provenance, gated by Gate-D-lite + contract conformance. Target contract is OMOP-standard-Condition (SNOMED and ICDO3 oncology extension), not SNOMED-only (bug-369).
  • Slice-1 — FK-closed OMOP table shape + full identity. Materializes production-shaped omop.person + omop.condition_occurrence, FK-closed, with a two-level identity registry and deterministic cross-study person dedup (Stage B). All OMOP/cBio specifics live in showcase/cbioportal_to_omop/; the FK-closed harness, identity registry, and collapse engine stay generic in core.

Live results (Databricks workspace.omop_stage_a)

Two MSK studies materialized and de-duplicated on the shared MSK-DMP P- key:

  • person: 53,562 (= chord 24,950 + impact 48,179 − 19,567 cross-study duplicates retired)
  • condition_occurrence: 79,371 (chord 25,040 + impact 54,331), FK-closed, 0 orphans
  • Concept coverage: chord 100%, impact 99.9% real concepts (66 rows → OMOP 0 sentinel)
  • Both studies pass Gate-D-lite (FK closure, required-not-null, missing-key disposition); collapse is idempotent

Dedup was verified against an adjudication set: 19,567 merges, 0 sex mismatches across 19,437 corroborable pairs, all over-collapse guards clean (P- namespace only, 0 blank keys, 0 cross-namespace TCGA collision, patient key a true PK in both studies), 0 residual keys split across >1 person_id.

Key design points

  • Two-level identity registry ((source_namespace, key) → uid → person_id) so Stage B can revise identity without rewriting surrogate PKs. person_id is registry-assigned, never a hash — a hash would be incompatible with dedup.
  • Ordered FK-closed write (person before condition); Databricks has no multi-table transaction, so the guarantee is write-order + FK-valid-at-rest, not atomicity.
  • Deterministic same-namespace collapse only. Probabilistic/cross-namespace dedup is deliberately deferred — de-identified cBio has no cross-namespace signal to judge on.
  • No fabricated data. condition_start_date is nullable (cBio carries only relative day-offsets, no absolute date); missing patient keys route to review; unmapped concepts use the standard OMOP 0 sentinel.

Testing

  • Engine work is TDD (identity registry, resolver, FK-closed compiler, Stage B collapse, Gate-D-lite); last full suite was green (2263 unit tests, mypy strict, R29 guard) as of the S1-10 engine commit.
  • This branch's final commits are the live Databricks runs + the durable plan record; no core src/ change since the last green suite.

Follow-ups

Filed as separate issues:

Also advances #79 (extract cBioPortal to its own package) via the showcase/cbioportal_to_omop/ relocation.

deanban added 26 commits July 22, 2026 18:32
Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
Generalize OncoTree/cBioPortal literals in domain_prompts, few_shot_healthcare,
and few_shot_healthcare_stage_a; relocate HIERARCHICAL_VOCABULARIES to
models/vocab_knowledge.py. Add tests/unit/test_engine_coupling.py asserting the
R29 denylist returns EMPTY against src/sema/engine/ (US-001 guard prerequisite).

Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
Single R29_POLICY constant (denylist + core paths + allowlist) scanned by
scripts/check_engine_coupling.py, wired as a unit-marked test so pytest fails
on a leak. Case-explicit denylist; missing core paths are skipped so the guard
tightens automatically as resolve/compile spine modules land.

Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
§1.5(f) confusion-matrix harness (mapping_goldset + _utils) scoring distinct-code, row-weighted, and per-frequency-bucket; scaffolds the 64 distinct ONCOTREE_CODE gold artifact (UNLABELLED — human-label gate left for US-012). No resolver. 29 unit tests + skip-guarded live coverage test.

Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
…005)

Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
…m) (US-006)

VocabularyResolver composes the four §4 stages (candidate match in the source
vocabulary, 'Maps to' standardization, the domain gate, single-model tail
disambiguation) into one per-code decision. The code-bearing hot path is pure
SQL with no LLM; resolve_and_store is the sole writer of value-mapping-store
rows (one row per §1.5(a) grain incl NO_MAP). The R29-scanned engine.py names
no domain literal — all concept-level construction lives in engine_utils.py.

Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
Add FieldMap.status (§1.5(d)), the §1.5(e) coverage rule (NO_MAP/CONSTANT-NULL/
rejected never cover), derive_verdict any_review_pending from field-map statuses,
and Slice0PlanAssembler composing assertions into a verdicted MappingPlan. Staging
obligation authored in the allowlisted OMOP policy; assembler stays R29-generic.

Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
…-009)

Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
…) (US-011)

Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
Make the Slice-0 chain multi-warehouse: the US-012A `sema fit` CLI runs on
DuckDB or live Databricks via an injected StagingBackend strategy, with no
new chain logic. DUCKDB_BACKEND is the default (temp-build + scoped swap,
byte-identical to before); DATABRICKS_BACKEND writes the §1.5(b) staging table
with an atomic Delta INSERT ... REPLACE WHERE scoped on source_schema/table.
The value-mapping store stays DuckDB (US-005 canonical) on every backend.

Adds hermetic unit tests (fake cursor asserts the rendered Databricks SQL and
drives the whole databricks CLI chain), a skip-guarded live e2e test, and a
runbook. The live run + metric acceptance remain a human gate (workspace
credentials + human-labelled 100% gold coverage, US-002/US-012).

Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
Sign-off and signed via DCO.

Signed-off-by: deanban <dean@datadepo.us>
Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
…-strict

The Slice-0 assembler's §1.5(e) staging coverage gate was dead in the live
path: build_slice0_fit_request used an ad-hoc single-field obligation under a
different ref namespace, and make_slice0_staging_obligation() had only test
callers. Wire the real 3-field obligation (condition_concept_id +
resolver_policy_ref + vocab_release) and emit the two run-constant columns as
CONSTANT MappingAssertions so the MappingPlan faithfully covers all three and
the coverage gate runs end-to-end (bug-365).

Add `sema fit --strict`: exit 3 on a failed Gate D-lite or a non-ACCEPTED eval
verdict; default behavior unchanged (bug-366).

Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
Handoff captures the review, the two wiring fixes (737338a), the measured
resolver behavior (63 single-survivor / 1 NO_MAP / 0 ties), the corrected
Databricks diagnosis (concept tables were unloaded), and the remaining critical
path (human labels + live run). Adds the OncoTree API NCIt/UMLS reference sheet
as the independent oracle for hand-labelling the 64 gold codes.

Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
delete_study_scoped wiped every structural node schema-wide before per-table
processing; on --resume the wiped tables were then skipped and never rebuilt
(or lost outright if the run was interrupted). Add loader.delete_table_scoped
(physical-anchor + orphan-GC, no schema-wide edge wipe) and clear only tables
lacking cached assertions on resume; fresh builds keep the full schema wipe.

Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
…gated (bug-369 F4)

Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
…ot gold ACCEPTED (bug-369 F1)

Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
…contract machine-readable (bug-369 F2)

Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
…g-369 F3)

Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
…re (bug-383)

run_fit built the strict-gating report from decisions_from_store, which reads
the entire value-mapping store filtered only by target/policy/vocab release. A
stale cached mapping for a code absent from the current source could contradict
a gold label and fail --strict even when Gate D-lite and contract conformance
passed for the run. Build the report from run_mappings (the exact current-run
grain), matching the F1 conformance gate. Adds a two-run regression test.

Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
deanban added 27 commits July 22, 2026 18:32
Durable plan for renaming the resolver_policy_ref and retiring the manifest
`vocabulary: SNOMED` anchor, now unblocked by the bug-374/bug-383 run-scoping
fixes. Captures the two identity-key hazards (store grain key + :VocabularyBinding
MERGE key), the grepped touch-point map, three locked decisions with
recommendations, and a TDD execution order. Not executed.

Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
…alog (bug-384, bug-385)

bug-384: VocabLookupProducer.produce read the whole persistent store via
DecisionSource.read_all(), scoped only by binding — so a stale same-policy row
leaked its status/confidence into the column assertion and masked an empty run's
NoResolvedDecisionError. Fold the run's Sequence[ValueMapping] directly (third
fix of the unscoped-read_all class, after bug-383/bug-374).

bug-385: ValueMappingStore.column_names() filtered information_schema.columns by
schema+table only, double-counting when a second catalog (poc.duckdb) is attached
with the same table. Scope to current_database().

Also correct the repoint plan's test-file count (15 -> 8).

Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
The run-scoping fixes (bug-374/383/384) replaced store.read_all() — which the
store PK collapses via ON CONFLICT(GRAIN_KEY) — with run_mappings, which is one
row per INPUT code and NOT grain-collapsed. Non-distinct source codes would then
inline duplicate VALUES rows and inflate the staging join. Add _dedupe_to_grain
(last-wins, mirroring the store upsert) right after resolve_and_store. Found by
Codex adversarial review; regression test with duplicated source_codes.

Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
Folds in the plan review: D1 must rename the manifest's top-level `vocabularies:`
declaration alongside the binding slot (a sentinel with no declaration falls back
to INLINE and the normalizer raises DanglingRefError); D2's cleanup DELETE is now
schema-qualified and backup-guarded; the ":Term not re-keyed" claim is scoped to
the current manifest shape; and the runtime consumers of resolver_policy_ref plus
the policy-registry key are listed as auto-covered touch points.

Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
…re SNOMED-named anchor)

Retire the SNOMED-specific legacy anchor in the policy-ref key. Rename the
OMOP_ONCOTREE_CONDITION_REF value omop.oncotree_to_snomed_condition ->
omop.oncotree_condition (constant NAME and registry key unchanged, so dispatch
flows through automatically) and the slice-0 manifest resolver_policy_ref.
Tests now import the constant instead of hardcoding the literal, so a future
rename stays one-line. Policy docstring/comments corrected to say
OMOP standard-Condition (vocabulary-agnostic), not SNOMED.

Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
Retire the SNOMED-named binding anchor. Rename BOTH the top-level
vocabularies: declaration (name: OMOP-Condition) AND the binding vocabulary:
slot in the same edit — the slot is resolved against the declaration via
vocab_source_index, so renaming only the slot would fall back to INLINE and
raise DanglingRefError in the normalizer. The value now names the OMOP
governance scope, not a source vocabulary. Add a manifest-load regression test
proving the clean load, plus a binding.vocabulary_name assertion. Align the
_omop_binding live-test helper to the same sentinel.

Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
Move the decision doc's Deferred items to Done, mark the plan EXECUTED, and
record the two-commit repoint + D2 cache wipe + live Databricks verification in
.wolf/cerebrum.md and memory.md.

Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
…sting garbage

Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
…tion (S1-02)

Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
…ath (S1-03)

Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
…ition_start_date (S1-04/D4)

Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
…n write (S1-06)

Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
…r OMOP shape (S1-07)

Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
…ope S1-08 Databricks bridge

Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
DuckDB temp-swap vs Databricks atomic Delta INSERT ... REPLACE WHERE (no
BEGIN/COMMIT, no client temp table); FkClosedCompiler and run_fk_closed_qa
become dialect-agnostic via a backend strategy, mirroring StagingBackend.

Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
…S1-08)

Mirror the whole registry into a Delta table (CREATE OR REPLACE TABLE +
batched INSERT VALUES) so the live parent SELECT DISTINCT entity_id and child
FK join can read it in-warehouse; full idempotent replace.

Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
…S1-08)

Wire resolve -> registry bridge -> FK-closed materialize -> Gate-D-lite for one
study on DuckDB or Databricks; OMOP physical specs via make_omop_fk_specs in the
R29-allowlisted policy. Verified live on cbioportal_msk_chord_2024.

Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
showcase/ lives at the repo root, not under src/, so the editable install
never puts it on sys.path — the showcase commands resolved only by accident
of CWD. The two except-ImportError swallow sites dropped fit/fit-omop-shape/
collapse-omop-identities whenever sema ran outside the repo. Add
ensure_showcase_importable() (no-op for wheel/uv-tool installs), guard the
imports with find_spec, and log a warning on a genuinely broken showcase.

Also: reject divergent per-study shape in run_stage_b_collapse (the rebuild
borrows the head's specs/decisions for all sources) and correct the
rebuild_after_collapse docstring on the FK-closure guard's true reach.

Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
Signed-off-by: deanban <3989225+deanban@users.noreply.github.com>
@deanban
deanban force-pushed the ralph/feat/omop-shape-identity branch from 80afbd9 to 88da67e Compare July 22, 2026 22:32
@deanban
deanban merged commit a604d13 into main Jul 22, 2026
3 checks passed
@deanban
deanban deleted the ralph/feat/omop-shape-identity branch July 22, 2026 22:37
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.

1 participant