"""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))