44 records to 12: supersession chains collapsed to their survivors, per-commit receipts folded into the decision they served, standing pre-rebuild decisions merged into one record map.yaml already owns. Every removed record is archived verbatim. `compact-evidence` is now the one durable deletion path; the status tool reads folded scalars, and the compaction helper imports the map readers instead of carrying copies. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
244 lines
8 KiB
Python
244 lines
8 KiB
Python
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())
|