CB-WP-0019 T03/T04: the cost rule written down, and the lifecycle
Some checks failed
ci / check (push) Has been cancelled

T03: InnerLoop v1.7 plus loop-lint's own-cost check. Six passes
under-reported themselves by 30-45%, never once high, and the rule lived
only in evidence files having been re-derived three times. The READING
is load bearing, not the boundary: CB-WP-0018 T04 applied 're-run the
instrument at the moment of quoting' alone and its figure was correct.
So the operative instruction is re-run when you quote, and loop-lint
fails an evidence file naming its own workplan beside a dollar amount
without marking it provisional.

It binds forward from this pass. The check fires on seven historical
files which ARE the evidence for the rule; making them comply would edit
the record to remove the thing it proves -- the same category error as a
live fact: tag on a dated measurement, which this pass also hit.

Lifecycle, at the maintainer's instruction: ready -> active -> done,
where ready means declared and not started. loop-lint fails a workplan
that has started and still says ready, one that is active with
everything closed, and one that is done with an open task. The first
version of that check was WRONG and its own self-test caught it: it
stripped the leading status: assuming frontmatter, which silently
dropped a real task once the frontmatter said ready or active.

Both new checks then fired on this pass's own artifacts and both were
right to.

T04: CB-EV-0017. The new meta budget's first reading is a breach it
caused -- 27% against the 20% line, because this pass cost $31.18
against product passes averaging ~$21. Reported rather than exempted:
ADR-0006 D2 covers the instrument repairs but not the rule-writing, and
the honest reading is that this should have been two passes.

CB-WP-0018 settled at $36.53/95 against $28.08/82 last reported, 30%
higher. Seven for seven.

make all exits 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-08-03 19:25:18 +02:00
parent 0b6f7c5bc8
commit 69c1658d20
6 changed files with 483 additions and 25 deletions

View file

