docs(spec): P39 build outcomes; plan checked off (refs #629-#633)
All checks were successful
CI / lint (push) Successful in 26s
CI / notebooks-smoke (push) Successful in 1m31s
Deploy / notebooks (push) Has been skipped
Deploy / zotero (push) Has been skipped
Deploy / docs (push) Has been skipped
Deploy / api (push) Has been skipped
Deploy / llm (push) Has been skipped
Deploy / mc (push) Has been skipped
Infra CI / notebooks (push) Successful in 1m1s
Infra CI / zotero (push) Successful in 14s
Infra CI / docs (push) Successful in 1m39s
Infra CI / api (push) Successful in 1m1s
Infra CI / llm (push) Successful in 47s
Infra CI / mc (push) Successful in 23s
Deploy / report (push) Successful in 13s
CI / test (push) Successful in 13m39s

This commit is contained in:
kert
2026-08-17 12:40:22 -04:00
parent afeec59875
commit c559cc10e2
2 changed files with 72 additions and 33 deletions

View File

@@ -35,9 +35,9 @@
**Interfaces:**
- Produces: bib attachments titled `CY{year} PFS NPRM Addendum E (proposed GPCIs)` on rule items `2KVJ2HKX` (2026) / `5ITGVDJV` (2027), discoverable via tag `sup:{year}_PFS_NPRM`. Task 2's `_discover_addendum_e` depends on these titles/filenames matching `(?i)addendum[_ ]?e`.
- [ ] **Step 1:** Add to `YearConfig`: `addendum_e_title: str`. Add to both CONFIGS entries: `addendum_e_title="CY2026 PFS NPRM Addendum E (proposed GPCIs)"` / `...CY2027...`. In `register()`, after the Addendum B attach block, repeat the same guarded attach for the Addendum E file (locate it in `cfg.dest_dir` via `re.compile(r"(?i)addendum[_ ]?e").search(p.name)` over `dest_dir.glob("*.xlsx")`, excluding `__MACOSX`); reuse `_existing_attachment` with the E title.
- [ ] **Step 2:** Run `uv run python dev/scripts/fetch_nprm_addenda.py --year 2026 --register && uv run python dev/scripts/fetch_nprm_addenda.py --year 2027 --register`. Expected: Addendum B prints `SKIP attach` (guard, from P38); Addendum E prints `Attached ...` first run. Re-run 2027 once more: E now prints `SKIP attach` (guard proven).
- [ ] **Step 3:** Commit: `git add dev/scripts/fetch_nprm_addenda.py && git commit -m "feat(pfs): register NPRM Addendum E (proposed GPCIs) in bib (refs #629)"`
- [x] **Step 1:** Add to `YearConfig`: `addendum_e_title: str`. Add to both CONFIGS entries: `addendum_e_title="CY2026 PFS NPRM Addendum E (proposed GPCIs)"` / `...CY2027...`. In `register()`, after the Addendum B attach block, repeat the same guarded attach for the Addendum E file (locate it in `cfg.dest_dir` via `re.compile(r"(?i)addendum[_ ]?e").search(p.name)` over `dest_dir.glob("*.xlsx")`, excluding `__MACOSX`); reuse `_existing_attachment` with the E title.
- [x] **Step 2:** Run `uv run python dev/scripts/fetch_nprm_addenda.py --year 2026 --register && uv run python dev/scripts/fetch_nprm_addenda.py --year 2027 --register`. Expected: Addendum B prints `SKIP attach` (guard, from P38); Addendum E prints `Attached ...` first run. Re-run 2027 once more: E now prints `SKIP attach` (guard proven).
- [x] **Step 3:** Commit: `git add dev/scripts/fetch_nprm_addenda.py && git commit -m "feat(pfs): register NPRM Addendum E (proposed GPCIs) in bib (refs #629)"`
### Task 2: `load_gpci_proposed` loader + tests ([#629](https://git.fhirworx.io/homelab/stack/issues/629), part 2)
@@ -49,16 +49,16 @@
- Consumes: `_source_for(year)` (existing), bib attachment discovery (generalize `_discover_addendum_b(tag)` → `_discover_attachment(tag, pattern)`; keep `_discover_addendum_b` delegating so existing tests/callers stand).
- Produces: `load_gpci_proposed(con, *, year: int) -> dict` returning `{"rows", "source_file", "cms_rule_id", "fr_citation", "pincite_key"}` — same contract as `load_rvu_proposed`, table `pfs.gpci_proposed` per Global Constraints. `ingest_pfs.py --nprm` now loads B **and** E per year, logging one `cms.ingest_log` row each (`table_name='pfs.gpci_proposed'`).
- [ ] **Step 1 (failing tests):** add to `tests/pfs/test_nprm.py`, mirroring the file's `_patch_discovery` idiom (patch `nprm._discover_addendum_e`):
- [x] **Step 1 (failing tests):** add to `tests/pfs/test_nprm.py`, mirroring the file's `_patch_discovery` idiom (patch `nprm._discover_addendum_e`):
- `test_load_gpci_proposed_cy2027_single_year` — xlsx-like CSV fixture with header row `Medicare Administrative Contractor,State,Locality Number,Locality Name,2027 PW GPCI (without 1.0 Floor),2027 PE GPCI,2027 MP GPCI`, two locality rows + one footnote row (`*Note...` in MAC column); assert 2 rows, `gpci_year==2027`, footnote dropped, locality `"00"` preserved as text.
- `test_load_gpci_proposed_cy2026_wide_melts_two_years` — fixture with 2025/2026/2027 triplets; assert rows = localities × 2 (gpci_year 2026 and 2027, no 2025), spot-check one PE value per year.
- `test_load_gpci_proposed_partitions_isolated` — load 2026 then 2027, reload 2027; assert CMS-1832-P partition untouched, no duplicate 1848-P rows.
- [ ] **Step 2:** `uv run pytest tests/pfs/test_nprm.py -q` — new tests FAIL (`load_gpci_proposed` missing), old ones PASS.
- [ ] **Step 3 (implement):** in `src/pfs/nprm.py`: `_ADDENDUM_E_RE = re.compile(r"(?i)addendum[_ ]?e")`; `_discover_attachment(tag, regex, kind)` generalization; `_GPCI_TARGET_COLUMNS` list per Global Constraints; `_gpci_schema_matches(con)` (copy `_schema_matches_target` pattern against `gpci_proposed`); `load_gpci_proposed(con, *, year)`:
- [x] **Step 2:** `uv run pytest tests/pfs/test_nprm.py -q` — new tests FAIL (`load_gpci_proposed` missing), old ones PASS.
- [x] **Step 3 (implement):** in `src/pfs/nprm.py`: `_ADDENDUM_E_RE = re.compile(r"(?i)addendum[_ ]?e")`; `_discover_attachment(tag, regex, kind)` generalization; `_GPCI_TARGET_COLUMNS` list per Global Constraints; `_gpci_schema_matches(con)` (copy `_schema_matches_target` pattern against `gpci_proposed`); `load_gpci_proposed(con, *, year)`:
read xlsx via `pl.read_excel(..., has_header=False)`, find the header row (first row whose col-0 startswith "Medicare Administrative"), name columns from it; melt: for each year-triplet `{yy} PW/PE/MP` with `yy >= year`, emit rows with `gpci_year=yy`; strip+cast GPCIs to float (strict=False); filter rows where MAC fails `^\d{5}$`; keep `state`, `locality` (zero-padded text), `locality_name` as-is; add `cms_rule_id`; create-schema/drop-stale/create-if-missing/delete-partition/insert exactly as `load_rvu_proposed` does.
- [ ] **Step 4:** `uv run pytest tests/pfs/test_nprm.py -q` — all PASS. Then a real-file smoke on host (monolith untouched): `uv run python -c "import duckdb; from pfs import nprm; con=duckdb.connect(); print(nprm.load_gpci_proposed(con, year=2027)); print(nprm.load_gpci_proposed(con, year=2026)); print(con.execute('SELECT cms_rule_id, gpci_year, count(*) FROM pfs.gpci_proposed GROUP BY ALL ORDER BY ALL').fetchall())"` — expect ≈113 rows per (rule, year) group, three groups.
- [ ] **Step 5:** wire `ingest_pfs.py`: inside the existing `--nprm` year loop, after the Addendum B call, call `load_gpci_proposed` + `ingest_log.log_ingest(..., table_name="pfs.gpci_proposed", ...)` with its returned fields (same run_id).
- [ ] **Step 6:** Commit: `git add src/pfs/nprm.py tests/pfs/test_nprm.py dev/scripts/ingest_pfs.py && git commit -m "feat(pfs): NPRM Addendum E loader -> pfs.gpci_proposed + ingest wiring (closes #629)"`
- [x] **Step 4:** `uv run pytest tests/pfs/test_nprm.py -q` — all PASS. Then a real-file smoke on host (monolith untouched): `uv run python -c "import duckdb; from pfs import nprm; con=duckdb.connect(); print(nprm.load_gpci_proposed(con, year=2027)); print(nprm.load_gpci_proposed(con, year=2026)); print(con.execute('SELECT cms_rule_id, gpci_year, count(*) FROM pfs.gpci_proposed GROUP BY ALL ORDER BY ALL').fetchall())"` — expect ≈113 rows per (rule, year) group, three groups.
- [x] **Step 5:** wire `ingest_pfs.py`: inside the existing `--nprm` year loop, after the Addendum B call, call `load_gpci_proposed` + `ingest_log.log_ingest(..., table_name="pfs.gpci_proposed", ...)` with its returned fields (same run_id).
- [x] **Step 6:** Commit: `git add src/pfs/nprm.py tests/pfs/test_nprm.py dev/scripts/ingest_pfs.py && git commit -m "feat(pfs): NPRM Addendum E loader -> pfs.gpci_proposed + ingest wiring (closes #629)"`
### Task 3: Monolith ingest + lake publish ([#630](https://git.fhirworx.io/homelab/stack/issues/630))
@@ -68,11 +68,11 @@
**Interfaces:**
- Produces (lake): `pfs.gpci_proposed` (~339 rows) and `cms.primary_care_service_code` (109 rows, columns `hcpcs_code`, `description`) readable via `connect.ducklake()`; refreshed `cms.ingest_log`. Tasks 4–5 read these names verbatim.
- [ ] **Step 1:** Check the DuckDB single-writer hazard (no marimo session holding aco.duckdb), then run `uv run python dev/scripts/ingest_pfs.py --nprm` on the host. Expected: per-year lines for `pfs.rvu_proposed` AND `pfs.gpci_proposed`, and replica republish.
- [ ] **Step 2:** Extend the publisher: add `"gpci_proposed"` to the `pfs` tuple; add `RENAMED: tuple[tuple[str, str], ...] = (("cms_provider_attribution._primary_care_hcpcs_codes", "cms.primary_care_service_code"),)` and a loop after the SCHEMAS loop that reads `src`, saves to the destination name, and read-back-verifies exactly like the main loop.
- [ ] **Step 3:** Publish from the notebooks container (script's documented invocation): `docker cp dev/scripts/publish_reference_to_lake.py notebooks:/tmp/ && docker exec -e POSTGRES_PASSWORD="$(grep '^POSTGRES_PASSWORD=' .env | cut -d= -f2)" notebooks env PYTHONPATH=/home/kert/src uv run --project /home/kert/workspace python /tmp/publish_reference_to_lake.py --schemas pfs cms`. Expected: every table `[OK]`, incl. the two new names.
- [ ] **Step 4:** Verify from the notebook's exact read path (notebooks container): `SELECT count(*) FROM cms.primary_care_service_code` → 109; `SELECT cms_rule_id, gpci_year, count(*) FROM pfs.gpci_proposed GROUP BY ALL` → three groups ≈113; `SELECT count(*) FROM cms.ingest_log WHERE table_name='pfs.gpci_proposed'` ≥ 2.
- [ ] **Step 5:** Commit: `git add dev/scripts/publish_reference_to_lake.py && git commit -m "feat(lake): publish gpci_proposed + primary_care_service_code to DuckLake (closes #630)"`. Comment the verification counts on [#630](https://git.fhirworx.io/homelab/stack/issues/630).
- [x] **Step 1:** Check the DuckDB single-writer hazard (no marimo session holding aco.duckdb), then run `uv run python dev/scripts/ingest_pfs.py --nprm` on the host. Expected: per-year lines for `pfs.rvu_proposed` AND `pfs.gpci_proposed`, and replica republish.
- [x] **Step 2:** Extend the publisher: add `"gpci_proposed"` to the `pfs` tuple; add `RENAMED: tuple[tuple[str, str], ...] = (("cms_provider_attribution._primary_care_hcpcs_codes", "cms.primary_care_service_code"),)` and a loop after the SCHEMAS loop that reads `src`, saves to the destination name, and read-back-verifies exactly like the main loop.
- [x] **Step 3:** Publish from the notebooks container (script's documented invocation): `docker cp dev/scripts/publish_reference_to_lake.py notebooks:/tmp/ && docker exec -e POSTGRES_PASSWORD="$(grep '^POSTGRES_PASSWORD=' .env | cut -d= -f2)" notebooks env PYTHONPATH=/home/kert/src uv run --project /home/kert/workspace python /tmp/publish_reference_to_lake.py --schemas pfs cms`. Expected: every table `[OK]`, incl. the two new names.
- [x] **Step 4:** Verify from the notebook's exact read path (notebooks container): `SELECT count(*) FROM cms.primary_care_service_code` → 109; `SELECT cms_rule_id, gpci_year, count(*) FROM pfs.gpci_proposed GROUP BY ALL` → three groups ≈113; `SELECT count(*) FROM cms.ingest_log WHERE table_name='pfs.gpci_proposed'` ≥ 2.
- [x] **Step 5:** Commit: `git add dev/scripts/publish_reference_to_lake.py && git commit -m "feat(lake): publish gpci_proposed + primary_care_service_code to DuckLake (closes #630)"`. Comment the verification counts on [#630](https://git.fhirworx.io/homelab/stack/issues/630).
### Task 4: Notebook — GPCI & Locality section ([#631](https://git.fhirworx.io/homelab/stack/issues/631))
@@ -83,8 +83,8 @@
- Consumes: `pfs.gpci` (final, 2016–2026), `pfs.gpci_proposed`, `pfs.calcs.payment.payment(rvu, gpci, cf)` (narwhals frames, joins on `mac`+`locality`), `proposed_for(2027)` CFs.
- Produces: section anchor cells Tasks 5 reuses (`gpci_2027p` polars frame: one row per locality with final-2026 and proposed-2027 work/pe/mp).
- [ ] **Step 1:** Load the `dataviz` skill (chart cells ahead).
- [ ] **Step 2:** Build the section (marimo cells, `hide_code=True`, matching the notebook's voice and sourcing conventions):
- [x] **Step 1:** Load the `dataviz` skill (chart cells ahead).
- [x] **Step 2:** Build the section (marimo cells, `hide_code=True`, matching the notebook's voice and sourcing conventions):
1. Intro cell: what GPCIs are; CY2027 proposes the year-2 phase-in values vs CMS-1832-P's projection; **work-floor callout** (proposed PW published without the 1.0 floor; the CY2026-final column carries the floor).
2. `gpci_2027p` frame: join `pfs.gpci_proposed` (CMS-1848-P, gpci_year 2027) to `pfs.gpci` (year 2026) on `mac, locality`; deltas for work/pe/mp.
3. Movers table + diverging bar (top ±15 localities by GAF-proxy mean delta), up/down/flat counts in prose.
@@ -92,8 +92,8 @@
5. Long-term: line chart of work/pe/mp GPCI 2016–2026 + 2027p (dashed/annotated "final rule pending") for the extreme localities (max/min mean GPCI 2026) + a national median trace.
6. Payment effect: `pfs.calcs.payment` on the 99213/27447/70553 trio at best/worst locality vs national (CF = `proposed_for(2027)` non-QP), small table.
7. Intro cell of the notebook: replace the "does not join pfs.gpci" disclaimer with a pointer to this section; keep the "national unadjusted unless stated" scope note. Provenance cell: extend the `table_name IN (...)` filter with `'pfs.gpci','pfs.gpci_proposed'` and drop the parenthetical that excludes gpci.
- [ ] **Step 3:** Gate: copy the single-notebook config trick and run `nb_integration.py --set ci-smoke` in the notebooks container → `pass=1`.
- [ ] **Step 4:** Commit: `git add notebooks/cy2027_pfs_proposed_rule.py && git commit -m "feat(notebooks): CY2027 NPRM GPCI & locality section (closes #631)"`
- [x] **Step 3:** Gate: copy the single-notebook config trick and run `nb_integration.py --set ci-smoke` in the notebooks container → `pass=1`.
- [x] **Step 4:** Commit: `git add notebooks/cy2027_pfs_proposed_rule.py && git commit -m "feat(notebooks): CY2027 NPRM GPCI & locality section (closes #631)"`
### Task 5: Notebook — Primary-care per-service section ([#632](https://git.fhirworx.io/homelab/stack/issues/632))
@@ -104,30 +104,30 @@
- Consumes: `cms.primary_care_service_code`, `pfs.rvu` 2015–2026, `pfs.rvu_proposed` (CMS-1848-P), `RULES[y].conversion_factor` 2015–2026, `proposed_for(2027)` CFs, `gpci_2027p` from Task 4.
- Produces: final notebook; provenance covers every table used.
- [ ] **Step 1:** Definition cell: the 109-code § 425.400(c) list read live; disclosures (current-law list applied retrospectively; unweighted). In-notebook sanity assert: `assert pcs.height == 109`.
- [ ] **Step 2:** Near-term frame `pqm_near`: LEFT JOIN codes → `pfs.rvu_proposed` (1848-P, mod-less rows) and → `pfs.rvu` (2026, mod-less); per code: work/total RVU both sides, national payment both sides (`total × CF`, CY2026 final CF vs CY2027 proposed non-QP CF), `pct_change`; classify `both / new_in_2027 / missing_from_2027`. Full sortable `mo.ui.table` (all 109 rows) + bar chart ranked by % payment change (both-side codes) + explicit callout listing one-sided codes (APCM family etc.).
- [ ] **Step 3:** Long-term frame `pqm_long`: codes × `pfs.rvu` 2015–2026 total RVU (mod-less) + 2027 proposed point; dollar column `total_rvu × RULES[year].conversion_factor` (2027p × proposed CF, labeled). Charts: code × year heatmap of YoY % RVU change; small-multiple dollar lines for the 12 largest cumulative movers; prose calling out the E/M-2021 step and the G-code cohorts that have no early-year history.
- [ ] **Step 4:** GPCI cross-cut cell: 99213 (or G0556 if only proposed) priced at the Task-4 extreme localities under the CY2027 proposed CF — table tying the two sections together.
- [ ] **Step 5:** Renumber later sections (APM → 5, Provenance → 6) and their headers; provenance SQL now covers `pfs.rvu, pfs.rvu_proposed, pfs.gpci, pfs.gpci_proposed`; add a prose line sourcing `cms.primary_care_service_code` (CCLF/attribution reference, not FR-ingested).
- [ ] **Step 6:** Gate: `nb_integration.py` single-notebook run → `pass=1`.
- [ ] **Step 7:** Commit: `git add notebooks/cy2027_pfs_proposed_rule.py && git commit -m "feat(notebooks): CY2027 NPRM primary-care (ACO attribution) per-service analysis (closes #632)"`
- [x] **Step 1:** Definition cell: the 109-code § 425.400(c) list read live; disclosures (current-law list applied retrospectively; unweighted). In-notebook sanity assert: `assert pcs.height == 109`.
- [x] **Step 2:** Near-term frame `pqm_near`: LEFT JOIN codes → `pfs.rvu_proposed` (1848-P, mod-less rows) and → `pfs.rvu` (2026, mod-less); per code: work/total RVU both sides, national payment both sides (`total × CF`, CY2026 final CF vs CY2027 proposed non-QP CF), `pct_change`; classify `both / new_in_2027 / missing_from_2027`. Full sortable `mo.ui.table` (all 109 rows) + bar chart ranked by % payment change (both-side codes) + explicit callout listing one-sided codes (APCM family etc.).
- [x] **Step 3:** Long-term frame `pqm_long`: codes × `pfs.rvu` 2015–2026 total RVU (mod-less) + 2027 proposed point; dollar column `total_rvu × RULES[year].conversion_factor` (2027p × proposed CF, labeled). Charts: code × year heatmap of YoY % RVU change; small-multiple dollar lines for the 12 largest cumulative movers; prose calling out the E/M-2021 step and the G-code cohorts that have no early-year history.
- [x] **Step 4:** GPCI cross-cut cell: 99213 (or G0556 if only proposed) priced at the Task-4 extreme localities under the CY2027 proposed CF — table tying the two sections together.
- [x] **Step 5:** Renumber later sections (APM → 5, Provenance → 6) and their headers; provenance SQL now covers `pfs.rvu, pfs.rvu_proposed, pfs.gpci, pfs.gpci_proposed`; add a prose line sourcing `cms.primary_care_service_code` (CCLF/attribution reference, not FR-ingested).
- [x] **Step 6:** Gate: `nb_integration.py` single-notebook run → `pass=1`.
- [x] **Step 7:** Commit: `git add notebooks/cy2027_pfs_proposed_rule.py && git commit -m "feat(notebooks): CY2027 NPRM primary-care (ACO attribution) per-service analysis (closes #632)"`
### Task 6: duckdb pin alignment ([#633](https://git.fhirworx.io/homelab/stack/issues/633))
**Files:**
- Modify: `pyproject.toml`, `uv.lock`
- [ ] **Step 1:** `sed`-bump every `duckdb>=1.0.0` floor in `pyproject.toml` to `duckdb>=1.5.4`; `uv lock` (confirm the lock resolves 1.5.x); `uv sync`.
- [ ] **Step 2:** Verify: host `uv run python -c "from conf import connect; print(connect.duckdb('aco').execute('select 1').fetchone())"` (monolith replica opens under 1.5.x); host ducklake attach still fails only on DNS (compose-internal), not version — acceptable proof is running the attach inside the **api** container after a rebuild is NOT in scope; the host-venv check suffices.
- [ ] **Step 3:** Full test suite (the pre-commit hook runs it on commit — that IS the run; budget ~6 min). If red on duckdb-related failures: `git checkout pyproject.toml uv.lock`, comment findings on [#633](https://git.fhirworx.io/homelab/stack/issues/633), leave the issue open, and continue to Task 7.
- [ ] **Step 4:** Commit (hook-gated): `git add pyproject.toml uv.lock && git commit -m "fix(deps): duckdb >=1.5.4 — attach migrated DuckLake catalog from host (closes #633)"`
- [x] **Step 1:** `sed`-bump every `duckdb>=1.0.0` floor in `pyproject.toml` to `duckdb>=1.5.4`; `uv lock` (confirm the lock resolves 1.5.x); `uv sync`.
- [x] **Step 2:** Verify: host `uv run python -c "from conf import connect; print(connect.duckdb('aco').execute('select 1').fetchone())"` (monolith replica opens under 1.5.x); host ducklake attach still fails only on DNS (compose-internal), not version — acceptable proof is running the attach inside the **api** container after a rebuild is NOT in scope; the host-venv check suffices.
- [x] **Step 3:** Full test suite (the pre-commit hook runs it on commit — that IS the run; budget ~6 min). If red on duckdb-related failures: `git checkout pyproject.toml uv.lock`, comment findings on [#633](https://git.fhirworx.io/homelab/stack/issues/633), leave the issue open, and continue to Task 7.
- [x] **Step 4:** Commit (hook-gated): `git add pyproject.toml uv.lock && git commit -m "fix(deps): duckdb >=1.5.4 — attach migrated DuckLake catalog from host (closes #633)"`
### Task 7: Close the loop
- [ ] **Step 1:** `git push`; confirm CI green on HEAD (stale auto-filed CI issues are known noise — check the actual run).
- [ ] **Step 2:** Verify all of #629–#633 closed (auto-close on push) or close with comments; comment final row-counts + `pass=1` evidence on [#631](https://git.fhirworx.io/homelab/stack/issues/631)/[#632](https://git.fhirworx.io/homelab/stack/issues/632); close [milestone P39](https://git.fhirworx.io/homelab/stack/milestone/40).
- [ ] **Step 3:** Append "P39 build outcomes" to the spec doc; commit `docs(spec): P39 build outcomes (refs #629-#633)`.
- [ ] **Step 4:** Memory: note in `zotero_rule_sync_incident.md`-adjacent project memory only if something non-obvious emerged (candidate: the Addendum E wide-format melt and the publish rename map).
- [x] **Step 1:** `git push`; confirm CI green on HEAD (stale auto-filed CI issues are known noise — check the actual run).
- [x] **Step 2:** Verify all of #629–#633 closed (auto-close on push) or close with comments; comment final row-counts + `pass=1` evidence on [#631](https://git.fhirworx.io/homelab/stack/issues/631)/[#632](https://git.fhirworx.io/homelab/stack/issues/632); close [milestone P39](https://git.fhirworx.io/homelab/stack/milestone/40).
- [x] **Step 3:** Append "P39 build outcomes" to the spec doc; commit `docs(spec): P39 build outcomes (refs #629-#633)`.
- [x] **Step 4:** Memory: note in `zotero_rule_sync_incident.md`-adjacent project memory only if something non-obvious emerged (candidate: the Addendum E wide-format melt and the publish rename map).
## Self-Review

View File

@@ -162,3 +162,42 @@ monolith; publish + notebook run in the notebooks container).
- Addendum D / GAF ingestion.
- Versioning the primary-care designation list by rule year.
- Retiring the monolith (#514) — P39 adds one publisher entry, no more.
## P39 build outcomes (2026-08-17)
Issues #629–#633, milestone P39 (5 commits, 39dfaa2..afeec59, direct on
main — mail-poller live-mounts this checkout and Tasks ran against the
live monolith/lake). All closed same-day; notebook gate `pass=1` in the
prod notebooks container after each notebook change.
- **#629 (39dfaa2 + the Addendum-E register commit)**:
`load_gpci_proposed` → `pfs.gpci_proposed` (long format with
`gpci_year`; CMS-1832-P carries 2026+2027, CMS-1848-P carries 2027 —
109 localities each; 2025 baseline restatement skipped; PW loaded
floor-free as published). Addendum E attached in bib for both years
(title-guarded, canonical storage paths). 3 new loader tests;
`ingest_pfs.py --nprm` logs `pfs.gpci_proposed` ingest rows.
- **#630 (fc26d5f)**: publisher gained `gpci_proposed` + a rename map;
`cms_provider_attribution._primary_care_hcpcs_codes` →
`cms.primary_care_service_code` (109 rows). Verified from
`connect.ducklake()` in the notebooks container.
- **#631/#632 (8c4e13c, one combined commit — the sections were a
single splice)**: new Sections 3 (GPCI & locality) and 4 (primary
care / attribution set); APM → 5, Provenance → 6; intro's "GPCI=1.0,
does not join pfs.gpci" disclaimer replaced; provenance covers the
gpci tables and sources the attribution list narratively.
- **#633 (afeec59)**: duckdb floor 1.0 → ≥1.5.4 (resolved 1.5.5); full
suite green under the new pin; monolith + replica open cleanly;
host-side `connect.ducklake()` no longer version-blocked (DNS to the
compose-internal catalog remains, by design).
- **Findings worth keeping**: the CY2027 NPRM re-proposes the CY2026
NPRM's 2027 GPCI phase-in for 103 of 109 localities (6 revised);
every designated primary-care code that prices anywhere prices in
*both* CY2026-final and CY2027-proposed tables — the 12 unmatched
are all deleted legacy codes still on the § 425.400(c) list (the
notebook's one-sided narrative was corrected against live data
before commit).
- **Deviation**: #631/#632 shipped as one commit; Task-3 ingest ran via
a targeted driver (same duckdb_batch/ingest_log/replica plumbing)
instead of the full `ingest_pfs.py --nprm` reload, to avoid an
unnecessary multi-minute `load_all` pass.