llm: chat and index use the derived code families (pfs.code_family) — refresh at service start, scale detect_codes/family_of, curate synonyms #699

Closed
opened 2026-09-09 17:59:21 +00:00 by kert · 3 comments
Owner

Follow-up from the P49 slice-1 whole-branch review (spec docs/superpowers/specs/2026-09-09-code-family-longitudinal-design.md §Components pfs/families.py).

Gap. Slice 1 derives families into pfs.code_family and exposes pfs.families.refresh_from(con) (in-place refresh of the live FAMILIES dict, hand families kept as a minimum), but the only callers are the stack pfs CLI. llm/evidence.py (detect_codes) and llm/chunk.py (find_codes) still see the five hand families, so the chat never benefits from the derived registry. The spec's original wording (load at import when the replica is present) conflicts with the global constraint that pfs.* must not touch DuckDB at import; the module docstring now says so.

Work.

  1. Call refresh_from(con) at llm service start (the replica handle in llm/evidence.py already exists) and on replica mtime change.
  2. Scale the lookups: detect_codes compiles one regex per synonym per family (≈32k with ~16k derived families) and family_of is a linear scan — build a code→family index and a single alternation regex (or a trie) at refresh time.
  3. Curate synonyms: derived keys such as GENE, ADM, REVERSE must not become chat triggers; only hand families and reviewed derived families get synonyms (a synonyms column or an allow-list), the rest match by code only.
  4. Golden test: "history of CCM" expands to the hand codes + G2058; a question containing the word "gene" does not expand to any family.

Blocks: #691 (longitudinal chat).

Follow-up from the P49 slice-1 whole-branch review (spec docs/superpowers/specs/2026-09-09-code-family-longitudinal-design.md §Components `pfs/families.py`). **Gap.** Slice 1 derives families into `pfs.code_family` and exposes `pfs.families.refresh_from(con)` (in-place refresh of the live `FAMILIES` dict, hand families kept as a minimum), but the only callers are the `stack pfs` CLI. `llm/evidence.py` (`detect_codes`) and `llm/chunk.py` (`find_codes`) still see the five hand families, so the chat never benefits from the derived registry. The spec's original wording (load at import when the replica is present) conflicts with the global constraint that `pfs.*` must not touch DuckDB at import; the module docstring now says so. **Work.** 1. Call `refresh_from(con)` at `llm` service start (the replica handle in `llm/evidence.py` already exists) and on replica mtime change. 2. Scale the lookups: `detect_codes` compiles one regex per synonym per family (≈32k with ~16k derived families) and `family_of` is a linear scan — build a code→family index and a single alternation regex (or a trie) at refresh time. 3. Curate synonyms: derived keys such as `GENE`, `ADM`, `REVERSE` must not become chat triggers; only hand families and reviewed derived families get synonyms (a `synonyms` column or an allow-list), the rest match by code only. 4. Golden test: "history of CCM" expands to the hand codes + G2058; a question containing the word "gene" does not expand to any family. Blocks: #691 (longitudinal chat).
kert added this to the P49: Code families as first-class objects — element model, lineage anchors, longitudinal chat milestone 2026-09-09 17:59:21 +00:00
kert added the llmpfs labels 2026-09-09 17:59:21 +00:00
Author
Owner

Measurements from the P49 slice-2 whole-branch review (2026-09-09) that this issue must absorb:

  • With the CPT-derived registry loaded (8,518 family keys after refresh_from), detect_codes costs 0.50 s per call: it compiles one regex per synonym per family and family_of is a linear scan. Precompile a single alternation at refresh time and build a code→family dict.
  • 1,334 of 8,518 derived family names are one or two words ("Repair", "Excision", "Incision", "Introduction", "Endoscopy", "Other Procedures", "Chemistry"). Slice 2 therefore gives derived families no synthetic synonyms (load_families leaves synonyms=() for non-hand families); matching derived families by name is this issue's job — gate on ≥3-word titles or a curated allow-list, never bare heading words.
  • Hand families keep their hand synonyms; the chat still sees only those until refresh_from is wired into the llm service here.
