diff --git a/packages/mosaic/framework/tools/wake/validate-973/check-973.py b/packages/mosaic/framework/tools/wake/validate-973/check-973.py new file mode 100755 index 00000000..d4d9b1e9 --- /dev/null +++ b/packages/mosaic/framework/tools/wake/validate-973/check-973.py @@ -0,0 +1,280 @@ +#!/usr/bin/env python3 +"""check-973.py — computation backend for the #973 validation harness. + +Everything here derives from exactly two inputs: the frozen denominator +artifact (denominator-089615f.json) and the SOURCE TEXT of the ten converted +suites at the current tree. It never reads the ledger — set arithmetic against +the runtime trace belongs to validate-973.sh, so the two legs of the +comparison come from independent code paths. + +Subcommands (all print sorted, stable output; non-zero exit on any failure): + + expected The expected coordinate set from the ARTIFACT: one + " :" row per denominator row (+3 = the + uniform header shift the converter applied; converter-verified). + Multi-grep lines stay ONE coordinate. + + static The converted-site inventory from the SOURCE TEXT at the current + tree: every non-comment line bearing a has_match/count_lines + token, as " :". Independent of the artifact + row list, so `expected == static` is a real check on the + conversion, not a tautology. (Amendment ONE, leg 1: the ledger is + an execution trace, not an inventory — the inventory must come + from the text.) + + arms The forced-error arm list: the 19 denominator canaries plus one + E-form arm (store-ack:733→736, a $(count_lines) capture compared + afterward — the A6 shape) plus one F-form arm (quarantine:560→563, + the multi-grep pipeline capture), as " : +
". Both extras are asserted to exist in the artifact with + the expected form — a renumber that moved them fails here, not + silently downstream. + + sweep Residual sweep: the denominator's own classifier (ported from the + frozen derivation) over the ten suites at the current tree must + find ZERO unconverted verdict-form grep sites; and, IN THE SAME + RUN, six per-form specimens planted into a temp copy of a real + suite must ALL be found with their correct forms — an instrument + that reports zero must first be seen finding what it claims to + find (A5). +""" + +import json +import re +import shutil +import sys +import tempfile +from pathlib import Path + +HERE = Path(__file__).resolve().parent +WAKE = HERE.parent +ART = HERE / "denominator-089615f.json" + +HEADER_SHIFT = 3 # converter inserted 3 header lines after SCRIPT_DIR in every suite + +# The two hand-picked extra arms (base coordinates; forms asserted at load). +EXTRA_ARMS = [ + ("test-wake-store-ack.sh", 733, "E-count-capture"), + ("test-wake-digest-quarantine.sh", 560, "F-extract-capture"), +] + +RX_HELPER = re.compile(r"(^|[^A-Za-z0-9_.-])(has_match|count_lines)([^A-Za-z0-9_.-]|$)") + +# ---- classifier, ported verbatim in logic from the frozen denominator +# ---- derivation (docs/journal/fleet/drift-derive-089615f__pepper.py) +RX_FAIL_SAME = re.compile(r"(\|\||&&)\s*fail") +RX_COUNT_SUB = re.compile(r"\$\(.*grep\s+[^)]*-c|\$\(\s*grep\s+-c") +RX_ASSIGN_SUB = re.compile(r'=\s*"?\$\(.*grep') +RX_IF = re.compile(r"^\s*(el)?if\s+.*grep") +RX_GREP = re.compile(r"(^|[^A-Za-z0-9_.-])grep([^A-Za-z0-9_.-]|$)") + + +def polarity(line): + m = RX_FAIL_SAME.search(line) + return "OR" if m.group(1) == "||" else "AND" + + +def classify(lines): + """Return (sites, dispo). Every line containing the word grep gets a row.""" + sites, dispo = [], [] + n = len(lines) + for i, raw in enumerate(lines): + line = raw + ln = i + 1 + if not RX_GREP.search(line): + continue + stripped = line.strip() + if stripped.startswith("#"): + dispo.append((ln, "comment", stripped)) + continue + nxt = "" + for j in range(i + 1, min(i + 3, n)): + if lines[j].strip(): + nxt = lines[j].strip() + break + if "$(" in line and RX_COUNT_SUB.search(line): + sites.append((ln, "E-count-capture", stripped)) + continue + if RX_ASSIGN_SUB.search(line) and "grep -c" not in line: + sites.append((ln, "F-extract-capture", stripped)) + continue + if RX_FAIL_SAME.search(line): + sites.append((ln, "A-same-line", stripped)) + continue + if stripped.endswith("\\"): + k = i + 1 + joined = stripped[:-1] + while k < n: + cont = lines[k].strip() + joined += " " + (cont[:-1] if cont.endswith("\\") else cont) + if not cont.endswith("\\"): + break + k += 1 + if re.search(r"(\|\||&&)\s*fail", joined) or ( + joined.rstrip().endswith(("||", "&&")) + and k + 1 < n + and lines[k + 1].strip().startswith("fail") + ): + sites.append((ln, "C-cont-backslash", stripped)) + continue + dispo.append((ln, "backslash-no-fail-continuation", stripped)) + continue + if stripped.endswith(("||", "&&")) and nxt.startswith("fail"): + sites.append((ln, "B-cont-operator", stripped)) + continue + if RX_IF.search(line): + window = " ".join(lines[j] for j in range(i, min(i + 5, n))) + if "fail" in window: + sites.append((ln, "D-if-form", stripped)) + continue + dispo.append((ln, "if-grep-no-fail-window", stripped)) + continue + win = " ".join(lines[j] for j in range(max(0, i - 2), min(i + 3, n))) + if re.search(r"fail", win, re.I): + dispo.append((ln, "BACKSTOP-HAND-REVIEW", stripped)) + else: + dispo.append((ln, "no-verdict-context", stripped)) + return sites, dispo + + +def load_art(): + art = json.loads(ART.read_text()) + assert art["total"] == 261 == len(art["rows"]), "artifact self-consistency" + return art + + +def helper_for(row): + return "count_lines" if row["form"].startswith("E") else "has_match" + + +def suite_files(art): + return sorted({r["file"] for r in art["rows"]}) + + +def cmd_expected(): + art = load_art() + out = sorted( + f"{helper_for(r)} {r['file']}:{r['line'] + HEADER_SHIFT}" for r in art["rows"] + ) + assert len(out) == len(set(out)) == 261, "expected set must be 261 distinct rows" + print("\n".join(out)) + return 0 + + +def cmd_static(): + art = load_art() + rows = [] + for f in suite_files(art): + for i, line in enumerate((WAKE / f).read_text().split("\n"), start=1): + if line.strip().startswith("#"): + continue + m = RX_HELPER.search(line) + if not m: + continue + helper = ( + "count_lines" + if RX_HELPER.search(line).group(2) == "count_lines" + else "has_match" + ) + rows.append(f"{helper} {f}:{i}") + print("\n".join(sorted(rows))) + return 0 + + +def cmd_arms(): + art = load_art() + by_key = {(r["file"], r["line"]): r for r in art["rows"]} + rows = [] + canaries = [r for r in art["rows"] if r.get("canary")] + assert len(canaries) == 19, f"expected 19 canaries, artifact has {len(canaries)}" + for r in canaries: + rows.append(f"{helper_for(r)} {r['file']}:{r['line'] + HEADER_SHIFT} {r['form']}") + for f, ln, want_form in EXTRA_ARMS: + r = by_key.get((f, ln)) + assert r is not None, f"extra arm {f}:{ln} not in artifact — renumbered?" + assert r["form"] == want_form, f"extra arm {f}:{ln} form {r['form']} != {want_form}" + rows.append(f"{helper_for(r)} {f}:{ln + HEADER_SHIFT} {r['form']}") + assert len(rows) == 21 + print("\n".join(rows)) + return 0 + + +PLANTS = [ + ("A-same-line", ['grep -q needle haystack || fail "plant-A"']), + ("B-cont-operator", ["grep -q needle haystack ||", ' fail "plant-B"']), + ("C-cont-backslash", ["grep -q needle \\", ' haystack || fail "plant-C"']), + ("D-if-form", ["if ! grep -q needle haystack; then", ' fail "plant-D"', "fi"]), + ("E-count-capture", ['[ "$(grep -c needle haystack)" = "1" ] || fail "plant-E"']), + ("F-extract-capture", ['val="$(grep needle haystack)"']), +] + + +def cmd_sweep(): + art = load_art() + bad = 0 + + # leg 1: real suites at the current tree must be residual-free + for f in suite_files(art): + lines = (WAKE / f).read_text().split("\n") + sites, _dispo = classify(lines) + residual = [] + for ln, form, text in sites: + if RX_HELPER.search(lines[ln - 1]): + # converted line whose PATTERN argument contains the word grep: + # not an unconverted site, but never silently absorbed either + print(f"SWEEP-NOTE {f}:{ln} converted line matches grep-token ({form}): {text[:80]}") + continue + residual.append((ln, form, text)) + for ln, form, text in residual: + print(f"SWEEP-RESIDUAL {f}:{ln} {form}: {text[:100]}") + bad += 1 + print(f"SWEEP {f}: {len(residual)} residual verdict site(s)") + + # leg 2, SAME RUN: the instrument must find six per-form plants + donor = suite_files(art)[0] + with tempfile.TemporaryDirectory() as td: + planted = Path(td) / donor + shutil.copy(WAKE / donor, planted) + base_lines = planted.read_text().split("\n") + offset = len(base_lines) + expect = {} + for form, snippet in PLANTS: + expect[offset + 1] = form # first physical line of each plant + base_lines.extend(snippet) + offset = len(base_lines) + planted.write_text("\n".join(base_lines)) + sites, _ = classify(planted.read_text().split("\n")) + found = {ln: form for ln, form, _t in sites if ln in expect} + unexpected = [(ln, form) for ln, form, _t in sites if ln not in expect] + hits = sum(1 for ln, form in expect.items() if found.get(ln) == form) + print(f"SWEEP-PLANTS found={hits}/6 in planted copy of {donor}") + if hits != 6: + for ln, form in sorted(expect.items()): + got = found.get(ln, "") + if got != form: + print(f"SWEEP-PLANT-MISS line {ln}: expected {form}, got {got}") + bad += 1 + if unexpected: + # the donor is a converted suite: any non-plant site the sweep finds + # in the copy contradicts the zero it just reported on the original + for ln, form in unexpected: + print(f"SWEEP-PLANT-UNEXPECTED {donor}(copy):{ln} {form}") + bad += 1 + + return 1 if bad else 0 + + +def main(): + cmds = { + "expected": cmd_expected, + "static": cmd_static, + "arms": cmd_arms, + "sweep": cmd_sweep, + } + if len(sys.argv) != 2 or sys.argv[1] not in cmds: + sys.exit(f"usage: check-973.py {{{'|'.join(cmds)}}}") + sys.exit(cmds[sys.argv[1]]()) + + +if __name__ == "__main__": + main() diff --git a/packages/mosaic/framework/tools/wake/validate-973/microtest-wake-assert.sh b/packages/mosaic/framework/tools/wake/validate-973/microtest-wake-assert.sh index 7b043eed..bec281ae 100755 --- a/packages/mosaic/framework/tools/wake/validate-973/microtest-wake-assert.sh +++ b/packages/mosaic/framework/tools/wake/validate-973/microtest-wake-assert.sh @@ -34,6 +34,14 @@ # misreports the continuation line, wake_assert_init aborts loudly and # nothing past init executes — a pin whose failure arm was never seen # firing is an undertaking, not a control. +# C11 the FAILED-summary template executes on the red path: a mini-suite +# driven deterministically red emits the exact converted summary shape +# (`FAILED ($(count_lines . "$FAILFILE") assertion(s))`) with the right +# count, exits 1, and the summary site's ledger row lands. The nine +# real-suite summary sites are structurally unreachable in a green run +# (guarded by [ -s "$FAILFILE" ]); their dispositions cite THIS check as +# the measured execution of the same template, so "unexecuted in the +# green run" never silently means "never executed anywhere". set -uo pipefail HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -247,6 +255,42 @@ else check C10 1 "rc=$rc out=$(printf '%s' "$out" | tail -n 2 | tr '\n' ' ')" fi +# --- C11: red path executes the converted FAILED-summary template ----------- +cat >"$TMP/mini-c.sh" <<'MINI_C' +#!/usr/bin/env bash +set -uo pipefail +. "$WAKE_COMMON" +wake_assert_init +TMP="$1" +FAILFILE="$TMP/failures-c" +: >"$FAILFILE" +fail_msg() { echo " FAIL: $*" >&2; echo x >>"$FAILFILE"; } +ok() { :; } +( + has_match -q alpha "$TMP/data.txt" && fail_msg "alpha present" # SITE:c-inverted (deterministically red: alpha IS in the fixture) +) && ok +echo +if [ -s "$FAILFILE" ]; then + echo "wake mini-c harness: FAILED ($(count_lines . "$FAILFILE") assertion(s))" >&2 # SITE:c-summary + exit 1 +fi +echo "wake mini-c harness: all invariants passed (1 group)" +MINI_C +chmod +x "$TMP/mini-c.sh" +LEDGER="$TMP/ledger-c11" +: >"$LEDGER" +out="$(WAKE_ASSERT_LEDGER="$LEDGER" bash "$TMP/mini-c.sh" "$TMP" 2>&1)" +rc=$? +summary_ln="$(site_line "$TMP/mini-c.sh" c-summary)" +if [ "$rc" -eq 1 ] && + printf '%s' "$out" | grep -q 'wake mini-c harness: FAILED (1 assertion(s))' && + ! printf '%s' "$out" | grep -q 'all invariants passed' && + grep -q "^count_lines mini-c.sh:${summary_ln}\$" "$LEDGER"; then + check C11 0 "" +else + check C11 1 "rc=$rc summary_ln=$summary_ln ledger=$(tr '\n' ' ' <"$LEDGER") out=$(printf '%s' "$out" | tail -n 2 | tr '\n' ' ')" +fi + echo if [ "$fails" -gt 0 ]; then echo "microtest-wake-assert: FAILED ($fails check(s))" >&2 diff --git a/packages/mosaic/framework/tools/wake/validate-973/unexecuted-sites-dispositions.txt b/packages/mosaic/framework/tools/wake/validate-973/unexecuted-sites-dispositions.txt new file mode 100644 index 00000000..c2a0ba0b --- /dev/null +++ b/packages/mosaic/framework/tools/wake/validate-973/unexecuted-sites-dispositions.txt @@ -0,0 +1,37 @@ +# unexecuted-sites-dispositions.txt — #973 validation, amendment ONE leg 3. +# +# The ledger is an execution trace, not an inventory: a green instrumented run +# cannot execute a site that only lives on a suite's red path. Every converted +# site that did NOT appear in the green-run trace is enumerated here with an +# individual disposition; validate-973.sh fails if any unexecuted site lacks a +# row here, and ALSO fails if a row here names a site that DID execute (stale +# disposition). Key = first two whitespace-separated fields; text after "—" is +# the adjudication. +# +# All nine sites below are the same structural shape, adjudicated one by one +# from source text: the suite's FAILED-branch summary line, +# echo "wake harness: FAILED ($(count_lines . "$FAILFILE") assertion(s))" >&2 +# guarded by `if [ -s "$FAILFILE" ]` — structurally unreachable while every +# assertion passes, which is precisely the state a green validation run is +# required to be in. (The tenth suite, test-wake-preimage.sh, uses its own +# X/Y summary format with no grep in the red branch, so it has no row here.) +# +# The disposition is NOT "it would work": the exact template is EXECUTED red +# in microtest C11 (deterministically failed mini-suite, same +# count_lines-in-substitution summary shape → right count, exit 1, ledger row +# at the summary coordinate), and the E-in-substitution abort path is proven +# by microtest C5 plus the forced-error arm at test-wake-store-ack.sh:736. +# Each site's conversion text is independently verified by the static +# inventory (check-973.py static == expected, all 261 rows). +# +# Verified guard per site (line numbers at branch tip, +3 header shift): + +count_lines test-wake-beacon.sh:350 — red-path summary; guard `[ -s "$FAILFILE" ]` at line 349; template execution measured by microtest C11; text verified by static inventory +count_lines test-wake-detector.sh:702 — red-path summary; guard `[ -s "$FAILFILE" ]` at line 701; template execution measured by microtest C11; text verified by static inventory +count_lines test-wake-digest-hmac.sh:434 — red-path summary; guard `[ -s "$FAILFILE" ]` at line 433; template execution measured by microtest C11; text verified by static inventory +count_lines test-wake-digest-quarantine.sh:584 — red-path summary; guard `[ -s "$FAILFILE" ]` at line 583; template execution measured by microtest C11; text verified by static inventory +count_lines test-wake-fn-oracle.sh:132 — red-path summary; guard `[ -s "$FAILFILE" ]` at line 131; template execution measured by microtest C11; text verified by static inventory +count_lines test-wake-install.sh:434 — red-path summary; guard `[ -s "$FAILFILE" ]` at line 433; template execution measured by microtest C11; text verified by static inventory +count_lines test-wake-reconcile.sh:389 — red-path summary; guard `[ -s "$FAILFILE" ]` at line 388; template execution measured by microtest C11; text verified by static inventory +count_lines test-wake-store-ack.sh:741 — red-path summary; guard `[ -s "$FAILFILE" ]` at line 740; template execution measured by microtest C11; text verified by static inventory +count_lines test-wake-store-enqueue-race.sh:208 — red-path summary (with "#927 TOCTOU reproduced (RED)" tail); guard `[ -s "$FAILFILE" ]` at line 207; template execution measured by microtest C11; text verified by static inventory diff --git a/packages/mosaic/framework/tools/wake/validate-973/validate-973.sh b/packages/mosaic/framework/tools/wake/validate-973/validate-973.sh new file mode 100755 index 00000000..f6e473e7 --- /dev/null +++ b/packages/mosaic/framework/tools/wake/validate-973/validate-973.sh @@ -0,0 +1,210 @@ +#!/usr/bin/env bash +# validate-973.sh — #973 validation driver. One run produces the complete +# evidence chain for the 261-site conversion: +# +# 0. instrument self-test (microtest) — no validate evidence is trusted +# before the instrument itself has been proven, including its abort arms. +# 1. expected set: 261 coordinates from the FROZEN artifact (+3 header +# shift), count asserted against the number declared below BEFORE any +# suite runs. +# 2. static inventory: converted call sites re-derived from SOURCE TEXT, +# must equal the expected set exactly (amendment ONE, leg 1 — the +# inventory comes from the text, never from the ledger). +# 3. green instrumented run: all ten suites with WAKE_ASSERT_LEDGER; each +# must exit 0 AND emit its own sentinel (per-suite formats differ and are +# pinned here — a suite that died early must never pass on another +# suite's output). +# 4. trace arithmetic on coordinate SETS (loops re-execute sites and the +# multi-grep lines append twice per pass, so counts are meaningless; +# sets are not): +# trace − expected MUST be empty (a helper ran at a coordinate the +# denominator never measured); +# expected − trace = converted-but-never-executed: enumerated, and +# every entry must carry a disposition in the +# committed unexecuted-sites-dispositions.txt, with +# no stale dispositions the other way (amendment +# ONE, legs 2+3 — the ledger is an execution trace, +# not an inventory; the difference is enumerated and +# individually dispositioned, never silently absent). +# 5. forced-error arms: the 19 denominator canaries plus one E-form and one +# F-form site, each run with WAKE_ASSERT_FORCE_GREP_ERROR_AT: the suite +# must emit the ARMED line (the arm proved it fired), the ABORT line +# naming the site, exit non-zero, emit NO sentinel, and the aborting +# site's ledger row must already be present (the append lands before the +# grep). +# 6. residual sweep: the denominator's own classifier finds zero unconverted +# verdict greps in the suites — and six per-form plants in the same run. +# +# Output discipline (A10): every line that reports on a suite names the file +# under test; exit codes are reported before failure counts. +set -uo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WAKE="$(cd "$HERE/.." && pwd)" +CHECK="$HERE/check-973.py" +DISPO="$HERE/unexecuted-sites-dispositions.txt" +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT + +# Declared BEFORE any suite runs (A2): the run must produce THESE numbers, +# not be described by whatever numbers it produced. +EXPECTED_SUITES=10 +EXPECTED_SITES=261 +EXPECTED_ARMS=21 + +fails=0 +flag() { + printf 'FAIL %s\n' "$*" + fails=$((fails + 1)) +} + +SUITES=( + test-wake-beacon.sh + test-wake-detector.sh + test-wake-digest-hmac.sh + test-wake-digest-quarantine.sh + test-wake-fn-oracle.sh + test-wake-install.sh + test-wake-preimage.sh + test-wake-reconcile.sh + test-wake-store-ack.sh + test-wake-store-enqueue-race.sh +) +[ "${#SUITES[@]}" -eq "$EXPECTED_SUITES" ] || + flag "suite list has ${#SUITES[@]} entries, declared $EXPECTED_SUITES" + +# Per-suite sentinel patterns, pinned: nine suites share the harness template +# (enqueue-race appends a tail after it); preimage uses its own format. +sentinel_for() { + case "$1" in + test-wake-preimage.sh) printf '%s' '^== test-wake-preimage: 17/17 passed ==$' ;; + *) printf '%s' 'harness: all invariants passed' ;; + esac +} + +# --- 0: instrument self-test ------------------------------------------------ +if bash "$HERE/microtest-wake-assert.sh" >"$TMP/microtest.out" 2>&1; then + echo "MICROTEST microtest-wake-assert.sh exit=0 (instrument proven)" +else + rc=$? + echo "MICROTEST microtest-wake-assert.sh exit=$rc" + sed 's/^/ /' "$TMP/microtest.out" | tail -n 15 + flag "instrument self-test failed — no validate evidence below is trustworthy" +fi + +# --- 1+2: expected set (artifact) vs static inventory (source text) --------- +python3 "$CHECK" expected | sort >"$TMP/expected.txt" || + flag "check-973.py expected failed" +n_expected="$(grep -c . "$TMP/expected.txt")" +echo "EXPECTED-SET $n_expected coordinates (declared: $EXPECTED_SITES)" +[ "$n_expected" -eq "$EXPECTED_SITES" ] || + flag "expected set has $n_expected coordinates, declared $EXPECTED_SITES" + +python3 "$CHECK" static | sort >"$TMP/static.txt" || + flag "check-973.py static failed" +if cmp -s "$TMP/expected.txt" "$TMP/static.txt"; then + echo "STATIC-INVENTORY equals expected set ($(grep -c . "$TMP/static.txt") rows from source text)" +else + flag "static inventory (source text) differs from expected set (artifact):" + diff "$TMP/expected.txt" "$TMP/static.txt" | head -n 20 | sed 's/^/ /' +fi + +# --- 3: green instrumented run ---------------------------------------------- +LEDGER="$TMP/ledger" +: >"$LEDGER" +for s in "${SUITES[@]}"; do + out="$(WAKE_ASSERT_LEDGER="$LEDGER" bash "$WAKE/$s" 2>&1)" + rc=$? + if printf '%s\n' "$out" | grep -Eq "$(sentinel_for "$s")"; then + sent="present" + else + sent="ABSENT" + fi + echo "SUITE $s exit=$rc sentinel=$sent" + [ "$rc" -eq 0 ] || flag "$s exited $rc in the green instrumented run" + [ "$sent" = "present" ] || flag "$s did not emit its sentinel" +done + +# --- 4: trace arithmetic on coordinate sets --------------------------------- +sort -u "$LEDGER" >"$TMP/trace.txt" +echo "TRACE $(grep -c . "$TMP/trace.txt") distinct coordinates from $(grep -c . "$LEDGER") ledger rows" + +comm -13 "$TMP/expected.txt" "$TMP/trace.txt" >"$TMP/rogue.txt" +if [ -s "$TMP/rogue.txt" ]; then + flag "trace contains coordinates OUTSIDE the frozen denominator:" + sed 's/^/ ROGUE /' "$TMP/rogue.txt" +else + echo "TRACE-MINUS-EXPECTED empty (no helper ran at an unmeasured coordinate)" +fi + +comm -23 "$TMP/expected.txt" "$TMP/trace.txt" >"$TMP/unexec.txt" +n_unexec="$(grep -c . "$TMP/unexec.txt" || true)" +echo "UNEXECUTED $n_unexec of $EXPECTED_SITES converted sites did not execute in the green run" +if [ ! -f "$DISPO" ]; then + flag "disposition file missing: $DISPO — every unexecuted site must be individually dispositioned" + sed 's/^/ UNDISPOSITIONED /' "$TMP/unexec.txt" +else + awk '!/^#/ && NF >= 2 {print $1, $2}' "$DISPO" | sort -u >"$TMP/dispo-keys.txt" + comm -23 "$TMP/unexec.txt" "$TMP/dispo-keys.txt" >"$TMP/undispo.txt" + comm -13 "$TMP/unexec.txt" "$TMP/dispo-keys.txt" >"$TMP/stale-dispo.txt" + if [ -s "$TMP/undispo.txt" ]; then + flag "unexecuted sites WITHOUT a disposition:" + sed 's/^/ UNDISPOSITIONED /' "$TMP/undispo.txt" + fi + if [ -s "$TMP/stale-dispo.txt" ]; then + flag "dispositions for sites that DID execute (stale — the file no longer matches the run):" + sed 's/^/ STALE-DISPO /' "$TMP/stale-dispo.txt" + fi + if [ ! -s "$TMP/undispo.txt" ] && [ ! -s "$TMP/stale-dispo.txt" ]; then + echo "DISPOSITIONS all $n_unexec unexecuted sites individually dispositioned, none stale" + fi +fi + +# --- 5: forced-error arms --------------------------------------------------- +python3 "$CHECK" arms >"$TMP/arms.txt" || flag "check-973.py arms failed" +n_arms="$(grep -c . "$TMP/arms.txt")" +echo "ARMS $n_arms forced-error arms (declared: $EXPECTED_ARMS)" +[ "$n_arms" -eq "$EXPECTED_ARMS" ] || + flag "arm list has $n_arms entries, declared $EXPECTED_ARMS" + +while read -r helper site form; do + f="${site%%:*}" + aled="$TMP/ledger-arm" + : >"$aled" + out="$(WAKE_ASSERT_LEDGER="$aled" WAKE_ASSERT_FORCE_GREP_ERROR_AT="$site" \ + bash "$WAKE/$f" 2>&1)" + rc=$? + bad="" + [ "$rc" -ne 0 ] || bad="$bad exit=0" + printf '%s\n' "$out" | grep -q "WAKE-ASSERT ARMED: forcing real grep error at $site" || + bad="$bad no-ARMED-line" + printf '%s\n' "$out" | grep -q "WAKE-ASSERT ABORT: ${helper} at ${site}: grep exit" || + bad="$bad no-ABORT-line" + printf '%s\n' "$out" | grep -Eq "$(sentinel_for "$f")" && + bad="$bad sentinel-emitted" + grep -q "^${helper} ${site}\$" "$aled" || + bad="$bad no-ledger-row" + if [ -z "$bad" ]; then + echo "ARM $site ($form) exit=$rc armed+abort+no-sentinel+ledger-row" + else + echo "ARM $site ($form) exit=$rc DEFECTS:$bad" + flag "arm $site ($form) failed:$bad" + fi +done <"$TMP/arms.txt" + +# --- 6: residual sweep ------------------------------------------------------ +if python3 "$CHECK" sweep >"$TMP/sweep.out" 2>&1; then + echo "SWEEP exit=0" +else + echo "SWEEP exit=$?" + flag "residual sweep failed" +fi +sed 's/^/ /' "$TMP/sweep.out" + +# --- summary (exit codes above, failure count last — A10) -------------------- +echo +if [ "$fails" -gt 0 ]; then + echo "validate-973: FAILED ($fails failure(s))" >&2 + exit 1 +fi +echo "validate-973: OK — $EXPECTED_SUITES suites, $EXPECTED_SITES sites, $EXPECTED_ARMS arms, sweep clean"