179 lines
6.1 KiB
Python
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()
|