@ -94,6 +94,102 @@ def check_evidence_no_unmeasured(root=REPO):
return out
def check_own_cost_not_quoted(root=REPO):
"""§Quoting a cost — an evidence file may not quote its own pass's
cost as final.
Six passes under-reported themselves by 30-45%, never once high, so a
self-quoted figure is not a rounding error but a known bias. The check
is deliberately narrow: it fires only when a line names the file's OWN
workplan beside a dollar amount and does not mark it provisional.
Quoting an earlier pass is exactly what the rule asks for.
**Binds forward, from the pass that wrote it down.** The rule was
written in CB-WP-0019 after six passes had each under-reported
themselves, and those six evidence files are the *evidence for the
rule*. Firing on them would demand the record be edited to remove the
thing it proves the same category error as putting a live `fact:`
tag on a dated measurement. So the check applies from CB-WP-0019 on,
and the older figures stay as they were reported.
"""
BINDS_FROM = 19
out = []
base = os.path.join(root, "evidence")
if not os.path.isdir(base):
return out
for f in sorted(os.listdir(base)):
if not f.endswith(".md"):
continue
rel = os.path.join("evidence", f)
text = open(os.path.join(root, rel)).read()
# The pass an evidence file belongs to is named in its header.
m = re.search(r"\b(CB-WP-\d{4})\b", text)
if not m:
continue
own = m.group(1)
if int(own.rsplit("-", 1)[1]) < BINDS_FROM:
continue
for i, line in enumerate(text.splitlines(), 1):
if own not in line or "$" not in line:
continue
if "provisional" in line.lower() or "not quoted" in line.lower():
continue
out.append(
Finding("own-cost", f"{rel}:{i}",
f"quotes {own}'s own cost as final; re-run the "
f"instrument and quote a settled pass, or mark it "
f"provisional")
)
return out
def check_workplan_lifecycle(root=REPO):
"""§Workplan lifecycle — `ready` means declared and NOT started.
A workplan with work done in it that still says `ready` answers the
wrong question: the status should say whether anyone is on it, not
only whether it is finished.
"""
out = []
base = os.path.join(root, "workplans")
if not os.path.isdir(base):
return out
for f in sorted(os.listdir(base)):
if not f.endswith(".md"):
continue
rel = os.path.join("workplans", f)
text = open(os.path.join(root, rel)).read()
m = re.search(r"^status:\s*(\S+)", text, re.M)
if not m:
continue
status = m.group(1)
# Parse the ```task blocks, not every `status:` in the file. The
# first version stripped the leading match assuming it was the
# frontmatter — which silently dropped a real task the moment the
# frontmatter said `ready` or `active`, because those do not match
# the task vocabulary. Caught by this check's own self-test.
tasks = [
t.group(1)
for block in re.findall(r"```task\n(.*?)```", text, re.S)
for t in [re.search(r"^status:\s*(\S+)", block, re.M)]
if t
]
if not tasks:
continue
closed = sum(1 for t in tasks if t in ("done", "cancel"))
started = closed > 0
if status == "ready" and started:
out.append(Finding("lifecycle", rel,
"still `ready` but work has started — use `active`"))
elif status == "active" and closed == len(tasks):
out.append(Finding("lifecycle", rel,
"every task is closed but status is `active` — use `done`"))
elif status == "done" and closed != len(tasks):
out.append(Finding("lifecycle", rel,
f"`done` with {len(tasks) - closed} task(s) still open"))
return out
def check_survey_tier_and_chaos(root=REPO):
"""§Loop tiers — tier declared, and the chaos roll recorded every time."""
out = []
@ -213,6 +309,8 @@ def check_gate_registry(root=REPO):
CHECKS = (
check_loadability,
check_evidence_no_unmeasured,
check_own_cost_not_quoted,
check_workplan_lifecycle,
check_survey_tier_and_chaos,
check_review_trail,
check_reporting_tools_self_test,
@ -245,6 +343,53 @@ def self_test():
len(f) == 1 and "Big.md" in f[0].path,
f"{len(f)} finding(s)")
# own-cost: a file quoting its OWN pass beside a dollar amount
# trips; the same line marked provisional does not; and a file
# quoting an EARLIER pass does not, because that is the rule.
def ev(name, body):
with open(os.path.join(tmp, "evidence", name), "w") as fh:
fh.write(body)
ev("CB-EV-0100-self.md", "CB-WP-0019 T04.\n| CB-WP-0019 | $9.99 |\n")
f = check_own_cost_not_quoted(tmp)
check("own-cost detects a pass quoting itself", len(f) == 1,
f"{len(f)} finding(s)")
ev("CB-EV-0100-self.md",
"CB-WP-0019 T04.\n| CB-WP-0019 | $9.99 provisional |\n")
check("own-cost accepts a figure marked provisional",
not check_own_cost_not_quoted(tmp))
ev("CB-EV-0100-self.md", "CB-WP-0019 T04.\n| CB-WP-0018 | $28.08 |\n")
check("own-cost accepts quoting an EARLIER pass",
not check_own_cost_not_quoted(tmp))
# and it binds forward: the six passes that PROVE the rule are the
# evidence for it, and must not be edited to satisfy it.
ev("CB-EV-0100-self.md", "CB-WP-0009 T04.\n| CB-WP-0009 | $6.73 |\n")
check("own-cost binds forward, not over the record it rests on",
not check_own_cost_not_quoted(tmp))
os.remove(os.path.join(tmp, "evidence", "CB-EV-0100-self.md"))
# lifecycle: `ready` with work started trips; `active` does not.
def wp(status, tasks):
body = f"---\nid: CB-WP-0100\nstatus: {status}\n---\n"
for t in tasks:
body += f"\n```task\nid: CB-WP-0100-T\nstatus: {t}\npriority: high\n```\n"
with open(os.path.join(tmp, "workplans", "CB-WP-0100-x.md"), "w") as fh:
fh.write(body)
wp("ready", ["done", "todo"])
f = check_workplan_lifecycle(tmp)
check("lifecycle detects `ready` after work has started",
len(f) == 1 and "active" in f[0].detail, f"{len(f)} finding(s)")
wp("active", ["done", "todo"])
check("lifecycle accepts `active` mid-flight",
not check_workplan_lifecycle(tmp))
wp("active", ["done", "cancel"])
f = check_workplan_lifecycle(tmp)
check("lifecycle detects `active` when everything is closed",
len(f) == 1 and "done" in f[0].detail, f"{len(f)} finding(s)")
wp("done", ["done", "todo"])
check("lifecycle detects `done` with an open task",
len(check_workplan_lifecycle(tmp)) == 1)
os.remove(os.path.join(tmp, "workplans", "CB-WP-0100-x.md"))
# gates: an unclassified `all:` dependency trips, and so does an
# entry naming a target the Makefile lacks.
with open(os.path.join(tmp, "Makefile"), "w") as fh: