Skip to content

measure-evaluate: drive the demo from the installed FHIR package - #44

Merged
aleksandrkislitsyn merged 3 commits into
mainfrom
measure-evaluate-package-driven
Sep 16, 2026
Merged

aleksandrkislitsyn merged 3 commits into
mainfrom
measure-evaluate-package-driven

Conversation

@aleksandrkislitsyn

Copy link
Copy Markdown
Contributor

The sample hardcoded a 12-measure registry and rebuilt its SQL layer from files in this repo. Both are now read from whatever package Aidbox has installed, so the same app serves a 12-measure package or a larger private one with no code change.

Measure catalog from Aidbox

app/catalog.py discovers measures from their SQLQuery Library resources (<measure>-summary / -per-patient / -evidence) and resolves each one's ViewDefinition dependencies transitively through relatedArtifact, dispatching on the canonical url (/ViewDefinition/ vs /Library/). Measure resources are optional metadata rather than a gate — a package shipping Libraries alone still yields a full catalog.

Measure/$evaluate-measure no longer rejects measures missing from service/measures-registry.json. Previously any measure outside those 12 returned:

Unknown measure: cms1017. Available: cms1154, cms124, … cms75

The registry now only supplies optional extras (exc_type, supported_version).

Materialization from the UI

POST /api/materialize builds the runtime state, per measure or for all of them:

  1. $materialize the needed ViewDefinitions into sof.* tables
  2. run the package's setup-NN-* scripts (terminology scaffolding, indexes)
  3. flatten far.valueset into sof.concept — the one step no package resource can express, since Aidbox keeps ValueSets in a registry the SoF engine cannot see

Views already present are skipped, so measures sharing views cost nothing on a second run. Buttons: Materialize all in the sidebar and overview, Rematerialize <MEASURE> in the measure detail header. Setup scripts run statement-by-statement so an index for a table this pass did not create cannot roll back the rest.

No wrapper views are created. Aidbox inlines the package's sql-view Libraries as CTEs when running a SQLQuery, so the measure SQL's bare patient_flat resolves without any database view — verified by dropping all 22 and re-running measures to identical numbers, while raw $sql on the same name still errors.

Demo app

  • Served at http://localhost:8090 by the app container; no separate python3 -m http.server step.
  • All local SQL removed — summary, per-patient and evidence come from the Libraries via $sqlquery-run. demo/measure-sql.json is deleted.
  • Sidebar filter (id substring + All / Open gaps / Needs setup) for navigating a large measure list; the overview grid follows it.

Setup

setup.py is replaced by load-demo-data.py, which only loads the sample bundles. Its other five steps either duplicated the package or actively conflicted with it — recreating the superseded wrapper layer, wiping sof.concept — and it only knew the 12 registry measures.

