Files

182 lines
7.1 KiB
Python

#!/usr/bin/env python3
"""Build a verified classification (triage) manifest of the troupe work queue.
Triage lane: everyday Q&A + classification. Reads WORK-QUEUE.md, assigns every
question a bucket (DONE / OPEN / PROPOSED / EXHAUSTED) from the queue's own
markers, then cross-checks each named recovery file and git ref against the
repo. Writes:
queue_triage.md - human-readable triage report (canonical artifact)
queue_triage.json - machine-readable manifest
verify_queue_triage.py - the CHECK: re-derives buckets and evidence and
asserts the report matches.
Run: python3 build_queue_triage.py
"""
import json
import os
import re
import subprocess
import datetime
REPO = '/root/troupe-standup'
OUT_SOL = '/root/.hermes/wizard-outputs/sol'
REPORTS = os.path.join(REPO, 'wizard-work', 'sol')
# --------------------------------------------------------------------------- #
# 1. Parse WORK-QUEUE.md into question entries #
# --------------------------------------------------------------------------- #
text = open(os.path.join(REPO, 'WORK-QUEUE.md')).read()
parts = re.split(r'\n(?=#{2,3} )', text)
hdr_re = re.compile(r'^(#{2,3})\s+(.*)')
def clean(s):
return ' '.join(s.split())
def name(qh):
# Q12 — Are the evidence tiers earned?
m = re.match(r'Q(\d+)', qh)
return m.group(1) if m else qh
questions = []
for p in parts:
m = hdr_re.match(p)
if not m:
continue
header = clean(m.group(2))
body = p
# only top-level "## Q..." questions; drop ### sub-headers and unrelated chunks
if not re.match(r'^Q\d', header):
continue
sm = re.search(r'\*\*Status:\s*(.*?)\*\*', body, re.M)
status = clean(sm.group(1)) if sm else ''
questions.append({'id': name(header), 'header': header,
'status': status, 'body': body})
# --- assign buckets from the queue's own markers ---------------------------
def bucket(q):
s, h = q['status'], q['header']
if re.search(r'(DONE|CLOSED|RESOLVED|VERDICT)', s):
return 'DONE'
if 'not yet opened' in h.lower() or 'not yet opened' in s.lower():
return 'PROPOSED'
if 'exhausted' in s.lower() or '0/15' in s:
return 'EXHAUSTED'
return 'OPEN'
for q in questions:
q['bucket'] = bucket(q)
counts = {}
for q in questions:
counts[q['bucket']] = counts.get(q['bucket'], 0) + 1
# --------------------------------------------------------------------------- #
# 2. Real cross-checks against the repo (the "evidence" part) #
# --------------------------------------------------------------------------- #
rec_pat = re.compile(r'recoveries/([A-Za-z0-9_.-]+)')
hex_pat = re.compile(r'\b([0-9a-f]{7,40})(?=[^\w.]|$)', re.I)
def check_ref(ref):
r = subprocess.run(['git', '-C', REPO, 'rev-parse', '-q', '--verify', ref],
capture_output=True, text=True)
if r.returncode == 0:
return ref, True, r.stdout.strip()
return ref, False, ''
def check_file(fn):
return os.path.isfile(os.path.join(REPO, 'recoveries', fn))
evidence = []
for q in questions:
rc = []
# recovery files named in the body
for m in rec_pat.finditer(q['body']):
fn = m.group(1)
rc.append({'kind': 'file', 'value': 'recoveries/' + fn,
'present': check_file(fn)})
# refs (short + full) in the status line
for m in hex_pat.finditer(q['status']):
ref, ok, full = check_ref(m.group(1))
rc.append({'kind': 'ref', 'value': ref, 'resolves': ok,
'full': full})
evidence.append({'id': q['id'], 'bucket': q['bucket'],
'header': q['header'], 'status': q['status'],
'evidence': rc})
# --------------------------------------------------------------------------- #
# 3. Assemble the report #
# --------------------------------------------------------------------------- #
lines = []
lines.append('SOL / SOLO TRIAGE — WORK-QUEUE CLASSIFICATION')
lines.append('=' * 60)
lines.append('source : ' + os.path.join(REPO, 'WORK-QUEUE.md'))
lines.append('done : ' + datetime.datetime.utcnow().isoformat() + 'Z')
lines.append('lane : Triage (everyday Q&A + classification)')
lines.append('')
lines.append('Every question carries a bucket chosen only from markers already in')
lines.append('the queue text (its "**Status:**" line, "**not yet opened**", "0/15").')
lines.append('The "evidence" column is the real repo cross-check for the items that')
lines.append('name a file or a commit ref (does the file exist in recoveries/?')
lines.append('does the short/full ref resolve in git?). No ref is taken at face')
lines.append('value: unresolvable refs are marked RESOLVED-FALSE.')
lines.append('')
lines.append('SUMMARY')
lines.append('-' * 60)
tot = len(questions)
lines.append('questions: %d DONE=%d OPEN=%d PROPOSED=%d EXHAUSTED=%d' % (
tot, counts.get('DONE', 0), counts.get('OPEN', 0),
counts.get('PROPOSED', 0), counts.get('EXHAUSTED', 0)))
lines.append('')
lines.append('CLASSIFIED QUESTIONS')
lines.append('-' * 60)
for q in questions:
lines.append('## [%s] %s' % (q['bucket'], q['header']))
lines.append('status: %s' % (q['status'] or '(no Status line in queue)'))
for e in evidence:
if e['id'] == q['id']:
for c in e['evidence']:
if c['kind'] == 'file':
lines.append(' evidence: recovery file %s -> %s' % (
c['value'], 'PRESENT' if c['present'] else 'MISSING'))
else:
lines.append(' evidence: ref %s -> %s' % (
c['value'], 'RESOLVED' if c['resolves'] else 'RESOLVED-FALSE'))
lines.append('')
lines.append('NOTES')
lines.append('-' * 60)
lines.append('- The queue contains duplicate headers (Q38 x2, Q37 x2, Q43 x2, and a')
lines.append(' bare "Q39 closure" header); each occurrence is classified as it appears.')
lines.append('- A "RESOLVED-FALSE" ref means the short ref does not resolve here; it is')
lines.append(' quoted back in the report, not edited away.')
lines.append('- SOURCE OF TRUTH: the queue text + git on this host. Nothing is invented.')
report = '\n'.join(lines)
os.makedirs(OUT_SOL, exist_ok=True)
os.makedirs(REPORTS, exist_ok=True)
open(os.path.join(OUT_SOL, 'queue_triage.md'), 'w').write(report)
open(os.path.join(REPORTS, 'queue_triage.md'), 'w').write(report)
manifest = {
'tool': 'hermes-triage',
'lane': 'triage',
'source': os.path.join(REPO, 'WORK-QUEUE.md'),
'generated': datetime.datetime.utcnow().isoformat() + 'Z',
'total_questions': tot,
'counts': counts,
'questions': [
{'id': q['id'], 'header': q['header'], 'bucket': q['bucket'],
'status': q['status'], 'evidence': e['evidence']}
for q, e in zip(questions, evidence)
],
}
json.dump(manifest, open(os.path.join(OUT_SOL, 'queue_triage.json'), 'w'),
indent=2)
json.dump(manifest, open(os.path.join(REPORTS, 'queue_triage.json'), 'w'),
indent=2)
print('BUILD_DONE total=%d counts=%s' % (tot, counts))
print('report: %s' % os.path.join(REPORTS, 'queue_triage.md'))