AtlatestRepositoryfolio
1
#!/bin/sh2
# sabotage-f2b.sh -- prove that `test/gate-f2b.sh` can go RED.3
#4
# A gate reported only green has been shown to be quiet, not to work. Every leg5
# below breaks the PRODUCT -- source under `src/` or `tools/`, not the gate's own6
# assertions -- rebuilds, and requires the named arm to fail. A leg that stays7
# green is a finding about the gate, and this script says so in those words8
# rather than exiting 0.9
#10
# WHY THE PRODUCT LAYER SPECIFICALLY11
#12
# Breaking an assertion only proves the assertion is wired to something.13
# Breaking the PRODUCT proves the gate executes the product rather than a mirror14
# of itself. These legs all sit in the second class: each plants a defect a15
# reasonable person might actually ship -- several are lifted verbatim from F2's16
# own sabotage list -- and asks whether the gate notices.17
#18
# RESTORE IS VERIFIED BY HASH, NOT BY `git checkout` REPORTING SUCCESS. A leg19
# that restored the source but left a stale build artifact would make every20
# later leg a measurement of the wrong tree, so each leg rebuilds after21
# restoring and the final check re-hashes every touched file against its22
# starting digest.24
set -u25
repo_dir=$(cd "$(dirname "$0")/.." && pwd)26
cd "$repo_dir" || exit 128
sigil="${SIGIL:-sigil}"29
redirects="$repo_dir/dev-redirects.sgl"30
export SIGIL_REDIRECTS="$redirects"32
work=$(mktemp -d /tmp/sabotage-f2b.XXXXXX) || { echo "SETUP-FAILED: no temp dir"; exit 3; }34
# Baselines live OUTSIDE `$work`, because `cleanup` deletes `$work`.35
#36
# On the interrupt path the trap ran first, removed `$work`, and the restore37
# verification then read empty baselines and reported "4 file(s) not restored"38
# against a tree that `git status` showed was perfectly clean. A verification39
# that reads its own reference from a directory its own teardown deletes reports40
# a false alarm exactly when things went wrong -- which is when you least want to41
# be chasing a phantom. Observed 2026-08-23 on a deliberately-killed run.42
base=$(mktemp -d /tmp/sabotage-f2b-base.XXXXXX) || { echo "SETUP-FAILED: no baseline dir"; exit 3; }44
TOUCHED="src/folio/doc/discovery.sgl src/folio/doc/bounded.sgl src/folio/doc/revision.sgl src/folio/doc/service.sgl"46
# THE TREE MUST BE CLEAN BEFORE WE START.47
#48
# Every leg restores with `git checkout --`, so a pre-existing uncommitted change49
# to a touched file would be silently destroyed, and every "restored" check would50
# compare against the wrong baseline.51
for f in $TOUCHED; do52
if ! git diff --quiet -- "$f" 2>/dev/null; then53
echo "SETUP-FAILED: $f has uncommitted changes. This suite restores with \`git checkout --\`" >&254
echo " and would DESTROY them. Commit or set them aside first." >&255
exit 356
fi57
done59
# Baseline digests, captured BEFORE anything is planted.60
for f in $TOUCHED; do61
sha256sum "$f" | cut -d' ' -f1 > "$base/$(echo "$f" | tr / _).base"62
done64
restore_all () {65
for f in $TOUCHED; do git checkout -- "$f" 2>/dev/null; done66
}68
# DO NOT `exit` FROM THIS TRAP. A RETRACTED FIX, KEPT AS A WARNING.69
#70
# A first pass here "fixed" a status-laundering bug that DID NOT EXIST. The71
# reasoning was that this script's `exit 1` was being overwritten by its EXIT72
# trap. Measured afterwards, on this host's /bin/sh (bash 5.2.37), an EXIT trap73
# does NOT reset the status in any of four variants -- plain command, function,74
# function whose last command fails, function whose last command succeeds. All75
# four exit 1.76
#77
# The real cause was the MEASUREMENT, not the script. The run was invoked as78
#79
# sh test/sabotage-f2b.sh 2>&1 | grep -E ... | head -8080
#81
# and a pipeline's status is its LAST command's -- `head`, which exits 0. The82
# suite reported its failure correctly and the harness reading it threw that away.83
#84
# The attempted fix -- `cleanup () { ...; exit "$final_status"; }` -- then85
# INTRODUCED the very defect it was meant to remove: with `final_status` still 0,86
# an INT or TERM mid-run exits 0, so a Ctrl-C'd sabotage run reports SUCCESS.87
# Verified: `kill -INT $$` under that trap exits 0.88
#89
# So the trap cleans up and says nothing about status, which was right all along.90
# The lesson worth keeping is the general one: **when checking whether something91
# can report failure, do not read its status through a pipe.**92
cleanup () {93
restore_all94
"$sigil" build --redirects "$redirects" >/dev/null 2>&195
rm -rf "$work"96
}97
trap cleanup EXIT INT TERM99
legs=0100
bad=0102
# $1 = leg label, $2 = arm number(s) to run, $3 = file, $4 = sed expression103
leg () {104
label=$1; arms=$2; file=$3; expr=$4105
legs=$((legs + 1))106
echo107
echo "=========================================================================="108
echo "LEG $legs: $label"109
echo " planting in $file, then requiring gate arm(s) [$arms] to go RED"110
echo "=========================================================================="112
before=$(sha256sum "$file" | cut -d' ' -f1)113
sed -i "$expr" "$file"114
after=$(sha256sum "$file" | cut -d' ' -f1)116
# THE PLANT MUST HAVE LANDED. A sed that matched nothing leaves the file117
# untouched, the gate then passes for the honest reason, and the leg would118
# score as "the gate did not catch it" -- convicting a correct gate. This is119
# the single most important check in the file.120
if [ "$before" = "$after" ]; then121
echo "LEG $legs INCONCLUSIVE: the plant did not change $file (the sed matched nothing)."122
echo " This says NOTHING about the gate. Fix the sed expression."123
bad=$((bad + 1))124
git checkout -- "$file" 2>/dev/null125
return126
fi128
if ! "$sigil" build --redirects "$redirects" > "$work/build.log" 2>&1; then129
# A plant that will not compile is still a valid red for arms that need the130
# tree, but it is a WEAKER result: it proves the build gates the product, not131
# that the gate's assertions discriminate. Say which one happened.132
echo "LEG $legs RED (build): the sabotaged tree does not compile, so the gate cannot run against it."133
echo " Weaker than an assertion red -- it proves the build catches this, not the gate."134
git checkout -- "$file" 2>/dev/null135
"$sigil" build --redirects "$redirects" >/dev/null 2>&1136
return137
fi139
sh test/gate-f2b.sh $arms > "$work/gate.log" 2>&1140
rc=$?141
if [ "$rc" -eq 0 ]; then142
echo "LEG $legs STAYED GREEN -- THIS IS A GATE DEFECT, NOT A PASS."143
echo " The product was broken as described and gate arm(s) [$arms] did not notice."144
grep -E '^(arm|GATE)' "$work/gate.log" | tail -6145
bad=$((bad + 1))146
elif [ "$rc" -eq 3 ]; then147
# SETUP-FAILED IS NOT A RED, and treating it as one is the exact conflation148
# the gate's own header refuses. A gate that could not establish its subject149
# has said nothing about the product, so a leg "passing" on exit 3 would be150
# scoring the gate's inability to run as evidence that it detects defects.151
# Found by an adversarial review.152
echo "LEG $legs INCONCLUSIVE: the gate exited 3 (SETUP-FAILED) -- it could not establish its subject,"153
echo " so this says NOTHING about whether the arm detects the planted defect."154
grep -E 'SETUP-FAILED' "$work/gate.log" | head -2 | sed 's/^/ /'155
bad=$((bad + 1))156
else157
echo "LEG $legs RED as required (gate exit $rc). The failure it reported:"158
grep 'GATE-F2B FAIL' "$work/gate.log" | head -3 | sed 's/^/ /'159
fi161
git checkout -- "$file" 2>/dev/null162
"$sigil" build --redirects "$redirects" >/dev/null 2>&1163
}165
echo "sabotage-f2b: sigil $("$sigil" --version 2>&1 | sed 's/\x1b\[[0-9;]*m//g' | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)"167
# WHICH SHAPE THE LEGS ARE PROVEN AGAINST.168
#169
# `F2B_SHAPE` is read by `gate-f2b.sh`; exporting it here makes every leg's gate170
# invocation test the same shape as this run's banner claims.171
#172
# THE RE-RUN IS NOT CEREMONY. Containment was proven for the DEV shape -- a173
# script under the `sigil` runtime with `-L` paths, which cannot ship. Bundling174
# changes what is spawned, so "the memory cap bites" and "the wall clock bites"175
# are claims about a program that is no longer the program. They have to be176
# re-established, not inherited. The first bundled gate run proved the point by177
# failing an arm that had been passing for a reason that had quietly stopped178
# being true.179
export F2B_SHAPE="${F2B_SHAPE:-dev}"180
echo "sabotage-f2b: SHAPE UNDER TEST = $F2B_SHAPE"182
# THE RUNTIME IS A SHARED, MOVING INPUT. RECORD IT AT BOTH ENDS.183
#184
# `dev-redirects.sgl` resolves the sigil runtime to the sibling `../sigil`185
# CHECKOUT, not to a pin, and other people merge into it. Every leg rebuilds, so186
# a merge landing mid-run means earlier legs linked a different runtime than187
# later ones -- and the transcript would look perfectly normal.188
#189
# That happened: a run straddled cb8d563f -> 2e9d23c0 (three commits, one of them190
# a GC hot-path perf change) and was only detectable because the sha had been191
# captured by hand outside the suite. The whole set had to be discarded.192
#193
# So the suite now samples the sha at start AND end and says plainly whether its194
# own results are attributable. A run that cannot attribute itself should not be195
# quoted, and it now says so in its own output rather than in someone's shell196
# history.197
# EVERY redirect target, not just `../sigil`.198
#199
# The first version of this check watched the sigil runtime alone, and that was200
# luck rather than care: `../sigil-doc` -- the PARSER this whole phase depends on201
# -- moved nine commits including a D8 feature change on the same afternoon, and202
# nothing here would have noticed. It happened to land 15 minutes before a run203
# rather than during one.204
#205
# So the fingerprint is derived from `dev-redirects.sgl` itself: every206
# `from-path` target that is a git checkout is sampled. Deriving it means a207
# redirect added later is covered without anyone remembering to add it here,208
# which is the failure mode a hand-listed pair would have.209
redirect_fingerprint () {210
for d in $(sed -n 's/.*from-path dir: "\([^"]*\)".*/\1/p' "$redirects" | sort -u); do211
case "$d" in212
/*) target="$d" ;;213
*) target="$repo_dir/$d" ;;214
esac215
if [ -d "$target/.git" ] || [ -f "$target/.git" ]; then216
printf '%s=%s\n' "$(basename "$target")" \217
"$(git -C "$target" rev-parse --short HEAD 2>/dev/null || echo UNKNOWN)"218
fi219
done220
}221
runtime_sha_start=$(redirect_fingerprint | tr '\n' ' ')222
[ -n "$runtime_sha_start" ] || { echo "SETUP-FAILED: no redirect targets resolved from $redirects; the attribution check would compare two empty strings and always pass" >&2; exit 3; }223
echo "sabotage-f2b: redirect targets (START) = $runtime_sha_start"225
# ---------------------------------------------------------------------------226
# PREFLIGHT: EVERY ARM MUST PASS ALONE ON A CLEAN TREE.227
#228
# A PERMANENT PROPERTY OF THIS SUITE, not a one-off repair.229
#230
# Each leg below invokes a SUBSET of the gate's arms and reads a non-zero exit as231
# "the plant was detected". That inference is only valid if the selected arms232
# pass on an unsabotaged tree. They did not: arms 2, 3 and 10 greped files that233
# only arms 1 and 5 created, so `gate-f2b.sh 3` failed on a clean tree and legs234
# 1, 2 and 8 reported "RED as required" -- with plausible `GATE-F2B FAIL` text --235
# WHETHER OR NOT THEIR PLANT DID ANYTHING. Three legs of void evidence that read236
# exactly like three legs of good evidence.237
#238
# This preflight makes that class impossible to reintroduce: if an arm ever again239
# depends on another arm's leftovers, the suite refuses to run rather than240
# reporting confident nonsense. It is checked for every arm any leg uses, not241
# just the three that were broken.242
# ---------------------------------------------------------------------------243
echo244
echo "PREFLIGHT: every arm used by a leg must pass ALONE on this clean tree"245
#246
# The list is DERIVED FROM THE LEG CALLS rather than written out, so adding a leg247
# that uses a new arm automatically extends the preflight. A hand-maintained248
# list would drift, and the drift would be silent in exactly the direction that249
# matters. If the derivation ever yields nothing, that is SETUP-FAILED, not an250
# empty-and-therefore-passing loop -- a for-loop over nothing succeeds, which is251
# how a preflight becomes decorative.252
preflight_arms=$(grep '^leg "' "$0" \253
| sed 's/^leg "[^"]*"[[:space:]]*//; s/[[:space:]]*\\$//; s/"//g' \254
| tr ' ' '\n' | grep -E '^[0-9]+$' | sort -un)255
[ -n "$preflight_arms" ] || { echo "SETUP-FAILED: could not derive the arm list from this script's leg calls; the preflight would silently check nothing" >&2; exit 3; }256
echo " arms used by legs: $(echo "$preflight_arms" | tr '\n' ' ')"257
pref_bad=0258
for a in $preflight_arms; do259
if sh test/gate-f2b.sh "$a" > "$work/pref-$a.log" 2>&1; then260
echo " arm $a alone on a clean tree: PASS"261
else262
echo " arm $a alone on a clean tree: FAIL -- this arm cannot produce its own subject," >&2263
echo " so any leg invoking it would report RED regardless of its plant." >&2264
grep -E 'GATE-F2B (FAIL|SETUP-FAILED)' "$work/pref-$a.log" | head -3 | sed 's/^/ /' >&2265
pref_bad=$((pref_bad + 1))266
fi267
done268
if [ "$pref_bad" -ne 0 ]; then269
echo "SABOTAGE-F2B ABORTED: $pref_bad arm(s) fail on a clean tree. No leg below would mean anything." >&2270
exit 3271
fi272
echo "PREFLIGHT OK: all arms used by legs pass standalone"274
# --- LEG 1 -----------------------------------------------------------------275
# F2's sabotage list, verbatim: "silently prefer `.sgd` duplicate over `.md`".276
# Make `find-collisions` never report a collision, which is exactly what277
# silently preferring one side looks like from the outside.278
leg "discovery silently swallows the .sgd/.md collision" 3 \279
src/folio/doc/discovery.sgl \280
's/(if (> (length group) 1)/(if (> (length group) 99999)/'282
# --- LEG 2 -----------------------------------------------------------------283
# Conflate "not seen" with "nothing to see": claim completeness unconditionally.284
# This is the defect that makes a scan of one readable directory out of nine285
# report "0 skipped" and read as exhaustive.286
leg "discovery calls every census COMPLETE, hiding unreadable directories" 2 \287
src/folio/doc/discovery.sgl \288
's/(null? (census-record-unreadable-dirs c)))/(null? (list)))/'290
# --- LEG 3 -----------------------------------------------------------------291
# Remove the memory cap from the worker shell -- F2 clause 2's named sabotage,292
# "remove the bounds -> the arm must go red by observing worker death contained,293
# not service death".294
leg "the bounded worker stops applying its memory cap" "4 5" \295
src/folio/doc/bounded.sgl \296
's/ulimit -v \\"\$cap\\" || exit 201; //'298
# --- LEG 4 -----------------------------------------------------------------299
# THE TRUST-BOUNDARY DEFECT ITSELF: treat `parse-error-unlocated` as a broken300
# document. This is the one that would mark healthy notes read-only whenever the301
# host is under memory pressure.302
leg "parse-error-unlocated is treated as a broken document" 7 \303
src/folio/doc/bounded.sgl \304
's/(unlocated? (pair? unlocated-lines))/(unlocated? #f)/'306
# --- LEG 5 -----------------------------------------------------------------307
# Drop the completion-marker requirement, so a truncated worker run scores as a308
# clean parse. This is the free-pass absence claim in its purest form.309
leg "a truncated worker run is accepted as a clean parse" 6 \310
src/folio/doc/bounded.sgl \311
's/((not done?)/((and #f (not done?))/'313
# --- LEG 6 -----------------------------------------------------------------314
# Emit uppercase hex. `valid-revision?` refuses uppercase precisely because a315
# case difference between two clients is a stale-write check that fails open on316
# some clients and not others -- so this must not survive the conformance arm.317
leg "revisions are emitted in uppercase hex" 8 \318
src/folio/doc/revision.sgl \319
's/(char-downcase (string-ref s i))/(char-upcase (string-ref s i))/'321
# --- LEG 7 -----------------------------------------------------------------322
# Report Markdown notes as `indexed` -- claiming the .sgd inspector successfully323
# inspected a file it never looked at. The counts stay internally consistent and324
# the denominator still balances; the population is simply described wrongly,325
# which is the version of this defect that survives review.326
#327
# (An earlier spelling of this leg wrapped the map in a filter and did not328
# compile, which yields only a BUILD red -- weaker evidence, because it proves the329
# compiler catches it rather than that the gate discriminates.)330
leg "markdown notes are reported as successfully indexed" 5 \331
src/folio/doc/service.sgl \332
"s/status: 'not-inspectable/status: 'indexed/"334
# --- LEG 8 -----------------------------------------------------------------335
# Collapse the workspace-level duplicate-id report, so two notes carrying one id336
# are silently tolerated.337
leg "workspace-level duplicate ids are not reported" 10 \338
src/folio/doc/service.sgl \339
's/(if (> (length paths) 1)/(if (> (length paths) 99999)/'341
# --- LEG 9 -----------------------------------------------------------------342
# Widen the memory-signature search back over ALL of stdout, which is343
# DOCUMENT-CONTROLLED. This is the defect an adversarial pass found in the first344
# version of this module: a healthy note whose task id is the string345
# "out of memory" is then classified unindexed/memory-cap and never indexed.346
#347
# Arm 7 gained a probe for exactly this, so the leg has something to hit.348
leg "the memory-signature search reads document-controlled output" 7 \349
src/folio/doc/bounded.sgl \350
's/(memory? (or (memory-signature? se)/(memory? (or (memory-signature? so) (memory-signature? se)/'352
# --- LEG 10 ----------------------------------------------------------------353
# Treat the completion marker as a SUBSTRING rather than a whole line, so a354
# document carrying the marker text in a node id can certify its own truncated355
# run. This is a DIFFERENT defect from leg 5: leg 5 removes the marker rule356
# entirely, this one leaves the rule in place and makes it forgeable by document357
# content. Arm 6's forgery probe is what distinguishes them.358
leg "the completion marker is matched as a substring, so a document can forge it" 6 \359
src/folio/doc/bounded.sgl \360
's/(has-exact-line? lines "F2B_WORKER_DONE")))/(contains? so "F2B_WORKER_DONE")))/'362
# ---------------------------------------------------------------------------363
restore_all364
"$sigil" build --redirects "$redirects" >/dev/null 2>&1366
echo367
echo "=========================================================================="368
echo "RESTORE VERIFICATION (by hash, not by git reporting success)"369
restore_bad=0370
for f in $TOUCHED; do371
want=$(cat "$base/$(echo "$f" | tr / _).base")372
got=$(sha256sum "$f" | cut -d' ' -f1)373
if [ "$want" = "$got" ]; then374
echo " OK $f"375
else376
echo " DIRTY $f -- expected $want got $got"377
restore_bad=$((restore_bad + 1))378
fi379
done381
echo "=========================================================================="382
# ATTRIBUTION CHECK -- does this run describe one runtime, or a boundary?383
runtime_sha_end=$(redirect_fingerprint | tr "\n" " ")384
echo "sabotage-f2b: redirect targets (END) = $runtime_sha_end"385
straddled=0386
if [ "$runtime_sha_start" != "$runtime_sha_end" ]; then387
straddled=1388
echo "=========================================================================="389
echo "SABOTAGE-F2B UNATTRIBUTABLE: a redirected dependency MOVED during this run."390
echo " start: $runtime_sha_start"391
echo " end: $runtime_sha_end"392
echo "Every leg rebuilds, so earlier legs linked a different runtime than later"393
echo "ones. These results describe a BOUNDARY, not a build. DO NOT QUOTE THEM --"394
echo "re-run against a held sha. This is not a product failure."395
fi397
echo "=========================================================================="398
if [ "$straddled" -ne 0 ]; then399
echo "SABOTAGE-F2B: results discarded (runtime moved mid-run); $bad leg(s) failed to go red before that."400
exit 2401
elif [ "$bad" -eq 0 ] && [ "$restore_bad" -eq 0 ]; then402
echo "SABOTAGE-F2B: all $legs legs went RED as required; tree restored clean."403
echo " attributable to: $runtime_sha_start (shape $F2B_SHAPE)"404
exit 0405
else406
echo "SABOTAGE-F2B: $bad leg(s) failed to go red, $restore_bad file(s) not restored."407
echo "A leg that stayed green is a GATE DEFECT and must be fixed before the gate's green means anything."408
exit 1409
fi