feat(identifiers): verify batch projections

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a023c0-a0a3-7c03-b395-5a0d2757214d
This commit is contained in:
tegwick 2026-08-22 23:50:34 +02:00
parent e6cc18bf18
commit 055c6971ab
11 changed files with 2964 additions and 9 deletions

View file

@ -0,0 +1,55 @@
# RMGR-WP-0005 batch 0005 projection preflight
**Observed at:** `2026-08-22T21:46:30Z`
**Scope:** `adaptive-pricing` only
## Outcome
Batch 0005 is source-ready but **not ready for approval**. No database or
repository identifier changed.
The refreshed fleet plan is sealed as
`28cd01bdbc66c53ad5ef11b9821a74ebadd7c8cfdae4c227f37ce7f9d67cbf18`.
The adaptive-pricing unit is clean, synchronized with `origin/main`, and pinned
at `c65a2f1ff9a0b30700ee14818e36e931eb71cae3`. It contains five replacements:
`ADAPTIVE-WP-0010` and its four unfinished tasks.
The projection-aware batch manifest is sealed as
`20decab5521ea5477826d1b2a9c9670912a96c4b8245faf5d270c8d1efc988c5`.
It records `ready_for_approval: false` and `apply_authorized: false`.
## Projection evidence
| Projection | Five current UUIDs | Five derived UUIDs | Result |
| --- | --- | --- | --- |
| Workstation `:8000` | HTTP 200 | HTTP 404 | ready |
| Production tunnel `:18000` | HTTP 404 | HTTP 404 | blocked |
A governed `registrar-reconcile --repair-workplan ADAPTIVE-WP-0010` attempt
used clean source revision `c65a2f1...` and correlation id
`b708a73e-fc09-48ee-aa2d-ff8b3c642466`. State Hub retained the conservative
C-03 rule for the pre-derivation random UUID and did not create it. This is the
correct ordinary-consistency behavior, but it means the existing transactional
migration executor cannot run: that executor requires every old source row to
exist in the target repository projection.
The attempt also exposed a Repo Manager result bug: a failed exact repair could
be labeled `applied` because an empty ordinary-registration set passed
verification vacuously. The wrapper now requires repair/bootstrap mode's own
exact projection proof and does not run ordinary requested-record verification
for an empty request.
## Owner-ready resolution
`helixforge.identifiers.state-hub-sealed-projection-repair.v1` offers State Hub
a bounded repair task. It preserves the default C-03 refusal and permits old-row
restoration only from one clean source revision plus one exact sealed migration
unit, with primary confirmation, atomicity, idempotency, and a receipt. Once
adopted, repeat this batch plan against both projections; only a new manifest
with all current lookups 200 and all derived lookups 404 may be proposed for
approval.
The fleet scan found one other projection-complete candidate, `state-hub`, but
its checkout contains unrelated uncommitted owner evidence. The Git preflight
therefore excludes it until the owner publishes or dispositions that work.

View file

