66 lines
3.2 KiB
Python
66 lines
3.2 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""Append corrections for published evidence records. Dry run unless --apply.
|
||
|
|
|
||
|
|
Original progress events are immutable. Corrections never clear the daily due
|
||
|
|
bit and never claim that the missing brief was recovered.
|
||
|
|
"""
|
||
|
|
import argparse
|
||
|
|
import json
|
||
|
|
import os
|
||
|
|
from pathlib import Path
|
||
|
|
import urllib.request
|
||
|
|
|
||
|
|
from verify_brief_durability import CORRECTION_TYPE, Origin, REPO, ROOT, brief_path, git, load_events
|
||
|
|
|
||
|
|
|
||
|
|
def main():
|
||
|
|
ap = argparse.ArgumentParser(description=__doc__)
|
||
|
|
ap.add_argument('--evidence', default='docs/evidence/2026-09-14-brief-completion-reconciliation.json')
|
||
|
|
ap.add_argument('--hub', default=os.environ.get('STATE_HUB_URL', 'http://127.0.0.1:8000'))
|
||
|
|
ap.add_argument('--apply', action='store_true')
|
||
|
|
args = ap.parse_args()
|
||
|
|
origin = Origin(ROOT)
|
||
|
|
path = Path(args.evidence)
|
||
|
|
if path.is_absolute() or '..' in path.parts:
|
||
|
|
raise ValueError('evidence must be a repository-relative path')
|
||
|
|
if not origin.blob(path.as_posix()):
|
||
|
|
raise RuntimeError('publish the evidence to origin/main before reconciliation')
|
||
|
|
evidence = json.loads(git(ROOT, 'show', f'{origin.sha}:{path.as_posix()}').stdout)
|
||
|
|
originals = {e['id']: e for e in load_events(args.hub, 10)}
|
||
|
|
existing = load_events(args.hub, 10, CORRECTION_TYPE)
|
||
|
|
pending = []
|
||
|
|
for record in evidence['corrections']:
|
||
|
|
original = originals[record['original_event_id']]
|
||
|
|
d = original['detail']
|
||
|
|
expected = brief_path(record['date'])
|
||
|
|
if (d.get('repo') != REPO or d.get('date') != record['date']
|
||
|
|
or d.get('path') != expected or record['path'] != expected
|
||
|
|
or record['disposition'] != 'false_completion' or origin.blob(expected)):
|
||
|
|
raise RuntimeError('original event or missing-file evidence changed; audit again')
|
||
|
|
if any(e.get('detail', {}).get('original_event_id') == original['id']
|
||
|
|
and origin.correction_recorded(e.get('detail', {})) for e in existing):
|
||
|
|
print(f"already reconciled {record['date']} {original['id']}")
|
||
|
|
continue
|
||
|
|
detail = {k: record[k] for k in ('original_event_id', 'date', 'path', 'disposition', 'reason')}
|
||
|
|
detail.update(repo=REPO, evidence_sha=origin.sha, evidence_path=path.as_posix())
|
||
|
|
pending.append(dict(event_type=CORRECTION_TYPE, author='codex',
|
||
|
|
summary=f"FI {record['date']}: acknowledge false completion; brief remains missing",
|
||
|
|
detail=detail))
|
||
|
|
# Validate all records before making the first write.
|
||
|
|
for event in pending:
|
||
|
|
if not args.apply:
|
||
|
|
print(json.dumps(event))
|
||
|
|
continue
|
||
|
|
original_id = event['detail']['original_event_id']
|
||
|
|
req = urllib.request.Request(f"{args.hub.rstrip('/')}/progress/",
|
||
|
|
data=json.dumps(event).encode(), method='POST',
|
||
|
|
headers={'Content-Type': 'application/json', 'Idempotency-Key': f'fi-false-completion-{original_id}'})
|
||
|
|
with urllib.request.urlopen(req, timeout=15) as response:
|
||
|
|
created = json.load(response)
|
||
|
|
print(f"reconciled {event['detail']['date']} correction={created['id']}")
|
||
|
|
return 0
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == '__main__':
|
||
|
|
raise SystemExit(main())
|