diff --git a/src/repo_manager/commands/registrar_reconcile.py b/src/repo_manager/commands/registrar_reconcile.py index 10e98b8..21d2e91 100644 --- a/src/repo_manager/commands/registrar_reconcile.py +++ b/src/repo_manager/commands/registrar_reconcile.py @@ -193,31 +193,50 @@ def _check_empty_repo_projection( 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() task_ids: set[str] = set() + record_ids: dict[str, set[str]] = {"intake": set(), "decision": set()} for path in sorted((repo / "workplans").glob("*.md")): parsed = parse_workplan_file(path, repo_root=repo) if parsed.frontmatter.get("type") != "workplan" or not parsed.id: continue 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) for task in parsed.tasks: 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: task_ids.add(task.state_hub_task_id) if not workplan_ids: - return set(), set(), "repository has no root workplans to rebuild" - return workplan_ids, task_ids, None + return set(), set(), record_ids, "repository has no root workplans to rebuild" + 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( - 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]: missing_workplans: list[str] = [] missing_tasks: list[str] = [] + missing_records: dict[str, list[str]] = {"intake": [], "decision": []} try: for projection_id in sorted(workplan_ids): response = httpx.get( @@ -233,6 +252,15 @@ def _verify_full_projection( missing_tasks.append(projection_id) else: 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: return {}, f"full projection verification failed: {exc}" evidence = { @@ -240,8 +268,12 @@ def _verify_full_projection( "expected_tasks": len(task_ids), "missing_workplans": missing_workplans, "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, None @@ -359,6 +391,7 @@ def registrar_reconcile( bootstrap_workplans: set[str] = set() bootstrap_tasks: set[str] = set() + bootstrap_records: dict[str, set[str]] = {"intake": set(), "decision": set()} if bootstrap_empty_projection: bootstrap_before, bootstrap_error = _check_empty_repo_projection(api_base, repo.name) evidence["bootstrap_projection_before"] = bootstrap_before @@ -369,10 +402,17 @@ def registrar_reconcile( {"code": "bootstrap_precondition_failed", "message": bootstrap_error}, 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"] = { "workplans": len(bootstrap_workplans), "tasks": len(bootstrap_tasks), + "intakes": len(bootstrap_records["intake"]), + "decisions": len(bootstrap_records["decision"]), } if source_error: return RegistrarResult( @@ -427,6 +467,7 @@ def registrar_reconcile( api_base, bootstrap_workplans, bootstrap_tasks, + bootstrap_records, ) evidence["bootstrap_projection"] = projection evidence["bootstrap_projection_verified"] = projection_error is None diff --git a/tests/test_registrar_reconcile.py b/tests/test_registrar_reconcile.py index d6e226d..3edd89b 100644 --- a/tests/test_registrar_reconcile.py +++ b/tests/test_registrar_reconcile.py @@ -219,12 +219,16 @@ def test_bootstraps_and_verifies_a_completely_empty_projection(tmp_path: Path, m monkeypatch.setattr( rr, "_verify_full_projection", - lambda _api, workplans, tasks: ( + lambda _api, workplans, tasks, records: ( { "expected_workplans": len(workplans), "expected_tasks": len(tasks), + "expected_intakes": len(records["intake"]), + "expected_decisions": len(records["decision"]), "missing_workplans": [], "missing_tasks": [], + "missing_intakes": [], + "missing_decisions": [], }, None, ), @@ -245,7 +249,12 @@ def test_bootstraps_and_verifies_a_completely_empty_projection(tmp_path: Path, m ) 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