Files
gods-eye-view/scripts/qa-firstrun-mutations.mjs
quanticsoul4772andClaude Opus 5.5 123bf55f39 fix(qa): repoint stale mutation anchors after the September refactors
The three mutation gates matched source text that has since moved or been
reflowed, so 37 of their 108 reverts reported STALE and could no longer
prove anything:

- qa-floorhold-mutations (2): the neighbour-floor code moved from
  src/data/groundFloor.js into src/services/groundFloor.js (ac977bd).
- qa-flyroute-mutations (8): src/cameraVerbs.js was reflowed by the
  formatter (2aca7ae); the anchors now match the formatted lines.
- qa-firstrun-mutations (27): the launcher markup, styles and startup moved
  to src/ui/templates/welcome.html, src/ui/styles/first-run.css and
  src/app/startupChrome.js; the rail default to panelPositionControls.js and
  the radio ESC handling to radioBindings.js (a67bc60 and follow-ups). The
  tool-schema pin now edits a tool description, since the context-mode enum
  it used to edit no longer lives in tools.js.

Every revert still reintroduces the same defect. Results: floor-hold 22/22,
fly_route 19/19, first-run 67/67 caught, with all source files restored.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-11 13:52:30 -07:00

526 lines
22 KiB
JavaScript

#!/usr/bin/env node
/**
* Are the first-run launcher's pins actually load-bearing?
*
* A pin that only goes red when you delete the whole feature proves very little.
* This reverts each decision INDIVIDUALLY — the smallest edit that reintroduces
* the original defect or contradicts the owner's ruling — and requires
* src/firstRunExperience.test.mjs to go red for it. Every entry names what it
* restores, so the count is reproducible rather than asserted in a commit
* message.
*
* node scripts/qa-firstrun-mutations.mjs
*
* Every touched file is restored on exit, including on failure.
*
* NOTE: two mutations edit the server voice tools/instructions, and a running dev server watches those
* files and restarts on every write. Writes are therefore content-guarded below
* so the file is touched exactly twice per mutation instead of on every
* iteration — enough that a dev server survives, but expect it to restart. If
* you are mid-QA on a live server, run this before or after, not during.
*/
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const TESTS = ['src/firstRunExperience.test.mjs', 'src/standalone/startupChrome.test.mjs'];
const FILES = {
keyboard: path.join(ROOT, 'src', 'ui', 'surfaceKeyboard.js'),
module: path.join(ROOT, 'src', 'firstRunExperience.js'),
html: path.join(ROOT, 'src', 'ui', 'templates', 'welcome.html'),
css: path.join(ROOT, 'src', 'ui', 'styles', 'first-run.css'),
voiceInstructions: path.join(ROOT, 'server/providers/openai/instructions.js'),
voiceDescriptions: path.join(
ROOT,
'server/providers/openai/toolDescriptions.js',
),
main: path.join(ROOT, 'src', 'app', 'startupChrome.js'),
ui: path.join(ROOT, 'src', 'ui', 'panelPositionControls.js'),
radio: path.join(ROOT, 'src', 'ui', 'radioBindings.js'),
docs: path.join(ROOT, 'docs', 'CURRENT-STATE.md'),
};
/** @type {Array<{defect: string, file: keyof FILES, from: string, to: string}>} */
const MUTATIONS = [
// ── Show policy (owner ruling: session-scoped dismiss vs durable checkbox) ──
{
defect: 'dismissing writes the DURABLE key, so the launcher never returns',
file: 'module',
from: "writeStored('session', sessionStorageRef, FIRST_RUN_SESSION_KEY, 'dismissed');",
to: "writeStored('local', sessionStorageRef, FIRST_RUN_STORAGE_KEY, 'suppressed');",
},
{
defect: 'the checkbox is ignored, so nothing can ever suppress the launcher',
file: 'module',
from: " ? writeStored('local', storage, FIRST_RUN_STORAGE_KEY, 'suppressed')",
to: ' ? false',
},
{
defect: 'unticking the box cannot take the suppression back',
file: 'module',
from: " : removeStored('local', storage, FIRST_RUN_STORAGE_KEY);",
to: ' : true;',
},
{
defect: 'the session flag is never read, so it re-nags on every reload',
file: 'module',
from: " if (\n readStored('session', sessionStorageRef, FIRST_RUN_SESSION_KEY) ===\n 'dismissed'\n )\n return false;",
to: ' // session check removed',
},
{
defect: 'the durable flag is never read, so the checkbox does nothing',
file: 'module',
from: " if (readStored('local', storage, FIRST_RUN_STORAGE_KEY) === 'suppressed')\n return false;",
to: ' // durable check removed',
},
{
defect: '?welcome=1 no longer outranks suppression, so demos cannot replay it',
file: 'module',
from: "if (params.get('welcome') === '1') return true;",
to: '// replay hatch removed',
},
{
defect: '?welcome=0 stops suppressing',
file: 'module',
from: "if (params.get('welcome') === '0') return false;",
to: '// suppression param removed',
},
{
defect: 'a share link gets the launcher over the view its author chose',
file: 'module',
from: 'if (hasShareState) return false;',
to: '// share bypass removed',
},
{
defect: 'storage is read from a DEFAULT PARAMETER, outside every guard',
file: 'module',
from: 'export function rememberFirstRunSessionDismissed(sessionStorageRef) {',
to: 'export function rememberFirstRunSessionDismissed(sessionStorageRef = globalThis.sessionStorage) {',
},
{
defect: 'shouldShowFirstRun resolves storage in its parameter list again',
file: 'module',
from: 'export function shouldShowFirstRun({\n hasShareState = false,\n storage,\n sessionStorageRef,',
to: 'export function shouldShowFirstRun({\n hasShareState = false,\n storage = globalThis.localStorage,\n sessionStorageRef = globalThis.sessionStorage,',
},
{
defect: 'the guarded resolver stops catching, so a hostile getter escapes',
file: 'module',
from: " try {\n return kind === 'session'\n ? globalThis.sessionStorage\n : globalThis.localStorage;\n } catch {\n // Privacy-restricted storage should not make first launch silent.\n return null;\n }",
to: " return kind === 'session' ? globalThis.sessionStorage : globalThis.localStorage;",
},
{
defect: 'a blocked getItem throws instead of reading as "nothing stored"',
file: 'module',
from: ' return resolveStore(kind, injected)?.getItem?.(key) ?? null;\n } catch {\n return null;\n }',
to: ' return resolveStore(kind, injected)?.getItem?.(key) ?? null;\n } finally {\n // no catch\n }',
},
// ── ESC arbitration ───────────────────────────────────────────────────────
{
defect: 'the key handler trusts the CLASS, so an invisible card eats ESC',
file: 'module',
from: 'isActive: () => !closing && isTopmost(),',
to: 'isActive: () => !closing,',
},
{
defect: 'the launcher stops yielding and fights Cockpit/Scenes for ESC',
file: 'module',
from: 'if (revealed && blocked) yieldToExclusiveSurface();\n else if (!revealed && !blocked) reveal();',
to: 'if (!revealed) reveal();',
},
{
defect: 'nothing watches for another surface taking the screen',
file: 'module',
from: " surfaceObserver?.observe(documentRef.body, {\n attributes: true,\n attributeFilter: ['class'],\n });",
to: ' /* observer never attached */',
},
{
defect: 'cockpit drops off the exclusive list and can stack with the launcher',
file: 'module',
from: "export const EXCLUSIVE_SURFACE_CLASSES = Object.freeze([\n 'cockpit-mode',",
to: 'export const EXCLUSIVE_SURFACE_CLASSES = Object.freeze([',
},
{
defect: 'a scene hides the launcher in CSS but the JS never learns of it',
file: 'css',
from: 'body.scene-playback-mode #first-run-launcher {',
to: 'body.scene-playback-mode-typo #first-run-launcher {',
},
{
defect: 'yielding steals focus back from the surface that just took over',
file: 'module',
from: 'dismiss({ restoreFocus: false });',
to: 'dismiss();',
},
{
defect: 'isTopmost stops hit-testing, so an unclassed overlay buries the card again',
file: 'module',
from: ' root.getClientRects().length > 0 &&\n !coveredByOverlay();',
to: ' root.getClientRects().length > 0;',
},
{
defect: 'an inconclusive hit test reads as COVERED, so ESC quietly stops working',
file: 'module',
from: ' return Boolean(hit) && !root.contains(hit);',
to: ' return !root.contains(hit);',
},
{
defect: 'the launcher stops honouring a key another surface already claimed',
file: 'keyboard',
from: ' || event.defaultPrevented',
to: ' /* belt removed */',
},
{
// Anchored past the call so it cannot land on the cockpit disclosure's own
// stopImmediatePropagation() a few lines below and prove nothing about this.
defect: 'the radio disclosure returns to stopPropagation, so one ESC does two things',
file: 'radio',
from: ' event.stopImmediatePropagation();\n const escapedFromDisclosure =\n event.target === this._contextRadioToggleBtn ||',
to: ' event.stopPropagation();\n const escapedFromDisclosure =\n event.target === this._contextRadioToggleBtn ||',
},
{
defect: 'the "no timer" decision is deleted, so the next editor re-litigates it blind',
file: 'module',
from: ' * ACCEPTED, DELIBERATELY NOT TIMED OUT: a surface class that never clears',
to: ' * (note removed)',
},
{
defect: 'a bounded reveal timer punches the card through a recording in progress',
file: 'module',
from: ' const syncToExclusiveSurfaces = () => {\n if (closing) return;',
to: ' const syncToExclusiveSurfaces = () => {\n if (closing) return;\n globalThis.setTimeout?.(reveal, 45000);',
},
{
defect: 'the accepted no-show vanishes from CURRENT-STATE and reads as a bug',
file: 'docs',
from: 'a surface class that never clears means no launcher for that page',
to: 'the launcher always turns up eventually for that page',
},
// ── The checkbox may not promise what storage refused ─────────────────────
{
defect: 'a refused write leaves the box ticked, promising a suppression nobody stored',
file: 'module',
from: ' if (setFirstRunSuppressed(wanted, storage)) return;',
to: ' setFirstRunSuppressed(wanted, storage);\n return;',
},
{
defect: 'a missing storage area reports the write as saved',
file: 'module',
from: " if (typeof store?.setItem !== 'function') return false;\n store.setItem(key, value);\n return true;",
to: ' store?.setItem?.(key, value);\n return true;',
},
{
defect: 'the scroll fade promises more list on a card where everything fits',
file: 'module',
from: 'choiceList.dataset.scrollable = String(overflows);',
to: "choiceList.dataset.scrollable = 'true'; void overflows;",
},
{
defect: 'the DISPLAY rail opens on first run again, stealing the impression',
file: 'ui',
from: "if (panelId === 'pp-toggles' && stored === null) collapsed = true;",
to: "if (panelId === 'pp-toggles' && stored === null) collapsed = false;",
},
{
defect: 'the first-run rail default overrides a stored user choice',
file: 'ui',
from: "if (panelId === 'pp-toggles' && stored === null) collapsed = true;",
to: "if (panelId === 'pp-toggles') collapsed = true;",
},
// ── Mission definitions ────────────────────────────────────────────────────
{
defect: 'the removed INFRASTRUCTURE tile comes back as a one-click globe dump',
file: 'module',
from: ' environmental: Object.freeze({',
to: " infrastructure: Object.freeze({\n kind: 'globe',\n layerIds: Object.freeze(['local-datacenters', 'local-dams', 'telegeography-submarine-cables']),\n busyText: 'Mapping global infrastructure…',\n }),\n environmental: Object.freeze({",
},
{
defect: 'ENVIRONMENTAL drops the keyless earthquakes that carry it without a key',
file: 'module',
from: "layerIds: Object.freeze(['earthquakes', 'local-firms']),",
to: "layerIds: Object.freeze(['local-firms']),",
},
{
defect: 'FIRMS is dropped again from the tile whose subcopy promises it',
file: 'module',
from: "layerIds: Object.freeze(['earthquakes', 'local-firms']),",
to: "layerIds: Object.freeze(['earthquakes']),",
},
{
defect: 'the tile stops promising the fires it actually turns on',
file: 'html',
from: '<small>Live earthquakes and active fires, from USGS and NASA</small>',
to: '<small>Live earthquakes worldwide, straight from USGS</small>',
},
{
defect: "the owner-authored first-run line is quietly rewritten",
file: 'html',
from: 'It feels like a forbidden cockpit—then you realize the sources are public and the data is real.',
to: "It feels like a forbidden cockpit. It isn't — every feed is public, and every contact is live.",
},
{
defect: 'the ledgered global-chip defect loses its pointer to the real fix',
file: 'module',
from: ' // means a KEY REQUIRED terminal state in src/loadingFeedback.js, a state',
to: ' // (note removed)',
},
{
defect: 'the globe missions never pull the camera out',
file: 'module',
from: ' .then(() => flyToGlobe())',
to: ' .then(() => null)',
},
{
defect: 'a refused layer is reported as a successful mission',
file: 'module',
from: 'return { ok: failedLayerIds.length === 0, choice, failedLayerIds };',
to: 'return { ok: true, choice, failedLayerIds };',
},
{
defect: 'a cancelled globe flight fails the whole mission',
file: 'module',
from: ' .catch(() => null);',
to: ' .catch(() => { throw new Error("flight failed"); });',
},
{
defect: 'Explore manually quietly runs a mission instead of nothing',
file: 'module',
from: "if (mission.kind === 'none') return { ok: true, choice };",
to: "if (mission.kind === 'none') return { ok: true, choice, ran: await setContextMode('contacts') };",
},
{
defect: 'an unknown choice is treated as a success',
file: 'module',
from: 'if (!mission) return { ok: false, choice };',
to: 'if (!mission) return { ok: true, choice };',
},
{
defect: 'the fires/quakes tile name stops being switchable from one constant',
file: 'module',
from: 'return ENVIRONMENTAL_LABELS[choice] || ENVIRONMENTAL_LABELS.ENVIRONMENTAL;',
to: 'return ENVIRONMENTAL_LABELS.ENVIRONMENTAL;',
},
// ── Defaults interplay (the ruling this whole integration turns on) ────────
{
defect: "mission layer enables stop persisting, unlike the clicks they stand for",
file: 'module',
from: "dataManager.setEnabled(layerId, true, { origin: 'user' })",
to: 'dataManager.setEnabled(layerId, true)',
},
{
defect: 'a mission hand-edits detection and kills the style auto-preset contract',
file: 'module',
from: ' flyToGlobe: () => styleManager.resetToGlobeView(),',
to: ' flyToGlobe: () => { styleManager._detectionUserOverridden = true; return styleManager.resetToGlobeView(); },',
},
{
defect: 'a mission persists a 3D-models choice nobody made',
file: 'module',
from: ' const result = await styleManager.setContextMode(mode);',
to: ' styleManager._setModels3dEnabled?.(true);\n const result = await styleManager.setContextMode(mode);',
},
{
defect: 'the Context panel is left collapsed, hiding the mode that just started',
file: 'module',
from: " styleManager.setPanelCollapsed?.('global-context-panel', false, {\n explicit: true,\n });",
to: ' // panel reveal removed',
},
{
defect: 'the decision table is deleted, so the next editor re-litigates it blind',
file: 'module',
from: ' * MISSION → APP STATE, AND WHAT IT IS ALLOWED TO PERSIST',
to: ' * (notes removed)',
},
// ── Accessibility and the transplant defects ──────────────────────────────
{
defect: 'disabling the focused tile drops the keyboard to <body> mid-flight',
file: 'module',
from: " for (const button of buttons)\n button.setAttribute('aria-disabled', String(next));",
to: ' for (const button of buttons) button.disabled = next;',
},
{
defect: 'Tab escapes into an app the visitor has not seen yet',
file: 'keyboard',
from: "if (event.key !== 'Tab') return;",
to: 'return;',
},
{
defect: 'ESC no longer dismisses the launcher',
file: 'keyboard',
from: "if (event.key === 'Escape') {",
to: 'if (false) {',
},
{
defect: 'focus is never returned to where the visitor left it',
file: 'keyboard',
from: "if (typeof target?.focus === 'function' && target.isConnected) {",
to: 'if (false) {',
},
{
defect: 'app hotkeys eat the launcher keys (bubble instead of capture)',
file: 'keyboard',
from: "documentRef.addEventListener('keydown', onKeyDown, true);",
to: "documentRef.addEventListener('keydown', onKeyDown);",
},
{
defect: 'the card stays clickable through its fade-out (double-launch)',
file: 'css',
from: ' transition:\n opacity 220ms ease,\n transform 280ms cubic-bezier(0.2, 0.8, 0.2, 1);\n /* Inert until revealed AND again while fading out, so a click during the\n dismiss animation cannot launch a second mission behind the fade. */\n pointer-events: none;',
to: ' transition:\n opacity 220ms ease,\n transform 280ms cubic-bezier(0.2, 0.8, 0.2, 1);',
},
{
defect: 'the launcher survives Clean-UI and recording mode',
file: 'css',
from: 'body.ui-clean-view #first-run-launcher,',
to: 'body.never-matches-anything #first-run-launcher,',
},
{
defect: 'the flex card defeats [hidden] and sits in the a11y tree pre-reveal',
file: 'css',
from: '#first-run-launcher[hidden] {\n display: none;\n}',
to: '/* [hidden] rule removed */',
},
{
defect: 'the card loses its viewport cap and clips on a short landscape phone',
file: 'css',
from: ' max-height: calc(100dvh - 1.5rem);',
to: ' /* cap removed */',
},
{
defect: 'the mission list stops scrolling, so tiles overflow the capped card',
file: 'css',
from: ' min-height: 0;\n overflow-y: auto;',
to: ' /* scroll removed */',
},
{
// Anchored to the launcher's OWN selectors: style.css has four other
// reduced-motion blocks, and a bare `@media` anchor would mutate one of
// those instead and prove nothing about this feature.
defect: "the launcher's reduced-motion opt-out is dropped",
file: 'css',
from: ' #first-run-launcher,\n .first-run-choices button,\n .first-run-arrow {\n transition: none;\n }\n /* Scrolling a tile into view must not animate either. */\n .first-run-choices {\n scroll-behavior: auto;\n }',
to: ' .first-run-nothing {\n transition: none;\n }',
},
// ── Markup and startup ordering ───────────────────────────────────────────
{
defect: 'a mission tile is dropped from the menu',
file: 'html',
from: ' <button type="button" data-first-run-choice="space-missions">',
to: ' <button type="button" data-first-run-choice-disabled="space-missions">',
},
{
defect: 'the menu order stops matching the owner\'s',
file: 'html',
from: '<button type="button" data-first-run-choice="environmental">',
to: '<button type="button" data-first-run-choice="zzz-environmental">',
},
{
defect: 'the "don\'t show again" checkbox is removed',
file: 'html',
from: '<input type="checkbox" data-first-run-suppress />',
to: '<span data-first-run-suppress-removed></span>',
},
{
defect: 'the status line stops being a polite live region',
file: 'html',
from: 'data-first-run-status role="status" aria-live="polite"',
to: 'data-first-run-status',
},
{
defect: 'the launcher is no longer announced as a dialog',
file: 'html',
from: '<aside id="first-run-launcher" role="dialog"',
to: '<aside id="first-run-launcher"',
},
{
defect: 'the launcher is revealed before the loading cover yields',
file: 'main',
from: " loadingScreen.classList.add('hidden');",
to: " revealFirstRun();\n loadingScreen.classList.add('hidden');",
},
{
defect: 'the globe missions lose their DataManager and cannot enable layers',
file: 'main',
from: 'firstRun = initializeWelcome?.({ styleManager, dataManager });',
to: 'firstRun = initializeWelcome?.({ styleManager });',
},
// ── Voice: schema must not drift ──────────────────────────────────────────
{
defect: 'the voice TOOL SCHEMA is edited (a Realtime prompt-cache bust)',
file: 'voiceDescriptions',
from: "'Counts, lists, superlatives and attribute filters over records loaded by ENABLED layers",
to: "'Counts, lists, superlatives, and attribute filters over records loaded by ENABLED layers",
},
{
defect: 'the instruction mapping is dropped, so voice cannot reach the modes',
file: 'voiceInstructions',
from: " 'NAMED VIEWS are shorthand",
to: " // 'NAMED VIEWS are shorthand",
},
];
const originals = new Map();
for (const [key, file] of Object.entries(FILES)) originals.set(key, fs.readFileSync(file, 'utf8'));
/** Write only on a real change: a no-op write still wakes every file watcher. */
const write = (file, next) => {
if (fs.readFileSync(file, 'utf8') !== next) fs.writeFileSync(file, next);
};
const restoreAll = () => {
for (const [key, file] of Object.entries(FILES)) write(file, originals.get(key));
};
process.on('exit', restoreAll);
let caught = 0;
const missed = [];
console.log(`\nFirst-run launcher pin strength — ${MUTATIONS.length} individual reverts\n`);
for (const { defect, file, from, to } of MUTATIONS) {
const original = originals.get(file);
if (!original.includes(from)) {
missed.push(`${defect} (ANCHOR MISSING — the mutation no longer applies)`);
console.log(` \x1b[31mSTALE\x1b[0m ${defect}`);
continue;
}
write(FILES[file], original.replace(from, to));
let red = false;
let by = '';
try {
execFileSync('node', ['--test', ...TESTS], { cwd: ROOT, encoding: 'utf8', stdio: 'pipe' });
} catch (error) {
red = true;
const failed = String(error.stdout || '').split('\n')
.filter((line) => line.trim().startsWith('✖') && line.includes('('))
.map((line) => line.trim().slice(2).split(' (')[0]);
by = [...new Set(failed)].slice(0, 2).join('; ');
}
write(FILES[file], original);
if (red) {
caught += 1;
console.log(` \x1b[32mRED \x1b[0m ${defect}\n caught by: ${by}`);
} else {
missed.push(defect);
console.log(` \x1b[31mGREEN\x1b[0m ${defect} <-- no pin covers this`);
}
}
restoreAll();
console.log(`\n ${caught}/${MUTATIONS.length} defects caught by the pins`);
if (missed.length) {
console.log('\n uncovered:');
for (const item of missed) console.log(` - ${item}`);
}
console.log('');
process.exitCode = missed.length ? 1 : 0;