Files
clide/tools/scripts/planning/repo_decisions.py
T
jpmschweitzerandClaude a96a54ec7f add Python stopgap planning tooling under tools/scripts/plan
Ports settled-reach's decisions_sync.py + ticket + decision scripts,
Scrum-stripped. Writes to .pql/pql.db (gitignored). Verb shape
mirrors the eventual `pql` subcommands so migration is a call-site
find-replace once pql ships feature parity (D-040, R-011).

Ticket IDs are T-NNN (TEXT PKs) — reshape from settled-reach's
integer auto-increment so the stopgap's writes are compatible with
pql's future reads without a data migration. No `sprints` table; the
pyramid is kanban / waterfall (D-035).

Verified end-to-end: sync parses 71 records (39 confirmed, 23 open
questions, 9 rejected) with 62 cross-refs and zero broken links;
`ticket new`, `ticket status`, `ticket board`, and `decisions show
--with-refs` all round-trip correctly.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-04-21 17:28:10 +02:00

130 lines
3.8 KiB
Python

"""Decisions repository — upsert records, query joined views."""
from __future__ import annotations
import sqlite3
from pathlib import Path
from .parser import Record, parse_all
def sync(conn: sqlite3.Connection, repo_root: Path) -> dict:
records, warnings = parse_all(repo_root)
known_ids = {r.id for r in records}
conn.execute("DELETE FROM decision_refs")
upserted = 0
for r in records:
conn.execute(
"""
INSERT INTO decisions (id, type, domain, title, status, date, file_path, synced_at)
VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'))
ON CONFLICT(id) DO UPDATE SET
type = excluded.type,
domain = excluded.domain,
title = excluded.title,
status = excluded.status,
date = excluded.date,
file_path = excluded.file_path,
synced_at = datetime('now')
""",
(r.id, r.type, r.domain, r.title, r.status, r.date, r.file_path),
)
upserted += 1
refs_created = 0
broken = 0
for r in records:
for target, ref_type, note in r.refs:
if target not in known_ids:
broken += 1
warnings.append(f"broken ref: {r.id}{target} ({ref_type})")
continue
try:
conn.execute(
"""INSERT OR IGNORE INTO decision_refs
(source_id, target_id, ref_type, note)
VALUES (?, ?, ?, ?)""",
(r.id, target, ref_type, note),
)
refs_created += 1
except sqlite3.IntegrityError:
pass
conn.commit()
return {
"synced": upserted,
"refs": refs_created,
"broken": broken,
"warnings": warnings,
}
def list_decisions(
conn: sqlite3.Connection,
type_: str | None = None,
domain: str | None = None,
status: str | None = None,
) -> list[sqlite3.Row]:
conditions: list[str] = []
params: list[str] = []
if type_:
conditions.append("type = ?")
params.append(type_)
if domain:
conditions.append("domain = ?")
params.append(domain)
if status:
conditions.append("status = ?")
params.append(status)
where = " AND ".join(conditions) if conditions else "1=1"
return list(
conn.execute(
f"SELECT id, type, domain, title, status, date, file_path "
f"FROM decisions WHERE {where} ORDER BY id",
params,
)
)
def get(conn: sqlite3.Connection, rid: str) -> sqlite3.Row | None:
return conn.execute(
"SELECT * FROM decisions WHERE id = ?", (rid,)
).fetchone()
def refs_of(conn: sqlite3.Connection, rid: str) -> list[sqlite3.Row]:
return list(
conn.execute(
"""
SELECT source_id, target_id, ref_type, note
FROM decision_refs
WHERE source_id = ? OR target_id = ?
ORDER BY ref_type, source_id, target_id
""",
(rid, rid),
)
)
def tickets_for(conn: sqlite3.Connection, rid: str) -> list[sqlite3.Row]:
return list(
conn.execute(
"SELECT id, type, title, status, priority FROM tickets "
"WHERE decision_ref = ? ORDER BY id",
(rid,),
)
)
def coverage(conn: sqlite3.Connection) -> list[sqlite3.Row]:
"""D-records without an implementing ticket."""
return list(
conn.execute(
"""
SELECT d.id, d.domain, d.title
FROM decisions d
LEFT JOIN tickets t ON t.decision_ref = d.id
WHERE d.type = 'confirmed' AND t.id IS NULL
ORDER BY d.id
"""
)
)