fix(registrar): scope identity preflight

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a02b22-9638-76d2-bbff-b7ea1770b118
This commit is contained in:
tegwick 2026-08-23 11:49:59 +02:00
parent 4398167580
commit 7a15f1da21
4 changed files with 152 additions and 2 deletions

View file

@ -79,6 +79,13 @@ pass the central API explicitly; under ADR-010 the workstation service at
that invocation. Unrelated consistency failures remain visible but do not turn
a successfully verified scoped registration into a false failure.
Identity preflight follows the same scope. An invalid identifier or conflicting
UUID assignment in the requested set fails closed before State Hub runs. Legacy
identity defects elsewhere in the repository remain in the command evidence and
the consistency report, but do not block assignment of unrelated canonical
records. Empty-projection bootstrap remains a full-repository operation and
therefore still requires the complete identity set to pass.
Work: `RMGR-WP-0005-T01`.
## Coding-assistant commit provenance

View file

@ -101,6 +101,42 @@ def _missing_identifiers(repo: Path) -> dict[str, list[str]]:
}
def _requested_identity_findings(
identity: dict[str, Any],
requested: dict[str, list[str]],
*,
full_repository: bool = False,
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
"""Return invalid IDs and collisions that can affect this invocation.
Ordinary registrar reconciliation is deliberately scoped to the records
missing projection UUIDs. Historical defects elsewhere in the repository
remain visible in ``record_identity`` evidence, but cannot prevent safe
assignment of an unrelated canonical ID. A full empty-projection rebuild
still requires the entire repository identity set to be valid.
"""
if full_repository:
return (
list(identity["invalid_identifiers"]),
list(identity["identity_collisions"]),
)
requested_by_kind = {
"workplan": set(requested["workplans"]),
"task": set(requested["tasks"]),
"intake": set(requested["intakes"]),
"decision": set(requested["decisions"]),
}
def affects_request(item: dict[str, Any]) -> bool:
return item.get("id") in requested_by_kind.get(str(item.get("kind")), set())
return (
[item for item in identity["invalid_identifiers"] if affects_request(item)],
[item for item in identity["identity_collisions"] if affects_request(item)],
)
def _check_git(repo: Path) -> tuple[dict[str, Any], str | None]:
status = _git(repo, "status", "--porcelain")
if status.returncode != 0:
@ -390,13 +426,30 @@ def registrar_reconcile(
"record_identity": identity,
}
if identity["identity_collisions"]:
blocking_invalid, blocking_collisions = _requested_identity_findings(
identity,
before,
full_repository=bootstrap_empty_projection,
)
evidence["blocking_invalid_identifiers"] = blocking_invalid
evidence["blocking_identity_collisions"] = blocking_collisions
if blocking_invalid:
return RegistrarResult(
"rejected",
evidence,
{
"code": "record_identifier_invalid",
"message": "a requested work-record id is not accepted by the canon registry",
},
cid,
)
if blocking_collisions:
return RegistrarResult(
"rejected",
evidence,
{
"code": "record_identity_collision",
"message": "same canonical work-record id has conflicting or incomplete UUID assignments",
"message": "a requested canonical work-record id has conflicting or incomplete UUID assignments",
},
cid,
)

View file

@ -51,6 +51,87 @@ def test_requires_explicit_primary_confirmation(tmp_path: Path) -> None:
assert result.error and result.error["code"] == "confirmation_required"
def test_unrelated_historical_collision_does_not_block_scoped_request(
tmp_path: Path,
) -> None:
repo = _fixture(tmp_path)
for number, task_uuid in (
(2, "22222222-2222-4222-8222-222222222222"),
(3, "33333333-3333-4333-8333-333333333333"),
):
(repo / "workplans" / f"DEMO-WP-000{number}.md").write_text(
f"""---
id: DEMO-WP-000{number}
type: workplan
title: Historical {number}
status: finished
state_hub_workstream_id: "{number}{'1' * 7}-1111-4111-8111-111111111111"
---
## Historical task
```task
id: DEMO-WP-9999-T01
status: done
priority: low
state_hub_task_id: "{task_uuid}"
```
""",
encoding="utf-8",
)
result = rr.registrar_reconcile(repo)
assert result.status == "rejected"
assert result.error and result.error["code"] == "confirmation_required"
assert result.evidence["record_identity"]["identity_collisions"]
assert result.evidence["blocking_identity_collisions"] == []
def test_requested_collision_still_fails_closed(tmp_path: Path) -> None:
repo = _fixture(tmp_path)
second = repo / "workplans" / "DEMO-WP-0002.md"
second.write_text(
"""---
id: DEMO-WP-0002
type: workplan
title: Conflicting request
status: active
---
## Conflicting task
```task
id: DEMO-WP-0001-T01
status: todo
priority: high
```
""",
encoding="utf-8",
)
result = rr.registrar_reconcile(repo)
assert result.status == "rejected"
assert result.error and result.error["code"] == "record_identity_collision"
assert result.evidence["blocking_identity_collisions"][0]["id"] == "DEMO-WP-0001-T01"
def test_requested_invalid_identifier_fails_before_registration(tmp_path: Path) -> None:
repo = _fixture(tmp_path)
workplan = repo / "workplans" / "DEMO-WP-0001.md"
workplan.write_text(
workplan.read_text(encoding="utf-8").replace("DEMO-WP-0001", "DEMO-INVALID"),
encoding="utf-8",
)
result = rr.registrar_reconcile(repo)
assert result.status == "rejected"
assert result.error and result.error["code"] == "record_identifier_invalid"
assert result.evidence["blocking_invalid_identifiers"][0]["id"] == "DEMO-INVALID"
def test_unlinked_closed_workplan_is_registrar_work(tmp_path: Path) -> None:
repo = _fixture(tmp_path)
workplan = repo / "workplans" / "DEMO-WP-0001.md"

View file

@ -307,6 +307,15 @@ against `ops-mason` indexes `MASON-0001-T01` once, preserves both task-block
locations, and emits the cleanup diagnostic. T02 remains `wait` because its
original pre-derivation reconciliation inventory is not yet exhausted.
**Scoped identity compatibility (2026-08-23):** Custodian owner work exposed
that the on-demand registrar still applied its collision gate repository-wide:
unrelated grandfathered bare `T01``T09` collisions prevented assignment of the
canonical `CUST-WP-0065-T01``T04` set. The preflight now intersects invalid IDs
and collision findings with the exact missing records requested by the command.
A requested defect still fails closed; unrelated history stays in evidence;
empty-projection bootstrap retains the full-repository gate. Regression coverage
proves all three cases.
## Derive identifiers deterministically
```task