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>
78 lines
2.2 KiB
Python
78 lines
2.2 KiB
Python
"""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()
|