feat(bib): code:/family: item tags from chunk anchors and FR paragraphs — stack bib code-tags (refs #688)
Rolls up pfs.anchors.anchor_metadata (already stamped on pgvector chunks by stack llm index/restamp, plus FR paragraph anchors for rules) to the bib item level: code:<HCPCS> and family:<KEY> tags, additive and idempotent via Store.add_tag. Items with more than --max-codes-per-item (200) distinct codes get family: tags only, plus code: for hand-family codes (ACP/CCM/PCM/TCM/APCM) — an RVU file or CPT edition naming thousands of codes doesn't need a tag cloud that size. Live run over the whole store: 67,198 items tagged, 486,519 tags added (22,111 code: tags, 786 family: tags), 141 wide items capped. The nightly zotero-sync.yml needs no change — tags ride along on items already in its --tag scopes.
This commit is contained in:
157
src/bib/codetags.py
Normal file
157
src/bib/codetags.py
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
"""``code:``/``family:`` item tags — ``stack bib code-tags``.
|
||||||
|
|
||||||
|
Chunk-level anchors (``pfs.anchors.anchor_metadata``, stamped by
|
||||||
|
``llm.index``/``llm.restamp`` into pgvector's ``cmetadata``) already say
|
||||||
|
which HCPCS/CPT codes and code families a piece of text names. This
|
||||||
|
module rolls those per-chunk anchors up to the bib *item* they belong
|
||||||
|
to and stamps ``code:<CODE>`` / ``family:<KEY>`` tags on the item —
|
||||||
|
so ``stack bib query``/``stack bib tag`` and any other item-level tool
|
||||||
|
can filter "everything about G0556" without touching pgvector.
|
||||||
|
|
||||||
|
Two sources feed the same shape (``item_key -> (codes, families)``):
|
||||||
|
|
||||||
|
* :func:`item_codes_from_chunks` — one ``GROUP BY`` round trip per
|
||||||
|
pgvector collection (comments, rules, corpus).
|
||||||
|
* :func:`item_codes_from_anchors` — rules only, straight from
|
||||||
|
``fr_anchors.text`` via ``pfs.anchors.anchor_metadata``, no pgvector
|
||||||
|
needed (useful before/without an index run, or with
|
||||||
|
``--anchors-only``).
|
||||||
|
|
||||||
|
:func:`apply_code_tags` stamps the tags. It is additive and idempotent
|
||||||
|
— ``Store.add_tag`` only writes when the tag is new — and caps the
|
||||||
|
per-item ``code:`` cloud at ``max_codes_per_item`` distinct codes: an
|
||||||
|
RVU file or code-list item can name thousands of codes, and a tag cloud
|
||||||
|
that size helps nobody. Wide items still get every ``family:`` tag,
|
||||||
|
plus ``code:`` for codes in a hand-registered family
|
||||||
|
(``pfs.families.HAND_FAMILIES`` — ACP/CCM/PCM/TCM/APCM) since those are
|
||||||
|
the ones anyone actually filters by.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Mapping
|
||||||
|
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
from bib.tag import Tag
|
||||||
|
from pfs.anchors import anchor_metadata
|
||||||
|
from pfs.families import HAND_FAMILIES, Family
|
||||||
|
|
||||||
|
CodeMap = dict[str, tuple[set[str], set[str]]]
|
||||||
|
|
||||||
|
#: Codes belonging to a hand-registered family — these survive the
|
||||||
|
#: ``max_codes_per_item`` cap even on a wide item (Ruling: brief Task 4).
|
||||||
|
HAND_FAMILY_CODES: frozenset[str] = frozenset(
|
||||||
|
code for fam in HAND_FAMILIES.values() for code in fam.codes
|
||||||
|
)
|
||||||
|
|
||||||
|
SELECT_CHUNK_CODES_SQL = (
|
||||||
|
"SELECT e.cmetadata->>'item_key', "
|
||||||
|
"string_agg(e.cmetadata->>'codes', ' '), "
|
||||||
|
"string_agg(e.cmetadata->>'families', ' ') "
|
||||||
|
"FROM langchain_pg_embedding e "
|
||||||
|
"WHERE e.collection_id = (SELECT uuid FROM langchain_pg_collection WHERE name = :collection) "
|
||||||
|
"AND coalesce(e.cmetadata->>'codes', '') <> '' "
|
||||||
|
"GROUP BY 1"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def item_codes_from_chunks(engine: Any, *, collection: str) -> CodeMap:
|
||||||
|
"""``item_key -> (codes, families)`` for one pgvector collection, via
|
||||||
|
a single ``GROUP BY e.cmetadata->>'item_key'`` round trip."""
|
||||||
|
with engine.begin() as conn:
|
||||||
|
rows = conn.execute(
|
||||||
|
text(SELECT_CHUNK_CODES_SQL), {"collection": collection}
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
out: CodeMap = {}
|
||||||
|
for item_key, codes_agg, families_agg in rows:
|
||||||
|
if not item_key:
|
||||||
|
continue
|
||||||
|
codes = {c for c in (codes_agg or "").split() if c}
|
||||||
|
families = {f for f in (families_agg or "").split() if f}
|
||||||
|
if not codes and not families:
|
||||||
|
continue
|
||||||
|
out[item_key] = (codes, families)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def item_codes_from_anchors(
|
||||||
|
store: Any,
|
||||||
|
*,
|
||||||
|
families: Mapping[str, Family] | None = None,
|
||||||
|
code_index: Mapping[str, tuple[str, ...]] | None = None,
|
||||||
|
) -> CodeMap:
|
||||||
|
"""``item_key -> (codes, families)`` for rules, computed straight from
|
||||||
|
``fr_anchors.text`` via ``anchor_metadata`` — no pgvector needed."""
|
||||||
|
con = store._con()
|
||||||
|
rows = con.execute(
|
||||||
|
"SELECT item_key, text FROM fr_anchors ORDER BY item_key, p_id"
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
out: CodeMap = {}
|
||||||
|
for row in rows:
|
||||||
|
item_key = row["item_key"]
|
||||||
|
md = anchor_metadata(
|
||||||
|
row["text"] or "", families=families, code_index=code_index
|
||||||
|
)
|
||||||
|
codes = {c for c in md["codes"].split() if c}
|
||||||
|
fams = {f for f in md["families"].split() if f}
|
||||||
|
if not codes and not fams:
|
||||||
|
continue
|
||||||
|
cur_codes, cur_fams = out.setdefault(item_key, (set(), set()))
|
||||||
|
cur_codes.update(codes)
|
||||||
|
cur_fams.update(fams)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def apply_code_tags(
|
||||||
|
store: Any,
|
||||||
|
mapping: CodeMap,
|
||||||
|
*,
|
||||||
|
dry_run: bool = False,
|
||||||
|
max_codes_per_item: int = 200,
|
||||||
|
) -> dict[str, int]:
|
||||||
|
"""Stamp ``code:``/``family:`` tags for every item in *mapping*.
|
||||||
|
|
||||||
|
Additive and idempotent: an item's current tags are read once and
|
||||||
|
only labels not already present count toward ``tags_added`` (and
|
||||||
|
are written, unless ``dry_run``) — a rerun over an unchanged mapping
|
||||||
|
adds zero tags. Items with more than ``max_codes_per_item`` distinct
|
||||||
|
codes get ``family:`` tags plus ``code:`` only for hand-family codes
|
||||||
|
(:data:`HAND_FAMILY_CODES`); such items count toward
|
||||||
|
``skipped_wide``. Items absent from *store* are skipped silently —
|
||||||
|
a caller can build *mapping* from a wider scope (e.g. pgvector) than
|
||||||
|
what's currently in bib.
|
||||||
|
|
||||||
|
Returns ``{"items": n, "tags_added": n, "skipped_wide": n}``.
|
||||||
|
"""
|
||||||
|
items = 0
|
||||||
|
tags_added = 0
|
||||||
|
skipped_wide = 0
|
||||||
|
|
||||||
|
for item_key, (codes, families) in mapping.items():
|
||||||
|
try:
|
||||||
|
current = set(store.get(item_key).tags)
|
||||||
|
except KeyError:
|
||||||
|
continue
|
||||||
|
items += 1
|
||||||
|
|
||||||
|
wide = len(codes) > max_codes_per_item
|
||||||
|
if wide:
|
||||||
|
skipped_wide += 1
|
||||||
|
tag_codes = codes & HAND_FAMILY_CODES
|
||||||
|
else:
|
||||||
|
tag_codes = codes
|
||||||
|
|
||||||
|
labels = [Tag.code(c).label for c in tag_codes] + [
|
||||||
|
Tag.family(f).label for f in families
|
||||||
|
]
|
||||||
|
for label in labels:
|
||||||
|
if label in current:
|
||||||
|
continue
|
||||||
|
tags_added += 1
|
||||||
|
if not dry_run:
|
||||||
|
store.add_tag(item_key, label)
|
||||||
|
|
||||||
|
return {"items": items, "tags_added": tags_added, "skipped_wide": skipped_wide}
|
||||||
@@ -49,6 +49,17 @@ Namespaces
|
|||||||
``fn:cclf.stg_beneficiary_xref``,
|
``fn:cclf.stg_beneficiary_xref``,
|
||||||
``fn:readmissions.int_encounter``
|
``fn:readmissions.int_encounter``
|
||||||
|
|
||||||
|
``code``
|
||||||
|
An HCPCS/CPT code the item names (upper-case), rolled up from
|
||||||
|
chunk-level anchors (``pfs.anchors.anchor_metadata``) or FR
|
||||||
|
paragraph text — see ``bib.codetags``:
|
||||||
|
``code:99490``, ``code:G0556``
|
||||||
|
|
||||||
|
``family``
|
||||||
|
A ``pfs.code_family``/``pfs.families`` family key the item touches
|
||||||
|
— the key exactly as registered, never lower-cased:
|
||||||
|
``family:CCM``, ``family:APCM``
|
||||||
|
|
||||||
Usage::
|
Usage::
|
||||||
|
|
||||||
from bib.tag import Tag
|
from bib.tag import Tag
|
||||||
@@ -238,6 +249,39 @@ class Tag(BaseModel):
|
|||||||
value = f"{item_key}/{locator}" if locator else item_key
|
value = f"{item_key}/{locator}" if locator else item_key
|
||||||
return cls(namespace="pin", value=value)
|
return cls(namespace="pin", value=value)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def code(cls, hcpcs: str) -> Tag:
|
||||||
|
"""Tag for an HCPCS/CPT code the item names.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
hcpcs : str
|
||||||
|
A code, e.g. ``99490``, ``G0556``. Upper-cased.
|
||||||
|
|
||||||
|
Examples::
|
||||||
|
|
||||||
|
Tag.code("g0556") # code:G0556
|
||||||
|
"""
|
||||||
|
return cls(namespace="code", value=hcpcs.upper())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def family(cls, key: str) -> Tag:
|
||||||
|
"""Tag for a ``pfs.families``/``pfs.code_family`` family key.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
key : str
|
||||||
|
The family key exactly as registered (e.g. ``CCM``,
|
||||||
|
``APCM``, or a derived ``pfs.code_family`` key) — never
|
||||||
|
lower-cased, since derived keys aren't necessarily
|
||||||
|
uppercase-safe round trips.
|
||||||
|
|
||||||
|
Examples::
|
||||||
|
|
||||||
|
Tag.family("CCM") # family:CCM
|
||||||
|
"""
|
||||||
|
return cls(namespace="family", value=key)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def fn(cls, qualified_name: str) -> Tag:
|
def fn(cls, qualified_name: str) -> Tag:
|
||||||
"""Tag linking a bibliography item to an express function.
|
"""Tag linking a bibliography item to an express function.
|
||||||
|
|||||||
101
src/cli/bib.py
101
src/cli/bib.py
@@ -912,3 +912,104 @@ def cfr_jump(
|
|||||||
typer.echo(cfrlink.md_link(ref))
|
typer.echo(cfrlink.md_link(ref))
|
||||||
else:
|
else:
|
||||||
typer.echo(cfrlink.url(cfrlink.parse_cite(ref)))
|
typer.echo(cfrlink.url(cfrlink.parse_cite(ref)))
|
||||||
|
|
||||||
|
|
||||||
|
@app.command(name="code-tags")
|
||||||
|
def code_tags(
|
||||||
|
collection: str = typer.Option(
|
||||||
|
"all", "--collection", help="Which collection: all | comments | rules | corpus."
|
||||||
|
),
|
||||||
|
anchors_only: bool = typer.Option(
|
||||||
|
False,
|
||||||
|
"--anchors-only",
|
||||||
|
help="Rules only, straight from fr_anchors text — skip pgvector entirely.",
|
||||||
|
),
|
||||||
|
dry_run: bool = typer.Option(
|
||||||
|
False, "--dry-run", help="Report per-collection item/tag counts; write nothing."
|
||||||
|
),
|
||||||
|
max_codes_per_item: int = typer.Option(
|
||||||
|
200,
|
||||||
|
"--max-codes-per-item",
|
||||||
|
help="Items with more distinct codes get family: tags only (+ hand-family codes).",
|
||||||
|
),
|
||||||
|
) -> None:
|
||||||
|
"""Stamp code:/family: item tags from chunk anchors and FR paragraphs.
|
||||||
|
|
||||||
|
Rolls up ``pfs.anchors.anchor_metadata`` — already stamped on
|
||||||
|
pgvector chunks by ``stack llm index``/``stack llm restamp`` — to
|
||||||
|
the bib item level: every code an item's chunks name becomes a
|
||||||
|
``code:<CODE>`` tag, every family a ``family:<KEY>`` tag. Rules also
|
||||||
|
get a pgvector-free source (``fr_anchors.text``, the FR paragraph
|
||||||
|
anchor map from ``stack bib fr-grab``); pass ``--anchors-only`` to
|
||||||
|
use only that and skip pgvector for rules (and skip comments/corpus
|
||||||
|
entirely — they have no other source). Additive and idempotent
|
||||||
|
(``bib.codetags.apply_code_tags``) — safe to rerun after every
|
||||||
|
``stack llm restamp``.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
|
||||||
|
from bib import connect
|
||||||
|
from bib.codetags import (
|
||||||
|
apply_code_tags,
|
||||||
|
item_codes_from_anchors,
|
||||||
|
item_codes_from_chunks,
|
||||||
|
)
|
||||||
|
from llm import config as llm_config
|
||||||
|
from llm.index import _engine
|
||||||
|
from pfs.anchors import code_family_index
|
||||||
|
from pfs.families import FAMILIES, refresh_from
|
||||||
|
|
||||||
|
collections = ("comments", "rules", "corpus")
|
||||||
|
if collection not in ("all", *collections):
|
||||||
|
raise typer.BadParameter("collection must be all, comments, rules or corpus")
|
||||||
|
targets = collections if collection == "all" else (collection,)
|
||||||
|
|
||||||
|
store = connect()
|
||||||
|
cfg = llm_config.load()
|
||||||
|
if os.path.exists(cfg.duckdb_replica):
|
||||||
|
import duckdb
|
||||||
|
|
||||||
|
con = duckdb.connect(cfg.duckdb_replica, read_only=True)
|
||||||
|
try:
|
||||||
|
refresh_from(con)
|
||||||
|
finally:
|
||||||
|
con.close()
|
||||||
|
code_index = code_family_index(FAMILIES)
|
||||||
|
|
||||||
|
engine = None
|
||||||
|
totals = {"items": 0, "tags_added": 0, "skipped_wide": 0}
|
||||||
|
for target in targets:
|
||||||
|
if anchors_only and target != "rules":
|
||||||
|
typer.echo(f"{target}: skipped (--anchors-only)")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if target == "rules":
|
||||||
|
mapping = item_codes_from_anchors(store, code_index=code_index)
|
||||||
|
if not anchors_only:
|
||||||
|
if engine is None:
|
||||||
|
engine = _engine(cfg)
|
||||||
|
for key, (codes, fams) in item_codes_from_chunks(
|
||||||
|
engine, collection=target
|
||||||
|
).items():
|
||||||
|
cur_codes, cur_fams = mapping.setdefault(key, (set(), set()))
|
||||||
|
mapping[key] = (cur_codes | codes, cur_fams | fams)
|
||||||
|
else:
|
||||||
|
if engine is None:
|
||||||
|
engine = _engine(cfg)
|
||||||
|
mapping = item_codes_from_chunks(engine, collection=target)
|
||||||
|
|
||||||
|
stats = apply_code_tags(
|
||||||
|
store, mapping, dry_run=dry_run, max_codes_per_item=max_codes_per_item
|
||||||
|
)
|
||||||
|
for k in totals:
|
||||||
|
totals[k] += stats[k]
|
||||||
|
typer.echo(
|
||||||
|
f"{target}: items={stats['items']} tags_added={stats['tags_added']} "
|
||||||
|
f"skipped_wide={stats['skipped_wide']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
prefix = "would-" if dry_run else ""
|
||||||
|
typer.echo(
|
||||||
|
f"total: {prefix}items={totals['items']} {prefix}tags_added={totals['tags_added']} "
|
||||||
|
f"skipped_wide={totals['skipped_wide']}"
|
||||||
|
)
|
||||||
|
|||||||
146
tests/bib/test_codetags.py
Normal file
146
tests/bib/test_codetags.py
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
"""Tests for bib.codetags — code:/family: item tags from chunk anchors
|
||||||
|
and FR paragraphs (Task 4, refs #688)."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from bib.codetags import (
|
||||||
|
apply_code_tags,
|
||||||
|
item_codes_from_anchors,
|
||||||
|
item_codes_from_chunks,
|
||||||
|
)
|
||||||
|
from bib.item import Rule, Source
|
||||||
|
from bib.store import Store
|
||||||
|
from bib.tag import Tag
|
||||||
|
|
||||||
|
|
||||||
|
def _store() -> Store:
|
||||||
|
return Store(":memory:")
|
||||||
|
|
||||||
|
|
||||||
|
class TestTagFactories:
|
||||||
|
def test_code_label(self) -> None:
|
||||||
|
assert Tag.code("G0556").label == "code:G0556"
|
||||||
|
|
||||||
|
def test_code_upper_cases(self) -> None:
|
||||||
|
assert Tag.code("g0556").label == "code:G0556"
|
||||||
|
|
||||||
|
def test_family_label(self) -> None:
|
||||||
|
assert Tag.family("CCM").label == "family:CCM"
|
||||||
|
|
||||||
|
|
||||||
|
class TestItemCodesFromChunks:
|
||||||
|
def test_group_by_item_key_unions_codes_and_families(self) -> None:
|
||||||
|
engine = MagicMock()
|
||||||
|
conn = engine.begin.return_value.__enter__.return_value
|
||||||
|
conn.execute.return_value.fetchall.return_value = [
|
||||||
|
("ITEM0001", "99490 99491", "CCM"),
|
||||||
|
("ITEM0002", "99497", ""),
|
||||||
|
]
|
||||||
|
mapping = item_codes_from_chunks(engine, collection="comments")
|
||||||
|
assert mapping["ITEM0001"] == ({"99490", "99491"}, {"CCM"})
|
||||||
|
assert mapping["ITEM0002"] == ({"99497"}, set())
|
||||||
|
|
||||||
|
def test_skips_rows_with_no_item_key(self) -> None:
|
||||||
|
engine = MagicMock()
|
||||||
|
conn = engine.begin.return_value.__enter__.return_value
|
||||||
|
conn.execute.return_value.fetchall.return_value = [
|
||||||
|
(None, "99490", ""),
|
||||||
|
]
|
||||||
|
mapping = item_codes_from_chunks(engine, collection="rules")
|
||||||
|
assert mapping == {}
|
||||||
|
|
||||||
|
|
||||||
|
class TestItemCodesFromAnchors:
|
||||||
|
def test_reads_fr_anchors_per_item(self) -> None:
|
||||||
|
store = _store()
|
||||||
|
key = store.create(Rule(title="CY 2026 PFS Final Rule"))
|
||||||
|
store._con().executemany(
|
||||||
|
"INSERT INTO fr_anchors (item_key, p_id, page, ordinal, text) VALUES (?,?,?,?,?)",
|
||||||
|
[
|
||||||
|
(key, 1, 100, 1, "CPT code 99490 is for chronic care management."),
|
||||||
|
(key, 2, 100, 2, "See also 99491 for extended CCM."),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
store._con().commit()
|
||||||
|
|
||||||
|
mapping = item_codes_from_anchors(store)
|
||||||
|
assert key in mapping
|
||||||
|
codes, families = mapping[key]
|
||||||
|
assert {"99490", "99491"} <= codes
|
||||||
|
assert "CCM" in families
|
||||||
|
|
||||||
|
def test_item_with_no_codes_is_absent(self) -> None:
|
||||||
|
store = _store()
|
||||||
|
key = store.create(Rule(title="No codes rule"))
|
||||||
|
store._con().execute(
|
||||||
|
"INSERT INTO fr_anchors (item_key, p_id, page, ordinal, text) VALUES (?,?,?,?,?)",
|
||||||
|
(key, 1, 1, 1, "This paragraph names no codes at all."),
|
||||||
|
)
|
||||||
|
store._con().commit()
|
||||||
|
|
||||||
|
mapping = item_codes_from_anchors(store)
|
||||||
|
assert key not in mapping
|
||||||
|
|
||||||
|
|
||||||
|
class TestApplyCodeTags:
|
||||||
|
def test_tags_items_with_code_and_family(self) -> None:
|
||||||
|
store = _store()
|
||||||
|
key = store.create(Source(title="Narrow item"))
|
||||||
|
mapping = {key: ({"99490", "99491"}, {"CCM"})}
|
||||||
|
|
||||||
|
stats = apply_code_tags(store, mapping)
|
||||||
|
|
||||||
|
assert stats == {"items": 1, "tags_added": 3, "skipped_wide": 0}
|
||||||
|
tags = set(store.get(key).tags)
|
||||||
|
assert {"code:99490", "code:99491", "family:CCM"} <= tags
|
||||||
|
|
||||||
|
def test_wide_item_gets_only_family_and_hand_family_codes(self) -> None:
|
||||||
|
store = _store()
|
||||||
|
key = store.create(Source(title="RVU file"))
|
||||||
|
wide_codes = {f"{n:05d}" for n in range(300)}
|
||||||
|
# G0556 is a hand-family (APCM) code and must survive the cap.
|
||||||
|
wide_codes.add("G0556")
|
||||||
|
mapping = {key: (wide_codes, {"APCM", "CCM"})}
|
||||||
|
|
||||||
|
stats = apply_code_tags(store, mapping, max_codes_per_item=200)
|
||||||
|
|
||||||
|
assert stats["items"] == 1
|
||||||
|
assert stats["skipped_wide"] == 1
|
||||||
|
tags = set(store.get(key).tags)
|
||||||
|
assert "code:G0556" in tags
|
||||||
|
assert "family:APCM" in tags
|
||||||
|
assert "family:CCM" in tags
|
||||||
|
# None of the plain wide codes made it through as code: tags.
|
||||||
|
assert not any(t.startswith("code:") and t != "code:G0556" for t in tags)
|
||||||
|
|
||||||
|
def test_rerun_adds_zero_new_tags(self) -> None:
|
||||||
|
store = _store()
|
||||||
|
key = store.create(Source(title="Idempotent item"))
|
||||||
|
mapping = {key: ({"99490"}, {"CCM"})}
|
||||||
|
|
||||||
|
first = apply_code_tags(store, mapping)
|
||||||
|
second = apply_code_tags(store, mapping)
|
||||||
|
|
||||||
|
assert first["tags_added"] == 2
|
||||||
|
assert second["tags_added"] == 0
|
||||||
|
assert second["items"] == 1
|
||||||
|
|
||||||
|
def test_dry_run_writes_nothing(self) -> None:
|
||||||
|
store = _store()
|
||||||
|
key = store.create(Source(title="Dry run item"))
|
||||||
|
mapping = {key: ({"99490"}, {"CCM"})}
|
||||||
|
|
||||||
|
stats = apply_code_tags(store, mapping, dry_run=True)
|
||||||
|
|
||||||
|
assert stats["tags_added"] == 2
|
||||||
|
assert store.get(key).tags == []
|
||||||
|
|
||||||
|
def test_unknown_item_key_is_skipped(self) -> None:
|
||||||
|
store = _store()
|
||||||
|
mapping = {"MISSING1": ({"99490"}, set())}
|
||||||
|
|
||||||
|
stats = apply_code_tags(store, mapping)
|
||||||
|
|
||||||
|
assert stats == {"items": 0, "tags_added": 0, "skipped_wide": 0}
|
||||||
@@ -263,3 +263,115 @@ class TestAllHelp:
|
|||||||
|
|
||||||
def test_refresh_oig(self):
|
def test_refresh_oig(self):
|
||||||
assert runner.invoke(app, ["refresh-oig", "--help"]).exit_code == 0
|
assert runner.invoke(app, ["refresh-oig", "--help"]).exit_code == 0
|
||||||
|
|
||||||
|
|
||||||
|
class TestCodeTags:
|
||||||
|
@patch("bib.codetags.apply_code_tags")
|
||||||
|
@patch("bib.codetags.item_codes_from_chunks")
|
||||||
|
@patch("bib.codetags.item_codes_from_anchors")
|
||||||
|
@patch("pfs.families.refresh_from")
|
||||||
|
@patch("pfs.anchors.code_family_index", return_value={})
|
||||||
|
@patch("os.path.exists", return_value=False)
|
||||||
|
@patch("llm.index._engine")
|
||||||
|
@patch("llm.config.load")
|
||||||
|
@patch("bib.connect")
|
||||||
|
def test_all_collections_run_rules_via_anchors_and_chunks(
|
||||||
|
self,
|
||||||
|
mc_connect,
|
||||||
|
mc_cfg,
|
||||||
|
mc_engine,
|
||||||
|
mc_exists,
|
||||||
|
mc_code_index,
|
||||||
|
mc_refresh,
|
||||||
|
mc_anchors,
|
||||||
|
mc_chunks,
|
||||||
|
mc_apply,
|
||||||
|
):
|
||||||
|
mc_anchors.return_value = {"R1": ({"99490"}, {"CCM"})}
|
||||||
|
mc_chunks.return_value = {"C1": ({"99497"}, set())}
|
||||||
|
mc_apply.return_value = {"items": 1, "tags_added": 1, "skipped_wide": 0}
|
||||||
|
|
||||||
|
result = runner.invoke(app, ["code-tags", "--collection", "all"])
|
||||||
|
|
||||||
|
assert result.exit_code == 0, result.output
|
||||||
|
mc_anchors.assert_called_once()
|
||||||
|
# rules + comments + corpus == 3 apply_code_tags calls
|
||||||
|
assert mc_apply.call_count == 3
|
||||||
|
for _, kwargs in mc_apply.call_args_list:
|
||||||
|
assert kwargs["dry_run"] is False
|
||||||
|
|
||||||
|
@patch("bib.codetags.apply_code_tags")
|
||||||
|
@patch("bib.codetags.item_codes_from_chunks")
|
||||||
|
@patch("bib.codetags.item_codes_from_anchors", return_value={})
|
||||||
|
@patch("pfs.families.refresh_from")
|
||||||
|
@patch("pfs.anchors.code_family_index", return_value={})
|
||||||
|
@patch("os.path.exists", return_value=False)
|
||||||
|
@patch("llm.index._engine")
|
||||||
|
@patch("llm.config.load")
|
||||||
|
@patch("bib.connect")
|
||||||
|
def test_dry_run_passes_through_and_writes_nothing(
|
||||||
|
self,
|
||||||
|
mc_connect,
|
||||||
|
mc_cfg,
|
||||||
|
mc_engine,
|
||||||
|
mc_exists,
|
||||||
|
mc_code_index,
|
||||||
|
mc_refresh,
|
||||||
|
mc_anchors,
|
||||||
|
mc_chunks,
|
||||||
|
mc_apply,
|
||||||
|
):
|
||||||
|
mc_chunks.return_value = {}
|
||||||
|
mc_apply.return_value = {"items": 0, "tags_added": 0, "skipped_wide": 0}
|
||||||
|
store = MagicMock()
|
||||||
|
mc_connect.return_value = store
|
||||||
|
|
||||||
|
result = runner.invoke(
|
||||||
|
app, ["code-tags", "--collection", "comments", "--dry-run"]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.exit_code == 0, result.output
|
||||||
|
assert mc_apply.call_args.kwargs["dry_run"] is True
|
||||||
|
store.add_tag.assert_not_called()
|
||||||
|
|
||||||
|
@patch("bib.codetags.apply_code_tags")
|
||||||
|
@patch("bib.codetags.item_codes_from_chunks")
|
||||||
|
@patch("bib.codetags.item_codes_from_anchors", return_value={})
|
||||||
|
@patch("pfs.families.refresh_from")
|
||||||
|
@patch("pfs.anchors.code_family_index", return_value={})
|
||||||
|
@patch("os.path.exists", return_value=False)
|
||||||
|
@patch("llm.index._engine")
|
||||||
|
@patch("llm.config.load")
|
||||||
|
@patch("bib.connect")
|
||||||
|
def test_anchors_only_skips_pgvector_entirely(
|
||||||
|
self,
|
||||||
|
mc_connect,
|
||||||
|
mc_cfg,
|
||||||
|
mc_engine,
|
||||||
|
mc_exists,
|
||||||
|
mc_code_index,
|
||||||
|
mc_refresh,
|
||||||
|
mc_anchors,
|
||||||
|
mc_chunks,
|
||||||
|
mc_apply,
|
||||||
|
):
|
||||||
|
mc_apply.return_value = {"items": 0, "tags_added": 0, "skipped_wide": 0}
|
||||||
|
|
||||||
|
result = runner.invoke(
|
||||||
|
app, ["code-tags", "--collection", "all", "--anchors-only"]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.exit_code == 0, result.output
|
||||||
|
mc_anchors.assert_called_once()
|
||||||
|
mc_chunks.assert_not_called()
|
||||||
|
mc_engine.assert_not_called()
|
||||||
|
# only the rules mapping gets applied — comments/corpus skipped
|
||||||
|
assert mc_apply.call_count == 1
|
||||||
|
|
||||||
|
def test_help(self):
|
||||||
|
assert runner.invoke(app, ["code-tags", "--help"]).exit_code == 0
|
||||||
|
|
||||||
|
@patch("bib.connect")
|
||||||
|
def test_bad_collection_raises_bad_parameter(self, mc_connect):
|
||||||
|
result = runner.invoke(app, ["code-tags", "--collection", "nope"])
|
||||||
|
assert result.exit_code != 0
|
||||||
|
|||||||
Reference in New Issue
Block a user