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:
parent
4398167580
commit
7a15f1da21
4 changed files with 152 additions and 2 deletions
|
|
@ -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
|
that invocation. Unrelated consistency failures remain visible but do not turn
|
||||||
a successfully verified scoped registration into a false failure.
|
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`.
|
Work: `RMGR-WP-0005-T01`.
|
||||||
|
|
||||||
## Coding-assistant commit provenance
|
## Coding-assistant commit provenance
|
||||||
|
|
|
||||||
|
|
@ -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]:
|
def _check_git(repo: Path) -> tuple[dict[str, Any], str | None]:
|
||||||
status = _git(repo, "status", "--porcelain")
|
status = _git(repo, "status", "--porcelain")
|
||||||
if status.returncode != 0:
|
if status.returncode != 0:
|
||||||
|
|
@ -390,13 +426,30 @@ def registrar_reconcile(
|
||||||
"record_identity": identity,
|
"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(
|
return RegistrarResult(
|
||||||
"rejected",
|
"rejected",
|
||||||
evidence,
|
evidence,
|
||||||
{
|
{
|
||||||
"code": "record_identity_collision",
|
"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,
|
cid,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,87 @@ def test_requires_explicit_primary_confirmation(tmp_path: Path) -> None:
|
||||||
assert result.error and result.error["code"] == "confirmation_required"
|
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:
|
def test_unlinked_closed_workplan_is_registrar_work(tmp_path: Path) -> None:
|
||||||
repo = _fixture(tmp_path)
|
repo = _fixture(tmp_path)
|
||||||
workplan = repo / "workplans" / "DEMO-WP-0001.md"
|
workplan = repo / "workplans" / "DEMO-WP-0001.md"
|
||||||
|
|
|
||||||
|
|
@ -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
|
locations, and emits the cleanup diagnostic. T02 remains `wait` because its
|
||||||
original pre-derivation reconciliation inventory is not yet exhausted.
|
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
|
## Derive identifiers deterministically
|
||||||
|
|
||||||
```task
|
```task
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue