diff --git a/scripts/tarbuckle-mumble.py b/scripts/tarbuckle-mumble.py index 5b64ff5..e75f29a 100755 --- a/scripts/tarbuckle-mumble.py +++ b/scripts/tarbuckle-mumble.py @@ -191,6 +191,26 @@ def source_lacks(path: str, *parts: str) -> bool: return "".join(parts) not in open(path, encoding="utf-8").read() +def source_has(path: str, *parts: str) -> bool: + """True if the joined needle DOES appear in `path`. The positive counterpart. + + ⚠ THIS IS THE POLARITY `source_lacks()` DOES NOT COVER, AND IT IS THE ONE THAT + HIDES. Both bugs come from writing a needle as a literal, which plants it in the + file being searched. In the NEGATIVE form that makes the control always FAIL — + loudly, so it gets fixed. In the POSITIVE form it makes the control always PASS — + silently, forever. The louder failure got the mechanism first; this is the quieter + one. Assembling from parts is the whole protection, exactly as above. + + ⚠ WHAT IT STILL CANNOT SEE: whether the code it finds ever RUNS. A source control + is a claim about text, never about behaviour — `tarbuckle-seam.py:165` asserted the + rejection log keeps evidence and did not move when condition G made every rejection + write path inert (15/15 before, 15/15 after). Aim it at the file that holds the + code, and pair it with a behavioural control where activation is the question. + PENDING-180. + """ + return "".join(parts) in open(path, encoding="utf-8").read() + + def soul_register() -> str | None: """The soul, verbatim, from the filed artifact. None if unreadable.""" try: @@ -466,6 +486,13 @@ def selftest() -> int: ck("A4n the predicate can fail", any(("PENDING" in q) for q in paths + ("/x/PENDING.md",))) + # A10 — the positive-form source predicate, with the arm the negative form has had + # since it was built. A control that cannot be shown to fail is not evidence. + ck("A10 source_has finds a needle that is really there", + source_has(__file__, "def source_", "lacks(")) + ck("A10n the predicate can fail", + not source_has(__file__, "def source_", "nowhere(")) + for name in checks: print(f" {'FAIL' if name in failed else 'ok '} {name}") print(f"{len(checks) - len(failed)}/{len(checks)} controls passed") diff --git a/scripts/tarbuckle-seam.py b/scripts/tarbuckle-seam.py index f6c566e..fb1969c 100755 --- a/scripts/tarbuckle-seam.py +++ b/scripts/tarbuckle-seam.py @@ -41,7 +41,7 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) # reimplementing is the "one canonical source" rule applied to a constraint: a second # copy of `acceptable()` is a second, quietly divergent standard. from tarbuckle_mumble_shim import (acceptable, soul_register, session_material, # noqa: E402 - source_lacks, muted, log_event, # noqa: E402 + source_lacks, source_has, muted, log_event, # noqa: E402 echoes_soul, log_rejection, REJECTS) LAST_TICK = os.path.expanduser("~/.claude/state/tarbuckle-last-tick") @@ -161,8 +161,24 @@ def selftest() -> int: ck("S3 generation is bounded", SEAM_TIMEOUT_S <= 20) ck("S3 timeout path returns silence, not a line", "log_silence(f\"seam generation exceeded" in src and "TimeoutExpired" in src) - ck("S3 rejection log keeps the evidence, not just the verdict", - "line[:200]" in src and "log_silence(why, line)" in src) + # ⚠ AIMED AT THE WRITER'S FILE, NOT THIS ONE. The 200-character truncation lives + # in the canonical writer (tarbuckle-mumble.py); the old form searched THIS source + # for it, found only the copy it had planted in itself, and could only ever pass. + # ⚠⚠ AND THE NEEDLE CAN BE PLANTED BY PROSE. Naming that truncation literally in + # this comment re-created the bug in the sentence explaining it — caught by mutation + # test, not by the selftest. source_has/source_lacks assemble the ASSERTION's needle + # from parts; nothing stops a comment elsewhere in the file planting a contiguous + # copy. Do not write the literal here. PENDING-180. + # ⚠ AND IT IS A CLAIM ABOUT TEXT, NEVER ABOUT BEHAVIOUR: it did not move when + # condition G made every rejection write path inert. Activation is covered + # behaviourally by the generator's own G / A8 controls, not here. + import tarbuckle_mumble_shim as _shim + ck("S3 the canonical writer keeps the evidence, not just the verdict", + source_has(_shim.__file__, "line[:", "200]")) + ck("S3 this seam routes rejections through that writer", + source_has(__file__, "log_silence(", "why, line)")) + ck("S3nn the predicate can fail", + not source_has(__file__, "log_silence(", "why, absent)")) ck("S3n no cached or fallback line exists", source_lacks(__file__, "FALLBACK", "_LINE"))