release.verdict-vocabulary-shared explains the sentinel with a set -u mechanism that measurement refutes #886

Closed
opened 2026-08-30 02:00:22 +02:00 by timothy · 1 comment
Owner

docs/decisions/records/release/verdict-vocabulary-shared.md explains the vocabulary sentinel with a mechanism that measurement refutes.

The claim

:47-48:

under set -u an unbound-variable error inside a FUNCTION aborts the function but not the script, and in that form NEITHER branch is then taken — execution continues straight past the gate

and :57:

A control-flow gate can be skipped by an abort; a value that was never assigned cannot be read.

The measurement

An unbound-variable error inside a function EXITS the shell. Measured on bash 3.2.57 and 5.3.15, across set -u alone, set -eu, and set -euo pipefail:

f() { echo \"in f\"; echo \"val=$DEFINITELY_UNSET\"; echo \"after-in-f\"; }
if ! f; then echo \"TOOK-THEN\"; else echo \"TOOK-ELSE\"; fi
echo \"REACHED-END\"

[set -u]            in f / DEFINITELY_UNSET: unbound variable / exit=127
[set -eu]           in f / DEFINITELY_UNSET: unbound variable / exit=1
[set -euo pipefail] in f / DEFINITELY_UNSET: unbound variable / exit=1

No TOOK-*, no REACHED-END, on either shell, in any mode. The set -u-alone row is the one that matters: the record justifies the concern by noting the read side deliberately runs without set -e, and the outcome is the same there. The old reading holds ONLY inside a command substitution (v=$(f) || v=FALLBACK continues, fallback taken), which is not the shape gating either file.

What is NOT in question

The fail-open the record documents is REAL: with .* in the positive list and the negative list written as a scalar, check-review-verdict.sh classified an explicit BLOCKED @ <head> as positive, exit 0. Only the attributed MECHANISM is wrong — \${#name[@]} on a scalar yields 1 rather than erroring, so no abort occurred in that scenario. The sentinel remains the right design; its surviving ground is the caller that never runs validation.

Scope — narrowed

ersatztv#845 corrected every CODE and TEST site: scripts/lib/h10-reviewers.sh (which had copied it), scripts/lib/review-verdict-vocabulary.sh (the source it was copied from, including a residual assertion seven lines below the retraction), scripts/check-review-verdict.sh (whose comment pointed readers AT the retracting library while asserting the opposite), scripts/tests/test_review_verdict_vocabulary.py, and docs/ci-cd.md.

What remains is the DECISION RECORD alone. Editing one is its own change with its own review surface, which is why it was not folded into #845.

Done-when

  • verdict-vocabulary-shared.md:47-48 and :57 state the measured behaviour, and the sentinel is justified on the ground that survives
  • The record keeps the measured fail-open it documents — only the mechanism attribution changes
  • Adversarial review passed
`docs/decisions/records/release/verdict-vocabulary-shared.md` explains the vocabulary sentinel with a mechanism that measurement refutes. ## The claim `:47-48`: > under `set -u` an unbound-variable error inside a FUNCTION aborts the function but not the script, and in that form NEITHER branch is then taken — execution continues straight past the gate and `:57`: > A control-flow gate can be skipped by an abort; a value that was never assigned cannot be read. ## The measurement An unbound-variable error inside a function EXITS the shell. Measured on bash 3.2.57 and 5.3.15, across `set -u` alone, `set -eu`, and `set -euo pipefail`: ``` f() { echo \"in f\"; echo \"val=$DEFINITELY_UNSET\"; echo \"after-in-f\"; } if ! f; then echo \"TOOK-THEN\"; else echo \"TOOK-ELSE\"; fi echo \"REACHED-END\" [set -u] in f / DEFINITELY_UNSET: unbound variable / exit=127 [set -eu] in f / DEFINITELY_UNSET: unbound variable / exit=1 [set -euo pipefail] in f / DEFINITELY_UNSET: unbound variable / exit=1 ``` No `TOOK-*`, no `REACHED-END`, on either shell, in any mode. The `set -u`-alone row is the one that matters: the record justifies the concern by noting the read side deliberately runs without `set -e`, and the outcome is the same there. The old reading holds ONLY inside a command substitution (`v=$(f) || v=FALLBACK` continues, fallback taken), which is not the shape gating either file. ## What is NOT in question The fail-open the record documents is REAL: with `.*` in the positive list and the negative list written as a scalar, `check-review-verdict.sh` classified an explicit `BLOCKED @ <head>` as `positive`, exit 0. Only the attributed MECHANISM is wrong — `\${#name[@]}` on a scalar yields 1 rather than erroring, so no abort occurred in that scenario. The sentinel remains the right design; its surviving ground is the caller that never runs validation. ## Scope — narrowed ersatztv#845 corrected every CODE and TEST site: `scripts/lib/h10-reviewers.sh` (which had copied it), `scripts/lib/review-verdict-vocabulary.sh` (the source it was copied from, including a residual assertion seven lines below the retraction), `scripts/check-review-verdict.sh` (whose comment pointed readers AT the retracting library while asserting the opposite), `scripts/tests/test_review_verdict_vocabulary.py`, and `docs/ci-cd.md`. What remains is the DECISION RECORD alone. Editing one is its own change with its own review surface, which is why it was not folded into #845. ## Done-when - [ ] `verdict-vocabulary-shared.md:47-48` and `:57` state the measured behaviour, and the sentinel is justified on the ground that survives - [ ] The record keeps the measured fail-open it documents — only the mechanism attribution changes - [ ] Adversarial review passed
timothy added the ci-cdpriority: low labels 2026-08-30 02:00:33 +02:00
Author
Owner

Closing as INVALID. The record is correct; the premise of this issue was my own mis-measurement.

What I got wrong

I probed the set -u behaviour with a PLAIN unset variable reference and generalised to ${#arr[@]}, the expansion the validator actually uses. Those are different shapes and they behave differently. Measured on bash 3.2.57 and 5.3.15, if ! f form, recording which arm ran and whether execution continued:

shape                        set -u                    set -eu
plain   $UNSET               exits, no branch          exits, no branch
unset   ${#UNSET_ARR[@]}     CONTINUES, no branch      exits, no branch
scalar  ${#SCALAR[@]}        CONTINUES, no branch      exits, no branch

Stated as behaviour rather than exit codes on purpose: the exit STATUS of the plain-unset row is invocation-dependent (127 under bash -c, 1 when the same probe runs from a script file), so the number is not a stable fact. What is stable is the arm taken and whether anything after the gate runs.

So the record is right exactly as written: under set -u an unbound-variable error inside a FUNCTION aborts the function but not the script, neither branch of if ! validate is taken, and execution continues past the gate. That is the shape the validator uses (${#ETV_VERDICT_POSITIVE_WORDS[@]}) and the flag set its consumer runs — scripts/check-review-verdict.sh is set -uo pipefail, deliberately without -e. The record's own frontmatter already carries the nuance ("nounset-safe only on a declared-empty array, and fatal on an unset name or a scalar"), which the measurement confirms.

The end-to-end confirmation is the repo's own test: test_MUTATION_with_the_array_assertion_disarmed_the_SENTINEL_still_refuses asserts "not validated" appears in stderr, and only the derived views emit that, AFTER the gate. It passes because execution continues past the gate.

What happened in ersatztv#845

#845 briefly "corrected" the claim in five places on the strength of the wrong probe. Cold review caught it before the branch was pushed, and every site was reverted — scripts/lib/review-verdict-vocabulary.sh and scripts/check-review-verdict.sh are byte-identical to main again, and the docs/ci-cd.md paragraph and test_review_verdict_vocabulary.py comment are restored verbatim. The one place the topic appears in new code is scripts/lib/h10-reviewers.sh, which states the route correctly and scopes it: real for ${#arr[@]} under set -u without -e (the vocabulary library's consumer), not reachable from post-review-verdict.sh, which runs set -euo pipefail where the shell exits on every shape.

Reusable

A runtime claim needs a probe that exercises the SAME CONSTRUCT, not merely the same flags. set -u is not one behaviour — it is one per expansion shape, and ${#name[@]} is the shape where the abort-and-continue is real. And report the OUTCOME (which arm ran, did execution continue) rather than an exit code, which can vary with how the probe was invoked.

Closing as INVALID. The record is correct; the premise of this issue was my own mis-measurement. ## What I got wrong I probed the `set -u` behaviour with a PLAIN unset variable reference and generalised to `${#arr[@]}`, the expansion the validator actually uses. Those are different shapes and they behave differently. Measured on bash 3.2.57 and 5.3.15, `if ! f` form, recording which arm ran and whether execution continued: ``` shape set -u set -eu plain $UNSET exits, no branch exits, no branch unset ${#UNSET_ARR[@]} CONTINUES, no branch exits, no branch scalar ${#SCALAR[@]} CONTINUES, no branch exits, no branch ``` Stated as behaviour rather than exit codes on purpose: the exit STATUS of the plain-unset row is invocation-dependent (127 under `bash -c`, 1 when the same probe runs from a script file), so the number is not a stable fact. What is stable is the arm taken and whether anything after the gate runs. So the record is right exactly as written: under `set -u` an unbound-variable error inside a FUNCTION aborts the function but not the script, neither branch of `if ! validate` is taken, and execution continues past the gate. That is the shape the validator uses (`${#ETV_VERDICT_POSITIVE_WORDS[@]}`) and the flag set its consumer runs — `scripts/check-review-verdict.sh` is `set -uo pipefail`, deliberately without `-e`. The record's own frontmatter already carries the nuance ("nounset-safe only on a *declared-empty* array, and fatal on an unset name or a scalar"), which the measurement confirms. The end-to-end confirmation is the repo's own test: `test_MUTATION_with_the_array_assertion_disarmed_the_SENTINEL_still_refuses` asserts `"not validated"` appears in stderr, and only the derived views emit that, AFTER the gate. It passes because execution continues past the gate. ## What happened in ersatztv#845 #845 briefly "corrected" the claim in five places on the strength of the wrong probe. Cold review caught it before the branch was pushed, and every site was reverted — `scripts/lib/review-verdict-vocabulary.sh` and `scripts/check-review-verdict.sh` are byte-identical to `main` again, and the `docs/ci-cd.md` paragraph and `test_review_verdict_vocabulary.py` comment are restored verbatim. The one place the topic appears in new code is `scripts/lib/h10-reviewers.sh`, which states the route correctly and scopes it: real for `${#arr[@]}` under `set -u` without `-e` (the vocabulary library's consumer), not reachable from `post-review-verdict.sh`, which runs `set -euo pipefail` where the shell exits on every shape. ## Reusable A runtime claim needs a probe that exercises the SAME CONSTRUCT, not merely the same flags. `set -u` is not one behaviour — it is one per expansion shape, and `${#name[@]}` is the shape where the abort-and-continue is real. And report the OUTCOME (which arm ran, did execution continue) rather than an exit code, which can vary with how the probe was invoked.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: timothy/ersatztv#886