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:
@@ -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()
|
||||
Reference in New Issue
Block a user