from __future__ import annotations import argparse import json from datetime import datetime, timezone from pathlib import Path from typing import Any from ghostlight_state_store import ( append_evidence, load_branches, migrate_legacy_evidence, replace_evidence, save_branches, ) ROOT = Path(__file__).resolve().parents[1] STATE_DIR = ROOT / "state" MAP_PATH = STATE_DIR / "map.yaml" BRANCHES_PATH = STATE_DIR / "branches.json" EVIDENCE_PATH = STATE_DIR / "evidence.jsonl" COVERAGE_PATH = STATE_DIR / "corpus-coverage.json" def read_text(path: Path) -> str: return path.read_text(encoding="utf-8-sig") def write_text(path: Path, content: str) -> None: path.write_text(content, encoding="utf-8") def utc_stamp() -> str: return datetime.now(timezone.utc).replace(microsecond=0).isoformat() def extract_map_field(name: str) -> str | None: """Read one ` name:` field under `current_status`, plain or block scalar.""" prefix = f" {name}:" lines = read_text(MAP_PATH).splitlines() for index, line in enumerate(lines): if not line.startswith(prefix): continue value = line.split(":", 1)[1].strip() if value not in {">", "|", ">-", "|-"}: return value block: list[str] = [] for continuation in lines[index + 1 :]: if continuation.strip() and not continuation.startswith(" "): break block.append(continuation.strip()) joiner = "\n" if value.startswith("|") else " " return joiner.join(part for part in block if part).strip() or None return None def extract_active_subgoals() -> list[str]: lines = read_text(MAP_PATH).splitlines() results: list[str] = [] in_section = False for line in lines: if not in_section: if line.strip() == "active_subgoals:": in_section = True continue if line.startswith(" - "): results.append(line[4:].strip()) continue if line and not line.startswith(" "): break return results def load_coverage_summary() -> str | None: if not COVERAGE_PATH.exists(): return None data = json.loads(read_text(COVERAGE_PATH)) entries = data.get("entries", []) accepted = [ entry for entry in entries if entry.get("status") in {"accepted", "accepted_as_draft"} ] target = data.get("coverage_targets", {}).get("practical_first_broad_target", {}) minimum = target.get("minimum") maximum = target.get("maximum") if isinstance(minimum, int) and isinstance(maximum, int): return f"{len(accepted)} / {minimum}-{maximum} accepted fixtures" return f"{len(accepted)} accepted fixtures" def cmd_status(_: argparse.Namespace) -> int: branches = load_branches().get("branches", []) active = [branch for branch in branches if branch.get("status") == "active"] summary = extract_map_field("summary") or "(missing)" next_action = extract_map_field("next_action") or "(missing)" subgoals = extract_active_subgoals() print(f"Workspace: {ROOT}") print(f"Summary: {summary}") print(f"Next action: {next_action}") print(f"Active branches: {len(active)} / {len(branches)}") coverage = load_coverage_summary() if coverage: print(f"Coverage: {coverage}") if subgoals: print("Active subgoals:") for item in subgoals: print(f"- {item}") return 0 def cmd_add_evidence(args: argparse.Namespace) -> int: record: dict[str, Any] = { "ts": utc_stamp(), "type": args.type, "status": args.status, "note": args.note, } if args.branch: record["branch"] = args.branch append_evidence(record) print("Appended evidence record.") return 0 def cmd_compact_evidence(args: argparse.Namespace) -> int: records: list[dict[str, Any]] = [] for number, line in enumerate(read_text(Path(args.replacement)).splitlines(), 1): if not line.strip(): continue record = json.loads(line) missing = {"ts", "type", "status", "note"} - set(record) if not isinstance(record, dict) or missing: raise SystemExit(f"replacement line {number} lacks {sorted(missing)}") records.append(record) if not records: raise SystemExit("refusing to compact the ledger to nothing") archived, kept = replace_evidence(records) print(f"Archived {archived} record(s) to state/evidence.archive.jsonl; ledger now holds {kept}.") return 0 def cmd_migrate_evidence(_: argparse.Namespace) -> int: imported = migrate_legacy_evidence() print(f"Imported {imported} legacy evidence record(s) into CultCache.") return 0 def cmd_add_branch(args: argparse.Namespace) -> int: data = load_branches() branches = data.setdefault("branches", []) if any(branch.get("id") == args.id for branch in branches): raise SystemExit(f"Branch '{args.id}' already exists.") branches.append( { "id": args.id, "hypothesis": args.hypothesis, "status": "active", "artifacts": args.artifact or [], "notes": args.note or "", } ) save_branches(data) print(f"Added branch '{args.id}'.") return 0 def cmd_close_branch(args: argparse.Namespace) -> int: data = load_branches() for branch in data.get("branches", []): if branch.get("id") != args.id: continue branch["status"] = args.status if args.note: branch["notes"] = args.note save_branches(data) print(f"Updated branch '{args.id}' to status '{args.status}'.") return 0 raise SystemExit(f"Branch '{args.id}' was not found.") def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( description="Inspect and update Ghostlight state files." ) subparsers = parser.add_subparsers(dest="command", required=True) status_parser = subparsers.add_parser("status", help="Show a compact state summary.") status_parser.set_defaults(func=cmd_status) evidence_parser = subparsers.add_parser( "add-evidence", help="Append one distilled JSONL evidence record." ) evidence_parser.add_argument("--type", required=True) evidence_parser.add_argument("--status", required=True) evidence_parser.add_argument("--note", required=True) evidence_parser.add_argument("--branch") evidence_parser.set_defaults(func=cmd_add_evidence) compact_parser = subparsers.add_parser( "compact-evidence", help="Replace the evidence document with a distilled JSONL file; every current record is archived first.", ) compact_parser.add_argument("--replacement", required=True) compact_parser.set_defaults(func=cmd_compact_evidence) migrate_evidence_parser = subparsers.add_parser( "migrate-evidence", help="Import legacy JSONL evidence missing from the CultCache authority.", ) migrate_evidence_parser.set_defaults(func=cmd_migrate_evidence) add_branch_parser = subparsers.add_parser( "add-branch", help="Create a new active branch entry." ) add_branch_parser.add_argument("--id", required=True) add_branch_parser.add_argument("--hypothesis", required=True) add_branch_parser.add_argument("--artifact", action="append") add_branch_parser.add_argument("--note") add_branch_parser.set_defaults(func=cmd_add_branch) close_branch_parser = subparsers.add_parser( "close-branch", help="Close an existing branch." ) close_branch_parser.add_argument("--id", required=True) close_branch_parser.add_argument( "--status", required=True, choices=["accepted", "rejected", "archived"] ) close_branch_parser.add_argument("--note") close_branch_parser.set_defaults(func=cmd_close_branch) return parser def main() -> int: parser = build_parser() args = parser.parse_args() return args.func(args) if __name__ == "__main__": raise SystemExit(main())