From 5c00c72d3b8e5d29a08132da9a76bde9d4405136 Mon Sep 17 00:00:00 2001 From: tegwick Date: Mon, 3 Aug 2026 18:55:11 +0200 Subject: [PATCH] status: a cancelled task is a closed one CB-WP-0005 read 5/8 and CB-WP-0007 read 2/6 in every status run since they closed, because only 'done' counted and their remaining tasks are 'cancel'. Two permanently-wrong numbers teach the reader to skip the column. Both now collapse into 'closed and complete' -- 18 of them -- while an open workplan with cancellations still shows the count separately, so a cancellation is visible rather than laundered into completion. The workplan block is extracted as render_workplans so the control is stated over what the tool PRINTS rather than over a literal the test wrote: a done+cancelled fixture must collapse, and one with a real todo must not. The first version of that control asserted arithmetic on its own input, which is the tautological shape ADR-0010 D2 demoted. Co-Authored-By: Claude Opus 5 --- tools/status.py | 42 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/tools/status.py b/tools/status.py index 1decc47..8ab398b 100644 --- a/tools/status.py +++ b/tools/status.py @@ -266,18 +266,37 @@ def report(): # length limit it is meant to keep. Collapse the fully-closed ones to # a single line rather than raising the limit — the precedent is # LOOP-LINT's four loadability breaches, each fixed structurally. + render_workplans(plans) + report_tail(plans) + + +def render_workplans(plans): + """The workplan block. Extracted so its behaviour can be asserted on + rather than re-derived in a test (ADR-0010 D2: controls are stated + over what the tool does, not over a literal the test wrote).""" print("\n workplans") closed = [] for wid, title, status, tasks, _kind in plans: + # `cancel` is a CLOSED outcome, not an open one. Counting only + # `done` left CB-WP-0005 reading "5/8" and CB-WP-0007 "2/6" in + # every status run since they closed — permanently wrong numbers, + # which teach the reader to skip the column. The two figures are + # kept separate so a cancellation is still visible rather than + # laundered into completion. done = sum(1 for _, s, _ in tasks if s == "done") - if status == "done" and done == len(tasks): + cancelled = sum(1 for _, s, _ in tasks if s == "cancel") + if status == "done" and done + cancelled == len(tasks): closed.append(wid.replace("CB-WP-", "")) continue short = (title or "")[:44] - print(f" {wid} {status:<12} {done}/{len(tasks)} done {short}") + note = f", {cancelled} cancelled" if cancelled else "" + print(f" {wid} {status:<12} {done}/{len(tasks)} done{note} {short}") if closed: print(f" {len(closed)} closed and complete: " + " ".join(closed)) + +def report_tail(plans): + """Next task, spend, budgets, fast gates — the rest of the report.""" nxt = next_task(plans) if nxt: wid, tid, prio = nxt @@ -351,6 +370,25 @@ def self_test(): abs(META_SOFT_PCT - 100 / TRAILING_PASSES) < 1e-9, f"{META_SOFT_PCT}% over {TRAILING_PASSES} passes; one meta pass at " f"parity reads {100 / TRAILING_PASSES:.0f}%") + # A workplan whose tasks are done-or-cancelled is CLOSED. Counting + # only `done` left two real workplans reading 5/8 and 2/6 forever. + def rendered(tasks): + out = io.StringIO() + saved, sys.stdout = sys.stdout, out + try: + render_workplans([("CB-WP-0000", "fixture", "done", tasks, "meta")]) + finally: + sys.stdout = saved + return out.getvalue() + + both = rendered([("T01", "done", "high"), ("T02", "cancel", "low")]) + check("a done+cancelled workplan collapses to closed", + "closed and complete" in both and "1/2" not in both, + both.strip().splitlines()[-1].strip()) + open_one = rendered([("T01", "done", "high"), ("T02", "todo", "low")]) + check("a workplan with a real todo does NOT collapse", + "1/2 done" in open_one, open_one.strip().splitlines()[-1].strip()) + check("the budget states what it is for", bool(META_PURPOSE) and "task at hand" in META_PURPOSE)