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
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.
86 lines
3.3 KiB
Python
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}
|