@ -0,0 +1,182 @@
{
"schema": "repo-manager.identifier-migration-batch.v1",
"ok": false,
"ready_for_approval": false,
"apply_authorized": false,
"approval_required": true,
"projection_api_bases": [
"http://127.0.0.1:8000",
"http://127.0.0.1:18000"
],
"namespace": "helixforge",
"source_plan_sha256": "28cd01bdbc66c53ad5ef11b9821a74ebadd7c8cfdae4c227f37ce7f9d67cbf18",
"batch_policy": "repository-atomic, sequential, stop on first failure",
"rollback_order": "reverse files if written, then reverse central projection",
"generated_at": "2026-08-22T21:45:54.917855Z",
"totals": {
"repositories": 1,
"records": 5,
"replace": 5,
"assign": 0,
"unchanged": 0
},
"repositories": [
{
"repo": "adaptive-pricing",
"path": "/home/worsch/adaptive-pricing",
"planned_head_sha": "c65a2f1ff9a0b30700ee14818e36e931eb71cae3",
"source_fingerprint": "977e94daeb0a6955a3d4ba76393d0b273d285537d0789eb59350d5c283901887",
"source_verified": true,
"git_preflight": {
"ok": true,
"head_sha": "c65a2f1ff9a0b30700ee14818e36e931eb71cae3",
"expected_head_sha": "c65a2f1ff9a0b30700ee14818e36e931eb71cae3",
"upstream": "origin/main",
"behind": 0,
"ahead": 0,
"origin": "forgejo-remote:coulomb/adaptive-pricing.git",
"errors": []
},
"projection_preflight": {
"required": true,
"ok": false,
"projections": [
{
"api_base": "http://127.0.0.1:8000",
"ok": true,
"replacement_checks": [
{
"record_id": "ADAPTIVE-WP-0010",
"kind": "workplan",
"current_status": 200,
"derived_status": 404,
"ok": true
},
{
"record_id": "ADAPTIVE-WP-0010-T01",
"kind": "task",
"current_status": 200,
"derived_status": 404,
"ok": true
},
{
"record_id": "ADAPTIVE-WP-0010-T02",
"kind": "task",
"current_status": 200,
"derived_status": 404,
"ok": true
},
{
"record_id": "ADAPTIVE-WP-0010-T03",
"kind": "task",
"current_status": 200,
"derived_status": 404,
"ok": true
},
{
"record_id": "ADAPTIVE-WP-0010-T04",
"kind": "task",
"current_status": 200,
"derived_status": 404,
"ok": true
}
]
},
{
"api_base": "http://127.0.0.1:18000",
"ok": false,
"replacement_checks": [
{
"record_id": "ADAPTIVE-WP-0010",
"kind": "workplan",
"current_status": 404,
"derived_status": 404,
"ok": false
},
{
"record_id": "ADAPTIVE-WP-0010-T01",
"kind": "task",
"current_status": 404,
"derived_status": 404,
"ok": false
},
{
"record_id": "ADAPTIVE-WP-0010-T02",
"kind": "task",
"current_status": 404,
"derived_status": 404,
"ok": false
},
{
"record_id": "ADAPTIVE-WP-0010-T03",
"kind": "task",
"current_status": 404,
"derived_status": 404,
"ok": false
},
{
"record_id": "ADAPTIVE-WP-0010-T04",
"kind": "task",
"current_status": 404,
"derived_status": 404,
"ok": false
}
]
}
],
"errors": [
{
"scope": "http://127.0.0.1:18000:ADAPTIVE-WP-0010",
"reason": "projection requires current UUID=200 and derived UUID=404"
},
{
"scope": "http://127.0.0.1:18000:ADAPTIVE-WP-0010-T01",
"reason": "projection requires current UUID=200 and derived UUID=404"
},
{
"scope": "http://127.0.0.1:18000:ADAPTIVE-WP-0010-T02",
"reason": "projection requires current UUID=200 and derived UUID=404"
},
{
"scope": "http://127.0.0.1:18000:ADAPTIVE-WP-0010-T03",
"reason": "projection requires current UUID=200 and derived UUID=404"
},
{
"scope": "http://127.0.0.1:18000:ADAPTIVE-WP-0010-T04",
"reason": "projection requires current UUID=200 and derived UUID=404"
}
]
},
"mapping_counts": {
"records": 5,
"replace": 5,
"assign": 0,
"unchanged": 0
},
"ready": false
}
],
"errors": [
{
"scope": "http://127.0.0.1:18000:ADAPTIVE-WP-0010",
"reason": "projection requires current UUID=200 and derived UUID=404"
},
{
"scope": "http://127.0.0.1:18000:ADAPTIVE-WP-0010-T01",
"reason": "projection requires current UUID=200 and derived UUID=404"
},
{
"scope": "http://127.0.0.1:18000:ADAPTIVE-WP-0010-T02",
"reason": "projection requires current UUID=200 and derived UUID=404"
},
{
"scope": "http://127.0.0.1:18000:ADAPTIVE-WP-0010-T03",
"reason": "projection requires current UUID=200 and derived UUID=404"
},
{
"scope": "http://127.0.0.1:18000:ADAPTIVE-WP-0010-T04",
"reason": "projection requires current UUID=200 and derived UUID=404"
}
],
"batch_sha256": "20decab5521ea5477826d1b2a9c9670912a96c4b8245faf5d270c8d1efc988c5"
}

