diff --git a/docs/docs/cli/pfs-lineage.md b/docs/docs/cli/pfs-lineage.md index 775343d..b6ec678 100644 --- a/docs/docs/cli/pfs-lineage.md +++ b/docs/docs/cli/pfs-lineage.md @@ -10,9 +10,17 @@ Usage: stack pfs lineage [OPTIONS] Timeline of a code: RVU-file diffs and FR paragraphs, cross-checked. + --all-payable computes lineage for every payable code in ONE pass over + fr_anchors instead of one query per code. + ╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ * --code TEXT [required] │ -│ --write Persist to pfs.code_event and republish. │ -│ --help Show this message and exit. │ +│ --code TEXT │ +│ --all-payable Every A/R/T code in the newest RVU year, one │ +│ inverted pass over fr_anchors. │ +│ --limit INTEGER Only the first N target codes (sorted) — for │ +│ smoke runs with --all-payable. │ +│ [default: 0] │ +│ --write Persist to pfs.code_event and republish. │ +│ --help Show this message and exit. │ ╰──────────────────────────────────────────────────────────────────────────────╯ ``` diff --git a/notebooks/code_families.py b/notebooks/code_families.py index 7b2477e..21d83ec 100644 --- a/notebooks/code_families.py +++ b/notebooks/code_families.py @@ -896,6 +896,109 @@ def _(alt, code, con, mo, not_built, pl): return +@app.cell(hide_code=True) +def _(code, mo, pl): + # ── 7b. What the chat sees ── + from llm.config import load as _load_llm_cfg + from llm.lineage import lineage_evidence as _lineage_evidence + + _cfg = _load_llm_cfg() # replica-only reads — no LLM_DB_PASSWORD, no pool + _ev = _lineage_evidence(f"history of {code}", _cfg) + + _intro = mo.md( + "## 7b. What the chat sees\n\n" + "This is the same timeline the chat itself builds when it answers a " + "question about this code. `llm.lineage.lineage_evidence` reads " + "`pfs.code_event` — populated for every PFS-payable code, not just " + "the codes with hand-built element extractions, by " + "`stack pfs lineage --all-payable --write` — collapses repeated " + "mentions of the same event down to one representative row, labels " + "each surviving row with the rule paragraph (or RVU-file year, or " + "CPT Changes edition) it comes from, and hands the model exactly " + "the bracketed labels shown below in its prompt, so any dated claim " + "in a chat answer can be traced back to the paragraph it came from." + ) + + if _ev is None: + _view = mo.vstack( + [ + _intro, + mo.md( + "_No lineage events for this code — run " + "`stack pfs lineage --all-payable --write`._" + ), + ] + ) + else: + _payload = _ev.payload() + _events = _payload["events"] + + def _codes_cell(e): + if e["from_codes"] or e["to_codes"]: + return ( + f"{e['code']} ({', '.join(e['from_codes'])} → " + f"{', '.join(e['to_codes'])})" + ) + return e["code"] + + def _label_cell(e): + return f"[{e['label']}]({e['url']})" if e["url"] else e["label"] + + _tbl = pl.DataFrame( + { + "Year": [e["year"] for e in _events], + "Event": [e["kind"] for e in _events], + "Codes": [_codes_cell(e) for e in _events], + "Label": [_label_cell(e) for e in _events], + } + ) + + _panels = [ + _intro, + mo.ui.table(_tbl, label=f"lineage_evidence events — {len(_events)} rows"), + mo.accordion( + { + "Prompt block the model sees": mo.md( + f"```\n{_ev.prompt_block()}\n```" + ) + } + ), + ] + + if _payload["element_diffs"]: + _panels.append( + mo.md( + "**Element differences**\n\n" + + "\n".join( + f"- `{d['type']}={d['value']}`: in {', '.join(d['in_codes'])}; " + f"not in {', '.join(d['not_in_codes'])} ({d['label']})" + for d in _payload["element_diffs"] + ) + ) + ) + if _payload.get("elements_note"): + _panels.append( + mo.md( + f"_{_payload['elements_note']} — element diffs cover only the " + "elements-extracted codes until #698 lands._" + ) + ) + if _payload["guidance"]: + _panels.append( + mo.md( + "**Guidance references**\n\n" + + "\n".join( + f"- {g['locator']} — {g['kind'].upper()} ({g['label']})" + for g in _payload["guidance"] + ) + ) + ) + + _view = mo.vstack(_panels) + _view + return + + @app.cell(hide_code=True) def _(NOTES, REPLICA_PATH, con, mo, pl, q, store): # ── 8. Provenance ── diff --git a/tests/notebooks/test_code_families_nb.py b/tests/notebooks/test_code_families_nb.py index 21392eb..8a5f1db 100644 --- a/tests/notebooks/test_code_families_nb.py +++ b/tests/notebooks/test_code_families_nb.py @@ -29,8 +29,8 @@ def test_cells_are_anonymous_and_banners_present(): tree = ast.parse(src) names = [n.name for n in tree.body if isinstance(n, ast.FunctionDef)] assert names and set(names) == {"_"} - banners = re.findall(r"# ── (\d)\. ", src) - assert [int(b) for b in banners] == list(range(0, 9)) + banners = re.findall(r"# ── (\S+)\. ", src) + assert banners == ["0", "1", "2", "3", "4", "5", "6", "7", "7b", "8"] def test_headless_run_degrades_without_data(monkeypatch, tmp_path):