Skip to content

Technical documentationInternal

For the team and anyone taking the product over. Not linked from the site and not indexed.

Technical documentation pages

Build, test and CI

This page describes every npm script, the build hooks, the unit and browser test harnesses, the performance and size gates, the licence check and the GitHub Actions workflows that run them.

Commands at a glance

All commands run from the repository root with Node 22.18 or later (.nvmrc, engines in package.json).

Script What it runs
npm run vendor node scripts/vendor.mjs && node scripts/notices.mjs
npm run dev next dev, after predev runs npm run vendor
npm run build next build, after prebuild (npm run vendor && node scripts/prebuild.mjs); postbuild then runs node scripts/postbuild.mjs
npm run vercel-build npm run build. Vercel runs this script in place of build when it exists (added 16 October 2026, dd30084); it changes nothing, since the inner npm run build keeps the prebuild and postbuild hooks
npm start next start
npm run lint eslint with eslint.config.mjs
npm test node --test over src/**/*.test.ts, content/**/*.test.ts, pipeline/**/*.test.ts and scripts/**/*.test.mjs
npm run test:e2e playwright test with playwright.config.ts
npm run test:perf playwright test --config playwright.perf.config.ts
npm run test:b2-acceptance node --test on src/lib/hosts/fit/b2-acceptance.check.ts only (outside the npm test glob), with the spec reporter to stdout and a TAP report in b2-acceptance.tap: the CSO's B2 condition 1(b′), with clause (i) amended after the data were seen (D-0137; SCI-262); it passes
npm run test:perf:worker playwright test --config playwright.perf-worker.config.ts
npm run test:cross-browser playwright test --config playwright.cross-browser.config.ts: the @cross-browser tests on WebKit and Firefox
npm run app-index scripts/app-index.mjs: writes the light client index
npm run accents scripts/accents.mjs: writes src/components/suite/accents.css
npm run tailwind-check scripts/tailwind-check.mjs: Tailwind emission check
npm run codemod-tokens scripts/codemod-tokens.mjs: brand-colour to semantic-token codemod
npm run size-check scripts/size-check.mjs: per-route size budgets, after a build
npm run check-boundary scripts/check-boundary.mjs: nothing reaches outside the project folder
npm run check-licences scripts/licences.mjs: the dependency-licence test
npm run snapshot-staleness scripts/snapshot-staleness.mjs: the Pipeline snapshot expiry alarm, and the Cellosaurus snapshot's age (warn at 120 days, fail at 180)
npm run snapshot-freshness scripts/snapshot-freshness.mjs: the Pipeline snapshot age report

Several scripts pass --disable-warning=MODULE_TYPELESS_PACKAGE_JSON to Node. They import TypeScript modules from content/ or src/, and package.json has no type field, so Node would otherwise warn on each run.

Build hooks

npm runs predev, prebuild and postbuild around dev and build automatically. Running npx next build directly skips them, so use npm run build.

Vendored engines and notices

scripts/vendor.mjs copies the browser builds that must be served as static files (Mol*, RDKit.js with its WebAssembly, 3Dmol.js) from node_modules into public/vendor/<name>@<version>/, with each package's licence text beside the files. The version in the path is the exact pin from package.json; the script refuses a range and deletes copies of other versions, so stale builds are never served. The RDKit npm package ships no licence file, so the copy kept in scripts/licenses/ is used, and the InChI code compiled into its WebAssembly travels as INCHI-LICENSE.txt beside it. The package list is exported so scripts/licences.mjs can check it, and src/lib/vendor.test.ts checks that src/lib/vendor.ts agrees with the pins.

scripts/notices.mjs walks the production dependency closure in node_modules and writes public/third-party-notices.txt with each package's licence text, then the third-party code bundled inside the vendored builds (InChI) and the four self-hosted fonts (SIL Open Font License 1.1), because bundlers strip licence comments from minified code. Which text travels with what is recorded once, in scripts/lib/licence-texts.mjs, which the licence check (scripts/licences.mjs) shares; scripts/licences.test.mjs checks the file's contents (Dependencies and licences). Both outputs are generated and gitignored.

Prebuild and postbuild steps

scripts/prebuild.mjs and scripts/postbuild.mjs call runSteps in scripts/lib/run-steps.mjs. It runs every .mjs file in scripts/prebuild/ or scripts/postbuild/ in name order, each in its own Node process. Files starting with _ are helpers and .test.mjs files are skipped. The first step that exits non-zero stops the run and fails the build. An app adds a step by adding scripts/prebuild/<slug>-<what>.mjs, so nobody edits package.json.

Step What it does
prebuild/assay-analyser-static-methods.mjs Derives each task island's chip labels into content/apps/assay-analyser.static-methods/<island>.ts from the method records, only the keys the island's import closure names
prebuild/assay-analyser-repro-engine.mjs A-4 item 4: transpiles src/lib/fit/analyse.ts and the modules it imports with TypeScript (types removed only) into one self-contained script with a small module table, written with its SHA-256 and each source's SHA-256 to src/apps/assay-analyser/repro/engine.generated.ts for the reproducibility package; --check (run by scripts/assay-repro-engine.test.mjs) fails when the engine changed and the file was not regenerated
prebuild/assay-analyser-limit-figures.mjs Writes Assay's two cap figures, as formatBytes rounds a limit, into content/apps/assay-analyser.limit-figures.ts; --check (run by scripts/assay-limit-figures.test.mjs) fails when they drift
prebuild/bioreactor-simulator-preset-runs.mjs Writes public/data/bioreactor-simulator/preset-runs.json from the model, closure residuals on an absolute 1e-9 grid (REL-09), and prints its SHA-256
prebuild/lab-calculators-static-methods.mjs Derives task-page chip labels into content/apps/lab-calculators.static-methods.ts
prebuild/suite-app-index.mjs Writes content/apps/app-index.generated.ts from the registry
prebuild/suite-walk-values.mjs Writes the J3 walk's quoted values to content/walks/j3.values.ts and the plate walk's to content/walks/plate.values.ts (scripts/lib/plate-walk-values.mjs, from the published MTT fixture) from the fixtures; scripts/walk-values.test.mjs checks both
postbuild/reference-routes.mjs Proves that every public reference page in REFERENCE_ROUTES (content/seo/registry.ts) and its social image are prerendered from /[app]/[task] with no fallback and carry the app's own CSP, then checks each page's HTML: canonical address, the article, one H1, schema and FAQ, related links
postbuild/tasks-routes.mjs Proves from Next's manifests that /[app]/[task], /[app]/app and /[app] coexist, that task pages are prerendered with no fallback, carry the right headers and canonical address, and that no file under src/app/(tasks) reads searchParams

The committed outputs are checked by unit tests (for example content/apps/app-index.test.ts and src/components/tools/bioreactor-simulator/preset-runs.test.ts), so a stale generated file fails npm test even without a build. On Vercel, tasks-routes.mjs and, since 16 October 2026 (4c59707), reference-routes.mjs also look for prerendered HTML under .vercel/output and skip the HTML content checks with a logged reason if they cannot find it (the manifest and header checks still run); CI runs them in full.

Other generators and checks

Script Purpose Where it runs
scripts/app-index.mjs (--check) Light client index from content/apps.ts; refuses product names from anywhere but content/app-names.ts prebuild; content/apps/app-index.test.ts
scripts/accents.mjs (--check) Per-app accent CSS from ACCENTS and APP_IDENTITY in the registry by hand; src/components/suite/tokens.test.ts compares the committed file
scripts/tailwind-check.mjs Compiles src/app/globals.css and asserts the emissions the token layer relies on (semantic variables, dark: and stage: variants, reduced-motion collapses, brand layer) scripts/tailwind-check.test.mjs under npm test
scripts/codemod-tokens.mjs <paths> [--check] Rewrites brand colour utilities to semantic tokens; --check exits 1 if anything is left by hand; tested by scripts/codemod-tokens.test.mjs
scripts/check-boundary.mjs Parses imports, path literals, stylesheet directives and tsconfig.json with TypeScript's parser and fails on anything resolving outside the folder or an undeclared package CI check job; scripts/check-boundary.test.mjs
scripts/removed-sources.mjs Fails on any mention of the success-rate tables removed under D-0009, outside docs/ and .claude/ CI check job; scripts/removed-sources.test.mjs
scripts/forbidden-phrases.mjs Flags positive regulatory-claim phrases in content/ and src/ using the definition in content/apps/regulatory.ts content/apps.test.ts under npm test
scripts/capture-previews.mjs [--only <slug,…>] Writes each app's product-page captures, public/media/<slug>/preview-light.webp and preview-dark.webp: the workspace at the registry's example (appHref(slug, app.example.params)), 1408 × 880, device scale 2, reduced motion, once per theme, after the shell's settled rule, no visible "Loading" text and no toast; then, for two apps, a step through the app's own controls (STEPS, the CPO's follow-up ruling of 17 October 2026): Calc chooses its in-app example "10 mM imatinib mesylate stock in DMSO" from its tools and sections menu, and Assay runs "Fit the curves" and opens the Fit view (the registry example's link carries neither state, and link formats are the CTO's); Chromium's canvas encoder makes the WebP (quality CAPTURE_QUALITY, default 0.8), so no image package is added. Starts next start on CAPTURE_PORT (3290) against the existing production build, or uses CAPTURE_BASE_URL; follows HTTPS_PROXY for the example's live requests. All ten apps are eligible after the CEO’s 18 October request. Plasmid and Bioreactor scroll their actual map/chart into view before capture; no UI is hidden or rewritten. Recapture after integration when owner changes alter these screens. content/apps/posters.test.ts fails until CAPTURED in content/apps/posters.ts matches the files by hand, after npm run build; commit the files
scripts/check-write-once.mjs --base <ref> Fails when a published fixture, manifest entry or versioned data file changed or disappeared relative to the base commit CI check-write-once job; see Data pipeline
scripts/check-committed-growth.mjs --base <ref> [--head <ref>] [--per-merge] The evidence rule (D-0126; the CTO's memo of 24 October 2026, docs/board/memos/2026-10-24-cto-bioreactor-stage.md §8, rules A to C). Compares the head's committed tree (HEAD unless --head; never the working tree) with its merge base against --base and fails, naming each path and the rule, on: under the evidence folders docs/progress, docs/reviews and docs/science/evidence, a zip (Playwright traces are zips) or other archive, a video, a HAR or network dump, anything inside a test-results/ or playwright-report/ folder, a raw .log, or any gzip that is not a machine summary (rule A2); an evidence image over 400 KB, a machine summary (.json, .jsonl, .ndjson, .csv, .tsv) over 1 MB that is not gzipped, or a gzipped one that is over 1 MB compressed, 1 MB or less uncompressed, unreadable or, as .json.gz, not JSON (A1); any file over 5 MB anywhere (A2); more than 150 files or 10 MB of evidence added on the branch (A3); a net copy, a new path whose bytes sit on more paths of the head's tree than of the merge base's (so a move is never a copy), unless the two paths are a file in a pipeline area (pipeline/<area>/…) and its first publication under public/verification/<slug>/<version>/; licence texts (LICENSE*, LICENCE*, COPYING*, NOTICE*, <name>-LICENSE*, <name>-LICENCE*, <name>-OFL.txt, case-sensitive), evidence images and empty files are exempt, copied machine summaries are not (B, see Oracles and verification); any new file under src/**/fixtures/ except a .ts module directly in src/lib/fixtures/, the server-side fixture reader (B2). Three decisions the CTO confirmed on 24 October 2026: a raw log is told from a machine summary by its name, so a .gz is a summary only when named <name>.{json,jsonl,ndjson,csv,tsv}.gz and it decompresses; units are decimal, as size-check's (400 KB is 400,000 bytes, 1 MB 1,000,000); --per-merge stays. Rule B makes no allowance for re-copied publications: a later version names the earlier file by path and SHA-256 (B3), which needs a resolver change not yet made, so whichever app re-publishes next builds B3 first, in its own branch. The per-file rules read every blob the branch adds or changes; the totals count new evidence paths, so a record edited in place is not new evidence. Evidence already on main is not judged: it is pruned forward with evidence.json manifests (the workflow-finish memo, §D7.3). A missing base or merge base is an error. --per-merge counts the evidence each first-parent merge brings against that merged branch, and the branch's own commits together CI check-write-once job when the Fast chain runs, and growth.yml on pushes that change the record folders (--per-merge on claude/merge-queue-* in both); scripts/check-committed-growth.test.mjs builds each case in a temporary git repository

check-boundary.mjs checks the project folder's edge, not module graphs. Unit tests under npm test hold graph rules: src/lib/test-util-imports.test.ts (no shipped module imports a *.test-util.ts, and the annotation display step has one importer); and two walks of a worker's static import closure with TypeScript's resolver, type-only imports skipped: src/components/tools/plasmid-map/worker-closure.test.ts for the analysis worker (REL-269) and src/apps/plasmid-map/construct-admission-worker-imports.test.ts for the construct admission worker (REL-23: it reaches none of contracts.ts, refs.ts, url-state.ts, content/apps/routes.ts, content/apps/maker.ts, src/lib/search/, form/format.ts, data/csv.ts or genbank-write.ts; a second case checks the walk finds refs.ts and the route map from contracts.ts).

scripts/build-search-catalogs.mjs and scripts/build-search-details.mjs rebuild the offline search catalogues from downloaded source files; they are run by hand and are described in Data pipeline.

Unit tests

npm test uses Node's built-in test runner (node:test with node:assert/strict) and Node's built-in TypeScript type stripping. There is no bundler, transpiler or test framework in between, which keeps tests fast and means they load exactly the modules the build uses. The globs are the four in package.json: .test.ts files under src/, content/ and pipeline/, and .test.mjs files under scripts/.

Type stripping imposes conventions on any module a test imports:

  • Relative imports carry their extension (import { x } from "./stats.ts"). tsconfig.json sets allowImportingTsExtensions so the compiler accepts this.
  • No path aliases. @/ and @content/ resolve in Next and TypeScript but not in Node. Modules written to be test-loadable say so in their header ("Plain module (relative imports), so the Node tests load it").
  • No JSX and no React in those modules; there are no .test.tsx files.
  • No syntax that needs code generation: enums, namespaces and parameter properties. eslint.config.mjs makes each an error for .ts, .tsx and .mts files, because one such file once stopped 23 test files loading in CI.

Some tests compare committed generated files with what their generators would write now (the client index, accents CSS, static method labels, preset runs). Others run repository-wide checks: the licence test (scripts/licences.test.mjs), the boundary check, the removed-sources check, the forbidden-phrase scan and the type-program check (scripts/type-program.test.mjs, under "Linting and type checking" below). src/components/tools/drug-pipeline-explorer/coverage.test.ts also fails if any non-test file under src/ mentions the frozen ClinicalTrials.gov fixtures, and src/lib/protein/boundary.test.ts enforces the import boundary between src/lib/protein/ and src/lib/seq/. A few depend on the calendar: src/components/tools/drug-pipeline-explorer/precedent/snapshot-age.test.ts fails when a shipped Pipeline or FDA precedent snapshot is close to expiry (see Data pipeline).

A known-failing check kept running (the CTO's ruling of 24 October 2026, docs/board/memos/2026-10-24-cto-bioreactor-b2-failing-test-in-chain.md). The CSO's acceptance test for the B2 engine's item 3, condition 1(b′), failed at one reading as first worded; the CEO amended its clause (i) after the data were seen (D-0137), and it now passes (SCI-262). Because a failing test in the npm test glob would stop the check job before the build, tsc and the size artifact for every session, it lives in src/lib/hosts/fit/b2-acceptance.check.ts, outside the glob, and runs as its own step (below). Both it and a tripwire inside npm test (batch-fit-oracle.test.ts, "1(b′) tripwire") read the same records from src/lib/hosts/fit/b2-convergence.ts: the tripwire expects no failure, so any failure of the amended condition turns the unit step red; under D-0137 such a failure fails item 3 outright. Merge records state the B2 step's outcome.

Linting and type checking

npm run lint runs ESLint with Next's core-web-vitals and typescript configurations plus three project rules in eslint.config.mjs:

  • the strip-only TypeScript restrictions above;
  • suite/no-brand-colour in src/components/suite/** and src/apps/**, which rejects brand colour utilities and raw hex colours in class strings (exempt: *.marketing.tsx, tokens.ts, chart-palette.ts, tests, and named exceptions in recipes.ts);
  • no-restricted-imports patterns that stop client code importing the full registry (content/apps.ts) or another app's full entry; shared code reads @content/apps/app-index instead. Type imports are allowed.

npx tsc --noEmit needs the route types Next generates, so run it after a build or after npx next typegen. CI runs it after the build as npx tsc --noEmit --tsBuildInfoFile .next/cache/.tsbuildinfo, so it reuses the build info the build's own check just wrote rather than checking the project a second time; it still fails on any type error.

The build's type check and its heap ceiling (the CTO's ruling of 17 October 2026, docs/board/memos/2026-10-17-cto-main-build-and-stage-c-rulings.md; Known issues, MNT-26). In Next 16.3.8 experimental.useTypeScriptCli defaults to true, so next build's "Running TypeScript" step is the tsc CLI run as a child process (tsc --project tsconfig.json --noEmit --tsBuildInfoFile .next/cache/.tsbuildinfo, node_modules/next/dist/lib/typescript/runTypeCheckCli.js) with the parent's environment. It checks everything tsconfig.json includes: product code, every test, e2e/, pipeline/, scripts/ and, through resolveJsonModule, every imported JSON fixture. On 000a4206 that program was 4,380 files, 413,131 lines of TypeScript and 103,252 lines of JSON, and used 2.24 GB of heap at the end of the run (measured by the CTO). GitHub's runner for this private repository gives Node a default heap of about 2 GB, and from db40fe80 (17 October 2026) the build died there with Ineffective mark-compacts near heap limit. The three SEO commits added 1.4 percent to the program's types (1,000,962 to 1,014,974) and were the step that crossed the limit, not a defect in those modules; the program had already grown 6.5 percent in types between 15 October (9fa37e06, 940,217) and the last green head (41e90925), over the 16 and 17 October pushes. The generated route types did not change.

So check.yml, ci.yml and scheduled.yml set NODE_OPTIONS: --max-old-space-size=4096 as workflow-level env:, which reaches the build's tsc child (Next strips the flag only from workers it starts with isolated memory, which the type check does not use) and the standalone check. It is a CI fact, set where CI is defined: not in package.json or .npmrc, so local builds keep Node's default. It is an interim with a recorded ceiling: the type program's end-of-run heap (npx tsc --noEmit --extendedDiagnostics, "Memory used") stays under 3,000,000 K and the flag stays at 4096. Either figure moving is a note to the CTO before the merge, not a bigger number. The repair is the type-surface fix queued under MNT-26: fixture JSON leaves the type program, read through fs and a typed readFixture<T>(path) by tests and by the server-rendered tables that import it today. src/lib/fixtures/read.ts holds that reader (server only, cached per path); Bioreactor's two files, scenarios.json and preset-runs.json, moved to it on 24 October 2026 (Stage H BR1.7), taking 59,513 lines of JSON and 100,337 types out of the program (see MNT-26). Refused by the same ruling: typescript.ignoreBuildErrors, excluding .next/types or any product code from the check, a tsconfig.build.json for the build (measured: it still carries 98,989 lines of JSON and 2.09 GB of heap, because the JSON arrives through product code) and a dependency for setting the variable.

Two guards keep the program from growing meanwhile (the CTO's rule C2 of 24 October 2026, D-0126; scripts/type-program.test.mjs under npm test). tsconfig.json excludes docs/progress and docs/reviews, the evidence folders (42 of the 45 .ts files under docs/ were the captures in docs/reviews/2026-10-14-stage-a-design-check), beside the older docs/science/evidence/2026-10-11-audit. And no non-test file under src/ or content/ may import a JSON file over 100 KB, in any syntax that brings it into the program (import, export … from, import … = require, import(), require(), import("…") types). The eleven import sites of seven files on 24 October 2026 are allowlisted in the test: scenarios.json and preset-runs.json (Bioreactor's two marketing tables), drc-reference.json and synthetic-curves.json (Assay's tables and SEO preset), approved-drugs.json (two SEO presets), the Pipeline precedent products.json and the server's compound catalogue (compounds.<hash>.json, written with <hash> so a regenerated catalogue keeps its entry). The test fails on an entry whose import is gone, so the list only shrinks, and on any new one; the fix for a new one is to read the file with fs on the server, or fetch it.

Browser tests

playwright.config.ts runs the specs under e2e/ (**/*.spec.ts) against next start on a production build, so run npm run build first.

Servers

Unless E2E_BASE_URL is set, Playwright starts two servers:

Server Port Environment Used by
Browser mode E2E_PORT (default 3210) as inherited the desktop, phone and desktop-dark projects
Account mode E2E_PORT + 1 WORKSPACE_ACCESS_MODE=account, WORKSPACE_SITE_URL and SUPABASE_URL set to the server's own address, a placeholder publishable key the desktop-account and phone-account projects

The account server exists because the account backups panel is hidden in the default browser Workspace (D-0032). Its specs (e2e/objects/workspace-backup.spec.ts) serve every account API themselves; no real provider is contacted. With E2E_BASE_URL set, no server is started, and E2E_ACCOUNT_BASE_URL gives the account projects' address. Outside CI an already running server is reused.

Projects and settings

Project Viewport Notes
desktop 1408 × 880 the desktop scale of D-0016
phone 375 × 667, scale 2 isMobile, hasTouch
desktop-dark 1408 × 880 colorScheme: "dark" and project storageState storing suite:theme=dark before the first page (CTO memo of 24 October 2026). Light is the default (D-0035), so the OS preference alone left most cases in Light. e2e/smoke/project-theme.spec.ts checks both the page fixture and contexts created through browser.newContext(). Tests whose subject is fresh default storage opt out explicitly with FRESH_STORAGE from e2e/support/theme.ts; their assertions stay unchanged. Scope the override to the fresh-default scenario, so other tests still inherit the project preference. Light-only pages must stay light
desktop-account, phone-account as above account-mode specs only, on the account server

Every project uses locale en-GB and time zone Europe/London. The test timeout is 45 s and the expect timeout 10 s. Every action has a bound of 20 s (actionTimeout) and every navigation one of 60 s (navigationTimeout), since 17 October 2026 (the CTO's ruling 1(a), B1; REL-214): before them an action with no bound of its own waited until the test's timeout (45 s, or 120 s to 240 s where a spec raises it) and then again on CI's retry, which is how the long failures spent their minutes. A missing target now fails at its action, and the error names the locator. Neither bound is raised to make a test pass: a step that is slow by nature gets an explicit, named wait before its action (expect(...).toBeVisible({ timeout }), at or under the old effective bound, with a comment). Traces are kept on failure and screenshots taken on failure. When CI is set: forbidOnly, one retry, two workers, and an HTML report and a JSON report (test-results/e2e-report.json) beside the list reporter. The JSON report is what the merger records the browser baseline of failing test titles from (CTO memo of 12 October 2026, ruling 1(d)).

Chromium is the executable at PLAYWRIGHT_CHROMIUM (default /opt/pw-browsers/chromium-1194/chrome-linux/chrome) when it exists; otherwise Playwright's managed full Chromium (channel: "chromium"), not the headless shell, because the headless shell does not dispatch focus events in an unfocused window. WebGL runs on SwiftShader (--use-angle=swiftshader) so the Mol* and 3Dmol viewers start headless.

Support helpers (e2e/support/)

File What it provides
test.ts The suite's test: an offline router and the CSP collector installed automatically on every page. Any request to another origin is aborted unless a fixtures entry serves it (glob or regex, with json, body or a repository-relative path). network.blocked lists refused requests; strictOffline: true fails the test on any. A same-origin molecule-description call is also stubbed. The automatic viewerLock fixture serialises the specs that start Mol* (VIEWER_SPECS: protein-explorer/, education/structure, inside-a-cell/tour, objects/objects, objects/send-to, objects/explicit-saves, search/catalogs, smoke/discovery-layout) across workers, projects and runs on one machine with a cross-process lock (a file created exclusively in the temp directory holding the owner's pid, reclaimed when that process is gone; checked with eight processes, 960 acquisitions, never two holders), adding the time waited to the test's timeout; project names, sharding and assertions are unchanged. Mol*'s start-up compiles its shaders inside Viewer.create on the main thread under SwiftShader (3.3 to 3.6 s unloaded; single tasks of about 20 s, up to 30 s, with eight workers on four cores), and two such pages starting together starved page.evaluate, animation frames and an IndexedDB open (REL-30, REL-31, REL-44). A test outside the list that loads molstar.js gets a molstar-unlocked annotation so the list can follow.
csp.ts Records every securitypolicyviolation event from the first script, for assertions that a page ran with no CSP violations
axe.ts The accessibility check: injects the pinned axe-core, waits for animations to settle, and expectNoSeriousViolations records serious or critical findings as a soft failure (expect.soft inside a boxed test.step, so the report names the calling spec's line) while returning lesser ones; the test still fails, but the product assertions after the call still run (the CTO's D-0080 Stage B ruling 1, 14 October 2026)
motion.ts Reduced-motion emulation through the media query and the Settings attribute (reduceMotion); each of the three sources alone, the OS setting, the Settings switch or save-data, with the detected triple asserted (reduceMotionBy, detectedTriggers, expectOnlyTrigger; MNT-240)
theme.ts FRESH_STORAGE, empty storage for tests whose subject is the fresh default, opting out of the dark project's stored preference with test.use({ storageState: FRESH_STORAGE })
layout.ts No horizontal page scroll; document language; the settle waits: settle (fonts and finite Web Animations, leaving out those inside a closed disclosure, REL-206; one whose finished has not come after 30 s fails the call with its name, target and ancestors, REL-233), scrollAtRest (the scroll position holds for six frames), headingFocusSettled (an app's focus move to a heading, then the scroll at rest) and mainThreadQuiet (a second of animation frames with no gap over 200 ms, for a test that times a short-lived state on a page whose startup work holds the main thread, such as the Protein dossier's Mol* viewer in software WebGL, REL-205). The polled waits poll every 50 ms, not on expect.poll's back-off, so a timed walk does not count the wait's own lag (REL-230)
announcements.ts Records every message in the polite live region
molecule.ts, work-capture.ts, workspace-management.ts Shared navigation steps for specific flows
walk.ts A guided walk's target/card geometry and phone bounds (PHONE_BAR_MAX 300 px; controls at least 44 px on at most two rows, three in the changed state). expectFocusClear presses Tab and checks the existing exact no-overlap predicate with expect.configure({ soft: true }).poll inside a boxed step: an obscured focus still fails, with the same matcher, page state and timeout, while later product assertions continue (D-0080; CTO rulings of 14 and 22 October 2026). Only individually recorded assertion errors are exempt; target geometry, bar dimensions, horizontal overflow, scientific values, hand-offs and removal checks remain hard

Recorded API responses live beside the specs (for example e2e/fixtures/protein-explorer/, written by record.mjs against a running app with live network).

The cross-browser set (playwright.cross-browser.config.ts)

The CTO's ruling 1(e) of 17 October 2026 (B7; scope D-0099). A smaller set of tests also runs on Playwright's Linux builds of WebKit and Firefox. The main config is unchanged, so the Chromium matrix and its baseline are unaffected. The cross-browser config follows playwright.perf.config.ts: it spreads the main config (servers, base URL, timeouts, the action and navigation bounds, retries, workers, reporters, the offline fixture router) and replaces only the projects:

Project Device Viewport
webkit devices["Desktop Safari"] 1408 × 880
webkit-phone devices["iPhone 13"] (touch, mobile, scale 3) 375 × 667
firefox devices["Desktop Firefox"] 1408 × 880

Each project sets grep: /@cross-browser/, the main desktop project's testIgnore (the timing gates and the account-mode specs), and launchOptions: {} with channel: undefined, because the main config's top-level use carries Chromium's executable path, the SwiftShader flags and, without the local executable, channel: "chromium". A project's channel: undefined does not clear the top-level value (Playwright skips undefined keys when it merges use), so the config also takes the main config's top-level use without channel and launchOptions: the first run on GitHub (39060339569 on 1e14f81b), where the local executable is absent, failed all 69 tests at launch with Unsupported webkit channel "chromium" (and firefox), while local runs, where the executable exists and the channel is never set, started.

A test joins the set by its tag, test(title, { tag: "@cross-browser" }, …), never by its title, so the baseline's titles do not change. The tagged tests (23 a project, 69 in all; the ruling's aim is 30 or fewer a project):

  • smoke/apps.spec.ts: "workspace <app> renders with its security headers" for each of the ten workspaces (which covers Sequence and Plasmid without touching their specs) and "scripts under /_next/static carry no security policy of their own";
  • molecule-workbench/workspace.spec.ts: a pasted SMILES in the fragment, an old ?smiles= link rewritten, an unreadable SMILES (RDKit's WebAssembly started under the CSP), and the Export menu's CSV, SVG and ZIP downloads (skipped on phone by the test itself);
  • store/transactions.spec.ts: two tabs guarding revisions; sharing/previews.spec.ts: a shared view preserving private inputs; lab-calculators/tasks.spec.ts: the molarity task page;
  • protein-explorer/home.spec.ts: the start; protein-explorer/toggle.spec.ts: the dossier and the Structures tab, which starts Mol*;
  • assay-analyser/workspace.spec.ts: four unreplicated doses fitted (an Assay fit well under 60 s); objects/work-capture.spec.ts: two Calc tasks saved and resumed from the Bench; smoke/suite-theme.spec.ts: fresh preferences painting light before hydration (the fresh case only).

None is in e2e/plasmid-map, e2e/sequence-toolkit or e2e/bioreactor-simulator, and none is an accessibility spec. Two of them, the Protein start and the molarity page, end with the shared axe check, as they do in the matrix; the ruling names both.

.github/workflows/cross-browser.yml runs the set daily and on demand (below). Its failures are read against docs/progress/e2e-baseline-cross-browser.json, which has the Chromium baseline's rules and is read and recorded with scripts/e2e-baseline.mjs --baseline docs/progress/e2e-baseline-cross-browser.json. WebKit on Linux is not Safari: Safari on macOS, iPhone and iPad is not tested automatically.

The first complete run is 39061727220 on 0baa9315 (17 October 2026, on the builder's branch through a temporary push trigger, since workflow_dispatch needs the workflow on the default branch): 69 tests, 66 passed, one skipped by its own test (Molecule's Export menu on phone), none failing both attempts, and two flaky on webkit-phone (the molarity page's answer not updated within 10 s after a typed concentration, and Concentration not found within 10 s after Resume), both appended to the cross-browser baseline's flaky. Jobs took 5.9 to 8.6 minutes; the tests 12.8 worker-minutes. After it the supported-browser statement was published in README.md and on /security (content/apps/security.ts), in the CTO's wording.

Timing gates

The timing gates under e2e/perf/ never run inside npm run test:e2e: every project in the main config ignores **/perf/**, because a timing gate beside another worker measures the machine rather than the page.

  • playwright.perf.config.ts (npm run test:perf) extends the main config with one worker, no retries, the desktop project only, tracing off, output in test-results/performance and a JSON report at test-results/performance-report.json. It runs two-second.spec.ts: with the cache primed and CPU and network throttled, a task page must answer within 2,000 ms of navigation; cold loads are measured but not gated. Bioreactor, protein-align and Assay have separate configs below.
  • playwright.perf-report.config.ts (npm run test:perf:report) runs assay-two-second.spec.ts and assay-workspace-fit.spec.ts, which report without gating (D-0049; CTO ruling 7 of 12 October 2026): CI runs them after test:perf in a step that may fail without failing the job. assay-workspace-fit.spec.ts presses Fit on the 96-well example five times at CPU 4× with the cache primed and measures, inside the page, from the press to the results region's data-results-ready stamp changing; it writes { median, min, max, loadBefore, loadAfter, commit, date } to its annotations and assay-workspace-fit.json, and reports "inconclusive" without failing when the one-minute load exceeds the core count. It also times a 115-plate paste (about 0.27 MiB) read and placed on the start, reported only. Under D-0076 (14 October 2026) both Assay timing cases have a 3,000 ms target, judged on a quiet 4-core reference machine; they report on every CI run and move to playwright.perf.config.ts only when the pre-live pass re-arms them (D-0065 item 4's three-run rule is superseded). src/lib/fit/first-fit.bench.test.ts, in npm test, times analyseCurve on the two-second gate's table, median of 20, with its parts, and fails above 60 ms.
  • playwright.perf-worker.config.ts (npm run test:perf:worker) runs bioreactor-worker.spec.ts (2,000 ms for a first change, 1,000 ms with the worker running) and protein-align.spec.ts (D-0055): EGFR (1,210 residues) against a synthetic 8,264-residue sequence, 9,999,440 cells, three jobs each in a new worker, the worker's own reported time. Protein-align: target 2,000 ms; measured 622.9 ms median, 4-core host, 11 October 2026; not measured on CI. It attaches protein-align-gate.json and fails as "inconclusive" when load before or after exceeds the core count. Both gates run weekly and on demand on separate runners in the scheduled.yml worker-gate matrix (CTO rulings of 10 and 11 October 2026, D-0047); a release needs a quiet-host pass within seven days. Since 24 October 2026 (the CTO's memo of that day, §2(a); REL-06) the Bioreactor spec settles before each set: it polls the one-minute load every 15 s for up to 180 s until it is at or below 0.75 × the cores, records the wait and the readings in its annotations and bioreactor-worker-gate.json, and no longer waits a fixed 30 s after an inconclusive attempt; the decision rule is unchanged (load before at most the cores, median of five adjusted runs) and the test's timeout is 30 minutes.

Size budgets

npm run size-check reads the build's own records (.next/build-manifest.json, .next/diagnostics/route-bundle-stats.json and the per-route client reference manifests) and measures gzip level 9 sizes in KB of 1,000 bytes. readBudgets in scripts/size-check.mjs reads every .json owner file in scripts/size-budgets/, sorted by filename, and collects its budgets rows. The directory is the current inventory: alongside suite.json and app/data-specific owners, shared features have their own owners. On 25 October 2026 it contains 22 owner files, including the shared bench-execution.json, bench-quick-start.json, bench-workflow.json, record-previews.json, recorded-methods.json, reference-routes.json, study-material-details.json, study-report-builder.json, study-report.json and unified-save.json. This inventory does not establish that every lazy export is already budgeted (MNT-214). Each budget has a kind:

Kind Measures
route the route's first-load JavaScript; scope: "total" includes framework files (not the noModule polyfill), the default app scope excludes them
css the CSS the route links
lazy what loading named client modules adds to the route's first load
chunk what one dynamic import downloads, found by a marker string the imported module keeps at runtime, less the route's first load; when the marker is in several chunk lists, the lists the route can reach (below) are the ones
chunk-family every emitted copy of an explicitly budgeted nested import: all matching lists, JS and CSS deduplicated, less only the declared route’s first load; missing or detached markers fail

Which list a chunk budget measures (the CTO's ruling of 17 October 2026, addendum A2 of docs/board/memos/2026-10-17-cto-main-build-and-stage-c-rulings.md; Known issues, REL-271 (filed as REL-207, renumbered)). The same module imported from several places gets one chunk list per import context (the product pages' live previews import the workspaces too). measureChunk follows the closure from the route's first load: every list whose loading chunk is in the set is reached, its list file and chunk files join the set, and this repeats until nothing is added. Among the lists that carry the marker, the reached ones are measured; if more than one distinct list is reached, the largest is measured and the row's detail names it, its loader and every reachable list's size; if none is reached and the lists differ, the row is unmatched as before. Until this fix the check looked one level deep, at lists written in the first load itself, and since the redesign's on-demand shell each workspace is loaded by a chunk that is itself loaded on demand (for six of the seven workspace rows, the app's definition chunk, drybench-entry:<app>; Molecule's loader carries no such marker): 18 rows (every app's workspace chunk, the Assay views, and the Pipeline, Calc and Molecule islands under them) reported "loaded by N different chunk lists". On 000a4206 all 18 now resolve to exactly one reachable list, at level 2 (the workspaces, Pipeline's figures) or level 3 (the views and islands inside a workspace); for example Plasmid's workspace reads 195.1 KB, the fifth of its nine lists (154.1 to 205.0 KB), and Assay's 268.4 KB of 202.7 to 274.8. Six of them read over their ceilings once measured: bioreactor-workspace-chunk 137.3 of 137, molecule-workspace-chunk 155.0 of 149.4, molecule-series-chunk 13.4 of 13.1, protein-workspace-chunk 169.1 of 166.7, assay-view-report 9.5 of 8.7 and assay-view-curve-diagnostics 12.5 of 12.4 KB. No budget, kind or marker was changed with the fix. Against docs/progress/assay-stage-size-base.json, where all six passed, they now read "passed at the base, over at the head" instead of "names nothing at the head", in a step that only reports.

A budget naming a route, module or marker that the build does not contain fails, so a stale budget cannot pass silently. Interim budgets (D-0048) carry a tighter releaseKB beside maxKB; CI checks maxKB, and node scripts/size-check.mjs --release checks the release targets. --json prints the measurements. Most budgets carry a $comment recording how their value was set; raising one is a decision, not a fix. On 14 October 2026 assay-definition-chunk became interim (D-0048): maxKB 36.2, releaseKB 32.4, after the registry entry's new verification sets measured 32.9 KB on 4080ed49; 32.4 returns at the pre-live pass by moving the entry's page content out of the chunk (docs/board/memos/2026-10-14-cto-definition-chunk-and-base.md). On 13 October 2026 the Assay coordinator raised bioreactor-workspace-chunk (interim, D-0048) from 136 to 137 KB, the reason recorded beside it: Assay A6 reads 136.2 KB against main's 135.2, about 0.4 KB from the growth readers and copy Bioreactor's measured-growth review loads and about 0.6 KB from Turbopack splitting a shared chunk (REL-42); size work is deferred to the pre-live pass.

--baseline <file> (the CTO's rule of 10 October 2026 for work done while main has budgets over) compares this build with a --json record of the base commit (compareToBaseline). It exits 1 when a budget that passed at the base fails at the head; when one already over or unmatched at the base grows by more than BASELINE_TOLERANCE_KB (0.5 KB, an estimate of measurement noise) or, having been over, names nothing at the head; when a budget new at the head is not met (new work is born green); when a base budget was removed; or when any maxKB was widened. Budgets that stay over without growing are reported, not failed. The Wave 1 continuation recorded its base as docs/progress/wave1b-size-base.json, and Wave 4 as docs/progress/wave4-size-base.json (base 83d1e09: 23 budgets over, none unmatched), with the 0.5 KB allowance counted across the whole wave. scripts/size-check.test.mjs covers the comparison.

The size gate's temporary form for the Assay stage (CTO addendum of 12 October 2026, PRC-13). main's repaired head carries two budgets over from before the stage (sequence-island-primer-design 47.6 of 40 KB, REL-22, fixed on claude/sequence-wave6-debt; plasmid-construct-admission-worker 30.7 of 20 KB, REL-23). While they stand, CI's check job writes node scripts/size-check.mjs --json to size-measurements.json (uploaded as an artifact for 30 days), runs --baseline docs/progress/assay-stage-size-base.json (a report step with continue-on-error from 14 October 2026, under D-0074 and the CTO's ruling of that day: while it failed it skipped the plain check after it), and runs the plain npm run size-check as a report step with continue-on-error. The base is the raw --json array recorded on 12 October 2026 from the repaired head's build (94 budgets, 0 unmatched, 2 over); it is re-recorded only from main's head by the merger, with the ruling cited in docs/progress/assay-stage.md. It was re-recorded the same day from main at 125bc499 (Assay's A-0) with the new cap in place, except the two debt rows, which are carried verbatim from the stage-original base (47.6 and 30.7 KB) with a carried field citing the CTO's ruling (docs/board/memos/2026-10-12-cto-re-base-carried-debts-and-cap.md); 125bc499 reads primer-design at 47.7, so 0.1 of the allowance is used. The 0.5 KB allowance on the two debts is counted across the whole stage. assay-workspace-chunk carries a Stage A cap of 250.8 KB: 242.4 measured plus 5 KB, plus 3.4 KB for the lost-work fix (D-0068 rule 8), a suite-wide configuration change under D-0066 item 1 measured by the merger on 125bc499 merged with 621072de, which is carried outside the stage's 5 KB (docs/board/memos/2026-10-12-cto-lost-work-size-and-merge-order.md); no further widening this stage, and an Assay merge whose --baseline delta on that chunk exceeds 1.0 KB stops for the CTO's ruling before its pull request (1.0, not 0.5, because Turbopack regrouping alone moves a chunk by up to about 0.6 KB). Step 6 (12 October 2026; ruling 3 of docs/board/memos/2026-10-12-cto-a0-3b-size-and-step-6.md) took the chunk from 248.7 KB on main at e97808b8 (after the lost-work fix) to 204,843 bytes by loading the Plate map panel, the Fit results block and the Report body with next/dynamic (src/apps/assay-analyser/views/); their new chunk budgets, assay-view-fit-results 28.5, assay-view-plate 18.8 and assay-view-report 8.7 KB, are the larger of two readings plus 15 percent, rounded up to 0.1 KB: the chunk alone and in a scratch merge with A-0's head with commit 3b re-applied, which is not pushed (the CTO's ruling docs/board/memos/2026-10-12-cto-step-6-budgets-and-the-3b-gate.md: Fit results from that third reading, 24,781 bytes, since 3b brings the figure core into it). Until 3b lands, a merge adding more than 1.0 KB to assay-view-fit-results stops for the CTO. A view imported from two import() sites gets two chunk lists, which the chunk kind refuses, so each view has one loader that the export intents share. The temporary form expires, the step reverting to the plain check and the base file being deleted, in the merge that makes the plain check pass on main or at the stage's exit, whichever comes first; the pre-live pass runs --release regardless (D-0048).

Stage F F-1b adds three Protein-only chunks, using the CTO-approved exact measured bytes × 1.15 rounded up to 0.1 decimal KB: protein-residue-map-worker (drybench-worker:protein-residue-map), 14,030 gzip bytes measured, 16.2 KB budget; protein-exact-evidence-projection (drybench-island:protein-exact-evidence-projection), 14,343 bytes, 16.5 KB budget; and protein-residue-map-client (drybench-island:protein-residue-map-client), 4,957 bytes, 5.8 KB budget. Existing ceilings stay unchanged, including workspace 166.7 KB, capture 22.2 KB and historical projection 8 KB. The client loads only after source/current readiness; a single 10-second operational deadline includes import, hashing and worker work with cancellation after every await. Shared binding/input/refusal primitives are a dependency leaf; evidence does not import wire-response validation. The recorded-projector loader selects historical-only or full exact validation before import; any exact capability hint, including malformed mixed tuples or late inputs, selects the full reader. Full joined closures remain counted. The quiet-host median target stays 2 seconds; lifecycle, actual worker performance and native image checks are separate from fixture agreement.

The Wave 8 evidence writer (drybench-export:bioreactor-evidence) is budgeted as one chunk-family, bioreactor-evidence-exports-family (4.3 KB; 3.7 measured), because it is imported on intent from three places and Turbopack emits two copies, which a chunk budget rejects (REL-21).

The CTO’s 11 October Stage 0 export ruling adds measured budgets for Kinetics and Thermal CSV families (17.5 and 20.1 KB) and Molecule Series CSV (7.4 KB). Export writers load on intent; qualification helpers stay available to panels and saved-result capture. Existing budgets and strict single-chunk ambiguity failures remain unchanged.

study-report.json also holds study-report-stock-input-projection (2.5 KB, measured 1.9 KB on 17 October 2026): the lazy stock input projection part, keyed on the marker its loader-only entry module carries (stock-input-projection-entry.ts), since the synchronous join's copies reach /study/[id] through reports.ts.

Each task-page island has its own chunk budget named by its marker drybench-task:<slug>/<task>. Wave 1 added Assay's growth-rate-window, michaelis-menten-initial-rates, enzyme-inhibition-ki, ki-from-ic50 and thermal-shift-apparent-tm and Wave 3 Calc's centrifuge-rcf-rpm, percent-solutions, cell-seeding, unit-converter, od600-cell-estimate and antibody-dilution, all at 40 KB with no interim margin (scripts/size-budgets/assay-analyser.json, lab-calculators.json). Task pages without an island (the standard-curve, master-mix and buffer-recipe pages) need no client budget.

Wave 4 put each app's workspace (Main) chunk under a budget, measured at the wave's base plus its new marker (the component's displayName) and budgeted at that plus 5% (CTO Wave 4 memo §2.0), and gave every new on-demand chunk a marker and a budget born green, at the first green measurement plus about 15% under the CTO's limit (§2.3). Measured figures are the build record's of 11 October 2026; no build was run for this page.

Budget (file) Marker maxKB Measured, KB
protein-workspace-chunk (protein-explorer.json) drybench-island:protein-workspace 166.7 163.6
protein-substitution-chunk drybench-island:protein-substitution 6.6 (limit 25, lowered after measuring) 5.7
protein-align-chunk drybench-island:protein-align 9.2 (limit 40) 8.0
protein-align-worker drybench-worker:protein-align 9.2 (limit 15) 8.0
molecule-workspace-chunk (molecule-workbench.json) drybench-island:molecule-workspace 149.4 145.1
molecule-series-chunk drybench-island:molecule-series 13.1 (limit 40; re-based from 12.4 on 13 October 2026, REL-49: the comma rule in parseNumber) 13.0
pipeline-workspace-chunk (drug-pipeline-explorer.json) drybench-island:pipeline-workspace 166.5 162.3
pipeline-coverage-chunk drybench-island:pipeline-coverage 1.2 (limit 10) 1.0
pipeline-figures-chunk drybench-export:pipeline-figures 11.9 (limit 25) 10.3
walk-steps-nearest (suite.json) drybench-steps:nearest@1 3.5 3.0

On the wave's branch, budgets already over at its base grew within the allowance: molecule-definition-chunk 26.8 to 27.1 KB (budget 22.6) and pipeline-definition-chunk 26.5 to 26.8 KB (budget 25). Against main's head after the merge, commit 1af3986 measured molecule-definition-chunk at 18.0 KB of 22.6 and molecule-workspace-chunk at 141.6 of 149.4; protein-definition-chunk stayed under (18.2 of 19.9). No maxKB was raised. walk-chunk (10 KB, 9.3 measured) was not lowered because J3's definition is still imported statically (CTO-3, a follow-up).

Chunk sizes depend on how Turbopack groups shared modules, which is heuristic: the same modules can cost up to about 0.6 KB more when they are regrouped into more files (docs/progress/wave1-assay.md, item 6). A change in one app can therefore move another app's island. After the Wave 3 merge, sequence-island-protein-mw-pi measured 42.3 KB against its 42 KB budget, attributed to Turbopack regrouping shared chunks that hold byte-identical modules rather than to a change in that island (reported by the integration run; not re-measured for this page, since no build was run). See REL-11 in Known issues.

Licence check

npm run check-licences (scripts/licences.mjs) runs five checks and exits 1 on any finding:

  1. The production dependency closure: every licence is an SPDX expression the allow-list accepts (MIT, BSD-2-Clause, BSD-3-Clause, Apache-2.0, ISC, 0BSD, CC0-1.0, Unlicense, plus recorded additions), or has a recorded exception, and a licence text travels with it.
  2. The development closure: no copyleft (GPL, LGPL, AGPL, SSPL, BUSL, EUPL) or missing licence in devDependencies either.
  3. The vendored browser builds: allowed licence and licence text present.
  4. A source scan of src/ and content/ for GPL licence headers and for identifiers of GPL tools whose code must never be ported.
  5. The registry: every source has a sourceMeta record, a source without a reuse licence has a decision or is display-only, every endpoint has a terms page, and every licence page was read within twelve months.

Under npm test the twelve months are measured from each app's updated date, so the result does not depend on the calendar. --release measures from today, for the pre-live pass. The allow-list additions and exceptions are held for the General Counsel (D-0010). A GPL dependency, or a feature built on a source that cannot ship commercially, means a rewrite at the pre-live pass.

GitHub Actions

All jobs run on ubuntu-24.04. Every job except those in oracles.yml and growth.yml sets up Node from .nvmrc with the npm cache and installs with npm ci; growth.yml sets up Node from .nvmrc and installs nothing. SCARF_ANALYTICS: "false" stops a transitive dependency's install telemetry. check.yml, ci.yml and scheduled.yml, the three that run npm run build, also set NODE_OPTIONS: --max-old-space-size=4096 at workflow level: the recorded heap ceiling under "Linting and type checking" (MNT-26).

check.yml: the fast chain, called by ci.yml and fast.yml

A reusable workflow (on: workflow_call) holding the two fast-chain jobs, so that D-0066's temporary size step expires in one place (the CTO's CI cost rulings of 12 October 2026, ruling 1). The github context inside it is the caller's, so write-once compares with a pull request's base, else the push's previous head, else main on a branch's first push; the growth guard compares the whole branch with its merge base with main. check has timeout-minutes: 30 (from 20 on 24 October 2026: the job measured 16 min 6 s, 19 min 53 s and 19 min 58 s on one diff that day, runs 40131993781, 40133856876 and 40135689134, the last cancelled at the 20-minute cap with every step green; the CTO's memo docs/board/memos/2026-10-24-cto-fast-chain-cap.md rules 30 for 50 percent headroom over the slowest runner; a cancelled run is never cited as green) and check-write-once timeout-minutes: 20.

Job Steps in order
check-write-once fetch the base commit as above, then node scripts/check-write-once.mjs --base FETCH_HEAD; then, unless the run was cancelled (so also when write-once fails), the evidence rule's growth guard (D-0126, rule C1): deepen the depth-1 checkout with commits and trees only, git fetch --no-tags --filter=blob:none --unshallow origin +refs/heads/main:refs/remotes/origin/main (main's whole history was about 6 MB of commits and trees on 24 October 2026; the head's blobs are already checked out), and run node scripts/check-committed-growth.mjs --base origin/main, with --per-merge on a claude/merge-queue-* branch. It comes after write-once because the filtered fetch makes the clone partial, and write-once's git show of the base's files would then fetch each blob separately. On main the merge base is the head, so it compares nothing
check npm run lint; npm run check-boundary; node scripts/removed-sources.mjs; npm test; npm run check-licences; node scripts/snapshot-freshness.mjs; npm run build; npx tsc --noEmit --tsBuildInfoFile .next/cache/.tsbuildinfo (reuses the build's type-check information); while the Assay stage's temporary size gate stands: size-check.mjs --json into size-measurements.json (artifact, 30 days), size-check.mjs --baseline docs/progress/assay-stage-size-base.json as a report (continue-on-error), then npm run size-check as a report (continue-on-error); then, on every push not cancelled, the step "B2 acceptance" (id: b2, npm run test:b2-acceptance, continue-on-error), a step writing its outcome to the job summary ("B2 acceptance 1(b′): PASSING ((i) amended after the data were seen, D-0137; SCI-262)", or "FAILING: item 3 fails outright under D-0137; tell the CSO (SCI-262)"), and b2-acceptance.tap uploaded as the b2-acceptance artifact (30 days); npm run snapshot-staleness -- --warn

snapshot-freshness fails the check job once the Pipeline snapshots have expired; snapshot-staleness --warn only prints a warning annotation.

fast.yml: every push to a claude/** branch and to main

The fast chain on every push (D-0069; the CFO's memo and the CTO's CI cost rulings of 12 October 2026): check.yml and nothing else, about 22 runner-minutes (measured 24 October 2026; about 10 when set); a newer push to the same branch cancels the run in progress (concurrency: fast-<ref>); pushes that change only docs/board/, docs/progress/, docs/reviews/ or docs/design/ run nothing. Its first job, tested ("Already run on this tree?"), finds the push's gated head (the CTO's ruling 1(d) of 17 October 2026, B6): it checks out with fetch-depth: 50 and filter: blob:none (the CTO's rule C3 of 24 October 2026: commits and trees only, so no blob of the fifty commits' history is downloaded, only the head's for the working tree; the filter input exists in actions/checkout from v4.1.0 and is passed to the same git fetch as --depth, checked against the v4 README, changelog and source on 24 October 2026; git diff --quiet compares blob ids and fetches no content), walks git rev-list --first-parent --max-count=30 HEAD, and accepts the first commit c for which git diff --quiet c HEAD -- . ':!docs/board' ':!docs/progress' ':!docs/reviews' ':!docs/design' holds and gh api lists a successful Fast chain run on c. When it finds one, the chain is skipped and the step summary reads "<ref> at <head>: the fast chain ran green in run <id> on the gated head <c>; the commits after it change only docs/board, docs/progress, docs/reviews or docs/design", which is the push's record; otherwise the summary says no gated head was found and the chain runs. Until 17 October the job looked up the pushed SHA alone, so a fast-forward of a gated head with a record commit on top found nothing and re-ran the whole chain, 12 to 14 minutes each time (Fast chain runs 39032089254, 39037954568, 39046722311 and 39047973743 that day). So D-0092's "checks on the exact head" reads, in practice, as the gated head with only record-path commits after it, which is how sessions already merged; the CTO asked the CEO to record that. The four folders are one list: scripts/workflows.test.mjs (under npm test) fails when the diff's exclusions and paths-ignore differ, when any workflow names the retired merger role, when the tested checkout loses its blob filter, when the write-once job stops running the growth guard after write-once against origin/main, and when growth.yml's paths differ from paths-ignore (below). A pull request starts no run: the checks shown on it are the push runs on its head commit. It expires at the Assay stage's exit; its ceiling is 8,000 runner-minutes a month, read by the CFO from the account's usage report at each stage checkpoint, above which the trigger becomes workflow_dispatch until the CTO and the CFO review.

Size provenance when the chain is reused (25 October 2026). A successful tested job may skip all of check.yml, so that wrapper run has no size-measurements artifact of its own. Its Reused size artifact provenance step uses scripts/find-green-fast.mjs --size-source to inspect at most thirty first-parent commits with the same four-folder diff predicate and at most thirty successful runs' artifact lists. It names the measured SHA, source run and non-expired artifact id in the summary. Reused wrapper runs with no artifact are passed over within those bounds. This is metadata discovery: it does not download, validate, copy or relabel a measurement, and it does not establish that report-only size checks passed. The consumer must download and validate the named JSON before comparing. Missing or expired metadata is reported explicitly; an API error is a failed advisory provenance step, never an empty measurement. Use the comparison build below when no valid source artifact is available.

Comparison worktrees. Turbopack refuses a node_modules symlink pointing outside the comparison project root. Record the exact base SHA, create a detached worktree at it, and perform a real npm ci there before the ordinary build; keep its generated output separate from the builder's tree. For example, from a checked-out repository:

git worktree add --detach ../drybench-size-base <recorded-base-SHA>
cd ../drybench-size-base
npm ci --no-audit --no-fund
NODE_OPTIONS=--max-old-space-size=4096 npm run build
node scripts/size-check.mjs --json > /tmp/drybench-base-size.json

The last command returns 1 when existing budgets are over; preserve and inspect its JSON rather than calling that a passing ceiling check. A physical dependency copy is usable only with the identical lockfile and no external symlinks; the reproducible default is npm ci. Do not switch a builder's checkout to the base, bypass vendor validation or disable Next checks. The issue-repair coordinator used this method successfully for base 13fc5714d on 25 October 2026; the resulting measurement is a local base build, not an Actions artifact.

growth.yml: every push that changes the record folders

The evidence rule's guard on the pushes fast.yml ignores (D-0126; Known issues, PRC-29; the CTO's ruling after the guard's rollout run of 24 October 2026). Evidence arrives in record-only pushes, which run no Fast chain, and a fast-forward of a gated head reuses its run, so the guard in check-write-once alone would miss it. On a push to claude/** or main whose changes include docs/board/, docs/progress/, docs/reviews/ or docs/design/ (its paths, the same list as fast.yml's paths-ignore, checked by scripts/workflows.test.mjs), one job, timeout-minutes: 10, checks out, sets up Node from .nvmrc with no install, deepens the checkout with commits and trees only (git fetch --no-tags --filter=blob:none --unshallow origin +refs/heads/main:refs/remotes/origin/main) and runs node scripts/check-committed-growth.mjs --base <base>, with --per-merge on a claude/merge-queue-* branch and on main (the CTO's addendum of 24 October 2026: each merged branch was capped on its own head, and a range with no merges counts the same either way; every per-file rule and rule B still judge the whole range). On a branch the base is origin/main, so the whole branch is judged against its merge base; on main it is the push's previous head (github.event.before), so the run judges what the push landed. A red run on main is recorded as failing on main's head (D-0074) and the evidence is pruned forward with an evidence.json manifest; history is never rewritten. A push that changes code and records runs both workflows. Runs for the same ref cancel each other; permissions are read-only.

ci.yml: nightly on main (03:47 UTC, D-0135); hand dispatch is the exception

The full matrix (D-0069): by schedule every night at 03:47 UTC on the default branch (nightly since D-0135, 24 October 2026, which superseded D-0098's weekly cadence and D-0101's once-a-day merge queue). Under D-0135 a visible change lands on main when its owner has a green Fast chain on the exact head and has run the browser projects for what it touches; the matrix then runs on main after the fact, and the first session to see its result compares failing titles with docs/progress/e2e-baseline.json and records new failures (D-0074) against the merge that caused them. workflow_dispatch remains for an exceptional run and keeps D-0101's gate: its first job, Daily matrix slot, refuses a dispatch in under a minute when the ref is not a claude/merge-queue-YYYY-MM-DD branch, when the bounded equivalent-head lookup finds no green Fast chain run, or when another dispatched run that the slot did not refuse was created in the last 20 hours. override: true with a non-empty instruction (the CEO's words) passes the slot and is written to the step summary. When the slot finds a green Fast chain on the dispatched commit or a proven equivalent first-parent head, the checks job is skipped. The nightly scheduled run takes no slot. Runs for the same ref queue rather than cancel, so a refused dispatch never stops a running matrix (the CTO's ruling of 18 October 2026). Permissions are read-only. A push to main that changes only files under docs/board/, docs/progress/ or docs/reviews/ runs nothing (paths-ignore, CTO memo of 12 October 2026, ruling 2); docs/technical/ is not ignored, because npm test checks it, and pull requests ignore nothing. There is no trigger on claude/** pushes (refused in the same ruling) and no separate fast workflow (that waits on the CFO).

Records-only dispatch heads (25 October 2026, CTO memo 2026-10-25-cto-codex-issue-ci-repairs.md). The slot checks out fifty commits with filter: blob:none and uses scripts/find-green-fast.mjs to examine at most thirty first-parent ancestors. A successful run from the workflow-specific fast.yml endpoint is reusable only when its SHA is that ancestor and its diff from the dispatched SHA is empty outside exactly docs/board, docs/progress, docs/reviews and docs/design. Failed, cancelled, current and absent runs are not evidence. The summary names the dispatched SHA, gated SHA and real run id. An API/history error or lack of evidence refuses a hand dispatch, including an override; a scheduled run without reusable evidence runs its own checks. Existing ref/20-hour override rules are unchanged. The helper's temporary-Git-history tests cover code, workflow, publication and technical-doc changes, unrelated branches, failed runs, lookup errors and the history bound.

This retains Fast's existing record-folder reuse predicate; PRC-20 (executable files under ignored record folders) remains open. It neither declares all test inputs identical nor certifies two independently green product branches as a checked merge. Under D-0092/D-0135/D-0140, a candidate integrates origin/main, runs its affected projects and lands only on that integrated head's green Fast record. No scheduled matrix or older run certifies a changed product tree.

Job Steps in order
checks check.yml (above)
e2e (12 shards of 60 min; REL-214, D-0098; the 25 October REL-540 integration ruling preserves this cap) npm run build; npx playwright install --with-deps chromium; npm run test:e2e -- --shard=N/12; on every run, pass or fail, upload playwright-report and test-results/e2e-report.json as playwright-report-N for three days, overwriting the same name on a re-run (the test-results tree repeats the traces the HTML report holds)
perf (15 min) npm run build; install Chromium; npm run test:perf (the two-second gate); npm run test:perf:report (Assay, continue-on-error); always upload the report for seven days (D-0065 item 4's three-run rule reads it)

The browser baseline for the Assay stage (D-0065 item 2). main carries browser failures from before the stage, so the shards are read against docs/progress/e2e-baseline.json: the failing test titles (file, line, title, project, with an owner stage and a known-issues.md id per entry) recorded by the merger from the CI run on the repaired head (run 38234867635 on e7af614, 12 October 2026: 35 entries, REL-25 to REL-27; 37 after the CTO's ruling of the same day added two desktop-dark cases from run 38256981079). Each entry also carries run, ruling, cause, symptom (the first error line, null until read from the report) and passedIn; a top-level flaky list holds every retry pass with its runs, and rules states the reading: a retry pass is never a pass; a title baselined on one project under a cause that is not viewport-, touch- or colour-scheme-specific joins another project mechanically when it fails both attempts there in a run whose candidate touches neither the spec nor the app; an entry leaves after expected in two consecutive full runs with its spec untouched. A candidate passes when every test outside the baseline passes after CI's one retry; a failure under e2e/assay-analyser/**, or in a spec the candidate's diff touches, fails the candidate whatever the baseline says. The baseline only shrinks (a test that passes leaves it); growing it is a CTO ruling with the cause recorded. It is read by the merger from each shard's e2e-report.json (uploaded on every run since 12 October), once a week from the scheduled run on main (D-0098) and from any run the merger dispatches on a candidate (D-0069); a failure outside the baseline on main is fixed or reverted the same day. scripts/e2e-baseline.mjs reads the twelve reports (below). It expires when Stage 0's remainder clears it or at the pre-live pass, whichever is first.

Reading and recording the baseline (scripts/e2e-baseline.mjs; the CTO's ruling 1(b) of 17 October 2026, B3). The script reads every e2e-report.json under a folder, in the layout gh run download writes (<dir>/playwright-report-N/test-results/e2e-report.json), and names tests from the repository root (e2e/…; Playwright's own paths are relative to testDir). Entries are matched by file, test title and project; the line is informational (REL-262), so a spec edit that moves a test does not detach its entry. Both modes print each shard's tests, outcomes, worker-minutes (the sum of every attempt's duration), the minutes spent on results that did not pass cleanly, and the report's wall time.

  • node scripts/e2e-baseline.mjs --compare <dir> [--list <file>] [--touched <spec>]… reads the run against docs/progress/e2e-baseline.json (--baseline names another file): failures inside and outside the baseline, baselined titles whose symptom changed (numbers ignored, so a timeout's milliseconds are not a change), flaky results, baselined entries that ended expected, and entries the run did not contain. It exits 1 when the run is incomplete, when a test outside failures failed both attempts, or when a test is flaky under e2e/assay-analyser/ or in a spec named by --touched (the passes rule).
  • node scripts/e2e-baseline.mjs --record <dir> --run <id> [--list <file>] [--issues <file>] [--head <sha>] [--date YYYY-MM-DD] [--dry-run] writes the baseline from a complete run, as the ruling's step 4 sets out: head (from the reports' own commit metadata; --head must agree), run, recordedOn; the run appended to runs; every unexpected result in failures as {file, line, title, project, runs, issue, owner, symptom}, symptom being the first error line of the first failing attempt; every flaky result appended to flaky; passedIn set on an earlier entry that ended expected, and cleared on one that failed both attempts again; rules kept byte for byte, with matches added once. An entry leaves only under shrinks, by hand. A failure's issue and owner come from the baseline's own entries for that file and title (the grows rule), else from --issues, a JSON array of {file, title, issue, owner, project?}; a failure with neither writes nothing and exits 1, naming each title that needs a Known issues row at the next free number. Recording the same run twice gives the same file.

The baseline as re-recorded on 17 October 2026 (B3): from ci.yml run 39076929838 on claude/ea-reliability at 5dc69c2f (main's ed293ef8 merged, with the B1 bounds), complete against the list (3,079 tests: 2,921 expected, 143 skipped, 3 flaky, 12 failing both attempts). Its failures are the 12: plasmid-map/cloning-walks.spec.ts:92 and :131 on all three projects and assembly.spec.ts:129 on phone (REL-27, Stage C), assay-analyser/planned-analysis.spec.ts:30 on all three (REL-457, fixed in the expectation after the run), marketing/suite-pages.spec.ts:112 on phone (REL-312, a named wait added after it) and objects/work-draft-protein.spec.ts:92 on phone (REL-310, open). The three flaky results are appended to flaky (REL-310, REL-312 and REL-272). Its head is the builder's branch, not main's: the session merging to main re-runs the record step on main's own run, as below, and entries fixed since get passedIn there.

"Complete" is the ruling's step 2: every shard 1 to 12 wrote its report (a killed shard writes none), the reports name one commit, and with --list the union of their tests equals the list. The procedure for the session that merges to main (D-0092), which re-runs the record step on main's own run:

gh workflow run ci.yml --ref main                    # note the run id and its headSha
gh run download <id> -p 'playwright-report-*' -D runs/<id>   # within the three-day retention
git checkout <headSha> && npx playwright test --list --reporter=json > runs/<id>/list.json
node scripts/e2e-baseline.mjs --compare runs/<id> --list runs/<id>/list.json
node scripts/e2e-baseline.mjs --record runs/<id> --run <id> --list runs/<id>/list.json [--issues issues.json]

The list.json is written outside the reports' folder layout or in its root, which the reader ignores unless it is named e2e-report.json. scripts/e2e-baseline.test.mjs covers the reader, the completeness check, the comparison, the record and its idempotence on reports built in the test (not committed: a .json under a fixtures/ folder is write-once protected).

cross-browser.yml: daily at 04:23 UTC on main, and on demand

The cross-browser set above (B7). One job per project (webkit, webkit-phone, firefox), fail-fast: false, 45 minutes each: npm ci, npm run build (with the workflow-level NODE_OPTIONS: --max-old-space-size=4096 of the other building workflows, MNT-26), npx playwright install --with-deps webkit firefox, npm run test:cross-browser -- --project=<project>, then scripts/e2e-baseline.mjs --compare against the cross-browser baseline into the step summary, and on every run the HTML report and test-results/e2e-report.json as cross-browser-report-<project> for seven days. It reports and never gates: the test and compare steps carry continue-on-error, and no other workflow or merge waits on it. About 60 runner-minutes a day (the CTO's estimate). Runs for the same ref cancel each other.

scheduled.yml: Mondays at 05:17 UTC, and on demand

Job What it does
worker-gate matrix gate: [bioreactor-worker, protein-align], one runner each, 45 minutes; build, install Chromium, a settle step (the one-minute load polled every 15 s for up to 180 s until at or below 0.75 × the cores, the wait written to the step summary), npm run test:perf:worker -- e2e/perf/${{ matrix.gate }}.spec.ts; keeps each separately named worker report for 30 days. A manual dispatch with only: worker-gate runs this job alone (every other job carries if: github.event_name != 'workflow_dispatch' || inputs.only != 'worker-gate'; the CTO's memo of 24 October 2026, §2(b))
verify one job per external check, fail-fast: false, each writing a heartbeat line to the step summary; see Data pipeline for the list
staleness npm run snapshot-staleness, which fails when a Pipeline snapshot is within 14 days of expiry or the Cellosaurus cell line snapshot was retrieved 180 or more days ago (a warning annotation from 120)
cellosaurus-release python3 -I pipeline/cellosaurus/fetch-release.py --check-newer: reads the first 4 KB of the current Cellosaurus release file and fails when a release newer than the served snapshot has been out for 30 days or more (a warning annotation before that); standard-library Python, the runner's own python3 (E10, the CTO's ruling 4(c))
capture-watchdog fetches data/exclusivity shallowly, reads its log.ndjson and runs pipeline/patent-exclusivity/capture.py --watchdog, which fails when a source's last successful capture is older than its allowance or the capture has never run (Stage I-0, D-0077)

Scheduled workflows run from the default branch only.

snapshots.yml: the 1st of each month at 05:17 UTC, and on demand

Regenerates the Pipeline example snapshots and the FDA precedent snapshots, builds a new dated FDA precedent examples fixture for them (examples-<retrievedAt date>.json; the step checks the spreadsheet's SHA-256 against cder-nme.json and fails if the dated file already exists), runs the write-once check against origin/main and then npm test, and opens a pull request from snapshots/refresh with peter-evans/create-pull-request. Its second job, cellosaurus, rebuilds the cell line snapshot from the current Cellosaurus release (and its write-once fixture for a new release), runs the cell line tests without letting them stop it, and opens a pull request from snapshots/cellosaurus whose body says whether they passed. It has write permission for contents and pull requests, and needs the repository setting that lets GitHub Actions create pull requests. Nothing merges itself. Details are in Data pipeline.

capture-patent.yml: daily at 10:23 UTC, and on demand

The Stage I-0 capture of the FDA Orange Book data files, the Purple Book download and the Paragraph IV list (D-0077; Data pipeline). One job, timeout-minutes: 15, concurrency: capture-patent without cancellation, permissions: contents: write, no npm ci and no secret beyond GITHUB_TOKEN: it fetches or starts the orphan branch data/exclusivity in a worktree, runs pipeline/patent-exclusivity/capture.py, writes vercel.json with {"ignoreCommand": "exit 0"} into the data branch so the Vercel project does not start a deployment for a capture commit (16 October 2026, dd30084; the file is the same every day, so it is committed once), commits and pushes what was captured to that branch only, writes a heartbeat to the step summary, and fails if any source failed. It never touches main; the merger rule stands. The script's exit code reaches later steps through a step output read into an environment variable, never interpolated into a command.

oracles.yml: on demand, and Mondays at 06:43 UTC

The oracle environment (CTO memo of 12 October 2026, ruling 3; Oracles and verification). Nothing in it gates ci.yml or a merge. Permissions are read-only except the image job's packages: write; the registry login uses GITHUB_TOKEN and the workflow uses no other secret.

Job When What it does
image (90 min) every run Tags the image ghcr.io/<owner>/<repo>/oracle-env:<16 hex> by a hash of pipeline/oracle-env/Dockerfile, its .dockerignore, the two scripts it installs and the three Assay requirements files; pulls it if published, otherwise builds and pushes it; writes the image with its digest (<tag>@sha256:…) to the job summary; prints those four record files (R-packages.csv, python-packages.txt, environment.json, image.txt) in the job log, because the artifact storage host is not reachable from every builder's container, and uploads them as oracle-env-record for 30 days
run (60 min) dispatched with script Checks that script is an existing .py or .R file under pipeline/ and version is x.y.z; records the runner's CPU model and whether it has AVX-512 (the oracles' last digits depend on it; Known issues, REL-33); runs run-oracle <script> in the image (pinned to x86-64-v3 and checked first; a refusal, exit 3, is reported as "refused: CPU class" and writes nothing) with the checkout mounted, --network none, as the runner's user, with ORACLE_ENV_IMAGE set; uploads every file the script wrote or changed as oracle-fixtures-<version> for 30 days. A builder copies them in, publishes with the app's publish.py <version> and commits; the workflow commits nothing
drift (90 min, not gating) the schedule, or dispatched with script empty Records the runner's CPU as run does; reruns each script pipeline/oracle-env/oracle_scripts.py lists (those the three Assay manifests name, in dependency order; for a lineage with a newer version, only the newest version's script; Node scripts are skipped and named in the summary) in the image, then compare_fixtures.py: JSON compared as data with every recordedOn key removed, and $.about.environment.cpu and $.about.environment.image (when both sides are digests) ignored by full path and reported in one summary line (CTO ruling of 12 October 2026 on the drift keys) (each drifted file lists every differing path, the first eight with both values), a manifest's SHA-256 of a file that differs only in recordedOn forgiven, any other file byte for byte, an uncommitted output counted as drift; a script run-oracle refused (exit 3) is reported as "refused: CPU class", never as drift, and also fails the job. Fails on any drift or failed script; uploads the regenerated files as oracle-drift

Inputs are passed to the shell through environment variables, never interpolated into a command. Like scheduled.yml, the schedule runs from the default branch only. Until the R fixtures are re-recorded in the image, the drift job is expected to fail on their recorded R 4.3.3 and on the three 0.4.0 fixtures' recorded Python 3.11.17 (see Known issues, SCI-35).

Release heads

The release criterion (the CTO's ruling 1(c) of 17 October 2026, B4, docs/board/memos/2026-10-17-cto-enterprise-actions.md; scope D-0099):

A release head is a main SHA named by an annotated tag release-YYYY-MM-DD[.n]. It qualifies when, on that SHA or its gated head (1(d)):

  • the Fast chain is green: write-once, lint, boundary, removed sources, unit, licences, snapshot freshness, build and types;
  • a dispatched ci.yml run is complete, by the test count, and every failing title is fixed or listed in the baseline for that project with an issue and an owner;
  • the cross-browser run is complete with every failure recorded, once B7 lands;
  • snapshot-staleness passes without --warn;
  • every size row over maxKB has a known-issues row.

Evidence goes in docs/progress/releases/<tag>/: size-measurements.json, the merged e2e-report.json.gz, the compare output, and record.md, which names the SHA, the run and artifact ids, the sha256 of each file, and the CTO's confirmation. The tag message names that path. Where the token allows, a GitHub Release carries the same files.

The tag is cut by the session that publishes an app version or status in the registry. Until the CEO decides A3, the criterion defines what may be called a release, not what may deploy. The gated head is the one fast.yml's tested job names (above): the newest first-parent commit with a green Fast chain whose tree differs from the SHA only under the record folders. "Complete, by the test count" and "listed in the baseline" are what node scripts/e2e-baseline.mjs --compare <dir> --list <file> checks, and its output is the compare output kept as evidence. The folder layout is described in docs/progress/releases/README.md. No release tag has been cut yet.

Before finishing a change

Run these locally, in this order, before handing over a change:

npm run lint
npm test
npm run build
npx tsc --noEmit
npm run test:e2e   # for anything a user sees

npm run size-check and npm run test:perf also need the build and are worth running when a change affects bundle size or page load. Deployment and environment variables are covered in Deployment and environment; the overall structure is in Architecture.

Public reference workflows (16 October 2026)

The postbuild reference-routes step asserts that every article in REFERENCE_ROUTES and its OG image are prerendered and checks canonical/schema/FAQ/CSP contracts. Registry tests cover metadata, scientific article lengths, unique routes and graph reachability. snapshot-staleness includes both selected trial snapshots. See Public reference workflows for the shared route, state, source and export contracts.

Frozen F-2 requirements exception (D-0142)

The CEO approved exactly the two inherited16-byte requirements copies in the readable F-2 pipeline and verification0.10.0, preserving frozen publication bytes. check-committed-growth.mjs pins their two paths, regular mode100644, content SHA-256, complete four-path content set, original two-path base and frozen0.10.0manifest SHA-256. This explicit historical exception to D-0126B does not exempt requirements files generally; another app/version, changed bytes/manifest, symlink, missing original pair or fifth copy remains refused. The eight new positive/adverse tests run alongside every existing growth test. Write-once and all size/evidence bounds remain unchanged.