mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
CI failed at format:check: the format scope still listed the deleted panelChrome.radioOwnership test, and panelRails.test.mjs needed Prettier after its glow list shrank. The new Context title colour outranked Cyber's red panel titles, and the collapsed Context launcher came out brighter than its siblings; the rule now applies only to the expanded panel outside Cyber. Cyber's layout-focus standby card had relied on the old dashed border and centring, so it drew a 3px frame with its text off the radar art; it now names its own 1px frame, padding and centring. Cyber also drops the shared segmented track, which drew a third frame around its already framed mode buttons. Tests now pin the Context header to its title and collapse control, the segmented control, 10px type, the missing halo and the scoped title colour, plus the Cyber standby and track overrides. Each fails on the previous CSS or markup. qa-radio enables Radio and starts the first Play from the visible controls, expanding Context and Radio the way a person must, then puts the panels back. The Escape test and its mutation describe what they still protect. CURRENT-STATE no longer describes the removed Radio icon or compact player.
526 lines
22 KiB
JavaScript
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 lands on the Cockpit utility disclosure's
|
|
// Escape claim and nowhere else.
|
|
defect: 'a Cockpit utility disclosure returns to stopPropagation, so later document listeners also act on its ESC',
|
|
file: 'radio',
|
|
from: " event.stopImmediatePropagation();\n const kind = displayOpen ? 'display' : 'radio';",
|
|
to: " event.stopPropagation();\n const kind = displayOpen ? 'display' : 'radio';",
|
|
},
|
|
{
|
|
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;
|