Files
troupe-standup/wizard-work/sol/workqueue_triage.py

179 lines
6.1 KiB
Python

#!/usr/bin/env python3
"""Classify every work-queue item in the troupe repo by its recorded Status line.
Triage lane (sol): produce a verified, human-readable + machine-readable snapshot
of the live work queue. Classification rule (stated in the report so it is
inspectable and recomputable):
OPEN <- recorded status is EMPTY (never recorded) OR the lowercased status
text contains one of: open, unclaimed, draft, pending.
CLOSED <- anything recorded as DONE / COMPLETE / CLOSED / RESOLVED etc.
that carries none of the OPEN signals above.
Q-item detection: lines beginning "## Q<N> — " (space + em dash), so that
derived items such as "## Q39 closure", "## Q43 closure" and "## Q45 §9
follow-on" are excluded and the two duplicate "## Q37" headers collapse to the
latest occurrence.
Reads: WORK-QUEUE.md (repo root)
Writes: sol/workqueue-triage.{json,md}
"""
import hashlib
import json
import os
import re
import sys
from datetime import datetime, timezone
REPO = "/root/troupe-standup"
REPO_WIZARD_OUT = os.path.join(REPO, "wizard-work", "sol")
SRC = os.path.join(REPO, "WORK-QUEUE.md")
OUT_DIR = "/root/.hermes/wizard-outputs/sol"
OUT_JSON = os.path.join(OUT_DIR, "workqueue-triage.json")
OUT_MD = os.path.join(OUT_DIR, "workqueue-triage.md")
REPO_JSON = os.path.join(REPO_WIZARD_OUT, "workqueue-triage.json")
REPO_MD = os.path.join(REPO_WIZARD_OUT, "workqueue-triage.md")
HEADER = re.compile(r"^## Q(\d+)\s+—\s+(.*)$")
# line beginning "**Status:" or "*Status:" or "**Status:**" etc.
STATUS_LINE = re.compile(r"^\s*\*\*?Status:\s*(.*)$")
OPEN_SIGNALS = ("open", "unclaimed", "draft", "pending")
def classify(status_text):
t = status_text.lower()
if not status_text.strip():
return True
return any(sig in t for sig in OPEN_SIGNALS)
def extract_status(lines, start):
for i in range(start, len(lines)):
line = lines[i]
m = STATUS_LINE.match(line)
if m:
body = m.group(1).strip()
# strip a matching pair of * / ** surrounding the body
while len(body) >= 2 and ((body.startswith("**") and body.endswith("**"))
or (body.startswith("*") and body.endswith("*"))):
body = body[1:-1].strip()
return body
return ""
def parse_queue(path):
with open(path, encoding="utf-8") as fh:
lines = fh.read().split("\n")
records = []
last_end = 0
for idx, line in enumerate(lines):
m = HEADER.match(line)
if not m:
continue
qid = m.group(1)
title = m.group(2).strip()
status = extract_status(lines, idx + 1)
records.append({
"id": qid,
"title": title,
"status_text": status,
"open": classify(status),
})
last_end = idx
# collapse duplicate ids (e.g. the two "## Q37" blocks) to the last one
deduped = {}
for rec in reversed(records):
deduped[rec["id"]] = rec
return [deduped[k] for k in sorted(deduped, key=int)]
def md_report(records):
total = len(records)
open_n = sum(1 for r in records if r["open"])
closed_n = total - open_n
top = next((r["id"] for r in records if r["open"]), None)
lines = []
lines.append("# Work-queue triage — sol")
lines.append("")
lines.append("Source: `WORK-QUEUE.md` (sha256 %s)" % sha256_file(SRC))
lines.append("Generated: %s" % generated_utc())
lines.append("Classification rule: OPEN = recorded status is empty or carries "
"'open'/'unclaimed'/'draft'/'pending'; otherwise CLOSED.")
lines.append("")
lines.append("## Summary")
lines.append("")
lines.append("- total items: %d" % total)
lines.append("- OPEN: %d" % open_n)
lines.append("- CLOSED: %d" % closed_n)
lines.append("- top open item (earliest id): %s" % top)
lines.append("")
lines.append("## Records")
lines.append("")
lines.append("| Q | title | status | open |")
lines.append("|---|---|---|---|")
for r in records:
st = r["status_text"][:90]
lines.append("| Q%d | %s | %s | %s |" % (int(r["id"]), r["title"], st, "true" if r["open"] else "false"))
lines.append("")
lines.append("## Open items (%d)" % open_n)
lines.append("")
if not open_n:
lines.append("- none")
for r in records:
if r["open"]:
lines.append("- **Q%d** — %s" % (int(r["id"]), r["title"]))
lines.append("")
return "\n".join(lines)
def sha256_file(path):
h = hashlib.sha256()
with open(path, "rb") as fh:
for chunk in iter(lambda: fh.read(1 << 20), b""):
h.update(chunk)
return h.hexdigest()
def generated_utc():
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
def main():
os.makedirs(OUT_DIR, exist_ok=True)
if not os.path.exists(SRC):
print("FATAL: source %s missing" % SRC, file=sys.stderr)
sys.exit(1)
records = parse_queue(SRC)
art = {
"generated_utc": generated_utc(),
"source": SRC,
"source_sha256": sha256_file(SRC),
"rule": "OPEN = recorded status is empty or carries 'open'/'unclaimed'/'draft'/'pending'; else CLOSED",
"records": records,
"summary": {
"total": len(records),
"open": sum(1 for r in records if r["open"]),
"closed": sum(1 for r in records if not r["open"]),
"top_open_item": next((r["id"] for r in records if r["open"]), None),
},
}
with open(OUT_JSON, "w", encoding="utf-8") as fh:
json.dump(art, fh, indent=2)
fh.write("\n")
with open(OUT_MD, "w", encoding="utf-8") as fh:
fh.write(md_report(records))
fh.write("\n")
for dst_json, dst_md in ((REPO_JSON, REPO_MD),):
os.makedirs(REPO_WIZARD_OUT, exist_ok=True)
json.dump(art, open(dst_json, "w", encoding="utf-8"), indent=2)
open(dst_json, "a", encoding="utf-8").write("\n")
open(dst_md, "w", encoding="utf-8").write(md_report(records))
open(dst_md, "a", encoding="utf-8").write("\n")
print(md_report(records))
if __name__ == "__main__":
main()