AtlatestRepositoryfolio
1
#!/usr/bin/env bash2
#3
# F2a -- the gate for the passive inspector. THIS IS A TRUST BOUNDARY GATE.4
#5
# READ THIS BEFORE EDITING AN ASSERTION.6
#7
# Every count is DERIVED FROM THE ARTIFACT it describes. Adding a test, a record8
# or a node kind must not require editing a number here; if it does, the check9
# has stopped describing its subject and become a second thing to keep in sync.10
#11
# Every ABSENCE assertion carries a POSITIVE CONTROL. That matters more here12
# than in most gates, because this phase's headline properties are BOTH13
# absences: "no author code ran" and "no forbidden module is reachable". An14
# all-empty result from four broken channels looks exactly like an all-empty15
# result from four working ones, so the controls run in the SAME ARM, on EVERY16
# run, not once when they were written.17
#18
# THE TWO LOAD-BEARING INSTRUMENTS ARE NOT WRITTEN IN SIGIL, AND THAT IS THE19
# POINT. `test/f2a-span-verify.py` opens the manuscript in binary mode and20
# recomputes every offset with Python's own codec; `test/f2a-import-closure.py`21
# walks the module graph from the filesystem. A span checker written against22
# the descriptor, in the descriptor's language, using the descriptor's23
# arithmetic, agrees with itself no matter what the code does -- taxonomy shape24
# 13, instruments with a shared blind spot are one measurement in several hats.25
#26
# Positive controls plant into `$(mktemp -d)` copies, never into the working27
# tree, so there is no restore step that has to be reached.28
set -euo pipefail30
repo_dir="$(cd "$(dirname "$0")/.." && pwd)"31
cd "$repo_dir"33
sigil="${SIGIL_F2A_COMPILER:-$repo_dir/../task-sigil-language-readers/build/dev/bin/sigil}"34
redirects="${SIGIL_F2A_REDIRECTS:-$repo_dir/dev-redirects.sgl}"35
sigil_doc_dir="${SIGIL_F2A_DOC_DIR:-$repo_dir/../sigil-doc}"36
stdlib_dir="${SIGIL_F2A_STDLIB_DIR:-$repo_dir/../sigil/packages/sigil-stdlib/src}"37
cache_dir="${XDG_CACHE_HOME:-$(mktemp -d)}"38
export XDG_CACHE_HOME="$cache_dir"39
# A DIRECT script run has no `--redirects` flag and reads the environment.40
export SIGIL_REDIRECTS="$redirects"42
work=$(mktemp -d)43
fail() { echo "F2A GATE RED: $*" >&2; exit 1; }45
# SHARED HELPER, DEFINED AT TOP LEVEL AND NOT INSIDE AN ARM.46
#47
# It lived inside arm 5 and arm 6 called it. A full run was green because arm 548
# always executes first and defines it -- an ORDER dependency nothing declared49
# and nothing checked, and the reason arm 6 had never been demonstrated able to50
# go red. Found by the sabotage driver's S4 leg, which requires a NAMED51
# signature: the leg went red with `diag_of: command not found` instead of the52
# assertion it was testing, which is a red for the wrong reason and exactly as53
# uninformative as a green for the wrong reason.54
diag_of() { # $1 fixture, $2 output path -> writes the manifest for it55
local fx="$1" out="$2"56
local rev="sha256:$(sha256sum "$fx" | cut -d' ' -f1)"57
# stderr is kept OUT of the manifest. Merged in, a compiler warning became a58
# manifest row and died inside the verifier as "unknown manifest row tag" -- a59
# confusing red, and inconsistent with arm 3, which separates them.60
F2A_MANIFEST_SOURCE="$fx" F2A_MANIFEST_REVISION="$rev" \61
"$sigil" -L build/dev/lib -L src -L test test/f2a-span-manifest.sgl > "$out" 2>"$out.err"62
}64
# ARM SELECTOR -- for the sabotage driver only.65
#66
# The arms overlap on purpose: the suite and the manifest assert the duplicate-id67
# contract through different instruments, and both see a byte/scalar defect. That68
# redundancy is a feature in a real run and a problem in a sabotage run, because69
# whichever arm fires FIRST hides whether the arm being tested can fire at all.70
# `F2A_GATE_ONLY_ARM=<n>` runs arm 1 (the build, which every later arm needs) plus71
# arm <n>, so a leg proves the arm it names.72
#73
# IT CANNOT BE USED TO FAKE A GREEN: a selective run prints74
# `F2A_GATE_ARM_<n>_GREEN`, never `F2A_GATE_GREEN`. The full marker is only75
# reachable when no selector is set, and every transcript is graded on that76
# marker.77
only_arm="${F2A_GATE_ONLY_ARM:-}"78
case "$only_arm" in79
""|1|2|3|4|5|6|7|8) : ;;80
*) fail "F2A_GATE_ONLY_ARM=$only_arm is not an arm this gate has" ;;81
esac82
should_run() { [ -z "$only_arm" ] || [ "$only_arm" = "$1" ]; }83
plain() { sed 's/\x1b\[[0-9;]*m//g' "$1"; }85
# EVERY EXTERNAL TOOL MUST EXIST. A missing tool degrades an assertion into a86
# weaker one that cannot fail, silently.87
for tool in sha256sum grep sed awk sort comm; do88
command -v "$tool" >/dev/null || fail "$tool is required by this gate and is not on PATH"89
done91
# PYTHON IS RESOLVED AT THE HOP, NOT ASSUMED. Measured on 2026-08-11: `python3`92
# was on PATH for the first several runs of this gate and then was NOT, mid93
# session, with no action from this worker -- `which python3` went from a hit to94
# `no python3 in (...)`. The two load-bearing instruments in this gate are95
# Python programs, so a PATH that moves underneath it turns the strongest arms96
# into "could not look". The gate refused loudly rather than skipping them,97
# which is the correct behaviour and also the reason this block exists.98
#99
# Resolution order, and the CHOSEN RUNNER IS PRINTED IN THE HEADER: an100
# interpreter picked by fallback is an input to every result below, and an101
# unrecorded input is how two runs of "the same gate" stop being the same gate.102
if command -v python3 >/dev/null 2>&1; then103
PY=(python3)104
py_source="PATH"105
elif command -v guix >/dev/null 2>&1 && guix shell python -- python3 --version >/dev/null 2>&1; then106
PY=(guix shell python -- python3)107
py_source="guix shell python"108
else109
fail "no python3 available, on PATH or via 'guix shell python'; the span verifier and the closure walker cannot run, and a gate that cannot run its own instruments is not a gate"110
fi111
[ -x "$sigil" ] || fail "compiler not found at $sigil"112
for f in src/folio/doc/inspect.sgl test/test-folio-doc-inspect.sgl \113
test/f2a-span-manifest.sgl test/f2a-span-verify.py \114
test/f2a-witness-probe.sgl test/f2a-import-closure.py \115
test/fixtures/f2a-corpus.sgd test/fixtures/f2a-witness.sgd \116
test/fixtures/f2a-duplicate.sgd test/fixtures/f2a-malformed.sgd \117
test/fixtures/f2a-no-header.sgd test/fixtures/f2a-bom.sgd \118
test/fixtures/f2a-shebang.sgd test/fixtures/f2a-reader-oom.sgd \119
test/fixtures/f2a-argument-edges.sgd; do120
[ -f "$f" ] || fail "$f is missing"121
done123
echo "=== F2a gate ==============================================="124
echo "repo : $repo_dir @ $(git rev-parse HEAD)"125
echo "compiler : $(plain <($sigil --version 2>&1) | tail -1)"126
# THE SIGIL-DOC SHA IS EVIDENCE, NOT DECORATION. Everything below is produced127
# through a dev redirect to a local sigil-doc WORKING TREE, so its commit is an128
# input to every result here and an uncommitted change there is an unrecorded129
# one. F1a learned this; the difference for F2a is that its evidence DOES route130
# through the parser, because this phase consumes a real parse API rather than131
# constructing nodes programmatically.132
sigil_doc_sha=$(git -C "$sigil_doc_dir" rev-parse HEAD)133
sigil_doc_tag=$(git -C "$sigil_doc_dir" describe --tags --exact-match HEAD 2>/dev/null || echo "(untagged)")134
sigil_doc_dirty=$(git -C "$sigil_doc_dir" status --porcelain | wc -l)135
echo "sigil-doc : $sigil_doc_sha $sigil_doc_tag (via dev-redirects)"136
echo "sigil-doc dirty : $sigil_doc_dirty files"137
echo "python3 : $("${PY[@]}" --version 2>&1 | tail -1) (via $py_source)"138
# EVERY RESOLVED INPUT, NOT JUST THE VERSION STRINGS. All five of these are139
# `${VAR:-default}`, so an exported SIGIL_F2A_COMPILER pointing at a different140
# binary of the same version, or a different stdlib root, produced a transcript141
# indistinguishable from a canonical run while grading different artefacts.142
echo "compiler path : $sigil"143
echo "redirects : $redirects"144
echo "sigil-doc dir : $sigil_doc_dir"145
echo "stdlib root : $stdlib_dir"146
echo "XDG_CACHE_HOME : $XDG_CACHE_HOME"147
echo "============================================================"148
if [ "$sigil_doc_dirty" != "0" ]; then149
git -C "$sigil_doc_dir" status --porcelain >&2150
fail "sigil-doc has $sigil_doc_dirty uncommitted file(s); evidence produced against it is not reproducible"151
fi153
# ---------------------------------------------------------------------------154
# ARM 1 -- the product builds. Asserted on the ARTIFACT, because this command155
# is usually piped and a pipeline reports its last stage.156
# ---------------------------------------------------------------------------157
echo "--- arm 1: build"158
build_log="$work/build.log"159
if ! "$sigil" build --redirects "$redirects" > "$build_log" 2>&1; then160
tail -30 "$build_log" >&2; fail "build exited non-zero"161
fi162
plain "$build_log" | grep -q "Bundle created" || { tail -30 "$build_log" >&2; fail "build produced no bundle"; }163
modules=$(plain "$build_log" | grep -oE '^ +[0-9]+ modules' | grep -oE '[0-9]+' | head -1 || true)164
[ -n "$modules" ] || fail "build log carried no module count"165
echo "arm 1 OK: bundle built, ${modules} modules"167
# ---------------------------------------------------------------------------168
# ARM 2 -- the inspector suite, with its denominator taken from the suite file.169
# A suite that silently stopped running half its cases still prints a green170
# summary; comparing the reported count against the file's own `(test "` forms171
# is what makes green mean "all of them ran".172
# ---------------------------------------------------------------------------173
if should_run 2; then174
echo "--- arm 2: inspector suite"175
suite=test/test-folio-doc-inspect.sgl176
expected_tests=$(grep -cE '^\(test "' "$suite")177
[ "$expected_tests" -ge 25 ] || fail "suite declares only $expected_tests tests"178
suite_log="$work/suite.log"179
if ! "$sigil" -L build/dev/lib test --redirects "$redirects" "$suite" > "$suite_log" 2>&1; then180
tail -40 "$suite_log" >&2; fail "inspector suite exited non-zero"181
fi182
reported=$(plain "$suite_log" | grep -oE 'Tests: +[0-9]+ passed' | grep -oE '[0-9]+' | head -1 || true)183
[ -n "$reported" ] || { tail -20 "$suite_log" >&2; fail "suite printed no test count"; }184
[ "$reported" = "$expected_tests" ] || fail "suite defines $expected_tests tests but $reported ran/passed"185
if plain "$suite_log" | grep -qE '[1-9][0-9]* failed'; then186
plain "$suite_log" | grep -E '[1-9][0-9]* failed' >&2187
fail "inspector suite reported failures"188
fi189
# POSITIVE CONTROL for that absence: the pattern must match a real failure line.190
printf 'Tests: 28 passed, 1 failed (29 total)\n' > "$work/failctl"191
grep -qE '[1-9][0-9]* failed' "$work/failctl" \192
|| fail "positive control: the failure pattern does not match the runner's real failure summary"193
echo "arm 2 OK: $reported/$expected_tests tests passed, denominator derived from $suite"194
fi196
# ---------------------------------------------------------------------------197
# ARM 3 -- SPANS, VERIFIED AGAINST THE FILE'S BYTES BY A DIFFERENT LANGUAGE.198
#199
# The revision handed to the inspector is computed by `sha256sum` -- coreutils,200
# a completely separate implementation from anything in this repository or in201
# the sigil toolchain. That is deliberate: the revision contract is "sha256202
# over the raw file bytes", it will have at least two implementations (native203
# and WASM), and the only way to keep them honest is to check against a third.204
# ---------------------------------------------------------------------------205
if should_run 3; then206
echo "--- arm 3: byte-exact spans"207
corpus=test/fixtures/f2a-corpus.sgd208
revision="sha256:$(sha256sum "$corpus" | cut -d' ' -f1)"209
manifest="$work/corpus-manifest.txt"210
if ! F2A_MANIFEST_SOURCE="$corpus" F2A_MANIFEST_REVISION="$revision" \211
"$sigil" -L build/dev/lib -L src -L test test/f2a-span-manifest.sgl > "$manifest" 2>"$work/manifest.err"; then212
tail -20 "$work/manifest.err" >&2; fail "span manifest probe exited non-zero"213
fi214
grep -q '^F2A_MANIFEST_DONE ' "$manifest" || fail "manifest has no trailer; it is truncated"215
# The descriptor must have carried the revision coreutils computed, unchanged.216
grep -qF "$revision" "$manifest" \217
|| fail "the manifest does not carry the sha256sum-computed revision"219
verify_log="$work/verify.log"220
if ! "${PY[@]}" test/f2a-span-verify.py "$corpus" "$manifest" > "$verify_log" 2>&1; then221
cat "$verify_log" >&2; fail "byte verifier rejected the corpus spans"222
fi223
cat "$verify_log"224
span_line=$(grep -oE 'F2A_SPAN_VERIFY nodes=[0-9]+ fields=[0-9]+ divergent=[0-9]+ divfields=[0-9]+ checks=[0-9]+ rows=[0-9]+ failed=0' "$verify_log") \225
|| { cat "$verify_log" >&2; fail "byte verifier printed no result line"; }226
# EXACT KEY MATCH, NOT SUBSTRING. `grep -oE 'fields=[0-9]+'` also matches227
# inside `divfields=43`, which produced a two-line value and a shell error228
# reading "integer expression expected" -- it failed safe, but the same229
# collision in a comparison would have compared against the wrong number.230
field_of() { echo "$1" | tr ' ' '\n' | grep "^$2=" | cut -d= -f2; }231
span_nodes=$(field_of "$span_line" nodes)232
span_fields=$(field_of "$span_line" fields)233
span_divergent=$(field_of "$span_line" divergent)234
span_checks=$(field_of "$span_line" checks)235
span_rows=$(field_of "$span_line" rows)236
# The verifier already refuses when rows_verified != rows parsed. This is the237
# denominator restated in the GATE transcript, where a later reader will look.238
[ "$span_rows" = "$((span_nodes + span_fields))" ] \239
|| fail "verifier completed $span_rows rows but the manifest carried $((span_nodes + span_fields))"240
[ "$span_nodes" -ge 40 ] || fail "byte verifier saw only $span_nodes nodes"241
[ "$span_fields" -ge 20 ] || fail "byte verifier saw only $span_fields fields"242
# NON-VACUITY, and it is not optional. Without multi-byte text before the243
# nodes, byte offset == scalar offset everywhere and every byte-versus-scalar244
# assertion in the verifier passes for free, however broken the arithmetic is.245
[ "$span_divergent" -ge 10 ] \246
|| fail "only $span_divergent span(s) have byte != scalar; the corpus cannot tell byte spans from character spans"248
# THE VERIFIER MUST BE ABLE TO GO RED, PROVEN ON THIS RUN, ON THIS INPUT.249
if F2A_SPAN_VERIFY_SELFTEST=1 "${PY[@]}" test/f2a-span-verify.py "$corpus" "$manifest" > "$work/verify-self.log" 2>&1; then250
cat "$work/verify-self.log" >&2251
fail "byte verifier EXITED 0 with a span deliberately corrupted -- it cannot go red"252
fi253
grep -q 'failed=' "$work/verify-self.log" || grep -q 'FAIL:' "$work/verify-self.log" \254
|| fail "byte verifier self-test failed for the wrong reason"255
echo "arm 3 OK: $span_checks byte-level checks over $span_nodes nodes and $span_fields fields; $span_divergent divergent; verifier proven able to fail"256
fi258
# ---------------------------------------------------------------------------259
# ARM 4 -- THE NO-EXECUTION WITNESS.260
#261
# FOUR CHANNELS, chosen because they FAIL DIFFERENTLY. SD-P4 already ships a262
# witness (gate-sd-p4.sh arm 2) and it is stdout-marker only: by construction it263
# cannot see execution that does not print. A witness inherited from an upstream264
# phase covers the upstream phase's risk surface, not this one's.265
#266
# 1. stdout markers -- blind to silent side effects267
# 2. filesystem sentinels -- blind to nothing that writes, sees the rest268
# 3. an ARGUMENT-POSITION attempt -- the shape this phase adds. Extracting269
# per-field values out of argument syntax is the only way F2a could newly270
# execute author code, and SD-P4's fixture contains no such attempt.271
# 4. the transitive import closure (arm 8) -- static, not runtime at all272
#273
# (a) POSITIVE CONTROL FIRST, EVERY RUN. Until the same manuscript evaluated274
# through the real `#lang` path trips every tripwire, the absence assertions275
# below mean nothing whatsoever.276
# ---------------------------------------------------------------------------277
if should_run 4; then278
echo "--- arm 4: no-execution witness"279
witness=test/fixtures/f2a-witness.sgd280
control_dir="$work/witness-control"281
passive_dir="$work/witness-passive"282
mkdir -p "$control_dir" "$passive_dir"284
control_log="$work/witness-control.log"285
F2A_WITNESS_DIR="$control_dir" "$sigil" -L build/dev/lib eval -f "$witness" > "$control_log" 2>&1 || true286
# THE POSITION CLASSES ARE DERIVED FROM THE PARSER'S GRAMMAR, NOT COLLECTED BY287
# EXHAUSTION. A manuscript offers exactly five positions at which author code288
# can sit: block boundary, bare-prose run (non-boundary -- SD-P3's fold),289
# argument group, brace body, and a @code raw body. The fixture now attempts290
# execution at all five, crossed with the attempt kinds (expression,291
# author-defined command, macro). That is what makes the set closed BY292
# CONSTRUCTION rather than "the shapes we thought of", which is the difference293
# between an answer and a bigger sample. The mid-prose pair was the missing294
# one; Fable named it, and it was a witness-fixture gap rather than a product295
# defect because the passive tree is flat and reports those nodes as ordinary296
# siblings -- but nothing had ever tripped there under evaluation.297
for marker in F2A-WITNESS-EXPR-RAN F2A-WITNESS-COMMAND-RAN F2A-WITNESS-ARG-RAN \298
F2A-WITNESS-MACRO-RAN F2A-WITNESS-MIDPROSE-RAN \299
F2A-WITNESS-MIDPROSE-COMMAND-RAN; do300
plain "$control_log" | grep -qF "$marker" \301
|| { cat "$control_log" >&2; fail "positive control: evaluating the fixture did not print $marker -- channel 1/3 cannot fire, so its absence proves nothing"; }302
done303
for sentinel in expr command argument macro midprose midprose-command; do304
[ -f "$control_dir/f2a-witness-$sentinel" ] \305
|| { ls -la "$control_dir" >&2; fail "positive control: evaluating the fixture did not write the '$sentinel' sentinel -- channel 2 cannot fire"; }306
done308
# (b) THE ASSERTION. Passive inspection, in a fresh process, must trip none of309
# it -- and must PROVE it reached the dangerous path while not tripping it.310
passive_log="$work/witness-passive.log"311
if ! F2A_WITNESS_DIR="$passive_dir" F2A_WITNESS_FIXTURE="$witness" \312
"$sigil" -L build/dev/lib -L src -L test test/f2a-witness-probe.sgl > "$passive_log" 2>&1; then313
cat "$passive_log" >&2; fail "witness probe exited non-zero"314
fi315
probe_line=$(plain "$passive_log" | grep -oE 'F2A_WITNESS_PROBE_INSPECTED nodes=[0-9]+ commands=[0-9]+ author-expressions=[0-9]+ fields=[0-9]+') \316
|| { cat "$passive_log" >&2; fail "witness probe printed no coverage line"; }317
for k in nodes commands author-expressions fields; do318
v=$(echo "$probe_line" | tr ' ' '\n' | grep "^$k=" | cut -d= -f2)319
[ "$v" -gt 0 ] || fail "witness probe reported $k=0 -- it inspected nothing, so 'no execution observed' is meaningless"320
done321
# THE DANGEROUS PATH WAS REACHED. Channel 3 is only a channel if the field whose322
# value is an expression was actually extracted; a witness that observed no323
# execution because it never looked is a control too small to fail.324
arg_field=$(plain "$passive_log" | grep -oE '^F2A_WITNESS_PROBE_ARGUMENT_FIELD head=[a-z-]+ length=-?[0-9]+') \325
|| { cat "$passive_log" >&2; fail "witness probe printed no argument-field line -- channel 3 was never exercised"; }326
echo "$arg_field" | grep -q 'head=begin' \327
|| fail "channel 3: the argument-position field was not read as the begin-form; got '$arg_field'"328
arg_len=$(echo "$arg_field" | grep -oE 'length=-?[0-9]+' | grep -oE -- '-?[0-9]+')329
[ "$arg_len" -ge 3 ] || fail "channel 3: the argument-position datum has length $arg_len; it is not the expression the fixture writes"330
# NOTE FOR ANYONE TIGHTENING THIS: do not make the probe print the datum ITSELF.331
# That datum contains the fixture's own stdout marker as a string literal, so332
# echoing it would put "F2A-WITNESS-ARG-RAN" on the stream the detector below333
# greps, and this arm would go red on a manuscript nothing executed.334
# ...and now the absences.335
if plain "$passive_log" | grep -q 'F2A-WITNESS-'; then336
cat "$passive_log" >&2; fail "AUTHOR CODE EXECUTED DURING PASSIVE INSPECTION"337
fi338
sentinels=$(find "$passive_dir" -type f | wc -l)339
[ "$sentinels" = "0" ] || { ls -la "$passive_dir" >&2; fail "passive inspection wrote $sentinels sentinel file(s) -- author code ran"; }340
# A `@code` body must stay inert on the CONTROL path too -- SD-P4's finding,341
# and the reason it is worth asserting separately: everything else in this arm342
# requires the control to RUN author code, so this is the one place the control343
# is checked for restraint rather than for firing.344
#345
# The passive side is deliberately NOT re-checked here. The blanket346
# `grep -q 'F2A-WITNESS-'` above already refuses every marker in the passive347
# log, including this one, so a second clause could never fire -- an assertion348
# shadowed by an earlier one in the same arm, which is a class this gate has349
# been bitten by three times.350
if plain "$control_log" | grep -q 'F2A-WITNESS-CODE-BODY-RAN'; then351
fail "a @code body was evaluated on the control path"352
fi353
# WHAT THIS ARM ACTUALLY ESTABLISHES, at the resolution it establishes it.354
# Two DETECTION channels (stdout markers, filesystem sentinels), each shown355
# able to fire in the same run by the control; plus a COVERAGE assertion that356
# the argument-position extraction ran and read its expression as data. The357
# third of those is not a detector -- execution in argument position is358
# detected by the first two -- and saying "3 channels proven able to fire"359
# overstated it by one. The static closure arm is arm 8, not this one.360
echo "arm 4 OK: 2 detection channels proven able to fire in this run, across all 5 grammar-derived positions; inspection tripped neither; argument-position extraction exercised and read as data"361
fi363
# ---------------------------------------------------------------------------364
# ARM 5 -- DIAGNOSTICS, and the reader-pathology regression.365
# ---------------------------------------------------------------------------366
if should_run 5; then367
echo "--- arm 5: diagnostics"368
for pair in "f2a-malformed.sgd parse-error" "f2a-no-header.sgd no-language-header"; do369
set -- $pair370
fx="test/fixtures/$1"; code="$2"371
diag_of "$fx" "$work/$1.manifest" || fail "manifest probe failed on $fx"372
grep -qE "^DIAGNOSTIC error $code " "$work/$1.manifest" \373
|| { cat "$work/$1.manifest" >&2; fail "$fx did not produce an error diagnostic with code $code"; }374
# A DIAGNOSTIC MUST NAME ITS FILE AND ITS POSITION. `file:?:?:` is what a375
# positionless one renders as, and it is refused here rather than accepted as376
# "well, there is a diagnostic".377
grep -E "^DIAGNOSTIC error $code " "$work/$1.manifest" | grep -qE "$1:[0-9]+:[0-9]+: error: $code: " \378
|| { grep '^DIAGNOSTIC' "$work/$1.manifest" >&2; fail "$fx's diagnostic does not name its file and position"; }379
done380
# POSITIVE CONTROL for that rendering pattern: it must NOT match a positionless381
# diagnostic, or "located" is not what is being asserted.382
if printf 'x.sgd:?:?: error: parse-error: boom\n' | grep -qE "x.sgd:[0-9]+:[0-9]+: error: parse-error: "; then383
fail "the located-diagnostic pattern accepts a positionless diagnostic; it is not checking location"384
fi385
# Clean documents must produce NO diagnostics -- including the two that exercise386
# the byte/scalar divergence at the very first node.387
for fx in f2a-bom.sgd f2a-shebang.sgd f2a-corpus.sgd; do388
diag_of "test/fixtures/$fx" "$work/$fx.manifest" || fail "manifest probe failed on $fx"389
grep -q '^PARSED yes' "$work/$fx.manifest" || fail "$fx did not parse"390
if grep -q '^DIAGNOSTIC' "$work/$fx.manifest"; then391
grep '^DIAGNOSTIC' "$work/$fx.manifest" >&2; fail "$fx is a clean manuscript but produced diagnostics"392
fi393
"${PY[@]}" test/f2a-span-verify.py "test/fixtures/$fx" "$work/$fx.manifest" > "$work/$fx.verify" 2>&1 \394
|| { cat "$work/$fx.verify" >&2; fail "byte verifier rejected $fx's spans"; }395
done396
# The BOM file's FIRST node must already diverge -- it is the smallest possible397
# manuscript that can tell the two coordinate systems apart.398
# REPORTED, NOT ASSERTED. The verifier itself dies when divergent < 1, so399
# reaching this line already guarantees >= 1 and an assertion here could not400
# fail on any input. The number is worth having in the transcript; dressing it401
# as a check was not.402
bom_div=$(grep -oE 'divergent=[0-9]+' "$work/f2a-bom.sgd.verify" | grep -oE '[0-9]+')403
echo " (BOM fixture: $bom_div divergent node span(s) -- byte != scalar at the very first node)"405
# ---- the reader pathology, t-f6e098 ---------------------------------------406
# THIS ARM CANNOT SKIP. The fix for `(}` shipped in sigil 0.18.5; below that the407
# reader still fatal-OOMs, so the arm asserts the state its toolchain is actually408
# in and says which one that was. A version-gated arm that SKIPPED would report409
# green for the same reason a missing tool does.410
#411
# It runs under a hard address-space cap, because without one this input eats the412
# host. That cap is precisely the bounded worker the F2 contract amendment413
# requires -- owned by F2b on the 2026-08-11 ruling, since a pure414
# source->descriptor function cannot protect itself from a hostile input.415
oom_fixture=test/fixtures/f2a-reader-oom.sgd416
compiler_version=$(plain <($sigil --version 2>&1) | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)417
[ -n "$compiler_version" ] || fail "could not read the compiler version"418
ver_ge() { # $1 >= $2, dotted419
[ "$(printf '%s\n%s\n' "$2" "$1" | sort -V | head -1)" = "$2" ]420
}421
oom_log="$work/reader-oom.log"422
set +e423
( ulimit -v 1500000; F2A_MANIFEST_SOURCE="$oom_fixture" \424
F2A_MANIFEST_REVISION="sha256:$(sha256sum "$oom_fixture" | cut -d' ' -f1)" \425
timeout 90 "$sigil" -L build/dev/lib -L src -L test test/f2a-span-manifest.sgl > "$oom_log" 2>&1 )426
oom_status=$?427
set -e428
if ver_ge "$compiler_version" "0.18.5"; then429
[ "$oom_status" = "0" ] || { tail -20 "$oom_log" >&2; fail "compiler $compiler_version carries the t-f6e098 fix, so the (} shape must yield a clean diagnostic; the probe exited $oom_status"; }430
grep -qE '^DIAGNOSTIC error parse-error' "$oom_log" \431
|| { tail -20 "$oom_log" >&2; fail "the (} shape did not produce a located parse diagnostic on a fixed toolchain"; }432
echo "arm 5 reader-pathology: compiler $compiler_version >= 0.18.5, clean diagnostic REQUIRED and observed"433
else434
[ "$oom_status" != "0" ] || { tail -20 "$oom_log" >&2; fail "compiler $compiler_version predates the t-f6e098 fix but the (} shape did NOT fail; this arm no longer describes its subject -- re-derive it"; }435
grep -qiE 'out of memory|Killed' "$oom_log" \436
|| { tail -20 "$oom_log" >&2; fail "the (} shape failed on compiler $compiler_version, but not with the memory signature t-f6e098 describes"; }437
echo "arm 5 reader-pathology: compiler $compiler_version PREDATES the 0.18.5 fix; the documented OOM is REQUIRED and was observed, contained by the ulimit. THIS IS THE PATHOLOGY, NOT A PASS: it flips to the strict assertion automatically at 0.18.5."438
fi439
echo "arm 5 OK: located diagnostics for parse-error and no-language-header; BOM/shebang/corpus clean and byte-verified"440
fi442
# ---------------------------------------------------------------------------443
# ARM 6 -- DUPLICATE IDS, asserted through a SECOND instrument.444
#445
# The suite asserts the contract through the API. This arm asserts it through446
# the manifest, i.e. through the serialized descriptor a consumer actually sees,447
# so a contract that held only inside the process would not survive here.448
# ---------------------------------------------------------------------------449
if should_run 6; then450
echo "--- arm 6: duplicate ids"451
dup=test/fixtures/f2a-duplicate.sgd452
diag_of "$dup" "$work/dup.manifest" || fail "manifest probe failed on $dup"453
dup_diags=$(grep -cE '^DIAGNOSTIC error duplicate-node-id ' "$work/dup.manifest" || true)454
# TWO, not one: every occurrence is reported and none is preferred, so the455
# report itself does not privilege document order.456
[ "$dup_diags" = "2" ] || { grep '^DIAGNOSTIC' "$work/dup.manifest" >&2; fail "expected 2 duplicate-node-id diagnostics, got $dup_diags"; }457
dup_nodes=$(grep -cE '^NODE [0-9.]+ command task folio t-7681 ' "$work/dup.manifest" || true)458
[ "$dup_nodes" = "2" ] || { fail "expected BOTH duplicate nodes in the descriptor, found $dup_nodes"; }459
# ...and the two diagnostics are at DIFFERENT positions.460
dup_positions=$(grep -E '^DIAGNOSTIC error duplicate-node-id ' "$work/dup.manifest" \461
| grep -oE 'f2a-duplicate.sgd:[0-9]+:[0-9]+' | sort -u | wc -l)462
[ "$dup_positions" = "2" ] || fail "the two duplicate diagnostics report $dup_positions distinct positions, not 2"463
"${PY[@]}" test/f2a-span-verify.py "$dup" "$work/dup.manifest" > "$work/dup.verify" 2>&1 \464
|| { cat "$work/dup.verify" >&2; fail "byte verifier rejected the duplicate fixture's spans"; }465
echo "arm 6 OK: both nodes present, 2 located diagnostics at 2 distinct positions, spans byte-verified"466
fi468
# ---------------------------------------------------------------------------469
# ARM 7 -- THE VOCABULARY IS DERIVED ON BOTH SIDES.470
#471
# `(folio doc inspect)` cannot import `(folio doc model)` -- that would drag472
# `(sigil doc model)` into the passive closure, which arm 8 refuses. So the473
# vocabulary is data in both files and NOTHING IN THE PRODUCT COMPARES THEM.474
# This arm is that comparison: record names and their `(id)` fields are derived475
# from `model.sgl`'s own `define-struct` lines, the inspector's list is derived476
# from its own alist, and set equality is required IN BOTH DIRECTIONS.477
# ---------------------------------------------------------------------------478
if should_run 7; then479
echo "--- arm 7: vocabulary derivation"480
model=src/folio/doc/model.sgl481
model_vocab="$work/model-vocab.txt"482
awk '483
/^ *\(define-struct [a-z-]+-record/ {484
line = $0485
match(line, /define-struct [a-z-]+-record/)486
name = substr(line, RSTART + 14, RLENGTH - 14 - 7)487
has_id = (line ~ /\(id\)/) ? "#t" : "#f"488
print name " " has_id489
}490
' "$model" | sort > "$model_vocab"491
model_count=$(wc -l < "$model_vocab")492
# `-gt 0`, not `-ge 11`, for the same reason the inspector-side floor was493
# loosened: a floor catches a broken derivation, but a floor of 11 ALSO fires494
# on a legitimate shrink (remove a record from the frozen model and both sides495
# shrink together, the diff stays clean) and it fires BEFORE the diff, hiding496
# the assertion this arm exists for.497
[ "$model_count" -gt 0 ] || fail "derived NO records from $model; the derivation is broken, not the file"499
inspect_vocab="$work/inspect-vocab.txt"500
sed '/^[[:space:]]*;/d' src/folio/doc/inspect.sgl \501
| grep -oE '\([a-z-]+ \. #[tf]\)' \502
| sed -E 's/^\(([a-z-]+) \. (#[tf])\)$/\1 \2/' | sort > "$inspect_vocab"503
inspect_count=$(wc -l < "$inspect_vocab")504
# NOT a floor of 11. A floor here catches a broken derivation but ALSO refuses505
# a legitimate shrink (remove a record from the frozen model and both sides506
# shrink together, the diff stays clean, and the floor produces a false red) --507
# and, measured by sabotage leg S8, it fires BEFORE the diff and hides it. The508
# derivation must produce something; agreement is the diff's job, not a509
# count's.510
[ "$inspect_count" -gt 0 ] || fail "derived NO vocabulary entries from inspect.sgl -- the derivation is broken, not the file"512
if ! diff -u "$model_vocab" "$inspect_vocab" > "$work/vocab.diff"; then513
cat "$work/vocab.diff" >&2514
fail "the inspector's vocabulary and the frozen model's records disagree (name or id-declaration)"515
fi516
# POSITIVE CONTROL, BOTH DIRECTIONS: the comparison must report a difference517
# when one exists, in each direction, or an empty diff means nothing.518
cp "$model_vocab" "$work/ctl-a"; echo "phantom-record #t" >> "$work/ctl-a"519
if diff -q "$work/ctl-a" "$inspect_vocab" >/dev/null; then520
fail "positive control: the vocabulary comparison did not notice an EXTRA model record"521
fi522
cp "$inspect_vocab" "$work/ctl-b"; sed -i '1d' "$work/ctl-b"523
if diff -q "$model_vocab" "$work/ctl-b" >/dev/null; then524
fail "positive control: the vocabulary comparison did not notice a MISSING inspector entry"525
fi526
# ...and the id-declaration half must be load-bearing on its own, not just the527
# names: flipping one `#t` to `#f` has to be seen.528
sed 's/^task #t$/task #f/' "$model_vocab" > "$work/ctl-c"529
if diff -q "$work/ctl-c" "$inspect_vocab" >/dev/null; then530
fail "positive control: the comparison ignores the id-declaration column"531
fi532
echo "arm 7 OK: $model_count records, both sides derived, set equality including id-declaration; comparison proven to see 3 kinds of drift"533
fi535
# ---------------------------------------------------------------------------536
# ARM 8 -- PASSIVITY, STATICALLY: direct allowlist AND transitive closure.537
#538
# The direct check is F1a's arm 6 shape and it stays. The TRANSITIVE walk is539
# what F1a's review left open as informational "for F2/P0" -- and it is what540
# found the two edges that made this module reach a network stack: `(sigil541
# language)` and `(sigil error)`, both entirely innocent-looking in an import542
# list.543
# ---------------------------------------------------------------------------544
if should_run 8; then545
echo "--- arm 8: passivity (direct allowlist + transitive closure)"546
extract_imports() {547
sed '/^[[:space:]]*;/d' "$1" | awk '548
/\(import/ { collecting = 1; buf = "" }549
collecting {550
buf = buf " " $0551
n = gsub(/\(/, "(", $0); m = gsub(/\)/, ")", $0)552
depth += n - m553
if (depth <= 0) { print buf; collecting = 0; depth = 0 }554
}555
' | grep -oE '\([a-z][a-z0-9-]*( [a-z0-9-]+)+\)' | sort -u556
}557
allowed='^\(sigil (core|struct|syntax|math|string|list)\)$|^\(sigil doc inspect\)$'558
imports=$(extract_imports src/folio/doc/inspect.sgl)559
[ -n "$imports" ] || fail "no import form extracted from inspect.sgl -- the extractor is broken, not the file"560
while IFS= read -r lib; do561
echo "$lib" | grep -qE "$allowed" || fail "inspect.sgl imports $lib, outside the passive allowlist"562
done <<< "$imports"563
# POSITIVE CONTROLS on the allowlist itself.564
for forbidden_lib in '(sigil process)' '(sigil shell)' '(sigil fs)' '(sigil io)' '(sigil error)' \565
'(sigil language)' '(sigil crypto)' '(sigil doc model)' '(folio doc model)'; do566
echo "$forbidden_lib" | grep -qE "$allowed" \567
&& fail "positive control: the allowlist ACCEPTS $forbidden_lib -- it is not restricting anything"568
done569
for permitted_lib in '(sigil core)' '(sigil doc inspect)' '(sigil syntax)'; do570
echo "$permitted_lib" | grep -qE "$allowed" \571
|| fail "positive control: the allowlist REFUSES $permitted_lib, which the core legitimately needs"572
done573
# ...and the extractor must SEE a planted import, or the allowlist walk is over574
# an empty list.575
ctl_dir=$(mktemp -d)576
sed 's/(import (sigil core) (sigil struct) (sigil math) (sigil string) (sigil list)/(import (sigil process) (sigil core) (sigil struct) (sigil math) (sigil string) (sigil list)/' \577
src/folio/doc/inspect.sgl > "$ctl_dir/planted.sgl"578
extract_imports "$ctl_dir/planted.sgl" | grep -q '(sigil process)' \579
|| fail "positive control: the import extractor did not see a planted (sigil process)"581
closure_log="$work/closure.log"582
if ! "${PY[@]}" test/f2a-import-closure.py '(folio doc inspect)' \583
src "$sigil_doc_dir/src" "$stdlib_dir" > "$closure_log" 2>&1; then584
cat "$closure_log" >&2; fail "transitive import closure is not clean"585
fi586
closure_line=$(grep -oE 'F2A_IMPORT_CLOSURE modules=[0-9]+ direct=[0-9]+ forbidden=0 leaves=[0-9]+' "$closure_log") \587
|| { cat "$closure_log" >&2; fail "closure walker printed no result line"; }588
closure_modules=$(echo "$closure_line" | tr ' ' '\n' | grep '^modules=' | cut -d= -f2)589
# The walker itself now refuses the I/O modules (they are in its FORBIDDEN590
# dict), so this grep is a SECOND OPINION over its printed closure rather than591
# the only thing standing between the passive core and a network stack. It592
# depends on the walker's output format, so it gets the positive control the593
# gate's own header demands of every absence assertion.594
if grep -qE '^ \(sigil (fs|io|process|shell|socket)\)$' "$closure_log"; then595
cat "$closure_log" >&2; fail "an I/O module is in the passive core's transitive closure"596
fi597
printf ' (sigil io)\n' > "$work/io-control"598
grep -qE '^ \(sigil (fs|io|process|shell|socket)\)$' "$work/io-control" \599
|| fail "positive control: the I/O pattern does not match the format the walker prints; it has been silently blind"601
# POSITIVE CONTROL FOR THE WALK ITSELF: a forbidden module TWO levels down.602
#603
# The previous control planted a root whose own direct import was `(folio doc604
# model)` -- itself in FORBIDDEN -- so a walker that examined nothing but the605
# root's import list would have passed it. It also relied on an unverified606
# `sed -i` into model.sgl, the exact hazard the sabotage driver built607
# `patch_file` to refuse. Now the root imports an ordinary, permitted module,608
# and THAT module imports `(sigil process)`; the assertion names the specific609
# violation rather than grepping for the word FORBIDDEN.610
plant_dir=$(mktemp -d)611
mkdir -p "$plant_dir/src/folio/doc"612
cat > "$plant_dir/src/folio/doc/inspect-planted.sgl" <<'PLANT'613
(define-library (folio doc inspect-planted)614
(import (folio doc planted-middle))615
(export nothing)616
(begin (define nothing #f)))617
PLANT618
cat > "$plant_dir/src/folio/doc/planted-middle.sgl" <<'MID'619
(define-library (folio doc planted-middle)620
(import (sigil core) (sigil process))621
(export nothing-middle)622
(begin (define nothing-middle #f)))623
MID624
if "${PY[@]}" test/f2a-import-closure.py '(folio doc inspect-planted)' \625
"$plant_dir/src" src "$sigil_doc_dir/src" "$stdlib_dir" > "$work/closure-ctl.log" 2>&1; then626
cat "$work/closure-ctl.log" >&2627
fail "positive control: the closure walker did NOT report a forbidden module reached TWO levels down"628
fi629
grep -q 'FORBIDDEN: (sigil process)' "$work/closure-ctl.log" \630
|| { cat "$work/closure-ctl.log" >&2; fail "positive control: the walker failed, but not by naming the planted (sigil process)"; }631
# ...and it must have got there through the intermediary, i.e. actually recursed.632
grep -q 'planted-middle' "$work/closure-ctl.log" \633
|| { cat "$work/closure-ctl.log" >&2; fail "positive control: the walker did not report the path it took; it may not have recursed"; }634
rm -rf "$plant_dir" "$ctl_dir"635
echo "arm 8 OK: direct imports inside the allowlist; transitive closure is $closure_modules modules with no I/O and no evaluation-side module; walker proven to see a violation TWO levels down, through a permitted intermediary"636
fi638
echo639
if [ -n "$only_arm" ]; then640
echo "F2A_GATE_ARM_${only_arm}_GREEN (SELECTIVE RUN -- this is NOT a full gate and must never be quoted as one)"641
exit 0642
fi643
echo "F2A_GATE_GREEN arms=8 tests=$reported span_checks=$span_checks span_nodes=$span_nodes span_fields=$span_fields divergent=$span_divergent records=$model_count closure=$closure_modules sigil_doc=$sigil_doc_sha"