166 lines
5.4 KiB
Python
166 lines
5.4 KiB
Python
import hashlib
|
|
import pathlib
|
|
import re
|
|
import sys
|
|
|
|
SRC = '/root/troupe-standup/WORK-QUEUE.md'
|
|
TSV = '/root/troupe-standup/wizard-work/ledger/workqueue-q-census.tsv'
|
|
|
|
def _strip_decorations(s):
|
|
s = s.rstrip()
|
|
start = s.rfind('*(')
|
|
if start == -1:
|
|
return s
|
|
i = start + 1
|
|
depth = 0
|
|
for j in range(i, len(s)):
|
|
if s[j] == '(':
|
|
depth += 1
|
|
elif s[j] == ')':
|
|
depth -= 1
|
|
if depth == 0:
|
|
return s[:start] + s[j + 2:].strip()
|
|
return s
|
|
|
|
def parse_sections(text):
|
|
lines = text.splitlines()
|
|
sections = []
|
|
counts = {}
|
|
cur = None
|
|
for raw in lines:
|
|
m = re.match(r'^## (Q\d+)(.*)$', raw)
|
|
if m:
|
|
if cur is not None:
|
|
sections.append(cur)
|
|
qid = m.group(1)
|
|
rest = m.group(2).strip()
|
|
is_closure = rest.startswith('closure')
|
|
if not is_closure:
|
|
counts[qid] = counts.get(qid, 0) + 1
|
|
qid_out = qid if counts[qid] == 1 else f'{qid} #{counts[qid]}'
|
|
else:
|
|
qid_out = qid + ' closure'
|
|
title = clean_title(_strip_decorations(rest))
|
|
cur = {
|
|
'q_id': qid_out,
|
|
'rest': rest,
|
|
'title': title,
|
|
'is_closure': is_closure,
|
|
'status_raw': None,
|
|
'body_lines': 0,
|
|
}
|
|
continue
|
|
if cur is not None:
|
|
cur['body_lines'] += 1
|
|
sm = re.match(r'\*\*Status:\s*(.*)$', raw)
|
|
if sm and cur['status_raw'] is None:
|
|
s = sm.group(1).strip()
|
|
if s.endswith('**'):
|
|
s = s[:-2].rstrip()
|
|
cur['status_raw'] = s
|
|
if cur is not None:
|
|
sections.append(cur)
|
|
return sections
|
|
|
|
def clean_title(title):
|
|
return title.lstrip('—-').strip()
|
|
|
|
def main():
|
|
text = pathlib.Path(SRC).read_text()
|
|
sections = parse_sections(text)
|
|
|
|
errors = []
|
|
|
|
# 1. Section count
|
|
if len(sections) != 97:
|
|
errors.append(f'expected 97 sections, got {len(sections)}')
|
|
|
|
# 2. Closure count
|
|
closures = [s for s in sections if s['is_closure']]
|
|
if len(closures) != 8:
|
|
errors.append(f'expected 8 closures, got {len(closures)}')
|
|
|
|
# 3. Deterministic regeneration: recompute TSV content and diff md5 vs committed
|
|
rows = []
|
|
for s in sections:
|
|
rows.append('\t'.join([
|
|
s['q_id'], s['rest'].rstrip(), s['status_raw'] or '',
|
|
re.search(r'[a-f0-9]{7,}', s['rest'] + ' ' + s['q_id']).group(0) if re.search(r'[a-f0-9]{7,}', s['rest'] + ' ' + s['q_id']) else '',
|
|
str(s['body_lines']),
|
|
'1' if s['is_closure'] else '0',
|
|
]))
|
|
expected_tsv = '#q_id\tannex\tstatus_raw\tkey_hash\tbody_lines\tclosure\n'
|
|
for r in rows:
|
|
expected_tsv += r + '\n'
|
|
expected_md5 = hashlib.md5(expected_tsv.encode('utf-8')).hexdigest()
|
|
actual_md5 = hashlib.md5(pathlib.Path(TSV).read_bytes()).hexdigest()
|
|
if expected_md5 != actual_md5:
|
|
errors.append(f'md5 mismatch: expected {expected_md5}, got {actual_md5}')
|
|
|
|
# 4. Anchor checks via a lookup map
|
|
lookup = {}
|
|
for s in sections:
|
|
lookup[s['q_id']] = s
|
|
|
|
def norm(s):
|
|
if s is None:
|
|
return ''
|
|
return s
|
|
|
|
def check_attr(qid, attr, expected_contains):
|
|
if qid not in lookup:
|
|
errors.append(f'{qid} missing')
|
|
return
|
|
v = norm(lookup[qid].get(attr))
|
|
if expected_contains not in v:
|
|
errors.append(f'{qid}.{attr} mismatch: {v!r} does not contain {expected_contains!r}')
|
|
|
|
# 5. Exact title anchor
|
|
if 'Q1' not in lookup or lookup['Q1']['rest'].lstrip('—-').strip() != 'Recover the fifteen silent rooms':
|
|
errors.append('Q1 title mismatch')
|
|
|
|
def status_of(qid):
|
|
return norm(lookup.get(qid, {}).get('status_raw'))
|
|
|
|
def hash_of(qid):
|
|
h = re.search(r'[a-f0-9]{7,}', lookup.get(qid, {}).get('rest', '') + ' ' + qid)
|
|
return h.group(0) if h else ''
|
|
|
|
if 'CLOSED — 0/15 recovered' not in norm(status_of('Q1')):
|
|
errors.append('Q1 status mismatch')
|
|
if 'DONE — recovered.' not in norm(status_of('Q2')):
|
|
errors.append('Q2 status mismatch')
|
|
if 'OPEN — awaiting Alexander' not in norm(status_of('Q45')):
|
|
errors.append('Q45 status mismatch')
|
|
if 'open and unclaimed' not in norm(status_of('Q75')):
|
|
errors.append('Q75 status mismatch')
|
|
if 'open and unclaimed' not in norm(status_of('Q88')):
|
|
errors.append('Q88 status mismatch')
|
|
if 'DONE at `9878f12`' not in norm(status_of('Q93 closure')):
|
|
errors.append('Q93 closure status mismatch')
|
|
if 'DONE' not in norm(status_of('Q15')):
|
|
errors.append('Q15 status mismatch')
|
|
|
|
if hash_of('Q75') != 'b82b023':
|
|
errors.append(f'Q75 hash {hash_of("Q75")!r}')
|
|
if hash_of('Q88') != 'b0a2886':
|
|
errors.append(f'Q88 hash {hash_of("Q88")!r}')
|
|
if hash_of('Q93 closure') != 'd56105a':
|
|
errors.append(f'Q93 closure hash {hash_of("Q93 closure")!r}')
|
|
|
|
# 6. Uniqueness of q_ids
|
|
qids = [s['q_id'] for s in sections]
|
|
if len(set(qids)) != len(qids):
|
|
errors.append('duplicate q_ids present')
|
|
|
|
if errors:
|
|
print('FAIL')
|
|
for e in errors:
|
|
print(' -', e)
|
|
sys.exit(1)
|
|
print('PASS: 97 sections, 8 closures, regeneration md5 match, all anchors verified')
|
|
sys.exit(0)
|
|
|
|
if __name__ == '__main__':
|
|
main()
|