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.
This commit is contained in:
@@ -34,7 +34,7 @@ The goal of P49 is that the chat, and the notebooks, can build a **longitudinal
|
||||
## 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.
|
||||
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.
|
||||
|
||||
@@ -286,10 +286,11 @@ def _derive_rows(con: Any) -> list[Any]:
|
||||
)
|
||||
|
||||
|
||||
def _print_families(rows: list[Any]) -> None:
|
||||
def _print_families(rows: list[Any], *, verbose: bool = False) -> None:
|
||||
by_key: dict[str, list[Any]] = {}
|
||||
for r in rows:
|
||||
by_key.setdefault(r.key, []).append(r)
|
||||
if verbose:
|
||||
for key, members in sorted(by_key.items()):
|
||||
typer.echo(f"{key}: {' '.join(f'{r.code}({r.role})' for r in members)}")
|
||||
total = len(by_key)
|
||||
@@ -308,16 +309,24 @@ def _print_families(rows: list[Any]) -> None:
|
||||
|
||||
|
||||
@app.command()
|
||||
def families(write: bool = typer.Option(False, "--write")) -> None:
|
||||
def families(
|
||||
write: bool = typer.Option(False, "--write"),
|
||||
verbose: bool = typer.Option(
|
||||
False, "--verbose", help="Print every family's members, not just the summary."
|
||||
),
|
||||
) -> None:
|
||||
"""Derive families from pfs.code_element / pfs.code_event / pfs.rvu."""
|
||||
if write:
|
||||
with _batch() as con:
|
||||
ensure_tables(con)
|
||||
rows = _derive_rows(con)
|
||||
_print_families(rows)
|
||||
write_families(con, rows)
|
||||
refresh_from(con)
|
||||
_publish()
|
||||
# refresh_from and printing don't need the write lock open — do
|
||||
# them after it's released, from the freshly published replica
|
||||
# (the same pattern `stack llm restamp` uses).
|
||||
refresh_from(_read())
|
||||
_print_families(rows, verbose=verbose)
|
||||
return
|
||||
# No --write: derive-and-print never needs the write lock (I4).
|
||||
con = _read()
|
||||
@@ -329,7 +338,7 @@ def families(write: bool = typer.Option(False, "--write")) -> None:
|
||||
typer.echo("no derived tables yet — run with --write")
|
||||
return
|
||||
raise
|
||||
_print_families(rows)
|
||||
_print_families(rows, verbose=verbose)
|
||||
finally:
|
||||
con.close()
|
||||
|
||||
|
||||
@@ -285,7 +285,9 @@ class TestFamilies:
|
||||
assert res.exit_code == 0, res.output
|
||||
fams = read_families(con)
|
||||
assert {r.code for r in fams if r.key == "CCM"} >= {"99490", "99439", "G2058"}
|
||||
assert "CCM" in res.output
|
||||
# Minor: --write prints the summary line by default; the
|
||||
# per-family dump ("CCM" among them) is --verbose only.
|
||||
assert "CCM" not in res.output
|
||||
# No pfs.cpt_* rows seeded in this test — the summary line must
|
||||
# still print, with cpt-named at 0 (I4: read-only-tolerant path
|
||||
# also applies to the write path when the CPT tables are empty).
|
||||
@@ -293,6 +295,22 @@ class TestFamilies:
|
||||
res.output
|
||||
)
|
||||
|
||||
def test_verbose_prints_the_per_family_dump(self, con):
|
||||
write_elements(
|
||||
con,
|
||||
"99439",
|
||||
[
|
||||
ElementRow(
|
||||
"99439", 2021, "relation", "addon-of", "99490", "", "K", 1, 1, "fr"
|
||||
)
|
||||
],
|
||||
[],
|
||||
)
|
||||
res = runner.invoke(app, ["pfs", "families", "--write", "--verbose"])
|
||||
assert res.exit_code == 0, res.output
|
||||
assert "CCM" in res.output
|
||||
assert "families: total" in res.output
|
||||
|
||||
def _ccm_cpt_edition(self, year=2024):
|
||||
section = CptSection(
|
||||
sec_id="sec_ccm",
|
||||
|
||||
Reference in New Issue
Block a user