Commit4578e89dRecorded25 Aug 2026Repositoryfolio

F3b: the gate with real-SIGKILL recovery arms, and the sabotage suite

Message

gate-f3b.sh: both harness self-tests; the 12-arm F3b battery; the 22-arm F3a battery as the no-regression arm; SIGKILL-mid-rename at four staggered delays PLUS a busy-poll round that kills the instant the manifest atomically appears -- the poll round CAUGHT the window on its first run (manifest_at_kill=yes) and recovery completed a real-killed validated set to the fully-new state with the report, so completion is demonstrated against a genuine SIGKILL, not only against constructed crash bytes. Coherence contract per round: every file fully-old or fully-new, all files agreeing, never torn, manifest never surviving recovery. Arm 5 constructs the UNPROVABLE state (validated manifest, corrupted temp) and requires the refusal-to-a-human with nothing touched.

sabotage-f3b.sh: six legs, predictions first, plants hash-guarded and build-checked, restores by checkout verified by hash, full gate re-run green at the end. T1 tolerates the raw-vs-semantic accounting -- the graph closure must catch the residue; T2 fells BOTH layers -- the stale referrer publishes and b5's content asserts catch it (the pairing shows the layers are independent, not redundant); T3 makes delete ignore referrers; T4 silences recovery entirely; T5 makes recovery tolerate the unprovable state (gate arm 5's driver observes it proceeding over garbage); TA is the assertion-layer leg on the bystander hash.

Committed BEFORE the sabotage dry-run, per this session's own a-dry-run-of-a-restore-script-is-still-a-restore lesson.

Changed
 test/f3b-corrupt-temp-driver.sgl |  50 ++++++++++++++++++++++++++++++++++++++
 test/f3b-recover-driver.sgl      |  24 +++++++++++++++++++
 test/f3b-renamer.sgl             |  25 +++++++++++++++++++
 test/gate-f3b.sh                 | 209 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 test/sabotage-f3b.sh             | 199 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 507 insertions(+)
