[FIX] Resolve the STATE-CLAIM; [HARDENING] file PENDING-163; add the preservation instrument
STATE-CLAIM: memory-index-claims-reviewed-127-unplaced -> resolved, pointing at
7a92460. Verified end to end by re-running governance-drift-check.py rather than
trusting the write: "1 of 3 open are NOW FALSE" -> "2 tracked, none falsified /
plus 1 RESOLVED", no dangling-pointer defect, so the resolution parses AND its
pointer resolves. The `resolved:` form was derived from the parser, not from
memory of the schema, which is also why the correction commit had to come first.
What that discharge is evidence for is written into the item so it cannot be
quoted as more: one marked claim, marked by its own author, corrected in the
immediately following session. Expressibility, not adoption. The 57 unmarked
claims are untouched.
PENDING-163 [HARDENING]: the global pre-commit hook refuses files over 5MB and
prints "Consider using Git LFS", but measures `wc -c < "$file"` — working-tree
size — so an LFS-tracked file stages as a ~130-byte pointer and is still refused.
Tried it; same refusal, same file. The hook is NOT modified: it is global and
governed by REVIEWED-100/105.
preserve-transcripts.py: PENDING-147 option (i). The archive itself is NOT in this
repo — 115MB of transcripts is not dotfiles material, which is what the hook was
right about even though its reasoning measures the wrong thing. It lives at
~/_Dev/claude-transcript-archive, outside the harness's pruned path, which is what
actually stops the clock. 43 transcripts, read-back PASS.
No guard was bypassed: no --no-verify, no per-repo core.hooksPath override, and no
empty .git left behind that would make the archive look tracked when it is not.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NvZAKSf9aqratbqHbU9LK5
This commit is contained in:
co-authored by
Claude Opus 5
parent
7a92460a60
commit
57f83740a5
Executable
+183
@@ -0,0 +1,183 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Preserve Claude Code session transcripts out of the harness's pruned directory.
|
||||
|
||||
WHY THIS EXISTS
|
||||
~/.claude/projects/-Users-davidglidden is pruned on a ~30-day retention policy
|
||||
(cleanupPeriodDays default 30). PENDING-147 established that the ladder trial's
|
||||
evidence therefore sits on a deletion clock nobody set: the pre-registered
|
||||
20-session falsifier in REVIEWED-95 Q6 cannot be graded if its earliest cohort
|
||||
has been deleted before the 20 sessions have run.
|
||||
|
||||
This copies transcripts to a git-tracked location so the population stops
|
||||
shrinking. It is PENDING-147 option (i), and ONLY option (i): copying files
|
||||
"changes no instrument, no doctrine and no ladder" and needs no ruling.
|
||||
|
||||
WHAT THIS DELIBERATELY DOES NOT DO
|
||||
It does NOT re-express governance-drift-check.py's `transcripts N` trigger over
|
||||
the preserved set. That would change an instrument a placed ruling leans on,
|
||||
inside a trial under the REVIEWED-123 freeze, and is not covered by the
|
||||
no-ruling justification above. The preserved set is inert with respect to the
|
||||
trigger until someone with the authority rules on it.
|
||||
|
||||
IDEMPOTENT
|
||||
Safe to re-run. New transcripts are added; transcripts whose source has grown
|
||||
(they are append-only) are refreshed; already-preserved files are re-verified.
|
||||
Preserved files are never deleted here, even when the source is pruned — that
|
||||
is the entire point.
|
||||
|
||||
INTEGRITY
|
||||
Preservation is proved by READING BACK, not by copy success. Every preserved
|
||||
file is re-hashed from disk after the copy and compared to the manifest. A
|
||||
write that "succeeded" into an unreadable file is the failure mode this guards
|
||||
(steward rule 2026-05-04, learned on MemPalace).
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
HOME = Path.home()
|
||||
SRC = HOME / ".claude/projects/-Users-davidglidden"
|
||||
DST = HOME / "_Dev/claude-transcript-archive"
|
||||
MANIFEST = DST / "manifest.json"
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
h = hashlib.sha256()
|
||||
with open(path, "rb") as f:
|
||||
for block in iter(lambda: f.read(1 << 20), b""):
|
||||
h.update(block)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def iso(ts: float) -> str:
|
||||
return datetime.fromtimestamp(ts, timezone.utc).isoformat(timespec="seconds")
|
||||
|
||||
|
||||
def now_iso() -> str:
|
||||
return datetime.now(timezone.utc).isoformat(timespec="seconds")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- controls
|
||||
def run_controls() -> bool:
|
||||
"""Same-run positive AND negative controls.
|
||||
|
||||
An absence is not evidence until the instrument is shown capable of detecting
|
||||
presence (epistemic standard, jurist Q2 ruling). These run on EVERY invocation
|
||||
rather than in a separate suite, so a failure cannot be re-run until green.
|
||||
"""
|
||||
ok = True
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
td = Path(td)
|
||||
|
||||
# positive: a known byte string hashes to its known digest
|
||||
p = td / "pos.bin"
|
||||
p.write_bytes(b"capablemind")
|
||||
expected = hashlib.sha256(b"capablemind").hexdigest()
|
||||
if sha256(p) != expected:
|
||||
print("CONTROL FAIL: hashing does not reproduce a known digest", file=sys.stderr)
|
||||
ok = False
|
||||
|
||||
# negative: a corrupted copy MUST be detected as differing
|
||||
a, b = td / "a.bin", td / "b.bin"
|
||||
a.write_bytes(b"x" * 4096)
|
||||
shutil.copy2(a, b)
|
||||
if sha256(a) != sha256(b):
|
||||
print("CONTROL FAIL: identical copy reported as differing", file=sys.stderr)
|
||||
ok = False
|
||||
with open(b, "r+b") as f: # flip one byte
|
||||
f.seek(2048)
|
||||
f.write(b"y")
|
||||
if sha256(a) == sha256(b):
|
||||
print("CONTROL FAIL: one-byte corruption NOT detected", file=sys.stderr)
|
||||
ok = False
|
||||
|
||||
# negative: mtime preservation must actually preserve
|
||||
old = 1_600_000_000
|
||||
os.utime(a, (old, old))
|
||||
c = td / "c.bin"
|
||||
shutil.copy2(a, c)
|
||||
if abs(c.stat().st_mtime - old) > 1:
|
||||
print("CONTROL FAIL: copy2 did not preserve mtime", file=sys.stderr)
|
||||
ok = False
|
||||
return ok
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not run_controls():
|
||||
print("INSTRUMENT NOT VERIFIED — controls failed; result is unestablished.",
|
||||
file=sys.stderr)
|
||||
return 2
|
||||
|
||||
if not SRC.is_dir():
|
||||
print(f"source directory absent: {SRC}", file=sys.stderr)
|
||||
return 2
|
||||
DST.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
manifest = json.loads(MANIFEST.read_text()) if MANIFEST.exists() else {"files": {}}
|
||||
files = manifest.setdefault("files", {})
|
||||
|
||||
added, refreshed, unchanged = [], [], []
|
||||
for src in sorted(SRC.glob("*.jsonl")):
|
||||
digest = sha256(src)
|
||||
rec = files.get(src.name)
|
||||
dst = DST / src.name
|
||||
if rec and rec["sha256"] == digest and dst.exists():
|
||||
unchanged.append(src.name)
|
||||
continue
|
||||
shutil.copy2(src, dst) # -p: mtime is load-bearing here
|
||||
entry = {
|
||||
"sha256": digest,
|
||||
"bytes": src.stat().st_size,
|
||||
"source_mtime": iso(src.stat().st_mtime),
|
||||
"first_preserved": (rec or {}).get("first_preserved") or now_iso(),
|
||||
"last_refreshed": now_iso(),
|
||||
}
|
||||
(refreshed if rec else added).append(src.name)
|
||||
files[src.name] = entry
|
||||
|
||||
# ---- READ BACK. Preservation is proved from disk, never from copy success.
|
||||
failures, orphaned = [], []
|
||||
for name, rec in files.items():
|
||||
p = DST / name
|
||||
if not p.exists():
|
||||
failures.append(f"{name}: MISSING from preserved set")
|
||||
continue
|
||||
if sha256(p) != rec["sha256"]:
|
||||
failures.append(f"{name}: HASH MISMATCH on read-back")
|
||||
if not (SRC / name).exists():
|
||||
orphaned.append(name) # pruned at source — preserved here. The point.
|
||||
|
||||
manifest["last_run"] = now_iso()
|
||||
manifest["readback"] = "PASS" if not failures else "FAIL"
|
||||
manifest["counts"] = {
|
||||
"preserved_total": len(files),
|
||||
"present_at_source": len(files) - len(orphaned),
|
||||
"pruned_at_source_but_preserved": len(orphaned),
|
||||
}
|
||||
MANIFEST.write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n")
|
||||
|
||||
total = sum(r["bytes"] for r in files.values())
|
||||
print(f"preserved total : {len(files)} transcripts, {total/1048576:.1f} MB")
|
||||
print(f" newly added : {len(added)}")
|
||||
print(f" refreshed : {len(refreshed)} (append-only growth at source)")
|
||||
print(f" unchanged : {len(unchanged)}")
|
||||
print(f" pruned at source, surviving only here : {len(orphaned)}")
|
||||
print(f"N-now at source (the trial's own counter): "
|
||||
f"{len(list(SRC.glob('*.jsonl')))}")
|
||||
if failures:
|
||||
print("\nREAD-BACK FAILURES:", file=sys.stderr)
|
||||
for f in failures:
|
||||
print(" " + f, file=sys.stderr)
|
||||
return 1
|
||||
print("read-back : PASS (every preserved file re-hashed from disk)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user