Commit graph

4 commits

Author SHA1 Message Date
3a89c3c5be 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 12:58:28 +02:00
a5ff873d3c lift-guard: eleven more classes, two of them regressions I introduced
Round-9 review (Claude, N1). The brief said "assume there are more"; there
were eleven, and two were introduced by the round-8 fix itself.

INTRODUCED BY THE ARITHMETIC TOKENISATION — the round-8 fix for a false
NEGATIVE created two false POSITIVES. The interior of `$(( ))` was tokenised
with `[A-Za-z_][A-Za-z0-9_]*`, which starts matching at the letter-bearing tail
of a numeric literal:

    echo $((0x1F))   ->  FATAL: reads x1F
    echo $((1e3))    ->  FATAL: reads e3

Now anchored so a match cannot begin after a digit or word character.

INTRODUCED BY THE INDIRECT-EXPANSION REFUSAL, and this is the one that matters.
`${!...}` has three meanings and `re.search(r'\$\{!')` cannot tell them apart:

    ${!name}                indirect expansion    — genuinely unanalysable
    ${!arr[@]} ${!arr[*]}   array KEY expansion   — ordinary
    ${!prefix*} ${!prefix@} variable-NAME listing — ordinary

A refusal is the most expensive verdict this tool has — it hard-fails the lift
— and it was firing on two ordinary constructs with a diagnostic naming a
feature they do not use. The reviewer found it LIVE: ltl-accumulator
check.sh:274 is `for cert in "${!CONES[@]}"`, so the day lift-guard is ported
there, any lift covering that line would have refused. The four forks carry
five arrays each, so it was one ordinary edit away from firing there too.
Now matched only for genuine `${!name}`.

SEVEN MORE BINDING FORMS the driver uses and the guard demanded anyway:

    let FOO=1 · (( FOO = 1 )) · BAR+=b · FOO[0]=x
    for (( i=0; i<3; i++ )) · select FOO in · getopts "o" FOO

Arithmetic contexts bind as well as read, so `(( i++ ))` and the C-style `for`
now contribute to assigns — without that, the reads-extraction added by the
round-8 fix demanded the very names those expressions assign.

Verified: all eleven silent, genuine `${!n}` still refuses, `$((X+1))` and
`((Y>0))` still caught by name, the whole round-8 matrix unchanged, a genuine
missing variable still fails, and the four lifting self-tests green in all four
forks.

The reviewer also discarded one candidate rather than report it — `i=0;
(( i++ ))` looks like a demand but is silent under driver ⊇ payload, which is
how the self-tests invoke it. That restraint is worth recording: a guard edited
twice for false alarms should not be handed a false alarm by its reviewer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-03 21:03:35 +02:00
6a0a9b9fe6 lift-guard: close all nine classes the reviewer demonstrated
Round-8 review (Claude, register key `lift-guard-regex-both-directions`).
Every class reproduced here before fixing, and re-tested after.

THREE FALSE NEGATIVES — the payload reads a name and the guard stayed silent,
which is the direction that costs something, because silence is what the tool
exists to prevent:

  echo $((X + 1))     arithmetic expansion reads X without a `$` before the
  (( Y > 0 ))         name, and the read pattern cannot match it: the
                      character after `$` is `(`. Both contexts are now
                      tokenised. `if [ $((inm + ins)) -eq 0 ]` is already live
                      at check.sh:464 — not lifted today, so latent, not absent.

  n=Q; ${!n}          indirect expansion defeats text analysis outright. The
                      guard now REFUSES the lift rather than passing it. Its
                      contract is "does not miss a dependency"; where it cannot
                      honour that it must say so, not shrug.

SIX FALSE POSITIVES — the driver defines the name and the guard cried wolf.
This direction matters too: a guard that raises false alarms gets edited away,
and then it guards nothing.

  case x in a) FOO=1 ;;      `)` added to the assignment delimiters
  if …; else FOO=1; fi       `else` added
  ! FOO=1                    `!` added
  mapfile -t FOO             binds a name with no `=` at all
  readarray -t FOO           likewise
  printf -v FOO "x"          likewise

The banner also over-claimed. It read as a completeness statement about
LIFTING; it is a completeness statement about VARIABLES. A lifted payload also
inherits functions, shell options, traps and a working directory, and this tool
models none of them — loud failures under `set -e`, but the header now says so
rather than implying otherwise.

Verified: all nine classes behave correctly, a genuine missing variable is
still caught by name, and the seven lifting self-tests pass in dalek plus the
four fast ones in each ported fork.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-03 13:14:20 +02:00
6b79ac835c verification: derive lift dependencies instead of hand-keeping them
Round-9 swept all ten instruments per fork instead of only the two buttons,
and found selftest-axgate.sh red in all four. Root cause, identical to the
KERNLOG defect fixed the day before: a self-test lifts one phase out of
check.sh and supplies its variables from a HAND-WRITTEN preamble, and the
phase later grew a dependency nobody told the preamble about.

Phase 2b stopped globbing Proofs/*.olean and started reading $PROOFS by
membership. Bash does not error on an unset array under `set -u` — it
expands to nothing — so the gate silently received

    expected := [".olean"]

one entry with an empty name, and rejected the baseline via its own
fail-closed absence check. Exit code right, reason wrong.

  · lift-guard.sh (new, pinned): derives every variable the lifted payload
    reads, subtracts what the driver defines, and fails AT LIFT TIME naming
    the remainder. Wired into all five lifting self-tests; selftest-shapes.sh
    drops its inline copy. One implementation, not five, since drifting out
    of sync is the entire failure mode.

  · selftest-axgate.sh: lifts PROOFS verbatim, and its case 2 now poisons a
    MANIFESTED leaf module rather than adding a stray file. The stray-file
    attack was a no-op against membership semantics — that is the dead-file
    gate's job, proven by selftest-harness case 8 — and it is also the
    weaker attack, since adding files to Proofs/ must clear a gate that
    editing an already-manifested module does not.

  · Phase 2c's accounting identity moves behind its own marker with its own
    ACCTFAIL verdict, so the phase is liftable by construction rather than
    by a self-test knowing where to stop. Truncating the lift range alone
    would have dropped Phase 2c's own verdict and left the test unable to fail.

  · The other four lifting self-tests assert on the PAYLOAD rather than the
    assembled driver, so a marker in the preamble cannot satisfy a check
    meant to prove the lift landed.

Certified: 8/8 self-tests green in all four forks (~212 assertions, none
failing); check.sh bytes unchanged and still matching the pin written before
the 3h08m round-9 sweep, whose 36 GREEN button rows therefore stand.
--audit-only re-run against the new HARNESS.sha256 in every fork.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-02 13:07:28 +02:00