AtlatestRepositoryfolio

folio / tree / testsabotage-f3b.sh

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 (see the population-split comment before T1 -- the first run's
8# mispredictions are what taught it):
9# T1 raw-vs-semantic ACCOUNTING tolerated -> b6 red: the prose/markdown
10# carriers are UNSTAGED, so only the accounting can refuse for them
11# T2 semantic pass returns NO sites + accounting tolerated -> b5 red on
12# its CONTENT asserts (referrers never touched; closure cannot see
13# unstaged files either)
14# T2c semantic pass keeps only the FIRST site per file + accounting
15# tolerated -> b5 red WITH the closure signature (staged residue is
16# closure's own population)
17# T3 delete ignores referrers -> b7-delete red
18# T4 recovery goes silent (recover-root! returns '() doing nothing) ->
19# b3-recovery-completes red (completion never happens, manifest stays)
20# T5 recovery TOLERATES an unprovable state (the first-pass proof clause
21# always passes) -> gate arm 5 red (the corrupt-temp driver observes
22# recovery proceeding over garbage instead of refusing to a human)
23# T2c semantic pass drops sites after the first per file (accounting
24# tolerated) -> b5 red WITH the closure signature (staged residue)
25# TA assertion layer: flip the bystander hash expectation -> b12 red
27set -u
28repo_dir=$(cd "$(dirname "$0")/.." && pwd)
29cd "$repo_dir" || exit 1
31work=$(mktemp -d /tmp/sabotage-f3b.XXXXXX) || { echo "SAB-F3B SETUP-FAILED: no temp dir"; exit 3; }
32trap 'chmod -R u+rwX "$work" 2>/dev/null; rm -rf "$work"' EXIT INT TERM
34sigil="${SIGIL:-sigil}"
35export SIGIL_REDIRECTS="${SIGIL_F3B_REDIRECTS:-$repo_dir/dev-redirects.sgl}"
36F3B_FIX="$repo_dir/test/fixtures/f3b"
38ST=src/folio/doc/structural.sgl
39MAN=src/folio/doc/manifest.sgl
40PROBE=test/f3b-probe.sgl
42legs=0; reds=0; setup_fails=0
43fail_leg () { echo "SAB-F3B LEG $1: DID NOT GO RED AS PREDICTED -- $2" >&2; }
44setup_failed_leg () { echo "SAB-F3B LEG $1: SETUP-FAILED -- $2" >&2; setup_fails=$((setup_fails + 1)); }
45hash_of () { sha256sum "$1" | cut -d' ' -f1; }
47h_st=$(hash_of "$ST"); h_man=$(hash_of "$MAN"); h_probe=$(hash_of "$PROBE")
48restore_all () {
49 git checkout -- "$ST" "$MAN" "$PROBE" || return 1
50 [ "$(hash_of "$ST")" = "$h_st" ] || { echo "SAB-F3B RESTORE FAILED: $ST" >&2; return 1; }
51 [ "$(hash_of "$MAN")" = "$h_man" ] || { echo "SAB-F3B RESTORE FAILED: $MAN" >&2; return 1; }
52 [ "$(hash_of "$PROBE")" = "$h_probe" ] || { echo "SAB-F3B RESTORE FAILED: $PROBE" >&2; return 1; }
53 return 0
55git diff --quiet -- "$ST" "$MAN" "$PROBE" \
56 || { echo "SAB-F3B SETUP-FAILED: dirty tree under the plant files"; exit 3; }
58plant_landed () {
59 leg=$1; file=$2; old=$3; marker=$4; want=$5
60 [ "$(hash_of "$file")" != "$old" ] || { setup_failed_leg "$leg" "no diff in $file"; return 1; }
61 got=$(grep -c "$marker" "$file" || true)
62 [ "$got" -eq "$want" ] || { setup_failed_leg "$leg" "marker '$marker' count $got, wanted $want"; return 1; }
63 return 0
65build_planted () {
66 if ! "$sigil" build >"$work/build-$1.log" 2>&1; then
67 setup_failed_leg "$1" "planted tree does not build"; return 1
68 fi
69 return 0
71run_battery () {
72 rb="$work/root-$1"; mkdir -p "$rb"; cp "$F3B_FIX"/* "$rb"/
73 F3B_ROOT="$rb" "$sigil" -L build/dev/lib -L src "$PROBE" >"$work/bat-$1.log" 2>&1
74 return $?
76expect () {
77 leg=$1; arm=$2; want=$3
78 grep -q "F3B_ARM_${arm}_${want}" "$work/bat-$leg.log" && return 0
79 fail_leg "$leg" "predicted $arm=$want; got: $(grep "F3B_ARM_${arm}_" "$work/bat-$leg.log" | head -1 || echo 'no marker')"
80 return 1
82leg_result () {
83 legs=$((legs + 1))
84 [ "$2" -eq 0 ] && { reds=$((reds + 1)); echo "SAB-F3B LEG $1: RED AS PREDICTED"; }
87# ---------------------------------------------------------------------------
88# THE POPULATION SPLIT THESE THREE LEGS DEMONSTRATE (learned from this
89# suite's own first run, whose predictions had it backwards): the ACCOUNTING
90# protects UNSTAGED carriers -- a prose or markdown file is never staged, so
91# graph closure structurally cannot see it; only the raw-vs-semantic
92# arithmetic can refuse for it. GRAPH CLOSURE protects STAGED files -- a
93# rewrite site dropped inside a touched file leaves residue closure sees.
94# The two layers partition the population rather than backing each other up,
95# and each leg below fells exactly one.
96# ---------------------------------------------------------------------------
97echo "SAB-F3B LEG T1: raw-vs-semantic accounting tolerated"
98echo " PREDICT RED: b6-rename-refusals (the prose and markdown occurrences"
99echo " are silently dropped, the renames PROCEED, and b6's"
100echo " expected refusals never come -- closure cannot help:"
101echo " those files are never staged)"
102echo " PREDICT GREEN: b5 (every occurrence in ITS web is rewritable), b1, b7"
103sed -i 's/(if (= accounted (length hits))/(if #t ;; SAB-T1/' src/folio/doc/structural.sgl
104if plant_landed T1 "$ST" "$h_st" "SAB-T1" 1 && build_planted T1; then
105 run_battery T1 || true
106 ok=0
107 expect T1 b6-rename-refusals RED || ok=1
108 expect T1 b5-rename GREEN || ok=1
109 expect T1 b1-stand-down GREEN || ok=1
110 expect T1 b7-delete GREEN || ok=1
111 leg_result T1 $ok
112fi
113restore_all || exit 3
115# ---------------------------------------------------------------------------
116echo "SAB-F3B LEG T2: the semantic pass returns NO sites (total silent loss)"
117echo " with the accounting tolerated -- referrer files are never touched at"
118echo " all, so closure (staged-only) cannot see it either; b5's own CONTENT"
119echo " asserts on the referrer files are what catches it"
120echo " PREDICT RED: b5-rename (residue in ref files)"
121echo " PREDICT GREEN: b1"
122sed -i 's/(if (= accounted (length hits))/(if #t ;; SAB-T1/' src/folio/doc/structural.sgl
123sed -i 's/(walk rest (if f (cons (list n f) out) out))/(walk rest (begin (quote sab-t2) out))/' src/folio/doc/structural.sgl
124if plant_landed T2 "$ST" "$h_st" "sab-t2" 1 && build_planted T2; then
125 run_battery T2 || true
126 ok=0
127 expect T2 b5-rename RED || ok=1
128 expect T2 b1-stand-down GREEN || ok=1
129 leg_result T2 $ok
130fi
131restore_all || exit 3
133# ---------------------------------------------------------------------------
134echo "SAB-F3B LEG T2c: the semantic pass drops every site AFTER the first per"
135echo " file (accounting tolerated) -- ref-two stays TOUCHED with one of its"
136echo " two sites stale, which is exactly the residue GRAPH CLOSURE exists"
137echo " to catch"
138echo " PREDICT RED: b5-rename WITH the closure signature"
139sed -i 's/(if (= accounted (length hits))/(if #t ;; SAB-T1/' src/folio/doc/structural.sgl
140sed -i 's/(walk rest (if f (cons (list n f) out) out))/(walk rest (if (and f (null? out)) (cons (list n f) out) (begin (quote sab-t2c) out)))/' src/folio/doc/structural.sgl
141if plant_landed T2c "$ST" "$h_st" "sab-t2c" 1 && build_planted T2c; then
142 run_battery T2c || true
143 ok=0
144 expect T2c b5-rename RED || ok=1
145 grep -q "closure" "$work/bat-T2c.log" \
146 || { fail_leg T2c "red, but not via the closure check"; ok=1; }
147 leg_result T2c $ok
148fi
149restore_all || exit 3
151# ---------------------------------------------------------------------------
152echo "SAB-F3B LEG T3: delete ignores referrers"
153echo " PREDICT RED: b7-delete (a referenced delete publishes; the arm"
154echo " expects the refusal)"
155echo " PREDICT GREEN: b1, b5"
156sed -i 's/((or (pair? semantic) (pair? raw-only))/(#f ;; SAB-T3/' src/folio/doc/structural.sgl
157if plant_landed T3 "$ST" "$h_st" "SAB-T3" 1 && build_planted T3; then
158 run_battery T3 || true
159 ok=0
160 expect T3 b7-delete RED || ok=1
161 expect T3 b1-stand-down GREEN || ok=1
162 expect T3 b5-rename GREEN || ok=1
163 leg_result T3 $ok
164fi
165restore_all || exit 3
167# ---------------------------------------------------------------------------
168echo "SAB-F3B LEG T4: recovery goes silent (does nothing, reports nothing)"
169echo " PREDICT RED: b3-recovery-completes (no completion, no report, the"
170echo " manifest survives into the arm's asserts)"
171echo " PREDICT GREEN: b1 (no crash state on its path)"
172sed -i 's/(if (file-exists? mpath)/(if #f ;; SAB-T4/' src/folio/doc/manifest.sgl
173if plant_landed T4 "$MAN" "$h_man" "SAB-T4" 1 && build_planted T4; then
174 run_battery T4 || true
175 ok=0
176 expect T4 b3-recovery-completes RED || ok=1
177 expect T4 b1-stand-down GREEN || ok=1
178 leg_result T4 $ok
179fi
180restore_all || exit 3
182# ---------------------------------------------------------------------------
183echo "SAB-F3B LEG T5: recovery TOLERATES the unprovable (the first-pass proof"
184echo " always passes) -- gate arm 5's driver must observe recovery proceeding"
185echo " over a corrupt temp instead of refusing to a human"
186# Anchor on the proof loop's own first cond test (unique in the file): making
187# it always true means every entry is 'proven' finishable, so recovery
188# proceeds over the corrupt temp. A bare `(else` anchor is NOT unique -- the
189# containment fix and the sweep added more of them.
190sed -i 's/((equal? cur (mentry-post-rev e)) #t)/((begin (quote sab-t5) #t) #t)/' src/folio/doc/manifest.sgl
191if ! grep -q "sab-t5" src/folio/doc/manifest.sgl; then
192 setup_failed_leg T5 "plant did not land (anchor moved)"
193else
194 count=$(grep -c "sab-t5" src/folio/doc/manifest.sgl)
195 if [ "$count" -ne 1 ]; then
196 setup_failed_leg T5 "anchor matched $count sites; the plant is not the plant"
197 restore_all || exit 3
198 else
199 if build_planted T5; then
200 r5="$work/root-T5"; mkdir -p "$r5"; cp "$F3B_FIX"/* "$r5"/
201 F3B_ROOT="$r5" "$sigil" -L build/dev/lib -L src test/f3b-corrupt-temp-driver.sgl >"$work/t5.log" 2>&1
202 ok=0
203 if grep -q "CORRUPT_TEMP_REFUSED_TO_HUMAN" "$work/t5.log"; then
204 fail_leg T5 "the driver still saw a refusal with the proof clause disabled"; ok=1
205 else
206 grep -qE "PROCEEDED_OVER_UNPROVABLE_STATE|RAISED_BUT_WRONG_SHAPE" "$work/t5.log" \
207 || { fail_leg T5 "no recognisable outcome: $(tail -2 "$work/t5.log")"; ok=1; }
208 fi
209 leg_result T5 $ok
210 fi
211 fi
212fi
213restore_all || exit 3
215# ---------------------------------------------------------------------------
216echo "SAB-F3B LEG TA: assertion layer -- flip the bystander hash expectation"
217echo " PREDICT RED: b12-bystander"
218# The marker lives INSIDE a string literal: the first spelling appended a
219# `;;` comment mid-expression, which swallowed the closing parens and killed
220# the whole probe at parse (observed: an EMPTY arm set, the worst red shape).
221sed -i 's/(equal? (revision-of-file (rooted "bystander.md")) bystander-hash)/(equal? "SAB-TA-impossible" (revision-of-file (rooted "bystander.md")))/' test/f3b-probe.sgl
222if plant_landed TA "$PROBE" "$h_probe" "SAB-TA-impossible" 1; then
223 run_battery TA || true
224 ok=0
225 expect TA b12-bystander RED || ok=1
226 leg_result TA $ok
227fi
228restore_all || exit 3
230# ---------------------------------------------------------------------------
231echo "SAB-F3B: rebuilding restored tree and re-running the FULL gate"
232"$sigil" build >"$work/build-final.log" 2>&1 || { echo "SAB-F3B SETUP-FAILED: restored tree does not build"; exit 3; }
233if sh test/gate-f3b.sh >"$work/gate-final.log" 2>&1; then
234 echo "SAB-F3B: restored gate GREEN"
235else
236 echo "SAB-F3B FAILED: gate RED on the RESTORED tree"; tail -5 "$work/gate-final.log"; exit 1
237fi
239echo "---"
240echo "F3B_SABOTAGE legs=$legs red=$reds setup_failed=$setup_fails"
241[ "$legs" -eq 7 ] && [ "$reds" -eq 7 ] && [ "$setup_fails" -eq 0 ] && exit 0
242exit 1