PROCITY/tools/qa.sh
m3ultra c3384b92c9 Lane F R40 §40.6: the notepad — F8, the tour, and the gate that caught the harness
THE PLAYTEST HARNESS, for John's bug-hunt session. F8 anywhere drops a structured note:
pos, mode, shop id+name when inside, street/locality via A's address layer, seed, town,
clock segment, live flags, fps, draws — plus a screenshot and a typed one-liner (Esc
cancels, Enter saves). Notes persist under the delta law (bounded, survive reload,
export/import, corrupt store REJECTED WHOLE and loudly) and export as one JSON of
tickets, schema documented in LANE_F_NOTES §40. Zero cost when unused; classic-gated by
construction.

?bugtour=1 walks the whole game in 11 stops (N advances, each with a caption of what to
look at): spawn → street_day → shop_buy → dig → opshop_bin (caption says the contents
are deliberately unarmed) → gig_night → tram → rain → night → fog_map → second_town.
DBG loads without ?dbg=1 so the tour is one flag.

THE GATE CAUGHT THE HARNESS. The first full strict run went RED on exactly one arm —
R30c classic purity, because the playtest constructor read localStorage eagerly on
?game=0. Fixed (lazy store), re-verified on the targeted smoke, and the new gate now
carries its own instrumented zero-calls arm so it cannot recur.

Wave-1 asks, all taken: D's door_footpath_check and B's r40_lane_b wired into qa.sh ·
D's index.html:433-437 door nudge now derives from buildShopfront's doorRect · A's
filed D2 closed (index.html:541 uses the canonical isOpen; remaining divergent call
sites listed for their owners) · README's stale ?r=N 307 row and the flags_check label
that still called the town selector a debt question, both corrected.

qa.sh --strict: 12 passed · 0 failed · 0 warn · 0 skipped. selfcheck 157,647/157,647,
0x5f76e76 unmoved.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 12:29:02 +10:00

200 lines
12 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# PROCITY Lane F — the QA gate runner (CITY_SPEC / LANE_F acceptance).
#
# tools/qa.sh run every gate whose inputs have landed; skip the rest
# tools/qa.sh --strict also FAIL on any still-pending lane deliverable (use at v1 sign-off)
#
# Design: F runs before AE finish. A gate whose target file does not exist yet is reported
# SKIPPED (yellow), not failed — so this is safe to run continuously as lanes land. Once every
# lane is in, `--strict` turns the skips into hard failures: that is the v1 readiness gate.
#
# Gates:
# 1. scaffold_check.mjs — scaffold + PRNG determinism law + lane-readiness matrix (F-owned)
# 2. citygen selfcheck — node web/js/citygen/selfcheck.js (Lane A ships it)
# 3. manifest validator — python3 pipeline/validate_manifest.py (Lane E ships it)
# 8. the R39 (v9 wave 1) gates — tools/qa/r39_address.mjs · r39_transmission.py · r39_runtime.py
#
# Browser-driven gates (determinism-PNG, 10-min soak, budget-HUD, ?noassets run) need the running
# game; they live in tools/soak.md + tools/shots.md and run once Lane B's index.html lands.
set -uo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT"
STRICT=0
[ "${1:-}" = "--strict" ] && STRICT=1
pass=0; failn=0; skip=0
c_red=$'\033[31m'; c_grn=$'\033[32m'; c_yel=$'\033[33m'; c_bold=$'\033[1m'; c_off=$'\033[0m'
hr() { printf '%s\n' "────────────────────────────────────────────────────────"; }
run_gate() { # run_gate "name" cmd...
local name="$1"; shift
printf '%s▶ %s%s\n' "$c_bold" "$name" "$c_off"
if "$@"; then printf '%s ✓ PASS%s %s\n\n' "$c_grn" "$c_off" "$name"; pass=$((pass+1));
else printf '%s ✗ FAIL%s %s\n\n' "$c_red" "$c_off" "$name"; failn=$((failn+1)); fi
}
skip_gate() { # skip_gate "name" "reason"
printf '%s▶ %s%s\n' "$c_bold" "$1" "$c_off"
printf '%s ⊘ SKIP%s %s\n\n' "$c_yel" "$c_off" "$2"
skip=$((skip+1))
}
warnn=0
soft_skip() { # non-blocking skip for a warn-level gate — does NOT trip --strict readiness
printf '%s▶ %s%s\n' "$c_bold" "$1" "$c_off"
printf '%s ⊘ SKIP%s %s (warn-level, non-blocking)\n\n' "$c_yel" "$c_off" "$2"
}
warn_gate() { # warn_gate "name" cmd... — runs, but a failure is a WARN not a FAIL (round-6 policy)
local name="$1"; shift
printf '%s▶ %s%s (warn-level)\n' "$c_bold" "$name" "$c_off"
if "$@"; then printf '%s ✓ PASS%s %s\n\n' "$c_grn" "$c_off" "$name"; pass=$((pass+1));
else printf '%s ! WARN%s %s (non-blocking this round — strict in r7)\n\n' "$c_yel" "$c_off" "$name"; warnn=$((warnn+1)); fi
}
printf '%sPROCITY QA GATES%s (%s)\n' "$c_bold" "$c_off" "$([ $STRICT = 1 ] && echo strict || echo lenient)"
hr
# ── Gate 1: scaffold + determinism + readiness (always) ──────────────────────
run_gate "scaffold_check (scaffold · PRNG determinism · readiness matrix)" \
node tools/qa/scaffold_check.mjs
# ── Gate 1b: cross-lane consistency (plan ↔ registry ↔ assets ↔ manifest) ────
# Self-skips (exit 0) until Lane A's plan.js + registry.js exist.
run_gate "consistency (plan ↔ registry ↔ assets ↔ manifest)" \
node tools/qa/consistency_check.mjs
# ── Gate 2: Lane A citygen selfcheck ─────────────────────────────────────────
if [ -f web/js/citygen/selfcheck.js ]; then
run_gate "citygen selfcheck (determinism · <100ms · lots/shops integrity)" \
node web/js/citygen/selfcheck.js
else
skip_gate "citygen selfcheck" "web/js/citygen/selfcheck.js not landed yet (Lane A)"
fi
# ── Gate 3: Lane E manifest validator ────────────────────────────────────────
# [Lane F R3] --depot: the 16 GLBs are published + live on the 3GOD depot (Fable, 2026-07-14), so the
# gate now REQUIRES them reachable (was a soft local-only check). Override the endpoint with GOD3_DEPOT
# (tailnet path) if digalot.fyi is unreachable; the validator falls back to the manifest's depot field.
if [ -f pipeline/validate_manifest.py ]; then
run_gate "manifest validator (files exist · footprints sane · GLBs live on depot · JSON parses)" \
python3 pipeline/validate_manifest.py --depot
else
skip_gate "manifest validator" "pipeline/validate_manifest.py not landed yet (Lane E)"
fi
# ── Gate 4 (warn-level, round 6): v2 flag enforcement harness ────────────────
# Flags-off regression (prime-law) + per-flag + all-on-combo smokes. Needs the Playwright venv +
# a browser (self-contained server), auto-skips where absent. STRICT as of R7 (F2): a harness FAIL
# now fails qa (flags-off regression + the four existing flags + all-on combo). New flags (stock,
# weather) report at warn inside the harness, so they never trip this gate until they graduate.
# Skip explicitly with --no-flags (fast determinism-only runs).
FLAGS_SKIP=0; for a in "$@"; do [ "$a" = "--no-flags" ] && FLAGS_SKIP=1; done
if [ "$FLAGS_SKIP" = 1 ]; then
soft_skip "v2 flags harness" "--no-flags"
elif [ -x tools/.venv/bin/python ] && tools/.venv/bin/python -c "import playwright" 2>/dev/null; then
run_gate "flags harness (classic regression · default-boot gate · per-flag · smokes · STRICT · R16 flip)" \
tools/.venv/bin/python tools/flags_check.py
else
soft_skip "v2 flags harness" "Playwright venv absent (python3 -m venv tools/.venv && …/pip install playwright)"
fi
# ── Gate 5 (Lane D, round 10): interior figure-scale regression guard ────────
# Enters a sample of open shops and asserts every interior rig figure's crown is human-sized
# [1.4,2.0] m AND under the room ceiling — so the R9 giant blocker (buildFigure normalised off
# head-above-hips → ~2× too tall) can never pass a gate again. run_gate ⇒ a giant FAILS qa.
# Needs the Playwright venv (auto-skips where absent, like the flags harness). Wired per ROUND10 §D.
SCALE_SKIP=0; for a in "$@"; do [ "$a" = "--no-scale" ] && SCALE_SKIP=1; done
if [ "$SCALE_SKIP" = 1 ]; then
soft_skip "interior figure-scale gate" "--no-scale"
elif [ -x tools/.venv/bin/python ] && tools/.venv/bin/python -c "import playwright" 2>/dev/null; then
run_gate "interior figure-scale (no giants: crown ∈ [1.4,2.0] m, under ceiling · Lane D R10)" \
tools/.venv/bin/python tools/qa/interior_scale_check.py
else
soft_skip "interior figure-scale gate" "Playwright venv absent (python3 -m venv tools/.venv && …/pip install playwright)"
fi
# ── Gate 6 (Lane F, round 15): full-week gig soak — OPT-IN (--soak), NON-STRICT ─
# The v3.0 release soak: 7 nights × every playing venue in one context — latch state per plan.gigs,
# cover stamp per venue per night, roster clears per night, leak geo/tex to a warmed baseline across
# ≥20 enter/exit cycles. Slow (~90s) + diagnostic, so it is OPT-IN and warn-level (never blocks a tag);
# the core 6 gates stay a fast strict run. Wired per ROUND15 §Lane F.3.
SOAK_RUN=0; for a in "$@"; do [ "$a" = "--soak" ] && SOAK_RUN=1; done
if [ "$SOAK_RUN" = 1 ]; then
if [ -x tools/.venv/bin/python ] && tools/.venv/bin/python -c "import playwright" 2>/dev/null; then
warn_gate "full-week gig soak (7 nights · cover/roster per night · leak-free · Lane F R15)" \
tools/.venv/bin/python tools/qa/week_soak.py
else
soft_skip "full-week gig soak" "Playwright venv absent"
fi
fi
# ── Gate 7 (Lane F, round 17): town-matrix smoke — OPT-IN (--matrix), NON-STRICT ─
# The v4 real-map scout evidence: boot / determinism / budget / district / noassets across the full town
# matrix — synthetic hero seeds + the 3 osm fixtures + Lane E's 5 real AU town caches. One town × gate
# table. Slow (~20 boots) + diagnostic, so OPT-IN + warn-level. Wired per ROUND17 §Lane F (ledger #7).
MATRIX_RUN=0; for a in "$@"; do [ "$a" = "--matrix" ] && MATRIX_RUN=1; done
if [ "$MATRIX_RUN" = 1 ]; then
if [ -x tools/.venv/bin/python ] && tools/.venv/bin/python -c "import playwright" 2>/dev/null; then
warn_gate "town-matrix (synthetic + fixtures + real caches × 5 gates · Lane F R17)" \
tools/.venv/bin/python tools/qa/town_matrix.py
else
soft_skip "town-matrix" "Playwright venv absent"
fi
fi
# ── Gate 8 (Lane F, round 39): the v9 wave-1 gates ───────────────────────────
# 39.1 THE ADDRESS LAYER (node, no browser) · 39.4 THE TRANSMISSION PRE-PASS (validator control +
# the interior draw sweep against a fabricated glass asset) · 39.2 THE FOG / THE SIGN / THE RUMMAGE
# BIN (browser). Each ships its falsifiability control in the same run — see LANE_F_NOTES §39.
# --no-r39 skips the browser half on a fast determinism-only run; the node half always runs.
run_gate "R39 address layer (>=97% corpus · degrade-to-district · the wrong-shift red arm)" \
node tools/qa/r39_address.mjs
R39_SKIP=0; for a in "$@"; do [ "$a" = "--no-r39" ] && R39_SKIP=1; done
if [ "$R39_SKIP" = 1 ]; then
soft_skip "R39 transmission + runtime gates" "--no-r39"
elif [ -x tools/.venv/bin/python ] && tools/.venv/bin/python -c "import playwright" 2>/dev/null; then
run_gate "R39 transmission pre-pass (validator hard-fail control · interior sweep vs a glass asset)" \
tools/.venv/bin/python tools/qa/r39_transmission.py --validator
run_gate "R39 runtime (THE SIGN · THE FOG both arms · THE RUMMAGE BIN, contents unarmed)" \
tools/.venv/bin/python tools/qa/r39_runtime.py
else
soft_skip "R39 transmission + runtime gates" "Playwright venv absent"
fi
# ── Gate 9 (Lane F, round 40): the R40 gates ─────────────────────────────────
# [Lane F R40] Three gates, wired per each lane's §40 ask:
# • D §40.5 — door→footpath (node, ~2s, zero deps): walkable-front ≥95% on all four towns, the
# back-point control non-vacuous, the kerb clamp FIX-ONLY. rc 0/1.
# • B §40.3 — the ?r= budget law (default 300 / diagnostic 420 + warn, measured stepwalk ≤ own
# ceiling), classic's town selector (RULED R40), fog signage both arms. Own no-store server.
# • F §40.6 — the playtest harness: off-boot byte-identical (no fetch, no DOM, no storage), the
# F8 note round-trip (drop → export → schema-validate → reload persists → corrupt-reject), and
# ?bugtour=1 advancing through every stop with zero console errors.
run_gate "R40 door→footpath (D §40.5: walkable ≥95% · back-point control · fix-only clamp)" \
node tools/qa/door_footpath_check.mjs
R40_SKIP=0; for a in "$@"; do [ "$a" = "--no-r40" ] && R40_SKIP=1; done
if [ "$R40_SKIP" = 1 ]; then
soft_skip "R40 browser gates (budget law · selector ruling · playtest harness)" "--no-r40"
elif [ -x tools/.venv/bin/python ] && tools/.venv/bin/python -c "import playwright" 2>/dev/null; then
run_gate "R40 ?r= budget law + classic selector + fog signage (B §40.3, both arms each)" \
tools/.venv/bin/python tools/qa/r40_lane_b.py
run_gate "R40 playtest harness (F §40.6: off byte-identical · F8 round-trip · bugtour clean)" \
tools/.venv/bin/python tools/qa/r40_playtest.py
else
soft_skip "R40 browser gates (budget law · selector ruling · playtest harness)" "Playwright venv absent"
fi
# ── Summary ──────────────────────────────────────────────────────────────────
hr
printf '%sSUMMARY%s %s%d passed%s · %s%d failed%s · %s%d warn%s · %s%d skipped%s\n' \
"$c_bold" "$c_off" "$c_grn" "$pass" "$c_off" "$c_red" "$failn" "$c_off" "$c_yel" "$warnn" "$c_off" "$c_yel" "$skip" "$c_off"
if [ "$failn" -gt 0 ]; then
printf '%s● QA RED%s — fix the failing gate(s) above.\n' "$c_red" "$c_off"; exit 1
fi
if [ "$STRICT" = 1 ] && [ "$skip" -gt 0 ]; then
printf '%s● QA NOT READY%s — %d gate(s) still pending; not v1 yet.\n' "$c_yel" "$c_off" "$skip"; exit 2
fi
printf '%s● QA GREEN%s — every landed gate passed.\n' "$c_grn" "$c_off"; exit 0