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>
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
"""Output formatting — table (default for TTY), json (for scripting)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
import sys
|
||||
from typing import Iterable, Mapping, Sequence
|
||||
|
||||
|
||||
def _is_tty() -> bool:
|
||||
return sys.stdout.isatty()
|
||||
|
||||
|
||||
def rows_to_dicts(rows: Iterable[sqlite3.Row]) -> list[dict]:
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
def emit(data, *, mode: str | None = None) -> None:
|
||||
"""Emit `data` as either JSON or a human table.
|
||||
|
||||
`mode` is "json" | "table" | None (auto: table on TTY, json otherwise).
|
||||
"""
|
||||
if mode is None:
|
||||
mode = "table" if _is_tty() else "json"
|
||||
if mode == "json":
|
||||
print(json.dumps(data, indent=2, default=str))
|
||||
return
|
||||
# table
|
||||
if isinstance(data, dict) and "rows" in data:
|
||||
_print_table(data["rows"])
|
||||
return
|
||||
if isinstance(data, list):
|
||||
_print_table(data)
|
||||
return
|
||||
if isinstance(data, dict):
|
||||
for k, v in data.items():
|
||||
print(f"{k}: {v}")
|
||||
return
|
||||
print(data)
|
||||
|
||||
|
||||
def _print_table(rows: Sequence[Mapping]) -> None:
|
||||
if not rows:
|
||||
print("(no rows)")
|
||||
return
|
||||
cols = list(rows[0].keys())
|
||||
widths = {c: len(c) for c in cols}
|
||||
for r in rows:
|
||||
for c in cols:
|
||||
widths[c] = max(widths[c], len(str(r.get(c, ""))))
|
||||
header = " ".join(c.ljust(widths[c]) for c in cols)
|
||||
print(header)
|
||||
print(" ".join("-" * widths[c] for c in cols))
|
||||
for r in rows:
|
||||
print(" ".join(str(r.get(c, "")).ljust(widths[c]) for c in cols))
|
||||
|
||||
|
||||
def emit_board(columns: dict[str, list[sqlite3.Row]], *, mode: str | None = None) -> None:
|
||||
if mode is None:
|
||||
mode = "table" if _is_tty() else "json"
|
||||
if mode == "json":
|
||||
print(json.dumps(
|
||||
{status: rows_to_dicts(rows) for status, rows in columns.items()},
|
||||
indent=2,
|
||||
default=str,
|
||||
))
|
||||
return
|
||||
for status, rows in columns.items():
|
||||
print(f"== {status.upper()} ({len(rows)}) ==")
|
||||
if not rows:
|
||||
print(" (empty)")
|
||||
continue
|
||||
for row in rows:
|
||||
prio = row["priority"] or "medium"
|
||||
who = row["assigned_to"] or "-"
|
||||
print(f" {row['id']} [{prio:8}] @{who:10} {row['title']}")
|
||||
print()
|
||||
Reference in New Issue
Block a user