From ffa638a46c22c3a5dce57fd56bec3ede08705412 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Sat, 2 May 2026 10:09:03 +0200 Subject: [PATCH] chore(db): add decisions-orphan-tickets CLI (#887) New `tooling/db/decision orphan-tickets` subcommand scans tickets with a decision_ref that doesn't match any row in the decisions table. Surfaces silently orphaned tickets from typo'd or renumbered D-IDs. Makefile target: `make decisions-orphan-tickets`. Co-Authored-By: Claude Opus 4.6 --- Makefile | 8 ++++++-- tooling/db/decisions_sync.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 57ae9ea82..1184b2b55 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ GODOT := $(shell command -v godot4 2>/dev/null || command -v godot 2>/dev/null) .PHONY: help setup build check-protocol client server game stop test lint lint-python setup-venv ci ci-client ci-server clean \ - decisions-sync decisions-coverage decisions-active decisions-orphan \ + decisions-sync decisions-coverage decisions-active decisions-orphan decisions-orphan-tickets \ db-backup db-install validate-content check-fact-ids setup-hooks install-hooks \ audit deny atlas-verify economy-db atlas-generate regen-db check-systems-db \ pre-pr pre-pr-lint pre-pr-build pre-pr-test pre-pr-validate pre-pr-fixtures \ @@ -48,7 +48,8 @@ help: @echo " make decisions-sync Sync decisions/*.md into SQLite" @echo " make decisions-coverage Each decision with its implementing ticket(s)" @echo " make decisions-active List active decisions" - @echo " make decisions-orphan Decisions without implementing tickets" + @echo " make decisions-orphan Decisions without implementing tickets" + @echo " make decisions-orphan-tickets Tickets with invalid or missing decision_ref" @echo " make audit Run cargo audit (security advisory check)" @echo " make deny Run cargo deny check (license/ban policy)" @echo " make validate-content Validate content YAML against schemas" @@ -404,6 +405,9 @@ decisions-active: decisions-orphan: @tooling/db/sqlite-query "SELECT id, title FROM decisions WHERE type='confirmed' AND status='active' AND id NOT IN (SELECT DISTINCT decision_ref FROM tickets WHERE decision_ref IS NOT NULL)" +decisions-orphan-tickets: + @tooling/db/decision orphan-tickets + # --- Content Validation --- validate-content: diff --git a/tooling/db/decisions_sync.py b/tooling/db/decisions_sync.py index 8a0e23aa0..efd622c7c 100644 --- a/tooling/db/decisions_sync.py +++ b/tooling/db/decisions_sync.py @@ -457,6 +457,35 @@ def show_decision(cfg, decision_id): conn.close() +def orphan_tickets(cfg): + """List tickets whose decision_ref is set but does not match any decision in the DB.""" + conn = get_connection(cfg) + try: + rows = conn.execute( + """SELECT t.id, t.title, t.decision_ref, t.status, t.team + FROM tickets t + WHERE t.decision_ref IS NOT NULL + AND t.decision_ref != '' + AND t.decision_ref NOT IN (SELECT id FROM decisions) + ORDER BY t.decision_ref, t.id""", + ).fetchall() + + orphans = [dict(r) for r in rows] + + return { + "ok": True, + "count": len(orphans), + "orphans": orphans, + "summary": ( + f"{len(orphans)} orphan ticket(s) found" + if orphans + else "No orphan tickets — all decision_ref values are valid" + ), + } + finally: + conn.close() + + def check_dupes(cfg): """Check for duplicate decision IDs across all markdown files.""" # Pre-existing collisions too deeply embedded to renumber (139+ references). @@ -508,6 +537,7 @@ Usage: decisions_sync.py next [D|Q|R] Show next available ID (all prefixes or one) decisions_sync.py claim [title] Claim next ID and insert placeholder decisions_sync.py check-dupes Check for duplicate IDs across markdown files + decisions_sync.py orphan-tickets List tickets with invalid/missing decision_ref decisions_sync.py --help Show this help message ID claiming workflow: @@ -554,6 +584,8 @@ def main(): result = claim_id(cfg, prefix, domain, title) elif cmd == "check-dupes": result = check_dupes(cfg) + elif cmd == "orphan-tickets": + result = orphan_tickets(cfg) else: result = {"ok": False, "error": f"Unknown command: {cmd}. Use --help for usage."}