From 7d04c054841a8494789e9b70f700174a60ad4701 Mon Sep 17 00:00:00 2001 From: tegwick Date: Tue, 29 Sep 2026 21:50:37 +0200 Subject: [PATCH] C-42: blocked workplan with no wait task (CUST-WP-0074-T06) Blocked-with-nothing-waiting is a stale status; warn until a qualified wait task exists or the workplan is re-statused. Co-Authored-By: Claude Fable 5.1 Assistant: claude-code Assistant-Model: sonnet Assistant-Process: 237582@bnt-lap001 Assistant-Session: f2b3d9f1-8fb9-4b9c-bc2b-837ec5dfc826 --- scripts/consistency_check.py | 21 +++++++++++++++++++++ tests/test_consistency_check.py | 25 +++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/scripts/consistency_check.py b/scripts/consistency_check.py index f41471d..c7e18d8 100644 --- a/scripts/consistency_check.py +++ b/scripts/consistency_check.py @@ -45,6 +45,7 @@ Checks: C-39 task-wait-unqualified WARN No wait task has none of task-block depends_on, needs_human: true, blocked_on: message-from:, wait_until: YYYY-MM-DD (CUST-WP-0074 rules 1/6) C-40 task-wait-blocker-satisfied WARN No wait task whose depends_on targets are all terminal, whose decision_id is resolved, or whose wait_until has passed (CUST-WP-0074 rules 3/4) C-41 task-wait-qualifier-drift WARN Yes needs_human/blocking_reason/decision_id/blocked_on/wait_until differ between file and DB (file wins) + C-42 workplan-blocked-no-wait WARN No blocked workplan has no task in status wait (CUST-WP-0074-T06) (C-20 also indexes task-block depends_on as workplan_dependencies rows with from_task_id set) (finished¬DoD-Ok is listed by `statehub quality-debt`, not per-file C-warn — avoids historical flood) @@ -1742,6 +1743,26 @@ def check_repo( }, ) + # C-42: a blocked workplan must say what it waits for — at least one + # task in `wait` (which C-39 then requires to be qualified). Blocked + # with no wait task is a stale status, not a block (CUST-WP-0074-T06). + if file_status == "blocked" and not any( + normalise_task_status(s) == "wait" for s in file_task_statuses + ): + report.add( + severity="WARN", + check_id="C-42", + message=( + f"Blocked workplan '{ws.get('slug')}' has no task in status " + f"wait — nothing states what it waits for; qualify a wait " + f"task or re-status the workplan" + ), + file_path=fname, + db_id=ws_id, + file_value=file_status, + fixable=False, + ) + # C-04: status drift — normalise file value before comparing so that # legacy file/API aliases are not treated as drift. Lifecycle repairs # take precedence so a stale planning file cannot regress active work. diff --git a/tests/test_consistency_check.py b/tests/test_consistency_check.py index 06c49f2..7e8a844 100644 --- a/tests/test_consistency_check.py +++ b/tests/test_consistency_check.py @@ -2442,6 +2442,31 @@ class TestTaskWaitQualifiers: assert len(self._issues(report, "C-39")) == 1 assert self._issues(report, "C-40") == [] + def test_c42_blocked_workplan_without_wait_task(self, tmp_path, monkeypatch): + # A blocked workplan whose only task is todo states nothing about what + # it waits for (CUST-WP-0074-T06). The head fixture is `blocked`. + repo = self._make_repo( + tmp_path, + "id: STATE-WP-0001-T01\nstatus: todo\npriority: high\n" + "state_hub_task_id: \"task-1\"", + ) + self._install(monkeypatch, self._fake_get(repo)) + report = check_repo("http://unused", "demo-repo") + c42 = self._issues(report, "C-42") + assert len(c42) == 1 and c42[0].fixable is False + assert "has no task in status wait" in c42[0].message + + # A qualified wait task satisfies it. + repo = self._make_repo( + tmp_path / "ok", + "id: STATE-WP-0001-T01\nstatus: wait\npriority: high\n" + "state_hub_task_id: \"task-1\"\nneeds_human: true\n" + "blocking_reason: \"operator decision\"", + ) + self._install(monkeypatch, self._fake_get(repo)) + report = check_repo("http://unused", "demo-repo") + assert self._issues(report, "C-42") == [] + def test_c41_ignores_blocks_that_do_not_carry_the_keys(self, tmp_path, monkeypatch): repo = self._make_repo( tmp_path,