feat(registrar): verify file-backed record rebuilds

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a023c0-a0a3-7c03-b395-5a0d2757214d
This commit is contained in:
tegwick 2026-08-22 12:23:44 +02:00
parent 72856a00ad
commit 6e9c93e7ca
2 changed files with 60 additions and 10 deletions

View file

@ -193,31 +193,50 @@ def _check_empty_repo_projection(
return evidence, None return evidence, None
def _authoritative_projection_ids(repo: Path) -> tuple[set[str], set[str], str | None]: def _authoritative_projection_ids(
repo: Path,
) -> tuple[set[str], set[str], dict[str, set[str]], str | None]:
workplan_ids: set[str] = set() workplan_ids: set[str] = set()
task_ids: set[str] = set() task_ids: set[str] = set()
record_ids: dict[str, set[str]] = {"intake": set(), "decision": set()}
for path in sorted((repo / "workplans").glob("*.md")): for path in sorted((repo / "workplans").glob("*.md")):
parsed = parse_workplan_file(path, repo_root=repo) parsed = parse_workplan_file(path, repo_root=repo)
if parsed.frontmatter.get("type") != "workplan" or not parsed.id: if parsed.frontmatter.get("type") != "workplan" or not parsed.id:
continue continue
if not parsed.state_hub_workstream_id: if not parsed.state_hub_workstream_id:
return set(), set(), f"{parsed.id} has no authoritative projection UUID" return set(), set(), record_ids, f"{parsed.id} has no authoritative projection UUID"
workplan_ids.add(parsed.state_hub_workstream_id) workplan_ids.add(parsed.state_hub_workstream_id)
for task in parsed.tasks: for task in parsed.tasks:
if task.id and not task.state_hub_task_id: if task.id and not task.state_hub_task_id:
return set(), set(), f"{task.id} has no authoritative projection UUID" return set(), set(), record_ids, f"{task.id} has no authoritative projection UUID"
if task.state_hub_task_id: if task.state_hub_task_id:
task_ids.add(task.state_hub_task_id) task_ids.add(task.state_hub_task_id)
if not workplan_ids: if not workplan_ids:
return set(), set(), "repository has no root workplans to rebuild" return set(), set(), record_ids, "repository has no root workplans to rebuild"
return workplan_ids, task_ids, None for path in iter_record_files(repo):
for record in parse_record_file(path, repo_root=repo):
if record.kind not in record_ids:
continue
if not record.uuid:
return (
set(),
set(),
record_ids,
f"{record.id} has no authoritative projection UUID",
)
record_ids[record.kind].add(record.uuid)
return workplan_ids, task_ids, record_ids, None
def _verify_full_projection( def _verify_full_projection(
api_base: str, workplan_ids: set[str], task_ids: set[str] api_base: str,
workplan_ids: set[str],
task_ids: set[str],
record_ids: dict[str, set[str]],
) -> tuple[dict[str, Any], str | None]: ) -> tuple[dict[str, Any], str | None]:
missing_workplans: list[str] = [] missing_workplans: list[str] = []
missing_tasks: list[str] = [] missing_tasks: list[str] = []
missing_records: dict[str, list[str]] = {"intake": [], "decision": []}
try: try:
for projection_id in sorted(workplan_ids): for projection_id in sorted(workplan_ids):
response = httpx.get( response = httpx.get(
@ -233,6 +252,15 @@ def _verify_full_projection(
missing_tasks.append(projection_id) missing_tasks.append(projection_id)
else: else:
response.raise_for_status() response.raise_for_status()
for kind, endpoint in (("intake", "intakes"), ("decision", "decisions")):
for projection_id in sorted(record_ids[kind]):
response = httpx.get(
f"{api_base.rstrip('/')}/{endpoint}/{projection_id}", timeout=10.0
)
if response.status_code == 404:
missing_records[kind].append(projection_id)
else:
response.raise_for_status()
except httpx.HTTPError as exc: except httpx.HTTPError as exc:
return {}, f"full projection verification failed: {exc}" return {}, f"full projection verification failed: {exc}"
evidence = { evidence = {
@ -240,8 +268,12 @@ def _verify_full_projection(
"expected_tasks": len(task_ids), "expected_tasks": len(task_ids),
"missing_workplans": missing_workplans, "missing_workplans": missing_workplans,
"missing_tasks": missing_tasks, "missing_tasks": missing_tasks,
"expected_intakes": len(record_ids["intake"]),
"expected_decisions": len(record_ids["decision"]),
"missing_intakes": missing_records["intake"],
"missing_decisions": missing_records["decision"],
} }
if missing_workplans or missing_tasks: if missing_workplans or missing_tasks or any(missing_records.values()):
return evidence, "full projection rebuild is incomplete" return evidence, "full projection rebuild is incomplete"
return evidence, None return evidence, None
@ -359,6 +391,7 @@ def registrar_reconcile(
bootstrap_workplans: set[str] = set() bootstrap_workplans: set[str] = set()
bootstrap_tasks: set[str] = set() bootstrap_tasks: set[str] = set()
bootstrap_records: dict[str, set[str]] = {"intake": set(), "decision": set()}
if bootstrap_empty_projection: if bootstrap_empty_projection:
bootstrap_before, bootstrap_error = _check_empty_repo_projection(api_base, repo.name) bootstrap_before, bootstrap_error = _check_empty_repo_projection(api_base, repo.name)
evidence["bootstrap_projection_before"] = bootstrap_before evidence["bootstrap_projection_before"] = bootstrap_before
@ -369,10 +402,17 @@ def registrar_reconcile(
{"code": "bootstrap_precondition_failed", "message": bootstrap_error}, {"code": "bootstrap_precondition_failed", "message": bootstrap_error},
cid, cid,
) )
bootstrap_workplans, bootstrap_tasks, source_error = _authoritative_projection_ids(repo) (
bootstrap_workplans,
bootstrap_tasks,
bootstrap_records,
source_error,
) = _authoritative_projection_ids(repo)
evidence["bootstrap_source"] = { evidence["bootstrap_source"] = {
"workplans": len(bootstrap_workplans), "workplans": len(bootstrap_workplans),
"tasks": len(bootstrap_tasks), "tasks": len(bootstrap_tasks),
"intakes": len(bootstrap_records["intake"]),
"decisions": len(bootstrap_records["decision"]),
} }
if source_error: if source_error:
return RegistrarResult( return RegistrarResult(
@ -427,6 +467,7 @@ def registrar_reconcile(
api_base, api_base,
bootstrap_workplans, bootstrap_workplans,
bootstrap_tasks, bootstrap_tasks,
bootstrap_records,
) )
evidence["bootstrap_projection"] = projection evidence["bootstrap_projection"] = projection
evidence["bootstrap_projection_verified"] = projection_error is None evidence["bootstrap_projection_verified"] = projection_error is None

View file

@ -219,12 +219,16 @@ def test_bootstraps_and_verifies_a_completely_empty_projection(tmp_path: Path, m
monkeypatch.setattr( monkeypatch.setattr(
rr, rr,
"_verify_full_projection", "_verify_full_projection",
lambda _api, workplans, tasks: ( lambda _api, workplans, tasks, records: (
{ {
"expected_workplans": len(workplans), "expected_workplans": len(workplans),
"expected_tasks": len(tasks), "expected_tasks": len(tasks),
"expected_intakes": len(records["intake"]),
"expected_decisions": len(records["decision"]),
"missing_workplans": [], "missing_workplans": [],
"missing_tasks": [], "missing_tasks": [],
"missing_intakes": [],
"missing_decisions": [],
}, },
None, None,
), ),
@ -245,7 +249,12 @@ def test_bootstraps_and_verifies_a_completely_empty_projection(tmp_path: Path, m
) )
assert result.status == "applied" assert result.status == "applied"
assert result.evidence["bootstrap_source"] == {"workplans": 1, "tasks": 1} assert result.evidence["bootstrap_source"] == {
"workplans": 1,
"tasks": 1,
"intakes": 0,
"decisions": 0,
}
assert result.evidence["bootstrap_projection_verified"] is True assert result.evidence["bootstrap_projection_verified"] is True