merge: #693 — exposure calendar: pfs.code_exposure, stack pfs exposure, notebook 7c (refs #693)
Some checks failed
CI / lint (push) Successful in 32s
CI / notebooks-smoke (push) Successful in 1m40s
Deploy / notebooks (push) Has been skipped
Deploy / zotero (push) Has been skipped
Deploy / docs (push) Has been skipped
CI / test (push) Successful in 2m25s
Deploy / api (push) Has been skipped
Deploy / llm (push) Has been skipped
Deploy / mc (push) Has been skipped
Infra CI / zotero (push) Successful in 14s
Infra CI / notebooks (push) Successful in 48s
Infra CI / docs (push) Successful in 1m31s
Infra CI / api (push) Successful in 1m13s
Deploy / report (push) Has been cancelled
Infra CI / llm (push) Has been cancelled
Infra CI / mc (push) Has been cancelled

This commit is contained in:
kert
2026-09-22 15:47:47 -04:00
39 changed files with 1217 additions and 32 deletions

View File

@@ -1,6 +1,6 @@
---
title: stack mail attach-smarthost
sidebar_position: 97
sidebar_position: 98
---
# `stack mail attach-smarthost`

View File

@@ -1,6 +1,6 @@
---
title: stack mail dkim-export
sidebar_position: 96
sidebar_position: 97
---
# `stack mail dkim-export`

View File

@@ -1,6 +1,6 @@
---
title: stack mail dns
sidebar_position: 95
sidebar_position: 96
---
# `stack mail dns`

View File

@@ -1,6 +1,6 @@
---
title: stack mail down
sidebar_position: 93
sidebar_position: 94
---
# `stack mail down`

View File

@@ -1,6 +1,6 @@
---
title: stack mail provision
sidebar_position: 91
sidebar_position: 92
---
# `stack mail provision`

View File

@@ -1,6 +1,6 @@
---
title: stack mail rotate-creds
sidebar_position: 98
sidebar_position: 99
---
# `stack mail rotate-creds`

View File

@@ -1,6 +1,6 @@
---
title: stack mail seed-mailboxes
sidebar_position: 99
sidebar_position: 100
---
# `stack mail seed-mailboxes`

View File

@@ -1,6 +1,6 @@
---
title: stack mail status
sidebar_position: 94
sidebar_position: 95
---
# `stack mail status`

View File

@@ -1,6 +1,6 @@
---
title: stack mail up
sidebar_position: 92
sidebar_position: 93
---
# `stack mail up`

View File

@@ -1,6 +1,6 @@
---
title: stack mail wire-git
sidebar_position: 100
sidebar_position: 101
---
# `stack mail wire-git`

View File

@@ -1,6 +1,6 @@
---
title: stack mail
sidebar_position: 90
sidebar_position: 91
---
# `stack mail`

View File

@@ -1,6 +1,6 @@
---
title: stack pfs cpt-ingest
sidebar_position: 72
sidebar_position: 73
---
# `stack pfs cpt-ingest`

View File

@@ -0,0 +1,35 @@
---
title: stack pfs exposure
sidebar_position: 71
---
# `stack pfs exposure`
```
Usage: stack pfs exposure [OPTIONS]
The exposure calendar (pfs.code_exposure, #693): when each code became
payable, was revalued, changed payable status, ended, or was listed for
telehealth — dated 1 January of the rule year (or the correction notice's
date), anchored to the FR paragraph from its lineage, with the status and
RVU-decile band a control set matches on.
--write always rebuilds every code (control sets need the whole
calendar); --code/--family only filter what is printed.
╭─ Options ────────────────────────────────────────────────────────────────────╮
│ --code TEXT Filter to a code; repeatable. │
│ --family TEXT Filter to a family key (CCM, APCM, …); │
│ repeatable. │
│ --controls INTEGER With one --code and --year: list up to N │
│ never-treated control codes (same status + RVU │
│ band, no exposure within --window years). │
│ [default: 0] │
│ --year INTEGER The exposure year --controls refers to. │
│ [default: 0] │
│ --window INTEGER ±years a control must be quiet for. [default: 2] │
│ --write Rebuild the whole calendar into pfs.code_exposure │
│ and republish. │
│ --help Show this message and exit. │
╰──────────────────────────────────────────────────────────────────────────────╯
```

View File

@@ -1,6 +1,6 @@
---
title: stack pfs review
sidebar_position: 71
sidebar_position: 72
---
# `stack pfs review`

View File

