Files
stack/docs/superpowers/specs/2026-09-09-code-family-longitudinal-design.md
kert 041378b16b fix(cli): pfs families --write prints the summary by default (refs #699)
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.
2026-09-09 21:20:20 -04:00

14 KiB
Raw Blame History

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 20152026, ten comment dockets 20172026, 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 ¶12441247, 79 FR 67716). The commenter-vs-CMS argument was about a single element (per calendar month vs per 30 days, ¶12481251).
  • HCPCS G0556G0558 (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 ¶11631185, 89 FR 9786497865). 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 ¶12491251).
  • 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 (20202021) → 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).
  • 9944199443 deleted by the CPT panel, last in the RVU file 2024, replaced by 9800898015 / 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: ~1013k per docket 20202023.
  • 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 20152026; 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, 9942499427, G0556G0558, 9944199443/9800898016, 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 ¶12441247 and JJ6AM5HJ ¶11631185). 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 9944199443 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.

  • 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.