fix(consistency): projection-retired rows are C-08 tombstones, not C-07 failures
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Build and Publish Multi-Context Image / build-and-push (push) Successful in 30s

Forge-projection retirement stamps the slug with "@retired-YYYYMMDD" and sets
projection_retired_at but leaves status untouched. Three railiance-platform
rows retired on 2026-08-26 while blocked (and re-set to blocked by a
2026-09-27 user-requested correction) therefore failed C-07 permanently, with
no file the repo could add. Their canonical successors are file-backed
RPF-WP-0015/0025/0027.

_check_orphan_db now reports retired rows (projection_retired_at, or the slug
stamp on APIs that do not yet expose it) as C-08 INFO. WorkplanRead exposes
projection_retired_at so read clients can filter tombstones.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 1370019@bnt-lap001
Assistant-Session: d462dd63-16de-4189-ae70-b9cd6566bf87
This commit is contained in:
tegwick 2026-10-11 21:08:20 +02:00
parent c1eee0a4cb
commit 5830ba3c15
3 changed files with 72 additions and 2 deletions

View file

@ -155,6 +155,9 @@ class WorkplanRead(WorkplanStatusMixin):
backing_relative_path: str | None = None
backing_archived: bool | None = None
backing_synced_at: datetime | None = None
# Set when forge projection retired the row (slug stamped "@retired-").
# A retired row is a tombstone, not live work, whatever its status.
projection_retired_at: datetime | None = None
# Derived (CUST-WP-0074 rule 5): "human" | "external" | "none" while
# status is blocked, else None. Set by
# api.services.wait_kind.annotate_workplans on read routes.

View file

@ -13,7 +13,7 @@ Checks:
C-06 workstream-unlinked WARN Yes Workplan has no state_hub_workstream_id
--fix derives the UUID from the canonical record id and writes it back
C-07 orphan-db-active FAIL No Active DB workstream, no backing file
C-08 orphan-db-closed INFO No Finished/archived DB workstream, no file
C-08 orphan-db-closed INFO No Finished/archived (or projection-retired) DB workstream, no file
C-09 workstream-repo-mismatch FAIL Yes DB workstream repo_id != file location
C-10 task-status-drift WARN Yes Task status differs between file and DB
C-11 task-unlinked WARN Yes Task block has no state_hub_task_id
@ -2522,6 +2522,14 @@ def _sync_workplan_bindings(
)
RETIRED_SLUG_MARK = "@retired-" # mirrors api.services.forge_projection
def _is_projection_retired(ws: dict) -> bool:
"""True for a row forge projection retired (field, or the slug stamp on older APIs)."""
return bool(ws.get("projection_retired_at")) or RETIRED_SLUG_MARK in (ws.get("slug") or "")
def _check_orphan_db(
api_base: str,
repo_id: str,
@ -2529,7 +2537,15 @@ def _check_orphan_db(
report: ConsistencyReport,
active_file_ws_ids: set[str] | None = None,
) -> None:
"""Flag DB workstreams with repo_id=this_repo that have no backing workplan file."""
"""Flag DB workstreams with repo_id=this_repo that have no backing workplan file.
A row retired by forge projection is a tombstone: retirement stamps the
slug with ``@retired-YYYYMMDD`` and sets ``projection_retired_at`` but
deliberately leaves status alone, so a row retired while blocked stays
blocked. Its canonical successor is file-backed under a new id. Such rows
are reported as C-08 INFO, never C-07 FAIL — the repo has nothing to fix,
and closing them by hand would rewrite history the retirement preserved.
"""
active_file_ws_ids = active_file_ws_ids or file_ws_ids
all_ws = _api_get(api_base, "/workplans", {"repo_id": repo_id})
if not isinstance(all_ws, list):
@ -2543,6 +2559,18 @@ def _check_orphan_db(
if normalised_status in CLOSED_WORKSTREAM_STATUSES and ws_id in file_ws_ids:
continue
ws_slug = ws.get("slug", "")
if _is_projection_retired(ws) and ws_id not in file_ws_ids:
report.add(
severity="INFO", check_id="C-08",
message=(
f"Retired DB workstream '{ws_slug}' "
f"(id={ws_id[:8]}…, status={ws_status}) has no backing workplan file "
f"— projection tombstone, not live work"
),
db_id=ws_id,
fixable=False,
)
continue
if normalised_status not in CLOSED_WORKSTREAM_STATUSES:
report.add(
severity="FAIL", check_id="C-07",

View file

@ -0,0 +1,39 @@
"""C-07 must not fail on rows forge projection retired (railiance-platform, 2026-10-11).
Retirement stamps the slug with ``@retired-YYYYMMDD`` and sets
``projection_retired_at`` but leaves status untouched, so a row retired while
blocked stayed blocked and C-07 reported it as an ADR-001 violation the repo
could never fix.
"""
from __future__ import annotations
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
import consistency_check # noqa: E402
from consistency_check import ConsistencyReport, _check_orphan_db # noqa: E402
def _run(monkeypatch, rows, file_ids=frozenset()):
monkeypatch.setattr(consistency_check, "_api_get", lambda *a, **k: rows)
report = ConsistencyReport(repo_slug="r", repo_path="/tmp/r")
_check_orphan_db("http://hub", "repo-id", set(file_ids), report)
return [(i.severity, i.check_id) for i in report.issues]
def test_retired_slug_blocked_row_is_info_not_fail(monkeypatch):
rows = [{"id": "038bc3c0-0000", "slug": "railiance-wp-0029@retired-20260826", "status": "blocked"}]
assert _run(monkeypatch, rows) == [("INFO", "C-08")]
def test_projection_retired_at_field_is_info_not_fail(monkeypatch):
rows = [{"id": "aaaa0000-0000", "slug": "x-wp-0001", "status": "active",
"projection_retired_at": "2026-08-26T00:00:00Z"}]
assert _run(monkeypatch, rows) == [("INFO", "C-08")]
def test_unretired_open_orphan_still_fails(monkeypatch):
rows = [{"id": "bbbb0000-0000", "slug": "x-wp-0002", "status": "blocked"}]
assert _run(monkeypatch, rows) == [("FAIL", "C-07")]