[FIX] tarbuckle-body: the selftest wrote the live occurrence log its comment said it never touched (PENDING-184)

The D block opened "Run against a temp dir, never live state", but _log_draw
hardcoded ~/.claude/state/tarbuckle-draws.jsonl and the selftest rebound only
LAST_TICK and SLOT, so D5's fire_tick appended a real-time tick record to the
live log on every run, and every mutant copy did the same. Two strays are
confirmed (2026-09-09T21:48:39, 2026-09-10T18:52:40); nothing in a tick record
separates live from test, so that is a floor.

The path is now a module global DRAWS, rebound and restored with the others.
Two new controls, both behavioural rather than source-string:
  D9  the live log is untouched (existence, size, SHA-256, before vs after)
  D10 the D block's tick landed in the redirected log. An absence-only check
      passes when the write silently vanishes; this is the arm that fails then.

Verified under a throwaway HOME: fixed 34/34 with the fake live log unchanged;
the pre-fix file 32/32 green while writing it; M1 (rebinding deleted) fails
exactly D9+D10; M2 (write vanishes) fails exactly D10 with D9 passing; main()
with a due tick still records its tick. Real HOME: 34/34, live log SHA-256
unchanged, measured outside the selftest. Five-selftest census: none writes.

Tag claim: this addresses the instance. The class census covered the five
Tarbuckle selftests only; fleet-wide the class is unassessed, and pursuing it
would be [HARDENING].

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJM5fwqp456LDGzqiZXsgu
This commit is contained in:
David F Glidden
2026-09-11 18:43:41 +02:00
co-authored by Claude Opus 5
parent 2af4335884
commit 37a2c86bdb
+39 -7
View File
@@ -55,6 +55,7 @@ MUMBLE_INTERVAL_MIN = 20 # §8, determined 2026-08-25
LOG = os.path.expanduser("~/.claude/state/tarbuckle-invocations.jsonl")
LAST_TICK = os.path.expanduser("~/.claude/state/tarbuckle-last-tick")
SLOT = os.path.expanduser("~/.claude/state/tarbuckle-slot.json")
DRAWS = os.path.expanduser("~/.claude/state/tarbuckle-draws.jsonl")
MUMBLE = os.path.expanduser("~/dotfiles/scripts/tarbuckle-mumble.py")
# 73% silent · 20% brief aside · 7% notable — v1 §8's table, hardcoded, unchanged.
@@ -93,9 +94,13 @@ def log_invocation(payload: dict) -> None:
def _log_draw(kind: str) -> None:
"""Occurrence only. Local, so the body needs nothing importable to tick."""
"""Occurrence only. Local, so the body needs nothing importable to tick.
Writes DRAWS, a module global, so the selftest can redirect it. Hardcoded, it
could not — and every selftest run wrote a real tick into the live log (PENDING-184).
"""
try:
d = os.path.expanduser("~/.claude/state/tarbuckle-draws.jsonl")
d = DRAWS
os.makedirs(os.path.dirname(d), exist_ok=True)
with open(d, "a") as fh:
fh.write(json.dumps({"t": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
@@ -277,11 +282,23 @@ def selftest() -> int:
ck("C6 marks are distinct", len(set(MARKS)) == len(MARKS))
# --- D: the tick, the draw, the slot. Run against a temp dir, never live state.
import tempfile, collections
global LAST_TICK, SLOT
_lt, _sl = LAST_TICK, SLOT
# ⚠ That sentence was false until 2026-09-11: DRAWS was hardcoded, so D5's tick
# landed in the live occurrence log on every run (PENDING-184). D9/D10 now check
# it, in both directions.
import tempfile, collections, hashlib
global LAST_TICK, SLOT, DRAWS
_lt, _sl, _dr = LAST_TICK, SLOT, DRAWS
def _fingerprint(p):
try:
b = open(p, "rb").read()
return (True, len(b), hashlib.sha256(b).hexdigest())
except OSError:
return (False, 0, "")
live_before = _fingerprint(_dr)
td = tempfile.mkdtemp()
LAST_TICK, SLOT = os.path.join(td, "tick"), os.path.join(td, "slot")
_redir = os.path.join(td, "draws.jsonl")
LAST_TICK, SLOT, DRAWS = os.path.join(td, "tick"), os.path.join(td, "slot"), _redir
try:
# D1 — the draw is keyed to nothing. Structural, like C3.
ck("D1 draw takes no arguments", draw.__code__.co_argcount == 0)
@@ -321,8 +338,23 @@ def selftest() -> int:
fresh_utterance(now + DISPLAY_WINDOW_S + 1) is None)
ck("D6n absent slot is silence",
(os.remove(SLOT), fresh_utterance(now))[1] is None)
# D9/D10 — the D block touches no live state, AND its writes land where they
# were sent. Both arms: an absence-only check passes when the write silently
# vanishes, and D10 is the arm that fails then (PENDING-184, mutation M2).
# ⚠ D9 can fail falsely if a real tick fires from another pane inside this
# sub-second block. That is the loud direction; it is not engineered away.
ck("D9 selftest leaves the live occurrence log untouched",
_fingerprint(_dr) == live_before)
try:
recs = [json.loads(l) for l in open(_redir) if l.strip()]
except OSError:
recs = []
ck("D10 the D block's tick landed in the redirected log",
len(recs) == 1 and recs[0].get("surface") == "tick"
and recs[0].get("outcome") in {k for k, _ in DRAW})
finally:
LAST_TICK, SLOT = _lt, _sl
LAST_TICK, SLOT, DRAWS = _lt, _sl, _dr
# D8 — §9's switches. mute keeps the body; off removes it.
import tempfile