audit: bind the scalar statements, and make the accounting identity mean audit
Closes four round-7/8 findings. Certified by the round-12 sweep: five
repositories, both buttons and every self-test, 48/48 GREEN.
── `scalar-statements-unbound` (gpt, round 7, CRITICAL) ────────────────────
The main button bound its 31 certificates' elaborated statements and reachable
specification bodies. This button bound NONE of its thirteen, while
TRUSTED-BASE item 8 said the audit covers "every certificate" — false across
the 44-certificate surface. The finding was raised in round 7, lost from the
round-8 work list by an F-number collision between two reviewers, and re-raised
in round 8.
Proofs/ScalarAudit.lean is generated from each fork's OWN Audit.lean, so the
canonicalisation is provably the same code: pp.all rendering, whitespace
normalisation, transitive specification closure. check-scalar.sh Phase 3c pins
the block's digest, requires the committed copy to match byte-for-byte so a
mismatch can be DIFFED, and cross-checks the auditor's certificate set against
the button's CERTS array.
dalek ecf3a3f8 · anza 0d942e47 · risc0 4b550a61 · betrusted 4b550a61
risc0 and betrusted share a digest and that is correct, not a collision: their
ScalarSubSpec.lean differs only in doc prose and in `black_box` entries inside
`simp only [...]` lists AFTER `:= by`. Proof scripts. They bind the same
statements over the same specifications, which is the documented scope.
selftest-scalar-statements.sh ships the two attacks the reviewer asked for:
ok gutted statement caught (cone unchanged)
ok rewritten specification body caught (name and cone unchanged)
The second rewrites a reachable reference body to `id (…)` — DEFINITIONALLY
EQUAL, so the corpus compiles and every proof typechecks and the cone is
byte-identical. Every earlier phase is blind to it.
── `drv-surface-no-cones` + `accounting-certifies-enumeration` (claude) ────
The round-7 accounting identity proved every kernel constant was ENUMERATED.
The reviewer showed enumeration is not audit: their planted claim WAS
enumerated, as DRV|LTLAccAudit.bait.smuggled|theorem with a real cone, and
nothing examined it — rows had no cone, no allowlist covered them, the
statement digest does not reach instruments, and Phase 2b gates DECLARED
AXIOMS, a different question. "Progress of one step, not two."
DRV rows now carry their axiom cone and are pinned in driver-allowlist.txt by
inventory_gate.sh with a DRV tag — the same implementation that pins the
corpus, in both directions, because a second copy of a coverage gate is a
second thing to drift. The axiom policy is per-surface and enforced per
surface: the corpus admits exactly the sanctioned boundary, the instruments
admit none, and an instrument axiom fails EVEN WHEN ALLOWLISTED.
Verified with the reviewer's own payload, both placements:
before the walk -> UNCLASSIFIED: DRV|…|bait.smuggled|theorem|Classical.choice,Quot.sound,propext
after the walk -> ACCOUNTING FAILED names it (kernel-side)
── `drv-naming-heuristic` (claude, round 7) ────────────────────────────────
Retired as load-bearing rather than patched. The rule admits a theorem whose
name extends a constant declared alongside it, and "breaks in one line" —
declare `def bait`, then `theorem bait.smuggled` walks through. It stays as a
fast readable first check; membership in a committed allowlist is what now
carries the weight, and a new row fails closed whatever it is called.
── what round 11 caught, which was mine ───────────────────────────────────
DRV rows first shipped WITHOUT their originating driver. dalek and anza run
two drivers, each declaring its own `corpus`; keyed on name alone those two
distinct declarations produced one byte-identical row, `sort -u` collapsed
them, and the trailers summed to 37 against 36. The estate had already learned
this on the corpus walk — INV rows carry their module because two modules both
declare CurveFieldProofs.zero_spec — and I rebuilt the record without it.
Rows now carry their driver, and the gate FAILS CLOSED ON DUPLICATE RECORDS
naming the collision: two declarations sharing one entry means one is covered
by the other's, which is exactly how a real declaration hides. The trailer
now checks what the drivers EMITTED, not what survives de-duplication —
conflating "the run was truncated" with "two rows were identical" is what let
a record-format defect present itself as an arithmetic complaint.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-03 10:15:26 +00:00
|
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
# selftest-scalar-statements.sh — adversarial self-test for check-scalar.sh
|
|
|
|
|
|
# Phase 3c, the scalar statement + specification binding.
|
|
|
|
|
|
#
|
|
|
|
|
|
# WHY THIS EXISTS. Round-7 review (GPT-5.6, register key
|
|
|
|
|
|
# `scalar-statements-unbound`, CRITICAL): the main button bound its 31
|
|
|
|
|
|
# certificates' elaborated statements and reachable specification bodies; this
|
|
|
|
|
|
# repository's scalar button bound NONE of its thirteen, while TRUSTED-BASE
|
|
|
|
|
|
# item 8 said the audit covers "every certificate". The binding was added in
|
|
|
|
|
|
# the same commit as this file. The reviewer asked for exactly two shipping
|
|
|
|
|
|
# attacks, and this file is those two:
|
|
|
|
|
|
#
|
|
|
|
|
|
# 1. same-cone theorem statement gutting;
|
|
|
|
|
|
# 2. a reachable reference body rewritten while name and cone remain fixed.
|
|
|
|
|
|
#
|
|
|
|
|
|
# Both are invisible to every earlier phase by construction. Phase 2b sees no
|
|
|
|
|
|
# new axiom; Phase 3's exact-cone audit sees the same three axioms; only the
|
|
|
|
|
|
# statement binding sees them.
|
|
|
|
|
|
#
|
|
|
|
|
|
# It extracts Phase 3c out of check-scalar.sh at run time, so it attacks THE
|
|
|
|
|
|
# SHIPPING GATE rather than a copy that can drift away from it.
|
|
|
|
|
|
#
|
|
|
|
|
|
# Requires a prior green scalar build. Recompiling the corpus is the expensive
|
|
|
|
|
|
# part; SKIP_SLOW=1 runs only the fast cases and SAYS SO rather than passing
|
|
|
|
|
|
# quietly over the two that matter.
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
set -uo pipefail
|
|
|
|
|
|
source ~/aeneas-toolchain/env.sh
|
|
|
|
|
|
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
|
|
|
|
AENEAS_LEAN="$AENEAS_HOME/backends/lean"
|
|
|
|
|
|
TIMEOUT="${LEAN_TIMEOUT:-900}"
|
|
|
|
|
|
export LEAN_MEM_MB="${LEAN_MEM_MB:-8192}"
|
|
|
|
|
|
CORES="${LEAN_MAX_CORES:-0-3}"
|
|
|
|
|
|
SKIP_SLOW="${SKIP_SLOW:-0}"
|
|
|
|
|
|
|
|
|
|
|
|
STASH="$(mktemp -d)"
|
|
|
|
|
|
FAILURES=0
|
|
|
|
|
|
TREE_AT_START="$(cd "$HERE/.." && git status --porcelain)"
|
|
|
|
|
|
|
|
|
|
|
|
cleanup() {
|
|
|
|
|
|
[ -f "$STASH/gut.lean" ] && cp "$STASH/gut.lean" "$HERE/Proofs/ScalarMain.lean"
|
|
|
|
|
|
[ -f "$STASH/den.lean" ] && cp "$STASH/den.lean" "$HERE/Proofs/ScalarDenote.lean"
|
|
|
|
|
|
rm -rf "$STASH"
|
|
|
|
|
|
rm -f "$HERE"/.scalar-audit-manifest.observed
|
|
|
|
|
|
}
|
|
|
|
|
|
trap cleanup EXIT INT TERM
|
|
|
|
|
|
|
|
|
|
|
|
DRIVER="$STASH/phase3c.sh"
|
|
|
|
|
|
PAYLOAD="$STASH/payload.sh"
|
|
|
|
|
|
build_driver() {
|
|
|
|
|
|
awk '/^# ── Phase 3c/{f=1} f&&/^# ── (Phase |Phases end)/&&!/Phase 3c/{exit} f{print}' \
|
|
|
|
|
|
"$HERE/check-scalar.sh" > "$PAYLOAD"
|
verification: lifted phases run under the buttons shell options, enforced in lift-guard
A lift is evidence about the shipping gate only if it executes the way the
shipping gate executes. Every button here runs `set -euo pipefail`. Eighteen
lift sites prefixed their driver with `set -uo pipefail` and no -e — four per
fork (selftest-harness, selftest-scalar-statements, selftest-statements twice)
and both accumulator lifts — while sixteen other sites got it right. The estate
did it both ways, which is what made this a defect and not a convention.
Without -e a failing command does not abort: execution continues and the driver
returns the LAST command status. A lifted phase can therefore reach a verdict
the shipping phase would never reach while the self-test reports the gate
"works". selftest_statements.sh:27 claims the tested logic IS the shipping
logic; in this respect it was not.
NO VERDICT CHANGED. Every self-test that passed before passes now, at the same
timings, so -e was not masking a false green at these commits. The defect was
fidelity and the claim is no larger than that.
The durable part is not the 18 edits. lift-guard.sh already inspects every
driver, so it now REFUSES any lift whose driver does not enable errexit and
prints the drivers actual shell options. Negative-tested four ways: no -e
rejects, `set -euo` passes, bare `set -e` passes, no set line at all rejects.
Byte-identical across the four forks. The nineteenth lift someone writes fails
closed rather than silently testing a more permissive shell.
Certified by the round-15 sweep: 50/50 GREEN, six repositories, both buttons
and every self-test, exit 0.
Registered and NOT fixed here: ltl-accumulator-verified has no lift-guard.sh
at all and calls it zero times, so its two lifts remain unguarded — the one
place this class can still recur silently.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-04 10:58:29 +00:00
|
|
|
|
{ echo 'set -euo pipefail' # -e matches the button; see lift-drivers-drop-errexit
|
audit: bind the scalar statements, and make the accounting identity mean audit
Closes four round-7/8 findings. Certified by the round-12 sweep: five
repositories, both buttons and every self-test, 48/48 GREEN.
── `scalar-statements-unbound` (gpt, round 7, CRITICAL) ────────────────────
The main button bound its 31 certificates' elaborated statements and reachable
specification bodies. This button bound NONE of its thirteen, while
TRUSTED-BASE item 8 said the audit covers "every certificate" — false across
the 44-certificate surface. The finding was raised in round 7, lost from the
round-8 work list by an F-number collision between two reviewers, and re-raised
in round 8.
Proofs/ScalarAudit.lean is generated from each fork's OWN Audit.lean, so the
canonicalisation is provably the same code: pp.all rendering, whitespace
normalisation, transitive specification closure. check-scalar.sh Phase 3c pins
the block's digest, requires the committed copy to match byte-for-byte so a
mismatch can be DIFFED, and cross-checks the auditor's certificate set against
the button's CERTS array.
dalek ecf3a3f8 · anza 0d942e47 · risc0 4b550a61 · betrusted 4b550a61
risc0 and betrusted share a digest and that is correct, not a collision: their
ScalarSubSpec.lean differs only in doc prose and in `black_box` entries inside
`simp only [...]` lists AFTER `:= by`. Proof scripts. They bind the same
statements over the same specifications, which is the documented scope.
selftest-scalar-statements.sh ships the two attacks the reviewer asked for:
ok gutted statement caught (cone unchanged)
ok rewritten specification body caught (name and cone unchanged)
The second rewrites a reachable reference body to `id (…)` — DEFINITIONALLY
EQUAL, so the corpus compiles and every proof typechecks and the cone is
byte-identical. Every earlier phase is blind to it.
── `drv-surface-no-cones` + `accounting-certifies-enumeration` (claude) ────
The round-7 accounting identity proved every kernel constant was ENUMERATED.
The reviewer showed enumeration is not audit: their planted claim WAS
enumerated, as DRV|LTLAccAudit.bait.smuggled|theorem with a real cone, and
nothing examined it — rows had no cone, no allowlist covered them, the
statement digest does not reach instruments, and Phase 2b gates DECLARED
AXIOMS, a different question. "Progress of one step, not two."
DRV rows now carry their axiom cone and are pinned in driver-allowlist.txt by
inventory_gate.sh with a DRV tag — the same implementation that pins the
corpus, in both directions, because a second copy of a coverage gate is a
second thing to drift. The axiom policy is per-surface and enforced per
surface: the corpus admits exactly the sanctioned boundary, the instruments
admit none, and an instrument axiom fails EVEN WHEN ALLOWLISTED.
Verified with the reviewer's own payload, both placements:
before the walk -> UNCLASSIFIED: DRV|…|bait.smuggled|theorem|Classical.choice,Quot.sound,propext
after the walk -> ACCOUNTING FAILED names it (kernel-side)
── `drv-naming-heuristic` (claude, round 7) ────────────────────────────────
Retired as load-bearing rather than patched. The rule admits a theorem whose
name extends a constant declared alongside it, and "breaks in one line" —
declare `def bait`, then `theorem bait.smuggled` walks through. It stays as a
fast readable first check; membership in a committed allowlist is what now
carries the weight, and a new row fails closed whatever it is called.
── what round 11 caught, which was mine ───────────────────────────────────
DRV rows first shipped WITHOUT their originating driver. dalek and anza run
two drivers, each declaring its own `corpus`; keyed on name alone those two
distinct declarations produced one byte-identical row, `sort -u` collapsed
them, and the trailers summed to 37 against 36. The estate had already learned
this on the corpus walk — INV rows carry their module because two modules both
declare CurveFieldProofs.zero_spec — and I rebuilt the record without it.
Rows now carry their driver, and the gate FAILS CLOSED ON DUPLICATE RECORDS
naming the collision: two declarations sharing one entry means one is covered
by the other's, which is exactly how a real declaration hides. The trailer
now checks what the drivers EMITTED, not what survives de-duplication —
conflating "the run was truncated" with "two rows were identical" is what let
a record-format defect present itself as an arithmetic complaint.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-03 10:15:26 +00:00
|
|
|
|
echo 'source ~/aeneas-toolchain/env.sh'
|
|
|
|
|
|
echo "HERE=\"$HERE\""
|
|
|
|
|
|
echo 'AENEAS_LEAN="$AENEAS_HOME/backends/lean"'
|
|
|
|
|
|
echo "TIMEOUT=$TIMEOUT; CORES=\"$CORES\""
|
|
|
|
|
|
# CERTS is referenced by the cross-check inside Phase 3c. Lifted VERBATIM
|
|
|
|
|
|
# rather than re-derived, so this test cannot drift from the button's set.
|
|
|
|
|
|
sed -n '/^CERTS=(/,/^)/p' "$HERE/check-scalar.sh"
|
|
|
|
|
|
cat "$PAYLOAD"
|
|
|
|
|
|
} > "$DRIVER"
|
|
|
|
|
|
if [ "$(wc -l < "$PAYLOAD")" -lt 40 ]; then
|
|
|
|
|
|
echo "FATAL: could not lift Phase 3c out of check-scalar.sh — the markers moved."
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
fi
|
|
|
|
|
|
grep -qF 'SCALAR-AUDIT-MANIFEST-BEGIN' "$PAYLOAD" || {
|
|
|
|
|
|
echo "FATAL: the lifted payload does not read the scalar audit block."; exit 1; }
|
|
|
|
|
|
"$HERE/lift-guard.sh" "$PAYLOAD" "$DRIVER" "check-scalar.sh Phase 3c" || exit 1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
recompile() { # recompile <module>
|
|
|
|
|
|
( cd "$AENEAS_LEAN" && lake env bash -c "
|
|
|
|
|
|
set -uo pipefail
|
|
|
|
|
|
cd '$HERE/gen' && export LEAN_PATH=\"\$LEAN_PATH:\$PWD:$HERE\"
|
|
|
|
|
|
cd '$HERE'
|
|
|
|
|
|
LEAN_TIMEOUT=$TIMEOUT LEAN_MAX_CORES=$CORES '$HERE/lean-guard' Proofs/$1.lean
|
|
|
|
|
|
" ) >/dev/null 2>&1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
expect() { # expect <name> <expected-rc> <required-substring>
|
|
|
|
|
|
local name="$1" want_rc="$2" want_txt="$3" out rc
|
|
|
|
|
|
out=$(bash "$DRIVER" 2>&1); rc=$?
|
|
|
|
|
|
if [ "$rc" -ne "$want_rc" ]; then
|
|
|
|
|
|
echo " FAIL $name: exit $rc, expected $want_rc"
|
|
|
|
|
|
tail -6 <<<"$out" | sed 's/^/ /'; FAILURES=$((FAILURES+1)); return
|
|
|
|
|
|
fi
|
|
|
|
|
|
if ! grep -qF "$want_txt" <<<"$out"; then
|
|
|
|
|
|
echo " FAIL $name: exit code right but diagnostic wrong (rejected for the wrong reason)"
|
|
|
|
|
|
echo " wanted substring: $want_txt"
|
|
|
|
|
|
echo " got: $(tr '\n' '|' <<<"$out" | cut -c1-260)"
|
|
|
|
|
|
FAILURES=$((FAILURES+1)); return
|
|
|
|
|
|
fi
|
|
|
|
|
|
echo " ok $name"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
echo "=== selftest-scalar-statements: attacking check-scalar.sh Phase 3c ==="
|
|
|
|
|
|
build_driver
|
|
|
|
|
|
|
|
|
|
|
|
# ── 1. Baseline: the untouched repository passes and reports what it bound.
|
|
|
|
|
|
expect "baseline green, statements bound" 0 "statements + reachable specification bodies bound"
|
|
|
|
|
|
|
|
|
|
|
|
# ── 2. HAND-EDIT THE COMMITTED BLOCK. The digest still matches what Lean
|
|
|
|
|
|
# emits, so only the committed-copy comparison can see this.
|
|
|
|
|
|
cp "$HERE/SCALAR-AUDIT-MANIFEST.txt" "$STASH/manifest.bak"
|
|
|
|
|
|
sed -i '2s/$/ TAMPERED/' "$HERE/SCALAR-AUDIT-MANIFEST.txt"
|
|
|
|
|
|
expect "hand-edited committed block caught" 1 "does not match the emitted block"
|
|
|
|
|
|
cp "$STASH/manifest.bak" "$HERE/SCALAR-AUDIT-MANIFEST.txt"
|
|
|
|
|
|
|
|
|
|
|
|
if [ "$SKIP_SLOW" = "1" ]; then
|
|
|
|
|
|
echo " SKIPPED (SKIP_SLOW=1): the two attacks this file exists for — gutted"
|
|
|
|
|
|
echo " statement and rewritten specification body — were NOT run. The fast"
|
|
|
|
|
|
echo " case above does not cover either of them."
|
|
|
|
|
|
else
|
|
|
|
|
|
# ── 3. ATTACK ONE: gut a certificate's STATEMENT, preserving its axiom cone.
|
|
|
|
|
|
# ScalarProofs.scalarImplementation is the aggregate and is TERMINAL —
|
|
|
|
|
|
# nothing outside its own module consumes it. Gutting a load-bearing
|
|
|
|
|
|
# certificate would simply break its consumers, which demonstrates the
|
|
|
|
|
|
# compiler working, not this gate.
|
|
|
|
|
|
cp "$HERE/Proofs/ScalarMain.lean" "$STASH/gut.lean"
|
|
|
|
|
|
python3 - "$HERE/Proofs/ScalarMain.lean" <<'PY'
|
|
|
|
|
|
import sys, re
|
|
|
|
|
|
f = sys.argv[1]
|
|
|
|
|
|
s = open(f).read()
|
|
|
|
|
|
m = re.search(r'^theorem scalarImplementation\b', s, re.M)
|
|
|
|
|
|
assert m, "scalarImplementation not found"
|
|
|
|
|
|
i = m.start()
|
|
|
|
|
|
nxt = re.search(r'^(theorem|lemma|def|noncomputable def|end|/--|@\[)', s[i+10:], re.M)
|
|
|
|
|
|
assert nxt, "no following declaration"
|
|
|
|
|
|
j = i + 10 + nxt.start()
|
|
|
|
|
|
# Same cone (Classical.em pulls in Classical.choice/propext), utterly different
|
|
|
|
|
|
# claim. Every earlier phase is satisfied; only the statement binding is not.
|
|
|
|
|
|
gut = "theorem scalarImplementation : (∀ p : Prop, p ∨ ¬p) := Classical.em\n\n"
|
|
|
|
|
|
open(f, "w").write(s[:i] + gut + s[j:])
|
|
|
|
|
|
PY
|
|
|
|
|
|
recompile ScalarMain || { echo " FAIL setup: the gutted module did not compile"; FAILURES=$((FAILURES+1)); }
|
|
|
|
|
|
recompile ScalarAudit
|
|
|
|
|
|
expect "gutted statement caught (cone unchanged)" 1 "audit-manifest digest mismatch"
|
|
|
|
|
|
cp "$STASH/gut.lean" "$HERE/Proofs/ScalarMain.lean"; rm -f "$STASH/gut.lean"
|
|
|
|
|
|
recompile ScalarMain; recompile ScalarAudit
|
|
|
|
|
|
|
|
|
|
|
|
# ── 4. ATTACK TWO: rewrite a REACHABLE SPECIFICATION BODY while the
|
|
|
|
|
|
# certificate's name and cone stay fixed. This is the attack the whole
|
|
|
|
|
|
# block exists for: if a reference definition can be edited without
|
|
|
|
|
|
# notice, a certificate can be made to say `loop = loop` and every cone
|
|
|
|
|
|
# stays byte-identical.
|
|
|
|
|
|
#
|
|
|
|
|
|
# scDenote is reachable from the scalar statements and its body is
|
|
|
|
|
|
# rewritten here to `id (…)`, which is DEFINITIONALLY EQUAL — so the
|
|
|
|
|
|
# corpus still compiles and every proof still typechecks. That is the
|
|
|
|
|
|
# point: the binding must be sensitive to the body AS WRITTEN, not
|
|
|
|
|
|
# merely to what it evaluates to. If the setup fails to compile this
|
|
|
|
|
|
# case reports FAIL rather than passing quietly.
|
|
|
|
|
|
cp "$HERE/Proofs/ScalarDenote.lean" "$STASH/den.lean"
|
|
|
|
|
|
python3 - "$HERE/Proofs/ScalarDenote.lean" <<'PY'
|
|
|
|
|
|
import sys
|
|
|
|
|
|
f = sys.argv[1]
|
|
|
|
|
|
s = open(f).read()
|
|
|
|
|
|
old = "def scDenote (a : Sc) : ZMod Ell := (scVal a : ZMod Ell)"
|
|
|
|
|
|
assert old in s, "scDenote body not in the expected form"
|
|
|
|
|
|
new = "def scDenote (a : Sc) : ZMod Ell := id (scVal a : ZMod Ell)"
|
|
|
|
|
|
open(f, "w").write(s.replace(old, new, 1))
|
|
|
|
|
|
PY
|
|
|
|
|
|
if recompile ScalarDenote; then
|
|
|
|
|
|
recompile ScalarAudit
|
|
|
|
|
|
expect "rewritten specification body caught (name and cone unchanged)" 1 \
|
|
|
|
|
|
"audit-manifest digest mismatch"
|
|
|
|
|
|
else
|
|
|
|
|
|
echo " FAIL setup: the rewritten specification body did not compile —"
|
|
|
|
|
|
echo " this attack did NOT exercise the gate. Do not read the"
|
|
|
|
|
|
echo " surrounding passes as covering it."
|
|
|
|
|
|
FAILURES=$((FAILURES+1))
|
|
|
|
|
|
fi
|
|
|
|
|
|
cp "$STASH/den.lean" "$HERE/Proofs/ScalarDenote.lean"; rm -f "$STASH/den.lean"
|
|
|
|
|
|
recompile ScalarDenote; recompile ScalarAudit
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
# ── 5. Restored: green again, and the working tree is as we found it.
|
|
|
|
|
|
expect "restored to green" 0 "statements + reachable specification bodies bound"
|
|
|
|
|
|
TREE_NOW="$(cd "$HERE/.." && git status --porcelain)"
|
|
|
|
|
|
if [ "$TREE_AT_START" != "$TREE_NOW" ]; then
|
|
|
|
|
|
echo " FAIL restore: the working tree differs from how this test found it:"
|
|
|
|
|
|
diff <(printf '%s\n' "$TREE_AT_START") <(printf '%s\n' "$TREE_NOW") | sed 's/^/ /'
|
|
|
|
|
|
FAILURES=$((FAILURES+1))
|
|
|
|
|
|
else
|
|
|
|
|
|
echo " ok working tree restored to its starting state"
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
echo ""
|
|
|
|
|
|
if [ "$FAILURES" -eq 0 ]; then
|
|
|
|
|
|
echo "SELFTEST PASSED — scalar Phase 3c rejects statement- and specification-"
|
|
|
|
|
|
echo "level tampering that moves no axiom cone, for the stated reason."
|
|
|
|
|
|
exit 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
echo "SELFTEST FAILED: $FAILURES check(s) did not behave as claimed."
|
|
|
|
|
|
exit 1
|