pdf-quiz-generator/backend/scripts/settle_tied_primaries.py
Daniel ab059673e5 content: every article has a high-yield view, and three ties that were not ties
**Missing views.** The note said 10 articles without a clinical view and 13
without a short one; the real numbers were 8 and 11, and the eight originals
turned out to carry no `variant` key at all, so everything in them was being
read as the long view. 33 sections written across 11 articles, every sentence
traceable to that article's own body, and the legacy sections stamped with the
variant they were always being treated as. No doses appear anywhere, because
not one of those eight states one. Every article now has a high-yield view and
`mdm_pass check` reads 330 of 331 conforming with no placeholder sections.

**Pediatric Dentistry** was neither an educator's job nor a delete. Its body is
real — scope, disparities, records and consent, public health, biomaterials —
and only its clinical view was the disclaimer. The three placeholder sections
are gone and the article stays: the reader hides a view an article does not
have, and a discipline overview about consent and public health has no bedside
path to write. Inventing one was the thing not to do.

**The 15 tied conditions were stale**, ticked with the evidence rather than
worked: the taxonomy rebuild gave every one of them a single home, and the
duplicate-name query now returns nothing.

**The 31 tied questions were 3**, and reading all three showed none was a real
tie. The best of them: a question about acute flaccid paralysis after minor
trauma was filed between "Nursemaid's Elbow" and "Radial Head Subluxation" —
two names for one condition, neither anything to do with the question. It is
under Spinal Cord Injury now, cross-filed to Acute Flaccid Paralysis, and the
two elbow links are removed rather than cross-filed: a wrong link surfaces a
question under a topic it does not belong to, which is the opposite of what
cross-filing is for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN
2026-09-12 18:16:59 +02:00

143 lines
6.3 KiB
Python

"""Give the three questions whose topics tied a primary, by reading them.
`promote_primary_categories` promotes a question from its system to a condition
when one candidate is the most specific. Where two candidates sit at the same
depth and carry the same number of questions there is nothing in the data to
separate them, so it leaves them alone and reports the count. That count was 31
before today's taxonomy rebuild and is 3 after it.
Three is small enough to read. Doing so shows that none of them is the even
contest the counts imply:
* Q1201 — a nonsurvivable head injury where the apnea test cannot be performed
and the next step is an ancillary cerebral blood flow scan. Brain Death is
what the question teaches; Intracranial Hemorrhage is how the child got there.
* Q853 — painless, large-volume rectal bleeding in a well 8-year-old whose
answer is heterotopic gastric mucosa. Gastrointestinal Bleeding is the
presentation; Juvenile Polyp is one of the wrong options, and stays as a
cross-link because the differential the explanation walks through includes it.
* Q826 — acute flaccid paralysis of both arms after minor trauma, answered by
MRI of the spine. Neither Nursemaid's Elbow nor Radial Head Subluxation has
anything to do with it; they are two names for one condition and the
classifier reached both from "wasn't moving his arms" in a toddler. Those two
links are removed rather than cross-filed, because a wrong link makes the
question surface under a topic it does not belong to.
Whatever a question moves off stays reachable: the system it was filed under
becomes a `question_category_links` row, and so does the second topic wherever
that topic is genuinely about the question.
Idempotent, and a dry run by default:
docker compose exec backend python -m scripts.settle_tied_primaries
docker compose exec backend python -m scripts.settle_tied_primaries --apply
"""
import sys
from sqlalchemy import text as sa_text
from app.database import SessionLocal
#: One decision per question. `system` is the primary it must still be filed
#: under for the decision to apply, `primary` the topic it moves to, `cross` the
#: topics kept as links, and `drop` the links that are simply wrong. Names are
#: carried beside every id so a renumbered taxonomy fails the guard instead of
#: silently filing a question under whatever now holds that id.
DECISIONS = [
{
"question": 1201,
"system": (14876, "Critical Care"),
"primary": (15297, "Brain Death"),
"cross": [(15438, "Intracranial Hemorrhage")],
"drop": [],
},
{
"question": 853,
"system": (14881, "Gastroenterology & Nutrition"),
"primary": (15526, "Gastrointestinal Bleeding"),
"cross": [(15891, "Juvenile Polyp")],
"drop": [],
},
{
"question": 826,
"system": (14879, "Emergency Medicine"),
"primary": (15606, "Spinal Cord Injury"),
"cross": [(15460, "Acute Flaccid Paralysis")],
"drop": [(15697, "Nursemaid's Elbow"), (15699, "Radial Head Subluxation")],
},
]
def category_name(db, category_id: int) -> str | None:
return db.execute(sa_text("SELECT name FROM question_categories WHERE id = :c"),
{"c": category_id}).scalar()
def main(apply: bool) -> int:
db = SessionLocal()
try:
planned, skipped = [], []
for decision in DECISIONS:
qid = decision["question"]
current = db.execute(sa_text(
"SELECT question_category_id FROM questions WHERE id = :q"), {"q": qid}).scalar()
if current is None:
skipped.append((qid, "no such question, or nothing filed"))
continue
if current == decision["primary"][0]:
skipped.append((qid, "already settled"))
continue
if current != decision["system"][0]:
skipped.append((qid, f"filed under {category_name(db, current)}, "
f"not {decision['system'][1]}"))
continue
wrong_name = next(
(f"{cid} is now {category_name(db, cid)!r}, expected {name!r}"
for cid, name in [decision["primary"], decision["system"],
*decision["cross"], *decision["drop"]]
if category_name(db, cid) != name), None)
if wrong_name:
skipped.append((qid, f"taxonomy has moved: {wrong_name}"))
continue
planned.append(decision)
for decision in planned:
qid = decision["question"]
print(f" Q{qid}: {decision['system'][1]} -> {decision['primary'][1]}")
print(f" cross-filed : {', '.join(n for _c, n in decision['cross'])}, "
f"{decision['system'][1]}")
if decision["drop"]:
print(f" links removed: {', '.join(n for _c, n in decision['drop'])}")
for qid, why in skipped:
print(f" Q{qid} skipped — {why}")
print(f"\n{len(planned)} questions to settle, {len(skipped)} left alone")
if not apply:
print("\ndry run. Pass --apply to write.")
return 0
for decision in planned:
qid = decision["question"]
db.execute(sa_text("UPDATE questions SET question_category_id = :c WHERE id = :q"),
{"c": decision["primary"][0], "q": qid})
for category_id, _name in [decision["system"], *decision["cross"]]:
db.execute(sa_text("""
INSERT INTO question_category_links (question_id, category_id)
SELECT :q, :c WHERE NOT EXISTS (
SELECT 1 FROM question_category_links
WHERE question_id = :q AND category_id = :c)
"""), {"q": qid, "c": category_id})
for category_id, _name in decision["drop"]:
db.execute(sa_text("""
DELETE FROM question_category_links
WHERE question_id = :q AND category_id = :c
"""), {"q": qid, "c": category_id})
db.commit()
print(f"\nsettled. {len(planned)} questions updated.")
return 0
finally:
db.close()
if __name__ == "__main__":
sys.exit(main("--apply" in sys.argv))