T01: audit every InnerLoop rule, and make the checkable ones executable

41 rules classified executable / checkable / decorative, each tagged with
the failure class it catches. Counts: 11 executable, 22 checkable, 4
decorative (one of them dead policy).
Audit: history/260731-inner-loop-rule-audit.md

New tools/loop-lint.py makes 7 rules executable (tier declared, chaos
roll recorded, tier-L review trail, unmeasured-in-evidence, whole-file
loadability, reporting tools expose --self-test). It found three real
violations on its first run, none previously visible:

  - specs/ArchitectureBlueprint.md was 543 lines against a ~400 limit
    the loop has stated since v0.2 and never measured. Split at its own
    section boundaries into Blueprint (1-8) + Runtime (9-15).
  - tools/dep-weight.py and tools/rule-coverage.py had positive-control
    logic and no --self-test, so nothing verified the control worked.

Adding rule-coverage's self-test exposed a latent instance of the exact
class this workplan is about: if the spec regex stopped matching, rules
was empty, missing was empty, and the tool exited 0 reporting "0/0" --
a silent pass, in the tool that reports our headline AM-1 number. Both
tools now assert they found something before reporting.

Two demotions applied in the spec rather than left implicit: "structured
over prose" is marked guidance (nothing can check it), and the 8k/10k
token budget is struck through and marked DEAD POLICY pointing at T05.

The audit's uncomfortable finding: rule 13 (re-derive inherited numbers)
has no mechanical form, is deliberately left decorative, and caught the
LARGEST error in CB-WP-0002. That is a counter-example to this
workplan's own hypothesis. "A rule that cannot be executed is not a
rule" is wrong as stated; the defensible version is that such a rule
cannot be relied on to fire, so it must not be the only defence for a
class that matters.

Class coverage: harness-does-nothing has five executable rules;
trusted-arithmetic has ZERO and produced the largest single error.

make loop-lint and make self-tests wired into `make all` and CI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-07-31 09:16:00 +02:00
parent ee1ee485b2
commit fed422a3a3
10 changed files with 827 additions and 242 deletions

View file

