fix: stop C-23 promoting proposed and C-15 overwriting files
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

ADR-007 / RMGR-WP-0005-T06: proposed stays a human gate. Task status
follows the file (ADR-001); wait is not "ahead" of progress or todo.
This commit is contained in:
tegwick 2026-08-18 13:36:26 +02:00
parent aa45c9b6b0
commit 6d038fd050
2 changed files with 66 additions and 34 deletions

View file

@ -19,7 +19,7 @@ Checks:
C-12 orphan-db-task WARN Yes DB task in workstream has no file backing unless terminal in a closed workstream
C-13 workstream-auto-complete WARN Yes All DB tasks done but workstream still active
C-14 ghost-duplicate WARN No Active topic workstream with no repo_id matches a file-backed title probable ghost from premature create_workstream() call
C-15 task-db-ahead WARN Yes DB task status is ahead of file regression prevented; writeback syncs file
C-15 task-db-ahead WARN No DB task is terminal and file is not report only; file wins (ADR-001)
C-16 repo-behind-remote WARN No Local repo is behind remote tracking branch --fix skipped to avoid clobbering remote progress
C-17 repo-ahead-push-failed WARN No Local repo has unpushed commits and push failed writes skipped to prevent runaway divergence
C-19 workstream-planning-drift WARN Yes planning_priority/planning_order differs between file and DB
@ -281,7 +281,10 @@ RENORMALIZATION_RULES: tuple[RenormalizationRule, ...] = (
"Workplan status is proposed, ready, or backlog while a linked "
"task is progress or wait."
),
repair="Patch the DB workstream and workplan frontmatter to status=active.",
repair=(
"ready/backlog: patch DB and file to active. "
"proposed: report only (ADR-007 decision 3)."
),
test_anchor="tests/test_consistency_check.py::TestLifecycleRenormalization",
),
)
@ -1302,18 +1305,27 @@ def check_repo(api_base: str, repo_slug: str, repo_path_override: str | None = N
task_statuses=[*file_task_statuses, *db_task_statuses],
)
if active_task_requires_activation:
# ADR-007 decision 3 / RMGR-WP-0005-T06: automation may report
# that a proposed workplan has started work, but must not promote
# proposed → active. proposed is a human review gate.
proposed_gate = file_status == "proposed"
report.add(
severity="WARN",
check_id="C-23",
message=(
f"Lifecycle drift in '{ws.get('slug')}': workplan status "
f"{file_status!r} has an active task — repair to 'active'"
f"{file_status!r} has an active task"
+ (
" — report only (ADR-007: do not auto-promote proposed)"
if proposed_gate
else " — repair to 'active'"
)
),
file_path=fname,
db_id=ws_id,
file_value=file_status,
db_value=ws.get("status", ""),
fixable=True,
fixable=not proposed_gate,
_fix_context={
"ws_id": ws_id,
"wp_file": str(wp_file),
@ -1544,48 +1556,45 @@ def check_repo(api_base: str, repo_slug: str, repo_path_override: str | None = N
fixable=False,
)
continue
# C-10 / C-15: task status drift
# C-10 / C-15: task status drift. ADR-001: the file originates
# work. Same-rank wait/progress is not "DB ahead" — writing
# wait over progress/todo is the CFED/RMGR-WP-0004-T02 bug.
# C-15 remains a report-only review signal when the DB is
# terminal and the file is not.
db_t_status = normalise_task_status(db_task.get("status", "todo"))
if t_status and db_t_status and t_status != db_t_status:
db_rank = STATUS_ORDER.get(db_t_status, 0)
file_rank = STATUS_ORDER.get(t_status, 0)
if db_rank >= file_rank:
# C-15: DB is already at the same rank or ahead — prevent
# regression. Writeback syncs the file to match DB.
db_terminal = db_t_status in {"done", "cancel"}
file_terminal = t_status in {"done", "cancel"}
if db_terminal and not file_terminal:
report.add(
severity="WARN", check_id="C-15",
message=(
f"DB task '{t_id}' is ahead of file "
f"DB task '{t_id}' is terminal "
f"(db={db_t_status!r}, file={t_status!r}) "
f"— regression prevented; writeback will sync file"
f"— report only; file wins (ADR-001 / RMGR-WP-0005-T06)"
),
file_path=f"{fname}#{t_id}",
db_id=t_sh_id,
file_value=t_status,
db_value=db_t_status,
fixable=True,
_fix_context={
"task_id": t_sh_id,
"wp_file": str(wp_file),
"task_block_id": t_id,
"db_status": db_t_status,
},
)
else:
# C-10: file is ahead — apply file→DB sync (normal drift)
report.add(
severity="WARN", check_id="C-10",
message=(
f"Task status drift '{t_id}': "
f"file={t_status!r} db={db_t_status!r} (file wins)"
),
file_path=f"{fname}#{t_id}",
db_id=t_sh_id,
file_value=t_status,
db_value=db_t_status,
fixable=True,
_fix_context={"task_id": t_sh_id, "status": t_status},
fixable=False,
)
# File always wins, including same-rank wait ↔ progress.
report.add(
severity="WARN", check_id="C-10",
message=(
f"Task status drift '{t_id}': "
f"file={t_status!r} db={db_t_status!r} (file wins)"
),
file_path=f"{fname}#{t_id}",
db_id=t_sh_id,
file_value=t_status,
db_value=db_t_status,
fixable=True,
_fix_context={"task_id": t_sh_id, "status": t_status},
)
file_description = task.get("description")
if isinstance(file_description, str):
file_description = file_description.strip() or None

View file

@ -993,10 +993,10 @@ class TestLifecycleRenormalization:
assert "C-23" in check_ids
assert "C-04" not in check_ids
issue = next(issue for issue in report.issues if issue.check_id == "C-23")
assert issue.fixable is True
assert issue.fixable is False
assert issue.file_value == "proposed"
def test_fix_repo_repairs_planning_workplan_with_active_task(self, tmp_path, monkeypatch):
def test_fix_repo_does_not_promote_proposed(self, tmp_path, monkeypatch):
repo = self._make_repo(tmp_path)
wp = repo / "workplans" / "STATE-WP-0001-demo.md"
patches = []
@ -1015,6 +1015,29 @@ class TestLifecycleRenormalization:
report = fix_repo("http://unused", "state-hub")
assert ("/workplans/ws-1", {"status": "active"}) not in patches
assert "status: proposed" in wp.read_text(encoding="utf-8")
assert not any("C-23 fixed" in fix for fix in report.fixes_applied)
def test_fix_repo_repairs_planning_workplan_with_active_task(self, tmp_path, monkeypatch):
repo = self._make_repo(tmp_path, status="ready")
wp = repo / "workplans" / "STATE-WP-0001-demo.md"
patches = []
def fake_patch(_api_base, path, body):
patches.append((path, body))
return {"ok": True}
monkeypatch.setattr("consistency_check._api_get", self._api_get_for_repo(repo))
monkeypatch.setattr("consistency_check._api_patch", fake_patch)
monkeypatch.setattr("consistency_check._detect_behind_remote", lambda _repo_path: False)
monkeypatch.setattr("consistency_check._detect_ahead_of_remote", lambda _repo_path: 0)
monkeypatch.setattr("consistency_check._git_commit_writeback", lambda *args, **kwargs: True)
monkeypatch.setattr("consistency_check._write_custodian_brief", lambda *args, **kwargs: False)
monkeypatch.setattr("consistency_check._git_push", lambda _repo_path: (True, "pushed"))
report = fix_repo("http://unused", "state-hub")
assert ("/workplans/ws-1", {"status": "active"}) in patches
assert "status: active" in wp.read_text(encoding="utf-8")
assert any("C-23 fixed" in fix for fix in report.fixes_applied)