feat(registrar): verify full empty-projection rebuild

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:07:09 +02:00
parent 5228f2e286
commit 707fb85652
3 changed files with 232 additions and 4 deletions

View file

@ -63,12 +63,18 @@ def main(argv: list[str] | None = None) -> int:
help="Confirm that --api-base is the authoritative hub",
)
p_registrar.add_argument("--push", action="store_true", help="Push the registrar commit")
p_registrar.add_argument(
registrar_mode = p_registrar.add_mutually_exclusive_group()
registrar_mode.add_argument(
"--repair-workplan",
default=None,
metavar="ID",
help="Rebuild and verify one already-identified workplan projection",
)
registrar_mode.add_argument(
"--bootstrap-empty-projection",
action="store_true",
help="Rebuild all authoritative UUIDs after proving the repo projection is empty",
)
p_cmd = sub.add_parser(
"update-task-status",
@ -419,6 +425,7 @@ def main(argv: list[str] | None = None) -> int:
confirm_primary=args.confirm_primary,
push=args.push,
repair_workplan=args.repair_workplan,
bootstrap_empty_projection=args.bootstrap_empty_projection,
)
print(json.dumps(result.to_dict(), indent=2))
return 0 if result.status in {"applied", "noop"} else 1

View file

@ -169,6 +169,83 @@ def _projection_exists(api_base: str, projection_id: str) -> tuple[dict[str, Any
return payload, None
def _check_empty_repo_projection(
api_base: str, repo_slug: str
) -> tuple[dict[str, Any], str | None]:
try:
repo_response = httpx.get(f"{api_base.rstrip('/')}/repos/{repo_slug}", timeout=10.0)
repo_response.raise_for_status()
repo_payload = repo_response.json()
response = httpx.get(
f"{api_base.rstrip('/')}/workplans/",
params={"repo_id": repo_payload["id"]},
timeout=10.0,
)
response.raise_for_status()
rows = response.json()
except (httpx.HTTPError, KeyError, ValueError) as exc:
return {}, f"empty projection preflight failed: {exc}"
if not isinstance(rows, list):
return {}, "empty projection preflight returned an invalid workplan collection"
evidence = {"repo_id": repo_payload["id"], "workplan_count": len(rows)}
if rows:
return evidence, "target repository projection is not empty"
return evidence, None
def _authoritative_projection_ids(repo: Path) -> tuple[set[str], set[str], str | None]:
workplan_ids: set[str] = set()
task_ids: set[str] = 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"
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"
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
def _verify_full_projection(
api_base: str, workplan_ids: set[str], task_ids: set[str]
) -> tuple[dict[str, Any], str | None]:
missing_workplans: list[str] = []
missing_tasks: list[str] = []
try:
for projection_id in sorted(workplan_ids):
response = httpx.get(
f"{api_base.rstrip('/')}/workplans/{projection_id}", timeout=10.0
)
if response.status_code == 404:
missing_workplans.append(projection_id)
else:
response.raise_for_status()
for projection_id in sorted(task_ids):
response = httpx.get(f"{api_base.rstrip('/')}/tasks/{projection_id}", timeout=10.0)
if response.status_code == 404:
missing_tasks.append(projection_id)
else:
response.raise_for_status()
except httpx.HTTPError as exc:
return {}, f"full projection verification failed: {exc}"
evidence = {
"expected_workplans": len(workplan_ids),
"expected_tasks": len(task_ids),
"missing_workplans": missing_workplans,
"missing_tasks": missing_tasks,
}
if missing_workplans or missing_tasks:
return evidence, "full projection rebuild is incomplete"
return evidence, None
def _run_statehub(command: list[str], *, env: dict[str, str]) -> subprocess.CompletedProcess[str]:
return subprocess.run(
command,
@ -204,6 +281,7 @@ def registrar_reconcile(
confirm_primary: bool = False,
push: bool = False,
repair_workplan: str | None = None,
bootstrap_empty_projection: bool = False,
) -> RegistrarResult:
"""Register missing workplan/task UUIDs through one scoped child process."""
cid = str(uuid.uuid4())
@ -217,6 +295,16 @@ def registrar_reconcile(
}
repair_projection_id = None
if repair_workplan and bootstrap_empty_projection:
return RegistrarResult(
"rejected",
evidence,
{
"code": "registrar_mode_conflict",
"message": "repair-workplan and bootstrap-empty-projection are mutually exclusive",
},
cid,
)
if repair_workplan:
repair_projection_id = _workplan_projection_id(repo, repair_workplan)
evidence["repair_workplan"] = repair_workplan
@ -242,7 +330,7 @@ def registrar_reconcile(
},
cid,
)
if not any(before.values()) and not repair_projection_id:
if not any(before.values()) and not repair_projection_id and not bootstrap_empty_projection:
evidence["missing_after"] = before
return RegistrarResult("noop", evidence, None, cid)
@ -269,6 +357,31 @@ def registrar_reconcile(
cid,
)
bootstrap_workplans: set[str] = set()
bootstrap_tasks: set[str] = set()
if bootstrap_empty_projection:
bootstrap_before, bootstrap_error = _check_empty_repo_projection(api_base, repo.name)
evidence["bootstrap_projection_before"] = bootstrap_before
if bootstrap_error:
return RegistrarResult(
"rejected",
evidence,
{"code": "bootstrap_precondition_failed", "message": bootstrap_error},
cid,
)
bootstrap_workplans, bootstrap_tasks, source_error = _authoritative_projection_ids(repo)
evidence["bootstrap_source"] = {
"workplans": len(bootstrap_workplans),
"tasks": len(bootstrap_tasks),
}
if source_error:
return RegistrarResult(
"rejected",
evidence,
{"code": "bootstrap_source_invalid", "message": source_error},
cid,
)
LOCK_PATH.parent.mkdir(parents=True, exist_ok=True)
with LOCK_PATH.open("a+", encoding="utf-8") as lock:
try:
@ -290,6 +403,8 @@ def registrar_reconcile(
"--api-base",
api_base.rstrip("/"),
]
if bootstrap_empty_projection:
command.append("--bootstrap-empty-projection")
completed = _run_statehub(command, env=child_env)
evidence["statehub_exit_code"] = completed.returncode
@ -306,8 +421,21 @@ def registrar_reconcile(
evidence["repair_projection_error"] = projection_error
repair_verified = projection_error is None
bootstrap_verified = False
if bootstrap_empty_projection:
projection, projection_error = _verify_full_projection(
api_base,
bootstrap_workplans,
bootstrap_tasks,
)
evidence["bootstrap_projection"] = projection
evidence["bootstrap_projection_verified"] = projection_error is None
if projection_error:
evidence["bootstrap_projection_error"] = projection_error
bootstrap_verified = projection_error is None
accepted_exit_codes = {0, 2}
if repair_verified:
if repair_verified or bootstrap_verified:
# A repository-scoped projection repair may coexist with legacy stale
# references that correctly keep the broader consistency report red.
accepted_exit_codes.add(1)
@ -324,7 +452,7 @@ def registrar_reconcile(
changed = _git(repo, "status", "--porcelain")
paths = [line[3:] for line in changed.stdout.splitlines() if len(line) > 3]
if repair_verified and paths:
if (repair_verified or bootstrap_verified) and paths:
restored, restore_error = _restore_generated_brief(repo, paths)
evidence["restored_generated_paths"] = restored
if restore_error: