From 729363d1051657b4811b5305f2c0cb63df3a4a92 Mon Sep 17 00:00:00 2001 From: gHashTag Date: Tue, 11 Aug 2026 01:18:05 +0700 Subject: [PATCH 1/3] ci(website): run the three checks that existed and never ran MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit anomaly-register A48: this project does not fail by missing things. Its site audit, its claim-guard and its five-label framework all caught the same overclaims, and every one failed at the step AFTER the finding -- the audit was written, then deleted, and the numbers stayed six months. So the three checks run on every PR touching apps/website: typecheck:ratchet errors may fall, never rise. 184 today. `npm run build` once claimed to typecheck, never ran in the deploy path, and 231 errors accrued unseen -- among them seven React branches that were statically false, so four panels rendered nothing in production. check:api a field the UI formats that no server emits, and any fallback not tagged with sample(). check:render real Chrome over CDP, every panel on all nine canvas layers, fails on the first uncaught error. vite build was green through all of the above: it proves the bundler succeeded, not that the page renders. Two changes to render-check for CI: CHROME_PATH is consulted first so it can override rather than only fill a gap, and Linux paths are added. A missing browser now skips with exit 0 instead of failing -- a missing browser is a fact about the environment, not a defect in the change, and a check that fails for a reason it already knows about gets muted within a week. Три проверки, которые существовали и не запускались, подключены к CI на каждый PR, трогающий сайт. Co-Authored-By: Claude Opus 5 --- .github/workflows/website-checks.yml | 69 +++++++++++++++++++++++++++ apps/website/scripts/render-check.mjs | 12 +++-- 2 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/website-checks.yml diff --git a/.github/workflows/website-checks.yml b/.github/workflows/website-checks.yml new file mode 100644 index 0000000000..56c9758607 --- /dev/null +++ b/.github/workflows/website-checks.yml @@ -0,0 +1,69 @@ +# The three checks that exist and never run. +# +# anomaly-register A48: this project does not fail by missing things. Its site +# audit, its claim-guard and its five-label framework all caught the same +# overclaims -- and every one of them failed at the step AFTER the finding. The +# audit was written, then deleted, and the numbers stayed for six months. +# +# So these run on every PR that touches the site. Each was written the day its +# failure was found and each has been run by hand since, which is another way +# of saying each has been forgotten at least once. +# +# typecheck:ratchet errors may fall, never rise (A31: `npm run build` claimed +# to typecheck, never ran in the deploy path, and 231 +# errors accrued unseen -- among them seven React branches +# that were statically false, so four panels rendered +# nothing in production) +# check:api a field the UI formats that no server emits, and any +# fallback not tagged with sample() (A36/A37: novelty:0.342 +# rendered as an FPGA measurement on a public route) +# check:render real Chrome over CDP, every panel on all nine canvas +# layers, fails on the first uncaught error (A39: vite +# build was green through all of the above -- it proves +# the bundler succeeded, not that the page renders) +name: Website checks + +on: + pull_request: + paths: ['apps/website/**', '.github/workflows/website-checks.yml'] + push: + branches: [main] + paths: ['apps/website/**'] + workflow_dispatch: + +jobs: + checks: + runs-on: ubuntu-latest + defaults: + run: + working-directory: apps/website + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '22' # render-check uses the WebSocket client built into 22 + cache: npm + cache-dependency-path: apps/website/package-lock.json + + - run: npm ci + + # Not a gate on the count, a gate on the direction. 184 today. + - name: typecheck ratchet + run: npm run typecheck:ratchet + + # Compares names only -- it cannot see units, which is why A34's five + # mappable fields stay unwired rather than relabelled as microseconds. + - name: API contract + run: npm run check:api + + - name: Build + run: npx vite build + + # Skips itself if no Chrome is found rather than failing: a missing + # browser is an environment fact, not a defect in the change. + - uses: browser-actions/setup-chrome@v1 + id: chrome + - name: Render check + env: + CHROME_PATH: ${{ steps.chrome.outputs.chrome-path }} + run: npm run check:render -- --no-build diff --git a/apps/website/scripts/render-check.mjs b/apps/website/scripts/render-check.mjs index 98a1442a7e..d46900ba4b 100644 --- a/apps/website/scripts/render-check.mjs +++ b/apps/website/scripts/render-check.mjs @@ -77,15 +77,21 @@ const SETTLE_MS = 250; // per click: React commit + a frame, not a data fet // Chrome, wherever this Mac keeps it. No download, no bundled browser: if the // developer has no Chrome, that is a fact to state, not to fix with 100 MB. const CHROMES = [ + // First, so it can override rather than only fill a gap -- CI sets it, and + // "set CHROME_PATH to force it" has to mean force. + process.env.CHROME_PATH, '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/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', '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/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary', '/Applications/Chromium.app/Contents/MacOS/Chromium', - process.env.CHROME_PATH, + '/usr/bin/google-chrome', '/usr/bin/chromium-browser', '/usr/bin/chromium', ].filter(Boolean); const CHROME = CHROMES.find(p => existsSync(p)); if (!CHROME) { - console.error(' no Chrome found. Set CHROME_PATH, or install Chrome — this check drives the one you already have.'); - process.exit(1); + // Skip, do not fail. A missing browser is a fact about the environment, not + // a defect in the change being made -- and a check that fails for a reason + // it already knows about gets muted within a week. + console.log(' no Chrome found — skipping the render check. Set CHROME_PATH to force it.'); + process.exit(0); } if (!process.argv.includes('--no-build')) { From 6bf699222307fcb00d611a39e2149a60519fa2c6 Mon Sep 17 00:00:00 2001 From: gHashTag Date: Tue, 11 Aug 2026 01:28:54 +0700 Subject: [PATCH 2/3] fix(website): the render check was reporting its own transition lock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I committed the CI workflow in 729363d10 while this check was failing, and found out by running it afterwards rather than before. Recorded because that is the failure the workflow exists to prevent. Three compounding bugs, all in how the check reached a layer: 1. It pressed each digit up to four times if the DOM did not change. Layer 1 is 'petals', which is where /canvas already starts, so the press is a no-op and it pressed three more times -- each re-entering switchLayer, which holds a `transitioning` flag across a 200ms fade and returns early while set. Hammering left the flag stuck and every LATER layer silently refused to switch. Nine empty layers, twice in a row, from a page that was fine. 2. Walking 1..9 in sequence chains the switches, so one missed press shifts everything after it -- a run reported the tools panels on layer 8 rather than 7. Now Escape returns to petals before each layer, making every switch a single hop from a known state. 3. Even then, runs alternated between 4 panels and 0 on an unchanged build. Zero panels is the correct answer for most layers and a symptom on the one that has them, and a single attempt cannot tell those apart. Each layer that yields nothing now redoes the whole hop once before believing it. Four consecutive runs now find the same four panels on layer 7. The cost is about 50s per run, since every empty layer pays for its retry. No change is also what "already on this layer" looks like, and that is not a failure -- the check says so and moves on rather than retrying into the lock. Проверка рендеринга сообщала о собственном замке переходов, а не о странице. Три ошибки: повтор нажатий залипал на замке, последовательный обход сдвигал слои, одиночная попытка не отличала пустой слой от неуспевшего. Co-Authored-By: Claude Opus 5 --- apps/website/scripts/render-check.mjs | 74 ++++++++++++++++++++++----- 1 file changed, 60 insertions(+), 14 deletions(-) diff --git a/apps/website/scripts/render-check.mjs b/apps/website/scripts/render-check.mjs index d46900ba4b..27573ba462 100644 --- a/apps/website/scripts/render-check.mjs +++ b/apps/website/scripts/render-check.mjs @@ -69,7 +69,7 @@ import { tmpdir } from 'node:os'; const ROOT = new URL('..', import.meta.url).pathname; const DIST = join(ROOT, 'dist'); -const ROUTE = '#/canvas'; +const ROUTE = "#/canvas"; const HEADED = process.argv.includes('--head'); const MAX_PANELS = 80; // a click that never expands anything must not loop forever const SETTLE_MS = 250; // per click: React commit + a frame, not a data fetch @@ -258,6 +258,22 @@ const LAYERS = 9; // mount, so every digit after that gets TYPED INTO THE BOX instead of reaching // the window handler. The symptom is indistinguishable from nine empty layers, // which is exactly what the first run of this loop reported. +// Escape returns to 'petals' (TrinityCanvas line 684). Going home before each +// layer makes every switch a single hop from a known state. +// +// Walking 1..9 in sequence chains them, and one missed press shifts everything +// after it: a run reported the tools panels on layer 8 instead of 7, and the +// next run found nothing at all. A check whose results depend on nine +// consecutive successes is a check that reports the transition lock, not the +// page. +const escape = async () => { + await evaluate('document.activeElement && document.activeElement.blur(), 1'); + for (const type of ['keyDown', 'keyUp']) + await call('Input.dispatchKeyEvent', { type, key: 'Escape', code: 'Escape', + windowsVirtualKeyCode: 27, nativeVirtualKeyCode: 27 }); + await wait(700); +}; + const key = async (text) => { await evaluate('document.activeElement && document.activeElement.blur(), 1'); for (const type of ['keyDown', 'keyUp']) @@ -296,28 +312,58 @@ for (let layer = 1; layer <= LAYERS; layer++) { // panels on one run and 0 on the next. A timing-flaky check is muted within // a week, so this presses, watches the DOM for a change, and presses again // if nothing moved -- and says so if it never does. + await escape(); const before = await evaluate('document.body.innerText.length'); + await key(String(layer)); let changed = false; - for (let attempt = 0; attempt < 4 && !changed; attempt++) { + for (let t = 0; t < 14 && !changed; t++) { + await wait(150); + changed = (await evaluate('document.body.innerText.length')) !== before; + } + // ONE retry, and only after the transition lock has had time to clear. + // + // The first version pressed up to four times. Layer 1 is 'petals', which is + // where /canvas already starts, so the press is a no-op and it pressed + // again three more times -- each call re-entering switchLayer, which holds a + // `transitioning` flag across a 200ms fade and returns early while it is + // set. Hammering it left the flag stuck and every LATER layer silently + // refused to switch: nine empty layers, reported twice in a row, from a + // page that was fine. + // + // No change is also what "already on this layer" looks like, and that is not + // a failure. Say so and move on rather than retrying into the lock. + if (!changed) { + await wait(700); await key(String(layer)); - for (let t = 0; t < 12 && !changed; t++) { - await wait(150); - changed = (await evaluate('document.body.innerText.length')) !== before; - } + await wait(900); + changed = (await evaluate('document.body.innerText.length')) !== before; + if (!changed) console.log(` layer ${layer}: no DOM change (already there, or the switch is broken)`); } - if (!changed) console.log(` layer ${layer}: no DOM change after 4 presses`); await wait(600); // the first data tick of whatever mounted const arriving = drain(); if (arriving.length) report(`on switching to layer ${layer}`, arriving); + // Probe, and if a layer yields nothing, do the whole hop again before + // believing it. Zero panels is the answer for most layers and a symptom on + // the one that has them, and the check cannot tell those apart from a single + // attempt -- runs alternated between 4 panels and 0 on an unchanged build. const countBefore = expanded.length; - for (let i = 0; i < MAX_PANELS; i++) { - const label = await evaluate(PROBE); - if (label === null) break; - expanded.push(`${layer}:${label}`); - await wait(SETTLE_MS); - const found = drain(); - if (found.length) report(`after expanding "${label}" on layer ${layer}`, found); + for (let attempt = 0; attempt < 2; attempt++) { + if (attempt) { + await escape(); + await key(String(layer)); + await wait(1400); + drain(); + } + for (let i = 0; i < MAX_PANELS; i++) { + const label = await evaluate(PROBE); + if (label === null) break; + expanded.push(`${layer}:${label}`); + await wait(SETTLE_MS); + const found = drain(); + if (found.length) report(`after expanding "${label}" on layer ${layer}`, found); + } + if (expanded.length > countBefore) break; } console.log(` layer ${layer}: ${expanded.length - countBefore} panel(s)`); } From 9ffff64d338c4aeac56344bb44c2e0e32bbf139c Mon Sep 17 00:00:00 2001 From: gHashTag Date: Tue, 11 Aug 2026 01:36:40 +0700 Subject: [PATCH 3/3] fix(ci): the render check could not start Chrome on the runner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The workflow ran on PR #653 -- which is the point, since I committed it without checking -- and failed at the render step: Error: Chrome exited (null) before listening CI runs Chrome as root in a container with no user namespaces, so the sandbox cannot start and Chrome dies before printing a debugging port. The message reads like a broken check rather than a missing kernel feature, which is why it is worth a comment rather than just a flag. --no-sandbox and --disable-dev-shm-usage, on Linux only. Dropping the sandbox on a developer's own machine is a real cost for no benefit, and this check runs there far more often than it runs here. Local run still finds the same four panels. Проверка рендеринга не могла запустить Chrome на раннере: в контейнере без user namespaces песочница не стартует. Флаги добавлены только для Linux. Co-Authored-By: Claude Opus 5 --- apps/website/scripts/render-check.mjs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/website/scripts/render-check.mjs b/apps/website/scripts/render-check.mjs index 27573ba462..44b2fd5dbc 100644 --- a/apps/website/scripts/render-check.mjs +++ b/apps/website/scripts/render-check.mjs @@ -135,6 +135,12 @@ const chrome = spawn(CHROME, [ '--no-first-run', '--no-default-browser-check', '--disable-extensions', '--disable-background-networking', '--disable-sync', '--mute-audio', '--window-size=1440,900', + // CI runs Chrome as root in a container with no user namespaces, where the + // sandbox cannot start and Chrome dies before it prints a debugging port -- + // "Chrome exited (null) before listening", which reads like a broken check + // rather than a missing kernel feature. Only on Linux: dropping the sandbox + // on a developer's own machine is a real cost for no benefit. + ...(process.platform === 'linux' ? ['--no-sandbox', '--disable-dev-shm-usage'] : []), // three.js is on this page. Without a software GL fallback every WebGL // context fails to create and the check reports the headless environment as // a rendering bug.