fix(consistency): projection-retired rows are C-08 tombstones, not C-07 failures
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:
parent
c1eee0a4cb
commit
5830ba3c15
3 changed files with 72 additions and 2 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
39
tests/test_c07_retired_rows.py
Normal file
39
tests/test_c07_retired_rows.py
Normal 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")]
|
||||
Loading…
Add table
Add a link
Reference in a new issue