The per-key dump ("CCM: 99490(base) ...") used to always print — useless
once derived families number in the thousands. --write now prints only
the summary line by default; the full dump is --verbose. refresh_from
and printing moved outside the duckdb_batch block (they don't need the
write lock open) — refresh_from now reads the freshly published
replica, the same pattern stack llm restamp uses.
Also: docs/superpowers/specs/2026-09-09-code-family-longitudinal-design.md
Decision 2 gets one sentence noting lineage's third source (cpt, the
AMA's own change years — effective years) and that a cpt_changed year
anchors an RVU event the way an FR event does.
105 lines
14 KiB
Markdown
105 lines
14 KiB
Markdown
# Code families as first-class objects — element model, lineage anchors, longitudinal chat, legal epidemiology
|
||
|
||
Tracker: milestone P49 (this spec) and milestone P50 (outcome series). Builds on P48 (`pfs/families.py`, `codes` chunk metadata, valuation event), P47 (dockets/seals), P40/P41 (FR + eCFR jump links), P35 (closed-vocabulary RAG tagging).
|
||
|
||
## Why
|
||
|
||
When the physician fee schedule decides coding and payment for a service, the unit of decision is a **code** or a **code family**: a bundle of logical elements that say what the service is and is not. CMS says this itself in the Medicare telehealth review process, where Step 3 is literally "Review the elements of the service as described by the HCPCS code" (CY2026 NPRM, 90 FR 32389, item `2KVJ2HKX` ¶398). The corpus already holds every layer of the record for those elements — 84 PFS rules with paragraph anchors back to CY2002, RVU files 2015–2026, ten comment dockets 2017–2026, 177 IOM chapters, 699 eCFR sections — but nothing ties the layers together *per code*. The chat can price a code (P48) but cannot answer "where did this code come from, what replaced what, how did the elements change, who objected, and what does the manual say".
|
||
|
||
The goal of P49 is that the chat, and the notebooks, can build a **longitudinal view of any code or code set payable under the PFS**: creation, predecessors and successors, element changes, valuation history, sub-regulatory guidance, and public reaction, each step anchored to a citable paragraph. P50 then joins that timeline to outcome series (utilization, BLS wages, industry markets) so the fee schedule can be studied as law with effects — legal epidemiology.
|
||
|
||
## What the corpus already shows (measured 2026-09-09)
|
||
|
||
**The "elements" construct is explicit in the record.**
|
||
- CPT 99490 was adopted for CY2015 in place of a CMS G-code; the FR prints its descriptor as a stem plus enumerated required elements — two or more chronic conditions expected to last ≥12 months; conditions placing the patient at significant risk; comprehensive care plan established/implemented/revised/monitored (CY2015 final rule `DE2VH9PD` ¶1244–1247, 79 FR 67716). The commenter-vs-CMS argument was about a single element (per calendar month vs per 30 days, ¶1248–1251).
|
||
- HCPCS G0556–G0558 (APCM, CY2025) print as a stem plus ~20 elements and `++` sub-elements: consent, initiation visit, 24/7 access, continuity, alternative care delivery, comprehensive care management, electronic care plan, transitions, community coordination, asynchronous communication, population data analysis, risk stratification, performance measurement (CY2025 final rule `JJ6AM5HJ` ¶1163–1185, 89 FR 97864–97865). G0557 is defined *by reference* to G0556's elements plus a population element (¶1186).
|
||
- The telehealth process (first printed in the CY2024 rules, `44F3F6VU`/`JJV3R692`, and repeated every year since) is a three-step element test: Step 1 separately payable under the PFS; Step 2 subject to §1834(m), i.e. is the service "in whole or in part, inherently a face-to-face service"; Step 3 each element capable of being furnished via an interactive telecommunications system as defined in § 410.78(a)(3) (`2KVJ2HKX` ¶394, ¶396, ¶398).
|
||
|
||
**Lineage is recoverable from the record but only by hand today.**
|
||
- G-code → CPT 99490 (CY2015 final `DE2VH9PD` ¶1249–1251).
|
||
- G2058 (RVU file 2020 only) → CPT 99439 (2021–): "descriptor identical to G2058", temporary crosswalk value (CY2021 final `YBM4IZUS` ¶1578, 85 FR 84639; ¶686, ¶2369).
|
||
- G2064/G2065 (2020–2021) → CPT 99424/99426 (2022–), including the knock-on into the RHC/FQHC G0511 rate (CY2022 final `JE7KYBW3` ¶1100/¶1111, 86 FR 65118; CY2023 final `MZ24MX5S` ¶1305).
|
||
- 99441–99443 deleted by the CPT panel, last in the RVU file 2024, replaced by 98008–98015 / 98016 (CY2025 rules `XFGGRBDH`/`JJ6AM5HJ` ¶489/¶765).
|
||
- Status changes visible in `pfs.rvu`: 99487/99489 B→A, 99497/99498 I→A, 99091 B→A, G2211 B→A.
|
||
- 99490 is discussed in 27 distinct rule documents from CY2015 final to CY2027 proposed; 284 rule chunks already carry `codes ∋ 99490`.
|
||
|
||
**Public reaction is in the corpus, in two forms.**
|
||
- Dockets CMS-2017-0092 … CMS-2026-2377 (10 dockets, 918,484 comment chunks). Abstract-level hits per docket: CCM 5/19/6/11/24/2/5/10/174/27; APCM 19 (CMS-2024-0256, the year it was proposed), 25, 5; G2211 3,353 in CMS-2023-0121 (the year it was activated), then 146 and 190. Telehealth: ~10–13k per docket 2020–2023.
|
||
- Before 2017 the only public-comment record is the FR itself: the `Comment:` / `Response:` paragraph pairs (e.g. `DE2VH9PD` ¶1244/¶1249, ¶1255) — a proxy series back to CY2002.
|
||
|
||
**Sub-regulatory guidance is present but unlinked.** 177 IOM chapters (MCPM ch. 12 `SPJB4FX2` and MBPM ch. 15 `YQFGB6JS` = 988 corpus chunks), 699 eCFR sections (§ 410.78 telehealth `SAJSGG28`, § 410.26 incident-to `Q78GP4Z9`, § 414.22 `T7PXFV4S`), MLN909188 CCM/PCM/APCM booklet (`PV8APQ4A`). None carries a `code:` tag; `comments` and `corpus` chunks carry no `codes` metadata (0 of 1.15 M), only `rules` do (29,595 of 67,340).
|
||
|
||
**Scale of "any code".** `pfs.rvu` has 18,641 distinct HCPCS over 2015–2026; 8,572 are status A/R/T in CY2026 (7,087 in 2015). The hand registry in `pfs/families.py` covers 5 families / 17 codes.
|
||
|
||
## Decisions
|
||
|
||
1. **Elements are a closed, typed vocabulary**, not free text. Element *types* are fixed in code; element *values* are a curated list that grows by review (P35 pattern: local model proposes from a closed list, humans accept). Types: `actor` (clinical staff directed by / personally by physician or QHP; RHC/FQHC), `time` (threshold minutes, "each additional"), `period` (calendar month, 30 days, per visit, 14/7 days post-discharge), `population` (≥2 chronic conditions ≥12 months; QMB; high risk), `activity` (consent; care plan; 24/7 access; transitions; medication reconciliation; …), `modality` (face-to-face; interactive telecommunications; audio-only; asynchronous), `relation` (add-on to; do-not-report-with; replaces; defined-by-reference-to; crosswalk-valued-to), `setting` (facility/non-facility; place of service), `billing` (one practitioner per month; initiating visit).
|
||
2. **Lineage is a table of dated events with anchors**, derived from two independent sources that must agree or be flagged: (a) `pfs.rvu`/`rvu_proposed` (first year, last year, status transitions, descriptor changes, RVU changes beyond a threshold) and (b) FR paragraphs whose text carries the code and an event verb (create/adopt/replace/delete/crosswalk/bundle/telehealth-list). Every event row stores `item_key, p_id, page` so the chat cites a paragraph, never a table. Lineage now has a third source, `cpt` — the AMA CPT Changes book's own "Effective January 1, YYYY" statement per code, ingested as `pfs.cpt_reference`/`pfs.cpt_code` change years, which are effective years rather than paragraph-anchored FR events; a `cpt_changed` year anchors an RVU event (an appearance/status change with no FR paragraph nearby) the same way an FR event does.
|
||
3. **Families are derived, not hand-listed.** A family = codes sharing a stem and differing in `time`/`actor`/`population` elements, plus CMS's own "code family" language in the FR ("The Chronic Care Management code family will be resurveyed", `YBM4IZUS` ¶1578) and `relation` elements (add-on/replaces). The five hand families become the regression fixture.
|
||
4. **Anchors live in two places, one per consumer.** Items get `code:<hcpcs>` and `family:<key>` tags in bib/Zotero (browsable, syncable); chunks get `codes`, `families`, and `elements` metadata in pgvector (retrievable). `comments` and `corpus` are re-indexed once with `--force` to stamp them, as P48 did for `rules`.
|
||
5. **Longitudinal retrieval is era-balanced, not recency-blended.** A "history of X" question retrieves per era (rule year) with a cap per era, so CY2015 and CY2027 both appear; the P34 recency blend stays for ordinary questions.
|
||
6. **Chat presents a timeline the way P48 presents a valuation**: a `lineage` SSE event before the first token, rendered as a table (year, event, codes, anchor link), and a prompt block whose rows carry bracketed labels the model must cite.
|
||
7. **Self-hosted inference only** for element extraction and stance tagging (llm module rule; qwen2.5 on the GPU pool).
|
||
8. **Outcome series (P50) are separate**: they never write into bib; they join on the lineage table's effective dates.
|
||
|
||
## Architecture
|
||
|
||
```
|
||
FR rules (fr_anchors) ─┐
|
||
RVU files (pfs.rvu) ──┼─► element extractor ─► pfs.code_element (code, year, type, value, anchor)
|
||
HCPCS/CPT descriptors ─┘ │
|
||
▼
|
||
family deriver ─► pfs.code_family (key, code, role, since, until, anchor)
|
||
│
|
||
rvu first/last/status ─┐ ▼
|
||
FR event paragraphs ───┴─► lineage builder ─► pfs.code_event (code, year, kind, from/to, item_key, p_id, page)
|
||
│
|
||
┌────────────────────┼─────────────────────┐
|
||
▼ ▼ ▼
|
||
bib tags code:/family: pgvector metadata notebooks
|
||
(rules, comments, IOM, codes/families/elements (timeline, reaction series)
|
||
eCFR, MLN) on all 3 collections
|
||
└────────────────────┬─────────────────────┘
|
||
▼
|
||
chat: detect codes → lineage(codes) + era-balanced retrieve → `lineage` SSE + prompt block → cited answer
|
||
│
|
||
P50: event calendar ─► BLS OEWS/CES, market series, Part B utilization ─► event-study notebook
|
||
```
|
||
|
||
## Components (P49)
|
||
|
||
### `pfs/elements.py` — element model
|
||
Pure dataclasses + the closed vocabulary (`ElementType` enum; `VOCAB: dict[ElementType, tuple[str, ...]]` seeded from the 99490, 99487/99489, 99491/99437, 99439, 99495/99496, 99497/99498, 99424–99427, G0556–G0558, 99441–99443/98008–98016, G2211 descriptors in the corpus). Descriptor parser for the deterministic parts: minutes, "each additional", period words, code references inside parentheticals ("Use in conjunction with 99490", "Do not report … 99487, 99489, 99491").
|
||
|
||
### `pfs/extract.py` — extractor
|
||
Input: a descriptor text plus the FR paragraph run that prints it (stem paragraph followed by element paragraphs, as in `DE2VH9PD` ¶1244–1247 and `JJ6AM5HJ` ¶1163–1185). Deterministic parse first; then the local model maps each element paragraph to a vocabulary value or proposes a new value (queued for review, never auto-added). Output rows into DuckDB `pfs.code_element` with the anchor. Runs from `stack pfs elements --code … | --family … | --all-payable`.
|
||
|
||
### `pfs/lineage.py` — events
|
||
Sources: (a) `pfs.rvu`/`rvu_proposed` diffs per code per year (appears, disappears, status change, descriptor change, |ΔRVU| > 10 %); (b) `fr_anchors` paragraphs where `codes ∋ code` and an event pattern matches (`finaliz…creat`, `replace`, `delet`, `crosswalk`, `bundled`, `telehealth services list`). Emits `pfs.code_event`. Cross-check rule: an RVU-file appearance without an FR paragraph within ±1 rule year, or vice versa, is flagged `unanchored` for review. The 5 worked lineages above are the fixture.
|
||
|
||
### `pfs/families.py` — derive
|
||
Keep the API (`FAMILIES`, `detect_codes`, `family_of`) but source `FAMILIES` from `pfs.code_family` at import time when the replica is present, falling back to the hand list. Family key = stem slug; synonyms = descriptor stems + FR family phrases.
|
||
|
||
### `bib/tag.py` + `bib/sync.py` — anchors on items
|
||
New namespaces `code:` and `family:`; tagger walks rules (via `fr_anchors.text`), comments (chunk text via pgvector or abstract), IOM chapters (attachment text), eCFR sections, MLN items. Nightly zotero-sync carries them (P38 tag-merge rules apply; `sup:` tags stay load-bearing).
|
||
|
||
### `llm/chunk.py` + `llm/index.py` — anchors on chunks
|
||
`codes` metadata on all three collections; add `families` (space-joined keys) and `elements` (space-joined `type=value` slugs when a chunk mentions a vocabulary value). One-time `stack llm index --collection all --force`.
|
||
|
||
### `bib/reaction.py` — reaction series
|
||
Per (family, docket): comment count, share of docket, stance (support/oppose/modify — closed vocab, local model, P35 pipeline), top organisations (`org:` tags exist). Pre-2017: `Comment:`/`Response:` pairs from `fr_anchors` where the pair mentions the family's codes. Output table `pfs.code_reaction` and a notebook.
|
||
|
||
### `llm/evidence.py` + `llm/rag.py` + `llm/web/chat.html` — longitudinal chat
|
||
`lineage_evidence(question)` → events + element diffs for the detected codes; `retrieve(..., mode="timeline")` era-balanced; `lineage` SSE event; prompt block; system prompt: cite the event label for every dated claim. UI: timeline table with FR jump links (P40) and eCFR links (P41).
|
||
|
||
### Evaluation
|
||
Golden questions with expected anchors: "history of CCM coding and payment", "what replaced G2058", "how did the APCM elements differ from CCM", "when were audio-only E/M codes payable and why did 99441–99443 end", "what did commenters say about G2211 in 2023 vs 2025", "does 99490 pass telehealth Step 3". Pass = every expected `(item_key, p_id)` appears in sources and the prose cites the event labels.
|
||
|
||
## P50 — legal epidemiology (scoped, not designed)
|
||
- `pfs.code_event` supplies the exposure calendar: effective date = 1 January of the rule year (or mid-year for correction rules), exposure = code/family becomes payable, revalued, or ends.
|
||
- Outcome series to ingest, each with its own module and provenance in bib: Medicare Physician & Other Practitioners PUF (utilization and allowed amounts per HCPCS, 2013–), BLS OEWS (occupation wages: 29-1xxx physicians, 29-2xxx/31-9xxx clinical support), BLS CES (health-care employment), market series (sector ETFs; telehealth/RPM/care-management tickers).
|
||
- Event-study notebook: pre/post windows around each event with never-treated code sets as controls; output effect sizes with confidence bands, never causal language in the chat.
|
||
|
||
## Out of scope (P49)
|
||
Locality pricing, OPPS, MA/commercial coding, CPT copyrighted long descriptors beyond what the FR prints, cloud LLM APIs.
|