Diff
test/f3b-corrupt-temp-driver.sgladded
@@ -0,0 +1,50 @@
+1
;;; f3b-corrupt-temp-driver.sgl -- construct the UNPROVABLE recovery state
+2
;;; (a validated manifest whose staged temp was corrupted after the crash)
+3
;;; and prove the next transaction REFUSES TO A HUMAN and touches nothing.
+4
;;; Used by gate-f3b.sh arm 5; sabotage-f3b.sh's recovery-tolerates plant
+5
;;; makes this driver's refusal vanish, which is that leg's red.
+6
(import (sigil core) (sigil io) (sigil fs) (sigil string) (sigil path)
+7
(folio doc revision) (folio doc bounded) (folio doc service)
+8
(folio doc manifest) (folio doc coordinator) (folio doc transaction)
+9
(folio doc structural))
+10
(define root (or (getenv "F3B_ROOT") (begin (println "DRIVER ERROR no root") (exit 3))))
+11
(define lib-dir (or (getenv "F3B_LIB") "build/dev/lib"))
+12
(define src-dir (or (getenv "F3B_SRC") "src"))
+13
(define (rooted n) (path-join root n))
+14
+15
(define target (rooted "ref-one.sgd"))
+16
(define pre-rev (revision-of-file target))
+17
(define good-post (string-replace (read-file-string target) "one site" "one FINISHED site"))
+18
(define temp ".folio-staging-ref-one.sgd.corrupt")
+19
(define backup ".folio-backup-ref-one.sgd.corrupt")
+20
(write-file-string (rooted temp) good-post)
+21
(write-file-bytes (rooted backup) (read-file-bytes target))
+22
(define post-rev (revision-of-file (rooted temp)))
+23
(write-transaction-manifest! root "corrupttest" "probe-op"
+24
(list (mentry "ref-one.sgd" pre-rev temp post-rev post-rev backup pre-rev)))
+25
;; NOW corrupt the temp -- the state recovery cannot prove safe to finish.
+26
(write-file-string (rooted temp) "garbage that matches no recorded hash")
+27
+28
(define coord (make-coordinator root
+29
(make-service (default-worker-argv lib-dir src-dir)
+30
default-limits)))
+31
(define raised
+32
(guard (e (#t (if (exception? e) (exception-message e) "raised")))
+33
(folio-insert-node! coord (rooted "dest.sgd") "incoming" 'update
+34
(revision-of-file (rooted "dest.sgd"))
+35
timestamp: "2026-08-25" id: "u-corrupt")
+36
#f))
+37
(cond
+38
((and (string? raised)
+39
(string-contains? raised "human")
+40
(equal? (revision-of-file target) pre-rev)
+41
(file-exists? (rooted ".folio-transaction-manifest")))
+42
(println "CORRUPT_TEMP_REFUSED_TO_HUMAN"))
+43
((string? raised)
+44
(println "RAISED_BUT_WRONG_SHAPE: ~a (target-moved=~a manifest-gone=~a)"
+45
raised
+46
(not (equal? (revision-of-file target) pre-rev))
+47
(not (file-exists? (rooted ".folio-transaction-manifest")))))
+48
(else
+49
(println "PROCEEDED_OVER_UNPROVABLE_STATE (target-rev=~a)"
+50
(revision-of-file target))))
test/f3b-recover-driver.sgladded
@@ -0,0 +1,24 @@
+1
;;; f3b-recover-driver.sgl -- run one transaction so the coordinator's
+2
;;; automatic recovery executes, and print every recovery action so the gate
+3
;;; can assert a self-heal was REPORTED. Used by gate-f3b.sh arm 4.
+4
(import (sigil core) (sigil io) (sigil fs) (sigil path)
+5
(folio doc revision) (folio doc bounded) (folio doc service)
+6
(folio doc coordinator) (folio doc transaction) (folio doc structural))
+7
(define root (or (getenv "F3B_ROOT") (begin (println "DRIVER ERROR no root") (exit 3))))
+8
(define lib-dir (or (getenv "F3B_LIB") "build/dev/lib"))
+9
(define src-dir (or (getenv "F3B_SRC") "src"))
+10
(define coord (make-coordinator root
+11
(make-service (default-worker-argv lib-dir src-dir)
+12
default-limits)))
+13
(define r (folio-insert-node! coord (path-join root "dest.sgd") "incoming" 'update
+14
(revision-of-file (path-join root "dest.sgd"))
+15
timestamp: "2026-08-25" id: "u-recdrv"
+16
body-text: "Recovery driver entry."))
+17
(cond
+18
((txn-result? r)
+19
(for-each (lambda (a) (println "~a" a)) (txn-result-recovery-actions r))
+20
(println "DRIVER_OK"))
+21
((txn-refusal? r)
+22
(for-each (lambda (a) (println "~a" a)) (txn-refusal-recovery-actions r))
+23
(println "DRIVER_REFUSED ~a: ~a" (txn-refusal-reason r) (txn-refusal-message r))
+24
(exit 1)))
test/f3b-renamer.sgladded
@@ -0,0 +1,25 @@
+1
;;; f3b-renamer.sgl -- one real writer process performing the 3-file rename,
+2
;;; for gate-f3b.sh's kill arms. Prints RENAMER_READY once the coordinator
+3
;;; is built, waits for the start file, runs the rename, prints RENAMER_DONE
+4
;;; or the refusal. The gate SIGKILLs it at staggered delays and then proves
+5
;;; the all-or-nothing invariant.
+6
(import (sigil core) (sigil io) (sigil fs) (sigil string) (sigil time) (sigil path)
+7
(folio doc revision) (folio doc bounded) (folio doc service)
+8
(folio doc coordinator) (folio doc transaction) (folio doc structural))
+9
+10
(define (need name)
+11
(or (getenv name) (begin (println "RENAMER ERROR ~a not set" name) (exit 3))))
+12
(define root (need "F3B_ROOT"))
+13
(define start-file (need "F3B_START_FILE"))
+14
(define lib-dir (or (getenv "F3B_LIB") "build/dev/lib"))
+15
(define src-dir (or (getenv "F3B_SRC") "src"))
+16
+17
(define coord (make-coordinator root
+18
(make-service (default-worker-argv lib-dir src-dir)
+19
default-limits)))
+20
(println "RENAMER_READY")
+21
(let wait () (unless (file-exists? start-file) (sleep 0.005) (wait)))
+22
(let ((r (folio-rename-id! coord "t-target" "t-retargeted"
+23
(revision-of-file (path-join root "owner.sgd")))))
+24
(cond ((sop-result? r) (println "RENAMER_DONE ~a" (sop-result-detail r)))
+25
((txn-refusal? r) (println "RENAMER_REFUSED ~a" (txn-refusal-reason r)))))
test/gate-f3b.shadded
@@ -0,0 +1,209 @@
+1
#!/bin/sh
+2
# gate-f3b.sh -- the F3b gate: structural operations, crash recovery, and
+3
# cross-file transactions.
+4
#
+5
# RUN: sh test/gate-f3b.sh (all arms)
+6
# SABOTAGE: sh test/sabotage-f3b.sh (proves these arms can go red)
+7
#
+8
# ARMS
+9
# 1 harness self-tests: both probe batteries' assertion primitives CAN fail
+10
# 2 the F3b battery (test/f3b-probe.sgl, 12 arms: stand-down control first,
+11
# both recovery branches with the discriminator, the 3-file rename with
+12
# graph closure, referrer-guarded delete, cross-file move with document
+13
# re-addressing, scoped-out wording, invalid-doc read-only, body
+14
# replacement, the hash-pinned Markdown bystander)
+15
# 3 the F3a battery (22 arms) -- the scalar layer must not regress
+16
# 4 REAL SIGKILL mid-rename at staggered delays: after recovery, every
+17
# file is fully-old or fully-new by content -- NEVER mixed -- and when a
+18
# manifest existed at kill time the recovery is REPORTED
+19
# 5 a VALIDATED manifest whose staged temp was corrupted after the crash:
+20
# recovery REFUSES to a human and touches nothing
+21
#
+22
# SETUP-FAILED is distinct from RED and GREEN throughout.
+23
+24
set -u
+25
repo_dir=$(cd "$(dirname "$0")/.." && pwd)
+26
cd "$repo_dir" || exit 1
+27
+28
work=$(mktemp -d /tmp/gate-f3b.XXXXXX) || { echo "GATE-F3B SETUP-FAILED: no temp dir"; exit 3; }
+29
trap 'chmod -R u+rwX "$work" 2>/dev/null; rm -rf "$work"' EXIT INT TERM
+30
+31
sigil="${SIGIL:-sigil}"
+32
export SIGIL_REDIRECTS="${SIGIL_F3B_REDIRECTS:-$repo_dir/dev-redirects.sgl}"
+33
F3A_FIX="$repo_dir/test/fixtures/f3a"
+34
F3B_FIX="$repo_dir/test/fixtures/f3b"
+35
+36
fails=0
+37
fail () { echo "GATE-F3B FAIL: $*" >&2; fails=$((fails + 1)); }
+38
setup_failed () { echo "GATE-F3B SETUP-FAILED: $*" >&2; exit 3; }
+39
prev_fails=0
+40
arm_summary () {
+41
if [ "$fails" -gt "$prev_fails" ]; then
+42
echo "GATE-F3B arm $1: FAILED ($((fails - prev_fails)) failure(s))"
+43
else
+44
echo "GATE-F3B arm $1: OK"
+45
fi
+46
prev_fails=$fails
+47
}
+48
+49
only_arms="${*:-1 2 3 4 5}"
+50
want () { case " $only_arms " in *" $1 "*) return 0;; *) return 1;; esac; }
+51
+52
fresh_f3b_root () { mkdir -p "$1" && cp "$F3B_FIX"/* "$1"/ || setup_failed "cannot populate f3b root"; }
+53
+54
echo "GATE-F3B: building (dev config)..."
+55
"$sigil" build >"$work/build.log" 2>&1 || setup_failed "sigil build failed"
+56
[ -d build/dev/lib ] || setup_failed "build/dev/lib absent after build"
+57
run_sgl () { "$sigil" -L build/dev/lib -L src "$@"; }
+58
+59
# Sequential cache warm-up before any concurrent spawn (the F3a lesson).
+60
warm="$work/warm"; fresh_f3b_root "$warm"
+61
F3B_PROBE_SELFTEST=1 F3B_ROOT="$warm" run_sgl test/f3b-probe.sgl >/dev/null 2>&1 || true
+62
+63
# ---------------------------------------------------------------------------
+64
if want 1; then
+65
r1="$work/st1"; fresh_f3b_root "$r1"
+66
F3B_PROBE_SELFTEST=1 F3B_ROOT="$r1" run_sgl test/f3b-probe.sgl >"$work/st-b.log" 2>&1 \
+67
&& grep -q "F3B_ARM_selftest_RED" "$work/st-b.log" \
+68
|| fail "arm1: the f3b harness did not record a red for a false assertion"
+69
r1a="$work/st1a"; mkdir -p "$r1a"; cp "$F3A_FIX"/*.sgd "$r1a"/
+70
F3A_PROBE_SELFTEST=1 F3A_ROOT="$r1a" run_sgl test/f3a-txn-probe.sgl >"$work/st-a.log" 2>&1 \
+71
&& grep -q "F3A_ARM_selftest_RED" "$work/st-a.log" \
+72
|| fail "arm1: the f3a harness did not record a red for a false assertion"
+73
arm_summary 1
+74
fi
+75
+76
# ---------------------------------------------------------------------------
+77
if want 2; then
+78
r2="$work/b"; fresh_f3b_root "$r2"
+79
F3B_ROOT="$r2" run_sgl test/f3b-probe.sgl >"$work/b.log" 2>&1 || fail "arm2: battery exited non-zero"
+80
grep -q "F3B_PROBE_DONE arms=12 green=12 red=0" "$work/b.log" \
+81
|| { fail "arm2: expected 12/12; got $(grep F3B_PROBE_DONE "$work/b.log" || echo none)"; \
+82
grep "_RED" "$work/b.log" >&2 || true; }
+83
arm_summary 2
+84
fi
+85
+86
# ---------------------------------------------------------------------------
+87
if want 3; then
+88
r3="$work/a"; mkdir -p "$r3"; cp "$F3A_FIX"/*.sgd "$r3"/
+89
F3A_ROOT="$r3" run_sgl test/f3a-txn-probe.sgl >"$work/a.log" 2>&1 || fail "arm3: f3a battery exited non-zero"
+90
grep -q "F3A_PROBE_DONE arms=22 green=22 red=0" "$work/a.log" \
+91
|| { fail "arm3: expected 22/22; got $(grep F3A_PROBE_DONE "$work/a.log" || echo none)"; \
+92
grep "_RED" "$work/a.log" >&2 || true; }
+93
arm_summary 3
+94
fi
+95
+96
# ---------------------------------------------------------------------------
+97
# Arm 4: SIGKILL mid-rename. Coherence contract per file after recovery:
+98
# fully-old (has t-target, no t-retargeted) or fully-new (the reverse), and
+99
# ALL FILES AGREE. A file with both, or files disagreeing, is a torn write.
+100
# ---------------------------------------------------------------------------
+101
if want 4; then
+102
for delay in 0.05 0.20 0.60 1.20; do
+103
rk="$work/kill-$delay"; fresh_f3b_root "$rk"
+104
startk="$work/ks-$delay"; rm -f "$startk"
+105
F3B_ROOT="$rk" F3B_START_FILE="$startk" \
+106
"$sigil" -L build/dev/lib -L src test/f3b-renamer.sgl >"$work/rn-$delay.log" 2>&1 &
+107
rn=$!
+108
tries=0
+109
while ! grep -q RENAMER_READY "$work/rn-$delay.log" 2>/dev/null; do
+110
tries=$((tries + 1)); [ "$tries" -gt 600 ] && break; sleep 0.1
+111
done
+112
grep -q RENAMER_READY "$work/rn-$delay.log" || setup_failed "arm4: renamer never became ready"
+113
: > "$startk"
+114
sleep "$delay"
+115
kill -9 "$rn" 2>/dev/null
+116
wait "$rn" 2>/dev/null
+117
manifest_at_kill=no
+118
[ -f "$rk/.folio-transaction-manifest" ] && manifest_at_kill=yes
+119
# Recovery driver: any next transaction (its log carries the report).
+120
F3B_ROOT="$rk" run_sgl test/f3b-recover-driver.sgl >"$work/rec-$delay.log" 2>&1 \
+121
|| fail "arm4 delay=$delay: the post-kill transaction failed"
+122
old_files=0; new_files=0; torn=0
+123
for f in owner.sgd ref-one.sgd ref-two.sgd; do
+124
has_old=no; has_new=no
+125
grep -q '"t-target"' "$rk/$f" && has_old=yes
+126
grep -q '"t-retargeted"' "$rk/$f" && has_new=yes
+127
if [ "$has_old" = yes ] && [ "$has_new" = yes ]; then torn=$((torn+1)); fi
+128
[ "$has_old" = yes ] && old_files=$((old_files+1))
+129
[ "$has_new" = yes ] && new_files=$((new_files+1))
+130
done
+131
[ "$torn" -eq 0 ] || fail "arm4 delay=$delay: a file carries BOTH ids -- torn"
+132
if [ "$old_files" -gt 0 ] && [ "$new_files" -gt 0 ]; then
+133
fail "arm4 delay=$delay: files DISAGREE (old=$old_files new=$new_files) -- partial rewrite survived recovery"
+134
fi
+135
if [ "$manifest_at_kill" = yes ]; then
+136
grep -q "recovery:" "$work/rec-$delay.log" \
+137
|| fail "arm4 delay=$delay: a manifest existed at kill time but no recovery was reported"
+138
[ "$new_files" -eq 3 ] \
+139
|| fail "arm4 delay=$delay: manifest existed (validated) but recovery did not complete to the new state"
+140
fi
+141
[ -f "$rk/.folio-transaction-manifest" ] && fail "arm4 delay=$delay: manifest survived recovery"
+142
echo "GATE-F3B arm4 delay=$delay: manifest_at_kill=$manifest_at_kill old=$old_files new=$new_files torn=$torn"
+143
done
+144
# Best-effort MANIFEST-WINDOW round: busy-poll for the manifest's atomic
+145
# appearance and kill the instant it exists. The window (renames +
+146
# cleanup) is milliseconds wide, so catching it is not guaranteed and NO
+147
# assertion here requires it -- the coherence contract holds either way,
+148
# and when the window IS caught this round exercises real-kill completion
+149
# (the deterministic completion coverage is the battery's b3 arm, which
+150
# constructs the identical bytes). The round REPORTS which case ran.
+151
rkw="$work/kill-window"; fresh_f3b_root "$rkw"
+152
startw="$work/ks-window"; rm -f "$startw"
+153
F3B_ROOT="$rkw" F3B_START_FILE="$startw" \
+154
"$sigil" -L build/dev/lib -L src test/f3b-renamer.sgl >"$work/rn-window.log" 2>&1 &
+155
rnw=$!
+156
tries=0
+157
while ! grep -q RENAMER_READY "$work/rn-window.log" 2>/dev/null; do
+158
tries=$((tries + 1)); [ "$tries" -gt 600 ] && break; sleep 0.1
+159
done
+160
: > "$startw"
+161
while kill -0 "$rnw" 2>/dev/null && [ ! -f "$rkw/.folio-transaction-manifest" ]; do :; done
+162
kill -9 "$rnw" 2>/dev/null
+163
wait "$rnw" 2>/dev/null
+164
window_hit=no
+165
[ -f "$rkw/.folio-transaction-manifest" ] && window_hit=yes
+166
F3B_ROOT="$rkw" run_sgl test/f3b-recover-driver.sgl >"$work/rec-window.log" 2>&1 \
+167
|| fail "arm4 window: the post-kill transaction failed"
+168
wold=0; wnew=0; wtorn=0
+169
for f in owner.sgd ref-one.sgd ref-two.sgd; do
+170
ho=no; hn=no
+171
grep -q '"t-target"' "$rkw/$f" && ho=yes
+172
grep -q '"t-retargeted"' "$rkw/$f" && hn=yes
+173
[ "$ho" = yes ] && [ "$hn" = yes ] && wtorn=$((wtorn+1))
+174
[ "$ho" = yes ] && wold=$((wold+1))
+175
[ "$hn" = yes ] && wnew=$((wnew+1))
+176
done
+177
[ "$wtorn" -eq 0 ] || fail "arm4 window: torn file"
+178
if [ "$wold" -gt 0 ] && [ "$wnew" -gt 0 ]; then
+179
fail "arm4 window: files disagree (old=$wold new=$wnew) after recovery"
+180
fi
+181
if [ "$window_hit" = yes ]; then
+182
[ "$wnew" -eq 3 ] || fail "arm4 window: manifest present at kill but recovery did not complete"
+183
grep -q "recovery:" "$work/rec-window.log" || fail "arm4 window: unreported recovery"
+184
fi
+185
echo "GATE-F3B arm4 window: manifest_at_kill=$window_hit old=$wold new=$wnew torn=$wtorn"
+186
arm_summary 4
+187
fi
+188
+189
# ---------------------------------------------------------------------------
+190
# Arm 5: a validated manifest whose temp was corrupted post-crash: recovery
+191
# REFUSES to a human, the target does not move, the manifest stays for the
+192
# human. (This is the unprovable state; guessing here is the defect.)
+193
# ---------------------------------------------------------------------------
+194
if want 5; then
+195
r5="$work/corrupt"; fresh_f3b_root "$r5"
+196
F3B_ROOT="$r5" run_sgl test/f3b-corrupt-temp-driver.sgl >"$work/corrupt.log" 2>&1
+197
grep -q "CORRUPT_TEMP_REFUSED_TO_HUMAN" "$work/corrupt.log" \
+198
|| { fail "arm5: recovery did not refuse the unprovable state; log:"; tail -5 "$work/corrupt.log" >&2; }
+199
arm_summary 5
+200
fi
+201
+202
echo "---"
+203
if [ "$fails" -eq 0 ]; then
+204
echo "F3B_GATE_GREEN arms_run=[$only_arms] f3b_arms=12 f3a_arms=22 kill_rounds=5 sha=$(git -C "$repo_dir" rev-parse --short HEAD 2>/dev/null || echo unknown)"
+205
exit 0
+206
else
+207
echo "F3B_GATE_RED failures=$fails arms_run=[$only_arms]"
+208
exit 1
+209
fi
test/sabotage-f3b.shadded
@@ -0,0 +1,199 @@
+1
#!/bin/sh
+2
# sabotage-f3b.sh -- proves gate-f3b.sh's arms can go RED. Product-layer
+3
# plants with the F3a discipline: prediction printed first (red AND green
+4
# sets), plant landed by hash + marker, planted tree must build, restores by
+5
# git checkout verified by hash, full gate re-run green at the end.
+6
#
+7
# Legs:
+8
# T1 raw-vs-semantic ACCOUNTING tolerated -> the graph-closure check
+9
# catches the stale referrer: b5-rename red (closure signature)
+10
# T2 T1 + closure check disabled -> the stale referrer PUBLISHES:
+11
# b5-rename red on its own content asserts (BOTH layers had to fall)
+12
# T3 delete ignores referrers -> b7-delete red
+13
# T4 recovery goes silent (recover-root! returns '() doing nothing) ->
+14
# b3-recovery-completes red (completion never happens, manifest stays)
+15
# T5 recovery TOLERATES an unprovable state (the first-pass proof clause
+16
# always passes) -> gate arm 5 red (the corrupt-temp driver observes
+17
# recovery proceeding over garbage instead of refusing to a human)
+18
# TA assertion layer: flip the bystander hash expectation -> b12 red
+19
+20
set -u
+21
repo_dir=$(cd "$(dirname "$0")/.." && pwd)
+22
cd "$repo_dir" || exit 1
+23
+24
work=$(mktemp -d /tmp/sabotage-f3b.XXXXXX) || { echo "SAB-F3B SETUP-FAILED: no temp dir"; exit 3; }
+25
trap 'chmod -R u+rwX "$work" 2>/dev/null; rm -rf "$work"' EXIT INT TERM
+26
+27
sigil="${SIGIL:-sigil}"
+28
export SIGIL_REDIRECTS="${SIGIL_F3B_REDIRECTS:-$repo_dir/dev-redirects.sgl}"
+29
F3B_FIX="$repo_dir/test/fixtures/f3b"
+30
+31
ST=src/folio/doc/structural.sgl
+32
MAN=src/folio/doc/manifest.sgl
+33
PROBE=test/f3b-probe.sgl
+34
+35
legs=0; reds=0; setup_fails=0
+36
fail_leg () { echo "SAB-F3B LEG $1: DID NOT GO RED AS PREDICTED -- $2" >&2; }
+37
setup_failed_leg () { echo "SAB-F3B LEG $1: SETUP-FAILED -- $2" >&2; setup_fails=$((setup_fails + 1)); }
+38
hash_of () { sha256sum "$1" | cut -d' ' -f1; }
+39
+40
h_st=$(hash_of "$ST"); h_man=$(hash_of "$MAN"); h_probe=$(hash_of "$PROBE")
+41
restore_all () {
+42
git checkout -- "$ST" "$MAN" "$PROBE" || return 1
+43
[ "$(hash_of "$ST")" = "$h_st" ] || { echo "SAB-F3B RESTORE FAILED: $ST" >&2; return 1; }
+44
[ "$(hash_of "$MAN")" = "$h_man" ] || { echo "SAB-F3B RESTORE FAILED: $MAN" >&2; return 1; }
+45
[ "$(hash_of "$PROBE")" = "$h_probe" ] || { echo "SAB-F3B RESTORE FAILED: $PROBE" >&2; return 1; }
+46
return 0
+47
}
+48
git diff --quiet -- "$ST" "$MAN" "$PROBE" \
+49
|| { echo "SAB-F3B SETUP-FAILED: dirty tree under the plant files"; exit 3; }
+50
+51
plant_landed () {
+52
leg=$1; file=$2; old=$3; marker=$4; want=$5
+53
[ "$(hash_of "$file")" != "$old" ] || { setup_failed_leg "$leg" "no diff in $file"; return 1; }
+54
got=$(grep -c "$marker" "$file" || true)
+55
[ "$got" -eq "$want" ] || { setup_failed_leg "$leg" "marker '$marker' count $got, wanted $want"; return 1; }
+56
return 0
+57
}
+58
build_planted () {
+59
if ! "$sigil" build >"$work/build-$1.log" 2>&1; then
+60
setup_failed_leg "$1" "planted tree does not build"; return 1
+61
fi
+62
return 0
+63
}
+64
run_battery () {
+65
rb="$work/root-$1"; mkdir -p "$rb"; cp "$F3B_FIX"/* "$rb"/
+66
F3B_ROOT="$rb" "$sigil" -L build/dev/lib -L src "$PROBE" >"$work/bat-$1.log" 2>&1
+67
return $?
+68
}
+69
expect () {
+70
leg=$1; arm=$2; want=$3
+71
grep -q "F3B_ARM_${arm}_${want}" "$work/bat-$leg.log" && return 0
+72
fail_leg "$leg" "predicted $arm=$want; got: $(grep "F3B_ARM_${arm}_" "$work/bat-$leg.log" | head -1 || echo 'no marker')"
+73
return 1
+74
}
+75
leg_result () {
+76
legs=$((legs + 1))
+77
[ "$2" -eq 0 ] && { reds=$((reds + 1)); echo "SAB-F3B LEG $1: RED AS PREDICTED"; }
+78
}
+79
+80
# ---------------------------------------------------------------------------
+81
echo "SAB-F3B LEG T1: raw-vs-semantic accounting tolerated"
+82
echo " PREDICT RED: b5-rename -- via the GRAPH CLOSURE check (the dropped"
+83
echo " accounting lets a semantically-invisible occurrence"
+84
echo " through, closure sees the residue and refuses)"
+85
echo " PREDICT GREEN: b1, b7 (delete's guard reads semantic+raw directly)"
+86
sed -i 's/(if (= accounted (length hits))/(if #t ;; SAB-T1/' src/folio/doc/structural.sgl
+87
if plant_landed T1 "$ST" "$h_st" "SAB-T1" 1 && build_planted T1; then
+88
run_battery T1 || true
+89
ok=0
+90
expect T1 b5-rename RED || ok=1
+91
grep -q "closure" "$work/bat-T1.log" || { fail_leg T1 "red, but not with the closure signature"; ok=1; }
+92
expect T1 b1-stand-down GREEN || ok=1
+93
expect T1 b7-delete GREEN || ok=1
+94
leg_result T1 $ok
+95
fi
+96
restore_all || exit 3
+97
+98
# ---------------------------------------------------------------------------
+99
echo "SAB-F3B LEG T2: accounting tolerated AND closure disabled -- the stale"
+100
echo " referrer PUBLISHES; b5's content asserts catch it (both layers fell)"
+101
echo " PREDICT RED: b5-rename (residue assert); b6 may cascade -- only b5 is"
+102
echo " asserted"
+103
sed -i 's/(if (= accounted (length hits))/(if #t ;; SAB-T1/' src/folio/doc/structural.sgl
+104
sed -i 's/(if (pair? leak)/(if #f ;; SAB-T2/' src/folio/doc/structural.sgl
+105
if plant_landed T2 "$ST" "$h_st" "SAB-T2" 1 && build_planted T2; then
+106
run_battery T2 || true
+107
ok=0
+108
expect T2 b5-rename RED || ok=1
+109
leg_result T2 $ok
+110
fi
+111
restore_all || exit 3
+112
+113
# ---------------------------------------------------------------------------
+114
echo "SAB-F3B LEG T3: delete ignores referrers"
+115
echo " PREDICT RED: b7-delete (a referenced delete publishes; the arm"
+116
echo " expects the refusal)"
+117
echo " PREDICT GREEN: b1, b5"
+118
sed -i 's/((or (pair? semantic) (pair? raw-only))/(#f ;; SAB-T3/' src/folio/doc/structural.sgl
+119
if plant_landed T3 "$ST" "$h_st" "SAB-T3" 1 && build_planted T3; then
+120
run_battery T3 || true
+121
ok=0
+122
expect T3 b7-delete RED || ok=1
+123
expect T3 b1-stand-down GREEN || ok=1
+124
expect T3 b5-rename GREEN || ok=1
+125
leg_result T3 $ok
+126
fi
+127
restore_all || exit 3
+128
+129
# ---------------------------------------------------------------------------
+130
echo "SAB-F3B LEG T4: recovery goes silent (does nothing, reports nothing)"
+131
echo " PREDICT RED: b3-recovery-completes (no completion, no report, the"
+132
echo " manifest survives into the arm's asserts)"
+133
echo " PREDICT GREEN: b1 (no crash state on its path)"
+134
sed -i 's/(if (file-exists? mpath)/(if #f ;; SAB-T4/' src/folio/doc/manifest.sgl
+135
if plant_landed T4 "$MAN" "$h_man" "SAB-T4" 1 && build_planted T4; then
+136
run_battery T4 || true
+137
ok=0
+138
expect T4 b3-recovery-completes RED || ok=1
+139
expect T4 b1-stand-down GREEN || ok=1
+140
leg_result T4 $ok
+141
fi
+142
restore_all || exit 3
+143
+144
# ---------------------------------------------------------------------------
+145
echo "SAB-F3B LEG T5: recovery TOLERATES the unprovable (the first-pass proof"
+146
echo " always passes) -- gate arm 5's driver must observe recovery proceeding"
+147
echo " over a corrupt temp instead of refusing to a human"
+148
sed -i 's/(else$/(#f ;; SAB-T5/' src/folio/doc/manifest.sgl
+149
if ! grep -q "SAB-T5" src/folio/doc/manifest.sgl; then
+150
setup_failed_leg T5 "plant did not land (anchor moved)"
+151
else
+152
# The blunt anchor above may hit several (else lines; use git to re-verify
+153
# only ONE landed where intended, else refuse the leg.
+154
count=$(grep -c "SAB-T5" src/folio/doc/manifest.sgl)
+155
if [ "$count" -ne 1 ]; then
+156
setup_failed_leg T5 "anchor matched $count sites; the plant is not the plant"
+157
restore_all || exit 3
+158
else
+159
if build_planted T5; then
+160
r5="$work/root-T5"; mkdir -p "$r5"; cp "$F3B_FIX"/* "$r5"/
+161
F3B_ROOT="$r5" "$sigil" -L build/dev/lib -L src test/f3b-corrupt-temp-driver.sgl >"$work/t5.log" 2>&1
+162
ok=0
+163
if grep -q "CORRUPT_TEMP_REFUSED_TO_HUMAN" "$work/t5.log"; then
+164
fail_leg T5 "the driver still saw a refusal with the proof clause disabled"; ok=1
+165
else
+166
grep -qE "PROCEEDED_OVER_UNPROVABLE_STATE|RAISED_BUT_WRONG_SHAPE" "$work/t5.log" \
+167
|| { fail_leg T5 "no recognisable outcome: $(tail -2 "$work/t5.log")"; ok=1; }
+168
fi
+169
leg_result T5 $ok
+170
fi
+171
fi
+172
fi
+173
restore_all || exit 3
+174
+175
# ---------------------------------------------------------------------------
+176
echo "SAB-F3B LEG TA: assertion layer -- flip the bystander hash expectation"
+177
echo " PREDICT RED: b12-bystander"
+178
sed -i 's/(equal? (revision-of-file (rooted "bystander.md")) bystander-hash)/(not (equal? (revision-of-file (rooted "bystander.md")) bystander-hash)) ;; SAB-TA/' test/f3b-probe.sgl
+179
if plant_landed TA "$PROBE" "$h_probe" "SAB-TA" 1; then
+180
run_battery TA || true
+181
ok=0
+182
expect TA b12-bystander RED || ok=1
+183
leg_result TA $ok
+184
fi
+185
restore_all || exit 3
+186
+187
# ---------------------------------------------------------------------------
+188
echo "SAB-F3B: rebuilding restored tree and re-running the FULL gate"
+189
"$sigil" build >"$work/build-final.log" 2>&1 || { echo "SAB-F3B SETUP-FAILED: restored tree does not build"; exit 3; }
+190
if sh test/gate-f3b.sh >"$work/gate-final.log" 2>&1; then
+191
echo "SAB-F3B: restored gate GREEN"
+192
else
+193
echo "SAB-F3B FAILED: gate RED on the RESTORED tree"; tail -5 "$work/gate-final.log"; exit 1
+194
fi
+195
+196
echo "---"
+197
echo "F3B_SABOTAGE legs=$legs red=$reds setup_failed=$setup_fails"
+198
[ "$legs" -eq 6 ] && [ "$reds" -eq 6 ] && [ "$setup_fails" -eq 0 ] && exit 0
+199
exit 1