AtlatestRepositoryfolio
1
#!/usr/bin/env bash2
#3
# F1a -- the gate for the frozen `folio/doc` semantic model.4
#5
# READ THIS BEFORE EDITING AN ASSERTION.6
#7
# Every count this gate checks is DERIVED FROM THE ARTIFACT it describes, never8
# written in as a literal. Adding a test, a record or a node kind must not9
# require editing a number here; if it does, the check has stopped describing10
# its subject and has become a second thing to keep in sync. That drift is the11
# single most common defect in this estate's gates.12
#13
# Every ABSENCE assertion carries a POSITIVE CONTROL that plants the forbidden14
# thing in a throwaway copy and requires the pattern to fire on it. An absence15
# check with no positive control passes for free -- when the tool is missing,16
# when the path is wrong, when the pattern never matched anything in its life.17
#18
# Positive controls plant into `$(mktemp -d)` copies and NEVER into the working19
# tree, so there is no restore step that has to be reached. A sabotage driver20
# whose undo can be skipped by an early exit leaves the damage behind.21
#22
# WHAT THIS GATE DELIBERATELY DOES NOT CHECK: the live-estate census behind the23
# document-address grammar. That measurement (2620 note names + 49 project24
# names, 1 rejection) is a DESIGN-TIME derivation recorded with its command in25
# the session log, not a standing check. Pinning a snapshot of a growing estate26
# into a gate produces a check that describes last week and fails on growth.27
# What is durable is the grammar's BEHAVIOUR per input class, and that is28
# asserted exhaustively in the model suite instead.29
set -euo pipefail31
repo_dir="$(cd "$(dirname "$0")/.." && pwd)"32
cd "$repo_dir"34
sigil="${SIGIL_F1A_COMPILER:-$repo_dir/../task-sigil-language-readers/build/dev/bin/sigil}"35
redirects="${SIGIL_F1A_REDIRECTS:-$repo_dir/dev-redirects.sgl}"36
sigil_doc_dir="${SIGIL_F1A_DOC_DIR:-$repo_dir/../sigil-doc}"37
cache_dir="${XDG_CACHE_HOME:-$(mktemp -d)}"38
export XDG_CACHE_HOME="$cache_dir"40
# The `test` subcommand takes `--redirects` as a flag; a DIRECT script run has no41
# such flag and reads the environment instead. Without this, arms 3 and 4 die42
# with "Missing dependencies" listing every package -- which is a real red, but43
# for a setup reason rather than anything about the product. Caught because the44
# gate was run as a script rather than trusted to match the commands I had been45
# running by hand, where this was set in the shell.46
export SIGIL_REDIRECTS="$redirects"48
fail() { echo "F1A GATE RED: $*" >&2; exit 1; }50
# THE COMPILER AND TEST RUNNER COLOUR THEIR OUTPUT, AND THE ESCAPES SIT BETWEEN51
# THE FIELDS A GATE WANTS TO READ. The summary line is really:52
#53
# Tests: ^[[32m30 passed^[[39m^[[2m (30 total)^[[22m54
#55
# so a pattern as innocent as `Tests: +[0-9]+ passed` matches NOTHING -- the56
# escape sequence sits between the spaces and the digits. That cost two full57
# gate runs here. It happened to fail safe; a pattern like `grep -c passed`58
# would have quietly returned the wrong number instead. Every log this gate59
# parses goes through `plain` first.60
plain() { sed 's/\x1b\[[0-9;]*m//g' "$1"; }62
echo "=== F1a gate ==============================================="63
echo "repo : $repo_dir @ $(git rev-parse HEAD)"64
echo "compiler : $($sigil --version 2>&1 | tail -1)"65
# The evidence for this phase was produced against sigil-doc MASTER through a66
# dev redirect, because no RELEASED sigil-doc contains `url-link` yet (v0.1.1 =67
# 42a0686 predates SD-P2). Recording the SHA here means a later reader cannot68
# mistake this for evidence against a tag.69
echo "sigil-doc : $(git -C "$sigil_doc_dir" rev-parse HEAD) (via dev-redirects, NOT a release)"70
sigil_doc_dirty=$(git -C "$sigil_doc_dir" status --porcelain | wc -l)71
echo "sigil-doc dirty : $sigil_doc_dirty files"72
echo "============================================================"73
# Printing the SHA is not the same as depending on it. Because every green74
# result here is produced through a redirect to a local sigil-doc WORKING TREE,75
# an uncommitted change there is an unrecorded input to this whole gate -- the76
# resolved-dependency-versions rule, one layer down. Refuse rather than produce77
# evidence nobody can reproduce.78
if [ "$sigil_doc_dirty" != "0" ]; then79
git -C "$sigil_doc_dir" status --porcelain >&280
fail "sigil-doc has $sigil_doc_dirty uncommitted file(s); evidence produced against it is not reproducible"81
fi83
# ---------------------------------------------------------------------------84
# ARM 1 -- the product still builds with the new dependency.85
#86
# Asserted on the ARTIFACT, not on an exit code: this command is often piped87
# while iterating, and a pipeline reports the last stage's status. "Bundle88
# created" is a claim only a finished build makes.89
# ---------------------------------------------------------------------------90
echo "--- arm 1: build"91
build_log=$(mktemp)92
if ! "$sigil" build --redirects "$redirects" > "$build_log" 2>&1; then93
tail -30 "$build_log" >&2; fail "build exited non-zero"94
fi95
plain "$build_log" | grep -q "Bundle created" || { tail -30 "$build_log" >&2; fail "build produced no bundle"; }96
modules=$(plain "$build_log" | grep -oE '^ +[0-9]+ modules' | grep -oE '[0-9]+' || true)97
modules=${modules%%$'\n'*}98
[ -n "$modules" ] || fail "build log carried no module count"99
echo "arm 1 OK: bundle built, ${modules} modules"101
# ---------------------------------------------------------------------------102
# ARM 2 -- the model suite.103
#104
# The expected test count is COUNTED FROM THE SUITE FILE. A suite that silently105
# stopped running half its cases would still print a green summary; comparing106
# the reported count against the file's own `(test "` forms is what makes the107
# green mean "all of them ran".108
# ---------------------------------------------------------------------------109
echo "--- arm 2: model suite"110
suite=test/test-folio-doc-model.sgl111
expected_tests=$(grep -cE '^\(test "' "$suite")112
[ "$expected_tests" -gt 20 ] || fail "suite declares only $expected_tests tests; expected the frozen vocabulary to need more"113
suite_log=$(mktemp)114
if ! "$sigil" -L build/dev/lib test --redirects "$redirects" "$suite" > "$suite_log" 2>&1; then115
tail -30 "$suite_log" >&2; fail "model suite exited non-zero"116
fi117
reported=$(plain "$suite_log" | grep -oE 'Tests: +[0-9]+ passed' | grep -oE '[0-9]+' || true)118
reported=${reported%%$'\n'*}119
[ -n "$reported" ] || { tail -20 "$suite_log" >&2; fail "model suite printed no test count"; }120
[ "$reported" = "$expected_tests" ] || fail "suite defines $expected_tests tests but only $reported ran/passed"121
# NOT `grep -q … && fail …`. Under `set -e` that form kills the gate on the122
# HEALTHY path: grep finds nothing, returns 1, and the && list's status is 1.123
# It cost a full run here and it fails in the safe direction only by luck --124
# the same construct guarding a benign action would have been a silent pass.125
# `[1-9][0-9]*` rather than `[0-9]+` so a hypothetical "0 failed" summary is126
# not read as a failure.127
if plain "$suite_log" | grep -qE '[1-9][0-9]* failed'; then128
plain "$suite_log" | grep -E '[1-9][0-9]* failed' >&2129
fail "model suite reported failures"130
fi131
# POSITIVE CONTROL for the absence assertion above: prove the pattern matches132
# the runner's real failure summary. An absence check whose pattern never133
# matched anything passes for free, which is this gate's own stated rule.134
printf 'Tests: 29 passed, 1 failed (30 total)\n' > "$suite_log.control"135
grep -qE '[1-9][0-9]* failed' "$suite_log.control" \136
|| fail "positive control: the failure pattern does not match the runner's real failure summary"137
echo "arm 2 OK: $reported/$expected_tests tests passed, count derived from $suite"139
# ---------------------------------------------------------------------------140
# ARM 3 -- publication fallback, and PROOF ITS HARNESS CAN FAIL.141
#142
# The probe is a direct script with a hand-rolled harness, because `(sigil test)`143
# forms are INERT in a direct run -- measured: a file asserting 1 = 2 printed144
# nothing and exited 0. So a green from this probe is worthless until the same145
# probe has been shown to go red on the same run. That is the self-test leg.146
# ---------------------------------------------------------------------------147
echo "--- arm 3: publication fallback"148
probe_log=$(mktemp)149
if ! "$sigil" -L build/dev/lib -L src -L test test/f1a-publication-probe.sgl > "$probe_log" 2>&1; then150
tail -20 "$probe_log" >&2; fail "publication probe exited non-zero"151
fi152
line=$(plain "$probe_log" | grep -oE 'F1A_PUBLICATION_PROBE checks=[0-9]+ failed=[0-9]+') \153
|| { tail -20 "$probe_log" >&2; fail "publication probe printed no result line"; }154
checks=$(echo "$line" | grep -oE 'checks=[0-9]+' | grep -oE '[0-9]+')155
failed=$(echo "$line" | grep -oE 'failed=[0-9]+' | grep -oE '[0-9]+')156
[ "$failed" = "0" ] || fail "publication probe reported $failed failed checks"157
[ "$checks" -ge 50 ] || fail "publication probe ran only $checks checks; it is not exercising three renderings"159
selftest_log=$(mktemp)160
if F1A_PROBE_SELFTEST=1 "$sigil" -L build/dev/lib -L src -L test \161
test/f1a-publication-probe.sgl > "$selftest_log" 2>&1; then162
fail "publication probe self-test EXITED 0 with a failure injected -- the harness cannot go red"163
fi164
plain "$selftest_log" | grep -qE 'F1A_PUBLICATION_PROBE checks=[0-9]+ failed=1$' \165
|| fail "publication probe self-test did not report exactly one failure"166
echo "arm 3 OK: $checks checks green, and the harness demonstrably goes red on injection"168
# ---------------------------------------------------------------------------169
# ARM 4 -- vocabulary coverage, both sides derived.170
#171
# The kind list comes from the model's own `define-struct` lines; the exercised172
# list comes from the pressure manuscript's event stream. Adding a record173
# without adding it to the manuscript fails here rather than shipping an174
# unexercised node. Neither side is a list somebody maintains by hand.175
# ---------------------------------------------------------------------------176
echo "--- arm 4: vocabulary coverage"177
kinds=$(grep -oE '^ *\(define-struct [a-z-]+-record' src/folio/doc/model.sgl \178
| sed -E 's/.*\(define-struct //; s/-record$//' | sort)179
kind_count=$(echo "$kinds" | wc -l)180
[ "$kind_count" -ge 11 ] || fail "only $kind_count records found in the model; the derivation is not matching"181
cov_log=$(mktemp)182
if ! "$sigil" -L build/dev/lib -L src -L test test/f1a-coverage-probe.sgl > "$cov_log" 2>&1; then183
tail -20 "$cov_log" >&2; fail "coverage probe exited non-zero"184
fi185
plain "$cov_log" | grep -q 'F1A_COVERAGE_PROBE kinds=' || fail "coverage probe printed no result line"186
detect_missing() { # $1 = newline-separated kind list; echoes those absent from $cov_log187
local out=""188
for k in $1; do189
plain "$cov_log" | grep -qE "^F1A_KIND ${k}\$" || out="$out $k"190
done191
echo "$out"192
}193
missing=$(detect_missing "$kinds")194
[ -z "$missing" ] || fail "the pressure manuscript never exercises:$missing"195
# POSITIVE CONTROL: run the SAME detector over a list containing a kind that196
# cannot possibly be present, and require it to be reported. Without this, an197
# empty `missing` is equally consistent with "everything is covered" and "the198
# loop never compared anything".199
control_missing=$(detect_missing "$(printf '%s\nthis-kind-cannot-exist\n' "$kinds")")200
case "$control_missing" in201
*this-kind-cannot-exist*) : ;;202
*) fail "positive control: the coverage detector did not report a kind that is definitely absent" ;;203
esac204
echo "arm 4 OK: all $kind_count model records exercised; detector proven to report an absent kind"206
# ---------------------------------------------------------------------------207
# ARM 5 -- no SQLite / MCP / Slate vocabulary in the semantic core.208
#209
# Scope is `src/folio/doc/**` and that scoping is the whole point: the rest of210
# this repository is a working MCP server and is FULL of these words. The211
# positive control below plants each forbidden word into a throwaway copy and212
# requires the pattern to fire, which is what separates "the core is clean" from213
# "my pattern never matched anything".214
# ---------------------------------------------------------------------------215
echo "--- arm 5: no backend/runtime vocabulary leakage"216
core_files=$(find src/folio/doc.sgl src/folio/doc -type f -name '*.sgl' | sort)217
core_count=$(echo "$core_files" | wc -l)218
[ "$core_count" -ge 6 ] || fail "expected at least 6 semantic-core files, found $core_count"219
for f in $core_files; do [ -f "$f" ] || fail "core file $f does not exist"; done220
# `\bslate\b`, not bare `slate`: the unanchored form matches "translate".221
# Safe direction (it can only over-match), fixed anyway -- a pattern that222
# fires on an innocent word teaches people to loosen the arm.223
forbidden='sqlite|fts5|json-rpc|jsonrpc|\bmcp\b|\bslate\b'224
if echo "$core_files" | xargs grep -rniE "$forbidden" ; then225
fail "backend/runtime vocabulary leaked into the semantic core"226
fi227
# positive control, in a copy -- nothing to restore, so nothing to forget228
control_dir=$(mktemp -d)229
cp -r src/folio/doc "$control_dir/doc"230
for word in sqlite fts5 json-rpc mcp slate; do231
echo ";; planted $word" >> "$control_dir/doc/model.sgl"232
grep -rqiE "$forbidden" "$control_dir/doc/model.sgl" \233
|| fail "positive control: the leakage pattern did NOT fire on planted '$word'"234
sed -i '$ d' "$control_dir/doc/model.sgl"235
done236
echo "arm 5 OK: $core_count core files clean; pattern proven to fire on all 5 planted words"238
# ---------------------------------------------------------------------------239
# ARM 6 -- passivity by import allowlist.240
#241
# The model may reach the language, structs, source locations and sigil-doc, and242
# nothing else. No filesystem, no process, no network, no shell. Import forms243
# are extracted with balanced-paren awk rather than by grepping for library-like244
# tokens, so a library named in a COMMENT cannot satisfy or trip this arm.245
# ---------------------------------------------------------------------------246
echo "--- arm 6: passivity (import allowlist)"247
# Comments are stripped BEFORE extraction. The previous version claimed a248
# library named in a comment "cannot satisfy or trip this arm"; review249
# demonstrated the opposite by putting `(sigil process)` in a comment on a line250
# containing `(import`, and the extractor emitted it. That is the same251
# self-matching shape arm 7 was already fixed for.252
#253
# The token filter deliberately matches ANY `(word ...)` library form, not just254
# sigil/folio ones, so an import from an unexpected namespace is SEEN and then255
# refused by the allowlist rather than being invisible to both.256
extract_imports() {257
sed '/^[[:space:]]*;/d' "$1" | awk '258
/\(import/ { collecting = 1; buf = "" }259
collecting {260
buf = buf " " $0261
n = gsub(/\(/, "(", $0); m = gsub(/\)/, ")", $0)262
depth += n - m263
if (depth <= 0) { print buf; collecting = 0; depth = 0 }264
}265
' | grep -oE '\([a-z][a-z0-9-]*( [a-z0-9-]+)+\)' | sort -u266
}267
# `(sigil doc …)` is NOT blanket-allowed, and this is an EXACT list rather than268
# a namespace pattern. `(sigil doc verify)` reaches `(sigil shell)` and269
# `(sigil process)`, and `presentation`, `render html`, `render typst` and270
# `print` all reach it in turn -- so allowing the namespace would let the271
# semantic core import a subprocess runner while this arm stayed green. Only the272
# four sigil-doc modules the core actually needs are listed; anything else,273
# inside that namespace or outside it, is refused.274
# WIDENED BY F1b, BY EXACTLY TWO ENTRIES, and the justification is a measurement275
# rather than an argument. `#lang folio/doc` resolves to `(folio doc reader)`,276
# which FORCES `src/folio/doc/reader.sgl` into the directory this arm scans; that277
# file must import `(sigil doc reader)`, and `src/folio/doc/author.sgl` must278
# import `(sigil doc private author-runtime)` to bind the three names the parser279
# emits by bare name. Neither was on this list, so the arm failed -- correctly,280
# by its own spec, on a correct tree.281
#282
# THE MEASUREMENT: `test/f2a-import-closure.py` walked the transitive closure of283
# `(sigil doc reader)` over `../sigil-doc/src`, `../sigil/packages/sigil-stdlib/src`284
# and `src`. Zero unresolvable libraries; the only forbidden modules reachable285
# are the three EVALUATION-SIDE entries F2a forbids by design (the author286
# runtime, `(sigil doc model)`, `(sigil doc author)`). NONE of `(sigil fs)`,287
# `(sigil io)`, `(sigil socket)`, `(sigil process)`, `(sigil shell)` or288
# `(sigil doc verify)` is reachable. The passivity claim this arm protects is289
# therefore intact.290
#291
# The reader TIER has its own, separately tight allowlist in292
# `test/gate-f1b.sh` arm 9. Two narrow lists, not one loose one.293
# AND FOUR MORE THAT WERE ALREADY BROKEN, by F2a's merge (`7d838e2`), NOT by294
# F1b. `src/folio/doc/inspect.sgl` imports `(sigil doc inspect)`,295
# `(sigil list)`, `(sigil math)` and `(sigil string)`, none of which was on this296
# list -- so this arm has been RED ON FOLIO MASTER since that commit landed, on297
# four independent counts, and arm 5 has been red on a fifth (a comment naming a298
# specific editor and bytecode target). Verified against a pristine299
# `git archive` of master extracted to /tmp, not inferred from this worktree.300
# F1a's own recorded green line says `core_files=7`; master has 8. That count301
# moving is independent proof nobody re-ran the arm after F2a landed.302
#303
# Each is legitimately passive: `(sigil doc inspect)` is SD-P4's passive parse304
# surface, whose no-I/O closure `test/gate-f2a.sh` arm 8 walks and enforces;305
# `(sigil list)`, `(sigil math)` and `(sigil string)` are pure data stdlib.306
allowed='^\((sigil (core|struct|syntax|list|math|string))\)$'307
allowed="$allowed"'|^\(sigil doc\)$|^\(sigil doc (model|traversal|reader|inspect)\)$'308
allowed="$allowed"'|^\(sigil doc private author-runtime\)$'309
# `(sigil doc author)` is admitted for a reason worth reading, because the310
# obvious one is wrong. `src/folio/doc/reader.sgl` does NOT import it. What311
# this extractor sees is the PRELUDE the reader EMITS -- a quoted datum,312
# `'(import (folio doc) (sigil doc author) (folio doc author))` -- and the313
# extractor cannot tell a real import form from a quoted one that happens to314
# look like it. That is a genuine limitation of this instrument, newly exposed315
# by the first module whose JOB is to emit import forms; it is recorded here316
# rather than worked around silently.317
#318
# It is admitted anyway, and honestly: the edge is REAL, merely deferred. A319
# folio manuscript's scope does link `(sigil doc author)` at evaluation time, by320
# design -- that is what "reuse the author vocabulary rather than fork it" means.321
# Refusing it would state something false about the dependency graph in order to322
# keep a pattern tidy.323
allowed="$allowed"'|^\(sigil doc author\)$'324
allowed="$allowed"'|^\(sigil doc render (plain|debug)\)$'325
allowed="$allowed"'|^\(folio doc( [a-z-]+)*\)$'326
for f in $core_files; do327
imports=$(extract_imports "$f")328
[ -n "$imports" ] || fail "no import form extracted from $f -- the extractor is broken, not the file"329
while IFS= read -r lib; do330
echo "$lib" | grep -qE "$allowed" || fail "$f imports $lib, which is outside the passive allowlist"331
done <<< "$imports"332
done333
# POSITIVE CONTROL: the extractor must SEE a forbidden import when one is there.334
cp -r src/folio/doc "$control_dir/doc2"335
sed -i 's/(import (sigil core) (sigil struct) (sigil syntax) (sigil doc model))/(import (sigil core) (sigil struct) (sigil syntax) (sigil doc model) (sigil process))/' \336
"$control_dir/doc2/model.sgl"337
planted=$(extract_imports "$control_dir/doc2/model.sgl")338
echo "$planted" | grep -q '(sigil process)' \339
|| fail "positive control: the import extractor did not see a planted (sigil process)"340
for forbidden_lib in '(sigil process)' '(sigil shell)' '(sigil fs)' '(sigil sqlite)' \341
'(sigil doc verify)' '(sigil doc presentation)' '(sigil doc render html)' \342
'(scheme file)' '(srfi 1)'; do343
if echo "$forbidden_lib" | grep -qE "$allowed"; then344
fail "positive control: the allowlist ACCEPTS $forbidden_lib -- it is not restricting anything"345
fi346
done347
# ...and the converse: it must still ACCEPT what the core legitimately uses, or348
# the arm is just refusing everything and calling that security.349
for permitted_lib in '(sigil core)' '(sigil doc model)' '(sigil doc render plain)' '(folio doc model)' \350
'(sigil doc reader)' '(sigil doc private author-runtime)'; do351
echo "$permitted_lib" | grep -qE "$allowed" \352
|| fail "positive control: the allowlist REFUSES $permitted_lib, which the core legitimately needs"353
done354
# THE WIDENING IS ASSERTED IN BOTH DIRECTIONS. A widened allowlist that no355
# longer refuses anything is the failure mode of widening one, so the refusal356
# controls above are not merely retained: these two neighbours of the newly357
# admitted entries must STILL be refused, or the two additions have been written358
# as a namespace pattern instead of as exact names.359
for near_miss in '(sigil doc private parser)' '(sigil doc render typst)' '(sigil doc print)'; do360
echo "$near_miss" | grep -qE "$allowed" \361
&& fail "positive control: widening for the reader tier also admitted $near_miss -- the new entries are not exact"362
done363
echo "arm 6 OK: every import in $core_count files is inside the allowlist; extractor proven to see a planted violation"365
# ---------------------------------------------------------------------------366
# ARM 7 -- `phase` stays a non-executable ordered container.367
#368
# D3 defers a workflow engine. An engine does not arrive as a commit called369
# "add a workflow engine"; it arrives as one innocuous field. So the record's370
# field set is pinned STRUCTURALLY, derived from the source, and the whole core371
# is checked for execution vocabulary.372
# ---------------------------------------------------------------------------373
echo "--- arm 7: phase is a non-executable ordered container"374
phase_line=$(grep -E '^ *\(define-struct phase-record' src/folio/doc/model.sgl) \375
|| fail "no phase-record definition found"376
phase_fields=$(echo "$phase_line" | grep -oE '\([a-z-]+\)' | sed -E 's/[()]//g' | grep -v '^define-struct$')377
phase_field_list=$(echo "$phase_fields" | tr '\n' ' ' | sed -E 's/ +$//')378
[ "$phase_field_list" = "id title status order children location" ] \379
|| fail "phase-record fields are now '$phase_field_list'; D3 freezes exactly 'id title status order children location'"380
# THIS ARM SCANS CODE, NOT PROSE, AND THAT DISTINCTION IS LOAD-BEARING.381
#382
# On its first run it went RED against two of this model's own COMMENTS -- the383
# lines explaining that a phase "carries no command, no runner, no retry policy"384
# and that an engine "arrives one innocuous field at a time (a command, a385
# runner, a retry count)". That is a check matching its own explanatory text: a386
# closed loop, invisible from inside because everything it reports is internally387
# consistent. The comments are correct and must stay; it is the arm that was388
# wrong to read them.389
#390
# Full-line comments only are stripped -- NOT everything after the first `;` --391
# because `publication.sgl` contains a semicolon inside a string literal392
# ("…this edition; pass #f") and a blunter filter would silently truncate real393
# code. The positive control below therefore plants CODE, not a comment; a394
# control that planted a comment would now prove nothing.395
#396
# Arm 5 above deliberately does the OPPOSITE and scans comments too, because397
# there the concern is the vocabulary appearing in the semantic core at all.398
code_only() { sed '/^[[:space:]]*;/d' "$1"; }399
# `\bcommand\b` WAS in this set and was REMOVED by F1b, deliberately and with a400
# reason rather than to make a red go away.401
#402
# It was added because the model's own warning names "a command, a runner, a403
# retry count" as how a workflow engine creeps in one innocuous field at a time.404
# That reasoning is sound for `model.sgl`, where `command` could only mean405
# something executable. It stopped being sound the moment a READER joined this406
# directory: in `sigil/doc` a "command" is a MANUSCRIPT COMMAND -- `@p`, `@task`,407
# `@section` -- and the author runtime's own signatures are408
# `author-normalize value loc command` and `author-flatten values loc command`.409
# The word now has two meanings here and the pattern cannot tell them apart, so410
# it matches `(define (children command values))` and calls a manuscript command411
# an execution engine.412
#413
# The alternative was renaming the parameter, which was considered and REFUSED:414
# contorting product code to satisfy a gate's vocabulary corrupts the thing being415
# measured in order to protect the measurement.416
#417
# WHAT STILL PROTECTS D3, so this is a narrowing and not a hole: `runner`,418
# `retry`, `exit-status`, `subprocess` and `spawn` have no second meaning in this419
# directory and all five remain refused; arm 7's FIELD-SET assertion above pins420
# `phase-record` to exactly six fields structurally, which is the check that421
# actually stops an engine arriving one field at a time; and the positive control422
# below still plants executable code and requires the pattern to fire.423
exec_vocab='\brunner\b|\bretry\b|\bexit-status\b|\bsubprocess\b|\bspawn\b'424
for f in $core_files; do425
if code_only "$f" | grep -niE "$exec_vocab"; then426
fail "execution vocabulary appeared in $f's CODE -- a workflow engine is creeping in"427
fi428
done429
# POSITIVE CONTROL: plant it as code, past the comment filter.430
cp src/folio/doc/model.sgl "$control_dir/exec-control.sgl"431
echo '(define (spawn-runner) (retry-subprocess))' >> "$control_dir/exec-control.sgl"432
code_only "$control_dir/exec-control.sgl" | grep -qiE "$exec_vocab" \433
|| fail "positive control: the execution-vocabulary pattern did not fire on planted CODE"434
# ...and prove the comment filter is what spares the real file: the same planted435
# text as a comment must NOT be seen.436
cp src/folio/doc/model.sgl "$control_dir/exec-control2.sgl"437
echo ';; (define (spawn-runner) (retry-subprocess))' >> "$control_dir/exec-control2.sgl"438
if code_only "$control_dir/exec-control2.sgl" | grep -qiE "$exec_vocab"; then439
fail "the comment filter is not filtering -- arm 7 would keep matching its own prose"440
fi441
echo "arm 7 OK: phase-record is exactly [$phase_field_list]; no execution vocabulary; pattern proven to fire"443
# ---------------------------------------------------------------------------444
# ARM 8 -- the canonical datum is TOTAL over every record's fields.445
#446
# Review's point, and it is a real hole: nothing checked that `folio->datum`447
# actually serialises every field a record declares. Adding a field to a record448
# and forgetting the datum was invisible everywhere -- the exact-datum literal449
# only covers the note root, and a field nobody serialises simply does not450
# appear, so no assertion can miss it.451
#452
# Both sides are derived: field names come from `model.sgl`'s `define-struct`453
# lines, and each record's datum branch is sliced out of `traversal.sgl` between454
# its `((<rec>? n)` head and its closing `(loc->datum (<rec>-location n))`.455
# `location` is excluded because it IS that closing tuple.456
# ---------------------------------------------------------------------------457
echo "--- arm 8: the canonical datum serialises every declared field"458
datum_missing=""459
datum_checked=0460
while read -r rec fields; do461
branch=$(awk -v r="$rec" '462
$0 ~ "\\(\\(" r "\\? n\\)" { inb = 1 }463
inb { print }464
inb && $0 ~ "loc->datum \\(" r "-location n\\)" { exit }465
' src/folio/doc/traversal.sgl)466
[ -n "$branch" ] || fail "no folio->datum branch found for $rec -- the slicer is broken, not the file"467
# COMMENTS STRIPPED BEFORE THE FIELD SEARCH. Without this, branch prose such468
# as `;; we do not serialise 'due` SATISFIES totality for `due` -- the arm469
# would confirm a field is serialised by reading a comment saying it is not.470
#471
# This is the same self-matching shape arm 7 was fixed for, in the arm written472
# immediately afterwards. I predicted that shape in arm 5, defended arm 5, and473
# left its neighbours open; the lesson is to defend the CLASS, not the474
# prediction. Arms 2 and 4 are immune by `^`-anchoring and arm 5 scans475
# comments deliberately, so arm 8 was the only remaining exposure.476
branch_code=$(echo "$branch" | sed '/^[[:space:]]*;/d')477
for f in $fields; do478
[ "$f" = "location" ] && continue479
datum_checked=$((datum_checked + 1))480
echo "$branch_code" | grep -qE "'$f\b" || datum_missing="$datum_missing $rec.$f"481
done482
done < <(grep -oE '^ *\(define-struct [a-z-]+-record.*' src/folio/doc/model.sgl \483
| sed -E "s/^ *\(define-struct ([a-z-]+)-record/\1/; s/[()]//g")484
[ -z "$datum_missing" ] || fail "folio->datum never serialises:$datum_missing"485
[ "$datum_checked" -ge 40 ] || fail "arm 8 only checked $datum_checked fields; the derivation is not matching"486
# POSITIVE CONTROL: the same slice-and-search must REPORT a field that is487
# genuinely absent, or an empty result means nothing.488
control_branch=$(awk '489
$0 ~ /\(\(task\? n\)/ { inb = 1 }490
inb { print }491
inb && $0 ~ /loc->datum \(task-location n\)/ { exit }492
' src/folio/doc/traversal.sgl)493
if echo "$control_branch" | grep -qE "'definitely-not-a-field\b"; then494
fail "positive control: the datum slicer matched a field that cannot exist"495
fi496
echo "$control_branch" | sed '/^[[:space:]]*;/d' | grep -qE "'status\b" \497
|| fail "positive control: the datum slicer cannot even see task's status field"498
# ...and the comment filter must be what spares it: the same field name, present499
# ONLY in a comment, must NOT satisfy totality.500
if printf ' ;; we deliberately do not serialise %s\n' "'never-a-real-field" \501
| sed '/^[[:space:]]*;/d' | grep -qE "'never-a-real-field\b"; then502
fail "arm 8's comment filter is not filtering -- branch prose can satisfy totality"503
fi504
echo "arm 8 OK: $datum_checked declared fields all serialised; slicer proven to see and to miss"506
echo507
echo "F1A_GATE_GREEN arms=8 tests=$reported checks=$checks kinds=$kind_count core_files=$core_count fields=$datum_checked"