Skip to content

ci(gates): two gates passed when their evidence vanished — make them fail closed - #584

Closed
avrabe wants to merge 1 commit into
mainfrom
ci/gates-fail-when-evidence-vanishes
Closed

avrabe wants to merge 1 commit into
mainfrom
ci/gates-fail-when-evidence-vanishes

Conversation

@avrabe

@avrabe avrabe commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

Prompted by jess#292 (an absence-assertion that passed on an empty log). A read-only audit of relay's workflows + called scripts found the class twice; both fixed and proven both ways against old and new.

1. spar.yml Analyze system roots (not required) — || true + grep ERROR only. Measured: a missing .aadl makes spar exit 1 printing Cannot read … with no ERROR line → gate passed having analysed nothing.

case old new
real models pass pass
missing input PASS fail
glob matching nothing PASS fail

No false-failure risk: all 5 roots exit 0 with 10–22 WARNINGs each.

2. check-float-discipline.sh (inside required Clippy) — a listed crate with no source was skipped silently; an empty tree printed OK.

case old new
real tree pass pass — scanned 22/22 crates, 52 files
empty tree PASS fail (22 flagged)
listed crate renamed away skipped fail
f64 in flight code fail fail

(The f64 control was wrong the first time — it landed in the gate's own cfg(test) exemption and read 0 hits for both; placed in real code, both catch it.)

Named, not fixed: verification-gate's "0 artifacts" detector is wording-coupled; the independence script prints OK on a bad base ref (mostly guarded upstream); gazebo's BIT-IDENTICAL check is skipped when the differential is off. Details in the commit.

Refs pulseengine/jess#292

🤖 Generated with Claude Code

https://claude.ai/code/session_01HvusAXYbHLyv3uTzfBcMbG

…fail closed

Prompted by jess#292, which found an absence-assertion in jess's CI that
passed on an EMPTY log ("a gate that asserted the absence of a skip, read no
evidence, and said 0"). A read-only audit of relay's workflows and the scripts
they call found the same class twice here. Each is now proven both ways with
negative controls run against the old and the new gate.

1. spar.yml "Analyze system roots" — VACUOUS on missing input (not required).
   `out=$(spar analyze ...) || true` then decided on `grep -qE ERROR` alone.
   Measured with spar itself: an unreadable/missing .aadl makes spar EXIT 1 and
   print "Cannot read <path>: No such file or directory" — no ERROR line — so
   the gate passed having analysed nothing. Same for a `spar/*.aadl` glob that
   stops matching, or a missing binary. Now the exit code is the verdict and the
   grep a second opinion. `|| rc=$?` (not a bare call) because `shell: bash`
   runs with -e and every root should still report.

     case                         old   new
     real models (spar/*.aadl)    pass  pass
     missing input file           PASS  fail
     glob matching nothing        PASS  fail

   No false-failure risk, measured: all five real roots exit 0 while printing
   10-22 WARNINGs each.

2. scripts/check-float-discipline.sh — LATENT in the REQUIRED Clippy check.
   `[ -d "$src" ] || continue` dropped a listed flight crate silently when it
   had no source, and nothing counted what was scanned; run in an empty tree
   the script printed "float discipline: OK" and exited 0. Every listed crate
   must now contribute a scanned file (else CANNOT DETERMINE, fail), and OK
   reports its denominator.

     case                               old       new
     real tree                          pass      pass — "scanned 22/22 flight crates, 52 .rs files"
     empty tree                         PASS      fail (22 crates flagged)
     a listed crate renamed away        skipped   fail
     f64 injected in flight code        fail      fail   (detection preserved)

   The f64 control was itself wrong the first time — appended at end-of-file,
   it landed inside the gate's deliberate cfg(test)-to-EOF exemption and read
   as "0 hits" for BOTH versions. Placed in real code (line 40, before the
   first cfg(test) at 354) both catch it. Recorded because a control that
   cannot fail looks exactly like a gate that cannot fail.

   This script's own comment already records one earlier vacuous incident (BSD
   awk has no \y); this is the second, of a different mechanism.

NAMED, NOT FIXED (assessed as guarded or out of scope for this change):
  - verification-gate.yml:349 detects "verified nothing" by grepping for the
    literal "# 0 artifact(s) matched". Guarded in practice (pipefail + the
    runner fails on 0 tests), but wording-coupled: renaming the print silences
    it. A positive-count assertion would be sturdier.
  - check-verification-independence.sh prints OK on an unresolvable base ref
    (git diff errors to stderr, `|| true`). The workflow's merge-base fallback
    makes that need two bad sources, so mostly guarded.
  - gazebo.yml's BIT-IDENTICAL check only fires if a DIFFERENTIAL line exists;
    a run with the differential off passes it (the LOOP CLOSES/^PASS checks
    still bound the empty-log case).

Refs pulseengine/jess#292

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvusAXYbHLyv3uTzfBcMbG
@avrabe

avrabe commented Oct 21, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #588 — this exact commit is cherry-picked there (-x records the source) and batched with the other CI-only changes, so it costs one CI cycle under strict protection instead of one each.

@avrabe avrabe closed this Oct 21, 2026
auto-merge was automatically disabled October 21, 2026 15:05

Pull request was closed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant