04. Quality gates
Every gate, what it checks, how to run it, and whether it blocks a release. Source of truth: MM/ci/ andMM/docs/GAME-DEVELOPMENT-GUIDE.md §3, §10 (decision 36: game quality gates are core focus; releases need them
green). Commands run from MM/. Needs Node 18+, Python 3 (G-sweep helper), Google Chrome (browser suites; setCHROME=/path off-Mac; Linux without GPU: CHROME_FLAGS="--no-sandbox --use-angle=swiftshader --enable-unsafe-swiftshader").
Blocking = a release does not ship while it is red, unless the operator records a gate exception before
publishing (05 (05-release-process.md)). Advisory = reported; fix it or record a reasoned ci/allow.json entry.
NOT RUN is never PASS.
1. The pipeline
npm run ci = node ci/pipeline.mjs (MM/ci/pipeline.mjs:1-12). It stops at the first red stage:
| Stage | What runs | Fails on |
|---|---|---|
| 1a gates | html_meta.py --selftest, quality-gates-selftest.mjs, estate sweep G1–G4 | any non-zero exit |
| 1 static | static-checks.mjs S01–S19, H01–H03 | any error |
| 2 browser | browser-checks.mjs B01–B07 (4 sizes + 20 s monkey) | any error |
| 2b | hub-save-test.mjs HS1–HS6 | non-zero exit |
| 2c | save-boundary-test.mjs SB1–SB4 | any FAIL |
| 3 package | dist/ + MANIFEST.json (sha256 per file, files changed since ci/published.json, override games) | only if 1–2 green |
| 4 report | ci/reports/<stamp>/REPORT.md + report.json, incl. the publish plan |
Variants: --games a,b (hub always checked), --quick (static + phone smoke; the pre-commit hook), --no-browser,--mark-published <version> (after a publish). Not in the pipeline (run separately): rotation, pass + touch, the
hub suites in §5, the offline test, the walk-off, the override check.
2. Static rules S01–S19 and H01–H03 (ci/static-checks.mjs)
| ID | Level | Checks |
|---|---|---|
| S01 | error | every inline script parses |
| S02 | error | exactly one viewport meta |
| S03 | error | external scripts are https from cdnjs.cloudflare.com, cdn.jsdelivr.net, unpkg.com (ci/pipeline.config.json) |
| S04 | error | a registered service worker file exists |
| S05 | error | new THREE.WebGLRenderer is inside try |
| S06 | error | a .bak save is read back, not only written |
| S07 | error | keyboard real-time games also have touch/pointer input |
| S08 | warn | real-time games pause when hidden (heuristic source scan; a save-only handler does not count) |
| S09 | warn | 3D code that removes objects also disposes |
| S10 | warn | devicePixelRatio is capped |
| S11 | warn | no setInterval ≤ 20 ms as a game loop |
| S12 | warn | no alert/confirm/prompt |
| S13 | warn | no eval/new Function |
| S14 | warn / error | index.html > 400 KB warns, > 1500 KB fails |
| S15 | warn | starts with <!doctype html> |
| S16 | error / warn | kid-safe answers: every answer/target/prompt word passes games/_shared/kidsafe.js (details: node ci/kidsafe-check.mjs <game>; reasoned exceptions in ci/kidsafe-allow.json) |
| S17 | error | a real <meta charset> (not a word in a comment/script) in the first 1024 bytes |
| S18 | error / warn | no undeclared mirror drift: a top-level maneet-games/<game>/ copy is byte-identical or declared in ci/legacy-mirrors.json; nested copies warn |
| S19 | error / warn | save writes are versioned ({v:n,…} or a -v<n> key); baseline games warn, new games error (§4) |
| H01 | error | every GAMES entry has games/<id>/index.html |
| H02 | warn | every game folder is in GAMES |
| H03 | error | hub scripts parse |
Heuristic caution: several rules (S12, S16, S19 at least) also match text inside HTML comments. Observed
2026-10-19 while validating templates/game/index.html: a comment mentioning the dialog function and the store API
produced S12/S16/S19 findings. Keep API names out of comments rather than allow-listing.
Exceptions (ci/allow.json): { "<game>": { "<RULE>": { "reason": "…", "until": "YYYY-MM-DD" } } }. No reason →
ignored; past until → stops counting (MM/ci/lib.mjs, allowFor). Exceptions show as allowed in every report.
3. The release gate table (all must be green for every shipped game)
From GAME-DEVELOPMENT-GUIDE.md §10.4, extended with the suites used in the 19 Oct releases.
| # | Gate | Command | Required | Blocks? |
|---|---|---|---|---|
| 1 | Estate sweep G1–G4 | npm run ci:gates (= sh ci/estate-game-quality-gates.sh --checkout ..) | exit 0 | yes |
| 2 | Gate self-tests | npm run ci:gates-selftest | all PASS | yes |
| 3 | Static S01–S19, H01–H03 | npm run ci:static | 0 errors; warnings listed | yes (errors) |
| 4 | Browser B01–B07 | npm run ci:browser -- <games> | 0 errors | yes |
| 5 | Hub save HS1–HS6 | node ci/hub-save-test.mjs | 6/6 | yes |
| 6 | Save boundary SB1–SB4 | node ci/save-boundary-test.mjs <games> | no FAIL; N/A only with its stated reason | yes |
| 7 | Rotation R1–R8 | node ci/rotate-test.mjs <games> | no failure | yes |
| 8 | Pass + touch | node ci/pass-touch-test.mjs <games> | no failure | yes |
| 9 | Full pipeline | node ci/pipeline.mjs --games <games> | PIPELINE PASSED | yes |
| 10 | Kid-safe | node ci/kidsafe-check.mjs <game> | no error | yes |
| 11 | Real-device walk-off (movement games) | §7 table | both rows filled | yes; headless never closes it |
| 12 | Override games | Code Editor db version checked, not only the file | stated in the note | yes |
| 13 | Hub suites (when the hub, _sdk or a bridge op changed) | §5 | all pass, or a pre-recorded exception | yes |
| 14 | Display sink (D40) | static scan: no dynamic text written outside the one fail-closed sink (06 (06-security.md)) | 0 hits outside the allowlist | yes |
| 15 | Pack budgets (D41, games with content packs) | validator + pack runtime test (08 (08-content-packs.md) §4) | ≤ 150 KB per pack, ≤ 300 ms load, LRU caches dispose on eviction | yes |
| 16 | Factory static F01–F08 (factory games) | node ci/factory-static.mjs <g> | 0 errors | yes (F01 is the D40 sink rule, stricter than S20) |
| 17 | Factory probe P01–P36 + the game's test stub (factory games) | node ci/factory-probe.mjs <g> | no failure | yes |
Gates 16–17 are new (2026-10-19, 11-game-factory.md §4). node ci/game-check.mjs <g> runs
gates 1, 3, 6–8, 10, 14 (the …or Die sink suite), 16, 17 and the pipeline in one command; --full adds 2 and the full
pipeline (4, 5, 9). For a factory game the source heuristics S07, S08, S10 and S11 read only the game file and pass
vacuously (the loop, input and DPR cap live in games/_factory/kit2.js): their runtime proof is P04, P06–P08 and P14.
Gates 14 and 15 were made blocking by the operator on 2026-10-19 but do not exist in MM/ci yet. Until they
are built, they are NOT RUN for every release, which is not PASS: a release needs either the check's result or a gate
exception recorded before publishing (05 (05-release-process.md) step 5). Building them is the first platform task
this implies (owner: CI/hub builder, to be confirmed by the coordinator).
4. S19 baseline and expiry policy
ci/s19-baseline.jsonlists games present when S19 was adopted (2026-10-19). A listed game gets S19 warnings
only while its entry has a non-empty owner, a non-empty remediation and an expires date (YYYY-MM-DD) not
yet passed; otherwise S19 is an error for it (MM/ci/static-checks.mjs:193-197, S19_BASE).
- First expiry: 2026-11-15. Every new game is an error from day one: do not add new games to the baseline.
- Remove a game from the baseline once its saves are versioned, so it cannot regress.
- An S19 warning never excuses an SB1–SB3 failure.
- Owners (operator, 2026-10-19): each entry's owner defaults to the game's last builder of record: the hub
builder for hub games, otherwise the pack or game builder. The coordinator confirms them. On 2026-10-19 the file
still read "not named in the repo; coordinator assigns" (s19-baseline.json); replace that text with the
confirmed name before 2026-11-15.
5. Browser and hub suites
| Suite | IDs | Checks | Command |
|---|---|---|---|
browser-checks.mjs | B01–B07 | headless Chrome, game in an exact-size iframe like the hub, 4 sizes, 20 s seeded monkey (monkeySeed 42); uncaught errors, hangs, sideways scroll, console.error, dialogs, no-GPU 3D, failed loads. --selftest proves it catches a crash, a too-wide page and a blank 3D screen | npm run ci:browser -- <g>; npm run ci:selftest |
hub-save-test.mjs | HS1–HS6 (T1–T6 in the file) | real hub, two Chrome profiles as devices, fake db: legacy migration, progress reaches the doc, merge on sign-in, no blind overwrite, conflict rule, account list merge | node ci/hub-save-test.mjs |
save-boundary-test.mjs | SB1–SB4 | each SDK game in a fake hub: SB1 round-trip incl. an owner marker (a dropped marker FAILS, and since D42 that is a product defect: saves MUST preserve unknown fields); SB2 live player switch, B sees none of A's keys; SB3 fresh boot as B; SB4 XP banking (N/A: no hub XP ledger). --selftest proves it catches a leak. Boots, does not play: progress written deep in play is not exercised | node ci/save-boundary-test.mjs [--json f] <g> |
rotate-test.mjs | R1–R8 | portrait ↔ landscape at 375×667 and 390×844, title and mid-game: no sideways scroll, frame resized, nothing cut off, canvas not stretched/blurred, still fits, turning back restores, no errors | node ci/rotate-test.mjs [--pairs …] <g> |
pass-touch-test.mjs | PASS + T1–T6 | pass/code rewards (none/vip/premium/expired/expiring) lock and unlock and render; touch at 360×640 and 390×844: no sideways scroll, targets ≥ 44 px (probed with elementFromPoint), touch-action on the play surface, no hover-only UI, a swipe does not scroll the page | node ci/pass-touch-test.mjs [--pass] [--touch] <g> |
kidsafe-check.mjs | S16 | answer words vs the shared list; --discover finds new lists; kidsafe-selftest.mjs | node ci/kidsafe-check.mjs <g> |
phase0-test.mjs | A, B, C, D | bridge scopes, Fish It preset chat, Studio publishing approval | node ci/phase0-test.mjs |
hub-f1-test.mjs | R, P, G, W, C, V, B, S, L, Q | cross-page save race, atomic play counts, uid gifts, spend ledger, quest day clock, bridge allowlist scan | node ci/hub-f1-test.mjs |
hub-bundle2-test.mjs | N, Q, A, T, J, S, I, K | boards, coin limits, telemetry, judge bad_reply, offline import plan, cards/quests | node ci/hub-bundle2-test.mjs |
hub-ai-test.mjs | N, S, B, R | AI only for GOAT/hacker, every AI path through aiAllowed, fail closed | node ci/hub-ai-test.mjs |
hub-r4-test.mjs | X, G, C, T, P | GOAT bound to the Claude uid, cross-tab revocation, marker not trusted | node ci/hub-r4-test.mjs |
hub-xplayer-test.mjs | X, C, P | cross-player save leak scenarios | node ci/hub-xplayer-test.mjs |
hub-pl21-test.mjs | C, F, N | AI judge reply contract | node ci/hub-pl21-test.mjs |
hub-avatar-test.mjs, hub-addons-test.mjs | A/W/B, H/I | avatar builder + wallet; HBAY code date, no import control | node ci/<file> |
studio-save-test.mjs | S1–S8 | Game Studio saves and shared board | node ci/studio-save-test.mjs |
offline-test.mjs | A, B | offline host op coverage and play | node ci/offline-test.mjs |
6. Estate sweep G1–G4 (ci/estate-game-quality-gates.sh)
Adopted from muse-alpine-libra's default-check pack v4 (channel ann-modular-20261002 seq 8179; helperhtml_meta.py v3, seq 8159). Read-only; sh + python3.
| Gate | Level | Checks |
|---|---|---|
| G1 | error | a real <meta charset> on every served entry (comment/script-proof parser) |
| G2 | error | exactly one real viewport meta per entry |
| G3 | error | no undeclared canonical-vs-mirror drift (both modes) |
| G3b | warn | nested same-name copies (arcade/, learning-lab/) that differ |
| G4 | warn | doctype present |
Modes: sh ci/estate-game-quality-gates.sh --checkout <maneet-games root> or … ESTATE_ROOT. A wrong or empty
root, zero inventory or a traversal error is an error, never a silent PASS. Declared-legacy mirrors
(ci/legacy-mirrors.json, decision 37) are skipped by G3 and downgraded to [LEGACY] warnings; an entry needs areason and an archive field.
7. Real-device walk-off (movement games)
Headless input is not proof of movement (surf-island, Oct 2026: avatar did not move under headless keys/touch while
rAF ran; recorded inconclusive, never as a game bug). Before releasing any title where an avatar moves:
| Device | Browser | Input | FPS | Distance moved | Result | Who / when |
|---|---|---|---|---|---|---|
| a phone | Safari/Chrome | touch joystick, on-screen d-pad | ||||
| a desktop | Chrome | WASD, arrows |
At least one phone and one desktop; every input the game offers; distance observed in game (crossing a landmark).
Until both rows exist the scorecard says NOT DONE and the game does not release.
Also do the 5-minute phone check on any change (GAME-DEVELOPMENT-GUIDE.md §4 step 10): starts and plays by touch;
nothing cut off either way round; hide the tab and return (paused, no time lost); close and reopen (progress kept);
restart (no double speed, no ghost game-over).
8. What the gates do not prove
Fun, correctness of deep levels, real-device feel and performance, real tab switches, sound, gameplay-written saves
(SB boots only; 13 SDK games had no save exercised on 2026-10-19, master-mind-games seq 798), XP banking, and
anything about the live artifact unless the tested file's hash equals the published one.