From 059de9358ebc94ad92f81b3f5c72ba6442e28618 Mon Sep 17 00:00:00 2001 From: tegwick Date: Sat, 22 Aug 2026 12:23:43 +0200 Subject: [PATCH] feat(consistency): rebuild authoritative intake IDs Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a023c0-a0a3-7c03-b395-5a0d2757214d --- api/schemas/decision.py | 3 ++- api/schemas/intake.py | 1 + scripts/consistency_check.py | 48 ++++++++++++++++++++++++++++++++---- tests/test_intake.py | 11 +++++++++ tests/test_routers_core.py | 11 +++++++++ 5 files changed, 68 insertions(+), 6 deletions(-) diff --git a/api/schemas/decision.py b/api/schemas/decision.py index 09e5260..edba07e 100644 --- a/api/schemas/decision.py +++ b/api/schemas/decision.py @@ -9,6 +9,7 @@ from pydantic import AliasChoices, Field class DecisionCreate(LegacyWorkstreamIdBodyMixin, BaseModel): + id: uuid.UUID | None = None topic_id: uuid.UUID | None = None workplan_id: uuid.UUID | None = Field( default=None, @@ -65,4 +66,4 @@ class DecisionRead(OptionalWorkplanIdCompatMixin, BaseModel): escalation_note: str | None = None superseded_by: uuid.UUID | None = None created_at: datetime - updated_at: datetime \ No newline at end of file + updated_at: datetime diff --git a/api/schemas/intake.py b/api/schemas/intake.py index ae1470d..54ff1ca 100644 --- a/api/schemas/intake.py +++ b/api/schemas/intake.py @@ -7,6 +7,7 @@ from api.models.intake import IntakeLane, IntakeOutcome, IntakeStatus class IntakeCreate(BaseModel): + id: uuid.UUID | None = None topic_id: uuid.UUID | None = None workplan_id: uuid.UUID | None = None repo_id: uuid.UUID | None = None diff --git a/scripts/consistency_check.py b/scripts/consistency_check.py index c222be0..2f05db0 100644 --- a/scripts/consistency_check.py +++ b/scripts/consistency_check.py @@ -987,7 +987,12 @@ def _inject_yaml_block_field( return False -def _check_work_record_registration(repo_dir: Path, report: "ConsistencyReport") -> None: +def _check_work_record_registration( + api_base: str, + repo_dir: Path, + report: "ConsistencyReport", + bootstrap_empty_projection: bool = False, +) -> None: """C-32: register kind: intake / kind: decision YAML blocks found anywhere in the repo against the hub (CUST-WP-0061-T02, work-record stage 3) — writes state_hub_intake_id / state_hub_decision_id back into @@ -1020,7 +1025,11 @@ def _check_work_record_registration(repo_dir: Path, report: "ConsistencyReport") id_field = _WORK_RECORD_ID_FIELD_BY_KIND[kind] existing = str(meta.get(id_field, "")).strip().strip('"') if existing and existing not in ("~", "null", "None", "none"): - continue + if not bootstrap_empty_projection: + continue + endpoint = _WORK_RECORD_CREATE_ENDPOINT_BY_KIND.get(kind) + if endpoint and _api_get(api_base, f"{endpoint}/{existing}") is not None: + continue rel = md.relative_to(repo_dir) if kind not in _WORK_RECORD_CREATE_ENDPOINT_BY_KIND: @@ -1036,11 +1045,21 @@ def _check_work_record_registration(repo_dir: Path, report: "ConsistencyReport") report.add( severity="WARN", check_id="C-32", - message=f"[{rel}] {kind} '{rid}' has no {id_field} — not indexed in DB", + message=( + f"[{rel}] {kind} '{rid}' is absent from the explicitly empty projection" + if existing + else f"[{rel}] {kind} '{rid}' has no {id_field} — not indexed in DB" + ), file_path=str(rel), file_value=rid, fixable=True, - _fix_context={"md_path": md, "kind": kind, "rid": rid, "meta": meta}, + _fix_context={ + "md_path": md, + "kind": kind, + "rid": rid, + "meta": meta, + "desired_record_id": existing or None, + }, ) @@ -1251,7 +1270,12 @@ def check_repo( _check_unregistered_work_records(repo_dir, report) # C-32: intake/decision work-record registration (CUST-WP-0061-T02) - _check_work_record_registration(repo_dir, report) + _check_work_record_registration( + api_base, + repo_dir, + report, + bootstrap_empty_projection=bootstrap_empty_projection, + ) # C-33: generated per-repo work-record index (CUST-WP-0061-T04) _check_work_record_index_freshness(repo_dir, repo_slug, report) @@ -3101,6 +3125,7 @@ def fix_repo( meta = ctx["meta"] id_field = _WORK_RECORD_ID_FIELD_BY_KIND[kind] title = str(meta.get("title") or rid) + desired_record_id = ctx.get("desired_record_id") repo_record = _api_get(api_base, f"/repos/{repo_slug}") if not repo_record: @@ -3111,6 +3136,7 @@ def fix_repo( created = None if kind == "intake": payload = { + "id": desired_record_id, "title": title, "repo_id": repo_record.get("id"), "description": meta.get("description"), @@ -3138,6 +3164,7 @@ def fix_repo( ) continue payload = { + "id": desired_record_id, "title": title, "topic_id": topic_id, "decision_type": meta.get("decision_type", "pending"), @@ -3154,6 +3181,17 @@ def fix_repo( continue new_id = created["id"] + if desired_record_id and new_id != desired_record_id: + report.fixes_applied.append( + f"C-32 FAIL {rid}: projection returned unexpected UUID {new_id}" + ) + continue + if desired_record_id: + report.fixes_applied.append( + f"C-32 fixed: created {kind} {new_id[:8]}… for {rid} " + "with the authoritative ID" + ) + continue if _inject_yaml_block_field(md_path, id_field, new_id, rid): report.fixes_applied.append( f"C-32 fixed: created {kind} {new_id[:8]}… for {rid}, " diff --git a/tests/test_intake.py b/tests/test_intake.py index 2eafc7a..ed69157 100644 --- a/tests/test_intake.py +++ b/tests/test_intake.py @@ -48,6 +48,17 @@ class TestIntakeCreateAndRead: assert body["lane"] == "green" assert body["outcome"] is None + async def test_create_preserves_explicit_authoritative_id(self, client): + await _create_domain(client) + topic = await _create_topic(client) + expected = "11111111-1111-4111-8111-111111111111" + r = await client.post( + "/intakes/", + json={"id": expected, "title": "file intake", "topic_id": topic["id"]}, + ) + assert r.status_code == 201 + assert r.json()["id"] == expected + async def test_get_unknown_intake_404s(self, client): r = await client.get("/intakes/00000000-0000-0000-0000-000000000000") assert r.status_code == 404 diff --git a/tests/test_routers_core.py b/tests/test_routers_core.py index 7f5592a..a60a7e6 100644 --- a/tests/test_routers_core.py +++ b/tests/test_routers_core.py @@ -407,6 +407,17 @@ class TestTasks: # --------------------------------------------------------------------------- class TestDecisions: + async def test_create_preserves_explicit_authoritative_id(self, client): + await _create_domain(client) + topic = await _create_topic(client) + expected = "22222222-2222-4222-8222-222222222222" + r = await client.post( + "/decisions/", + json={"id": expected, "title": "File decision", "topic_id": topic["id"]}, + ) + assert r.status_code == 201 + assert r.json()["id"] == expected + async def test_create_and_resolve(self, client): await _create_domain(client) topic = await _create_topic(client)