bcda: BCDA v3 — client, crosswalk-driven flatteners, vendored upstream, proving notebook (P52) #1140
Reference in New Issue
Block a user
Delete Branch "feat/bcda-v3"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #1139 (milestone P52). Design:
docs/superpowers/specs/2026-10-25-bcda-v3-design.md.What changes
Client (
bcda.client) — default API versionv3;_typeFilteronstart_export/exportwithtype_filter_tag()for the System-Type tag;jobs()sends the bulk-data headers BCDA requires (Prefer: respond-async,Accept: application/fhir+json— the old call 400'd on v2 and v3) and unwraps the Task Bundle;completed_jobs()lists reusable exports; every non-2xx raisesBcdaHttpErrorcarrying the OperationOutcome diagnostics (still anhttpx.HTTPStatusError); a poll that hits404 Job has been cancelledraisesJobCancelledError;/_versionand/_health.Store —
adopt(client, job_url)records and downloads a job this store did not start (completed jobs stay downloadable 24 h; the public sandbox entity is shared, so someone else's EOB job is as good as ours);export(..., cancel_retries=N)restarts after server-side cancellation and records eachcancelledtransition;type_filterpassthrough.Flatteners —
bcda.express.v3, table-driven from the BCDA v3 Data Dictionary (fhirPath→ column,sourceColumnfor v3-only columns,ccwMappingas the crosswalk onto the CCW-named columns the tables already had). v3 moved every BlueButton code offresources/variables/<name>ontofhir/StructureDefinition/<CODE>/fhir/CodeSystem/<CODE>and consolidated amounts intoadjudication/total/item.adjudication, so the v2 suffix match returned NULL for almost everything. Dispatch is per resource (v3.is_v3); v2 payloads, seeds and tests are unchanged.flatten_exportnow streams multi-part exports (<Type>.N.ndjson) to Parquet with model-derived Arrow schemas — a Large-entity EOB export is 192 files / 2.2 GB.Tables — v3-native nullable columns on
bcda.patient(contact language, source keys),bcda.coverage(one row per plan +period_start/end, buy-in, entitlement, dual, Part C/D contract/PBP/LIS, Rx class ids),bcda.explanation_of_benefit(System-Type/Final-Action tags, EOB-TYPE, claim control + original control number, IDR dates, header totals and PPS/IME/DSH adjudication amounts, Part D codes) andbcda.eob_item(NDC quantity, revenue-centre indicators, ANSI reason/group codes, Part D and DME line amounts). Race is crosswalked from US Core OMB codes intobene_race_cd; sex fromus-core-sexinto CCW coding.Pipeline —
cclf.stg_beneficiary_demographicsprefers the Part A coverage row and joins the v3 Dual row;eligibilityreadscoalesce(dual_01, dual_stus_cd).load_bcda()gathers the newest export directory per resource type (a v3 pull is three single-type jobs).Crosswalk as data —
dev/scripts/gen_bcda_v3_crosswalk.pyflattens the dictionary intodev/seeds/bcda_v3_data_dictionary.csv,bcda_v3_cclf_crosswalk.csv(293 CCLF elements → v3 field + fhirPath) andbcda_v3_cclf_absent.csv(52 CCLF fields v3 does not carry); tests assert the committed seeds match the vendored workbook.Vendored upstream —
dev/vendor/MANIFEST.tomlpins CMSgov/bcda-app at7decdcc(v3 handler + tests, request validation middleware, OperationOutcome writer, v3 Postman collection, gzip fixtures, sandbox config, LICENSE) and bcda.cms.gov's v3 data dictionary + official sample ndjson.dev/scripts/vendor_bcda_app.py --verify|--check|--sync(stdlib only); new weeklybcda-vendorworkflow runs--checkand files an issue on drift. Flattener tests run on the official samples.Notebook —
notebooks/bcda_v3_sandbox.py: preflight → auth/attribution → pull (adopt a completed job <24 h, else one export per type with cancel retries) → flatten + load + replica publish → proof checks → dictionary field population + CCLF crosswalk. Network/load steps only withBCDA_PULL=1/BCDA_LOAD=1or the buttons, so the nightly notebooks run never starts an export.Sandbox findings (2026-10-25, Large Model Entity)
Tests
bcdapackage at 100% line coverage; suite-wide gate (99%) green.Connector pipeline rewritten for v3 (
52ebad2)bcda.express.cclf/bcda.pipe.cclfhad never run end to end (its contracts named CCLF columns the EOB table does not have) and routed on the v2 near-line RIC code, which v3 emits on only some claims. Now:_stg_claim_headercarries the v3 semantics: dropsNotFinalActionclaims, derivesclaim_familyfromEOB-TYPE→CLM-TYPE-CD→pde_id→ RIC, collapses a claim seen in two sources on its claim control number (NCH/DDPS over SharedSystem, newestlast_updated), resolves the MBI. The four family stages are filters on it.medical_claimmapping for institutional / carrier / DME lines on the canonicalInputLayerMedicalClaimcontract: C4BB admission period, NUBC type of bill (newclm_bill_type_cd; rebuilt as0+ the 3-char v3 frequency code when missing), MS-DRG, revenue centre, HCPCS + modifiers, role-specific NPIs and tax numbers, paid/allowed/charge/coinsurance/deductible from the consolidated v3 Adjudication amounts, paid date fromclm_pd_dt→ NCH weekly → CMS process.ExplanationOfBenefitandaco.table.input_layer.InputLayer*;load_bcda(run_pipeline=True),stack load bcda --run-pipeline,stack run bcda.Large-entity pull (94 s incl. re-flatten): 221,021 medical lines on 28,282 claims (= FinalAction non-PDE EOBs by type; 4,111 NotFinalAction excluded), 11,968 pharmacy rows (= FinalAction PDEs), 9,582 eligibility rows. The notebook's new section 6 reconciles input_layer against
bcda.*: 16/16 checks pass. Suite: 15,703 passed, 99.01% coverage.BCDA manager — Streamlit app +
stack bcdaCLIsrc/bcda/ui/mirrors the CMS server's surface (CMSgov/bcda-app, vendored): preflight (/_version,/_health, metadata), auth + attribution,$exporton Group/all · Group/runout · Patient with_type/_since/_typeFilter(System-Type, outcome, service-date builder), job history with adopt (completed < 24 h) and cancel, plus the local side: store jobs/files/export dirs, flatten + load + connector + replica publish, data browsing on the read-only replica (summaries + SQL), the v3 data dictionary and CCLF crosswalk, and the vendored-upstream manifest with an on-demand drift check.bcda.ui.serviceis Streamlit-free and shared bystack bcda preflight|auth|jobs|export|adopt|cancel|store|load|vendor.bcda.ui.runnerruns long operations on daemon threads (survives reruns, captures each run'sbcda.*log lines, state flips last); ast.fragmentpanel shows runs live.BCDA_CLIENT_ID/SECRETfrom the environment, or custom (sandbox/production, v3/v2).stack bcda uilaunchesstreamlit run src/bcda/ui/app.py; Streamlit added to thebcdaextra. Containerising it is left for a follow-up.AppTest(buttons, forms, error paths), app shell, CLI — new modules at 100% coverage. Suite: 15,754 passed, 99.02%.Containerised
infra/images/bcda.Dockerfile(api pattern;--extra cli --extra bcda; Streamlit config ininfra/images/bcda/streamlit.toml; health/_stcore/health), compose servicebcda(gateway network,./datarw,./devro,BCDA_*from.env), Traefik routebcda.<domain>behind git-sso, dashboard tile,bcdasubdomain,[images.bcda]→ deploy.yml build/scan/push job. Built locally (2.15 GB), healthy, root 200; running on the host./metricsbcda.ui.metrics(perf.meter / #579 convention) publishes the OTel Prometheus registry onbcda:8502/metricsfrom a newpython -m bcda.uientrypoint (Streamlit only runs the script when a browser session opens, so the server has to start with the process). Instruments:stack_bcda_runs_total{kind,state},stack_bcda_runs_inflight{kind},stack_bcda_run_seconds{kind},stack_bcda_page_views_total{page}, gaugesstack_bcda_store_jobs{status},stack_bcda_store_bytes. Prometheus target added; verified up with live values.polarsadded to thebcdaextra (the views import it; it was dev-only, which crash-looped the first metrics image). Suite: 15,760 passed, 99.02%.Published at bcda.fhirworx.io
A new subdomain needs two Cloudflare writes beyond stack.toml/Traefik/CoreDNS: the tunnel's remote ingress list (it overrides the wildcard in
infra/cloudflared/config.yml; an unlisted host answers 530 at the edge) and a proxied CNAME to the tunnel.bootstrap_sso.pygainsbcdainSUBDOMAINSand a--dns-onlymode that does just those two writes and names the missing zone scope on failure (ae9e6f5,926acd8). Both writes applied;bcda.fhirworx.ioresolves publicly and the edge returns the SSO redirect.House style (
6c218b5)src/bcda/ui/theme.tomlcarries the dashboard tokens (assets/css/dashboard.css) as a Streamlit theme file;python -m bcda.uipasses it as--theme.base, so the container andstack bcda uimatch.bcda.ui.brandadds the Google Fonts import, the dashboard masthead at the top of the sidebar, eyebrows above page titles, and the repo favicon. Family names are quoted in the theme file because Streamlit 1.66 emits the body font unquoted and an unquotedSource Serif 4is invalid CSS (verified by rendering the rolled container headlessly).tests/bcda/test_ui_brand.pypins the theme todashboard.css.Incremental
_sincepulls (a05cc51)Design:
docs/superpowers/specs/2026-10-25-bcda-since-reconciliation-design.md.bcda.syncturns a_sinceexport from "the newest directory wins" into a reconciled store:BENE_SK, Coverage id, EOB claim control number, since each claim version gets a new FHIR id): newestlastUpdatedwins, then NCH/DDPS over SharedSystem, then latertransactionTime, later applied sequence, larger id.NotFinalActionwinners stay as tombstones; only a baseline retires rows;eob_itemfollows its header._since, split by whether the enrollee was already known — Group/all backfills new enrollees' full history by contract), tombstoned, adjusted (clm_orig_cntl_num→bcda.eob_supersession), retired, orphans, scope drift._sinceexport (block / warn / ok): format, future, floor (2026-03-26), no_baseline, gap, overlap, endpoint_mix (runout vs current population), backfill_scope, scope_filter, inflight, unapplied, attribution_newer, stale_baseline.next_since= the previous job'stransactionTime, as BCDA recommends.transaction_time/requeston job rows,exports/<key>/manifest.json,resumebackfills both; empty deltas still advance the watermark.stack bcda sync status|check|apply|pull,stack bcda export --since auto,stack bcda resume; Exports page_sincemodes with the checks shown; new Sync page; notebook §8.Verified on the real store: the three baselines reconciled over the legacy tables as all-unchanged with identical loaded counts; a real
/PatientEOB delta with a_typeFilterreconciled as 248 unchanged with the drift on record.Databricks App playbook (
8cd4633) and a fallback fix (af9c088)cloud/databricks/README.md+cloud/databricks/bcda-app/scaffold: how to run the manager as a Databricks App for a user without catalog rights — admin pre-provisions catalog/schema/volume/warehouse/secrets, the app's service principal gets schema-level grants (attaching auc_securablegrantsUSE CATALOG/USE SCHEMAitself), store on a UC volume via fsspec, Delta tables through the warehouse, Data page on the user's own token, compute/egress/source-size optimisation, runbook, phases, open questions, sources.The legacy transactionTime fallback now uses the job's start instant (overlap-safe) rather than its completion (gap-prone) — caught on the real store, where the sandbox stamps
transactionTimeas midnight UTC.Upstream review (
2e44650)docs/superpowers/specs/2026-10-25-bcda-upstream-review.md: bcda-app7decdcc, its last 400 PRs and the docs source read against what we built. The_sincecontract we implemented is the server's (_lastUpdated=gt<since>,le<transactionTime>;transactionTime= BFD load completion, PR #1483). Eight gaps filed as #1293–#1300; the two that change claim identity are error files / partial jobs advancing the watermark (#1293) and control-number chains, the DME VMS→NCH digit and Part D natural keys (#1294).Upstream-review gaps closed (
2cc1300, closes #1293–#1300)Partial exports: manifest
errorfiles downloaded as OperationOutcome, every URL attempted, oneDownloadError; a partial job reconciles but never becomes the watermark (partial_jobcheck, Store page issues,stack bcda errors). Claim identity per CMS's dedupe guidance:clm_cntl_num_match(VMS→NCH digit),clm_natural_key(Part D),superseded_by(therelatedchain, walked; new invariant),prvdr_srvc_id_qlfyr_cd;bcda.syncand the connector key claims the same way. Attribution:attribution_expired(block),attribution_floor(warn),attribution_updaterecorded per job, server 404/400/429 texts explained.completed_jobs()honours the 24 h retention.build_type_filterfollowsbcda/fhir/param.go(outcome lists, service-date bounds and prefixes).runout_windowcheck and population labels. Vendor manifest pins the behaviour-defining upstream files, the docs source repo and the PAC/unmapped dictionaries. Design record quotes the server's(since, transactionTime]window and the silent exclusions.Real store re-reconciled with
--force: every rowunchanged, loaded counts identical.Provider identity: CCN ↔ NPI ↔ TIN (
63e9c00)Institutional claims carry no TIN in BCDA v3 or CCLF, and CMS publishes no provider TIN on data.cms.gov. The connector now gives professional/DME lines their carrier TIN as
billing_tin;stack load cmsvendors the institutional universe (PECOS enrollments ×6, Provider of Services, cost reports) intocms.*and derivescms.ccn_crosswalk(65,161 CCN↔NPI rows),cms.provider_tin_observedandcms.ccn_tin(participant-list hookcms.aco_participant_tin), with a coverage line and a Data page query.stack bcdaCLI over the client, store, loader, connector and crosswalksrc/bcda/ui/theme.toml is a Streamlit theme file carrying the dashboard tokens from assets/css/dashboard.css (cream paper, near-black ink, navy primary and sidebar, warm beige surfaces, 2px radius, Playfair Display / Source Serif 4 / JetBrains Mono). `python -m bcda.ui` passes it as --theme.base unless the caller set one, so the container and `stack bcda ui` look the same without a .streamlit/config.toml per cwd; the image's streamlit.toml keeps only server settings. bcda.ui.brand adds what a theme file cannot: the Google Fonts import, the dashboard masthead (rule, display title, monospace eyebrow) at the top of the sidebar, and brand.title() eyebrows above every page title. app.py renders its own page links after the masthead (navigation position="hidden") so the masthead comes first as on the dashboard. The favicon is the repo's assets/icons/fav32.png, baked into the image. Family names in the theme file stay double-quoted: Streamlit 1.66 emits the body font unquoted, and an unquoted family with a digit ("Source Serif 4") is an invalid font-family, so the browser silently fell back to its default serif while the headings (quoted on their path) were fine. Found by rendering the rolled container headlessly on the compose network and reading computed styles. tests/bcda/test_ui_brand.py pins the theme's hexes and font stacks to dashboard.css and renders inject/masthead/title through AppTest; test_ui_main covers the --theme.base handling. refs #1139View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.