Some checks failed
ci / check (push) Failing after 4s
The condition is met and the tally was verified rather than recalled. ADR-0017 D2 named window 3 as the decider; windows 2 and 3 each produced exactly one override and each changed nothing. Every roll in window 3 was cross-checked against the workplan that made it, because the last time this project tallied chaos rolls from memory it was wrong and asserted the wrong figure four times (F23). All twelve agree. But meeting the condition is not evidence. P(an override changes nothing) is 1/3, and each window had exactly one override, so the condition fires on a 1/9 coincidence. ADR-0017 restated it to be REACHABLE and made it weak in the process; reachability was checked and discriminating power was not. Worse, it measures the wrong subject. It asks whether an override changed the tier; the question is whether changing the tier helped. Under it, a die that always changed the tier could never be retired however useless its changes were. The real ground is stronger. Four overrides across roughly forty declarations, and the mechanism's value has never once been demonstrated. The one substantive intervention dropped CB-WP-0011 from a structural L to S, and that work then needed CB-WP-0016 and CB-WP-0017 to fix defects a human found by playing. Not offered as causation — a tier is process weight, not a guarantee — but it is the only evidence we have about an override's consequences and it points the wrong way. And the purpose has no live evidence of need: 17 M, 11 S, 4 L across every workplan, with the only two structural/declared mismatches being the window-1 overrides themselves. Tier declaration has not ossified. So: retired, with nothing replacing it. Adding a successor to guard against ossification that has not occurred would invent a gate for an instance we do not have. The revival trigger is stated: tiers collapsing toward one value, or a pass declaring below its structural tier to dodge a review. InnerLoop loses the chaos paragraph, loop-lint loses the chaos-recorded check — a check that outlives its rule becomes an obstruction — and its self-test now asserts the opposite: a note with no roll must pass. ChaosRollHistory is closed. Window 4 ends incomplete at four declarations, the last of which is this one, rolled because the rule was still in force. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
711 lines
30 KiB
Python
711 lines
30 KiB
Python
#!/usr/bin/env python3
|
|
"""Executable checks for specs/InnerLoop.md rules (CB-WP-0003 T01).
|
|
|
|
The loop's own rules were prose. A rule nobody can run is a suggestion,
|
|
and the audit in history/260731-inner-loop-rule-audit.md found several
|
|
that were already being violated with no signal. This makes the
|
|
mechanically-checkable ones fail a command.
|
|
|
|
Each check names the InnerLoop rule it enforces. Checks that cannot be
|
|
made mechanical are recorded in the audit as `checkable` or `decorative`
|
|
and are deliberately absent here — see the audit for why.
|
|
|
|
Positive control (InnerLoop v1.1 §Step 5): --self-test asserts each check
|
|
actually detects its failure, using fixtures with known answers. A linter
|
|
that passes everything because its matcher is broken is the same defect
|
|
class as a benchmark timing rejected work.
|
|
|
|
Usage:
|
|
python3 tools/loop-lint.py # lint the repo
|
|
python3 tools/loop-lint.py --self-test # positive control
|
|
"""
|
|
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
from repo import ROOT as REPO # noqa: E402 (single source of fact, T01)
|
|
LOADABILITY_LIMIT = 400
|
|
|
|
# Artifact classes the loop produces. history/ is an append-only trail
|
|
# (verbatim challenge text is not something to split), so it is exempt.
|
|
LOOP_DIRS = ("specs", "research", "decisions", "evidence", "workplans")
|
|
|
|
|
|
class Finding:
|
|
def __init__(self, rule, path, detail):
|
|
self.rule, self.path, self.detail = rule, path, detail
|
|
|
|
def __str__(self):
|
|
return f" [{self.rule}] {self.path}\n {self.detail}"
|
|
|
|
|
|
def _md_files(root=REPO):
|
|
"""Loop artifacts only.
|
|
|
|
Vendored third-party trees (a baseline harness ships its own
|
|
node_modules) are not artifacts this loop produces, and linting them
|
|
buries the two real findings under eighteen irrelevant ones.
|
|
"""
|
|
for d in LOOP_DIRS:
|
|
base = os.path.join(root, d)
|
|
for dirpath, dirnames, files in os.walk(base):
|
|
dirnames[:] = [x for x in dirnames if x != "node_modules"]
|
|
for f in sorted(files):
|
|
if f.endswith(".md"):
|
|
yield os.path.relpath(os.path.join(dirpath, f), root)
|
|
|
|
|
|
def check_loadability(root=REPO):
|
|
"""§Agentic-efficiency 1 — every loop artifact stays under ~400 lines."""
|
|
out = []
|
|
for rel in _md_files(root):
|
|
with open(os.path.join(root, rel)) as fh:
|
|
n = sum(1 for _ in fh)
|
|
if n > LOADABILITY_LIMIT:
|
|
out.append(
|
|
Finding(
|
|
"loadability",
|
|
rel,
|
|
f"{n} lines exceeds the ~{LOADABILITY_LIMIT}-line limit; "
|
|
f"split and link with relative paths",
|
|
)
|
|
)
|
|
return out
|
|
|
|
|
|
def check_evidence_no_unmeasured(root=REPO):
|
|
"""§Rubric — `unmeasured` is legal in a survey, illegal in an evidence file."""
|
|
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)
|
|
for i, line in enumerate(open(os.path.join(root, rel)), 1):
|
|
# A row asserting the verdict, not prose discussing the word.
|
|
if re.search(r"\|\s*unmeasured\s*\|", line):
|
|
out.append(
|
|
Finding("evidence-unmeasured", f"{rel}:{i}",
|
|
"verdict `unmeasured` in an evidence table")
|
|
)
|
|
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 — the tier is declared.
|
|
|
|
**The chaos half is retired** (ADR-0021, 2026-08-08). Requiring the
|
|
roll would fail every future research note: a check that outlives its
|
|
rule stops being a control and becomes an obstruction. The name is
|
|
kept because `gates.toml` and the self-tests refer to it.
|
|
"""
|
|
out = []
|
|
base = os.path.join(root, "research")
|
|
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("research", f)
|
|
text = open(os.path.join(root, rel)).read()
|
|
if not re.search(r"^tier:\s*[SML]\b", text, re.M):
|
|
out.append(Finding("tier-declared", rel, "no `tier:` declaration"))
|
|
return out
|
|
|
|
|
|
def check_review_trail(root=REPO):
|
|
"""§Step 2 — a tier-L survey carries research/challenge/response history."""
|
|
out = []
|
|
base = os.path.join(root, "research")
|
|
hist = os.path.join(root, "history")
|
|
if not (os.path.isdir(base) and os.path.isdir(hist)):
|
|
return out
|
|
files = os.listdir(hist)
|
|
for f in sorted(os.listdir(base)):
|
|
if not f.endswith(".md"):
|
|
continue
|
|
rel = os.path.join("research", f)
|
|
text = open(os.path.join(root, rel)).read()
|
|
if not re.search(r"^tier:\s*L\b", text, re.M):
|
|
continue
|
|
if not re.search(r"^status:\s*approved", text, re.M):
|
|
continue
|
|
for kind in ("challenge", "response"):
|
|
if not any(x.endswith(f"-{kind}.md") and kind in x for x in files):
|
|
out.append(
|
|
Finding("review-trail", rel,
|
|
f"tier-L approved survey with no history/*-{kind}.md")
|
|
)
|
|
return out
|
|
|
|
|
|
def check_reporting_tools_self_test(root=REPO):
|
|
"""§Step 5 v1.1 — every tool that reports a number exposes --self-test."""
|
|
out = []
|
|
base = os.path.join(root, "tools")
|
|
if not os.path.isdir(base):
|
|
return out
|
|
for f in sorted(os.listdir(base)):
|
|
if not f.endswith(".py"):
|
|
continue
|
|
rel = os.path.join("tools", f)
|
|
text = open(os.path.join(root, rel)).read()
|
|
if "--self-test" not in text:
|
|
out.append(
|
|
Finding("self-test", rel,
|
|
"reporting tool with no --self-test entry point; "
|
|
"nothing verifies its positive control still works")
|
|
)
|
|
return out
|
|
|
|
|
|
def check_gate_registry(root=REPO):
|
|
"""ADR-0006 D3 — every control gate is in `gates.toml`, and every
|
|
entry names a real target.
|
|
|
|
The failure this prevents is drift in the direction nobody notices: a
|
|
gate added to `make all` with no registry entry never acquires a
|
|
review date, which is how five mechanisms accumulated with no way to
|
|
retire any of them.
|
|
"""
|
|
out = []
|
|
registry = os.path.join(root, "gates.toml")
|
|
makefile = os.path.join(root, "Makefile")
|
|
if not (os.path.exists(registry) and os.path.exists(makefile)):
|
|
return out
|
|
try:
|
|
import tomllib
|
|
except ModuleNotFoundError: # pragma: no cover
|
|
return out
|
|
|
|
with open(registry, "rb") as fh:
|
|
data = tomllib.load(fh)
|
|
gates = data.get("gate") or []
|
|
if not gates:
|
|
return [Finding("gates", "gates.toml", "registry contains no gates")]
|
|
registered = {g.get("target") for g in gates if g.get("target")}
|
|
exempt = set(data.get("not_control_gates") or [])
|
|
|
|
text = open(makefile).read()
|
|
m = re.search(r"^all:(.*)$", text, re.M)
|
|
deps = m.group(1).split() if m else []
|
|
targets = {ln.split(":", 1)[0].strip() for ln in text.splitlines()
|
|
if ln and not ln[0].isspace() and ":" in ln and not ln.startswith(".")}
|
|
|
|
for dep in deps:
|
|
if dep not in registered and dep not in exempt:
|
|
out.append(Finding(
|
|
"gates", "gates.toml",
|
|
f"`make all` runs {dep!r}, which is neither a registered "
|
|
f"control gate nor listed in not_control_gates — classify it, "
|
|
f"so it cannot acquire permanence without a review date"))
|
|
for target in sorted(registered):
|
|
if target not in targets:
|
|
out.append(Finding(
|
|
"gates", "gates.toml",
|
|
f"entry names target {target!r}, which the Makefile lacks"))
|
|
|
|
# CB-REV-0002 #7, generalised. The registry checked that a gate's
|
|
# target EXISTS; it never checked that the gate RUNS. `panels` was
|
|
# added to close "the harness is run by no gate" and passed lint while
|
|
# running nowhere -- the same defect, one level up.
|
|
#
|
|
# A gate may legitimately be manual. It must SAY so, and one that says
|
|
# it runs in `make all` must be there.
|
|
for g in gates:
|
|
target = g.get("target")
|
|
if not target:
|
|
continue
|
|
cadence = g.get("cadence")
|
|
if cadence not in ("all", "manual"):
|
|
out.append(Finding(
|
|
"gates", "gates.toml",
|
|
f"{target!r} declares no cadence — say `all` or `manual`. "
|
|
f"`none` was a pure loophole: a real target that runs "
|
|
f"nowhere and passes, which is the condition this rule "
|
|
f"exists to prevent (CB-REV-0003 #7)"))
|
|
elif cadence == "all" and target not in deps:
|
|
out.append(Finding(
|
|
"gates", "gates.toml",
|
|
f"{target!r} declares cadence=\"all\" and `make all` does not "
|
|
f"run it — a gate that does not run is not a gate"))
|
|
return out
|
|
|
|
|
|
def check_ornament_falsifier(root=REPO):
|
|
"""Ornamentation I5 — an open declaration names what would refute it.
|
|
|
|
**A declaration is a claim that something does not matter**, and this
|
|
project's finding register is largely a list of times that claim was
|
|
wrong. One that cannot be wrong is not a claim, it is a preference
|
|
with a table row.
|
|
|
|
Presence, never adequacy -- the same split as ADR-0018 D5.
|
|
"""
|
|
out = []
|
|
reg = os.path.join(root, "specs", "OrnamentRegister.md")
|
|
if not os.path.exists(reg):
|
|
return out
|
|
with open(reg) as fh:
|
|
text = fh.read()
|
|
m = re.search(r"<!-- ornament-register:begin -->(.*?)"
|
|
r"<!-- ornament-register:end -->", text, re.S)
|
|
if not m:
|
|
return out
|
|
for line in m.group(1).splitlines():
|
|
line = line.strip()
|
|
if not line.startswith("|") or line.startswith("|---"):
|
|
continue
|
|
cells = [c.strip() for c in line.strip("|").split("|")]
|
|
if len(cells) != 5 or cells[0] == "id":
|
|
continue
|
|
oid, state = cells[0], cells[3]
|
|
# A refuted or withdrawn row is history; the rule binds a claim
|
|
# that is still being made.
|
|
if state != "declared":
|
|
continue
|
|
body = re.search(rf"^- \*\*{re.escape(oid)} [^\n]*(?:\n(?!- \*\*O\d).*)*",
|
|
text, re.M)
|
|
if not body or "Falsifier:" not in body.group(0):
|
|
out.append(Finding(
|
|
"ornament",
|
|
"specs/OrnamentRegister.md",
|
|
f"{oid} is declared ornamentation and names no falsifier "
|
|
f"(Ornamentation.md I5). Say what would make it mechanism. "
|
|
f"NOTE: this checks presence, not adequacy.",
|
|
))
|
|
return out
|
|
|
|
|
|
def check_sensitivity_stated(root=REPO):
|
|
"""GameDesign §1.4 / ADR-0018 — a finding whose claim is arithmetic
|
|
must name the variable it depends on.
|
|
|
|
**This checks PRESENCE, NEVER ADEQUACY.** It cannot tell whether the
|
|
variable named is the right one; that is the judgement the rule exists
|
|
to force, and it stays with the author and the reviewer. A green run
|
|
here means "somebody wrote a sensitivity line", not "the claim was
|
|
verified" -- and if it is ever read as the second, the control has
|
|
become a way of not looking.
|
|
|
|
Seven claims in this project were arithmetically correct about the
|
|
wrong subject. Three of them would have been caught by varying
|
|
something; this is the half of that a machine can see.
|
|
"""
|
|
out = []
|
|
reg = os.path.join(root, "specs", "FindingRegister.md")
|
|
if not os.path.exists(reg):
|
|
return out
|
|
text = open(reg).read()
|
|
try:
|
|
block = text.split("<!-- design-register:begin -->")[1] \
|
|
.split("<!-- design-register:end -->")[0]
|
|
except IndexError:
|
|
return out
|
|
|
|
# Kinds whose claim is a quantity. `inert` and `unplayed` are about
|
|
# whether a thing happens at all, which has no denominator to get
|
|
# wrong.
|
|
ARITHMETIC = {"inconsistent", "degenerate", "underdetermined"}
|
|
exempt = set(re.findall(r"^<!-- sensitivity-exempt:\s*(\S+)\s+(.+?)\s*-->$",
|
|
text, re.M))
|
|
exempt_ids = {e[0] for e in exempt}
|
|
|
|
for line in block.splitlines():
|
|
line = line.strip()
|
|
if not line.startswith("|") or line.startswith("|---"):
|
|
continue
|
|
cells = [c.strip() for c in line.strip("|").split("|")]
|
|
if len(cells) != 7 or cells[0] == "id":
|
|
continue
|
|
fid, kind, state = cells[0], cells[1], cells[2]
|
|
# Closed rows are history; the rule binds what is still claimed.
|
|
# And a `note` is by definition a finding WITHOUT a reproduction
|
|
# (GameDesign §3.1) -- there is no measurement to be sensitive
|
|
# about, so requiring one would be asking for a sensitivity
|
|
# statement about nothing.
|
|
if state in ("withdrawn", "applied", "note") or kind not in ARITHMETIC:
|
|
continue
|
|
if fid in exempt_ids:
|
|
continue
|
|
# The prose block for this finding must say what moves it.
|
|
body = ""
|
|
m = re.search(rf"^- \*\*{re.escape(fid)} [^\n]*(?:\n(?!- \*\*F?U?\d).*)*",
|
|
text, re.M)
|
|
if m:
|
|
body = m.group(0)
|
|
if not re.search(r"varie[sd]|varying|sensitivit|moves with|held fixed|"
|
|
r"one number|second policy", body, re.I):
|
|
out.append(Finding(
|
|
"sensitivity", "specs/FindingRegister.md",
|
|
f"{fid} ({kind}) states a quantity and names no variable it "
|
|
f"depends on (GameDesign §1.4). Say what would move it, or "
|
|
f"add `<!-- sensitivity-exempt: {fid} <reason> -->`. "
|
|
f"NOTE: this checks presence, not adequacy."))
|
|
return out
|
|
|
|
|
|
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,
|
|
check_gate_registry,
|
|
check_sensitivity_stated,
|
|
check_ornament_falsifier,
|
|
)
|
|
|
|
|
|
def self_test():
|
|
"""Each check must DETECT its failure, not merely run."""
|
|
import shutil
|
|
import tempfile
|
|
|
|
results = []
|
|
|
|
def check(name, ok, detail=""):
|
|
results.append((name, ok, detail))
|
|
|
|
tmp = tempfile.mkdtemp()
|
|
try:
|
|
for d in LOOP_DIRS + ("tools", "history"):
|
|
os.makedirs(os.path.join(tmp, d), exist_ok=True)
|
|
|
|
# loadability: 401 lines must trip, 400 must not.
|
|
with open(os.path.join(tmp, "specs", "Big.md"), "w") as fh:
|
|
fh.write("x\n" * (LOADABILITY_LIMIT + 1))
|
|
with open(os.path.join(tmp, "specs", "Ok.md"), "w") as fh:
|
|
fh.write("x\n" * LOADABILITY_LIMIT)
|
|
f = check_loadability(tmp)
|
|
check("loadability detects overlong artifact",
|
|
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)
|
|
# GameDesign §1.4 / ADR-0018. Four controls, because a check that
|
|
# cannot say NO is decoration and one that cannot say YES fires on
|
|
# everything.
|
|
os.makedirs(os.path.join(tmp, "specs"), exist_ok=True)
|
|
|
|
def reg(kind, state, prose):
|
|
body = ("<!-- design-register:begin -->\n\n"
|
|
"| id | kind | state | reproduction | role | raised | owner |\n"
|
|
"|---|---|---|---|---|---|---|\n"
|
|
f"| F99 | {kind} | {state} | x.rs | counterexample | 2026-01-01 | us |\n"
|
|
"\n<!-- design-register:end -->\n\n"
|
|
f"- **F99 — a claim.** {prose}\n")
|
|
with open(os.path.join(tmp, "specs", "FindingRegister.md"), "w") as fh:
|
|
fh.write(body)
|
|
|
|
reg("degenerate", "raised", "It is 42.")
|
|
check("sensitivity: an arithmetic claim with no variable is caught",
|
|
len(check_sensitivity_stated(tmp)) == 1)
|
|
reg("degenerate", "raised", "It is 42, and it varies with seat count.")
|
|
check("sensitivity: naming the variable clears it",
|
|
not check_sensitivity_stated(tmp),
|
|
"without this it would fire on everything")
|
|
reg("inert", "raised", "It is 42.")
|
|
check("sensitivity: a non-arithmetic kind is not asked",
|
|
not check_sensitivity_stated(tmp),
|
|
"`inert` is about whether a thing happens, not how much")
|
|
reg("degenerate", "note", "It is 42.")
|
|
check("sensitivity: a note has no measurement to be sensitive about",
|
|
not check_sensitivity_stated(tmp), "GameDesign §3.1")
|
|
|
|
# Ornamentation I5. Same shape: it must say NO and it must say YES.
|
|
def orn(state, prose):
|
|
body = ("<!-- ornament-register:begin -->\n\n"
|
|
"| id | ornaments | grounded | state | raised |\n"
|
|
"|---|---|---|---|---|\n"
|
|
f"| O9 | a thing | provisional | {state} | 2026-01-01 |\n"
|
|
"\n<!-- ornament-register:end -->\n\n"
|
|
f"- **O9 — a thing.** {prose}\n")
|
|
with open(os.path.join(tmp, "specs", "OrnamentRegister.md"), "w") as fh:
|
|
fh.write(body)
|
|
|
|
orn("declared", "It does not matter.")
|
|
check("ornament: a declaration with no falsifier is caught",
|
|
len(check_ornament_falsifier(tmp)) == 1,
|
|
"a claim that cannot be wrong is not a claim")
|
|
orn("declared", "It does not matter. **Falsifier:** a rule naming it.")
|
|
check("ornament: naming a falsifier clears it",
|
|
not check_ornament_falsifier(tmp),
|
|
"without this it would fire on everything")
|
|
orn("refuted", "It does not matter.")
|
|
check("ornament: a refuted row is history, not a live claim",
|
|
not check_ornament_falsifier(tmp))
|
|
|
|
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:
|
|
fh.write("all: coverage newthing\ncoverage:\n\techo\n")
|
|
with open(os.path.join(tmp, "gates.toml"), "w") as fh:
|
|
fh.write('not_control_gates = []\n\n[[gate]]\nid = "G"\n'
|
|
'name = "n"\ntarget = "coverage"\ncadence = "all"\n'
|
|
'checks = "c"\n'
|
|
'added = "2026-01-01"\nreview_by = "2026-02-01"\n'
|
|
'retire_if = "r"\n')
|
|
f = check_gate_registry(tmp)
|
|
check("gate registry detects an unclassified all: dependency",
|
|
len(f) == 1 and "newthing" in f[0].detail, f"{len(f)} finding(s)")
|
|
with open(os.path.join(tmp, "gates.toml"), "w") as fh:
|
|
fh.write('not_control_gates = ["newthing", "coverage"]\n\n[[gate]]\nid = "G"\n'
|
|
'name = "n"\ntarget = "ghost"\ncadence = "manual"\n'
|
|
'checks = "c"\n'
|
|
'added = "2026-01-01"\nreview_by = "2026-02-01"\n'
|
|
'retire_if = "r"\n')
|
|
f = check_gate_registry(tmp)
|
|
check("gate registry detects an entry naming a missing target",
|
|
len(f) == 1 and "ghost" in f[0].detail, f"{len(f)} finding(s)")
|
|
|
|
# CB-REV-0002 #7 generalised: a gate that does not run is not a
|
|
# gate. Both directions, because a rule that cannot say NO is
|
|
# decoration and one that cannot say YES fires on everything.
|
|
with open(os.path.join(tmp, "Makefile"), "w") as fh:
|
|
fh.write("all: coverage\ncoverage:\n\techo\npanels:\n\techo\n")
|
|
with open(os.path.join(tmp, "gates.toml"), "w") as fh:
|
|
fh.write('not_control_gates = []\n\n[[gate]]\nid = "G"\n'
|
|
'name = "n"\ntarget = "coverage"\ncadence = "all"\n'
|
|
'checks = "c"\n\n[[gate]]\nid = "P"\nname = "p"\n'
|
|
'target = "panels"\ncadence = "all"\nchecks = "c"\n')
|
|
f = check_gate_registry(tmp)
|
|
check("gates: a cadence=all gate absent from `make all` is caught",
|
|
len(f) == 1 and "does not run it" in f[0].detail,
|
|
"the panels gate passed lint while running nowhere")
|
|
with open(os.path.join(tmp, "gates.toml"), "w") as fh:
|
|
fh.write('not_control_gates = []\n\n[[gate]]\nid = "G"\n'
|
|
'name = "n"\ntarget = "coverage"\nchecks = "c"\n')
|
|
check("gates: a gate with no declared cadence is caught",
|
|
any("declares no cadence" in x.detail for x in check_gate_registry(tmp)))
|
|
with open(os.path.join(tmp, "gates.toml"), "w") as fh:
|
|
fh.write('not_control_gates = []\n\n[[gate]]\nid = "G"\n'
|
|
'name = "n"\ntarget = "panels"\ncadence = "manual"\n'
|
|
'checks = "c"\n\n[[gate]]\nid = "C"\nname = "c"\n'
|
|
'target = "coverage"\ncadence = "all"\nchecks = "c"\n')
|
|
check("gates: a gate may be manual, and saying so clears it",
|
|
not check_gate_registry(tmp),
|
|
"without this every manual gate would fire")
|
|
os.unlink(os.path.join(tmp, "gates.toml"))
|
|
os.unlink(os.path.join(tmp, "Makefile"))
|
|
|
|
# evidence: a table verdict trips; the word in prose does not.
|
|
with open(os.path.join(tmp, "evidence", "E.md"), "w") as fh:
|
|
fh.write("| AC-1 | x | unmeasured |\n"
|
|
"the word unmeasured appearing in prose is fine\n")
|
|
f = check_evidence_no_unmeasured(tmp)
|
|
check("evidence-unmeasured detects a table verdict, not prose",
|
|
len(f) == 1, f"{len(f)} finding(s), expected exactly 1")
|
|
|
|
# tier: a missing declaration trips. The chaos half is retired
|
|
# (ADR-0021), so a note without a roll must now PASS — the
|
|
# control that used to require one would fail every new note.
|
|
os.makedirs(os.path.join(tmp, "research"), exist_ok=True)
|
|
with open(os.path.join(tmp, "research", "CB-RES-9999-x.md"), "w") as fh:
|
|
fh.write("---\nid: CB-RES-9999\n---\n\nno tier here\n")
|
|
f = check_survey_tier_and_chaos(tmp)
|
|
check("tier: a survey with no tier declaration is caught",
|
|
len(f) == 1 and f[0].rule == "tier-declared", f"{[x.rule for x in f]}")
|
|
with open(os.path.join(tmp, "research", "CB-RES-9999-x.md"), "w") as fh:
|
|
fh.write("---\nid: CB-RES-9999\ntier: M\n---\n\nno chaos line\n")
|
|
check("tier: a survey with no chaos roll now PASSES",
|
|
not check_survey_tier_and_chaos(tmp),
|
|
"ADR-0021 retired the roll; requiring it would fail every new note")
|
|
|
|
# self-test: a tool without the flag trips.
|
|
with open(os.path.join(tmp, "tools", "silent.py"), "w") as fh:
|
|
fh.write("print(42)\n")
|
|
f = check_reporting_tools_self_test(tmp)
|
|
check("self-test detects a tool lacking --self-test",
|
|
len(f) == 1 and "silent.py" in f[0].path, f"{len(f)} finding(s)")
|
|
|
|
# review trail: approved tier-L survey with no history trips.
|
|
with open(os.path.join(tmp, "research", "C.md"), "w") as fh:
|
|
fh.write("tier: L (structural L, chaos 3)\nstatus: approved\n")
|
|
f = check_review_trail(tmp)
|
|
check("review-trail detects a missing challenge/response",
|
|
len(f) == 2, f"{len(f)} finding(s), expected 2")
|
|
finally:
|
|
shutil.rmtree(tmp, ignore_errors=True)
|
|
|
|
print("loop-lint self-test (positive control)")
|
|
ok = True
|
|
for name, passed, detail in results:
|
|
print(f" [{'ok ' if passed else 'FAIL'}] {name}"
|
|
+ (f" — {detail}" if detail else ""))
|
|
ok &= passed
|
|
return 0 if ok else 1
|
|
|
|
|
|
def main():
|
|
if "--self-test" in sys.argv:
|
|
return self_test()
|
|
|
|
findings = []
|
|
for c in CHECKS:
|
|
findings.extend(c())
|
|
|
|
print("loop-lint — executable InnerLoop rules")
|
|
if not findings:
|
|
print(" no findings")
|
|
return 0
|
|
by_rule = {}
|
|
for f in findings:
|
|
by_rule.setdefault(f.rule, []).append(f)
|
|
for rule, fs in sorted(by_rule.items()):
|
|
print(f"\n{rule} ({len(fs)}):")
|
|
for f in fs:
|
|
print(str(f))
|
|
print(f"\n{len(findings)} finding(s)")
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|