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>
130 lines
3.8 KiB
Python
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
|
|
"""
|
|
)
|
|
)
|