#!/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 A–E 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) # # 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 # ── 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