Files
stack/tests/rex/comments/test_coordination.py
kert c9b6a28f07
Some checks failed
CI / lint (push) Successful in 45s
CI / notebooks-smoke (push) Successful in 1m31s
Deploy / notebooks (push) Has been skipped
Deploy / zotero (push) Has been skipped
Deploy / docs (push) Has been skipped
Deploy / api (push) Has been skipped
Deploy / mc (push) Has been skipped
Infra CI / notebooks (push) Successful in 57s
Infra CI / zotero (push) Successful in 14s
Infra CI / docs (push) Successful in 1m43s
CI / test (push) Has been cancelled
Deploy / report (push) Has been cancelled
Infra CI / mc (push) Has been cancelled
Infra CI / api (push) Has been cancelled
feat(comments): coordination + form-letter detection, no LLM required (refs #255)
rex.comments.coordination: hti5 methodology — Jaccard similarity on
character 5-gram shingles (threshold 0.45) over normalized text,
union-find grouping with stable group ids, form-letter flag at >=3
members. Short texts (<200 chars) are excluded: two short 'I oppose'
notes are agreement, not coordination.

rex.comments.table: shared load path for skin_subs.rulemaking_comments
— outer-joins the classification (#254) and coordination (#255) JSONL
caches onto the comment identity rows, so whichever pass runs first
populates the table and the other enriches it without clobbering.
classify_comments.py refactored onto it; new analyze_coordination.py
driver.

Run against CMS-2025-0304 (the CY2026 OPPS skin-sub docket): of the
384 skin-sub-relevant comments, 203 (53%) are form letters across 31
coordinated groups — largest campaigns 72 and 36 members. Table loaded:
384 rows, coordination columns filled, classification columns NULL
until the #254 LLM run (blocked on API credits).

59 comments tests green.
2026-07-10 22:17:42 -04:00

86 lines
3.3 KiB
Python

"""Tests for rex.comments.coordination — form-letter/campaign detection."""
from __future__ import annotations
from rex.comments.coordination import (
analyze,
find_groups,
form_letter_groups,
jaccard,
normalize,
shingles,
)
TEMPLATE = (
"I am writing to strongly oppose the proposed reclassification of skin "
"substitutes from biologicals to incident-to supplies. The flat rate of "
"$127.28 per square centimeter will devastate patient access to advanced "
"wound care products and harm innovation in cellular tissue-based "
"products. I urge CMS to withdraw this proposal and preserve ASP plus "
"six percent reimbursement for these critical therapies."
)
# Same template, light personalization — the classic form-letter signature.
VARIANT_A = "Dear Administrator, " + TEMPLATE + " Sincerely, Dr. Alice Smith, DPM"
VARIANT_B = "To whom it may concern: " + TEMPLATE + " Regards, Bob Jones, NP"
VARIANT_C = TEMPLATE + " Respectfully submitted, Carol Lee, Wound Care Clinic"
INDEPENDENT = (
"As a health economist who has studied the skin substitute market for a "
"decade, I support the proposed payment change. Average sales price "
"inflation in this sector reflects strategic price-setting rather than "
"clinical value, and the fraud indictments of the past two years show "
"the incentive structure is broken. A packaged rate is overdue, though "
"CMS should phase it in over two years to avoid access cliffs in rural "
"areas where few wound-care alternatives exist for beneficiaries."
)
class TestPrimitives:
def test_normalize_strips_punctuation_and_case(self):
assert normalize("The $127.28/cm² Rate!") == "the 127 28 cm rate"
def test_shingles_short_text_empty(self):
assert shingles("abc") == frozenset()
def test_jaccard_identity_and_disjoint(self):
a = shingles(TEMPLATE)
assert jaccard(a, a) == 1.0
assert jaccard(a, frozenset()) == 0.0
class TestFindGroups:
def test_template_variants_group_independent_stays_out(self):
groups = find_groups(
{
"c-a": VARIANT_A,
"c-b": VARIANT_B,
"c-c": VARIANT_C,
"c-x": INDEPENDENT,
}
)
assert groups.get("c-a") == groups.get("c-b") == groups.get("c-c") == "c-a"
assert "c-x" not in groups
def test_short_texts_never_grouped(self):
groups = find_groups({"s1": "I oppose this rule", "s2": "I oppose this rule"})
assert groups == {}
def test_group_id_stable_regardless_of_order(self):
texts = {"z-late": VARIANT_A, "a-early": VARIANT_B}
assert set(find_groups(texts).values()) == {"a-early"}
class TestFormLetters:
def test_min_size_threshold(self):
groups = {"a": "g1", "b": "g1", "c": "g1", "d": "g2", "e": "g2"}
assert form_letter_groups(groups) == {"g1"}
class TestAnalyze:
def test_every_input_gets_a_record(self):
results = analyze(
{"c-a": VARIANT_A, "c-b": VARIANT_B, "c-c": VARIANT_C, "c-x": INDEPENDENT}
)
assert set(results) == {"c-a", "c-b", "c-c", "c-x"}
assert results["c-a"]["is_form_letter"] is True
assert results["c-x"] == {"coordination_group": "", "is_form_letter": False}