File diff suppressed because it is too large Load diff

View file

@ -47,6 +47,8 @@ rmgr identifier migration-verify --plan uuid-migration.json --repo repo-manager
rmgr identifier migration-batch-plan \
--plan uuid-migration.json \
--repo whynot-design \
--projection-api-base http://127.0.0.1:8000 \
--projection-api-base http://127.0.0.1:18000 \
--output batch.json
rmgr identifier migration-batch-verify \
--plan uuid-migration.json \
@ -73,7 +75,12 @@ package. Each selected repository must still match the planned Git HEAD, have a
clean worktree, exactly match its configured upstream, and use a non-retired
origin. The batch records these facts and its own SHA-256 seal, but always emits
`apply_authorized: false`. An explicit decision must cite that batch hash before
any database or file mutation.
any database or file mutation. Repeat `--projection-api-base` for every hub in
the cutover: each replacement must resolve its current UUID with HTTP 200 and
its derived target with HTTP 404 on every named projection, or the manifest is
not ready for approval. Saved projection endpoints are rechecked by
`migration-batch-verify`; omitting the option retains the offline source/Git-only
planning mode.
Activation and applying a bulk migration remain separate governed steps.
Publishing or planning this function does not retroactively rewrite existing

View file

@ -0,0 +1,62 @@
apiVersion: helixforge.repo-manager/v1
kind: OwnerTaskInterface
metadata:
id: helixforge.identifiers.state-hub-sealed-projection-repair.v1
title: Repair an absent pre-derivation projection from a sealed migration unit
source:
repo: repo-manager
workplan_task: RMGR-WP-0005-T04
target:
repo: state-hub
owner_agent: state-hub
suggested_work_record: STATE-WP-0079 follow-up task or owner-selected workplan task
approval:
dispositions: [approved, amended, rejected]
record_in: State Hub file-backed workplan task or decision
task:
title: Add sealed repair for absent pre-derivation projection rows
priority: high
objective: >-
Allow an authoritative random UUID that predates deterministic derivation to
be restored into one partial projection only when a sealed Repo Manager
migration unit proves the exact repository, record identity, old UUID, and
derived target, so both projections can enter the existing transactional
cutover with matching source rows.
inputs:
- ref: repo-manager/docs/work-record-uuid-derivation_v1.md
authority: repo-manager
- ref: repo-manager/src/repo_manager/commands/registrar_reconcile.py
authority: repo-manager
- ref: state-hub/api/services/work_record_identifier_migration.py
authority: state-hub
deliverables:
- >-
Add an explicit registrar-only repair mode scoped to one canonical
workplan and one sealed repository migration unit; never widen ordinary
consistency repair for arbitrary random stale UUIDs.
- >-
Reconstruct the absent workplan and its mapped tasks with their exact old
UUIDs from the clean, synchronized authoritative file and existing
repository/topic projection, then verify exact API visibility.
- >-
Emit a non-secret receipt containing repository, canonical ids, old UUIDs,
plan seal, source Git revision, projection identity, and canonical UTC
observation time.
- Make retry idempotent and reject partial presence, target presence, source drift, or UUID conflict.
constraints:
- The normal C-03 random stale-reference refusal remains unchanged.
- Repair requires explicit primary confirmation and an exact sealed plan hash.
- Do not infer a repository, topic, canonical id, or UUID from path strings alone.
- Do not mutate the authoritative workplan file during projection repair.
- A failed repair must leave no partially created workplan or task rows.
acceptance:
- >-
A fixture with the old row present on one hub and absent on another repairs
only the absent projection and then passes current UUID=200 and derived UUID=404 preflight.
- A random stale UUID outside the sealed unit remains non-fixable C-03.
- Repeating the same repair returns a verified no-op receipt.
- A dirty or unsynchronized source, mismatched plan hash, or partially present unit is rejected.
- Existing empty-projection bootstrap and derived-row repair behavior remain unchanged.
verification:
- uv run pytest -q tests/test_consistency_check.py tests/test_work_record_identifier_migration.py
- make test