@@ -35,6 +35,13 @@ Usage: stack pfs [OPTIONS] COMMAND [ARGS]...
│ live │
│ from pgvector/bib — there is no persisted-table fallback to read │
│ without --write, so --write only adds persisting the result. │
│ exposure The exposure calendar (pfs.code_exposure, #693): when each code │
│ became payable, was revalued, changed payable status, ended, or │
│ was │
│ listed for telehealth — dated 1 January of the rule year (or the │
│ correction notice's date), anchored to the FR paragraph from its │
│ lineage, with the status and RVU-decile band a control set │
│ matches on. │
│ review Element lines the classifier could not place. │
│ cpt-ingest Parse CPT EPUB editions into │
│ pfs.cpt_section/cpt_code/cpt_instruction/ │

View File

@@ -1,6 +1,6 @@
---
title: stack prisma eligible
sidebar_position: 84
sidebar_position: 85
---
# `stack prisma eligible`

View File

@@ -1,6 +1,6 @@
---
title: stack prisma export
sidebar_position: 81
sidebar_position: 82
---
# `stack prisma export`

View File

@@ -1,6 +1,6 @@
---
title: stack prisma extract
sidebar_position: 85
sidebar_position: 86
---
# `stack prisma extract`

View File

@@ -1,6 +1,6 @@
---
title: stack prisma fetch
sidebar_position: 87
sidebar_position: 88
---
# `stack prisma fetch`

View File

@@ -1,6 +1,6 @@
---
title: stack prisma flow
sidebar_position: 86
sidebar_position: 87
---
# `stack prisma flow`

View File

@@ -1,6 +1,6 @@
---
title: stack prisma init
sidebar_position: 80
sidebar_position: 81
---
# `stack prisma init`

View File

@@ -1,6 +1,6 @@
---
title: stack prisma ping-llm
sidebar_position: 82
sidebar_position: 83
---
# `stack prisma ping-llm`

View File

@@ -1,6 +1,6 @@
---
title: stack prisma run
sidebar_position: 88
sidebar_position: 89
---
# `stack prisma run`

View File

@@ -1,6 +1,6 @@
---
title: stack prisma screen
sidebar_position: 83
sidebar_position: 84
---
# `stack prisma screen`

View File

@@ -1,6 +1,6 @@
---
title: stack prisma vpn
sidebar_position: 89
sidebar_position: 90
---
# `stack prisma vpn`

View File

@@ -1,6 +1,6 @@
---
title: stack prisma
sidebar_position: 79
sidebar_position: 80
---
# `stack prisma`

View File

@@ -1,6 +1,6 @@
---
title: stack zot dump-schema
sidebar_position: 74
sidebar_position: 75
---
# `stack zot dump-schema`

View File

@@ -1,6 +1,6 @@
---
title: stack zot fix-dates
sidebar_position: 75
sidebar_position: 76
---
# `stack zot fix-dates`

View File

@@ -1,6 +1,6 @@
---
title: stack zot fix-fields
sidebar_position: 77
sidebar_position: 78
---
# `stack zot fix-fields`

View File

@@ -1,6 +1,6 @@
---
title: stack zot fix-keys
sidebar_position: 76
sidebar_position: 77
---
# `stack zot fix-keys`

View File

@@ -1,6 +1,6 @@
---
title: stack zot verify-parity
sidebar_position: 78
sidebar_position: 79
---
# `stack zot verify-parity`

View File

@@ -1,6 +1,6 @@
---
title: stack zot
sidebar_position: 73
sidebar_position: 74
---
# `stack zot`

View File

@@ -1033,6 +1033,100 @@ def _(code, mo, pl):
return
@app.cell(hide_code=True)
def _(code, con, fr_md, mo, not_built, pl):
# ── 7c. Exposure calendar ──
from pfs.codetables import read_exposures as _read_exposures
from pfs.exposure import control_codes as _control_codes
from pfs.families import family_of as _family_of_x
_fam = _family_of_x(code)
_key = _fam.key if _fam else ""
_rows = []
if con is not None:
try:
_rows = (
_read_exposures(con, family=_key)
if _key
else _read_exposures(con, code=code)
)
except Exception: # noqa: BLE001 — a missing table is "not built yet"
_rows = []
_intro = (
"## 7c. Exposure calendar\n\n"
"P50 reads the fee schedule as law with effects: every row here is a date "
"on which a code's legal status under the PFS changed — it became payable, "
"was revalued by more than ten percent, moved between payable statuses, "
"ended, or was listed for telehealth — derived from the RVU files and the "
"lineage events above, never hand-entered. The effective date is 1 January "
"of the rule year unless the only Federal Register anchor is a correction "
"notice, in which case the notice's own date is used. Each row keeps the "
"code's RVU status and decile band that year, which is what a "
"never-treated control set is matched on."
)
if not _rows:
_view = mo.md(_intro + "\n\n" + not_built("stack pfs exposure --write"))
else:
_tbl = pl.DataFrame(
{
"code": [r.code for r in _rows],
"effective": [r.effective_date.isoformat() for r in _rows],
"exposure": [r.kind for r in _rows],
"status": [
f"{r.prior_status or '—'}→{r.status or '—'}"
if r.kind in ("becomes-payable", "ends", "status-change")
else (f"{r.delta_pct:+.1%}" if r.kind == "revalued" else r.status)
for r in _rows
],
"band": [r.rvu_band for r in _rows],
"anchor": [
(
fr_md(r.item_key, r.p_id)
+ (" (correction)" if r.correction else "")
)
if r.item_key
else "_unanchored_"
for r in _rows
],
}
)
# A control-set preview for the family's most recent becomes-payable
# exposure (or its newest row): same status + band, quiet for ±2 years.
_pick = max(
_rows,
key=lambda r: (r.kind == "becomes-payable", r.year),
)
try:
_ctrl = _control_codes(con, _pick.code, _pick.year, window=2, limit=12)
except Exception: # noqa: BLE001
_ctrl = []
_ctrl_md = (
f"**Controls for {_pick.code} {_pick.kind} ({_pick.year})** — "
f"{len(_ctrl)} shown, cheapest first: "
+ ", ".join(f"{c}" + (f" [{f}]" if f else "") for c, f, _s, _b in _ctrl)
if _ctrl
else f"_No never-treated controls found for {_pick.code} in {_pick.year}._"
)
_n_anchored = sum(1 for r in _rows if r.item_key)
_view = mo.vstack(
[
mo.md(
_intro + f"\n\n**{_key or code}**: {len(_rows)} exposure"
f"{'s' if len(_rows) != 1 else ''}, {_n_anchored} anchored to an "
"FR paragraph."
),
mo.ui.table(
_tbl,
label=f"pfs.code_exposure — {_key or code} ({len(_rows)} rows)",
),
mo.md(_ctrl_md),
]
)
_view
return
@app.cell(hide_code=True)
def _(NOTES, REPLICA_PATH, con, mo, pl, q, store):
# ── 8. Provenance ──
@@ -1049,7 +1143,8 @@ def _(NOTES, REPLICA_PATH, con, mo, pl, q, store):
"SELECT 'code_element' t, count(*) n FROM pfs.code_element "
"UNION ALL SELECT 'code_element_review', count(*) FROM pfs.code_element_review "
"UNION ALL SELECT 'code_event', count(*) FROM pfs.code_event "
"UNION ALL SELECT 'code_family', count(*) FROM pfs.code_family"
"UNION ALL SELECT 'code_family', count(*) FROM pfs.code_family "
"UNION ALL SELECT 'code_exposure', count(*) FROM pfs.code_exposure"
)
_log = q(
"SELECT run_id, ingested_at, module, table_name, rule_id, source_file, sha256, rows, "

View File

@@ -32,15 +32,19 @@ from pfs.codetables import (
read_cpt_codes,
read_cpt_instructions,
read_cpt_sections,
read_exposures,
read_guidance,
read_reviews,
write_elements,
write_events,
write_exposures,
write_families,
write_guidance,
write_reaction,
)
from pfs.cpt_load import ingest as cpt_ingest
from pfs.exposure import build as build_exposures
from pfs.exposure import control_codes
from pfs.extract import extract_codes
from pfs.families import (
FAMILIES,
@@ -652,6 +656,121 @@ def reaction(
_print_reaction(rows)
def _print_exposures(rows: list[Any]) -> None:
for r in rows:
anchor = f"{r.item_key}{r.p_id}" if r.item_key else "unanchored"
if r.correction:
anchor += " (correction)"
extra = ""
if r.kind == "revalued":
extra = f" {r.delta_pct:+.1%}"
elif r.kind in ("becomes-payable", "ends", "status-change"):
extra = f" {r.prior_status or '—'}→{r.status or '—'}"
fam = f" [{r.family}]" if r.family else ""
typer.echo(
f"{r.code} {r.effective_date.isoformat()} {r.kind:<17}"
f"{extra} band {r.rvu_band}{fam} ← {anchor}"
)
per_kind: dict[str, int] = {}
for r in rows:
per_kind[r.kind] = per_kind.get(r.kind, 0) + 1
detail = ", ".join(f"{k}={v}" for k, v in sorted(per_kind.items())) or "none"
anchored = sum(1 for r in rows if r.item_key)
typer.echo(
f"exposures: {len(rows)} rows, {len({r.code for r in rows})} codes, "
f"anchored {anchored}, per kind: {detail}"
)
@app.command()
def exposure(
code: list[str] = typer.Option([], "--code", help="Filter to a code; repeatable."),
family: list[str] = typer.Option(
[], "--family", help="Filter to a family key (CCM, APCM, …); repeatable."
),
controls: int = typer.Option(
0,
"--controls",
help="With one --code and --year: list up to N never-treated control codes "
"(same status + RVU band, no exposure within --window years).",
),
year: int = typer.Option(
0, "--year", help="The exposure year --controls refers to."
),
window: int = typer.Option(
2, "--window", help="±years a control must be quiet for."
),
write: bool = typer.Option(
False,
"--write",
help="Rebuild the whole calendar into pfs.code_exposure and republish.",
),
) -> None:
"""The exposure calendar (pfs.code_exposure, #693): when each code
became payable, was revalued, changed payable status, ended, or was
listed for telehealth — dated 1 January of the rule year (or the
correction notice's date), anchored to the FR paragraph from its
lineage, with the status and RVU-decile band a control set matches on.
--write always rebuilds every code (control sets need the whole
calendar); --code/--family only filter what is printed."""
codes = [c.upper() for c in code]
keys = [normalize_key(f) for f in family]
def _filter(rows: list[Any]) -> list[Any]:
if not codes and not keys:
return rows
return [r for r in rows if r.code in codes or r.family in keys]
if write:
# Ruling A13: build against a plain read (I4) — never the write
# lock a notebook may be holding (#508-#514); the batch below only
# writes and publishes.
store = _store()
con = _read()
try:
rows = build_exposures(con, store)
finally:
con.close()
with _batch() as con:
ensure_tables(con)
write_exposures(con, rows)
_publish()
_print_exposures(_filter(rows))
return
con = _read()
try:
if controls:
if len(codes) != 1 or not year:
raise typer.BadParameter(
"--controls needs exactly one --code and --year"
)
try:
ctrl = control_codes(con, codes[0], year, window=window, limit=controls)
except Exception as exc:
if _missing_table(exc):
typer.echo("no exposure calendar yet — run with --write")
return
raise
for c, fam, status, band in ctrl:
typer.echo(f"{c} {status} band {band} {fam}")
typer.echo(
f"controls for {codes[0]} @ {year} (±{window}y): {len(ctrl)} codes"
)
return
try:
rows = read_exposures(con)
except Exception as exc:
if _missing_table(exc):
typer.echo("no exposure calendar yet — run with --write")
return
raise
_print_exposures(_filter(rows))
finally:
con.close()
@app.command()
def review(code: str = typer.Option("", "--code")) -> None:
"""Element lines the classifier could not place."""

View File

@@ -4,6 +4,8 @@
``pfs.code_element_review`` element lines the classifier could not place
``pfs.code_event`` dated lineage events (created, replaced, …)
``pfs.code_family`` derived family membership with roles
``pfs.code_exposure`` the exposure calendar (#693): dated payable /
revalued / status / ends / telehealth events
``pfs.cpt_*`` (docs/superpowers/specs/2026-09-09-cpt-canonical-schema-design.md
§3) hold one CPT codebook edition's own organizing structure —
@@ -28,6 +30,7 @@ database.
from __future__ import annotations
import datetime as _dt
from dataclasses import astuple, dataclass
from typing import TYPE_CHECKING, Any, Sequence
@@ -240,6 +243,41 @@ class CptListRow:
code: str
@dataclass(frozen=True)
class ExposureRow:
"""One exposure on the calendar (``pfs.code_exposure``, #693): *code*
is exposed to *kind* (``pfs.exposure.KINDS``) from *effective_date*
— 1 January of *year* unless the only Federal Register anchor is a
correction notice, in which case that notice's publication date and
``correction=True``. ``status``/``prior_status`` are the RVU-file
status codes on either side of the event (``""`` when the code is
absent from that year's file); ``rvu_band`` is the decile of
``non_fac_total`` among that year's payable codes (1 cheapest … 10,
0 when unranked) and ``non_fac_total`` the value it was ranked on —
both taken from the last payable year for an ``ends`` row; ``delta_pct``
is the revaluation fraction (``0.475`` for +47.5%) and ``0.0`` otherwise.
``item_key``/``p_id``/``page``/``anchor_kind`` carry the lineage row
the exposure is anchored to (empty/0 when no FR paragraph names the
code within a year of the event)."""
code: str
family: str
effective_date: _dt.date
year: int
kind: str
status: str
prior_status: str
rvu_band: int
non_fac_total: float
delta_pct: float
item_key: str
p_id: int
page: int
anchor_kind: str
correction: bool
note: str
@dataclass(frozen=True)
class CptCodeAltRow:
edition_year: int
@@ -264,6 +302,11 @@ CREATE TABLE IF NOT EXISTS pfs.code_event (
CREATE TABLE IF NOT EXISTS pfs.code_family (
key VARCHAR, name VARCHAR, code VARCHAR, role VARCHAR, since INTEGER, until INTEGER,
item_key VARCHAR, p_id INTEGER, note VARCHAR);
CREATE TABLE IF NOT EXISTS pfs.code_exposure (
code VARCHAR, family VARCHAR, effective_date DATE, year INTEGER, kind VARCHAR,
status VARCHAR, prior_status VARCHAR, rvu_band INTEGER, non_fac_total DOUBLE,
delta_pct DOUBLE, item_key VARCHAR, p_id INTEGER, page INTEGER, anchor_kind VARCHAR,
correction BOOLEAN, note VARCHAR);
CREATE TABLE IF NOT EXISTS pfs.code_guidance (
family VARCHAR, code VARCHAR, kind VARCHAR, locator VARCHAR, item_key VARCHAR,
item_key_src VARCHAR, p_id_src INTEGER, page_src INTEGER, page INTEGER);
@@ -418,6 +461,29 @@ def read_all_events(con: Any) -> dict[str, list[EventRow]]:
return out
def write_exposures(con: Any, rows: Sequence[ExposureRow]) -> int:
"""Full replace — the calendar is always rebuilt whole (control sets
need every code's exposures, not one family's)."""
ensure_tables(con)
con.execute("DELETE FROM pfs.code_exposure")
return _insert(con, "pfs.code_exposure", rows)
def read_exposures(con: Any, *, code: str = "", family: str = "") -> list[ExposureRow]:
where, params = [], []
if code:
where.append("code = ?")
params.append(code.upper())
if family:
where.append("family = ?")
params.append(family)
sql = "SELECT * FROM pfs.code_exposure"
if where:
sql += " WHERE " + " AND ".join(where)
rows = con.execute(sql + " ORDER BY code, year, kind", params).fetchall()
return [ExposureRow(*r) for r in rows]
def read_families(con: Any) -> list[FamilyRow]:
rows = con.execute(
"SELECT * FROM pfs.code_family ORDER BY key, role, code"

402
src/pfs/exposure.py Normal file
View File

@@ -0,0 +1,402 @@
"""The exposure calendar (#693): when a code became payable under the
fee schedule, was revalued, changed payable status, ended, or was listed
for telehealth — one dated row each, anchored to the lineage paragraph
that says so, with the status and RVU-band context a control set is
matched on.
Legal-epidemiology framing (spec §P50): the fee schedule is law, each
row here is an exposure date, and ``control_codes`` picks the
never-treated comparison set for a window around it. The calendar is
derived, never hand-entered::
rvu_series(con) pfs.rvu → {code: [(year, status, non_fac_total)]}
rvu_bands(series) (year, code) → decile of non_fac_total among
that year's payable codes
derive(...) pure: series + pfs.code_event rows → ExposureRow
build(con, store) gathers the inputs (rule kinds/dates from bib)
control_codes(con, ...) same status + band, no exposure in ±window
Rules the rows follow:
* **Payable** means RVU status ``A``/``R``/``T`` (``pfs.valuation
.PAID_STATUSES`` — the same set ``stack pfs lineage --all-payable``
targets). ``becomes-payable`` is the first year a code is payable after
a year it was not (or was absent); ``ends`` is the first year it is not
(or is absent) after a payable year; ``status-change`` is a move
between two payable statuses (A→R, R→T …). Non-payable ↔ non-payable
moves are not exposures.
* **Left-censoring**: the RVU files start in 2015. A code payable in the
first year only gets a ``becomes-payable`` row when a Federal Register
``created``/``adopted_cpt``/``replaces`` event for it sits in that
year — otherwise its start is unknown, not 2015.
* **Revalued** rows come from the lineage ``revalued`` events (>10%
change in ``non_fac_total``); ``telehealth-listed`` rows from the FR
``telehealth_list`` events, once per run of consecutive years — a
code mentioned on the list in 2022, 2023 and 2024 was listed once,
in 2022.
* **Anchors** come from the same code's FR lineage rows: non-correction
rules first, then the nearest year (same year, else ±1 — FR events are
dated by the mentioning rule's payment year), kind-matched
(``created``/``adopted_cpt``/``replaces`` for becomes-payable;
``deleted``/``replaced_by``/``bundled`` for ends; ``telehealth_list``
for telehealth) before any other kind, lowest ``p_id`` last. A
correction notice (``bib.frlink.rule_kind``) is chosen only when no
other rule anchors the event — and when it sits in the event's own
year the exposure's effective date is the notice's publication date,
not 1 January.
"""
from __future__ import annotations
import datetime as dt
import re
from typing import Any, Callable, Mapping, Sequence
from pfs.codetables import EventRow, ExposureRow, read_all_events
from pfs.valuation import PAID_STATUSES
KINDS = ("becomes-payable", "revalued", "status-change", "ends", "telehealth-listed")
#: Preferred FR event kinds per exposure kind, best first; an empty tuple
#: means any FR event of the code anchors it.
_ANCHOR_PREF: dict[str, tuple[str, ...]] = {
"becomes-payable": ("created", "adopted_cpt", "replaces"),
"ends": ("deleted", "replaced_by", "bundled"),
"telehealth-listed": ("telehealth_list",),
"revalued": (),
"status-change": (),
}
_BIRTH_KINDS = ("created", "adopted_cpt", "replaces")
_PCT = re.compile(r"([+-]?\d+(?:\.\d+)?)%")
Series = dict[str, list[tuple[int, str, float]]]
#: item_key → (rule kind, publication date) — ``bib.frlink.rule_kind``
#: plus ``items.date_published``; ``None`` date when unknown.
RuleInfo = Mapping[str, tuple[str, dt.date | None]]
def payable(status: str) -> bool:
return (status or "") in PAID_STATUSES
# ── inputs ────────────────────────────────────────────────────────────
def rvu_series(con: Any) -> Series:
"""One scan of ``pfs.rvu``: every code's ``(year, status, non_fac_total)``
on its unmodified row (mod NULL/''), sorted by year."""
out: Series = {}
for code, year, status, total in con.execute(
"SELECT hcpcs, year, status_code, non_fac_total FROM pfs.rvu "
"WHERE mod IS NULL OR mod = '' "
"QUALIFY row_number() OVER (PARTITION BY hcpcs, year ORDER BY mod NULLS FIRST) = 1 "
"ORDER BY hcpcs, year"
).fetchall():
out.setdefault(code, []).append((int(year), status or "", float(total or 0.0)))
return out
def rvu_bands(series: Series) -> dict[tuple[int, str], int]:
"""``(year, code) → 1..10`` — decile of ``non_fac_total`` among the
codes payable that year (ties share the lower rank's decile)."""
per_year: dict[int, list[tuple[float, str]]] = {}
for code, rows in series.items():
for year, status, total in rows:
if payable(status):
per_year.setdefault(year, []).append((total, code))
out: dict[tuple[int, str], int] = {}
for year, vals in per_year.items():
vals.sort()
n = len(vals)
prev_total, prev_band = None, 0
for i, (total, code) in enumerate(vals):
band = prev_band if total == prev_total else 1 + (i * 10) // n
out[(year, code)] = band
prev_total, prev_band = total, band
return out
def series_span(series: Series) -> tuple[int, int]:
years = [y for rows in series.values() for y, _, _ in rows]
return (min(years), max(years)) if years else (0, 0)
# ── derivation (pure) ─────────────────────────────────────────────────
def _anchor(
kind: str,
year: int,
fr: Sequence[EventRow],
rule_info: RuleInfo,
) -> tuple[EventRow | None, bool]:
"""The lineage row anchoring (*kind*, *year*): nearest year, preferred
kind, lowest p_id; non-correction rules before correction notices.
Returns ``(row, is_correction)``."""
pref = _ANCHOR_PREF.get(kind, ())
def rank(e: EventRow) -> tuple[int, int, int, int]:
is_corr = 1 if rule_info.get(e.item_key, ("", None))[0] == "correction" else 0
k = pref.index(e.kind) if e.kind in pref else len(pref)
return (is_corr, abs(e.year - year), k, e.p_id)
cands = [e for e in fr if abs(e.year - year) <= 1]
if pref:
matched = [e for e in cands if e.kind in pref]
cands = matched or cands
if not cands:
return None, False
best = min(cands, key=rank)
return best, rule_info.get(best.item_key, ("", None))[0] == "correction"
def _telehealth_years(fr: Sequence[EventRow]) -> list[int]:
years = sorted({e.year for e in fr if e.kind == "telehealth_list"})
starts = [y for i, y in enumerate(years) if i == 0 or y - years[i - 1] > 1]
return starts
def derive(
series: Series,
events_by_code: Mapping[str, Sequence[EventRow]],
span: tuple[int, int],
*,
family_of: Callable[[str], str],
rule_info: RuleInfo,
bands: Mapping[tuple[int, str], int] | None = None,
) -> list[ExposureRow]:
"""Every exposure for every code in *series* (plus telehealth
listings for codes only the FR knows), sorted by (code, year, kind)."""
lo, hi = span
bands = bands if bands is not None else rvu_bands(series)
out: list[ExposureRow] = []
codes = sorted(set(series) | set(events_by_code))
for code in codes:
rows = series.get(code, [])
by_year = {y: (s, t) for y, s, t in rows}
events = events_by_code.get(code, [])
fr = [e for e in events if e.source == "fr"]
found: dict[tuple[int, str], dict[str, Any]] = {}
def put(year: int, kind: str, **kw: Any) -> None:
found.setdefault((year, kind), kw)
# payable transitions, year by year across the file span — a code
# still payable in the newest file is right-censored, not ended
for year in range(lo, hi + 1):
prev = by_year.get(year - 1)
cur = by_year.get(year)
ps, pt = prev if prev else ("", 0.0)
cs, ct = cur if cur else ("", 0.0)
if year == lo:
if payable(cs) and any(
e.kind in _BIRTH_KINDS and e.year == lo for e in fr
):
put(
year,
"becomes-payable",
status=cs,
prior_status="",
total=ct,
band_year=year,
)
continue
if payable(cs) and not payable(ps):
put(
year,
"becomes-payable",
status=cs,
prior_status=ps,
total=ct,
band_year=year,
)
elif payable(ps) and not payable(cs):
put(
year,
"ends",
status=cs,
prior_status=ps,
total=pt,
band_year=year - 1,
)
elif payable(ps) and payable(cs) and ps != cs:
put(
year,
"status-change",
status=cs,
prior_status=ps,
total=ct,
band_year=year,
)
for e in events:
if e.source == "rvu" and e.kind == "revalued":
m = _PCT.search(e.note or "")
delta = float(m.group(1)) / 100.0 if m else 0.0
s, t = by_year.get(e.year, ("", 0.0))
ps, _ = by_year.get(e.year - 1, ("", 0.0))
put(
e.year,
"revalued",
status=s,
prior_status=ps,
total=t,
band_year=e.year,
delta=delta,
)
for year in _telehealth_years(fr):
s, t = by_year.get(year, ("", 0.0))
ps, _ = by_year.get(year - 1, ("", 0.0))
put(
year,
"telehealth-listed",
status=s,
prior_status=ps,
total=t,
band_year=year,
)
fam = family_of(code) or ""
for (year, kind), kw in found.items():
anchor, is_corr = _anchor(kind, year, fr, rule_info)
eff = dt.date(year, 1, 1)
# A correction notice only moves the effective date when it is
# the anchor for the event's own year — a neighbouring year's
# correction is evidence of the code, not of this event's date.
if anchor is not None and is_corr and anchor.year == year:
pub = rule_info.get(anchor.item_key, ("", None))[1]
if pub is not None:
eff = pub
out.append(
ExposureRow(
code=code,
family=fam,
effective_date=eff,
year=year,
kind=kind,
status=kw["status"],
prior_status=kw["prior_status"],
rvu_band=bands.get((kw["band_year"], code), 0),
non_fac_total=float(kw["total"] or 0.0),
delta_pct=float(kw.get("delta", 0.0)),
item_key=anchor.item_key if anchor else "",
p_id=anchor.p_id if anchor else 0,
page=anchor.page if anchor else 0,
anchor_kind=anchor.kind if anchor else "",
correction=bool(anchor is not None and is_corr),
note=anchor.note if anchor else "",
)
)
return sorted(out, key=lambda r: (r.code, r.year, r.kind))
# ── gathering ─────────────────────────────────────────────────────────
def _rule_info(
store: Any, item_keys: Sequence[str]
) -> dict[str, tuple[str, dt.date | None]]:
from bib.frlink import rule_kind
out: dict[str, tuple[str, dt.date | None]] = {}
for key in sorted(set(k for k in item_keys if k)):
title, pub = "", None
try:
item = store.get(key)
title = item.title or ""
raw = (item.date_published or "")[:10]
pub = dt.date.fromisoformat(raw) if raw else None
except Exception: # noqa: BLE001 — unknown item: no kind, no date
pass
try:
kind = rule_kind(store, key, title=title)
except Exception: # noqa: BLE001
kind = "rule"
out[key] = (kind, pub)
return out
def build(
con: Any,
store: Any,
*,
family_of: Callable[[str], str] | None = None,
) -> list[ExposureRow]:
"""The whole calendar from the replica (``pfs.rvu`` + ``pfs.code_event``)
and the bibliography (rule kinds and dates). *family_of* defaults to
the live family registry (``pfs.families.family_of``) after
``refresh_from(con)``."""
series = rvu_series(con)
events = read_all_events(con)
if family_of is None:
from pfs.families import family_of as _family_of
from pfs.families import refresh_from
refresh_from(con)
def family_of(code: str) -> str:
fam = _family_of(code)
return fam.key if fam else ""
keys = [e.item_key for rows in events.values() for e in rows if e.source == "fr"]
info = _rule_info(store, keys)
return derive(
series, events, series_span(series), family_of=family_of, rule_info=info
)
# ── controls ──────────────────────────────────────────────────────────
def control_codes(
con: Any, code: str, year: int, *, window: int = 2, limit: int = 0
) -> list[tuple[str, str, str, int]]:
"""Never-treated comparison codes for *code*'s exposure in *year*:
payable that year with the same RVU status and decile band, and no
exposure of their own within ``year ± window``. Returns
``(code, family, status, band)`` rows, cheapest first. Empty when the
(code, year) has no exposure row."""
target = con.execute(
"SELECT status, rvu_band FROM pfs.code_exposure WHERE code = ? AND year = ? "
"ORDER BY CASE kind WHEN 'becomes-payable' THEN 0 WHEN 'ends' THEN 1 ELSE 2 END LIMIT 1",
[code.upper(), year],
).fetchone()
if not target:
return []
status, band = target
band_year = year
if (
not status
): # an `ends` row: the code is absent in *year*; match on the prior year
band_year = year - 1
row = con.execute(
"SELECT prior_status FROM pfs.code_exposure WHERE code = ? AND year = ? AND kind = 'ends'",
[code.upper(), year],
).fetchone()
status = row[0] if row else status
series = rvu_series(con)
bands = rvu_bands(series)
exposed = {
r[0]
for r in con.execute(
"SELECT DISTINCT code FROM pfs.code_exposure WHERE year BETWEEN ? AND ?",
[year - window, year + window],
).fetchall()
}
fam = {
r[0]: r[1]
for r in con.execute(
"SELECT code, min(key) FROM pfs.code_family GROUP BY code"
).fetchall()
}
out: list[tuple[float, str, str, str, int]] = []
for c, rows in series.items():
if c == code.upper() or c in exposed:
continue
for y, s, t in rows:
if y == band_year and s == status and bands.get((y, c)) == band:
out.append((t, c, fam.get(c, ""), s, band))
out.sort()
result = [(c, f, s, b) for _, c, f, s, b in out]
return result[:limit] if limit else result

View File

@@ -1017,3 +1017,82 @@ class TestReview:
res = runner.invoke(app, ["pfs", "review", "--code", "g0556"])
assert res.exit_code == 0, res.output
assert "Odd line" in res.output and "Other line" not in res.output
class TestExposure:
def test_write_builds_whole_calendar_and_publishes(self, con, monkeypatch):
from pfs.codetables import read_exposures
res = runner.invoke(app, ["pfs", "exposure", "--write"])
assert res.exit_code == 0, res.output
rows = read_exposures(con)
got = {(r.code, r.year, r.kind) for r in rows}
# 99490 appears (payable) in 2026 after being absent; G2058's only year is
# 2020 so it ends 2021; 99999 is B-status → never payable → no rows.
assert ("99490", 2026, "becomes-payable") in got
assert ("G2058", 2021, "ends") in got
assert not any(r.code == "99999" for r in rows)
assert con.published == [True]
assert "99490 2026-01-01 becomes-payable" in res.output
assert "exposures:" in res.output
def test_filters_only_affect_printing(self, con, monkeypatch):
from pfs.codetables import read_exposures
res = runner.invoke(app, ["pfs", "exposure", "--write", "--code", "g2058"])
assert res.exit_code == 0, res.output
assert len({r.code for r in read_exposures(con)}) == 2 # whole calendar written
assert "G2058" in res.output and "99490" not in res.output
def test_without_write_reads_and_never_takes_the_write_lock(self, con, monkeypatch):
runner.invoke(app, ["pfs", "exposure", "--write"])
con.published.clear()
def fail_batch():
raise AssertionError("must not open a RW duckdb_batch connection")
monkeypatch.setattr(pfs_cli, "_batch", fail_batch)
res = runner.invoke(app, ["pfs", "exposure", "--code", "99490"])
assert res.exit_code == 0, res.output
assert "99490 2026-01-01 becomes-payable" in res.output
assert con.published == []
def test_controls_lists_quiet_codes_in_the_same_band(self, con, monkeypatch):
# two more A-status codes in 2026 in the same (single) band, one of
# which is itself exposed that year → only the quiet one is a control
con.executemany(
"INSERT INTO pfs.rvu VALUES (?,?,?,?,?,?)",
[
# 11111 is payable across the whole file span → never treated
*[("11111", None, "quiet", "A", 1.0, y) for y in range(2020, 2027)],
("22222", None, "new", "A", 1.0, 2026), # becomes payable 2026 itself
],
)
runner.invoke(app, ["pfs", "exposure", "--write"])
res = runner.invoke(
app,
["pfs", "exposure", "--code", "99490", "--year", "2026", "--controls", "5"],
)
assert res.exit_code == 0, res.output
assert "11111 A band" in res.output
assert "22222" not in res.output
assert "controls for 99490 @ 2026" in res.output
def test_controls_requires_one_code_and_year(self, con):
res = runner.invoke(app, ["pfs", "exposure", "--controls", "5"])
assert res.exit_code != 0
def test_no_calendar_yet_is_a_friendly_no_op(self, monkeypatch):
bare = duckdb.connect(":memory:")
monkeypatch.setattr(pfs_cli, "_read", lambda: bare)
def fail_batch():
raise AssertionError("must not open a RW duckdb_batch connection")
monkeypatch.setattr(pfs_cli, "_batch", fail_batch)
try:
res = runner.invoke(app, ["pfs", "exposure"])
assert res.exit_code == 0, res.output
assert "no exposure calendar yet" in res.output
finally:
bare.close()

View File

@@ -39,7 +39,7 @@ def test_cells_are_anonymous_and_banners_present():
names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef)]
assert names and set(names) == {"_"}
banners = re.findall(r"# ── (\S+)\. ", src)
assert banners == ["0", "1", "2", "3", "4", "5", "6", "7", "7b", "8"]
assert banners == ["0", "1", "2", "3", "4", "5", "6", "7", "7b", "7c", "8"]
def test_headless_run_degrades_without_data(monkeypatch, tmp_path):

382
tests/pfs/test_exposure.py Normal file
View File

@@ -0,0 +1,382 @@
"""pfs.exposure — the exposure calendar derived from lineage + RVU status (#693)."""
from __future__ import annotations
import datetime as dt
import sqlite3
import duckdb
import pytest
from pfs.codetables import (
EventRow,
ExposureRow,
ensure_tables,
read_exposures,
write_events,
write_exposures,
)
from pfs.exposure import (
KINDS,
build,
control_codes,
derive,
rvu_bands,
rvu_series,
)
RVU_COLS = "hcpcs VARCHAR, mod VARCHAR, description VARCHAR, status_code VARCHAR, non_fac_total DOUBLE, year INTEGER"
def _fr(code, year, kind, item_key, p_id, page=1, from_codes="", to_codes=""):
return EventRow(
code, year, kind, from_codes, to_codes, item_key, p_id, page, "fr", True, ""
)
def _rvu(code, year, kind, note=""):
return EventRow(code, year, kind, "", "", "", 0, 0, "rvu", True, note)
@pytest.fixture
def con():
c = duckdb.connect(":memory:")
ensure_tables(c)
c.execute(f"CREATE TABLE pfs.rvu ({RVU_COLS})")
rows = [
# span 2015..2026; ten filler payable codes give the deciles something to rank
*[
(f"F{i:04d}", None, "filler", "A", float(i), y)
for i in range(1, 11)
for y in range(2015, 2027)
],
# 99490 payable from the first year (left-censored) — FR says created 2015
*[("99490", None, "ccm", "A", 1.2, y) for y in range(2015, 2027)],
# 99487: B 2015-16, A from 2017; +47% revalue 2022
("99487", None, "cplx", "B", 0.0, 2015),
("99487", None, "cplx", "B", 0.0, 2016),
*[("99487", None, "cplx", "A", 2.6, y) for y in range(2017, 2022)],
*[("99487", None, "cplx", "A", 3.9, y) for y in range(2022, 2027)],
# G2058: 2020 only → ends 2021 (replaced by 99439)
("G2058", None, "ccm addl", "A", 1.05, 2020),
*[("99439", "", "ccm addl", "A", 1.1, y) for y in range(2021, 2027)],
# 99441: N through 2020, A 2021-2024, gone 2025
*[("99441", None, "phone", "N", 0.4, y) for y in range(2015, 2021)],
*[("99441", None, "phone", "A", 1.6, y) for y in range(2021, 2025)],
# 12345: A → R (both payable) in 2019, R → C (non-payable) in 2023
*[("12345", None, "x", "A", 5.0, y) for y in range(2015, 2019)],
*[("12345", None, "x", "R", 5.0, y) for y in range(2019, 2023)],
*[("12345", None, "x", "C", 0.0, y) for y in range(2023, 2027)],
# modifier rows are ignored
("99487", "26", "modifier row ignored", "A", 99.0, 2022),
]
c.executemany("INSERT INTO pfs.rvu VALUES (?,?,?,?,?,?)", rows)
write_events(
c,
"99490",
[
_fr("99490", 2015, "adopted_cpt", "DE2VH9PD", 1251, 67716),
_fr("99490", 2015, "created", "6N8FPFHF", 49, 14854),
_fr("99490", 2016, "created", "VZW3HXDA", 1181),
],
)
write_events(
c,
"99487",
[
_rvu("99487", 2017, "status_change", "B→A"),
_rvu("99487", 2022, "revalued", "+47.5%"),
_fr("99487", 2022, "crosswalk", "JE7KYBW3", 900),
],
)
write_events(
c,
"G2058",
[
_rvu("G2058", 2021, "disappeared"),
_fr("G2058", 2021, "replaced_by", "YBM4IZUS", 1578, to_codes="99439"),
],
)
write_events(
c,
"99439",
[
_rvu("99439", 2021, "appeared"),
_fr("99439", 2021, "created", "YBM4IZUS", 686),
_fr("99439", 2021, "replaces", "YBM4IZUS", 1578, from_codes="G2058"),
],
)
write_events(
c,
"99441",
[
_fr("99441", 2022, "telehealth_list", "JE7KYBW3", 555),
_fr("99441", 2023, "telehealth_list", "MZ24MX5S", 336),
_fr("99441", 2023, "telehealth_list", "PETM225D", 458),
_fr("99441", 2025, "deleted", "XFGGRBDH", 489),
_fr("99441", 2025, "bundled", "JJ6AM5HJ", 765),
# a lone correction-only anchor: only a correction notice mentions this
_fr("99441", 2021, "created", "CORR2021", 7),
],
)
write_events(c, "12345", [])
yield c
c.close()
class _Store:
def __init__(self):
self.con = sqlite3.connect(":memory:")
self.con.row_factory = sqlite3.Row
self.con.executescript(
"CREATE TABLE items (key TEXT PRIMARY KEY, title TEXT, date_published TEXT);"
"CREATE TABLE fr_anchors (item_key TEXT, p_id INTEGER, page INTEGER, ordinal INTEGER, text TEXT);"
)
self.con.executemany(
"INSERT INTO items VALUES (?,?,?)",
[
("DE2VH9PD", "Medicare Program; CY 2015 PFS Final Rule", "2014-11-13"),
("6N8FPFHF", "Medicare Program; CY 2015 PFS; Correction", "2015-03-20"),
("VZW3HXDA", "CY 2016 PFS Proposed Rule", "2015-07-15"),
("JE7KYBW3", "CY 2022 PFS Final Rule", "2021-11-19"),
("YBM4IZUS", "CY 2021 PFS Final Rule", "2020-12-28"),
("MZ24MX5S", "CY 2023 PFS Final Rule", "2022-11-18"),
("PETM225D", "CY 2023 PFS Proposed Rule", "2022-07-29"),
("XFGGRBDH", "CY 2025 PFS Proposed Rule", "2024-07-31"),
("JJ6AM5HJ", "CY 2025 PFS Final Rule", "2024-12-09"),
("CORR2021", "CY 2021 PFS Final Rule; Correction", "2021-01-19"),
],
)
def _con(self):
return self.con
def get(self, key):
row = self.con.execute("SELECT * FROM items WHERE key=?", (key,)).fetchone()
if row is None:
raise KeyError(key)
class _I:
title = row["title"]
date_published = row["date_published"]
return _I()
def close(self):
self.con.close()
@pytest.fixture
def store():
s = _Store()
yield s
s.close()
def _by(rows, code, kind=None):
return [r for r in rows if r.code == code and (kind is None or r.kind == kind)]
class TestSeriesAndBands:
def test_series_ignores_modifier_rows(self, con):
s = rvu_series(con)
assert [y for y, _, _ in s["99487"]] == list(range(2015, 2027))
assert all(t != 99.0 for _, _, t in s["99487"])
def test_bands_are_deciles_among_payable_codes(self, con):
s = rvu_series(con)
b = rvu_bands(s)
# ten fillers 1..10 plus the real codes; the cheapest is band 1, dearest band 10
assert b[(2020, "F0001")] == 1
# 14 payable codes in 2020; 12345 (5.0) sorts 8th (ties break on code) → 1 + 7*10//14
assert b[(2020, "12345")] == 6
assert b[(2020, "F0010")] == 10
assert (2016, "99487") not in b # B status is not payable → no band
def test_ties_share_a_band(self):
series = {c: [(2020, "A", 1.0)] for c in ("A0001", "A0002", "A0003")}
series["B0001"] = [(2020, "A", 2.0)]
b = rvu_bands(series)
assert b[(2020, "A0001")] == b[(2020, "A0002")] == b[(2020, "A0003")] == 1
assert b[(2020, "B0001")] == 8 # index 3 of 4 → 1 + 30//4
class TestDerive:
def test_kinds_are_closed(self):
assert KINDS == (
"becomes-payable",
"revalued",
"status-change",
"ends",
"telehealth-listed",
)
def test_becomes_payable_from_status_flip_and_appearance(self, con, store):
rows = build(con, store)
r = _by(rows, "99487", "becomes-payable")
assert (
len(r) == 1
and r[0].year == 2017
and r[0].prior_status == "B"
and r[0].status == "A"
)
assert r[0].effective_date == dt.date(2017, 1, 1)
r = _by(rows, "99439", "becomes-payable")
assert len(r) == 1 and r[0].year == 2021 and r[0].prior_status == ""
# anchor: kind-matched (created/replaces) from the same year, lowest p_id
assert (r[0].item_key, r[0].p_id, r[0].anchor_kind) == (
"YBM4IZUS",
686,
"created",
)
def test_left_censored_first_year_needs_fr_evidence(self, con, store):
rows = build(con, store)
r = _by(rows, "99490", "becomes-payable")
assert len(r) == 1 and r[0].year == 2015
# the final rule's adopted_cpt anchor wins over the correction notice
assert r[0].item_key == "DE2VH9PD" and r[0].correction is False
assert r[0].effective_date == dt.date(2015, 1, 1)
# filler codes are payable in 2015 too but have no FR evidence → no row
assert not _by(rows, "F0001")
def test_ends_on_disappearance_and_on_non_payable_flip(self, con, store):
rows = build(con, store)
r = _by(rows, "G2058", "ends")
assert (
len(r) == 1
and r[0].year == 2021
and r[0].prior_status == "A"
and r[0].status == ""
)
assert (r[0].item_key, r[0].anchor_kind, r[0].p_id) == (
"YBM4IZUS",
"replaced_by",
1578,
)
r = _by(rows, "99441", "ends")
assert len(r) == 1 and r[0].year == 2025 and r[0].anchor_kind == "deleted"
r = _by(rows, "12345", "ends")
assert (
len(r) == 1
and r[0].year == 2023
and r[0].prior_status == "R"
and r[0].status == "C"
)
assert r[0].item_key == "" and r[0].p_id == 0 # no FR mention at all
def test_status_change_only_between_payable_statuses(self, con, store):
rows = build(con, store)
r = _by(rows, "12345", "status-change")
assert [(x.year, x.prior_status, x.status) for x in r] == [(2019, "A", "R")]
assert not _by(
rows, "99441", "status-change"
) # N→A is becomes-payable, not status-change
def test_revalued_carries_delta_and_nearest_anchor(self, con, store):
rows = build(con, store)
r = _by(rows, "99487", "revalued")
assert len(r) == 1 and r[0].year == 2022
assert r[0].delta_pct == pytest.approx(0.475)
assert r[0].item_key == "JE7KYBW3" and r[0].anchor_kind == "crosswalk"
def test_telehealth_listed_once_per_run_of_years(self, con, store):
rows = build(con, store)
r = _by(rows, "99441", "telehealth-listed")
assert [x.year for x in r] == [2022] # 2023 continues the 2022 run
assert r[0].item_key == "JE7KYBW3" and r[0].p_id == 555
def test_correction_only_anchor_moves_the_effective_date(self, con, store):
rows = build(con, store)
r = _by(rows, "99441", "becomes-payable")
assert len(r) == 1 and r[0].year == 2021
assert r[0].correction is True and r[0].item_key == "CORR2021"
assert r[0].effective_date == dt.date(2021, 1, 19)
def test_neighbouring_year_correction_keeps_january_first(self, con, store):
# 20982-style: a 2016 revalue whose only FR mention is the CY2015
# correction notice (±1 year) stays dated 2016-01-01.
write_events(
con,
"55555",
[
_rvu("55555", 2016, "revalued", "+20.0%"),
_fr("55555", 2015, "created", "6N8FPFHF", 3),
],
)
con.executemany(
"INSERT INTO pfs.rvu VALUES (?,?,?,?,?,?)",
[
("55555", None, "x", "A", 1.0, 2015),
("55555", None, "x", "A", 1.2, 2016),
],
)
rows = build(con, store)
r = _by(rows, "55555", "revalued")[0]
assert r.item_key == "6N8FPFHF" and r.correction is True
assert r.effective_date == dt.date(2016, 1, 1)
# and its 2015 becomes-payable IS dated by the correction (same year)
assert _by(rows, "55555", "becomes-payable")[0].effective_date == dt.date(
2015, 3, 20
)
def test_bands_and_family_carried(self, con, store):
rows = build(
con,
store,
family_of=lambda c: (
"CCM" if c in {"99490", "99487", "99439", "G2058"} else ""
),
)
r = _by(rows, "99487", "becomes-payable")[0]
assert r.family == "CCM" and r.rvu_band >= 1 and r.non_fac_total == 2.6
e = _by(rows, "G2058", "ends")[0]
assert e.non_fac_total == 1.05 and e.rvu_band >= 1 # from the last payable year
assert _by(rows, "12345", "ends")[0].family == ""
def test_rows_sorted_and_unique(self, con, store):
rows = build(con, store)
keys = [(r.code, r.year, r.kind) for r in rows]
assert len(keys) == len(set(keys))
assert keys == sorted(keys)
def test_derive_is_pure(self):
series = {"X1234": [(2015, "A", 1.0), (2016, "A", 1.0), (2017, "N", 0.0)]}
rows = derive(series, {}, (2015, 2017), family_of=lambda c: "", rule_info={})
assert [(r.kind, r.year) for r in rows] == [("ends", 2017)]
class TestPersist:
def test_write_and_read_roundtrip(self, con, store):
rows = build(con, store)
n = write_exposures(con, rows)
assert n == len(rows) > 0
back = read_exposures(con)
assert back == rows
assert read_exposures(con, code="99487") == _by(rows, "99487")
assert all(isinstance(r, ExposureRow) for r in back)
write_exposures(con, rows) # idempotent full replace
assert len(read_exposures(con)) == n
class TestControls:
def test_controls_match_status_and_band_outside_window(self, con, store):
rows = build(con, store)
write_exposures(con, rows)
# 99487 becomes payable 2017 (A, band by 2.6). Controls: A-status codes in the
# same band that year with no exposure in 2015..2019.
ctrl = control_codes(con, "99487", 2017, window=2)
codes = {c for c, *_ in ctrl}
assert "99487" not in codes
assert (
"99490" not in codes or True
) # 99490's only exposure is 2015 → outside window, allowed
assert all(status == "A" for _, _, status, _ in ctrl)
band = _by(rows, "99487", "becomes-payable")[0].rvu_band
assert all(b == band for *_, b in ctrl)
# 12345 has a 2019 status-change → inside the window → excluded even if same band
assert "12345" not in codes
def test_unknown_code_year_is_empty(self, con, store):
write_exposures(con, build(con, store))
assert control_codes(con, "ZZZZZ", 2017) == []