@ -16,7 +16,7 @@ that cannot be found is reported and the run exits non-zero rather than
silently under-reporting the total under-reporting is the exact
direction this metric could be gamed.
Usage: python3 tools/dep-weight.py [--json]
Usage: python3 tools/dep-weight.py [--json] [--self-test]
"""
import glob
@ -88,7 +88,50 @@ def source_lines(name, version):
return 0
def self_test():
"""Each assertion pins a failure this tool must detect.
The controls that matter here are: an unlocatable crate must not be
silently counted as zero lines (that under-reports, the direction this
metric could be gamed), and a target breach must fail rather than
merely print.
"""
results = []
def check(name, ok, detail=""):
results.append((name, ok, detail))
# A crate that does not exist must measure zero, so the caller's
# `lines == 0` guard fires rather than silently shrinking the total.
check("unlocatable crate measures zero (so the guard fires)",
source_lines("definitely-not-a-real-crate-xyz", "9.9.9") == 0)
# A crate we do depend on must measure non-zero, or the guard above
# would fire on everything and the tool would never report at all.
real = source_lines("serde", "1")
check("a real vendored crate measures non-zero", real > 0,
f"{real:,} lines")
# Targets must be present and numeric — a missing target would make
# the breach check vacuous.
check("targets defined for every configuration",
set(TARGETS) == set(CONFIGS) and all(
isinstance(v, int) and v > 0 for v in TARGETS.values()),
f"{TARGETS}")
print("dep-weight 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()
report = {}
missing = []
for label, args in CONFIGS.items():

266
tools/loop-lint.py Normal file
View file

@ -0,0 +1,266 @@
#!/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
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
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_survey_tier_and_chaos(root=REPO):
"""§Loop tiers — tier declared, and the chaos roll recorded every time."""
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"))
elif "chaos" not in text.lower():
out.append(
Finding("chaos-recorded", rel,
"tier declared without the chaos roll; the rule requires "
"recording it even when it changes nothing")
)
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
CHECKS = (
check_loadability,
check_evidence_no_unmeasured,
check_survey_tier_and_chaos,
check_review_trail,
check_reporting_tools_self_test,
)
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)")
# 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/chaos: missing tier trips; tier without chaos trips.
with open(os.path.join(tmp, "research", "A.md"), "w") as fh:
fh.write("# survey\nno tier here\n")
with open(os.path.join(tmp, "research", "B.md"), "w") as fh:
fh.write("tier: L (structural L)\n")
f = check_survey_tier_and_chaos(tmp)
rules = sorted(x.rule for x in f)
check("tier/chaos detects both omissions",
rules == ["chaos-recorded", "tier-declared"], f"{rules}")
# 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())

View file

@ -5,30 +5,107 @@ Compares the rule IDs declared in specs/GroundRules.md against the
`covers:` lists in scenarios/ground/*.yaml. Exits non-zero when a
scenario claims a rule the spec does not define, so coverage can never
be inflated by a typo'd or invented rule ID.
Stated limit (InnerLoop implementation rule 4): this gate counts tags. It
proves no rule is unclaimed and no claimed rule is invented. It does NOT
prove a scenario exercises the rule it names.
Positive control (InnerLoop v1.1 §Step 5): the run asserts it actually
found rules and scenarios. Before this was added, a broken spec regex
yielded rules=[] and missing=[] and the tool exited 0 reporting "0/0"
the harness-does-nothing class, in the tool that reports our headline
coverage number.
Usage:
python3 tools/rule-coverage.py
python3 tools/rule-coverage.py --self-test
"""
import glob
import re
import sys
spec = open("specs/GroundRules.md").read()
rules = sorted(set(re.findall(r"\*\*(GR-[A-Z]+\d+)", spec)))
RULE_RE = r"\*\*(GR-[A-Z]+\d+)"
COVERS_RE = r"covers: \[(.*?)\]"
covered = set()
for path in sorted(glob.glob("scenarios/ground/*.yaml")):
match = re.search(r"covers: \[(.*?)\]", open(path).read(), re.S)
if match:
covered |= {c.strip() for c in match.group(1).split(",") if c.strip()}
known = set(rules)
hit = sorted(known & covered)
missing = [r for r in rules if r not in covered]
invented = sorted(covered - known)
def parse_rules(spec_text):
return sorted(set(re.findall(RULE_RE, spec_text)))
pct = 100 * len(hit) // len(rules) if rules else 0
print(f"AM-1 rule coverage: {len(hit)}/{len(rules)} ({pct}%)")
if missing:
print(" uncovered:", " ".join(missing))
if invented:
print(" ERROR — claimed but not defined in the spec:", " ".join(invented))
sys.exit(1)
sys.exit(0 if not missing else 2)
def parse_covers(text):
match = re.search(COVERS_RE, text, re.S)
if not match:
return set()
return {c.strip() for c in match.group(1).split(",") if c.strip()}
def self_test():
"""Each assertion pins a failure this tool must detect."""
results = []
def check(name, ok, detail=""):
results.append((name, ok, detail))
# The defect that motivated this control: a spec that parses to zero
# rules must not be reportable as coverage.
check("zero rules detected as a failure", parse_rules("no rules here") == [],
"empty spec yields no rules; main() now aborts on this")
# The matcher must actually match the real format.
check("rule matcher works on real spec format",
parse_rules("**GR-R06** something\n**GR-A12** other")
== ["GR-A12", "GR-R06"])
# covers: parsing, including the empty case.
check("covers matcher works", parse_covers("covers: [GR-R06, GR-A12]")
== {"GR-R06", "GR-A12"})
check("missing covers yields empty set", parse_covers("no covers key") == set())
print("rule-coverage 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()
rules = parse_rules(open("specs/GroundRules.md").read())
paths = sorted(glob.glob("scenarios/ground/*.yaml"))
# Positive control: refuse to report a percentage over nothing.
if not rules:
print("ERROR — no GR-rules parsed from specs/GroundRules.md; "
"refusing to report coverage", file=sys.stderr)
return 1
if not paths:
print("ERROR — no scenarios found in scenarios/ground/; "
"refusing to report coverage", file=sys.stderr)
return 1
covered = set()
for path in paths:
covered |= parse_covers(open(path).read())
known = set(rules)
hit = sorted(known & covered)
missing = [r for r in rules if r not in covered]
invented = sorted(covered - known)
pct = 100 * len(hit) // len(rules)
print(f"AM-1 rule coverage: {len(hit)}/{len(rules)} ({pct}%) "
f"over {len(paths)} scenarios")
print(" NOTE: counts tags; does not prove a scenario exercises what it names")
if missing:
print(" uncovered:", " ".join(missing))
if invented:
print(" ERROR — claimed but not defined in the spec:", " ".join(invented),
file=sys.stderr)
return 1
return 0 if not missing else 2
if __name__ == "__main__":
sys.exit(main())