Measurements from the P49 slice-2 whole-branch review (2026-09-09) that this issue must absorb: - With the CPT-derived registry loaded (8,518 family keys after `refresh_from`), `detect_codes` costs **0.50 s per call**: it compiles one regex per synonym per family and `family_of` is a linear scan. Precompile a single alternation at refresh time and build a code→family dict. - **1,334 of 8,518 derived family names are one or two words** ("Repair", "Excision", "Incision", "Introduction", "Endoscopy", "Other Procedures", "Chemistry"). Slice 2 therefore gives derived families **no synthetic synonyms** (`load_families` leaves `synonyms=()` for non-hand families); matching derived families by name is this issue's job — gate on ≥3-word titles or a curated allow-list, never bare heading words. - Hand families keep their hand synonyms; the chat still sees only those until `refresh_from` is wired into the llm service here.
Author
Owner

Items 1–2 done in da345bc: refresh_from runs whenever the chat opens or reopens the replica (llm.evidence._connect, plus warm() at service start); detect_codes uses a trie-compiled phrase alternation (name phrases only for CPT-named families ≥ 12 chars / ≥ 2 words) and a code→family index (family_of O(1)); detect_codes ~0.01–0.05 ms on the 8,518-key registry. New: Detection.wide and MAX_FAMILY_EXPAND=20 (a family with more codes is listed but does not expand its codes), per-turn cap chat_codes_max=24. Item 3 (synonym curation) remains open; also open: readers of FAMILIES do not take the registry lock (the swap is update-then-delete, so a reader sees old ∪ new, never empty).

Items 1–2 done in da345bc: refresh_from runs whenever the chat opens or reopens the replica (llm.evidence._connect, plus warm() at service start); detect_codes uses a trie-compiled phrase alternation (name phrases only for CPT-named families ≥ 12 chars / ≥ 2 words) and a code→family index (family_of O(1)); detect_codes ~0.01–0.05 ms on the 8,518-key registry. New: Detection.wide and MAX_FAMILY_EXPAND=20 (a family with more codes is listed but does not expand its codes), per-turn cap chat_codes_max=24. Item 3 (synonym curation) remains open; also open: readers of FAMILIES do not take the registry lock (the swap is update-then-delete, so a reader sees old ∪ new, never empty).
Author
Owner

Closing: items 1–2 shipped in da345bc (refresh_from on every replica open + warm() at startup; trie-compiled phrase regex and code→family index, detect_codes ~0.01–0.05 ms on 8,518 keys). Item 3 (synonym curation) is addressed by Ruling B1 rather than a curated list: a derived family's name is a chat trigger only when it is a real CPT heading (Family.cpt) of ≥ 12 characters and ≥ 2 words, so GENE/ADM/REVERSE-style keys never fire; stem-derived families are reachable by member code only. Wide families (> 20 codes) are listed but do not expand their codes (Detection.wide). If a curated synonym list is still wanted later, open a fresh issue against pfs.families.HAND_FAMILIES.

Closing: items 1–2 shipped in da345bc (refresh_from on every replica open + warm() at startup; trie-compiled phrase regex and code→family index, detect_codes ~0.01–0.05 ms on 8,518 keys). Item 3 (synonym curation) is addressed by Ruling B1 rather than a curated list: a derived family's name is a chat trigger only when it is a real CPT heading (Family.cpt) of ≥ 12 characters and ≥ 2 words, so GENE/ADM/REVERSE-style keys never fire; stem-derived families are reachable by member code only. Wide families (> 20 codes) are listed but do not expand their codes (Detection.wide). If a curated synonym list is still wanted later, open a fresh issue against pfs.families.HAND_FAMILIES.
kert closed this issue 2026-09-11 20:47:56 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: homelab/stack#699