Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / tools/verify/visual-sweep/sweep.js |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | tools/verify/visual-sweep/sweep.js |
| Original SHA-256 | 24e64a7cd92158de08142d518e00f10e3d30897d82891c10927a53e293ec8392 |
| Git blob SHA | edaad649a27bab277a65d2b8ef074b48e3dac887 |
| Reading-copy SHA-256 | 30109afa289cbf43df3aa658184be93b9d5c7217a717edd377be76a3dfcbec2d |
Recovered source transcript
Download complete source text ↓493 lines · 28.7 KB
Credential-bearing and unrelated personal details were removed from this reading copy. The source identity and original checksum remain recorded.
// The Gate C instrument: how Claude verifies with its eyes, made mechanical.
//
// Licensed by docs/BUILD_CONTINUATION_PROTOCOL.md Section 0 item 4 as tools/verify/visual-sweep. It logs in
// through the REAL login flow as each role the phase touches, walks a route manifest, renders at five
// viewports, and asserts. It does not replace looking; it makes looking repeatable and makes the boring
// failures impossible to miss so a human can spend attention on the interesting ones.
//
// POSITIVE CONTROL FIRST, always. Run with --control and a deliberately broken page is injected: the sweep
// MUST fail on it before any real run is allowed to count. This build has been burned four times by an
// instrument that returned a clean negative while measuring nothing, including a route interception that
// silently never fired and made a pixel diff prove exactly nothing.
//
// Usage:
// node sweep.js --base https://host --roles a@b:pw,c@d:pw --out DIR [--control]
const { chromium } = require('playwright');
const fs = require('fs');
const path = require('path');
// THE MATRIX IS SPLIT BY SURFACE, not relaxed. Recorded product decision, Joseph, 28 July 2026.
//
// authenticated 768 and up. missionMATCH is a desk and capture tool used at a desk, and the supported
// viewport floor is 768 for the duration of the design partner program. Below the floor is
// UNSUPPORTED, which is a different statement from broken.
// public all five. Candidates arrive on phones, so a public surface holds the whole range with no
// relaxation whatsoever.
//
// A single relaxed number would have quietly excused the public pages too, which is exactly the failure this
// split prevents. Pass --surface public to assert the full matrix.
const VIEWPORTS_AUTHENTICATED = [768, 1024, 1440];
const VIEWPORTS_PUBLIC = [320, 390, 768, 1024, 1440];
// THE SHELL BASELINE, the standing guard rule. The application shell needs about 732px of content width,
// measured at Gate C on 28 July 2026. That number is a known quantity, and the guard is that NO NEW SCREEN
// MAY RAISE IT: a route whose minimum content width exceeds the baseline is a regression even at supported
// widths, because without this assertion "we support 768" decays into "we support whatever the widest screen
// happens to need". The allowance is deliberately small.
const SHELL_MIN_WIDTH_BASELINE = 732;
const SHELL_MIN_WIDTH_ALLOWANCE = 24;
// Below the supported floor on purpose: the guard needs a width the page cannot satisfy in order to observe
// what the page actually requires.
const PROBE_WIDTH = 320;
// TRANSPORT CLASS. These say something about the link between the browser and the host, not about the screen
// under test, and they are the only findings eligible to be dismissed by a confirming reload.
const TRANSPORT_KINDS = new Set(['CONSOLE_ERROR', 'FAILED_REQUEST', 'LOAD_FAILED']);
const isTransport = f => TRANSPORT_KINDS.has(f.kind);
// THE ONE ALLOWED ERROR RESPONSE, named exactly, with its reason, and deliberately hard to widen.
//
// An anonymous visitor loading any page triggers the session probe, and the API answers 403 "no session",
// which is correct behaviour: nobody is logged in. Chromium logs every non 2xx fetch to the console, so the
// zero console errors assertion would fail forever on the login page for a system that is working.
//
// The allowance is keyed to the ROLE, the STATUS, and the EXACT path together, so it cannot quietly cover a
// 403 anywhere else, a different status on the same path, or the same response seen while logged in, which
// WOULD be a real defect. If any other error response is present on the page, nothing is dropped at all.
//
// Separately: 403 is the wrong code for "not authenticated", 401 is. That is a real observation and it is
// recorded in docs/IMPROVEMENTS.md rather than fixed here, because changing an auth status code mid gate is
// exactly the unscoped drift the protocol exists to prevent.
const ALLOWED_ERROR_RESPONSES = [{ role: 'anonymous', status: 403, path: '/api/auth/session' }];
const isAllowedResponse = (role, entry) => ALLOWED_ERROR_RESPONSES.some(a => {
if (a.role !== role) return false;
const m = entry.match(/^(\d{3}) [A-Z]+ (\S+)$/);
if (!m) return false;
return Number(m[1]) === a.status && new URL(m[2]).pathname === a.path;
});
function arg(name, fallback) {
const i = process.argv.indexOf('--' + name);
return i > -1 ? process.argv[i + 1] : fallback;
}
const CONTROL = process.argv.includes('--control');
const SURFACE = arg('surface', 'authenticated');
// ============================================================================
// THE PIXEL REGRESSION HALF OF GATE C. Added 29 July 2026, and it should have existed from the start.
// ============================================================================
//
// The protocol has always asked for this in as many words: "Screens the phase did NOT touch are pixel diffed
// against their approved baselines with pixelmatch. A diff above threshold on an untouched screen is a failure,
// not a curiosity." NONE OF IT WAS IMPLEMENTED. There was no pixelmatch, no baseline directory, and no
// comparison of any kind.
//
// WORSE, THE GAP WAS MISATTRIBUTED. Two Gate C write ups recorded that the pixel half "cannot run yet" because
// no contact sheet had been approved, which reads as waiting on a person. It was waiting on code. When the
// approval finally came it unblocked nothing, because there was nothing to unblock. That is the seventh time in
// this build that a missing implementation was filed as a pending decision, and it is the most expensive kind
// of bookkeeping error: it moves an engineering task into someone else's queue where nobody works on it.
//
// HOW IT WORKS. A baseline is a PNG under BASELINES with the same filename the capture gets. Every capture with
// a matching baseline is compared. A capture with none is reported as NEW, which is information rather than a
// failure, so the first run can establish a set.
//
// DIMENSIONS ARE PART OF THE COMPARISON. Full page screenshots change height when layout changes, and a naive
// diff would either crash or silently skip exactly the case worth catching, so a size change IS a regression
// and is reported as one with both sizes named.
//
// THE THRESHOLD IS A RATIO, not a pixel count, because a full page capture at 1440 has many times the pixels of
// one at 320 and a fixed count would mean something different at each width.
const BASELINES = arg('baselines', path.join(__dirname, 'baselines', SURFACE));
const APPROVE = process.argv.includes('--approve');
const PIXEL_THRESHOLD = Number(arg('pixel-threshold', '0.005')); // 0.5 percent of pixels
let pixelmatch = null, PNG = null;
try {
pixelmatch = require('pixelmatch');
if (pixelmatch && pixelmatch.default) pixelmatch = pixelmatch.default;
PNG = require('pngjs').PNG;
} catch (e) {
// Named loudly at the end of the run rather than silently skipped, which is the whole lesson above.
}
const pixelReport = { compared: 0, isNew: 0, regressions: 0, unavailable: !pixelmatch || !PNG };
function diffCapture(file, role, width, route) {
if (!pixelmatch || !PNG) return null;
const basePath = path.join(BASELINES, file);
if (!fs.existsSync(basePath)) { pixelReport.isNew++; return null; }
const a = PNG.sync.read(fs.readFileSync(basePath));
const b = PNG.sync.read(fs.readFileSync(path.join(OUT, file)));
if (a.width !== b.width || a.height !== b.height) {
pixelReport.regressions++;
return {
role, width, route, kind: 'PIXEL_REGRESSION',
detail: `capture size changed, baseline ${a.width}x${a.height} against ${b.width}x${b.height}`,
};
}
pixelReport.compared++;
const diff = new PNG({ width: a.width, height: a.height });
const changed = pixelmatch(a.data, b.data, diff.data, a.width, a.height, { threshold: 0.1 });
const ratio = changed / (a.width * a.height);
if (ratio > PIXEL_THRESHOLD) {
pixelReport.regressions++;
fs.writeFileSync(path.join(OUT, 'diff_' + file), PNG.sync.write(diff));
return {
role, width, route, kind: 'PIXEL_REGRESSION',
detail: `${changed} pixels differ, ${(ratio * 100).toFixed(3)} percent, threshold ${(PIXEL_THRESHOLD * 100).toFixed(3)} percent, see diff_${file}`,
};
}
return null;
}
const VIEWPORTS = SURFACE === 'public' ? VIEWPORTS_PUBLIC : VIEWPORTS_AUTHENTICATED;
const BASE = arg('base', 'http://127.0.0.1:8097');
const OUT = arg('out', './visual');
const ROUTES = (arg('routes', '/,/settings,/admin,/compliance,/command,/screen,/slate,/recruit')).split(',');
// ROLES MAY COME FROM THE ENVIRONMENT, and on a run that lasts minutes it should.
// --roles puts every fixture password in argv, where ps exposes it to any account on the host for the whole
// sweep. MM_SWEEP_ROLES carries the same value out of argv. The flag is kept for the recorded runbook.
const ROLES = (arg('roles', '') || process.env.MM_SWEEP_ROLES || '').split(',').filter(Boolean).map(r => {
const [email, ...rest] = r.split(':');
return { email, password: [credential removed]':') };
});
const goWithRetry = async (page, url) => {
for (let i = 0; i < 4; i++) {
try { await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 40000 }); return true; }
catch (e) { await new Promise(s => setTimeout(s, 1200)); }
}
return false;
};
async function login(page, base, role) {
if (!role) return true;
// RETRIED AND CAUGHT, because a tailnet blip during the CSRF fetch used to throw out of an unguarded
// page.evaluate and kill the ENTIRE sweep on its first route. A crashed sweep is an UNMEASURED sweep, and
// an instrument that dies silently on transport noise is the same failure as one that passes silently.
// The limiter is also deliberately strict, so a transient throttle deserves a pause rather than a verdict.
for (let attempt = 0; attempt < 3; attempt++) {
if (attempt) await new Promise(s => setTimeout(s, 2500));
if (!await goWithRetry(page, base + '/')) continue;
try {
// Through the REAL login flow, not by injecting a cookie. A sweep that fakes its own session proves
// nothing about whether a person can actually get in.
const csrf = await page.evaluate(async (b) => {
const r = await fetch(b + '/api/auth/csrf', { credentials: 'include' });
return (await r.json()).csrf_token;
}, base);
const status = await page.evaluate(async ({ b, c, e, p }) => {
const r = await fetch(b + '/api/auth/login', {
method: 'POST', credentials: 'include',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': c },
body: JSON.stringify({ email: e, password: p }),
});
return r.status;
}, { b: base, c: csrf, e: role.email, p: role.password });
if (status === 200) return true;
// 401 is a real answer and retrying it just burns the limiter; anything else may be transport.
if (status === 401 || status === 403) return false;
} catch (e) {
// fall through to the next attempt
}
}
return false;
}
(async () => {
fs.mkdirSync(OUT, { recursive: true });
// PIN THE HOSTNAME TO THE LOOPBACK, keeping the URL, the Origin, the SNI, and the certificate identical.
//
// Bubbles resolves its own public hostname to its public address and hairpins back through the router,
// across a host that has TWO active interfaces on the same LAN, wired and wireless. Chromium sees the
// routes flap and reports net::ERR_NETWORK_CHANGED, which the sweep then reported as the application's
// fault. That is a property of this host's networking and says nothing whatsoever about missionMATCH.
//
// This is NOT the earlier 127.0.0.1 mistake, which changed the URL and so sent the wrong Origin and had
// every login refused by the CSRF allowlist. The address is remapped underneath an unchanged URL, so the
// request the server receives is byte for byte the one it would receive from outside. Pass --no-pin to
// sweep over the real wire when the wire is the thing being tested.
const host = new URL(BASE).hostname;
const resolverArgs = process.argv.includes('--no-pin') || host === '127.0.0.1' || host === 'localhost'
? [] : [`--host-resolver-rules=MAP ${host} 127.0.0.1`];
const browser = await chromium.launch({ executablePath: '/usr/bin/chromium', args: [
'--no-sandbox',
...resolverArgs,
// QUIC over the tailnet produced ERR_QUIC_PROTOCOL_ERROR and ERR_NETWORK_CHANGED on roughly a quarter
// of requests, which surfaced as FAILED_REQUEST and EMPTY_SCREEN findings that were transport noise
// rather than defects. An instrument that reports its own flakiness as the system's faults trains
// people to ignore it, so the transport is pinned to something stable instead.
'--disable-quic',
'--disable-features=NetworkService,NetworkServiceInProcess',
] });
const findings = [];
const shots = [];
const probed = [];
const transient = [];
const expected = [];
const targets = ROLES.length ? ROLES : [null];
for (const role of targets) {
const label = role ? role.email.split('@')[0] : 'anonymous';
// LOG IN ONCE PER ROLE, then reuse the session across every viewport.
//
// The first version logged in per role PER VIEWPORT, which is five logins in a few seconds and the auth
// rate limiter correctly threw "rate limited" at it. That limiter exists to slow password guessing and
// it was doing its job; the sweep was the thing behaving badly. Reusing the storage state is also more
// faithful, because a real person logs in once and then resizes their window.
let storageState;
if (role) {
const authCtx = await browser.newContext();
const authPage = await authCtx.newPage();
if (!await login(authPage, BASE, role)) {
findings.push({ role: label, width: 0, route: '(login)', kind: 'LOGIN_FAILED' });
await authCtx.close();
continue;
}
storageState = await authCtx.storageState();
await authCtx.close();
}
for (const width of VIEWPORTS) {
const ctx = await browser.newContext({ viewport: { width, height: 900 }, deviceScaleFactor: 1, storageState });
const page = await ctx.newPage();
const consoleErrors = [];
const failedRequests = [];
// ERROR RESPONSES WITH THEIR URL AND STATUS. Chromium logs "the server responded with a status of 403"
// to the console WITHOUT naming the request, which is not something anyone can act on. Correlating the
// console line to the actual response is the difference between a finding and a rumour.
const badResponses = [];
page.on('console', m => { if (m.type() === 'error') consoleErrors.push(m.text().slice(0, 160)); });
// WITH THE REASON. A failed request reported as a bare URL is not actionable: ERR_NETWORK_CHANGED is a
// statement about the tailnet and ERR_ABORTED is usually the sweep's own retry cancelling a navigation,
// while a real refusal or timeout is a statement about the application. Recording only the URL made all
// three look identical.
page.on('response', r => { if (r.status() >= 400) badResponses.push(`${r.status()} ${r.request().method()} ${r.url().slice(0, 110)}`); });
page.on('requestfailed', r => failedRequests.push(`${r.failure()?.errorText || 'unknown'} ${r.url().slice(0, 110)}`));
// ONE ROUTE, MEASURED. Returns findings rather than pushing them, so the caller can decide whether a
// finding is confirmed or was a blip.
const checkRoute = async (route, shoot) => {
const f = [];
consoleErrors.length = 0; failedRequests.length = 0; badResponses.length = 0;
if (!await goWithRetry(page, BASE + route)) return [{ role: label, width, route, kind: 'LOAD_FAILED' }];
await page.waitForLoadState('load').catch(() => {});
// The SPA hydrates and fetches after load. Asserting before that settles reports an EMPTY_SCREEN for
// a screen that is merely still arriving.
await page.waitForTimeout(1200);
// THE PLANTED DEFECT for the positive control: an element wide enough to force horizontal overflow.
// The sweep must catch it, or its clean runs mean nothing. The shell guard gets its own planted
// defect in the probe pass below, because a brand new assertion that has never been seen to fail is
// indistinguishable from one that is silently skipped, and this build has been burned by exactly
// that eight separate times.
if (CONTROL) {
await page.evaluate(() => {
const d = document.createElement('div');
d.style.cssText = 'width:4000px;height:12px;background:red';
d.id = 'sweep-positive-control';
document.body.appendChild(d);
});
await page.waitForTimeout(120);
}
const m = await page.evaluate(() => {
const doc = document.documentElement;
return {
scrollW: doc.scrollWidth, clientW: doc.clientWidth,
emptyish: document.body.innerText.trim().length < 20,
rawError: /Application error|Unhandled|TypeError:|Cannot read propert/i.test(document.body.innerText),
};
});
if (m.scrollW > m.clientW + 1) {
f.push({ role: label, width, route, kind: 'HORIZONTAL_OVERFLOW', detail: `${m.scrollW} > ${m.clientW}` });
}
// THE DOCUMENT ITSELF FAILED TO ARRIVE, so nothing measured on this page means anything.
//
// This surfaced as an EMPTY_SCREEN finding, which is NOT transport class and so was reported as an
// application defect, when the truth was that the HTML never landed. An instrument that blames the
// screen for a page it never received is worse than one that stays quiet: it manufactures findings.
// An inconclusive observation is returned as such and the caller loads it again.
const docFailed = failedRequests.find(r => r.endsWith(BASE + route) || r.endsWith(BASE + route + '/'));
if (docFailed) return [{ role: label, width, route, kind: 'INCONCLUSIVE', detail: docFailed }];
if (consoleErrors.length) {
const allAllowed = badResponses.length > 0 && badResponses.every(r => isAllowedResponse(label, r));
const onlyResourceNoise = consoleErrors.every(c => /Failed to load resource/i.test(c));
if (allAllowed && onlyResourceNoise) {
expected.push({ role: label, width, route, responses: [...badResponses] });
} else {
f.push({ role: label, width, route, kind: 'CONSOLE_ERROR', detail: consoleErrors[0] + (badResponses.length ? ` [${badResponses.join(', ')}]` : '') });
}
}
if (failedRequests.length) f.push({ role: label, width, route, kind: 'FAILED_REQUEST', detail: failedRequests[0] });
if (m.rawError) f.push({ role: label, width, route, kind: 'RAW_ERROR_RENDERED' });
if (m.emptyish) f.push({ role: label, width, route, kind: 'EMPTY_SCREEN' });
if (shoot) {
const file = `${label}_${width}_${route.replace(/[^a-z0-9]/gi, '_') || 'root'}.png`;
await page.screenshot({ path: path.join(OUT, file), fullPage: true });
shots.push({ role: label, width, route, file });
// THE PIXEL REGRESSION HALF. See the diffCapture comment for why this did not exist until now.
const reg = diffCapture(file, label, width, route);
if (reg) f.push(reg);
}
return f;
};
for (const route of ROUTES) {
// Up to three attempts while the DOCUMENT keeps failing to arrive. Only after the third does the
// route become a reported UNREACHABLE, which is a real finding about a page that genuinely will not
// load rather than a guess about a page that merely did not load this time.
let f = await checkRoute(route, true);
for (let a = 0; a < 2 && f.length === 1 && f[0].kind === 'INCONCLUSIVE'; a++) {
await page.waitForTimeout(1500);
// SHOOT AGAIN. An inconclusive first attempt returns before the screenshot, so without this a route
// that recovered on retry silently vanished from the contact sheet, and the sheet is the artifact a
// human actually reviews. The capture count dropped from 54 to 40 before this was caught.
f = await checkRoute(route, true);
}
if (f.length === 1 && f[0].kind === 'INCONCLUSIVE') {
f = [{ ...f[0], kind: 'UNREACHABLE', detail: `the document failed to arrive on three consecutive attempts: ${f[0].detail}` }];
}
// CONFIRMED BY REPEAT, and ONLY for transport class findings.
//
// Pinning QUIC off cut the noise from 78 findings to 37, but ERR_NETWORK_CHANGED over the tailnet
// survived it, and a run whose every finding is a network blip reports the LINK as the application's
// defect. That trains people to skim the output, which is how a real finding gets missed.
//
// So a route whose findings are ALL transport class is loaded again, and a finding is kept only if
// it happens twice. A genuinely broken endpoint fails both times and survives untouched; a blip does
// not. The moment a finding is NOT transport class, nothing is retried and nothing is dropped:
// an overflow, a raw error, or an empty screen is a verdict on the first sighting.
//
// Dropped findings are COUNTED AND WRITTEN OUT, never silently discarded, because a suppression
// nobody can see is indistinguishable from an instrument that measures nothing.
if (f.length && f.every(isTransport)) {
const second = await checkRoute(route, false);
const seen = new Set(second.map(x => x.kind));
for (const x of f) {
if (!seen.has(x.kind)) transient.push({ ...x, note: 'did not reproduce on immediate reload' });
}
f = f.filter(x => seen.has(x.kind));
}
findings.push(...f);
}
await ctx.close();
}
// THE STANDING GUARD, MEASURED RATHER THAN INFERRED.
//
// The first attempt computed a minimum by walking the DOM for unshrinkable elements, and it reported
// "route requires 1440px" on a 1440 viewport, because an element with flex-shrink:0 and width:100% is
// not a fixed minimum, it is merely as wide as its container right now. That measurement would have
// false positived on every route forever, which trains people to ignore the finding, so it was replaced
// rather than tuned.
//
// What is measured now is the same quantity the 732 baseline came from: render the route at a width
// BELOW the supported floor and read scrollWidth. A page cannot compress past its true minimum, so
// scrollWidth at the probe width IS the minimum. This probe ASSERTS NOTHING ABOUT THE PROBE WIDTH, it
// only reads a number: the authenticated matrix still starts at 768 and 320 is still unsupported.
const probeCtx = await browser.newContext({ viewport: { width: PROBE_WIDTH, height: 900 }, deviceScaleFactor: 1, storageState });
const probePage = await probeCtx.newPage();
for (const route of ROUTES) {
if (!await goWithRetry(probePage, BASE + route)) continue;
await probePage.waitForLoadState('load').catch(() => {});
await probePage.waitForTimeout(1200);
if (CONTROL) {
await probePage.evaluate(() => {
const d = document.createElement('div');
d.style.cssText = 'width:4000px;height:12px;background:red';
document.body.appendChild(d);
});
await probePage.waitForTimeout(120);
}
const minWidth = await probePage.evaluate(() => document.documentElement.scrollWidth);
probed.push({ role: label, route, minWidth });
if (minWidth > SHELL_MIN_WIDTH_BASELINE + SHELL_MIN_WIDTH_ALLOWANCE) {
findings.push({
role: label, width: PROBE_WIDTH, route, kind: 'SHELL_MIN_WIDTH_REGRESSION',
detail: `route requires ${minWidth}px, shell baseline is ${SHELL_MIN_WIDTH_BASELINE}px`,
});
}
}
await probeCtx.close();
}
await browser.close();
// The contact sheet: a whole sweep reviewed in one scroll, which is the point.
const sheet = `<!doctype html><meta charset=utf-8><title>visual sweep</title>
<style>body{font:14px system-ui;background:#111;color:#eee;margin:0;padding:24px}
h2{margin:28px 0 8px;font-size:15px;color:#9cf}img{max-width:320px;border:1px solid #333;vertical-align:top;margin:4px}
.f{color:#f88}.ok{color:#8f8}figure{display:inline-block;margin:0 8px 16px;text-align:center}figcaption{font-size:11px;color:#888}</style>
<h1>Visual sweep, ${new Date().toISOString()}</h1>
<p class="${findings.length ? 'f' : 'ok'}">${findings.length} finding(s) across ${shots.length} captures</p>
<pre class="f">${findings.map(f => JSON.stringify(f)).join('\n')}</pre>
${shots.map(s => `<figure><img src="${s.file}" loading="lazy"><figcaption>${s.role} ${s.width} ${s.route}</figcaption></figure>`).join('')}`;
fs.writeFileSync(path.join(OUT, 'contact-sheet.html'), sheet);
fs.writeFileSync(path.join(OUT, 'findings.json'), JSON.stringify(findings, null, 1));
fs.writeFileSync(path.join(OUT, 'min-widths.json'), JSON.stringify(probed, null, 1));
fs.writeFileSync(path.join(OUT, 'transient.json'), JSON.stringify(transient, null, 1));
fs.writeFileSync(path.join(OUT, 'expected.json'), JSON.stringify(expected, null, 1));
console.log(`surface=${SURFACE} viewports=${VIEWPORTS.join(',')} captures=${shots.length} findings=${findings.length}`);
if (probed.length) {
const widest = probed.reduce((a, b) => (b.minWidth > a.minWidth ? b : a));
console.log(` shell guard: widest route requires ${widest.minWidth}px (${widest.route}), baseline ${SHELL_MIN_WIDTH_BASELINE}px + ${SHELL_MIN_WIDTH_ALLOWANCE}px allowance`);
}
if (expected.length) console.log(` ${expected.length} named expected error response(s) allowed, see expected.json`);
// THE PIXEL HALF REPORTS EVEN WHEN IT HAS NOTHING TO SAY, because a silent pixel half is exactly how this
// came to be missing for two Gate C runs while both write ups described it as merely unapproved.
if (pixelReport.unavailable) {
console.log(' PIXEL REGRESSION UNAVAILABLE: pixelmatch or pngjs is not installed, so this run did NOT');
console.log(' perform the diff the protocol requires. Install them before quoting this run as complete.');
findings.push({ role: 'instrument', width: 0, route: '(pixel)', kind: 'PIXEL_HALF_UNAVAILABLE' });
} else {
console.log(` pixel regression: ${pixelReport.compared} compared, ${pixelReport.isNew} without a baseline, ${pixelReport.regressions} regression(s), threshold ${(PIXEL_THRESHOLD * 100).toFixed(3)} percent`);
if (pixelReport.compared === 0 && pixelReport.isNew > 0) {
console.log(` no baselines exist yet at ${BASELINES}. Run with --approve to establish them, which is the`);
console.log(' recorded approval step, and note that a run with zero comparisons proves nothing about pixels.');
}
}
// --approve PROMOTES THIS RUN'S CAPTURES TO BASELINES, and it refuses to do so from a dirty run.
// Approving a run that already has findings would enshrine the defect as the thing future runs are measured
// against, which is worse than having no baseline at all.
if (APPROVE) {
if (findings.length) {
console.log(` APPROVAL REFUSED: ${findings.length} finding(s) in this run. A baseline set may only be`);
console.log(' promoted from a clean run, or the defect becomes the standard everything is compared to.');
} else {
fs.mkdirSync(BASELINES, { recursive: true });
let n = 0;
for (const s of shots) { fs.copyFileSync(path.join(OUT, s.file), path.join(BASELINES, s.file)); n++; }
fs.writeFileSync(path.join(BASELINES, 'APPROVAL.txt'),
`Baselines approved by Joseph, recorded ${new Date().toISOString()}\n` +
`surface=${SURFACE} captures=${n} viewports=${VIEWPORTS.join(',')}\n` +
`roles=${ROLES.map(r => r.email.split('@')[0]).join(',') || 'anonymous'}\n` +
`threshold=${PIXEL_THRESHOLD}\n`);
console.log(` BASELINES ESTABLISHED: ${n} captures written to ${BASELINES}, approval recorded in APPROVAL.txt`);
}
}
if (transient.length) console.log(` ${transient.length} transport finding(s) did not reproduce and were dropped, see transient.json`);
findings.slice(0, 12).forEach(f => console.log(' ' + JSON.stringify(f)));
if (CONTROL) {
// BOTH assertions must have fired. Catching one and missing the other would leave half the instrument
// unproven while the run reported success, which is the same lie in a smaller package.
const missing = ['HORIZONTAL_OVERFLOW', 'SHELL_MIN_WIDTH_REGRESSION'].filter(k => !findings.some(f => f.kind === k));
if (missing.length) {
console.error(`POSITIVE CONTROL FAILED: planted defects went undetected by ${missing.join(' and ')}`);
process.exit(1);
}
console.log('positive control OK: the planted overflow AND the planted min width regression were both DETECTED');
process.exit(0);
}
process.exit(findings.length ? 1 : 0);
})();