The init bundle now inlines data/*-clinical-data.json, so a fresh box comes up with patients loaded and needs no data step. It is a batch, so one rejected resource cannot sink the load; pass --no-demo-data for an Aidbox holding real data. init.json is generated and gitignored (multi-megabyte once the data is inlined) and README step 1 builds it alongside the package .tgz.

sql/01-wrapper-views.sql and sql/02-shared-exclusions.sql are dropped — the package ships both layers now.

Sample-data fixes

Two resources failed validation on load, and the Organization took two Coverage resources with it:

Resource Defect
Condition.clinicalStatus (cms143) display 'InActive', should be 'Inactive'
Organization/example (cms165) neither name nor identifier, violates org-1

A fresh boot now logs zero batch errors; Coverage goes 31 → 33.

Verification

Full teardown (docker compose down -v), rebuilt both artifacts from the README, fresh boot:

batch errors: 0
patients: 485   coverage: 33   organization: 16

materialize: 22 tables, 2 setup scripts, 115,201 concepts, 0 failures
cms130  ip=204 exc=80 num=8  open=116
cms131  ip=57  exc=26 num=11 open=20
cms165  ip=75  exc=42 num=2  open=31

Demo driven in a headless browser: measures listed, live gap counts, filter and materialize buttons working, no JS errors.

🤖 Generated with Claude Code

aleksandrkislitsyn and others added 3 commits September 15, 2026 16:31
The sample hardcoded a 12-measure registry and rebuilt its SQL layer from
files in this repo. Both are now read from whatever package Aidbox has
installed, so the same app serves a 12-measure package or a larger private
one with no code change.

Measure catalog from Aidbox
- app/catalog.py discovers measures from their SQLQuery `Library` resources
  (<measure>-summary / -per-patient / -evidence) and resolves each one's
  ViewDefinition dependencies transitively through `relatedArtifact`,
  dispatching on the canonical url (/ViewDefinition/ vs /Library/).
- `Measure` resources are optional metadata, not a gate: a package that ships
  Libraries alone still yields a full catalog.
- Measure/$evaluate-measure no longer rejects measures missing from
  service/measures-registry.json; the registry now only supplies optional
  extras (exc_type, supported_version). Previously any measure outside those
  12 returned 404.

Materialization from the UI
- POST /api/materialize builds the runtime state per measure or for all of
  them: $materialize the needed ViewDefinitions, run the package's
  `setup-NN-*` scripts, then flatten far.valueset into sof.concept (the one
  step no package resource can express, since Aidbox keeps ValueSets in a
  registry the SoF engine cannot see).
- Views already present are skipped, so measures sharing views cost nothing
  on a second run. Buttons: "Materialize all" in the sidebar and overview,
  "Rematerialize <MEASURE>" in the measure detail header.
- Setup scripts run statement-by-statement so an index for a table this pass
  did not create cannot roll back the rest.
- No wrapper views are created: Aidbox inlines the package's `sql-view`
  Libraries as CTEs when running a SQLQuery, so the measure SQL's bare
  `patient_flat` resolves without any database view. Verified by dropping all
  22 and re-running measures to identical numbers.

Demo app
- Serves at http://localhost:8090 (was a separate `python3 -m http.server`).
- All local SQL removed; summary, per-patient and evidence now come from the
  Libraries via $sqlquery-run. demo/measure-sql.json is deleted.
- Sidebar filter (id substring + All / Open gaps / Needs setup) for
  navigating a large measure list; the overview grid follows it.

Setup
- setup.py is replaced by load-demo-data.py, which only loads the sample
  bundles. Its other five steps either duplicated the package or actively
  conflicted with it (recreating the superseded wrapper layer, wiping
  sof.concept), and it only knew the 12 registry measures.
- The init bundle now inlines data/*-clinical-data.json, so a fresh box comes
  up with patients loaded and needs no data step. It is a `batch`, so one
  rejected resource cannot sink the load. Build with --no-demo-data for an
  Aidbox holding real data.
- init.json is generated and gitignored (it is multi-megabyte once the demo
  data is inlined); README step 1 builds it alongside the package .tgz.
- sql/01-wrapper-views.sql and sql/02-shared-exclusions.sql are dropped —
  the package ships both layers now.

Fixes two sample-data defects that failed validation on load: a
Condition.clinicalStatus display of 'InActive' (should be 'Inactive') and an
Organization with neither name nor identifier, which also took two Coverage
resources with it. A fresh boot now logs zero batch errors.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ge ships

sql/03-sof-indexes.sql covered all 22 flat tables, but the package built from
this repo ships 10 ViewDefinitions — so 13 of those tables can never exist and
24 of the 39 CREATE INDEX statements targeted nothing. The per-statement
execution in run_setup_libraries() kept that from failing the install, but it
meant 24 skipped statements on every materialize.

Trim the script to the 9 flat tables the shipped ViewDefinitions produce, and
narrow the trailing ANALYZE to match (it listed all 22, so as a single
statement it would have errored outright).

The library's `depends-on` metadata was already correct — _setup_lineage()
filters to ViewDefinitions present on disk — so the SQL body and the declared
dependencies now agree: 9 tables, 9 ViewDefinitions.

Verified: 15 statements, all 16 (with ANALYZE) apply with zero failures against
a live box, and a full materialize reports no failed steps with measure results
unchanged (cms130 204/80/8, cms131 57/26/11, cms165 75/42/2).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The sample was missing from the Aidbox Custom Operations list, so it was not
discoverable from the repository index. Placed after the IPS entries, matching
the section's existing order.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@aleksandrkislitsyn
aleksandrkislitsyn merged commit 9bf0b75 into main Sep 16, 2026
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