From 8bd2e5ff92e07513dc8bbe09a7540a3ca3946eab Mon Sep 17 00:00:00 2001 From: tegwick Date: Mon, 14 Sep 2026 00:37:51 +0200 Subject: [PATCH] Verify brief publication against live origin and record historical gaps Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a09cbd-43c1-79f3-809e-1ee97b40b64d --- README.md | 4 +- docs/brief-durability-audit.md | 66 +++++ docs/daily-brief-playbook.md | 8 + ...09-14-brief-completion-reconciliation.json | 98 +++++++ docs/recurrence-ops.md | 8 + docs/state-hub-delivery.md | 8 + scripts/reconcile_brief_completions.py | 65 +++++ scripts/test_brief_durability.py | 136 ++++++++++ scripts/verify_brief_durability.py | 254 ++++++++++++------ ...4-operational-loop-and-scaleway-reserve.md | 8 +- 10 files changed, 565 insertions(+), 90 deletions(-) create mode 100644 docs/brief-durability-audit.md create mode 100644 docs/evidence/2026-09-14-brief-completion-reconciliation.json create mode 100644 scripts/reconcile_brief_completions.py create mode 100644 scripts/test_brief_durability.py diff --git a/README.md b/README.md index 51f1661..e657bb8 100644 --- a/README.md +++ b/README.md @@ -88,11 +88,13 @@ Never stage model weights in WSL. See [streaming operations](docs/streaming-rese **Durability check** (hub events vs git files): ```bash -python3 scripts/verify_brief_durability.py --require-hub +python3 scripts/verify_brief_durability.py --require-hub --allow-reconciled ``` --- +Historical gaps remain visible; [audit and reconciliation contract](docs/brief-durability-audit.md). + ## License MIT — see [`LICENSE`](LICENSE). Model weights retain **upstream** licenses. diff --git a/docs/brief-durability-audit.md b/docs/brief-durability-audit.md new file mode 100644 index 0000000..91c4a70 --- /dev/null +++ b/docs/brief-durability-audit.md @@ -0,0 +1,66 @@ +# Daily brief durability audit + +The verifier resolves the live `origin/main` commit with `git ls-remote` and +reads that immutable Git tree. If necessary, it fetches missing objects. Local +files, local commits and stale remote-tracking refs cannot establish completion. +An unreachable remote or Hub is an error (exit 2), never a successful check. +Hub progress reads are paginated; repeating/malformed pages fail closed. + +```bash +python3 scripts/verify_brief_durability.py --require-hub +python3 scripts/verify_brief_durability.py --allow-reconciled --json +python3 scripts/verify_brief_durability.py --allow-reconciled --require-date 2026-09-14 +``` + +The last command is the acceptance check for the September 14 scheduled run. +Before that run publishes, it must fail. A known historical gap cannot satisfy +`--require-date`. + +For events dated September 13, 2026 onward (or any event containing push +attestation), the verifier requires `pushed=true`, the canonical date/path, +and a full `origin_sha` that is an ancestor of the live remote head and contains +the nonempty brief. The current remote tree must also contain the file. +Earlier events with files on origin are reported separately as legacy evidence; +the audit does not invent historical push attestations. + +## September 7–11 reconciliation + +The five original `fi_daily_brief` events remain intact in the append-only Hub +log. The evidence file +[`2026-09-14-brief-completion-reconciliation.json`](evidence/2026-09-14-brief-completion-reconciliation.json) +records exact event IDs, original details and the audited remote commit. No +missing brief is reconstructed or counted as successful. + +After publishing this evidence to origin, append one correction per event: + +```bash +python3 scripts/reconcile_brief_completions.py # review, read-only +python3 scripts/reconcile_brief_completions.py --apply # append corrections +``` + +The helper validates every source event and missing path before writing. It +uses stable idempotency keys and skips already recorded corrections. Correction +events use `fi_daily_brief_reconciliation`, never the completion event type. + +The verifier accepts a correction only when its event ID/date/path/disposition/ +reason matches the evidence JSON in a published ancestor commit. Default strict +mode still fails on these missing dates. `--allow-reconciled` explicitly allows +the acknowledged historical debt, prints every gap, and fails on any new gap or +invalid attestation. + +## Next-run readiness (2026-09-14 Berlin) + +Temporal schedule `activity-schedule-3169ab1f-882b-59c7-9763-d014dc96f4fc` is +unpaused; its next action is September 14 at 07:30 Europe/Berlin (05:30 UTC). +The claim-loop service is active and was restarted after executor commit +`11020e8` (push-before-completion). Keep its FI checkout synchronized before the +run: the executor pushes its current branch and does not pull automatically. + +The activity-core resolver also needs the publication contract: only an event +with `pushed=true`, a full SHA, canonical path and matching `detail.date` may +clear today's due bit. An event for yesterday delivered today must leave today +due. The Git audit remains the authoritative check of actual origin objects; +the resolver checks the executor's attestation. + +This preflight is not evidence that the future scheduled run succeeded. Run +the exact-date audit after publication and retain its result. diff --git a/docs/daily-brief-playbook.md b/docs/daily-brief-playbook.md index 11fb03c..241b4a3 100644 --- a/docs/daily-brief-playbook.md +++ b/docs/daily-brief-playbook.md @@ -141,3 +141,11 @@ The baseline survey (2026-07-24) already covers the field. The **first daily** should only list changes **after** that document’s research window. If nothing moved, a short no-delta brief still completes the rhythm and posts `fi_daily_brief`. + +## Durability audit and historical corrections + +See [the Git-backed audit contract](brief-durability-audit.md) for live-origin verification, +paginated Hub reads, and append-only correction records. `--allow-reconciled` +explicitly acknowledges the five missing September 7–11 briefs; it never +counts them as successful. Use `--require-date YYYY-MM-DD` to prove a scheduled +day has a valid published completion. An unavailable Hub/remote fails the audit. diff --git a/docs/evidence/2026-09-14-brief-completion-reconciliation.json b/docs/evidence/2026-09-14-brief-completion-reconciliation.json new file mode 100644 index 0000000..2b6425a --- /dev/null +++ b/docs/evidence/2026-09-14-brief-completion-reconciliation.json @@ -0,0 +1,98 @@ +{ + "schema_version": 1, + "repo": "freedom-intelligence", + "checked_origin_sha": "8a65297d56da216eea1e4cea362da3bfbd68fff6", + "corrections": [ + { + "original_event_id": "5d9461ef-bf99-44b2-90fe-d8a3bf5524f9", + "date": "2026-09-07", + "path": "briefs/2026/09/2026-09-07.md", + "disposition": "false_completion", + "reason": "Executor reported a local commit without origin publication; no brief exists in the audited origin/main tree.", + "original_created_at": "2026-09-07T05:30:15.016103Z", + "original_detail": { + "date": "2026-09-07", + "path": "briefs/2026/09/2026-09-07.md", + "repo": "freedom-intelligence", + "wrote": true, + "executor": "rein-aharness", + "committed": true, + "skipped_existing": false, + "collection_candidates": 0 + } + }, + { + "original_event_id": "90b7719f-4bcf-475b-8042-715ee8bc8127", + "date": "2026-09-08", + "path": "briefs/2026/09/2026-09-08.md", + "disposition": "false_completion", + "reason": "Executor reported a local commit without origin publication; no brief exists in the audited origin/main tree.", + "original_created_at": "2026-09-08T05:30:26.872826Z", + "original_detail": { + "date": "2026-09-08", + "path": "briefs/2026/09/2026-09-08.md", + "repo": "freedom-intelligence", + "wrote": true, + "executor": "rein-aharness", + "committed": true, + "skipped_existing": false, + "collection_candidates": 2 + } + }, + { + "original_event_id": "54754f04-5175-4755-8473-ee42ebe95232", + "date": "2026-09-09", + "path": "briefs/2026/09/2026-09-09.md", + "disposition": "false_completion", + "reason": "Executor reported a local commit without origin publication; no brief exists in the audited origin/main tree.", + "original_created_at": "2026-09-09T05:30:22.905105Z", + "original_detail": { + "date": "2026-09-09", + "path": "briefs/2026/09/2026-09-09.md", + "repo": "freedom-intelligence", + "wrote": true, + "executor": "rein-aharness", + "committed": true, + "skipped_existing": false, + "collection_candidates": 0 + } + }, + { + "original_event_id": "1dc604ca-43d2-49de-a39b-dd90f05b10a8", + "date": "2026-09-10", + "path": "briefs/2026/09/2026-09-10.md", + "disposition": "false_completion", + "reason": "Executor reported a local commit without origin publication; no brief exists in the audited origin/main tree.", + "original_created_at": "2026-09-10T05:30:31.005526Z", + "original_detail": { + "date": "2026-09-10", + "path": "briefs/2026/09/2026-09-10.md", + "repo": "freedom-intelligence", + "wrote": true, + "executor": "rein-aharness", + "committed": true, + "skipped_existing": false, + "collection_candidates": 1 + } + }, + { + "original_event_id": "84144b03-dce4-4623-9191-5e36fe3c2692", + "date": "2026-09-11", + "path": "briefs/2026/09/2026-09-11.md", + "disposition": "false_completion", + "reason": "Executor reported a local commit without origin publication; no brief exists in the audited origin/main tree.", + "original_created_at": "2026-09-11T05:30:13.749386Z", + "original_detail": { + "date": "2026-09-11", + "path": "briefs/2026/09/2026-09-11.md", + "repo": "freedom-intelligence", + "wrote": true, + "executor": "rein-aharness", + "committed": true, + "skipped_existing": false, + "collection_candidates": 1 + } + } + ], + "interpretation": "These five dates remain missing. Append-only corrections acknowledge false completions; they do not create briefs, claim recovery, or satisfy a required-date check." +} diff --git a/docs/recurrence-ops.md b/docs/recurrence-ops.md index f880bcc..dac8d4f 100644 --- a/docs/recurrence-ops.md +++ b/docs/recurrence-ops.md @@ -143,3 +143,11 @@ python3 scripts/verify_brief_durability.py --require-hub - Skipping the completion event (leaves `due=true` forever) - Posting `fi_daily_brief` for a local-only commit (clears `due`, loses the day) - Reusing `railiance-platform-pg-backup` for weights (30-day expiry) + +## Durability audit and historical corrections + +See [the Git-backed audit contract](brief-durability-audit.md) for live-origin verification, +paginated Hub reads, and append-only correction records. `--allow-reconciled` +explicitly acknowledges the five missing September 7–11 briefs; it never +counts them as successful. Use `--require-date YYYY-MM-DD` to prove a scheduled +day has a valid published completion. An unavailable Hub/remote fails the audit. diff --git a/docs/state-hub-delivery.md b/docs/state-hub-delivery.md index 44f6408..a02ef27 100644 --- a/docs/state-hub-delivery.md +++ b/docs/state-hub-delivery.md @@ -77,3 +77,11 @@ tunnel ports). Check health before declaring hub down. | ---- | ------ | | 2026-07-23 | Stub created | | 2026-07-24 | `fi_daily_brief` contract + activity-core linkage documented | + +## Durability audit and historical corrections + +See [the Git-backed audit contract](brief-durability-audit.md) for live-origin verification, +paginated Hub reads, and append-only correction records. `--allow-reconciled` +explicitly acknowledges the five missing September 7–11 briefs; it never +counts them as successful. Use `--require-date YYYY-MM-DD` to prove a scheduled +day has a valid published completion. An unavailable Hub/remote fails the audit. diff --git a/scripts/reconcile_brief_completions.py b/scripts/reconcile_brief_completions.py new file mode 100644 index 0000000..f66b770 --- /dev/null +++ b/scripts/reconcile_brief_completions.py @@ -0,0 +1,65 @@ +#!/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()) diff --git a/scripts/test_brief_durability.py b/scripts/test_brief_durability.py new file mode 100644 index 0000000..f59f769 --- /dev/null +++ b/scripts/test_brief_durability.py @@ -0,0 +1,136 @@ +import io +import json +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch + +from verify_brief_durability import Origin, assess, brief_path, git, load_events + + +class DurabilityTests(unittest.TestCase): + def setUp(self): + self.tmp = tempfile.TemporaryDirectory() + self.addCleanup(self.tmp.cleanup) + self.root = Path(self.tmp.name) + self.remote = self.root / 'remote.git' + self.repo = self.root / 'repo' + self.repo.mkdir() + git(self.root, 'init', '--bare', str(self.remote)) + git(self.repo, 'init', '-b', 'main') + git(self.repo, 'config', 'user.name', 'Test') + git(self.repo, 'config', 'user.email', 'test@example.invalid') + git(self.repo, 'remote', 'add', 'origin', str(self.remote)) + (self.repo / 'README.md').write_text('fixture\n') + self.commit() + git(self.repo, 'push', 'origin', 'main') + + def commit(self): + git(self.repo, 'add', '.') + git(self.repo, 'commit', '-m', 'fixture') + return git(self.repo, 'rev-parse', 'HEAD').stdout.strip() + + def write_brief(self, day='2026-09-14', text='A brief\n'): + p = self.repo / brief_path(day) + p.parent.mkdir(parents=True, exist_ok=True) + p.write_text(text) + return p + + def event(self, day='2026-09-14', **kwargs): + detail = dict(repo='freedom-intelligence', date=day, path=brief_path(day), **kwargs) + return dict(id='event-1', detail=detail) + + def test_untracked_file_does_not_count(self): + self.write_brief() + self.assertFalse(Origin(self.repo).blob(brief_path('2026-09-14'))) + + def test_unpushed_commit_does_not_count(self): + self.write_brief() + self.commit() + self.assertFalse(Origin(self.repo).blob(brief_path('2026-09-14'))) + + def test_fresh_remote_beats_stale_tracking_ref(self): + old = git(self.repo, 'rev-parse', 'HEAD').stdout.strip() + self.write_brief() + sha = self.commit() + git(self.repo, 'push', 'origin', 'main') + git(self.repo, 'update-ref', 'refs/remotes/origin/main', old) + origin = Origin(self.repo) + self.assertEqual(origin.sha, sha) + self.assertTrue(origin.blob(brief_path('2026-09-14'))) + + def test_fetches_missing_remote_objects(self): + clone = self.root / 'other' + git(self.root, 'clone', '-b', 'main', str(self.remote), str(clone)) + self.write_brief() + self.commit() + git(self.repo, 'push', 'origin', 'main') + self.assertTrue(Origin(clone).blob(brief_path('2026-09-14'))) + + def test_valid_published_completion(self): + self.write_brief() + sha = self.commit() + git(self.repo, 'push', 'origin', 'main') + result = assess([self.event(pushed=True, origin_sha=sha)], [], Origin(self.repo), '2026-09-14') + self.assertEqual(len(result['valid']), 1) + self.assertEqual(result['failures'], []) + + def test_unpublished_attestation_rejected_even_when_file_is_remote(self): + self.write_brief() + self.commit() + git(self.repo, 'push', 'origin', 'main') + (self.repo / 'extra').write_text('unpublished') + unpublished = self.commit() + result = assess([self.event(pushed=True, origin_sha=unpublished)], [], Origin(self.repo)) + self.assertEqual(len(result['failures']), 1) + + def test_missing_push_evidence_rejected_for_new_events(self): + self.write_brief() + self.commit() + git(self.repo, 'push', 'origin', 'main') + self.assertEqual(len(assess([self.event()], [], Origin(self.repo))['failures']), 1) + + def test_empty_file_rejected(self): + self.write_brief(text='') + self.commit() + git(self.repo, 'push', 'origin', 'main') + self.assertFalse(Origin(self.repo).blob(brief_path('2026-09-14'))) + + def test_acknowledged_gap_is_not_a_success(self): + event = self.event('2026-09-07') + record = dict(original_event_id=event['id'], date='2026-09-07', + path=brief_path('2026-09-07'), disposition='false_completion', reason='unpublished') + (self.repo / 'evidence.json').write_text(json.dumps({'corrections': [record]})) + sha = self.commit() + git(self.repo, 'push', 'origin', 'main') + correction = dict(detail=dict(record, repo='freedom-intelligence', + evidence_path='evidence.json', evidence_sha=sha)) + result = assess([event], [correction], Origin(self.repo), '2026-09-07') + self.assertEqual(len(result['reconciled']), 1) + self.assertEqual(result['valid'], []) + self.assertEqual(len(result['failures']), 1) # required day still fails + correction['detail']['original_event_id'] = 'different-event' + result = assess([event], [correction], Origin(self.repo)) + self.assertEqual(result['reconciled'], []) + self.assertEqual(len(result['failures']), 1) + + def test_malformed_dates_fail_without_path_traversal(self): + result = assess([dict(id='bad', detail={'date': '../../etc/passwd'})], [], Origin(self.repo)) + self.assertEqual(len(result['failures']), 1) + + +class PaginationTests(unittest.TestCase): + def test_reads_all_pages(self): + pages = [[{'id': '1'}, {'id': '2'}], [{'id': '3'}]] + with patch('urllib.request.urlopen', side_effect=[io.BytesIO(json.dumps(p).encode()) for p in pages]) as call: + self.assertEqual(len(load_events('http://hub', 1, page_size=2)), 3) + self.assertIn('offset=2', call.call_args.args[0].full_url) + + def test_repeating_page_fails(self): + with patch('urllib.request.urlopen', side_effect=[io.BytesIO(b'[{"id":"1"}]'), io.BytesIO(b'[{"id":"1"}]')]): + with self.assertRaises(RuntimeError): + load_events('http://hub', 1, page_size=1) + + +if __name__ == '__main__': + unittest.main() diff --git a/scripts/verify_brief_durability.py b/scripts/verify_brief_durability.py index f6afe91..c52e2cd 100755 --- a/scripts/verify_brief_durability.py +++ b/scripts/verify_brief_durability.py @@ -1,114 +1,192 @@ #!/usr/bin/env python3 -"""Fail if State Hub fi_daily_brief events are not on origin/main. +"""Audit Hub completions against a freshly resolved origin/main Git tree. -A brief day is complete only when briefs/YYYY/MM/YYYY-MM-DD.md exists in -this git repository (the clone should be origin/main). Hub events that -claim wrote/committed without a matching file are the 2026-08/09 failure -mode: local commit on railiance, due-bit cleared, durable memory empty. - -Usage: - python3 scripts/verify_brief_durability.py - STATE_HUB_URL=http://127.0.0.1:8000 python3 scripts/verify_brief_durability.py - -Exit 0 = every hub success event has a brief file. -Exit 1 = mismatch (or hub unreachable when --require-hub is set). +Exit 0: checks pass (or explicitly allowed acknowledged historical gaps). +Exit 1: missing/invalid evidence. Exit 2: verification could not run reliably. +--require-date YYYY-MM-DD also requires a valid completion for that day. """ from __future__ import annotations import argparse +from datetime import date import json import os -import sys -import urllib.error -import urllib.request from pathlib import Path +import re +import subprocess +import sys +import urllib.parse +import urllib.request ROOT = Path(__file__).resolve().parents[1] +REPO = 'freedom-intelligence' +CONTRACT_START = '2026-09-13' +CORRECTION_TYPE = 'fi_daily_brief_reconciliation' +SHA = re.compile(r'[0-9a-f]{40}') -def brief_path(date: str) -> Path: - year, month, _ = date.split("-") - return ROOT / "briefs" / year / month / f"{date}.md" +def brief_path(day: str) -> str: + if date.fromisoformat(day).isoformat() != day: + raise ValueError('date must be YYYY-MM-DD') + return f'briefs/{day[:4]}/{day[5:7]}/{day}.md' -def load_events(url: str, timeout: float) -> list[dict]: - req = urllib.request.Request( - f"{url.rstrip('/')}/progress/?event_type=fi_daily_brief", - headers={"Accept": "application/json"}, - ) - with urllib.request.urlopen(req, timeout=timeout) as resp: - data = json.loads(resp.read().decode()) - if isinstance(data, list): - return data - if isinstance(data, dict): - for key in ("items", "events", "results"): - if isinstance(data.get(key), list): - return data[key] - raise RuntimeError(f"unexpected hub payload type {type(data)}") +def git(root: Path, *args: str, check=True): + result = subprocess.run(['git', '-C', str(root), *args], capture_output=True, + text=True, timeout=120) + if check and result.returncode: + raise RuntimeError(f'git {args[0]} failed: {result.stderr.strip()}') + return result + + +class Origin: + def __init__(self, root: Path): + self.root = root + # Never trust a stale tracking ref or the checked-out branch. + remote = git(root, 'ls-remote', '--exit-code', 'origin', 'refs/heads/main').stdout.split() + if len(remote) != 2 or not SHA.fullmatch(remote[0]): + raise RuntimeError('origin/main did not resolve to one commit') + self.sha = remote[0] + if git(root, 'cat-file', '-e', f'{self.sha}^{{commit}}', check=False).returncode: + git(root, 'fetch', '--no-tags', '--no-write-fetch-head', 'origin', 'refs/heads/main') + git(root, 'cat-file', '-e', f'{self.sha}^{{commit}}') + + def blob(self, path: str, revision: str | None = None) -> bool: + revision = revision or self.sha + if not SHA.fullmatch(revision): + return False + entry = git(self.root, 'ls-tree', revision, '--', path, check=False) + fields = entry.stdout.split() + # Reject symlinks, trees and empty files as completion artifacts. + if entry.returncode or len(fields) != 4 or fields[0] not in {'100644', '100755'}: + return False + return fields[1] == 'blob' and int(git(self.root, 'cat-file', '-s', fields[2]).stdout) > 0 + + def published(self, revision: str, path: str) -> bool: + if not SHA.fullmatch(revision): + return False + return (git(self.root, 'merge-base', '--is-ancestor', revision, self.sha, + check=False).returncode == 0 and self.blob(path, revision)) + + def correction_recorded(self, detail: dict) -> bool: + revision = str(detail.get('evidence_sha', '')) + path = str(detail.get('evidence_path', '')) + if not self.published(revision, path): + return False + try: + evidence = json.loads(git(self.root, 'show', f'{revision}:{path}').stdout) + records = evidence['corrections'] + except (ValueError, KeyError, TypeError): + return False + fields = ('original_event_id', 'date', 'path', 'disposition', 'reason') + return any(isinstance(row, dict) and all(row.get(k) == detail.get(k) for k in fields) + for row in records) + + +def load_events(url: str, timeout: float, event_type='fi_daily_brief', page_size=1000) -> list[dict]: + events = [] + seen = set() + for offset in range(0, 1_000_000, page_size): + query = urllib.parse.urlencode(dict(event_type=event_type, limit=page_size, offset=offset)) + req = urllib.request.Request(f"{url.rstrip('/')}/progress/?{query}", + headers={'Accept': 'application/json'}) + with urllib.request.urlopen(req, timeout=timeout) as response: + page = json.load(response) + if not isinstance(page, list): + raise RuntimeError('unexpected Hub progress payload: expected a list') + for event in page: + if not isinstance(event, dict) or not event.get('id') or event['id'] in seen: + raise RuntimeError('malformed or unstable Hub pagination; retry verification') + seen.add(event['id']) + events.extend(page) + if len(page) < page_size: + return events + raise RuntimeError('Hub pagination exceeded safety bound') + + +def assess(events, corrections, origin, required_date=None): + result = {'origin_sha': origin.sha, 'valid': [], 'legacy': [], 'reconciled': [], 'failures': []} + correction_index = {} + for correction in corrections: + d = correction.get('detail') or {} + if not isinstance(d, dict) or d.get('repo') != REPO: + continue + if (d.get('disposition') == 'false_completion' and d.get('reason') + and origin.correction_recorded(d)): + correction_index.setdefault(d.get('original_event_id'), []).append(d) + for event in events: + d = event.get('detail') or {} + if not isinstance(d, dict): + result['failures'].append({'id': event.get('id'), 'reason': 'invalid event detail'}) + continue + if d.get('repo') not in {None, REPO}: + continue + row = {'id': event.get('id'), 'date': d.get('date')} + try: + path = brief_path(d.get('date', '')) + except (ValueError, TypeError): + result['failures'].append(dict(row, reason='invalid or missing date')) + continue + if not origin.blob(path): + acknowledged = any(c.get('date') == d['date'] and c.get('path') == path + for c in correction_index.get(event.get('id'), [])) + target = 'reconciled' if acknowledged else 'failures' + result[target].append(dict(row, reason='brief absent from origin/main', path=path)) + continue + if d.get('path', path) != path: + result['failures'].append(dict(row, reason='event path does not match date')) + continue + modern = d['date'] >= CONTRACT_START or 'pushed' in d or 'origin_sha' in d + if modern: + if (d.get('pushed') is not True or d.get('path') != path + or not origin.published(str(d.get('origin_sha', '')), path)): + result['failures'].append(dict(row, reason='invalid pushed/origin_sha publication evidence')) + continue + result['valid'].append(row) + else: + # Historic files exist on origin but predate the push-attestation contract. + result['legacy'].append(row) + if required_date and required_date not in {r['date'] for r in result['valid']}: + result['failures'].append({'date': required_date, 'reason': 'required day has no valid published completion'}) + return result def main() -> int: ap = argparse.ArgumentParser(description=__doc__) - ap.add_argument( - "--hub", - default=os.environ.get("STATE_HUB_URL", "http://127.0.0.1:8000"), - ) - ap.add_argument("--timeout", type=float, default=8.0) - ap.add_argument( - "--require-hub", - action="store_true", - help="exit 1 if the hub cannot be reached (default: warn and check git only)", - ) + ap.add_argument('--hub', default=os.environ.get('STATE_HUB_URL', 'http://127.0.0.1:8000')) + ap.add_argument('--timeout', type=float, default=8) + ap.add_argument('--require-hub', action='store_true', help='compatibility flag; Hub is always required') + ap.add_argument('--require-date', help='require a valid pushed completion for this exact day') + ap.add_argument('--allow-reconciled', action='store_true', help='acknowledged historical gaps do not fail the audit') + ap.add_argument('--json', action='store_true', help='machine-readable audit result') args = ap.parse_args() - - git_dates = sorted( - p.stem - for p in (ROOT / "briefs").rglob("20*.md") - if p.name[0].isdigit() - ) - print(f"git briefs: {len(git_dates)} last={git_dates[-1] if git_dates else '-'}") - try: + if args.require_date: + brief_path(args.require_date) + origin = Origin(ROOT) events = load_events(args.hub, args.timeout) - except (urllib.error.URLError, TimeoutError, json.JSONDecodeError, RuntimeError) as exc: - msg = f"hub unreachable or unreadable at {args.hub}: {exc}" - if args.require_hub: - print(f"FAIL {msg}", file=sys.stderr) - return 1 - print(f"WARN {msg}") - return 0 - - missing: list[str] = [] - present: list[str] = [] - for ev in events: - detail = ev.get("detail") or {} - date = str(detail.get("date") or "") - if not date: - continue - repo = detail.get("repo") - if repo and repo != "freedom-intelligence": - continue - path = brief_path(date) - if path.is_file(): - present.append(date) + corrections = load_events(args.hub, args.timeout, CORRECTION_TYPE) + result = assess(events, corrections, origin, args.require_date) + except (OSError, ValueError, RuntimeError, subprocess.SubprocessError) as exc: + if args.json: + print(json.dumps({'ok': False, 'error': str(exc)})) else: - missing.append(date) - - missing = sorted(set(missing)) - present = sorted(set(present)) - print(f"hub fi_daily_brief dates on git: {len(present)}") - if missing: - print("FAIL hub events with no brief file:") - for date in missing: - print(f" {date} expected {brief_path(date).relative_to(ROOT)}") - print( - "A local-only commit must not clear due. " - "Push to origin, or treat the hub event as a false completion." - ) - return 1 - print("ok: every hub fi_daily_brief date has a brief file") - return 0 + print(f'ERROR verification unavailable: {exc}', file=sys.stderr) + return 2 + result['ok'] = not result['failures'] and (args.allow_reconciled or not result['reconciled']) + if args.json: + print(json.dumps(result, indent=2)) + else: + print(f'origin/main: {origin.sha} (resolved from live remote)') + print(f"valid published events: {len(result['valid'])}; legacy files on origin: {len(result['legacy'])}") + for row in result['reconciled']: + print(f"GAP acknowledged false completion: {row['date']} event={row['id']}") + for row in result['failures']: + print(f"FAIL {row.get('date', '-')}: {row['reason']}") + print('PASS (acknowledged gaps remain missing)' if result['ok'] and result['reconciled'] + else 'PASS' if result['ok'] else 'FAIL durability audit') + return 0 if result['ok'] else 1 -if __name__ == "__main__": +if __name__ == '__main__': sys.exit(main()) diff --git a/workplans/FI-WP-0004-operational-loop-and-scaleway-reserve.md b/workplans/FI-WP-0004-operational-loop-and-scaleway-reserve.md index 91ce3b0..73a2a49 100644 --- a/workplans/FI-WP-0004-operational-loop-and-scaleway-reserve.md +++ b/workplans/FI-WP-0004-operational-loop-and-scaleway-reserve.md @@ -105,6 +105,12 @@ state_hub_task_id: "e4fbc634-462c-5f07-8a27-ed6ad7315bfe" - Tighten `activity-definitions/fi-daily-research-brief.md` and the playbook so a local-only commit does not clear `due`. +**2026-09-14 follow-up:** verifier now resolves live origin/main, reads Git +objects, paginates Hub events, and validates pushed/SHA evidence. Exact-event +corrections preserve the five missing dates as explicit historical gaps. +`--require-date` proves a specific scheduled day; see +`docs/brief-durability-audit.md`. + **Done when:** `python3 scripts/verify_brief_durability.py` fails on the known Sep 2026 hub/git mismatch (or reports it as debt until those events are reconciled). @@ -217,7 +223,7 @@ Operator / railiance-platform: ```task id: FI-WP-0004-T09 -status: in_progress +status: progress priority: medium state_hub_task_id: "304816de-5065-5928-b9bd-da1b5019c3d4" ```