carebundle 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- carebundle-0.1.0/.github/workflows/ci.yml +211 -0
- carebundle-0.1.0/.github/workflows/release.yml +134 -0
- carebundle-0.1.0/.gitignore +12 -0
- carebundle-0.1.0/BENCHMARK.md +250 -0
- carebundle-0.1.0/CHANGELOG.md +86 -0
- carebundle-0.1.0/CONFORMANCE.md +94 -0
- carebundle-0.1.0/FIDELITY.md +93 -0
- carebundle-0.1.0/LICENSE +202 -0
- carebundle-0.1.0/PKG-INFO +511 -0
- carebundle-0.1.0/README.md +477 -0
- carebundle-0.1.0/RELEASING.md +114 -0
- carebundle-0.1.0/ROADMAP.md +428 -0
- carebundle-0.1.0/carebundle/__init__.py +37 -0
- carebundle-0.1.0/carebundle/benchmark/__init__.py +23 -0
- carebundle-0.1.0/carebundle/benchmark/cqm.py +205 -0
- carebundle-0.1.0/carebundle/builders/__init__.py +0 -0
- carebundle-0.1.0/carebundle/builders/clinical.py +188 -0
- carebundle-0.1.0/carebundle/builders/orders.py +104 -0
- carebundle-0.1.0/carebundle/builders/people.py +64 -0
- carebundle-0.1.0/carebundle/calibration/__init__.py +0 -0
- carebundle-0.1.0/carebundle/calibration/custom.py +231 -0
- carebundle-0.1.0/carebundle/calibration/data/nhanes_targets.json +2445 -0
- carebundle-0.1.0/carebundle/calibration/nhanes.py +356 -0
- carebundle-0.1.0/carebundle/calibration/xpt.py +130 -0
- carebundle-0.1.0/carebundle/cli.py +276 -0
- carebundle-0.1.0/carebundle/conformance/__init__.py +0 -0
- carebundle-0.1.0/carebundle/conformance/validator.py +162 -0
- carebundle-0.1.0/carebundle/core/__init__.py +0 -0
- carebundle-0.1.0/carebundle/core/bundle.py +110 -0
- carebundle-0.1.0/carebundle/core/ids.py +41 -0
- carebundle-0.1.0/carebundle/core/safety.py +115 -0
- carebundle-0.1.0/carebundle/core/uscore.py +27 -0
- carebundle-0.1.0/carebundle/correlation/__init__.py +0 -0
- carebundle-0.1.0/carebundle/correlation/distributions.py +358 -0
- carebundle-0.1.0/carebundle/correlation/engine.py +84 -0
- carebundle-0.1.0/carebundle/correlation/relations.py +312 -0
- carebundle-0.1.0/carebundle/fidelity/__init__.py +0 -0
- carebundle-0.1.0/carebundle/fidelity/report.py +472 -0
- carebundle-0.1.0/carebundle/generate.py +462 -0
- carebundle-0.1.0/carebundle/history.py +260 -0
- carebundle-0.1.0/carebundle/imperfection/__init__.py +30 -0
- carebundle-0.1.0/carebundle/imperfection/defects.py +250 -0
- carebundle-0.1.0/carebundle/models/__init__.py +0 -0
- carebundle-0.1.0/carebundle/models/r4.py +1046 -0
- carebundle-0.1.0/carebundle/profiles/__init__.py +0 -0
- carebundle-0.1.0/carebundle/profiles/base.py +219 -0
- carebundle-0.1.0/carebundle/profiles/library.py +657 -0
- carebundle-0.1.0/carebundle/py.typed +0 -0
- carebundle-0.1.0/carebundle/spec/__init__.py +0 -0
- carebundle-0.1.0/carebundle/spec/codegen.py +307 -0
- carebundle-0.1.0/carebundle/terminology/__init__.py +0 -0
- carebundle-0.1.0/carebundle/terminology/codes.py +398 -0
- carebundle-0.1.0/carebundle/terminology/systems.py +28 -0
- carebundle-0.1.0/carebundle/terminology/verify.py +174 -0
- carebundle-0.1.0/pyproject.toml +85 -0
- carebundle-0.1.0/synthetic-fhir-generator-build-doc.md +658 -0
- carebundle-0.1.0/tests/conftest.py +37 -0
- carebundle-0.1.0/tests/golden/bundle-ckd_stage3.json +3432 -0
- carebundle-0.1.0/tests/golden/bundle-healthy.json +2832 -0
- carebundle-0.1.0/tests/golden/bundle-hypertension.json +3030 -0
- carebundle-0.1.0/tests/golden/bundle-type2_diabetes.json +3324 -0
- carebundle-0.1.0/tests/golden/patient-F.json +37 -0
- carebundle-0.1.0/tests/golden/patient-M.json +37 -0
- carebundle-0.1.0/tests/test_benchmark.py +160 -0
- carebundle-0.1.0/tests/test_bundle.py +297 -0
- carebundle-0.1.0/tests/test_calibration.py +246 -0
- carebundle-0.1.0/tests/test_cli.py +183 -0
- carebundle-0.1.0/tests/test_cohort.py +203 -0
- carebundle-0.1.0/tests/test_conformance.py +105 -0
- carebundle-0.1.0/tests/test_correlation.py +352 -0
- carebundle-0.1.0/tests/test_custom_calibration.py +187 -0
- carebundle-0.1.0/tests/test_determinism.py +77 -0
- carebundle-0.1.0/tests/test_fidelity.py +92 -0
- carebundle-0.1.0/tests/test_golden.py +67 -0
- carebundle-0.1.0/tests/test_history.py +131 -0
- carebundle-0.1.0/tests/test_imperfection.py +161 -0
- carebundle-0.1.0/tests/test_public_api.py +174 -0
- carebundle-0.1.0/tests/test_terminology.py +268 -0
- carebundle-0.1.0/tests/test_validator_harness.py +142 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
push:
|
|
6
|
+
branches: [main]
|
|
7
|
+
schedule:
|
|
8
|
+
# Nightly full conformance matrix + fidelity report.
|
|
9
|
+
- cron: "0 6 * * *"
|
|
10
|
+
# So the nightly-only jobs can be exercised on demand. A job that has only ever been
|
|
11
|
+
# scheduled is a job nobody has watched fail.
|
|
12
|
+
workflow_dispatch:
|
|
13
|
+
|
|
14
|
+
env:
|
|
15
|
+
VALIDATOR_URL: https://github.com/hapifhir/org.hl7.fhir.core/releases/latest/download/validator_cli.jar
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
# Fast gate. No JVM — this is the loop contributors wait on.
|
|
19
|
+
unit:
|
|
20
|
+
runs-on: ${{ matrix.os }}
|
|
21
|
+
strategy:
|
|
22
|
+
# Every version in `requires-python`, because the README claims the whole range.
|
|
23
|
+
# Do not trim this to save minutes without trimming the claim to match.
|
|
24
|
+
#
|
|
25
|
+
# macOS and Windows carry one version each rather than the full grid: the package
|
|
26
|
+
# is pure Python, so the cross-platform risk is concentrated in the CLI's POSIX
|
|
27
|
+
# assumptions (SIGPIPE, os.dup2 on a real stdout fd) and in path handling, none of
|
|
28
|
+
# which vary by Python minor. `requires-python` names no platform, so leaving
|
|
29
|
+
# Windows untested would be claiming support nobody had checked.
|
|
30
|
+
fail-fast: false
|
|
31
|
+
matrix:
|
|
32
|
+
os: [ubuntu-latest]
|
|
33
|
+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
34
|
+
include:
|
|
35
|
+
- os: macos-latest
|
|
36
|
+
python-version: "3.12"
|
|
37
|
+
- os: windows-latest
|
|
38
|
+
python-version: "3.12"
|
|
39
|
+
# Windows defaults to PowerShell, which quotes differently. Git Bash ships on the
|
|
40
|
+
# Windows runners, so pinning bash keeps all three platforms running the identical
|
|
41
|
+
# command rather than three dialects of it.
|
|
42
|
+
defaults:
|
|
43
|
+
run:
|
|
44
|
+
shell: bash
|
|
45
|
+
steps:
|
|
46
|
+
- uses: actions/checkout@v4
|
|
47
|
+
# Required, and not only for the matrix to mean anything: without it
|
|
48
|
+
# `uv pip install --system` targets the runner's system interpreter, which is
|
|
49
|
+
# PEP 668 externally-managed and refuses the install outright.
|
|
50
|
+
- uses: actions/setup-python@v5
|
|
51
|
+
with:
|
|
52
|
+
python-version: ${{ matrix.python-version }}
|
|
53
|
+
- uses: astral-sh/setup-uv@v5
|
|
54
|
+
- name: Install
|
|
55
|
+
run: uv pip install --system -e ".[dev]"
|
|
56
|
+
- name: Confirm the matrix version is the one under test
|
|
57
|
+
run: python -c "import sys; assert '.'.join(map(str, sys.version_info[:2])) == '${{ matrix.python-version }}', sys.version"
|
|
58
|
+
- name: Lint
|
|
59
|
+
run: ruff check carebundle tests
|
|
60
|
+
- name: Unit, property and determinism tests
|
|
61
|
+
run: pytest tests/ -q -m "not conformance and not fidelity" --cov --cov-report=term-missing
|
|
62
|
+
|
|
63
|
+
# Guards the artefacts themselves. The unit job installs from the source tree, so it
|
|
64
|
+
# cannot catch a packaging fault: a missing py.typed, a data file left out of the
|
|
65
|
+
# wheel, or metadata PyPI will reject. This installs the built wheel into a clean
|
|
66
|
+
# environment, outside the repo, and exercises the README's entry points there.
|
|
67
|
+
package:
|
|
68
|
+
runs-on: ubuntu-latest
|
|
69
|
+
steps:
|
|
70
|
+
- uses: actions/checkout@v4
|
|
71
|
+
- uses: actions/setup-python@v5
|
|
72
|
+
with:
|
|
73
|
+
python-version: "3.12"
|
|
74
|
+
- name: Build sdist and wheel
|
|
75
|
+
run: |
|
|
76
|
+
python -m pip install --quiet build twine
|
|
77
|
+
python -m build
|
|
78
|
+
- name: Check metadata renders on PyPI
|
|
79
|
+
run: twine check --strict dist/*
|
|
80
|
+
- name: Install the wheel into a clean environment
|
|
81
|
+
run: |
|
|
82
|
+
python -m venv /tmp/probe
|
|
83
|
+
/tmp/probe/bin/pip install --quiet dist/*.whl
|
|
84
|
+
- name: Smoke-test the installed package from outside the repo
|
|
85
|
+
working-directory: /tmp
|
|
86
|
+
run: |
|
|
87
|
+
/tmp/probe/bin/python -c "
|
|
88
|
+
import json, pathlib, carebundle
|
|
89
|
+
from carebundle import (generate_bundle, generate_history, generate_cohort,
|
|
90
|
+
to_json, Imperfection, inject_defects,
|
|
91
|
+
calibrate_profile, Quartiles)
|
|
92
|
+
|
|
93
|
+
bundle = json.loads(to_json(generate_bundle(profile='type2_diabetes', seed=42, sex='F')))
|
|
94
|
+
assert bundle['resourceType'] == 'Bundle', bundle['resourceType']
|
|
95
|
+
assert bundle['entry'], 'installed package generated an empty bundle'
|
|
96
|
+
|
|
97
|
+
# Every public entry point, because the source-tree job cannot catch a
|
|
98
|
+
# packaging fault and each of these is a separate import surface.
|
|
99
|
+
history = json.loads(to_json(generate_history(profile='hypertension', seed=42, visits=4)))
|
|
100
|
+
encounters = [e for e in history['entry'] if e['resource']['resourceType'] == 'Encounter']
|
|
101
|
+
assert len(encounters) == 4, encounters
|
|
102
|
+
|
|
103
|
+
_, defects = inject_defects(bundle, Imperfection(missing_field=0.3), seed=5)
|
|
104
|
+
assert defects, 'imperfection injected nothing'
|
|
105
|
+
|
|
106
|
+
calibrate_profile('ci_probe', base='type2_diabetes',
|
|
107
|
+
marginals={'hba1c': Quartiles(median=8.4, q1=7.5, q3=9.8, low=6.0, high=13.5),
|
|
108
|
+
'glucose': Quartiles(median=190, q1=155, q3=240, low=90, high=420)})
|
|
109
|
+
assert json.loads(to_json(generate_bundle(profile='ci_probe', seed=42)))['entry']
|
|
110
|
+
|
|
111
|
+
assert len(list(generate_cohort(count=3, seed=1))) == 3
|
|
112
|
+
|
|
113
|
+
marker = pathlib.Path(carebundle.__file__).parent / 'py.typed'
|
|
114
|
+
assert marker.exists(), 'py.typed missing from the wheel'
|
|
115
|
+
data = pathlib.Path(carebundle.__file__).parent / 'calibration/data/nhanes_targets.json'
|
|
116
|
+
assert data.exists(), 'calibration data missing from the wheel'
|
|
117
|
+
print('installed package OK:', len(bundle['entry']), 'entries')
|
|
118
|
+
"
|
|
119
|
+
/tmp/probe/bin/carebundle profiles
|
|
120
|
+
/tmp/probe/bin/carebundle generate --profile healthy --seed 1 --out /tmp/fixtures
|
|
121
|
+
- name: A closed pipe must not print a traceback
|
|
122
|
+
working-directory: /tmp
|
|
123
|
+
run: |
|
|
124
|
+
/tmp/probe/bin/carebundle generate --profile healthy --seed 1 2>/tmp/err.txt | head -3
|
|
125
|
+
if [ -s /tmp/err.txt ]; then echo "::error::stderr noise on a closed pipe"; cat /tmp/err.txt; exit 1; fi
|
|
126
|
+
- uses: actions/upload-artifact@v4
|
|
127
|
+
with:
|
|
128
|
+
name: distributions
|
|
129
|
+
path: dist/
|
|
130
|
+
|
|
131
|
+
# Authoritative conformance gate. The JVM lives HERE and only here — it is never a
|
|
132
|
+
# runtime or install dependency of the published package (build doc Section 10).
|
|
133
|
+
conformance:
|
|
134
|
+
runs-on: ubuntu-latest
|
|
135
|
+
steps:
|
|
136
|
+
- uses: actions/checkout@v4
|
|
137
|
+
- uses: actions/setup-java@v4
|
|
138
|
+
with:
|
|
139
|
+
distribution: temurin
|
|
140
|
+
java-version: "17"
|
|
141
|
+
- uses: actions/setup-python@v5
|
|
142
|
+
with:
|
|
143
|
+
python-version: "3.12"
|
|
144
|
+
- uses: astral-sh/setup-uv@v5
|
|
145
|
+
- name: Install
|
|
146
|
+
run: uv pip install --system -e ".[dev]"
|
|
147
|
+
- name: Fetch HL7 validator
|
|
148
|
+
run: |
|
|
149
|
+
mkdir -p .tools
|
|
150
|
+
curl -sSL -o .tools/validator_cli.jar "$VALIDATOR_URL"
|
|
151
|
+
- name: Cache FHIR package registry
|
|
152
|
+
uses: actions/cache@v4
|
|
153
|
+
with:
|
|
154
|
+
path: ~/.fhir
|
|
155
|
+
key: fhir-packages-us-core-6.1.0
|
|
156
|
+
- name: Fetch R4 spec definitions
|
|
157
|
+
run: |
|
|
158
|
+
mkdir -p .tools/spec
|
|
159
|
+
curl -sSL -o .tools/spec/definitions.json.zip https://hl7.org/fhir/R4/definitions.json.zip
|
|
160
|
+
(cd .tools/spec && unzip -o -q definitions.json.zip)
|
|
161
|
+
- name: Verify generated models match the spec
|
|
162
|
+
run: |
|
|
163
|
+
python -m carebundle.spec.codegen
|
|
164
|
+
git diff --exit-code carebundle/models/r4.py \
|
|
165
|
+
|| (echo "::error::carebundle/models/r4.py is stale — run 'python -m carebundle.spec.codegen' and commit" && exit 1)
|
|
166
|
+
- name: US Core conformance
|
|
167
|
+
# On PRs this runs a small sample to keep the loop tight; nightly runs the
|
|
168
|
+
# full profile x resource-type matrix.
|
|
169
|
+
run: |
|
|
170
|
+
if [ "${{ github.event_name }}" = "schedule" ]; then
|
|
171
|
+
pytest tests/ -q -m conformance --conformance-scale=full
|
|
172
|
+
else
|
|
173
|
+
pytest tests/ -q -m conformance --conformance-scale=sample
|
|
174
|
+
fi
|
|
175
|
+
|
|
176
|
+
# Re-check every shipped code against its source vocabulary. Curated subsets go
|
|
177
|
+
# stale (build doc Section 14), and a display that drifts is rejected outright by
|
|
178
|
+
# the HL7 validator. Nightly because it depends on three external APIs.
|
|
179
|
+
terminology:
|
|
180
|
+
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
|
|
181
|
+
runs-on: ubuntu-latest
|
|
182
|
+
steps:
|
|
183
|
+
- uses: actions/checkout@v4
|
|
184
|
+
- uses: actions/setup-python@v5
|
|
185
|
+
with:
|
|
186
|
+
python-version: "3.12"
|
|
187
|
+
- uses: astral-sh/setup-uv@v5
|
|
188
|
+
- name: Install
|
|
189
|
+
run: uv pip install --system -e ".[dev]"
|
|
190
|
+
- name: Verify codes against LOINC, RxNorm and ICD-10-CM
|
|
191
|
+
run: python -m carebundle.terminology.verify
|
|
192
|
+
|
|
193
|
+
# Statistical evidence for the clinical-coherence claim (build doc Section 8).
|
|
194
|
+
# Nightly only: N=10,000 patients per profile is too slow for a PR gate.
|
|
195
|
+
fidelity:
|
|
196
|
+
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
|
|
197
|
+
runs-on: ubuntu-latest
|
|
198
|
+
steps:
|
|
199
|
+
- uses: actions/checkout@v4
|
|
200
|
+
- uses: actions/setup-python@v5
|
|
201
|
+
with:
|
|
202
|
+
python-version: "3.12"
|
|
203
|
+
- uses: astral-sh/setup-uv@v5
|
|
204
|
+
- name: Install
|
|
205
|
+
run: uv pip install --system -e ".[dev]"
|
|
206
|
+
- name: Fidelity report
|
|
207
|
+
run: pytest tests/ -q -m fidelity
|
|
208
|
+
- uses: actions/upload-artifact@v4
|
|
209
|
+
with:
|
|
210
|
+
name: fidelity-report
|
|
211
|
+
path: FIDELITY.md
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Publishes to PyPI when a v* tag is pushed. Uses Trusted Publishing (OIDC), so there
|
|
4
|
+
# is no API token in this repository's secrets to leak or rotate — PyPI is configured
|
|
5
|
+
# to trust this workflow in this repository instead.
|
|
6
|
+
#
|
|
7
|
+
# Before the first run, register the publisher at
|
|
8
|
+
# https://pypi.org/manage/account/publishing/ with:
|
|
9
|
+
# project: carebundle | owner: 27jackson08 | repo: fhirfaker
|
|
10
|
+
# workflow: release.yml | environment: pypi
|
|
11
|
+
#
|
|
12
|
+
# workflow_dispatch runs everything EXCEPT the publish job, so the pipeline can be
|
|
13
|
+
# rehearsed on a branch. Publishing is gated on `github.ref_type == 'tag'`, which a
|
|
14
|
+
# manual dispatch on a branch can never satisfy — the dry run cannot upload by
|
|
15
|
+
# accident. Rehearse before relying on this: the previous CI workflow was silently
|
|
16
|
+
# broken for months precisely because nobody had watched it run.
|
|
17
|
+
on:
|
|
18
|
+
push:
|
|
19
|
+
tags: ["v*"]
|
|
20
|
+
workflow_dispatch:
|
|
21
|
+
|
|
22
|
+
permissions:
|
|
23
|
+
contents: read
|
|
24
|
+
|
|
25
|
+
jobs:
|
|
26
|
+
# A tag that disagrees with pyproject is the classic release footgun: it publishes
|
|
27
|
+
# the wrong version under the right name, and PyPI will not let you reuse the number.
|
|
28
|
+
# Fail here rather than after upload.
|
|
29
|
+
verify:
|
|
30
|
+
runs-on: ubuntu-latest
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/checkout@v4
|
|
33
|
+
- uses: actions/setup-python@v5
|
|
34
|
+
with:
|
|
35
|
+
python-version: "3.12"
|
|
36
|
+
- name: Tag must match the version in pyproject.toml
|
|
37
|
+
# Only meaningful on a tag; on a dispatch rehearsal GITHUB_REF_NAME is a branch.
|
|
38
|
+
if: github.ref_type == 'tag'
|
|
39
|
+
run: |
|
|
40
|
+
python - <<'PY'
|
|
41
|
+
import os, pathlib, re, sys, tomllib
|
|
42
|
+
tag = os.environ["GITHUB_REF_NAME"]
|
|
43
|
+
version = tomllib.loads(pathlib.Path("pyproject.toml").read_text())["project"]["version"]
|
|
44
|
+
if tag != f"v{version}":
|
|
45
|
+
sys.exit(f"::error::tag {tag} does not match pyproject version {version} (expected v{version})")
|
|
46
|
+
changelog = pathlib.Path("CHANGELOG.md").read_text()
|
|
47
|
+
if not re.search(rf"^## \[{re.escape(version)}\]", changelog, re.M):
|
|
48
|
+
sys.exit(f"::error::CHANGELOG.md has no '## [{version}]' section")
|
|
49
|
+
print(f"tag {tag} matches pyproject and CHANGELOG")
|
|
50
|
+
PY
|
|
51
|
+
|
|
52
|
+
# Do not publish something the gates have not passed. This duplicates ci.yml
|
|
53
|
+
# deliberately: a tag can be pushed at a commit CI never ran on.
|
|
54
|
+
gates:
|
|
55
|
+
needs: verify
|
|
56
|
+
runs-on: ubuntu-latest
|
|
57
|
+
steps:
|
|
58
|
+
- uses: actions/checkout@v4
|
|
59
|
+
- uses: actions/setup-java@v4
|
|
60
|
+
with:
|
|
61
|
+
distribution: temurin
|
|
62
|
+
java-version: "17"
|
|
63
|
+
# Without this, `uv pip install --system` targets the runner's PEP 668
|
|
64
|
+
# externally-managed system interpreter and the install fails.
|
|
65
|
+
- uses: actions/setup-python@v5
|
|
66
|
+
with:
|
|
67
|
+
python-version: "3.12"
|
|
68
|
+
- uses: astral-sh/setup-uv@v5
|
|
69
|
+
- name: Install
|
|
70
|
+
run: uv pip install --system -e ".[dev]"
|
|
71
|
+
- name: Lint
|
|
72
|
+
run: ruff check carebundle tests
|
|
73
|
+
- name: Unit, property and determinism tests
|
|
74
|
+
run: pytest tests/ -q -m "not conformance and not fidelity" --cov --cov-report=term-missing
|
|
75
|
+
- name: Fetch HL7 validator and R4 definitions
|
|
76
|
+
run: |
|
|
77
|
+
mkdir -p .tools/spec
|
|
78
|
+
curl -sSL -o .tools/validator_cli.jar \
|
|
79
|
+
"https://github.com/hapifhir/org.hl7.fhir.core/releases/latest/download/validator_cli.jar"
|
|
80
|
+
curl -sSL -o .tools/spec/definitions.json.zip https://hl7.org/fhir/R4/definitions.json.zip
|
|
81
|
+
(cd .tools/spec && unzip -o -q definitions.json.zip)
|
|
82
|
+
- name: US Core conformance (full matrix)
|
|
83
|
+
run: pytest tests/ -q -m conformance --conformance-scale=full
|
|
84
|
+
|
|
85
|
+
build:
|
|
86
|
+
needs: gates
|
|
87
|
+
runs-on: ubuntu-latest
|
|
88
|
+
steps:
|
|
89
|
+
- uses: actions/checkout@v4
|
|
90
|
+
- uses: actions/setup-python@v5
|
|
91
|
+
with:
|
|
92
|
+
python-version: "3.12"
|
|
93
|
+
- name: Build
|
|
94
|
+
run: |
|
|
95
|
+
python -m pip install --quiet build twine
|
|
96
|
+
python -m build
|
|
97
|
+
- name: Check metadata
|
|
98
|
+
run: twine check --strict dist/*
|
|
99
|
+
- name: Smoke-test the wheel outside the repo
|
|
100
|
+
run: |
|
|
101
|
+
python -m venv /tmp/probe
|
|
102
|
+
/tmp/probe/bin/pip install --quiet dist/*.whl
|
|
103
|
+
cd /tmp && /tmp/probe/bin/python -c "
|
|
104
|
+
import json, carebundle
|
|
105
|
+
b = json.loads(carebundle.to_json(
|
|
106
|
+
carebundle.generate_bundle(profile='type2_diabetes', seed=42, sex='F')))
|
|
107
|
+
assert b['resourceType'] == 'Bundle' and b['entry']
|
|
108
|
+
print('wheel OK:', len(b['entry']), 'entries')
|
|
109
|
+
"
|
|
110
|
+
- uses: actions/upload-artifact@v4
|
|
111
|
+
with:
|
|
112
|
+
name: dist
|
|
113
|
+
path: dist/
|
|
114
|
+
|
|
115
|
+
publish:
|
|
116
|
+
needs: build
|
|
117
|
+
# Tags only. A workflow_dispatch rehearsal on a branch stops after `build`, so the
|
|
118
|
+
# irreversible step cannot be reached by anything except pushing a version tag.
|
|
119
|
+
if: github.ref_type == 'tag'
|
|
120
|
+
runs-on: ubuntu-latest
|
|
121
|
+
# The environment gives you a manual approval gate: enable required reviewers on
|
|
122
|
+
# the `pypi` environment and the upload waits for a human. PyPI uploads cannot be
|
|
123
|
+
# undone — a deleted version number can never be reused.
|
|
124
|
+
environment:
|
|
125
|
+
name: pypi
|
|
126
|
+
url: https://pypi.org/p/carebundle
|
|
127
|
+
permissions:
|
|
128
|
+
id-token: write
|
|
129
|
+
steps:
|
|
130
|
+
- uses: actions/download-artifact@v4
|
|
131
|
+
with:
|
|
132
|
+
name: dist
|
|
133
|
+
path: dist/
|
|
134
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
.venv/
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.pyc
|
|
4
|
+
.pytest_cache/
|
|
5
|
+
.ruff_cache/
|
|
6
|
+
.coverage
|
|
7
|
+
build/
|
|
8
|
+
dist/
|
|
9
|
+
# Dev/CI tooling: the validator jar and FHIR spec archives are fetched, not vendored.
|
|
10
|
+
.tools/
|
|
11
|
+
# macOS Finder metadata — never wanted in a published sdist.
|
|
12
|
+
.DS_Store
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# Clinical quality measure benchmark
|
|
2
|
+
|
|
3
|
+
Regenerate with `pytest -m fidelity tests/test_benchmark.py`. Measures are computed
|
|
4
|
+
from the **emitted FHIR**, not from the internal draw — a measure engine reading the
|
|
5
|
+
sampler's own state would be marking its own homework.
|
|
6
|
+
|
|
7
|
+
## Why this benchmark and not another
|
|
8
|
+
|
|
9
|
+
Synthea's published validation ([Chen et al., *BMC Med Inform Decis Mak* 2019](https://pmc.ncbi.nlm.nih.gov/articles/PMC6416981/))
|
|
10
|
+
measured it against four CMS clinical quality measures. The result splits cleanly:
|
|
11
|
+
|
|
12
|
+
| Measure | Type | Synthea | Real (MA) | Real (US) |
|
|
13
|
+
|---|---|---:|---:|---:|
|
|
14
|
+
| Colorectal cancer screening | process | 68.7% | 77.3% | 69.8% |
|
|
15
|
+
| COPD 30-day mortality | outcome | 0.7% | 7.0% | 8.0% |
|
|
16
|
+
| Complications after hip/knee replacement | outcome | **0%** | 2.9% | 2.8% |
|
|
17
|
+
| Controlling high blood pressure | outcome | **0%** | 74.5% | 69.7% |
|
|
18
|
+
|
|
19
|
+
Synthea tracks reality on the process measure and collapses on every outcome measure.
|
|
20
|
+
The authors name the cause: synthetic generators "do not currently model for deviations
|
|
21
|
+
in care and the potential outcomes that may result from care deviations."
|
|
22
|
+
|
|
23
|
+
That is a statement about architecture, not a bug report. A state machine over care
|
|
24
|
+
pathways can decide *whether a patient was screened*. It has no representation of what
|
|
25
|
+
the blood pressure did after the thiazide started, so a control rate cannot emerge from
|
|
26
|
+
it. This project models clinical state directly, which is the machinery an outcome
|
|
27
|
+
measure needs.
|
|
28
|
+
|
|
29
|
+
## Results
|
|
30
|
+
|
|
31
|
+
| Measure | Synthea | **carebundle** | Real (US) | Real (MA) |
|
|
32
|
+
|---|---:|---:|---:|---:|
|
|
33
|
+
| Controlling high blood pressure (CBP) | 0% | **71.5%** | 69.7% | 74.5% |
|
|
34
|
+
| Colorectal cancer screening | 68.7% | *not modelled* | 69.8% | 77.3% |
|
|
35
|
+
| COPD 30-day mortality | 0.7% | *not modelled* | 8.0% | 7.0% |
|
|
36
|
+
| Complications after hip/knee replacement | 0% | *not modelled* | 2.8% | 2.9% |
|
|
37
|
+
|
|
38
|
+
**The three "not modelled" rows are published deliberately.** A benchmark that quietly
|
|
39
|
+
dropped the measures it loses would be worthless as evidence. *Not modelled* is a
|
|
40
|
+
different claim from 0%, and the measure engine keeps them distinguishable: a zero
|
|
41
|
+
denominator is reported as a zero denominator, never as a rate.
|
|
42
|
+
|
|
43
|
+
**They are also not going to be modelled**, and the reason is licensing rather than
|
|
44
|
+
effort. All three need `Procedure` resources, and the procedures in question have no
|
|
45
|
+
realistic public-domain coding:
|
|
46
|
+
|
|
47
|
+
- Screening colonoscopy is **CPT 45378** in the ambulatory setting where screening
|
|
48
|
+
happens; CPT is AMA-licensed. Hip and knee replacement likewise.
|
|
49
|
+
- SNOMED CT needs an affiliate licence and bars redistribution.
|
|
50
|
+
- ICD-10-PCS *is* public domain and US Core accepts it, so `0DJD8ZZ` would validate —
|
|
51
|
+
but PCS is inpatient facility coding, and no real US ambulatory system emits it for a
|
|
52
|
+
screening colonoscopy. Conformant and wrong. Checked and rejected rather than used,
|
|
53
|
+
because winning a benchmark row with a code no real system emits would forfeit the
|
|
54
|
+
realism claim the row is meant to evidence.
|
|
55
|
+
|
|
56
|
+
The measures this project can reach are those defined over labs, vitals and diagnoses —
|
|
57
|
+
which it codes with LOINC and ICD-10-CM. That is a real and narrow boundary, and it is
|
|
58
|
+
better stated than discovered.
|
|
59
|
+
|
|
60
|
+
### Controlling high blood pressure, in detail
|
|
61
|
+
|
|
62
|
+
Denominator is the HEDIS/NCQA definition — ages 18–85, coded hypertension diagnosis,
|
|
63
|
+
an outpatient encounter, and a recorded blood pressure. Numerator is a most-recent
|
|
64
|
+
reading below **140/90**, with both components required.
|
|
65
|
+
|
|
66
|
+
| Population | Denominator | CBP rate |
|
|
67
|
+
|---|---:|---:|
|
|
68
|
+
| `hypertension` profile | 1500 | 70.3% |
|
|
69
|
+
| `type2_diabetes` profile | 1045 | 70.8% |
|
|
70
|
+
| `ckd_stage3` profile | 1197 | 71.8% |
|
|
71
|
+
| **Mixed cohort (prevalence-drawn)** | **1822 / 4000** | **71.5%** |
|
|
72
|
+
| `healthy` profile | 0 | *not in denominator* |
|
|
73
|
+
|
|
74
|
+
## How the number was produced, and why it is not tuned
|
|
75
|
+
|
|
76
|
+
This is the part that matters. A benchmark you fit to is worthless, so every input
|
|
77
|
+
comes from a cited source and the rate is what those inputs imply.
|
|
78
|
+
|
|
79
|
+
**Inputs (fitted to sources):**
|
|
80
|
+
|
|
81
|
+
1. **Who is treated.** NHANES, August 2021–August 2023 ([NCHS Data Brief](https://www.ncbi.nlm.nih.gov/books/NBK612761/)):
|
|
82
|
+
59.2% of US adults with hypertension are aware of it, 51.2% take medication. So
|
|
83
|
+
51.2 / 59.2 = **86.5% of diagnosed hypertensives are on treatment**, and
|
|
84
|
+
diagnosed-and-in-care is exactly the population these profiles represent. The
|
|
85
|
+
per-drug prescribing probabilities are *solved* for that fraction rather than
|
|
86
|
+
hand-written, so the cited target and the emitted probabilities cannot drift apart.
|
|
87
|
+
2. **How much treatment lowers pressure.** Law MR, Wald NJ, Morris JK, *BMJ*
|
|
88
|
+
2003;326:1427 — a meta-analysis of 354 randomised placebo-controlled trials. One
|
|
89
|
+
standard-dose agent lowers blood pressure by **9.1 systolic / 5.5 diastolic**, and
|
|
90
|
+
the reduction is larger from a higher starting pressure (1.0 mmHg more systolic per
|
|
91
|
+
10 mmHg higher pre-treatment). A [2025 *Lancet* meta-analysis](https://pubmed.ncbi.nlm.nih.gov/40885583/)
|
|
92
|
+
of 484 trials puts monotherapy at 8.7 (95% CI 8.2–9.2), bracketing the same figure.
|
|
93
|
+
|
|
94
|
+
**Output (not fitted):** the control rate. Nothing in the model was adjusted to reach
|
|
95
|
+
71.5%. Change either cited input and the number moves — which is exactly what happened
|
|
96
|
+
when titration was added, and is documented below rather than smoothed over.
|
|
97
|
+
|
|
98
|
+
The mechanism is the same computed-identity discipline used for eGFR and Friedewald
|
|
99
|
+
LDL: the copula draws a *pre-treatment* pressure, the prescribing rules escalate on it
|
|
100
|
+
(you add an agent because the patient is uncontrolled), and the *recorded* pressure is
|
|
101
|
+
then computed from the regimen the patient actually received. Effects are applied per
|
|
102
|
+
distinct drug class sequentially, so Law's baseline-dependence produces diminishing
|
|
103
|
+
returns by construction rather than by a fudge factor. A bundle therefore cannot
|
|
104
|
+
prescribe three antihypertensives beside an untreated-looking 168/102.
|
|
105
|
+
|
|
106
|
+
## The titration hypothesis, tested and confirmed
|
|
107
|
+
|
|
108
|
+
The first version of this benchmark measured **64.1%** against a US comparator of
|
|
109
|
+
69.7% and published the 5.6-point shortfall rather than closing it, along with a
|
|
110
|
+
specific, falsifiable explanation:
|
|
111
|
+
|
|
112
|
+
> The most likely mechanism is dose titration: HEDIS scores the *most recent* reading
|
|
113
|
+
> of a year in which clinicians repeatedly re-measure and escalate until the patient
|
|
114
|
+
> reaches goal, whereas this models a single visit with a fixed regimen at standard
|
|
115
|
+
> doses.
|
|
116
|
+
|
|
117
|
+
**That prediction was then tested and it held.** Modelling titration moved the rate
|
|
118
|
+
from 64.1% to **71.5%**, between the US (69.7%) and Massachusetts (74.5%) comparators.
|
|
119
|
+
|
|
120
|
+
What makes this a real test rather than a fit:
|
|
121
|
+
|
|
122
|
+
- The titration effect size comes from a **different study** than the base effect —
|
|
123
|
+
the [2025 *Lancet* meta-analysis](https://pubmed.ncbi.nlm.nih.gov/40885583/) of 484
|
|
124
|
+
trials, at 1.5 mmHg systolic per dose doubling — so the correction was not
|
|
125
|
+
calibrated against the residual it was predicting.
|
|
126
|
+
- Escalation is **conditional on being above goal**, which is what titration is. A
|
|
127
|
+
patient already at target is never escalated, so the mechanism cannot inflate the
|
|
128
|
+
control rate from the wrong end by pushing controlled patients further down.
|
|
129
|
+
- The ceiling of two doublings is clinically motivated (beyond roughly four times
|
|
130
|
+
standard dose, another agent is preferred to another doubling), not fitted. Raising
|
|
131
|
+
it would push the rate higher; it was fixed before the rate was measured.
|
|
132
|
+
|
|
133
|
+
The direction of the original error also ruled out the obvious alternative. Law's
|
|
134
|
+
figures are **trial efficacy**, and real-world effectiveness is normally *lower*
|
|
135
|
+
because of adherence — so an adherence gap would have made the modelled rate too
|
|
136
|
+
**high**, not too low. Undershooting pointed at a missing treatment intensity, which
|
|
137
|
+
is what titration supplies.
|
|
138
|
+
|
|
139
|
+
## Non-adherence: a prediction that failed, and was not shipped
|
|
140
|
+
|
|
141
|
+
The previous section predicted that adding non-adherence "should push the rate down".
|
|
142
|
+
It does — by far too much to be right.
|
|
143
|
+
|
|
144
|
+
| Model | CBP rate |
|
|
145
|
+
|---|---:|
|
|
146
|
+
| Current (no explicit adherence term) | 74.7% |
|
|
147
|
+
| 45% of patients take nothing ([Abegaz 2017 meta-analysis](https://pubmed.ncbi.nlm.nih.gov/28121920/)) | 50.4% |
|
|
148
|
+
| Effect scaled by proportion-of-days-covered (62.2%) | 49.3% |
|
|
149
|
+
| **Real-world comparators** | **69.7% – 74.5%** |
|
|
150
|
+
|
|
151
|
+
Both formulations land roughly 20 points **below** reality. The prediction was right
|
|
152
|
+
about direction and badly wrong about magnitude, and the honest conclusion is that the
|
|
153
|
+
naive adherence term is a worse model, not a missing one. It is therefore **not
|
|
154
|
+
shipped**. Two reasons it fails:
|
|
155
|
+
|
|
156
|
+
- **The population-wide adherence figures do not transfer to this denominator.** HEDIS
|
|
157
|
+
CBP counts patients who are diagnosed *and* have an outpatient encounter — people
|
|
158
|
+
engaged enough with care to show up. Adherence meta-analyses are drawn from a much
|
|
159
|
+
broader population. Applying the broader figure to the narrower denominator imports
|
|
160
|
+
a selection effect that the denominator has already excluded.
|
|
161
|
+
- **"Non-adherent" is not "untreated".** A patient with 62% days covered takes most of
|
|
162
|
+
their doses and gets most of the benefit. Modelling them as receiving nothing is a
|
|
163
|
+
category error, and the PDC-scaled variant fails for the related reason that response
|
|
164
|
+
is not linear in coverage.
|
|
165
|
+
|
|
166
|
+
Recording this matters more than the result. The rate is close to reality *without*
|
|
167
|
+
this term, so a naive version would have made the benchmark worse while sounding more
|
|
168
|
+
sophisticated — and the reason it is excluded is that it contradicts the data by 20
|
|
169
|
+
points, which is model falsification rather than parameter fitting. If adherence is
|
|
170
|
+
modelled later it needs a dose-response formulation and a denominator-matched source.
|
|
171
|
+
|
|
172
|
+
## Why there is no second measure: CMS122, and what it taught
|
|
173
|
+
|
|
174
|
+
The obvious candidate for a second measure was **CMS122, Diabetes: HbA1c Poor Control
|
|
175
|
+
(>9%)**. It needs only a diagnosis and a lab result, both of which this project codes
|
|
176
|
+
with ICD-10-CM and LOINC, so unlike the colonoscopy measures it is not blocked by
|
|
177
|
+
licensing. It is blocked by something more interesting.
|
|
178
|
+
|
|
179
|
+
**The measure has no single national rate.** CMS's own
|
|
180
|
+
[2024 quality benchmarks](https://www.cms.gov/files/document/2024-quality-benchmarks.csv)
|
|
181
|
+
publish three, for the same measure in the same year:
|
|
182
|
+
|
|
183
|
+
| Collection type | Average performance rate |
|
|
184
|
+
|---|---:|
|
|
185
|
+
| Medicare Part B Claims | 11.70% |
|
|
186
|
+
| MIPS CQM | 27.30% |
|
|
187
|
+
| eCQM (CMS122v12) | 43.53% |
|
|
188
|
+
|
|
189
|
+
A four-fold spread is not measurement noise, and the numerator definition explains it:
|
|
190
|
+
|
|
191
|
+
> "Patients whose most recent HbA1c level (performed during the measurement period) is
|
|
192
|
+
> >9.0% **or is missing, or was not performed** during the measurement period"
|
|
193
|
+
|
|
194
|
+
CMS122 does not measure glycaemic control. It measures glycaemic control **plus
|
|
195
|
+
testing completeness**, summed into one number. The more a collection method depends on
|
|
196
|
+
complete EHR capture, the more untested patients inflate the rate — which is exactly the
|
|
197
|
+
ordering above.
|
|
198
|
+
|
|
199
|
+
**A generator with perfect data capture cannot reproduce that.** Every diabetic here has
|
|
200
|
+
an HbA1c, so this project would always score at the "everyone was tested" floor. Our
|
|
201
|
+
generated rate above 9.0% is 12.9%, which sits near the claims figure and near NHANES's
|
|
202
|
+
measured 12.9% — and that agreement would be *misleading* to publish as reproducing
|
|
203
|
+
CMS122, because the measure it would claim to reproduce is counting something else.
|
|
204
|
+
|
|
205
|
+
Two things follow, and both are worth more than the row would have been.
|
|
206
|
+
|
|
207
|
+
**The blocker generalises.** Any quality measure whose numerator includes "or is
|
|
208
|
+
missing" is partly a data-completeness measure, and synthetic data with complete capture
|
|
209
|
+
can only ever reproduce the clinical half. That is a structural limit on benchmarking
|
|
210
|
+
synthetic generators against real quality measures, and it applies to Synthea equally.
|
|
211
|
+
|
|
212
|
+
**It is also a use for the imperfection module.** `carebundle.imperfection` exists to
|
|
213
|
+
omit fields on purpose. A future version could model the missing-result component
|
|
214
|
+
explicitly and reproduce the full measure — the only route to CMS122 that would not be
|
|
215
|
+
quietly comparing two different quantities.
|
|
216
|
+
|
|
217
|
+
**A caveat this exposed in the CBP measure above.** HEDIS CBP has the same shape: a
|
|
218
|
+
member with no blood-pressure reading counts as not controlled. This implementation
|
|
219
|
+
instead requires a reading to enter the denominator. It makes no difference to the
|
|
220
|
+
number, because every generated patient has one — but it means the 71.5% figure is a
|
|
221
|
+
*clinical* control rate compared against real-world rates that blend control with
|
|
222
|
+
capture. The real-world comparators are therefore, if anything, slightly pessimistic
|
|
223
|
+
relative to what is being measured here.
|
|
224
|
+
|
|
225
|
+
## Remaining caveats
|
|
226
|
+
|
|
227
|
+
- The comparators are from 2019 and reflect the plans and years measured then. The
|
|
228
|
+
national HEDIS figure has sat in the low-to-mid 60s in other years.
|
|
229
|
+
- Titration is modelled here as an equilibrium: the recorded pressure is where a
|
|
230
|
+
titrated patient ends up, which is the right value for a most-recent-reading measure.
|
|
231
|
+
The trajectory behind it is now available separately via `generate_history`, which
|
|
232
|
+
emits the same patient across several reviews as therapy is escalated.
|
|
233
|
+
|
|
234
|
+
## What this does and does not establish
|
|
235
|
+
|
|
236
|
+
It establishes that a distributional model reproduces an outcome measure that a
|
|
237
|
+
pathway simulator scores **0%** on, from independently cited inputs, checked in CI.
|
|
238
|
+
|
|
239
|
+
It does not establish that this is a better synthetic data generator than Synthea.
|
|
240
|
+
Synthea covers 231 conditions, a lifetime per patient, and three of the four measures
|
|
241
|
+
in this very table. On breadth it is not close, and [ROADMAP.md](ROADMAP.md) does not
|
|
242
|
+
propose competing there.
|
|
243
|
+
|
|
244
|
+
## Sources
|
|
245
|
+
|
|
246
|
+
- [Chen J et al., "The validity of synthetic clinical data … (Synthea) using clinical quality measures", *BMC Med Inform Decis Mak* 2019](https://pmc.ncbi.nlm.nih.gov/articles/PMC6416981/)
|
|
247
|
+
- [NCHS Data Brief: Hypertension Prevalence, Awareness, Treatment, and Control, US, Aug 2021–Aug 2023](https://www.ncbi.nlm.nih.gov/books/NBK612761/)
|
|
248
|
+
- [Law MR, Wald NJ, Morris JK, "Value of low dose combination treatment with blood pressure lowering drugs", *BMJ* 2003;326:1427](https://www.ncbi.nlm.nih.gov/pmc/articles/PMC162261/)
|
|
249
|
+
- [Blood pressure-lowering efficacy of antihypertensive drugs and their combinations, *Lancet* 2025](https://pubmed.ncbi.nlm.nih.gov/40885583/)
|
|
250
|
+
- [NCQA HEDIS: Controlling High Blood Pressure (CBP)](https://www.ncqa.org/report-cards/health-plans/state-of-health-care-quality-report/controlling-high-blood-pressure-cbp/)
|