CB-WP-0030: a control for the wrong-subject family
Some checks failed
ci / check (push) Failing after 4s
Some checks failed
ci / check (push) Failing after 4s
Seven claims in this project have been wrong the same way — the arithmetic right, the subject wrong. Four reached a document or another repo. ADR-0018 states the rule: a measured claim names the variable it depends on and shows the measurement moving with it. GameDesign §1.4 adds it as a fourth admissibility clause; the existing three are untouched. Honest yield is 3 clear + 1 partial of 7. It misses a count never made and a claim that is not a number, and D3 names those so nobody stops looking. The tempting fix — closing facts-check's 62 untagged literals — would have caught 1 of 7 and is deferred as separate work rather than bundled. loop-lint gains check_sensitivity_stated over arithmetic register rows, mutation-proven red on F17 and green when restored, with four self-test controls. It checks presence, never adequacy, and says so in its output. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
ffd2279e16
commit
d7279483f6
6 changed files with 503 additions and 6 deletions
|
|
@ -306,6 +306,74 @@ def check_gate_registry(root=REPO):
|
|||
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,
|
||||
|
|
@ -315,6 +383,7 @@ CHECKS = (
|
|||
check_review_trail,
|
||||
check_reporting_tools_self_test,
|
||||
check_gate_registry,
|
||||
check_sensitivity_stated,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -374,6 +443,36 @@ def self_test():
|
|||
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")
|
||||
|
||||
wp("ready", ["done", "todo"])
|
||||
f = check_workplan_lifecycle(tmp)
|
||||
check("lifecycle detects `ready` after work has started",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue