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:
parent
ee1ee485b2
commit
fed422a3a3
10 changed files with 827 additions and 242 deletions
|
|
@ -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
266
tools/loop-lint.py
Normal 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())
|
||||
|
|
@ -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())
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue