docs(meta): switch ticket-reference convention #N -> T-N (pql migration phase 3)

Adopts the T-NNN convention (T-N == old #N == pql ticket id) across the active
operational layer: governance/ decision records, .claude/{rules,agents,skills},
CLAUDE.md, DECISIONS.md. 283 references rewritten.

Guarded against false positives (17 correctly skipped, each logged):
  - PR references kept (PR #136/#138/... — PRs are a separate #-namespace)
  - non-ticket numbers kept (#4122; the "#1 process failure" idiom; "task #3")
  - only #N where N is an actual ticket id is rewritten; the 1-4 digit word-bounded
    match also excludes 6-digit hex colours in the visual decision records

Git history is NOT rewritten (a commit's #N already equals T-N numerically), and
historical archives (docs/sprints, docs/discussions, docs/workshops) keep their
point-in-time #N. The /pr-process ticket-ID extraction logic moves to T-NNN in the
Phase 4 consumer cutover.

Verified: pql decisions validate ok; sync 357 records / 1057 refs / broken 0 (the
prose edits don't affect decision parsing or the tickets.decision_ref linkage).
Transform committed at tooling/pql-migrate/retag_ticket_refs.py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-06 12:41:52 +02:00
co-authored by Claude Opus 4.8
parent 83bb18b385
commit aefbb4bd88
18 changed files with 314 additions and 223 deletions
+91
View File
@@ -0,0 +1,91 @@
#!/usr/bin/env python3
"""Rewrite ticket references #N -> T-N in the active layer (pql migration, Phase 3).
Switches the project's ticket-reference convention to T-NNN, matching the pql id
(T-N == old #N). Operates on markdown in the active operational layer only:
CLAUDE.md, DECISIONS.md, governance/, .claude/{rules,agents,skills}. Git history is
NOT rewritten (a commit's #N already equals T-N numerically). Historical archives
(docs/sprints, docs/discussions, docs/workshops) keep their point-in-time #N.
Guards against false positives (every skip is logged with a reason):
- N must be an actual ticket id (excludes years, the #4122 stray, hex colors —
6-digit hexes are also excluded by the 1-4 digit bound + word boundary).
- NOT a PR reference: "PR #136" / "pull request #138" stay (PRs are a separate
#-namespace from tickets in this repo).
- NOT a semantic non-ticket: "the #1 process failure", "task #3".
Default is a dry run. Pass --apply to write.
"""
import glob
import os
import re
import sqlite3
import sys
APPLY = "--apply" in sys.argv
REPO = "/var/mnt/data/projects/settled-reach/main"
SRC = os.environ.get("SR_DB_PATH", "/var/home/jeroenschweitzer/Projects/settled-reach/settledreach.db")
TICKET_IDS = set(str(r[0]) for r in sqlite3.connect(f"file:{SRC}?mode=ro", uri=True).execute("SELECT id FROM tickets"))
REF = re.compile(r"(?<![\w#&])#(\d{1,4})\b")
PR_BEFORE = re.compile(r"(?:PR|pr|Pull Request|pull request)s?\s*$")
TASK_BEFORE = re.compile(r"task\s*$", re.I)
SEMANTIC_AFTER = re.compile(r"^\s*process\b") # "#1 process failure"
FILES = sorted(set(
[f"{REPO}/CLAUDE.md", f"{REPO}/DECISIONS.md"]
+ glob.glob(f"{REPO}/governance/**/*.md", recursive=True)
+ glob.glob(f"{REPO}/.claude/rules/*.md")
+ glob.glob(f"{REPO}/.claude/agents/*.md")
+ glob.glob(f"{REPO}/.claude/skills/**/*.md", recursive=True)
))
def transform(text):
rewrites, skips = [], []
def repl(m):
n = m.group(1)
before = text[max(0, m.start() - 24):m.start()]
after = text[m.end():m.end() + 24]
if n not in TICKET_IDS:
skips.append((m.group(0), "not-a-ticket-id")); return m.group(0)
if PR_BEFORE.search(before):
skips.append((m.group(0), "PR reference")); return m.group(0)
if TASK_BEFORE.search(before):
skips.append((m.group(0), "task-number")); return m.group(0)
if SEMANTIC_AFTER.search(after):
skips.append((m.group(0), "semantic '#N process'")); return m.group(0)
rewrites.append((m.group(0), f"T-{n}")); return f"T-{n}"
return REF.sub(repl, text), rewrites, skips
def main():
print(f"=== retag_ticket_refs.py ({'APPLY' if APPLY else 'DRY-RUN'}) ===\n")
tot_rw, tot_sk, all_skips = 0, 0, []
for f in FILES:
text = open(f, encoding="utf-8").read()
new, rewrites, skips = transform(text)
if rewrites:
rel = f.split("/main/", 1)[-1]
print(f"{rel}: {len(rewrites)} rewrite(s)" + (f", {len(skips)} skip(s)" if skips else ""))
tot_rw += len(rewrites)
tot_sk += len(skips)
all_skips += [(f.split('/main/',1)[-1], s) for s in skips]
if APPLY and rewrites:
open(f, "w", encoding="utf-8").write(new)
print(f"\n--- SKIPPED (not rewritten), grouped by reason ---")
by_reason = {}
for rel, (tok, reason) in all_skips:
by_reason.setdefault(reason, []).append(f"{tok} ({os.path.basename(rel)})")
for reason, items in sorted(by_reason.items()):
print(f" {reason}: {len(items)}")
for it in sorted(set(items))[:12]:
print(f" {it}")
print(f"\n{tot_rw} rewrite(s), {tot_sk} skip(s)." + ("" if APPLY else " (dry run — pass --apply)"))
if __name__ == "__main__":
main()