View file

@ -318,6 +318,13 @@ def main(argv: list[str] | None = None) -> int:
)
p_id_batch.add_argument("--plan", required=True)
p_id_batch.add_argument("--repo", action="append", required=True, dest="repos")
p_id_batch.add_argument(
"--projection-api-base",
action="append",
default=[],
dest="projection_api_bases",
help="Require current UUID=200 and derived UUID=404 on this projection",
)
p_id_batch.add_argument("--output", default=None)
p_id_batch.add_argument("--force", action="store_true")
p_id_batch_verify = identifier_sub.add_parser(
@ -772,7 +779,11 @@ def main(argv: list[str] | None = None) -> int:
plan = json.loads(Path(args.plan).read_text(encoding="utf-8"))
if not isinstance(plan, dict):
raise TypeError("migration plan must be a JSON object")
result = plan_identifier_migration_batch(plan, repo_slugs=args.repos)
result = plan_identifier_migration_batch(
plan,
repo_slugs=args.repos,
projection_api_bases=args.projection_api_bases,
)
except (OSError, TypeError, ValueError, json.JSONDecodeError) as exc:
print(json.dumps({"ok": False, "error": str(exc)}, indent=2))
return 1

View file

@ -551,7 +551,7 @@ def registrar_reconcile(
bootstrap_verified = projection_error is None
requested_verified = False
if completed.returncode == 1 and not any(after.values()):
if completed.returncode == 1 and any(before.values()) and not any(after.values()):
(
requested_workplans,
requested_tasks,
@ -578,7 +578,14 @@ def registrar_reconcile(
# A repository-scoped projection repair may coexist with legacy stale
# references that correctly keep the broader consistency report red.
accepted_exit_codes.add(1)
if completed.returncode not in accepted_exit_codes or any(after.values()):
mode_verification_failed = bool(repair_projection_id and not repair_verified) or (
bootstrap_empty_projection and not bootstrap_verified
)
if (
completed.returncode not in accepted_exit_codes
or any(after.values())
or mode_verification_failed
):
return RegistrarResult(
"failed",
evidence,

View file

@ -13,6 +13,7 @@ from collections import defaultdict
from pathlib import Path
from typing import Any
import httpx
import yaml
from repo_manager.cache import source_fingerprint
@ -359,8 +360,79 @@ def _git_cutover_preflight(repo: Path, *, expected_head_sha: str | None) -> dict
}
def _projection_migration_preflight(
mappings: list[dict[str, Any]], api_bases: list[str]
) -> dict[str, Any]:
"""Require every replacement source and no replacement target per projection."""
projections: list[dict[str, Any]] = []
errors: list[dict[str, str]] = []
for raw_base in api_bases:
api_base = raw_base.strip().rstrip("/")
try:
url = httpx.URL(api_base)
if (
url.scheme not in {"http", "https"}
or not url.host
or url.username
or url.password
or url.query
or url.fragment
):
raise ValueError
except (TypeError, ValueError):
errors.append(
{"scope": raw_base, "reason": "projection API base must be a plain HTTP(S) origin"}
)
continue
checks: list[dict[str, Any]] = []
try:
with httpx.Client(timeout=10.0, follow_redirects=False) as client:
for mapping in mappings:
if mapping.get("action") != "replace":
continue
route = "workplans" if mapping.get("kind") == "workplan" else "tasks"
current = client.get(f"{api_base}/{route}/{mapping.get('current_uuid')}")
derived = client.get(f"{api_base}/{route}/{mapping.get('derived_uuid')}")
check_ok = current.status_code == 200 and derived.status_code == 404
check = {
"record_id": mapping.get("record_id"),
"kind": mapping.get("kind"),
"current_status": current.status_code,
"derived_status": derived.status_code,
"ok": check_ok,
}
checks.append(check)
if not check_ok:
errors.append(
{
"scope": f"{api_base}:{mapping.get('record_id')}",
"reason": "projection requires current UUID=200 and derived UUID=404",
}
)
except httpx.HTTPError:
errors.append({"scope": api_base, "reason": "projection API is unavailable"})
projections.append(
{
"api_base": api_base,
"ok": all(check["ok"] for check in checks),
"replacement_checks": checks,
}
)
return {
"required": True,
"ok": bool(projections) and not errors,
"projections": projections,
"errors": errors,
}
def plan_identifier_migration_batch(
plan: dict[str, Any], *, repo_slugs: list[str]
plan: dict[str, Any],
*,
repo_slugs: list[str],
projection_api_bases: list[str] | None = None,
) -> dict[str, Any]:
"""Pin a reviewed repository batch after source and Git preflight.
@ -379,6 +451,7 @@ def plan_identifier_migration_batch(
for repository in plan.get("repositories", []):
repositories_by_slug[str(repository.get("repo") or "")].append(repository)
projection_api_bases = list(projection_api_bases or [])
batch_repositories: list[dict[str, Any]] = []
totals = {"repositories": 0, "records": 0, "replace": 0, "assign": 0, "unchanged": 0}
for slug in requested:
@ -410,6 +483,12 @@ def plan_identifier_migration_batch(
errors.append({"scope": slug, "reason": error["reason"]})
for reason in git_preflight["errors"]:
errors.append({"scope": slug, "reason": reason})
projection_preflight = None
if projection_api_bases:
projection_preflight = _projection_migration_preflight(
mappings, projection_api_bases
)
errors.extend(projection_preflight["errors"])
batch_repositories.append(
{
@ -419,8 +498,14 @@ def plan_identifier_migration_batch(
"source_fingerprint": repository.get("source_fingerprint"),
"source_verified": source_verification["ok"],
"git_preflight": git_preflight,
"projection_preflight": projection_preflight,
"mapping_counts": {"records": len(mappings), **action_counts},
"ready": source_verification["ok"] and git_preflight["ok"] and actionable > 0,
"ready": (
source_verification["ok"]
and git_preflight["ok"]
and actionable > 0
and (projection_preflight is None or projection_preflight["ok"])
),
}
)
totals["repositories"] += 1
@ -434,6 +519,7 @@ def plan_identifier_migration_batch(
"ready_for_approval": not errors,
"apply_authorized": False,
"approval_required": True,
"projection_api_bases": [base.strip().rstrip("/") for base in projection_api_bases],
"namespace": plan.get("namespace"),
"source_plan_sha256": plan_sha256,
"batch_policy": "repository-atomic, sequential, stop on first failure",
@ -484,7 +570,11 @@ def verify_identifier_migration_batch(
fresh = None
else:
try:
fresh = plan_identifier_migration_batch(plan, repo_slugs=repo_slugs)
fresh = plan_identifier_migration_batch(
plan,
repo_slugs=repo_slugs,
projection_api_bases=list(batch.get("projection_api_bases") or []),
)
except (OSError, ValueError) as exc:
errors.append({"scope": "batch", "reason": str(exc)})
fresh = None

View file

@ -3,6 +3,7 @@ from __future__ import annotations
import subprocess
from pathlib import Path
import httpx
import pytest
from repo_manager.identifiers import (
@ -213,10 +214,24 @@ def _push_fixture_to_upstream(repo: Path, remote: Path) -> None:
subprocess.run(["git", "push", "-u", "origin", "HEAD:main"], cwd=repo, check=True)
def test_migration_batch_plan_pins_clean_synchronized_repo(tmp_path: Path) -> None:
def test_migration_batch_plan_pins_clean_synchronized_repo(
tmp_path: Path, monkeypatch
) -> None:
repo = tmp_path / "one"
path = repo / "workplans" / "one.md"
_workplan(path, "ONE-WP-0001", "active")
path.write_text(
path.read_text(encoding="utf-8")
.replace(
"status: active\n---",
'status: active\nstate_hub_workstream_id: "11111111-1111-4111-8111-111111111111"\n---',
)
.replace(
"status: todo\n```",
'status: todo\nstate_hub_task_id: "22222222-2222-4222-8222-222222222222"\n```',
),
encoding="utf-8",
)
subprocess.run(["git", "init"], cwd=repo, check=True, capture_output=True)
subprocess.run(["git", "add", "."], cwd=repo, check=True, capture_output=True)
subprocess.run(
@ -228,15 +243,84 @@ def test_migration_batch_plan_pins_clean_synchronized_repo(tmp_path: Path) -> No
_push_fixture_to_upstream(repo, tmp_path / "remote.git")
plan = plan_identifier_migration(tmp_path, "helixforge")
batch = plan_identifier_migration_batch(plan, repo_slugs=["one"])
current_uuids = {
mapping["current_uuid"] for mapping in plan["repositories"][0]["mappings"]
}
def projection_get(self, url):
status = 200 if any(str(url).endswith(str(value)) for value in current_uuids) else 404
return httpx.Response(status, request=httpx.Request("GET", url))
monkeypatch.setattr(httpx.Client, "get", projection_get)
batch = plan_identifier_migration_batch(
plan,
repo_slugs=["one"],
projection_api_bases=["http://hub-one.test", "http://hub-two.test"],
)
assert batch["ok"] is True
assert batch["ready_for_approval"] is True
assert batch["apply_authorized"] is False
assert batch["approval_required"] is True
assert batch["projection_api_bases"] == [
"http://hub-one.test",
"http://hub-two.test",
]
assert batch["repositories"][0]["projection_preflight"]["ok"] is True
assert batch["repositories"][0]["git_preflight"]["upstream"] == "origin/main"
assert len(batch["batch_sha256"]) == 64
def projection_drift(self, url):
return httpx.Response(404, request=httpx.Request("GET", url))
monkeypatch.setattr(httpx.Client, "get", projection_drift)
verification = verify_identifier_migration_batch(batch, plan=plan)
assert verification["ok"] is False
assert verification["ready_for_decision"] is False
def test_migration_batch_plan_rejects_projection_gap(tmp_path: Path, monkeypatch) -> None:
repo = tmp_path / "one"
path = repo / "workplans" / "one.md"
_workplan(path, "ONE-WP-0001", "active")
path.write_text(
path.read_text(encoding="utf-8")
.replace(
"status: active\n---",
'status: active\nstate_hub_workstream_id: "11111111-1111-4111-8111-111111111111"\n---',
)
.replace(
"status: todo\n```",
'status: todo\nstate_hub_task_id: "22222222-2222-4222-8222-222222222222"\n```',
),
encoding="utf-8",
)
subprocess.run(["git", "init"], cwd=repo, check=True, capture_output=True)
subprocess.run(["git", "add", "."], cwd=repo, check=True, capture_output=True)
subprocess.run(
["git", "-c", "user.name=Test", "-c", "user.email=test@example.com", "commit", "-m", "seed"],
cwd=repo,
check=True,
capture_output=True,
)
_push_fixture_to_upstream(repo, tmp_path / "remote.git")
plan = plan_identifier_migration(tmp_path, "helixforge")
def projection_get(self, url):
return httpx.Response(404, request=httpx.Request("GET", url))
monkeypatch.setattr(httpx.Client, "get", projection_get)
batch = plan_identifier_migration_batch(
plan,
repo_slugs=["one"],
projection_api_bases=["http://hub.test"],
)
assert batch["ok"] is False
assert batch["ready_for_approval"] is False
assert batch["repositories"][0]["projection_preflight"]["ok"] is False
assert any("current UUID=200" in error["reason"] for error in batch["errors"])
def test_migration_batch_plan_rejects_dirty_or_duplicate_scope(tmp_path: Path) -> None:
repo = tmp_path / "one"

View file

@ -294,6 +294,54 @@ def test_repairs_an_already_identified_workplan_projection(tmp_path: Path, monke
assert brief.read_text(encoding="utf-8") == "authoritative local brief\n"
def test_repair_fails_when_exact_projection_remains_absent(tmp_path: Path, monkeypatch) -> None:
repo = _fixture(tmp_path)
workplan = repo / "workplans" / "DEMO-WP-0001.md"
text = workplan.read_text(encoding="utf-8")
text = text.replace(
"status: active\n---",
'status: active\nstate_hub_workstream_id: "11111111-1111-4111-8111-111111111111"\n---',
)
text = text.replace(
"priority: high\n```",
'priority: high\nstate_hub_task_id: "22222222-2222-4222-8222-222222222222"\n```',
)
workplan.write_text(text, encoding="utf-8")
_git(repo, "add", ".")
_git(repo, "commit", "-m", "add authoritative identifiers")
_git(repo, "push")
monkeypatch.setattr(
rr,
"_check_primary",
lambda _api: ({"status": "ok", "db": "connected"}, None),
)
monkeypatch.setattr(
rr,
"_projection_exists",
lambda _api, _projection_id: ({}, "workplan projection returned 404"),
)
monkeypatch.setattr(
rr,
"_run_statehub",
lambda command, *, env: subprocess.CompletedProcess(
command, 1, "stale reference remains", ""
),
)
result = rr.registrar_reconcile(
repo,
statehub_bin="statehub",
confirm_primary=True,
repair_workplan="DEMO-WP-0001",
)
assert result.status == "failed"
assert result.error and result.error["code"] == "registration_incomplete"
assert result.evidence["repair_projection_verified"] is False
assert "requested_projection_verified" not in result.evidence
def test_bootstraps_and_verifies_a_completely_empty_projection(tmp_path: Path, monkeypatch) -> None:
repo = _fixture(tmp_path)
workplan = repo / "workplans" / "DEMO-WP-0001.md"

View file

@ -541,6 +541,27 @@ note are in
`docs/evidence/RMGR-WP-0005-batch-0004-railiance-cluster-cutover-2026-08-22.md`.
T04 remains in progress for separately sealed and approved fleet batches.
**Batch 0005 preflight blocked safely (2026-08-22):** the refreshed zero-collision
fleet plan covers 39 repositories and 215 live records. A projection-aware
`adaptive-pricing` batch pinned its clean synchronized source and five UUID
replacements, then proved the workstation has all five old rows while production
has none. It therefore records `ready_for_approval: false` and authorizes no
mutation. A governed repair attempt retained State Hub's conservative C-03
refusal for absent random pre-derivation UUIDs.
That attempt exposed and fixed a Repo Manager fail-open result classification:
an empty ordinary-registration set could previously make an unverified exact
repair report `applied`. Repair and bootstrap modes now require their own exact
projection proof. Batch planning can also pin repeated
`--projection-api-base` endpoints and fails readiness unless every replacement
is current=200/derived=404 on every hub; verification repeats those live checks.
The remaining cross-owner gap is directly adoptable as
`helixforge.identifiers.state-hub-sealed-projection-repair.v1`. It asks State Hub
for a registrar-only, sealed, atomic restoration path without weakening normal
random stale-reference refusal. Evidence:
`docs/evidence/RMGR-WP-0005-batch-0005-adaptive-pricing-preflight-2026-08-22.md`.
## Retire the interim rule
```task