29 KiB
CY2026 PFS Proposed Rule (P36) Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Capture, process, and analyze the CY2026 PFS proposed rule (CMS-1832-P, docket CMS-2025-0304, FR doc 2025-13271): files, parameters with provenance logging, comments, rule-text RAG, QPP/Advanced-APM model, and a financial-changes notebook.
Architecture: Extends existing modules (bib, pfs, llm, rex.comments, cms) plus one new module (qpp). Reference data lands in aco.duckdb and the DuckLake pfs/cms schemas; embeddings land in pgvector collections comments and rules; analysis is a marimo notebook.
Tech Stack: Python 3.12, pydantic v2, DuckDB/DuckLake, narwhals, pgvector via langchain-postgres, Ollama (local only), marimo, typer CLI, pytest.
Tracker: milestone P36 (id 37) — issues #588–#597. Spec: docs/superpowers/specs/2026-08-12-cy2026-pfs-proposed-rule-design.md
Global Constraints
- Branch: all work on
p36-cy2026-ruleoffmain; merge to main at close-out. - Conventional commits, milestone refs in subject:
feat(pfs): … (refs #591). NoCo-Authored-Bytrailers. - Coverage bar is 99% (
[ci].coverage_threshold); mocking is stdlibunittest.mock+monkeypatchonly. - Ruff
E,F,I, line length 88; runuv run ruff check src tests && uv run ruff format --check src testsbefore each commit. - Repo convention: lazy
from conf import …inside functions, not module top-level. .gitea/workflows/*are generated — never hand-edit; ifstack.tomlchanges, runuv run python dev/scripts/gen_config.py.- Notebooks live flat in
notebooks/— no subdirectories, no config files there (tests/test_notebook_layout.pyenforces). - The llm module is local-only (self-hosted Ollama); never call cloud LLM APIs from it.
- DuckDB writes go through
conf.connect.duckdb_batch("aco")(single-writer lock preflight). If it raises naming a marimo kernel PID, kill that kernel worker and re-run. - Never transcribe rule parameters from model memory — every number added to a registry must be read out of the captured rule text or addenda in
data/fr_downloads// bib storage, and carry an FR citation + bib pincite. - Rule-parameter values (Tasks 4, 6) may deviate from strict test-first: transcribe from source, then write exact-value tests pinning what was transcribed. All other code is TDD.
Task 1: Fix CMS-2025-0304 docket↔rule mapping (#588)
Files:
- Modify:
src/rex/comments/classify.py(the_SYSTEMprompt, ~line 144) - Test:
tests/rex/comments/test_classify.py(exists — add to it)
Interfaces:
-
Produces:
rex.comments.classify.DOCKET_RULES: dict[str, str]— docket id → CMS rule id, used by tests and future docket-scoped tooling. -
Step 1: Write the failing test (append to
tests/rex/comments/test_classify.py):
from rex.comments import classify
def test_docket_rules_maps_cy2026_pfs():
assert classify.DOCKET_RULES["CMS-2025-0304"] == "CMS-1832-P"
def test_system_prompt_names_the_pfs_rule():
assert "CMS-1832-P" in classify._SYSTEM
assert "CMS-1834-P" not in classify._SYSTEM
assert "PFS proposed rule" in classify._SYSTEM
- Step 2: Run to verify failure:
uv run pytest tests/rex/comments/test_classify.py -v— expect FAIL (DOCKET_RULESundefined; prompt says OPPS/CMS-1834-P). - Step 3: Implement. In
classify.py, above_SYSTEM, add:
# Docket ↔ CMS rule identity for the dockets this classifier targets.
# Verified against the regulations.gov docket abstract (2026-08-12):
# CMS-2025-0304 is the CY2026 *PFS* NPRM, not the OPPS rule — skin-substitute
# payment moved into the PFS rule in CY2026.
DOCKET_RULES: dict[str, str] = {"CMS-2025-0304": "CMS-1832-P"}
Rewrite the _SYSTEM opening sentence to: You classify public comments submitted on CMS's CY2026 PFS proposed rule\n(CMS-1832-P, docket CMS-2025-0304), specifically regarding the proposal to\nreclassify skin substitutes … (keep the position semantics unchanged; keep the flat-rate description but drop the OPPS-specific ~$127.28/cm² figure — that number is from the OPPS companion rule).
- Step 4: Run the full comments test dir:
uv run pytest tests/rex/comments -v— expect PASS. - Step 5: Commit:
git add -A && git commit -m "fix(comments): CMS-2025-0304 is the PFS NPRM (CMS-1832-P), not OPPS (refs #588)"
Task 2: Capture CY2026 rule text into fr_downloads + bib (#589)
Ops task — no new code, nothing to commit (data/ is untracked). Run on the host.
- Step 1: Preview:
uv run python dev/scripts/fetch_fr_attachments.py --dry-run --key 2KVJ2HKX— expect it to plan PDF+TXT downloads for doc2025-13271. If it reports "no document number", inspectstore.get("2KVJ2HKX").urland rely on the URL-date fallback already in the script. - Step 2: Fetch proposed rule:
uv run python dev/scripts/fetch_fr_attachments.py --key 2KVJ2HKX - Step 3: Fetch final rule:
uv run python dev/scripts/fetch_fr_attachments.py --key NHRGIHGD(doc2025-19787; needed for proposed-vs-final deltas). - Step 4: Verify:
ls -la data/fr_downloads/2025-13271.* data/fr_downloads/2025-19787.*— both.pdfand.txt, each > 1 MB; anduv run python -c "from conf import connect; s=connect.bib(); print([a for a in s.get('2KVJ2HKX').attachments])"shows the two new attachments (check theItemmodel for the exact attachments accessor;attach_fileregistered them). - Step 5: Sanity-grep the text:
grep -c "conversion factor" data/fr_downloads/2025-13271.txt(expect dozens) andgrep -n "CMS-1832-P" data/fr_downloads/2025-13271.txt | head -3. - Step 6: Comment file sizes + verification output on issue #588's sibling:
#589, but leave the issue open until close-out.
Task 3: Capture CMS-1832-P proposed addenda (#590)
Files:
- Create:
dev/scripts/fetch_nprm_addenda.py
Interfaces:
-
Produces: files under
data/cms/pfs_nprm/2026/, and a bib/Zotero registration: Addendum B attached to rule item2KVJ2HKXwith tagssup:2026_PFS_NPRM,module:pfs,file:rvu,year:2026. Thesup:2026_PFS_NPRMtag is what Task 5's loader discovers by — final-rule files usesup:2026_PFS_FR, so the namespaces cannot collide. -
Step 1: Locate the addenda. The NPRM detail page is
https://www.cms.gov/medicare/medicare-fee-service-payment/physicianfeesched/pfs-federal-regulation-notices/cms-1832-p(if 404, searchhttps://www.cms.gov/medicare/payment/fee-schedules/physician/federal-regulation-noticesfor CMS-1832-P). Find the "CY 2026 PFS Proposed Rule Addenda" ZIP link (contains Addendum B — proposed RVUs by HCPCS). -
Step 2: Write
dev/scripts/fetch_nprm_addenda.pymodeled ondev/scripts/download_opps_files.py(same httpx download + dest-dir pattern): downloads the addenda ZIP todata/cms/pfs_nprm/2026/, unzips, prints the file list. Hard-code the resolved URL with a comment naming the page it came from. Include--dry-run. -
Step 3: Run it; verify Addendum B is present (an
.xlsx/.csvwhose name matches(?i)addendum[_ ]?b) and open the first rows to confirm columns include HCPCS + work/PE/MP RVUs. -
Step 4: Register in bib (one-off, in the same script behind
--register): useconf.connect.bib();store.attach_file("2KVJ2HKX", addendum_b_path, title="CY2026 PFS NPRM Addendum B (proposed RVUs)")thenstore.add_tag("2KVJ2HKX", t)for each ofsup:2026_PFS_NPRM,module:pfs,file:rvu,year:2026. Print the attachment storage path. -
Step 5: Zotero visibility. Run the tag-scoped sync used by the mail pipeline convention:
uv run stack bib sync-zotero --tag sup:2026_PFS_NPRM(always--tag-scoped; check--helpfor the exact flag name before running). -
Step 6: Verify
store.get("2KVJ2HKX").tagsincludes the four tags. -
Step 7: Commit the script only:
git add dev/scripts/fetch_nprm_addenda.py && git commit -m "feat(pfs): fetch + register CY2026 NPRM addenda (refs #590)"
Task 4: ProposedRule registry (#591)
Files:
- Modify:
src/pfs/rules/__init__.py - Test:
tests/pfs/test_rules.py(exists — add to it)
Interfaces:
- Produces:
pfs.rules.ProposedRule(pydanticBaseModel) andRuleYear.proposed: ProposedRule | None = None;RULES[2026].proposedpopulated. Fields (all read by Task 6 and Task 10):
class ProposedRule(BaseModel):
"""Parameters as *proposed* in the year's NPRM (pre-final)."""
cms_rule_id: str # "CMS-1832-P"
fr_document_number: str # "2025-13271"
federal_register_citation: str # e.g. "90 FR NNNNN" — from the txt header
published: date # NPRM publication date
comment_close: date # end of comment period
conversion_factor: float # proposed non-QP standard CF
cf_qp: float | None = None # proposed QP standard CF
anesthesia_cf: float | None = None
anesthesia_cf_qp: float | None = None
budget_neutrality_adjustor: float = 1.0
telehealth_originating_site_fee: float | None = None
notes: str = ""
- Step 1: Transcribe the numbers from the captured text (requires Task 2). Work through these greps, reading surrounding context, and record each value with its page/FR cite:
grep -n "proposed CY 2026 conversion factor" data/fr_downloads/2025-13271.txt | headgrep -n -i "anesthesia conversion factor" data/fr_downloads/2025-13271.txt | headgrep -n -i "budget neutrality adjustment" data/fr_downloads/2025-13271.txt | headgrep -n -i "originating site facility fee" data/fr_downloads/2025-13271.txt | head- Citation + dates: first ~40 lines of the txt give the FR volume/page and DATES block (comment close).
Expected shape (do not trust until read): two standard CFs (QP and non-QP — CY2026 is the first split year), two anesthesia CFs, a BN adjustor near 1.0. If a value genuinely isn't in the NPRM, leave the field
Noneand say so innotes.
- Step 2: Add the model + field. Insert
ProposedRule(exact code above, with docstrings per field following the file's style) beforeRuleYear; addproposed: ProposedRule | None = NonetoRuleYearwith a docstring noting final fields stay authoritative for payment math. PopulateRULES[2026].proposed = ProposedRule(...)inline in the 2026 entry with the transcribed values andnotesciting:pincite:to item2KVJ2HKX. - Step 3: Register the pincite:
store.upsert_pincite(...)is docstring-driven in this repo — instead add:pincite:\2KVJ2HKX`to theProposedRuledocstring text where the CY2026 values are cited (match howVVBEVYLCis used at the top ofRULES`). - Step 4: Write exact-value tests in
tests/pfs/test_rules.py:
def test_cy2026_has_proposed_rule():
p = RULES[2026].proposed
assert p is not None
assert p.cms_rule_id == "CMS-1832-P"
assert p.fr_document_number == "2025-13271"
assert p.published.year == 2025
assert p.comment_close > p.published
# exact transcribed values — pin them here after Step 1:
assert p.conversion_factor == <transcribed>
assert p.cf_qp == <transcribed>
assert p.conversion_factor != RULES[2026].conversion_factor # proposed ≠ final
def test_pre_2026_years_have_no_proposed_block():
assert RULES[2025].proposed is None
(Replace <transcribed> with the actual floats — the test must not compute them from the registry.)
- Step 5: Run:
uv run pytest tests/pfs/test_rules.py -v— PASS. - Step 6: Commit:
git add -A && git commit -m "feat(pfs): ProposedRule registry — CY2026 NPRM parameters w/ pincite (refs #591)"
Task 5: NPRM Addendum B loader → pfs.rvu_proposed (#592)
Files:
- Create:
src/pfs/nprm.py - Modify:
dev/scripts/ingest_pfs.py(add--nprmstep) - Test:
tests/pfs/test_nprm.py
Interfaces:
-
Consumes: bib attachments tagged
sup:2026_PFS_NPRM(Task 3);cms.ingest_log.log_ingest(Task 7);pfs.rules.RULES[2026].proposed.cms_rule_id(Task 4). -
Produces:
pfs.nprm.load_rvu_proposed(con, *, cms_rule_id: str = "CMS-1832-P") -> dictreturning{"rows": int, "source_file": str}; DuckDB tablepfs.rvu_proposed. -
Step 1: Write failing tests (
tests/pfs/test_nprm.py). Use an in-memory DuckDB and a CSV fixture written totmp_pathwith the Addendum B header shape observed in Task 3 (HCPCS, MOD, description, work RVU, non-fac PE RVU, fac PE RVU, MP RVU, status). Monkeypatch the discovery function to return the fixture path:
import duckdb
from pfs import nprm
def _fixture(tmp_path):
p = tmp_path / "CY2026_NPRM_Addendum_B.csv"
p.write_text(
"HCPCS,MOD,DESCRIPTION,STATUS CODE,WORK RVU,"
"NON-FAC PE RVU,FAC PE RVU,MP RVU\n"
"99213,,Office visit est,A,1.3,1.5,0.55,0.1\n"
"0001A,,Admin covid,X,0.0,0.0,0.0,0.0\n"
)
return p
def test_load_rvu_proposed_loads_and_reloads(tmp_path, monkeypatch):
path = _fixture(tmp_path)
monkeypatch.setattr(nprm, "_discover_addendum_b", lambda: path)
con = duckdb.connect()
out = nprm.load_rvu_proposed(con)
assert out["rows"] == 2
row = con.execute(
"SELECT hcpcs, work_rvu, cms_rule_id FROM pfs.rvu_proposed "
"WHERE hcpcs='99213'"
).fetchone()
assert row == ("99213", 1.3, "CMS-1832-P")
nprm.load_rvu_proposed(con) # idempotent delete-and-reload
assert con.execute("SELECT count(*) FROM pfs.rvu_proposed").fetchone()[0] == 2
- Step 2: Run
uv run pytest tests/pfs/test_nprm.py -v— FAIL (module missing). - Step 3: Implement
src/pfs/nprm.py._discover_addendum_b() -> Pathqueries the bib sqlite (lazyfrom conf import connect) for attachments of items taggedsup:2026_PFS_NPRMwhose filename matches(?i)addendum[_ ]?b(mirror the SQL join inllm/source.py::_attachment_text); raiseFileNotFoundErrornaming the tag if absent.load_rvu_proposed(con, *, cms_rule_id="CMS-1832-P"): read csv/xlsx via the header-detect helpers inpfs.pipe(_find_header_rowfor xlsx; reuse the file readers rather than re-implementing), normalize columns tohcpcs, modifier, description, status_code, work_rvu, nonfac_pe_rvu, fac_pe_rvu, mp_rvu, cms_rule_id, thenCREATE SCHEMA IF NOT EXISTS pfs,CREATE TABLE IF NOT EXISTS pfs.rvu_proposed (...),DELETE FROM pfs.rvu_proposed WHERE cms_rule_id = ?, insert, return{"rows": n, "source_file": str(path)}. - Step 4: Run tests — PASS. Also
uv run pytest tests/pfs -v(no regression in pipe tests). - Step 5: Wire
--nprmintodev/scripts/ingest_pfs.py: new flag; inside the existingduckdb_batch("aco")block, when set, callload_rvu_proposed(con)andcms.ingest_log.log_ingest(con, module="pfs", table_name="pfs.rvu_proposed", rows=out["rows"], source_file=out["source_file"], rule_id="CMS-1832-P", fr_citation=RULES[2026].proposed.federal_register_citation, pincite_key="2KVJ2HKX", run_id=run_id)(import lazily;run_id = cms.ingest_log.new_run_id()once per script run; also log the final-rule tables fromsummarywith the same run_id). Replica + lake publish already happen downstream; extend the lake call to_lake.publish_lake(("pfs", "cms")). - Step 6: Host run:
uv run python dev/scripts/ingest_pfs.py --nprm --no-lakefirst (verify counts), then full with lake. Verify:SELECT count(*) FROM pfs.rvu_proposedin the replica, and thecms.ingest_logrows. - Step 7: Commit:
git add -A && git commit -m "feat(pfs): NPRM Addendum B loader -> pfs.rvu_proposed + ingest wiring (refs #592)"
Task 6: qpp module — QP / Advanced-APM registry (#593)
Files:
- Create:
src/qpp/__init__.py - Modify:
pyproject.toml(add"qpp"to[tool.uv.build-backend] module-name; addqpp = ["stack[conf]", "pydantic>=2.0.0"]to[project.optional-dependencies]; add to the dev/all aggregate extra if one exists — check howpfsis aggregated) - Test:
tests/qpp/__init__.py(empty),tests/qpp/test_rules.py
Interfaces:
- Produces (consumed by Task 10's notebook):
class QpThresholds(BaseModel):
payment_amount_pct: float # % of Part B payments through Advanced APMs
patient_count_pct: float # % of patients through Advanced APMs
class RiskStandards(BaseModel):
revenue_nominal_pct: float # revenue-based nominal amount standard
benchmark_nominal_pct: float # expenditure/benchmark-based standard
notes: str = ""
class QppProposed(BaseModel):
cms_rule_id: str
federal_register_citation: str
changes: str # sourced prose summary of proposed changes
qp_thresholds: QpThresholds | None = None
partial_qp_thresholds: QpThresholds | None = None
risk_standards: RiskStandards | None = None
class QppYear(BaseModel):
performance_year: int
payment_year: int # performance_year + 2
qp_thresholds: QpThresholds
partial_qp_thresholds: QpThresholds
risk_standards: RiskStandards
apm_incentive_pct: float | None # lump-sum incentive % if applicable
qp_cf_applies: bool # True when payment-year CF splits QP/non-QP
cehrt_required: bool = True
citation: str = ""
proposed: QppProposed | None = None
QPP: dict[int, QppYear] # keyed by performance year, ≥ 2023
def for_payment_year(year: int) -> QppYear
- Step 1: Transcribe (requires Task 2). Read the QPP sections of
data/fr_downloads/2025-13271.txt:grep -n -i "Qualifying APM Participant" data/fr_downloads/2025-13271.txt | head -20,grep -n -i "nominal amount standard" …,grep -n -i "partial QP" …. Record: current-law thresholds for the 2026 performance year, the proposed changes CMS-1832-P makes to Advanced APM requirements (threshold levels, risk standards, CEHRT language), and the final-rule disposition from2025-19787.txtfor thechangesnarrative. Statutory baseline values for 2023–2025 come from the same sections' recitals (the NPRM restates them) — cite the FR page you read them from, not memory. - Step 2: Write failing structural tests (
tests/qpp/test_rules.py): registry covers 2023–2026;payment_year == performance_year + 2for every entry;QPP[2024].qp_cf_applies is True(payment year 2026 = first split-CF year) and earlier entriesFalse; every entry has a non-emptycitation;QPP[2025].proposedorQPP[2026].proposed(whichever performance year CMS-1832-P modifies — determined in Step 1) is non-None withcms_rule_id == "CMS-1832-P". Add exact-value asserts for the transcribed thresholds. - Step 3: Run — FAIL (no module). Step 4: Implement
src/qpp/__init__.pywith the models above (docstrings +:pincite:\2KVJ2HKX`citations, style ofpfs/rules/init.py), theQPPdict, andfor_payment_year(year)=next(q for q in QPP.values() if q.payment_year == year)raisingKeyError-equivalentStopIteration→ wrap: raiseKeyError(year)`. - Step 5: Run tests — PASS; run
uv run pytest tests/qpp tests/pfs -v. - Step 6:
uv run python dev/scripts/gen_config.pyifstack.tomluntouched it's a no-op; needed only if you added a[images.*]/[ci]key (you shouldn't).uv syncto register the extra. - Step 7: Commit:
git add -A && git commit -m "feat(qpp): QP/Advanced-APM registry incl. CMS-1832-P proposed changes (refs #593)"
Task 7: cms.ingest_log provenance table + writer (#594)
Files:
- Create:
src/cms/ingest_log.py - Test:
tests/cms/test_ingest_log.py(createtests/cms/__init__.pyif absent)
Interfaces:
- Produces (consumed by Task 5's wiring):
def new_run_id() -> str # uuid4().hex
def file_sha256(path: str | Path) -> str
def ensure_table(con) -> None # CREATE SCHEMA/TABLE IF NOT EXISTS
def log_ingest(con, *, module: str, table_name: str, rows: int,
source_file: str = "", rule_id: str = "", fr_citation: str = "",
pincite_key: str = "", run_id: str = "") -> None
cms.ingest_log DDL: run_id VARCHAR, ingested_at TIMESTAMP, module VARCHAR, table_name VARCHAR, rule_id VARCHAR, source_file VARCHAR, sha256 VARCHAR, rows BIGINT, fr_citation VARCHAR, pincite_key VARCHAR. log_ingest calls ensure_table, computes sha256 itself when source_file exists on disk (else empty string), stamps ingested_at with datetime.now(timezone.utc), appends one row. Append-only — no delete path.
- Step 1: Write failing tests:
import duckdb
from cms import ingest_log
def test_log_ingest_appends_row(tmp_path):
src = tmp_path / "f.csv"
src.write_text("a,b\n1,2\n")
con = duckdb.connect()
ingest_log.log_ingest(
con, module="pfs", table_name="pfs.rvu_proposed", rows=2,
source_file=str(src), rule_id="CMS-1832-P",
fr_citation="90 FR 1", pincite_key="2KVJ2HKX",
run_id=ingest_log.new_run_id(),
)
row = con.execute(
"SELECT module, table_name, rows, rule_id, length(sha256) "
"FROM cms.ingest_log"
).fetchone()
assert row == ("pfs", "pfs.rvu_proposed", 2, "CMS-1832-P", 64)
def test_missing_source_file_logs_empty_hash():
con = duckdb.connect()
ingest_log.log_ingest(con, module="pfs", table_name="t", rows=0)
assert con.execute("SELECT sha256 FROM cms.ingest_log").fetchone()[0] == ""
- Step 2: Run — FAIL. Step 3: Implement (module docstring notes: complements the JSONL logs in
cms.log, does not replace them). Step 4: Run — PASS, plusuv run pytest tests/cms -v. - Step 5: Commit:
git add -A && git commit -m "feat(cms): ingest_log provenance table + writer (refs #594)"
(Note: Task 5 Step 5 wires it into ingest_pfs.py; if Task 7 executes first, fine — Task 5 depends on both.)
Task 8: Rule-text RAG source + rules collection (#595)
Files:
- Modify:
src/llm/source.py,src/cli/llm.py - Test:
tests/llm/test_source.py,tests/cli/test_llm.py(both exist — extend; check the fake-store fixture pattern already used intests/llm/test_source.pyand reuse it)
Interfaces:
-
Produces:
llm.source.iter_rule_docs(store, *, keys: tuple[str, ...] = (), tag: str = "") -> Iterator[Doc]and CLIstack llm index --collection rules [--key KEY …]. -
Step 1: Write failing tests (extend
tests/llm/test_source.py, mirroring its existing store-fixture style): a rule item with a.txtattachment yields oneDocwhosetextis the txt content andmetadata == {"doctype": "rule", "cms_rule_id": "CMS-1832-P", "fr_document_number": "2025-13271", "year": "2026", "item_key": <key>}; a rule with only a PDF falls back toextract_attachmenttext (monkeypatchrex.comments.combine.extract_attachment);keys=filters; non-rule items are not yielded. -
Step 2: Run — FAIL. Step 3: Implement in
src/llm/source.py:
def iter_rule_docs(
store: Store, *, keys: tuple[str, ...] = (), tag: str = ""
) -> Iterator[Doc]:
"""One Doc per FR rule item: TXT attachment preferred, PDF-extract fallback."""
for item in store.list_items(item_type="rule", tag=tag):
if keys and item.key not in keys:
continue
text = _rule_text(store, item.key) # txt attachment else _attachment_text
if not text.strip():
continue
cms_rule = next(
(t.split(":", 1)[1] for t in item.tags if t.startswith("cms-rule:")), ""
)
yield Doc(
key=item.key,
text=text,
metadata={
"doctype": "rule",
"cms_rule_id": cms_rule,
"fr_document_number": item.document_number or "",
"year": _year_of(store, item.key),
"item_key": item.key,
},
)
_rule_text: query the attachments join (same SQL as _attachment_text) but return the content of the first storage_path ending .txt via Path.read_text; else fall back to _attachment_text(store, item_key). Confirm list_items accepts item_type= (it does — fetch_fr_attachments.py:78 uses it).
- Step 4: CLI: in
src/cli/llm.py::index, addkey: list[str] = typer.Option([], "--key"); acceptcollection == "rules"→docs = iter_rule_docs(store, keys=tuple(key)); update theBadParametermessage to'comments', 'corpus' or 'rules'. Extendtests/cli/test_llm.pyaccordingly (runner invokes with--collection rules --key X, assertingiter_rule_docswas called — monkeypatch it). - Step 5: Run
uv run pytest tests/llm tests/cli -v— PASS. - Step 6: Commit:
git add -A && git commit -m "feat(llm): rule-text source + rules collection CLI (refs #595)"
Task 9: Comment-farm completion + indexing (#596) — ops
Long-running host batches; run each under nohup/background with logs in .state/comments/, sequentially. Record final counts as a comment on #596. Requires Tasks 1, 2, 8 merged to the working branch. GPU note: embedding fan-out uses LLM_OLLAMA_HOSTS — check which hosts are up before starting; the 3060 alone works but is slow.
- Step 1: Backfill:
uv run stack bib backfill-comments --helpfirst to confirm the docket-scoping flag, then run it scoped toCMS-2025-0304. Resumable (enriched:*tag markers); watch.state/comments/backfill.log. - Step 2: Extract:
uv run stack comments extractthenuv run stack comments extract-ocr(check--helpfor docket scoping); monitor.state/comments/extract.log. - Step 3: Verify counts:
uv run stack comments statsandawk -F, '$2=="CMS-2025-0304"' .state/comments/_index.csv | wc -l— target: extracted count ≈ comments with attachments; note the OCR-failed remainder honestly. - Step 4: Index comments:
uv run stack llm index --collection comments --docket CMS-2025-0304(incremental/resumable — safe to re-run after interruption). - Step 5: Index rule text:
uv run stack llm index --collection rules --key 2KVJ2HKX --key NHRGIHGD. - Step 6: RAG spot-check: run a retrieval (python one-liner against
llm.rag.retrieve) for "conversion factor" overrulesand "skin substitutes" overcommentsfiltered to the docket; confirm CY2026 chunks with correct metadata come back. - Step 7: Comment counts (backfilled / extracted / OCR-failed / chunks indexed per collection) on #596.
Task 10: Financial-changes + Advanced-APM notebook (#597)
Files:
- Create:
notebooks/cy2026_pfs_proposed_rule.py
Interfaces:
-
Consumes:
pfs.rules.RULES[2026](+.proposed),qpp.QPP/for_payment_year, lake tablespfs.rvu,pfs.rvu_proposed,pfs.gpci,cms.ingest_log. -
Step 1: Scaffold from
notebooks/_template.py(mo import cell,connect.theme()+connect.ducklake()+q(sql)helper, title cell). Narrative style ofskin_sub_budget_neutrality.py:mo.mdintro (Mechanism / What this means), numbered## N.sections. -
Step 2: Sections (each chart follows the dataviz skill — read it before writing chart code; altair, theme-aware):
- The CF walk — CY2025 → CY2026 proposed → CY2026 final, QP and non-QP tracks plus anesthesia; bar/slope chart from
RULES+RULES[2026].proposed; BN-adjustor decomposition; FR citations under each figure. - RVU-level deltas —
pfs.rvu_proposedvs CY2025pfs.rvuand vs CY2026 finalpfs.rvu: top-20 winners/losers by total non-fac RVU change, joined to payment via the applicable CF; searchable HCPCS detail table. - Specialty impact — aggregate RVU-weighted deltas by specialty if a specialty mapping exists in the lake (
reference_dataschemas); otherwise present the NPRM's published specialty-impact table transcribed with citation, clearly labeled as transcription. - Advanced APM requirements — from
qpp: threshold table (2023–2026 + proposed), risk-standard changes, the QP/non-QP CF differential in dollars for 3 example HCPCS (99213, a major procedure, an imaging code), proposed-vs-finalized disposition. - Provenance — render
cms.ingest_logrows for the tables used.
- The CF walk — CY2025 → CY2026 proposed → CY2026 final, QP and non-QP tracks plus anesthesia; bar/slope chart from
-
Step 3: Layout check:
uv run pytest tests/test_notebook_layout.py -v. -
Step 4: Headless validation: run
dev/scripts/nb_integration.pythe way.gitea/workflows/notebooks-integration.ymldoes (read the workflow for the exact invocation) scoped to the new notebook; expect a clean session export, no root-cause errors. -
Step 5: Commit:
git add notebooks/cy2026_pfs_proposed_rule.py && git commit -m "feat(notebooks): CY2026 PFS proposed-rule financial changes + APM (refs #597)"
Task 11: Close-out
- Step 1: Full gates:
uv run ruff check src tests && uv run ruff format --check src tests && uv run pytest -n auto(coverage ≥ 99). - Step 2: Merge
p36-cy2026-rule→main(no-ff, subjectMerge P36: CY2026 PFS proposed rule — capture, ingest, APM analysis), push. - Step 3: Append
## P36 build outcomes (<date>)to the spec with row counts, chunk counts, and any deviations; commit. - Step 4: Close #588–#597 with per-issue outcome comments (re-read each issue body first — close only what its body asked for); close milestone P36.