Skip to content

refactor(ui): define one design-token owner for static and workspace views - #238

Merged
iperev merged 15 commits into
mainfrom
chain/v2-design-tokens
Oct 25, 2026
Merged

iperev merged 15 commits into
mainfrom
chain/v2-design-tokens

Conversation

@iperev

@iperev iperev commented Oct 24, 2026

Copy link
Copy Markdown
Contributor

refactor(ui): define one design-token owner for static and workspace views

Part of the UI-DESIGN-01 chain. Base branch: chain/rp-npm-absence-race (#237), which sits on #236, #235, #234, #233, #232, #231 and #230. This PR is one commit on that branch's head. Merge after #237. The chain merges after the 0.17.0 release. That release's change record, release/change-record.v2.json (0.16.0 to 0.17.0), stays unchanged here; these non-breaking rendering changes belong to the next release's change record.

Why

The static views (styled by internal/kernel/browserdoc) and the requirement-browser-server workspace (styled by internal/command/requirementbrowser/assets/workspace.css) wrote their colors, sizes and breakpoints as literals: 317 literal colors and sizes in 267 declarations (18 of them custom-property declarations), plus five media and matchMedia lengths and five icon presentation attributes. The UI-DESIGN-01 row asks for one shipped token set used by both style owners and for a test that refuses a literal color or size outside it. This PR builds that mechanism with today's values. In the pinned engines it changes no computed value of any property the views had before and no pixel; the views now also carry the token custom properties; the visual result is the job of a later change in the chain.

What changes

One token owner. internal/kernel/browserdoc/tokens.css is embedded with go:embed; browserdoc.TokenStylesheet() returns its exact bytes. It holds only custom-property declarations in :root at the top level, in @media (prefers-color-scheme: dark) and in @media (forced-colors: active). Each token holds the value that its style owner used before.

  • Static views: the single inline <style> holds the token stylesheet followed by the browserdoc stylesheet, whose declarations now read the tokens. The dark color-scheme rules keep their selectors and properties and read separate dark tokens; folding them into a switchable palette is left to the redesign. The static CSP is unchanged.
  • Workspace: the page links /assets/tokens.css before /assets/workspace.css. The new exact route serves the token bytes for GET and HEAD with the same headers as workspace.css (including cache-control: no-store) and answers other methods with 405. The workspace CSP (style-src 'self') is unchanged.
  • The workspace palette (light and dark) moved verbatim from workspace.css into the token stylesheet. The system colors of the forced-colors rules became forced-colors tokens.
  • The icon script no longer writes width, height, stroke-width, stroke-linecap and stroke-linejoin on each icon. .workspace-icon, whose size rule already overrode the width and height attributes, now sets the stroke width from a token and the round caps and joins.
  • No Go package enters the browser-check closure (go list -deps of the test server and the verifier is unchanged and equals the internal/ selectors of proofkit.browser-check), so proofkit/witness-plan.json is unchanged.

The literal grammar test (internal/command/requirementbrowser/design_token_*_test.go). It reads the token stylesheet, the static style (the <style> content without the token prefix), workspace.css, every assets/*.js and the rendered HTML of the five static view fixtures and of the workspace. It tokenizes CSS with a CSS Syntax Level 3 tokenizer and analyzes declaration values only, so #cafe in a selector is not a color. REQ-PROOFKIT-QUALITY-028 states the definitions: a literal color includes contrast-color() and a vendor-prefixed keyword such as -webkit-link (only -webkit-box and -apple-system, which the owners use as a display value and a font family, are admitted), a literal size includes a unitless number in the font, line, tab, outline, stroke, SVG geometry (cx, cy, r, rx, ry, x, y), zoom, scale, perspective and transform properties, and a vendor-prefixed property is classified as the property without its prefix, so -webkit-transform counts as transform. It refuses:

  1. a literal color or size or an attr() in either style owner, including var() fallbacks and calc/min/max/clamp arguments, and any var() other than var(--name);
  2. a var() naming no token, no custom property of its owner and none of --graph-card-width, --graph-card-height, --graph-edge-stroke; an owner custom property that shadows a token; a dark declaration of anything other than a palette token, whose base value holds a literal color; a dark or forced-colors declaration of a base token with a value of another category (a literal color, a literal size or neither; a value that holds both counts as a color); a forced-colors token used outside forced-colors rules; a token that no owner, matchMedia query or rendered media attribute uses;
  3. an owner custom property holding a literal (--navigation-width: 0 stays legal);
  4. token-stylesheet content other than the three :root contexts, an empty, important or repeated token, a token referring to a custom property or an attribute, a breakpoint token that is not one dimension, a system color outside forced-colors, a forced-colors token that is not one system color or is also declared outside forced-colors;
  5. @import, @font-face, url( and image-set( in the three stylesheets;
  6. an @media, @container, matchMedia or rendered media-attribute length that is not textually a --breakpoint-* token's value, and a matchMedia argument that is not one string literal;
  7. a listed spelling of a script style write (.style, a bracketed or object style key, attributeStyleMap, styleMap, cssText, insertRule, deleteRule, replaceSync, cssRules, styleSheets, .sheet, adoptedStyleSheets, CSSStyleSheet, .animate(, KeyframeEffect, a style attribute through setAttribute, toggleAttribute, createAttribute, setAttributeNS or createAttributeNS with any namespace, innerHTML, outerHTML, insertAdjacentHTML, setHTML, setHTMLUnsafe, parseHTML, parseHTMLUnsafe, createContextualFragment, DOMParser, srcdoc, document.write, document.writeln, or a style or link element; attribute and element names in any letter case) other than the eight geometry writes of workspace-graph.js, and a listed non-geometry SVG presentation attribute, written as an object key or through setAttribute or setAttributeNS, other than fill="none", stroke="currentColor" and the arrowhead marker reference (CSS cannot carry that reference, because rule 5 refuses url(); an SVG width or height is admitted only inside the icon shape table or as the graph's 100%; an admitted value counts only when it is one whole string literal, so "currentColor" && "red" is refused;
  8. a style= or presentational attribute in rendered HTML (the HTML presentational hints, such as bgcolor, border, color, height, size and width, and the SVG presentation attributes of rule 7 other than its admitted values), a static view without exactly one <style> and a workspace with one. The scan ends a comment where the HTML parser does (at -->, at --!>, and at once after <!--> or <!--->), and it continues after the content of script, style, textarea and title where the HTML parser ends it as text; after an svg or math start tag, and always for xmp, iframe, noembed, noframes and noscript, it also scans that content as markup on its own, so a comment or quoted value that the content opens does not hide later markup.

127 mutants each name the violation text they must produce, so each pins the clause that writes it; every clause, listed style-write spelling, SVG presentation attribute, HTML presentational attribute, size property, color function and size keyword has one. A subtest requires the listed exceptions to stay admitted: a percentage in each of the 32 layout properties, and fr, also in a vendor-prefixed layout property such as -webkit-transform; unitless numbers outside the size properties; the two admitted vendor keywords; a breakpoint length in a media attribute; markup inside a comment that follows an empty <!----> comment; and fill="none" and stroke="currentColor" in a script or in rendered HTML, and SVG geometry other than width and height in either; a script may write width or height only in the icon shape table or as the graph's 100%, and rendered HTML carries neither on any element, so a static icon draws its shapes with path. Two more tests pin the consumers: every static view inlines the token file's exact bytes before the static style, the workspace links exactly tokens.css then workspace.css, the served asset equals the file, and browserdoc emits one <style> that opens with it.

Pinned values re-captured, each from test output after its cause was isolated:

  • scripts/proof-lookup-structure.test.mjs (REQ-PROOFKIT-QUALITY-004): a second declared stream delta for the three HTML rows. The content of the only <style> must have the pinned SHA-256; replacing it with the declared previous stylesheet and then inverting the existing card-group delta must reproduce the frozen stream. Its mutants, a moved style element, a second style element and a changed byte outside the style, each break conservation. The frozen corpus is untouched.
  • relation_parity_test.go: the eight full-output digests are re-captured with a comment. With the previous style content put back, all eight previous digests recur, so the style content is the only change.
  • proofkit/cli-contract.v2.json: the native source digest of internal/command/requirementbrowser (commands requirement-browser-server and view), whose non-test Go files changed; the generated projections follow. The public CLI ABI hash and the requirement-browser-server|input|union compact v2 wire observation change only through that digest: substituting back the previous digest and its three derived contract hashes reproduces both previous values exactly. Under the contract-identity rule in docs/proofkit-contract-map.md, no contractId changes, because no wire shape or specified meaning changes.
  • workspace_test.go: four route-table rows for assets/tokens.css (GET, HEAD, POST 405, trailing slash 404).

Visual controls. The text color token now lives in the token stylesheet, so the one-unit text color control mutates a copied tokens.css, the routed unchanged control serves unchanged copies of both stylesheets, and the other four controls still mutate workspace.css. Test titles and the expected test sets are unchanged. tests/visual/references.json, environment.json and fonts.conf are byte-unchanged; the diff has no PNG.

Requirements and docs.

  • REQ-PROOFKIT-QUALITY-028 states the token owner, its two consumers and route, the grammar's definitions and eight rules, and the controls' stylesheets. It gains four non-claims:
    • the font-face check identifies a face through the family name that the recorded font configuration resolves, so it does not detect a configuration that redirects a named Liberation face (the digests and the owner's review cover that);
    • the recorded font-configuration hash binds tests/visual/fonts.conf, not the system and user configuration it includes, and the engine fixture checks only FONTCONFIG_FILE;
    • the grammar test's limits, including renderings of inputs other than the fixtures, which are not scanned for style or presentational attributes;
    • the script checks are lexical and match listed spellings line by line, so a matchMedia call, style write or SVG presentation attribute reached through a computed, concatenated or escaped name (for example one that reaches animate or styleMap), through reflection such as Reflect.set, through an alias or a destructured reference or through a non-literal key such as a Map entry, a write through a property, method or attribute that the test does not list, such as the disabled property of a stylesheet link, and a listed name or call spelled other than in the form it matches (with whitespace, a line break or a comment between its tokens, as an optional call, with a further argument, a prefixed qualified name or a comma in its namespace argument, or as a shorthand object property), are not claimed to be detected.
  • The binding's non-claims are string-equal to the source. Three new scenarios bind the owner, route and grammar tests, with coverage-inventory entries. The overview line describes the token owner and the grammar's refusals.
  • CONTRIBUTING.md: the re-admission rule now names the pixel-identity non-claim of REQ-PROOFKIT-QUALITY-028 and of the visual proof record, Apple M1 and Arm Neoverse N2, and its four copies (the requirement, the binding, visual_record.go and the writer). It says that the gate's record identifies an N2 runner by implementer and part, while the Apple model comes from the host (sysctl -n machdep.cpu.brand_string).

Compatibility

Judged against release 0.16.0. Not breaking.

  • Shipped output bytes change. The HTML of every static rendering (--format html of the source, spec-tree, proof and coverage views, and the static browser-server views): the <style> content is the token stylesheet followed by the rewritten stylesheet. The workspace HTML gains one <link>. The browser server answers GET /assets/tokens.css, which returned 404 before. The served workspace.css and workspace-icons.js change. The htmlByteLength value in browser-server plans grows. In the pinned engines, no computed value of a property that existed before and no rendered pixel changes (Proof); the token custom properties are added.
  • Public contracts do not change. CLI JSON input and output wire shapes, their specified meanings, contractIds and exit codes are unchanged. In the shipped proofkit/cli-contract.v2.json only the native source digest changes. The shipped requirement-bindings.json, the supply-chain requirements.v2.json and overview.md change as described above.
  • tests/, scripts/ and CONTRIBUTING.md are not in the npm package.

Proof

Proof: A local npm run check sequence on darwin/arm64 passed (exit 0) on tree 80fedbb7ba82 with Node 26.11.1, npm 12.2.0, CPython 3.15.0 and Go 1.27.3, with no Go test verdict replayed from the test cache: 561/561 runtime browser cases in Chromium, Firefox and WebKit with zero retries. One recorded deviation: the host firewall blocks Go-built binaries from the network, so go:vulncheck ran against a vulnerability-database snapshot fetched with curl (modified 2026-10-21T00:27:29Z): no vulnerabilities. An earlier full gate on this tree failed two runtime browser tests on 30 s timeouts while the host load average exceeded 180 on 8 CPUs; this rerun at moderate load passed.

Narrow evidence on darwin/arm64 (Apple M1) with Node 26.11.1, npm 12.2.0 and Go 1.27.3; the pinned browser-test SDK's engines are Chromium 153.0.8010.12, Firefox 155.0 and WebKit 26.6. The substitution, computed-style and pinned-value evidence below was produced on an earlier candidate of this change whose CSS, scripts, Go product source and references are byte-identical to this tree's; later candidates changed only the grammar test files, REQ-PROOFKIT-QUALITY-028 with its binding and the overview line, so that evidence carries over. The grammar evidence below was rerun on this tree.

  • Substitution identity (a script kept with the evidence, not a test, because it relates this change to its predecessor and becomes false by design when the redesign changes token values). Replacing every var(--token) that the change introduced with the token's value reproduces the previous static stylesheet byte for byte (4092 bytes) and the previous workspace.css after whitespace normalization. Its listed exceptions are moves, each checked: the workspace palette moved verbatim; the forced-colors system colors moved into tokens (substitution restores them, so no textual exception is needed); the icon presentation attributes moved into .workspace-icon. Falsifiers: a token value changed (--static-space-8: 9px) or a var() swapped in workspace.css each fail it.
  • Computed-style identity in Chromium, Firefox and WebKit, before and after the change, over every element and its ::before, ::after, ::marker and ::placeholder, in light and dark. The states were: default at 320 and 1440, hover on one control per view, Table mode, all details open, root font size 200% at 320 and 1440, forced-colors emulation, keyboard focus on each element reached by Tab, and each workspace panel open at 320 with its ::backdrop and a pixel capture. That makes 178 states per engine and 534 engine states. Two runs of each side: before against after is identical in both pairs, and each side against its repeat is identical. The only difference is structural: the workspace head's added <link>. Custom properties are compared by the names that existed before; the token custom properties are additions. The comparison is exact, so it also covers font weights 400 and 500, and 600 to 900, which render identically with the recorded Liberation faces. Falsifier: a server built with --text: #202523 and --static-space-8: 9px differs in every static state and every light workspace state of every engine and in no dark workspace state, whose text token is separate.
  • Reference gate unchanged. tests/visual/references.json is byte-unchanged. On a candidate tree that differs from this one only in the two grammar test files, REQ-PROOFKIT-QUALITY-028 and its binding, so that every rendered byte is the same (MATERIALIZED_TREE equal to that candidate's tree), npm run browser:visual -- --require-reference-environment passed in a fresh container of mcr.microsoft.com/playwright@sha256:a0f44989… (linux/arm64, --user 1001 --ipc=host --init): 190 of 190 tests (169 references, 18 controls, 3 font-face checks), record passed and clean, a candidate inventory byte-equal to references.json, and all 169 capture PNGs byte-identical to the admitted gallery of test(browser): gate rendered views on owner-admitted reference digests #230. An earlier candidate with the same rendering inputs passed the same gate in two more fresh containers, one of them the CONTRIBUTING recipe.
  • Grammar falsifiers.
    • All 127 mutants are refused with their violation text, and the shipped sources and the listed exceptions produce no violation. On an earlier candidate, each of six strengthenings (prefix-free property classification, contrast-color(), whole-literal SVG values, parser-equal comment ends, markup reading inside svg and math, and the reworded unused-token text), applied one at a time to the previous grammar, kept the shipped sources and the listed exceptions admitted and made its own mutants refused.
    • Run against the grammar of an earlier candidate of this change, 73 of the 127 mutants fail. 49 of them get no violation of their rule at all: attr(), vendor-prefixed colors, size properties and perspective, contrast-color(), both category directions in the dark context, an attribute in a token, the four media-attribute checks, SVG presentation attributes in any case, namespace or quote, an SVG attribute value that is an expression, presentational attributes in rendered HTML, the four comment and SVG-title scanner cases, markup nested in a raw-text element inside svg, markup after each of the five elements parsed as text, and 22 script spellings such as styleMap, deleteRule, .animate(, setAttribute("Style"), setAttributeNS("", "style") and createElement("STYLE"). The others lack a listed violation: the 14 added size properties, the 12 added SVG presentation attributes, the forced-colors category directions, or a reworded violation text.
    • On an earlier candidate of this change, disabling each of 53 clauses in turn, removing each of the 28 style-write spellings, the 68 SVG presentation attributes, the 34 HTML presentational attributes and the 22 size properties in turn, and dropping either admitted vendor keyword each turned the grammar tests red, and so did disabling each of five admissions; this tree keeps those clauses, lists and mutants.
    • On this tree, reverting each of the 17 rules that this change adds or pins last turns its named mutant red: whole-literal SVG values, resuming after a whole literal, geometry-name shadowing, the unitless number in an owner custom property, a dark-context system color, a bad URL, the template-literal style attribute, quoted and unquoted media-attribute unescaping, contrast-color(), prefix-free property classification (which also turns the listed exceptions red), the three comment ends, the bounded markup reading of raw-text elements after svg, the inherited foreign flag, and the unused-token text. Dropping the markup reading for the elements parsed as text turns red the noscript mutant and the five mutants that expect its unterminated-comment refusal. Removing each of the five elements parsed as text, perspective, each of the 13 color functions or each of the 12 size keywords turns its mutant red, and removing each of the 32 layout properties turns the listed exceptions red. So do disabling the dimension, percentage, number, unused-token and unscannable-HTML clauses and removing each of the 22 other size properties or either admitted vendor keyword, which an earlier candidate with the same clauses showed.
    • Before the change, this tree's grammar reports 299 rule-1, 18 rule-3, 5 rule-6 and 5 rule-7 violations.
  • Pinned-value differentials as described above. A binding non-claim edited by one word fails TestSelfHostingRequirementBindingRecordsPreserveSourceAuthority.
  • On this tree: go test -p 2 -count=1 ./internal/command/requirementbrowser/... ./internal/app/..., npm run command-contract:check, go vet, go tool staticcheck and gofmt on the changed package, text-policy, source-hygiene, the binding-reading node suite scripts/admission-records-structure.test.mjs and git diff --check passed. Earlier candidates with the same product source and the same rendered bytes passed go test -p 2 -count=1 ./... (148 packages ok), browser:static-check (72/72), boundary-contract:check (316/316), compact-, receipt-, spec-tree- and graph-contract:check and npm pack --dry-run (26 files; shipped files changed: the supply-chain spec pair, cli-contract.v2.json and requirement-bindings.json).
  • Host stall, not caused by this change: on this host, a Node spawnSync call with about 430 KB of input occasionally never delivers EOF to the CLI, which then blocks in read(2) on its stdin. Earlier runs of a candidate of this change and of test(browser): assert media emulation in the document under test #231 each lost single tests to it. The test-transport fix (fix(test): give spawned CLI children their input as a file, not a socket #232) is below this change in the chain.

Non-claims

  • The token set is today's two palettes and scales under one owner. It is not claimed to be a unified design system, that each token category is used by both style owners, or that any token value meets a contrast or target-size criterion; the accessibility matrix and the redesign own those.
  • The grammar test reads only the shipped stylesheets, the workspace scripts and the fixture renderings; other inputs' renderings are not scanned for style or presentational attributes. Its script rules are lexical: they match listed spellings line by line, not parsed JavaScript, so a write through a computed, concatenated or escaped name, reflection, an alias or a non-literal key, or through a property, method or attribute that the test does not list, or a listed spelling written in another form (whitespace, a line break or a comment between tokens, an optional call, a further argument, a prefixed qualified name, a comma in the namespace argument, a shorthand object property), is not detected (a REQ-PROOFKIT-QUALITY-028 non-claim). Its literal definitions are the closed lists stated in REQ-PROOFKIT-QUALITY-028.
  • Computed-style identity is host-local evidence for this change on darwin/arm64 with the pinned engines. Pixel identity is claimed only through the reference gate's recorded environment and calibrated CPU models.
  • Who reviews a reference change, a human usability study and WCAG conformance certification remain non-claims.

iperev added 11 commits October 22, 2026 01:20
…y vulnerability

govulncheck reports GO-2026-6809 (crypto/x509 distrusts compromised
intermediate certificates) as reachable in Go 1.27.2 through the CLI's
diagnostic output path, so the release candidate and every source gate
fail at go:vulncheck. Go 1.27.3 contains the fix.

- go.mod: toolchain go1.27.3; the go directive and every module
  requirement are unchanged.
- release/change-record.v2.json: the toolchain addition now states that
  release binaries are built with Go 1.27.3 and why; the pinned copy in
  internal/tools/releasechange/record_test.go is regenerated from it.

Proof on this tree:
A local npm run check sequence on darwin/arm64 passed (exit 0) on tree
f7cfc5f with npm 12.2.0, CPython 3.15.0 and Go 1.27.3: 561/561
browser cases in Chromium, Firefox and WebKit with zero retries. One
recorded deviation: the host firewall blocks Go-built binaries from the
network, so the go:vulncheck step ran govulncheck against a vulnerability
database snapshot fetched with curl (modified 2026-10-21T00:27:29Z) and
found no vulnerabilities. With the same snapshot, Go 1.27.2 is reported
affected by GO-2026-6809.
Add the required job "quality / platform smoke / linux-arm64" on the
GitHub-hosted ubuntu-24.04-arm runner. Its first step fails unless uname -m
reports aarch64. It then sets up the same pinned Node 26.11.1, verified npm,
Go and Python 3.15.0 toolchains as the macOS platform jobs, runs the
rootpath, repositorytransaction, processgroup and projectstatus packages
natively, runs npm run npm:version and npm run platform:smoke, builds the
complete current release wheel set, and runs the CPython 3.11.0
installed-wheel smoke, which selects the manylinux_2_17_aarch64 wheel and
the pinned linux/arm64 CPython 3.11.0 image digest. The required aggregate
needs the job and requires its success.

The workflow oracles pin the job's name, runner, 15-minute timeout and exact
ordered steps, the five required leaf jobs and the aggregate shell. They
refuse mutants that move the job to another runner, drop, move or weaken its
architecture check, drop or narrow its invariant, smoke, wheel-set or
CPython 3.11.0 steps, or remove it from the aggregate needs or result
checks. The Python runtime oracle counts the job as a pinned Python
consumer.

README states that required CI executes Linux arm64 on ubuntu-24.04-arm and
x64 on ubuntu-24.04, that CI does not execute a host at the manylinux 2.17
floor, and that the CPython 3.11.0 smoke runs on both Linux architectures;
its projection test follows. REQ-PROOFKIT-QUALITY-011 now names the Linux
arm64 platform job, its architecture check and its CPython 3.11.0 step, and
five required leaf jobs instead of four. CONTRIBUTING lists ubuntu-24.04-arm
among the source-check targets.

Proof on this tree:
A local npm run check sequence on darwin/arm64 passed (exit 0) on tree
5573507 with npm 12.2.0, CPython 3.15.0 and Go 1.27.3: 561/561
browser cases in Chromium, Firefox and WebKit with zero retries. One
recorded deviation: the host firewall blocks Go-built binaries from the
network, so go:vulncheck ran against a vulnerability database snapshot
fetched with curl (modified 2026-10-21T00:27:29Z): no vulnerabilities.
The new arm64 job's success is proven only by hosted CI on this PR.
Add a visual reference gate for the rendered requirement views. npm run
browser:visual, which npm run check does not run, compares fourteen views (the
source, spec-tree, structured proof, compact proof and coverage static
renderings in Cards and Table modes, and the Specifications, Coverage, Diff and
Traceability workspace tabs) in Chromium, Firefox and Playwright WebKit, light
and dark, at 320 and 1440 CSS pixels, plus one Chromium README scene: 169
references. A reference is a digest, not an image. tests/visual/references.json
binds, for each reference ID <engine>/<view>.<scheme>.<width>, the width, the
height and the SHA-256 of the canonical pixel encoding that
REQ-PROOFKIT-QUALITY-028 specifies. No reference image is stored in the
repository.

tests/visual/environment.json records the reference environment: the
mcr.microsoft.com/playwright:v1.63.0-noble image with its index and platform
digests on linux/arm64, and the observed OS identity, machine architecture,
image marker, fontconfig, FreeType, HarfBuzz and font package versions, the
SHA-256 of the committed tests/visual/fonts.conf, the fc-match results for
monospace, sans-serif, serif, system-ui and ui-monospace, the installed engine
revisions, the absence of emulation and the launched browser versions. fc-match
reads the text after an unescaped hyphen as a point size, so each family is
queried by its whole name with every hyphen escaped. fonts.conf includes the
system configuration and prefers Liberation Sans for sans-serif and system-ui,
Liberation Serif for serif and Liberation Mono for monospace and ui-monospace,
which have real bold faces. The system configuration appends sans-serif to
every pattern that names no generic family, so the strong sans-serif
preference makes each family fontconfig does not know a strong alias of
Liberation Sans, which WebKit accepts for the first family that WebKit looks up
in fontconfig; without the ui-monospace mapping WebKit drew the product's
monospace stacks in Liberation Sans. The gate copies fonts.conf into its run
directory and sets the copy as FONTCONFIG_FILE before observing, so fc-match
and every browser read the hashed bytes. The engine fixture requires the copy
to equal the test snapshot's fonts.conf and requires that no project sets its
own browser environment, so every browser inherits that FONTCONFIG_FILE.
references.json also binds the SHA-256 of the environment record's canonical
JSON.

The gate observes the environment, writes it with the CPU vendor, model and
features to observed-environment.json in its run directory and prints the CPU
before launching any browser. On another environment it records the state
unverified with the divergent fields and exits 0; --require-reference-environment
makes that state exit 1. Firefox drops color-scheme emulation at a navigation
into or out of a document served with Cross-Origin-Opener-Policy: same-origin
unless both documents have the same origin and send that header, including the
first navigation from about:blank; emulation applied after the last such
navigation holds across reload and across navigation within one fixture. The
gate therefore applies the color scheme and reduced motion after the first
navigation, reloads, applies them again after the reload, the last navigation
of every captured view, and asserts both through the matching media queries
before each capture. It fits the window to the document height, cut at 2400
CSS pixels, captures each view twice, requires byte-equal PNG captures, decodes
them with node:zlib, refusing tRNS outside palette images and a second IHDR,
and requires the dimensions and the canonical digest to equal the inventory
entry; a failure names the ID and both digests and sizes. Every
reference-environment run, passing or failing, keeps its captures, a candidate
inventory and the observed environment below its run directory, and a run
after which the materialized tests/visual tree gained, lost or changed a file
fails. Six controls per engine serve a copied workspace stylesheet: a routed
unchanged copy must keep the admitted digest, so routing alone cannot change
the pixels, and a one-unit text color change, a one-pixel shift, a font-weight
change from 400 to 700, a change of the sans-serif family to serif and a
change of the monospace family to sans-serif must each change it. A font-face
check per engine reads the font stacks of the workspace and static roots and
of the workspace and static monospace elements from their rendered views,
draws each beside the named Liberation faces in regular and bold, and requires
exact pixel equality with Liberation Sans for the roots and with Liberation
Mono for the monospace stacks, which must also be monospaced. The visual
Chromium project adds --disable-partial-raster, without which repeated
launches rasterized rounded corners differently, and the visual Firefox
project adds browser.chrome.site_icons=false; the runtime harness is
unchanged. One runtime-harness site with the same Firefox pattern remains:
tests/browser/workspace-evidence-layout.spec.mjs emulates the color scheme
before its first navigation, so its two dark Firefox rows render light; the
runtime harness site is fixed by the next change in this chain,
test(browser): assert media emulation in the document under test.

browserproofverify selects its input manifest, witness command, record path,
admissible states and run-directory retention from a closed table. The runtime
manifest, witness command, record path, record format and run cleanup are
unchanged. The visual row keeps its run directory, writes
artifacts/proofkit/browser-visual-proof.json with the state passed or
unverified, recomputes the environment divergence itself, requires each engine
to pass exactly its expected comparisons, controls and font-face check with
zero retries, and refuses an inventory with a missing, extra, duplicate or
malformed entry, dimensions out of bounds or a binding to another environment.
Any failure exits non-zero without writing a record. References are admitted
by the owner action --admit-references --from <run directory>, which the gate
never calls. It admits only a run whose observed environment equals the
recorded one and that holds exactly one regular capture file per ID and no
unstable directory, recomputes every digest from the PNG captures through the
same canonical encoding, refuses a run whose candidate inventory disagrees or
whose observed environment claims a divergence, and only then writes
references.json. It cannot authenticate the run directory as unmodified gate
output; only a passing gate on the admitting tree proves the admitted digests.

browsertestserver gains a compact proof selector and a README project
selector, which materializes a committed synthetic example.dispatch project,
serves it through the same project inspection path as the view command and
removes the temporary project on every exit path. A test admits that project
through the project plan.

CI gains the required job "quality / browser visual references" on
ubuntu-24.04-arm in the digest-pinned image with --user 1001 --ipc=host
--init. It sets up Node 26.11.1, the verified npm, Go and the locked tooling,
runs npm run browser:visual -- --require-reference-environment, uploads the
captures, the candidate inventory and the observed environment after every
outcome as browser-visual-captures-<sha>-<attempt> for 30 days, and uploads the
record only after success. The required aggregate needs the job, and
REQ-PROOFKIT-QUALITY-011 now counts six required leaf jobs in the aggregate.
REQ-PROOFKIT-QUALITY-025 admits exactly this job container and requires every
workflow container image reference to be pinned by a sha256 digest; the
workflow oracles pin the job's container, steps, uploads, runner, timeout and
aggregate membership and refuse an unpinned image.

REQ-PROOFKIT-QUALITY-028 states the reference gate, the canonical pixel
encoding, the font-face check and the admission with its bindings, coverage
inventory entries, overview line and non-claims: weights 400 and 500, and
weights 600 to 900, each render identically because only regular and bold
faces are installed; a family name that WebKit looks up and fontconfig does
not know, other than ui-monospace, resolves to Liberation Sans in WebKit;
pixel identity is claimed only on the calibrated Apple M1 and Arm Neoverse N2
CPU models; admission does not authenticate its run directory; admitted
images are reproducible only while the pinned image and the pinned toolchain
and dependency downloads remain available. The binding record carries the
requirement's source non-claims. The witness plan adds the browser-visual
command and policy, the container-linux-arm64-browser environment class and
the browser-visual parallel group, and the go-test policy lists the visual
records and the digest script its tests read. CONTRIBUTING.md explains how to
view an admitted image from a clone in the pinned image with checksum-verified
toolchains and how references are re-admitted, and requires, before merge, a
passing gate on each CPU model named in the non-claims or a change of those
non-claims. The UI-DESIGN-01 backlog row now says that reference screenshot
digests are committed and the screenshots are reproducible in the recorded
environment, as the owner decided.

Proof on this tree:
A local npm run check sequence on darwin/arm64 passed (exit 0) on tree
f67bb17 with Node 26.11.1, npm 12.2.0, CPython 3.15.0 and Go 1.27.3:
561/561 runtime browser cases in Chromium, Firefox and WebKit with zero
retries. One recorded deviation: the host firewall blocks Go-built binaries
from the network, so go:vulncheck ran against a vulnerability-database
snapshot fetched with curl (modified 2026-10-21T00:27:29Z): no
vulnerabilities. npm run check does not run the visual gate. Its proof is
two fresh-container runs in the pinned arm64 image on Apple M1 (190/190
each, candidates equal to references.json) and three hosted CI runs of this
tree on Arm Neoverse N2 (190/190 each, all jobs green).
Pinned Firefox drops color-scheme emulation at a navigation into or out of
a document served with Cross-Origin-Opener-Policy: same-origin, unless both
documents have the same origin and send that header. That includes the
first navigation from about:blank, a navigation to another fixture (each
fixture server has its own port) and history traversal across such a
boundary; every fixture document sends the header. Emulation applied after
the last such navigation holds across reload and across navigation between
documents of the same fixture. Chromium and WebKit are unaffected, and
reduced-motion and forced-colors emulation survived every such navigation
tested in every engine. Playwright's default light scheme is applied before
navigation as well, so pinned Firefox drops it; tests that do not call the
helper run in Firefox under its own preference.

The reflow spec tests/browser/workspace-evidence-layout.spec.mjs, the
native reflow witness of REQ-PROOFKIT-SPEC-039 and REQ-PROOFKIT-SPEC-040,
emulated before openWorkspace, so its two Firefox dark rows (390x844 and
1280x800) ran in the light scheme. No emulation site asserted that its
emulation took effect.

- tests/browser/workspace-navigation-harness.mjs: emulateMediaInEffect
  applies page.emulateMedia and then asserts, in one observation, that the
  page holds a served http document and that matchMedia matches every
  given color-scheme, reduced-motion and forced-colors value. It refuses
  any other feature or value, and the http check refuses a call made in
  the initial about:blank document, where a match says nothing about the
  next document. Every pinned engine reflects forced-colors emulation in
  matchMedia, so that feature is asserted in all three. The assertion
  proves the media feature, which switches the author forced-colors rules,
  not an engine forced-color palette, which WebKit does not apply.
- tests/browser/workspace-evidence-layout.spec.mjs: each reflow row
  emulates after openWorkspace through the helper, without a reload. The
  workspace reads the color scheme and the motion preference only through
  CSS (media queries and the root color-scheme property), which follows
  emulation in the loaded document.
- tests/browser/workspace.spec.mjs (rendered light and dark contrast),
  tests/browser/static-view.spec.mjs (static control contrast) and
  tests/browser/workspace-graph.spec.mjs (dark and forced-colors graph
  paint) emulate through the helper. They already emulated after
  navigation.

Test titles, requirement statements, bindings and the browser proof input
manifest are unchanged. No statement of REQ-PROOFKIT-SPEC-039 or
REQ-PROOFKIT-SPEC-040 names a color scheme, the light and dark contrast
oracles of REQ-PROOFKIT-QUALITY-022 already ran under the intended
scheme, and the runtime proof inputs select tests/browser as a whole.

Proof on this tree:
A local npm run check sequence on darwin/arm64 passed (exit 0) on tree
14809a0 with Node 26.11.1, npm 12.2.0, CPython 3.15.0 and Go 1.27.3:
561/561 runtime browser cases in Chromium, Firefox and WebKit with zero
retries. One recorded deviation: the host firewall blocks Go-built binaries
from the network, so go:vulncheck ran against a vulnerability-database
snapshot fetched with curl (modified 2026-10-21T00:27:29Z): no
vulnerabilities.
The Node suites passed CLI input with spawnSync's `input` option. Node then
gives the child one end of a Unix-domain socket pair, writes the input, calls
shutdown(SHUT_WR) on its end and keeps that end open until the child exits
or the timeout kills it. On macOS, a child blocked in read(2) on that socket
is sometimes not woken by the shutdown, although the socket already holds
end of file, so the call ends only at its timeout. Node 22, 24 and 26 stall
this way on darwin/arm64, and so does the same syscall sequence written in
C, without Node or libuv. With a pipe, with a close instead of the shutdown
or with a regular file, that reproducer did not stall, and neither did Node
on Linux. The suites saw the stall as an occasional spawnSync ETIMEDOUT of
one CLI call.

- scripts/cli-process.mjs adds spawnWithInputFile(file, args, options). It
  is spawnSync(file, args, options) except that options.input is written to
  a temporary file that the child gets as its standard input. The file has
  mode 0600 in a fresh directory of mode 0700, and the directory is removed
  as soon as the file is open, before the child starts, or at once when
  writing or opening the file throws. The removal uses path calls only; if
  it fails, the input descriptor is closed and the call throws. Status,
  signal, error, stdout, stderr, timeout, maxBuffer and the other passed
  options keep spawnSync's meaning, except that writing the input can no
  longer fail with EPIPE. An unsupported option is refused, and so is any
  args that is not an array, including the two-argument form
  spawnWithInputFile(file, options), which spawnSync would read as its
  options while it drops the third argument. The result also reports
  stdinUnreadBytes, the input bytes past the file offset that the child
  shares with the parent when it exits: read(2) and lseek(2) on the
  inherited descriptor move it; pread(2), mmap(2) and, on Linux, reopening
  /dev/stdin do not.
- All 42 spawnSync calls with input in 29 test files under scripts/ use it.
- assertCLITransport accepts a non-zero stdinUnreadBytes under the rule it
  already applies to EPIPE: only from a child that exited normally with a
  non-zero status and no signal. EPIPE stays accepted for the asynchronous
  pipe transport of the schema-completeness test.
- The early-refusal test of the browser server asserts that stdinUnreadBytes
  equals the size of its input, so the refusal read none of it, instead of
  asserting EPIPE.
- scripts/cli-process.test.mjs covers the file transport, the unread-input
  and EPIPE rules through both transports, the kept and refused options,
  the removal of the input file before the child runs and after a throw,
  a failed write included, and the closing of the input descriptor after a
  failed removal. It adds a lexical source check that refuses an `input`
  member in the arguments of a direct spawnSync, execFileSync or execSync
  call under scripts/ and tests/.
- proofkit/witness-plan.json: the boundary-contract-check policy declares
  scripts and tests, which that source check reads.

No timeout, retry or assertion is relaxed.

Proof on this tree:
A local npm run check sequence on darwin/arm64 passed (exit 0) on tree
600c4bf with Node 26.11.1, npm 12.2.0, CPython 3.15.0 and Go 1.27.3,
with no Go test verdict replayed from the test cache: 561/561 runtime
browser cases in Chromium, Firefox and WebKit with zero retries. One
recorded deviation: the host firewall blocks Go-built binaries from the
network, so go:vulncheck ran against a vulnerability-database snapshot
fetched with curl (modified 2026-10-21T00:27:29Z): no vulnerabilities.
The TypeScript adapter that json-report-cli-adapter-source generates ran
the CLI with spawnSync(binaryPath, args, {cwd, env, input, maxBuffer}) and
no timeout. On Unix-like systems, Node gives the child one end of a
Unix-domain socket pair for the input option, writes the input, shuts down
writing and keeps its end open until the child exits. On macOS, a child
blocked in read(2) on that socket is sometimes not woken by the shutdown,
so an adapter call that passed input could block forever. With the adapter
generated by the parent commit, Node 26.11.1 and the CLI on 59 captured
spec-proof-bundle-admission inputs (58 of them 514 to 585 KB as stable
JSON), 9 of 3540 calls on darwin/arm64 did not return within 5 seconds.
The one held for 60 seconds stayed blocked, with a Unix socket as its
standard input. The adapter generated by this commit returned in all of
2950 such calls.

- The generated runProofkitCommand writes the stable JSON input to a file
  created with O_EXCL and mode 0600 in a fresh mkdtemp directory under
  os.tmpdir() of the calling process, opens it read-only and gives that
  descriptor to spawnSync as stdio[0]. Except on Windows, it then removes
  the file and the directory by path (rmSync of the file, rmdirSync of
  the directory) before the CLI starts, so the CLI reads a file that no
  longer has a name, and a signal that ends the calling process during
  the call leaves no input on disk. When the call returns or throws, the
  descriptor is closed and whatever is not yet removed is removed: on
  Windows, where an open file can keep its name until it is closed, after
  a throw before the early removal, and after a failed early removal. The
  removal needs no descriptor of its own, and a failed removal fails the
  call. Errors of this transport are thrown through
  formatProofkitCliError, as the errors that spawnSync returns already
  were.
- A call without input (inputMode "none") gives the child the null device
  instead of a shut-down socket. The adapter passes no --input flag for
  such a call, so the CLI does not read its standard input.
- cwd, env and maxBuffer are passed as before, and stdout, stderr, status
  and the errors that spawnSync returns are handled as before. Argument
  errors that spawnSync throws itself, such as a negative maxBuffer or a
  NUL byte in binaryPath, cwd or an env value, are now thrown as plain
  Errors carrying formatProofkitCliError's redacted, bounded message,
  without Node's error class or code. A secret-shaped env value that
  Node's message quotes is therefore redacted, where it was shown before.
  A child that exits without reading a large input no longer makes the
  call throw EPIPE; the call reports the child's status and diagnostic
  instead.
- The generated source is 1189 lines with digest
  sha256:e782b2cb6e17f1671a43418cf224a566365a4ebe90730a40699adbffbd1b0ac4.
  The bundle keeps its record shape, generatorId
  proofkit.json-report-cli-adapter-source.typescript.v2 and contractId
  proofkit.json-report-cli-adapter-source.output.v1. Neither the CLI
  contract nor a bound requirement statement specifies how the adapter
  feeds the CLI's standard input, so under the contract-identity rule of
  docs/proofkit-contract-map.md this is a defect correction.
- proofkit/cli-contract.v2.json re-pins the native-source digest of the
  command's output contract. The generated contract projections and the
  public CLI ABI hash follow from that digest.
- scripts/generated-starter-structure.test.mjs declares the byte deltas of
  the three adapter bundle rows from the frozen predecessor streams: the
  imports, the transport, the source digest and the pretty and compact
  line count. The corpus is unchanged.
- A source check requires that the single spawnSync call passes exactly
  cwd, env, maxBuffer and a stdin descriptor, with no input member, and
  that the input file is removed by path right after it is opened, except
  on Windows. The generated-adapter semantics test probes the child's
  standard input: a regular file of mode 0600 with no remaining link that
  holds exactly the stable JSON, alone in a mode 0700 directory when it is
  opened and gone before the child runs, or the null device for a call
  without input, although the harness's own standard input is a pipe.
  The Windows branch, run by reporting win32 as the platform, keeps the
  file under its name until the call returns. Simulated failures of the
  read-only open, of either removal call and of the end-of-call removal
  make the call throw and leave no descriptor open; only the failed
  end-of-call removal leaves the directory. The test also covers an early
  refusal, maxBuffer, a negative maxBuffer, a secret-shaped env value, a
  missing binary and a missing temporary directory.
- README.md states where a call with input writes it, that the caller
  needs a writable temporary directory, when the input is removed on
  Windows and elsewhere, and when it can remain on disk.

No timeout is added.

Proof on this tree:
A local npm run check sequence on darwin/arm64 passed (exit 0) on tree
a17845a with Node 26.11.1, npm 12.2.0, CPython 3.15.0 and Go 1.27.3,
with no Go test verdict replayed from the test cache: 561/561 runtime
browser cases in Chromium, Firefox and WebKit with zero retries. One
recorded deviation: the host firewall blocks Go-built binaries from the
network, so go:vulncheck ran against a vulnerability-database snapshot
fetched with curl (modified 2026-10-21T00:27:29Z): no vulnerabilities.
go:test ran `go test ./...`. Go's test cache replays a package's passing
result while the test binary, the files the test process opened and the
environment variables it read are unchanged. Many tests here run a child
process, such as node, git or a built CLI, and the files that child reads
are not part of that key. So a full gate could report a cached pass for a
test that fails on the tree under test. On the parent commit's tree, with
scripts/source-hygiene.mjs changed to exit 0 before it checks anything,
`go test ./scripts -run '^TestSourceHygieneRejectsMalformedUTF8$'`
reported `ok (cached)`; the same command with -count=1 failed. Recent
local gates served 120 to 143 of the 148 test packages from this cache.
The witness plan already declares proofkit.go-test, like each of its
commands, with cachePolicy "disabled", which witness-scheduler-plan
treats as not cacheable; the command now behaves as declared.

- package.json: go:test is `go test -count=1 ./...`. -count=1 is the flag
  Go documents for disabling the test cache, so every test package runs on
  the tree under test. The build cache is still used.
- proofkit/witness-plan.json: the proofkit.go-test argv is ["go", "test",
  "-count=1", "./..."]. witness-scheduler-plan admits the plan, and of its
  node digests only that of proofkit.go-test changes; no tracked file pins
  it.
- proofkit/requirement-bindings.json: the command bound to
  proofkit.go-test is `go test -count=1 ./...`, as the existing
  witness-command identity test requires.
- scripts/workflow_source_oracles_test.go requires go:test to be that
  command and the witness argv and the bound command to equal it. It
  refuses each of the three without -count=1, a narrower package pattern,
  a repeated count and a masked failure. A second test requires a -count=
  flag on every Go test invocation it finds in the package scripts, the
  witness plan argv and the YAML files under .github. It reads shell text
  lexically and finds an invocation only as adjacent go and test fields:
  it misses one in a subshell or in backquotes, one through an absolute
  go path and `go -C . test`, and it accepts -count=1 in a comment and
  -count=0. The other invocations it finds already carry -count=:
  go:bench, the legacy Unicode positive witness, and the CI workflow's
  legacy Unicode, cross-filesystem deletion and platform invariant steps.
  CI's "Run all Go tests" step and the release workflow's full check run
  go:test itself.

The Go code that starts `go test` itself already passes -count=1: the
command oracle behind self:coverage, the schema-completeness mutation
witnesses and one coverage-metrics test.

Hosted CI time is unchanged: recent hosted quality / source jobs and the
release candidate's check served none of the test packages from the
cache, even when they restored it, so they already ran every package.
A local gate whose test packages the cache used to serve now links and
runs all of them, so it takes longer, and on a loaded host it comes
closer to Go's per-package test timeout and to the 600-second bound
declared for proofkit.go-test. Test results are not made deterministic
by this change.

Proof on this tree:
A local npm run check sequence on darwin/arm64 passed (exit 0) on tree
18bff04 with Node 26.11.1, npm 12.2.0, CPython 3.15.0 and Go 1.27.3,
with no Go test verdict replayed from the test cache: 561/561 runtime
browser cases in Chromium, Firefox and WebKit with zero retries. One
recorded deviation: the host firewall blocks Go-built binaries from the
network, so go:vulncheck ran against a vulnerability-database snapshot
fetched with curl (modified 2026-10-21T00:27:29Z): no vulnerabilities. The
gate runner already passes -count=1 to every Go command, so this run does
not discriminate the change; the oracle tests and the stale-verdict
reproduction above do.
GATE-DIFF-01 asked for a closeout gate that inherits the verdicts of
unchanged steps from a passing parent tree. Measured on this repository's
recent commits and gates, its rules save a mean of at most 0.2 % of a
hosted gate, and the trace check they need on every gate would cost more
than that: any overhead above 0.2 % does, and Linux micro-benchmarks of
its cheapest suitable mode showed factors of 1.5 and 11. The row is
deferred with its measurement, the premises found false and a reopen
condition.

- GATE-DIFF-01 moves from NEXT to DEFERRED. Its scope keeps the
  unsoundness statement and adds the measurement: over the 40
  first-parent commits from 6d19004 to 834f6ae, the last seven of them
  not yet on main when measured, the rules would have saved a mean of at
  most 2.1 s per commit, 0.2 % of a 22.7-minute hosted gate, because 34
  of the 40 commits hit a full-gate trigger and the costliest steps
  cannot be inherited. It lists the premises found false: whole-worktree
  reads by browser:check, by the ./... steps and by the CLI builds of
  three node contract steps; same-tree verdict flips of browser:check and
  boundary-contract:check; Firefox under sandbox-exec on macOS;
  release:closeout without selectors; an incomplete toolchain list; and
  go:test verdicts replayed from Go's test cache, which the parent commit
  fixes.
- The scope records that the known causes of the boundary-contract:check
  flips are fixed: the early-refusal EPIPE race by the shared CLI
  transport assertion, and the lost macOS Unix-socket wake-up by giving
  spawned CLI children their input as a file. It also records the
  browser:check flips: two WebKit navigations that did not reach their
  target document and six Firefox and Chromium page operations that did
  not complete, all at the 30-second test timeout, in local full gates
  whose browser tests took 2.6 to 4.6 times as long as on the passing
  reruns, with no load sampled during them.
- The completion condition starts with the reopen condition. Its first
  clause asks for 20 consecutive full gates of one unchanged tree that
  pass browser:check and boundary-contract:check with zero retries inside
  a load envelope: the 1-minute load average per CPU, sampled at least
  once a minute during those steps, has a median of at most 6 and a
  maximum of at most 12, and at least five of the gates have a median of
  at least 3. A gate outside the envelope is recorded but neither counts
  nor restarts the count, and no timeout is raised, no retry added and no
  CSP, console or security-policy check weakened. The other clauses stay:
  fewer than half of the last 40 first-parent commits on a full-gate
  trigger, and a trace overhead measured on the real hosted gate below
  the mean saving then measured. A reopened row first traces every
  step's reads on its own tree, recomputes the saving and resolves each
  false premise.
- The requirement text stays, except that its toolchain list also names
  Python, git, shellcheck and pyflakes, and its release:closeout trigger
  names the witness command that release:closeout must first get.

No row is added for browser:check. With fixed 30-second timeouts any
test fails under enough contention, so its verdict can be a function of
the tree only inside a stated load envelope. None of the recorded
failures is known to lie inside one, and the reopen condition above is
the only consumer of that claim.

AGENTS.md and every product surface are unchanged.

Proof on this tree:
A local npm run check sequence on darwin/arm64 passed (exit 0) on tree
99c57d7 with Node 26.11.1, npm 12.2.0, CPython 3.15.0 and Go 1.27.3,
with no Go test verdict replayed from the test cache: 561/561 runtime
browser cases in Chromium, Firefox and WebKit with zero retries. One
recorded deviation: the host firewall blocks Go-built binaries from the
network, so go:vulncheck ran against a vulnerability-database snapshot
fetched with curl (modified 2026-10-21T00:27:29Z): no vulnerabilities.
The OSV source scan of the parent tree fails: package-lock.json locks
fast-uri 3.1.8, which four advisories affect: GHSA-333x-w4r6-6crp
(CVE-2026-110725), GHSA-w74x-v6f9-j3xg (CVE-2026-110306),
GHSA-wjr9-j795-jxcr (CVE-2026-110180) and GHSA-xhq7-xmqj-769h
(CVE-2026-110095). Each names 3.1.9 as the first fixed 3.x version.

- package-lock.json: fast-uri 3.1.9 with its registry integrity. No
  other entry changes; ajv 8.20.0 still requires fast-uri through
  ^3.0.1.
- BACKLOG.md: a BLOCKED row records the fast-uri copy that the pinned
  browser-test SDK inlines, which no lock entry can change.

fast-uri is a development dependency only. In the lock, ajv is its
only dependent, and only the Node schema tests load that ajv. There,
ajv passes fast-uri only schema identifiers and references, with
default options. In every tracked invocation they come from its
bundled meta-schemas and this repository's contract schemas; an
operator-set PROOFKIT_*_CONTRACT or PROOFKIT_*_BASELINE variable
substitutes that file's schemas. Validated instance data never reaches
it. The npm package declares no runtime dependency, and neither it nor
the Python wheels contain fast-uri.

playwright-core 1.63.0 inlines a separate fast-uri 3.1.3 into
lib/utilsBundle.js. This change does not touch that copy, and the OSV
scan does not see it. Loading that file runs only the copy's module
initializers. Only the Model Context Protocol client and server inlined
in the same file call its exported functions; the browser suite never
does. The copy stays affected by every fast-uri advisory fixed after
3.1.3, and no Playwright release inlines a fixed fast-uri yet.

No other file pins the lock or a digest of it. The release SBOM is
built from the Go module graph and the release files, the package gate
pins only the direct development dependencies in package.json, there
is no Python lock, and the witness plan and the browser proof manifests
list package-lock.json only as an input path.

Proof on this tree:
A local npm run check sequence on darwin/arm64 passed (exit 0) on tree
90a6ec0 with Node 26.11.1, npm 12.2.0, CPython 3.15.0 and Go 1.27.3,
with no Go test verdict replayed from the test cache: 561/561 runtime
browser cases in Chromium, Firefox and WebKit with zero retries. One
recorded deviation: the host firewall blocks Go-built binaries from the
network, so go:vulncheck ran against a vulnerability-database snapshot
fetched with curl (modified 2026-10-21T00:27:29Z): no vulnerabilities. An
earlier full gate on a candidate of this change failed once in an unchanged
package, internal/tools/releasepreflight, on a timing race in a
counterexample test that uncached Go test runs now expose; the next change
in this chain fixes that race.
…signal

TestReleaseWorkflowExecutesNPMAbsenceBranches failed once in a local
full gate on a candidate of the parent change, which did not touch this
package: the removed-cleanup-wait counterexample reported "classifier
exited without completing admission" and "write |1: broken pipe" instead
of "carrier cleanup did not wait for classifier exit". With the macOS
/bin/bash 3.2 it failed in 1 of 200 runs at a 1-minute load of 20 to 36
on 8 CPUs, and in 9 of 800 runs with eight added busy loops (400 of them
with failure-message diagnostics only). Every failing subtest took 3.02
to 3.05 s. The three other cancellation subtests failed the same way; in
3 of the 10 failures Bash 3.2 instead crashed with SIGSEGV in the same
wait.

The carrier cancels with one SIGTERM after the shim reports P, its last
step before it waits for the classifier. The classifier blocks on a
release pipe until the controller releases it after the cleanup has
started. Bash 3.2 checks for pending traps when a command starts, and
wait does not check again before it blocks. Where that SIGTERM lands
decides the outcome:

- Before wait sets interrupt_immediately, the trap handler only records
  the trap, and wait blocks until the awaited child exits, here only at
  the classifier's own 3-second test timeout. Then the trap runs, the
  exit oracle reads EOF instead of a live classifier, and the release
  write fails with EPIPE.
- After wait sets interrupt_immediately but while its setjmp of
  wait_intr_buf still runs, the trap handler longjmps to that incomplete
  buffer, and Bash faults with SIGSEGV. Because the script traps EXIT,
  Bash 3.2 only records the fault and lives on until SIGCHLD from the
  classifier's timeout. Its terminating-signal path then runs the
  recorded TERM trap, whose exit 124 ends the EXIT trap before cleanup
  writes W: the trace holds cancelled, and the controller reads C, then
  EOF.

Sending the signal while Bash expands wait's operand reproduces the
first outcome in 120 of 120 cancellation subtests. A standalone Bash 3.2
script with one trapped SIGTERM and an EXIT trap reproduces both, the
second with the crash-report frames of the measured runs from the signal
handler down to wait's setjmp; Bash 5.2 shows neither. The extracted
release workflow bytes contain no trap, wait or signal handling; the
defect is in the test carrier.

- npm_absence_branch_test.go: after P, the controlled shim reads
  descriptor 7, the read end of a new signal-sent pipe, instead of
  waiting for the classifier, and then returns 97. The pipe reaches EOF
  only after the signal was sent, so the read ends at the latest at that
  EOF and the trap runs before the next command while the classifier is
  still held. If the trap did not run first, the controller would read W
  where it requires C, or C without the W that must follow; both fail.
  The classifier's spawn closes descriptor 7. The uncontrolled shim
  still waits for the classifier.
- A new schedule, "signal pending when the carrier blocks", expands the
  read's redirection with a command substitution that reports E and then
  waits for that EOF; the controller signals only after E. The
  blocked-entrypoint cancellation test runs it, and the
  removed-cleanup-wait counterexample runs it besides its immediate
  schedule. With the shim's old wait restored, both fail on every run
  under Bash 3.2, the counterexample with the gate's message.
- npm_absence_carrier_test.go: the control protocol owns the new pipe.
  cmd.Cancel sends its SIGTERM only through signalCarrier, which calls
  the signal sender and closes the pipe only after the sender returned.
  The new subtest "signal precedes its pipe close" gives signalCarrier a
  sender that reads the pipe without blocking; it requires the pipe open
  at the signal and at EOF afterwards, so a swapped order fails on every
  run.

The counterexample still requires its cause, the trace view, absent,
cancelled, reaped and exit 124. No assertion, timeout or deadline
changes, and no retry is added. On this tree 0 of 1000 runs failed at
host load under Bash 3.2 and 0 of 200 under Ubuntu 24.04's Bash 5.2; an
earlier version of this change, with the same carrier and the same
signal-then-close order written inline, passed 4038 of 4038 runs with
eight busy loops. This carrier's TERM trap is the only shell signal trap
in the repository. Every other signalled process installs a Go, Python
or Node handler, not a shell trap.

Proof on this tree:
A local npm run check sequence on darwin/arm64 passed (exit 0) on tree
7f6336a with Node 26.11.1, npm 12.2.0, CPython 3.15.0 and Go 1.27.3,
with no Go test verdict replayed from the test cache: 561/561 runtime
browser cases in Chromium, Firefox and WebKit with zero retries. One
recorded deviation: the host firewall blocks Go-built binaries from the
network, so go:vulncheck ran against a vulnerability-database snapshot
fetched with curl (modified 2026-10-21T00:27:29Z): no vulnerabilities.
…views

The static views, styled by internal/kernel/browserdoc, and the
requirement-browser workspace, styled by assets/workspace.css, wrote their
colors, sizes and breakpoints as literals. Every literal color and size in
the declarations of both style owners is now a token of one stylesheet,
internal/kernel/browserdoc/tokens.css, which the binary embeds, and every
media and matchMedia length equals a breakpoint token;
browserdoc.TokenStylesheet returns its exact bytes. Each token holds the
value that its style owner used before, so the change is a substitution: in
the pinned engines every computed property value that the views had before,
and every pixel, is unchanged; the views now also carry the token custom
properties.

- Static views: the single inline style element holds the token stylesheet
  followed by the browserdoc stylesheet, whose declarations read the tokens.
  The dark color-scheme rules keep their selectors and properties and read
  separate dark tokens. The static CSP is unchanged.
- Workspace: the page links /assets/tokens.css before /assets/workspace.css.
  The route serves the token bytes for GET and HEAD with the headers of
  workspace.css, including cache-control: no-store, and answers other
  methods with 405; the workspace CSP is unchanged. The workspace palette,
  light and dark, moved verbatim from workspace.css into the token
  stylesheet, and the system colors of the forced-colors rules became
  forced-colors tokens. The icon script no longer writes width, height,
  stroke-width, stroke-linecap and stroke-linejoin on each icon:
  .workspace-icon, whose size rule already overrode the width and height
  attributes, now sets the stroke width from a token and the round caps and
  joins.
- tokens.css holds only custom-property declarations in :root at the top
  level, in @media (prefers-color-scheme: dark) and in
  @media (forced-colors: active).

A literal grammar test in internal/command/requirementbrowser reads the
token stylesheet, the static style, workspace.css, every workspace script
and the rendered HTML of every static view fixture and of the workspace. It
tokenizes CSS by CSS Syntax Level 3 and analyzes declaration values, not
selectors. Its literal colors include contrast-color() and vendor-prefixed
keywords other than -webkit-box and -apple-system, its literal sizes
include a unitless number in the font, line, tab, outline, stroke, SVG
geometry, zoom, scale, perspective and transform properties, and a
vendor-prefixed property is classified as the property without its prefix.
It refuses: a literal color or size or an attr() in either style owner,
including var() fallbacks and calc arguments, and any var() other than
var(--name); a var() naming no token, no custom property of its owner and
none of the three graph geometry properties, an owner custom property that
shadows a token, a dark declaration of anything other than a palette token,
whose base value holds a literal color, a dark or forced-colors declaration
of a base token with a value of another category (a literal color, a
literal size or neither; a value that holds both counts as a color), a
forced-colors token outside forced-colors rules and an unused token; an
owner custom property holding a literal; any token stylesheet content other
than the three :root contexts, an empty, important or repeated token, a
token that refers to a custom property or an attribute, a breakpoint token
that is not one dimension, a system color outside forced-colors; @import,
@font-face, url( and image-set(; a media, container, matchMedia or rendered
media attribute length that is not textually a breakpoint token's value,
and a matchMedia argument that is not one string literal; a listed spelling
of a script style write other than the eight graph geometry writes of
workspace-graph.js, with attribute and element names in any letter case and
setAttributeNS with any namespace, and a listed non-geometry SVG
presentation attribute written as an object key or through setAttribute or
setAttributeNS, other than fill none, stroke currentColor and the arrowhead
marker reference, each admitted only as one whole string literal; a style
or presentational attribute in rendered HTML, a static view without exactly
one style element and a workspace with one. The rendered-HTML scan ends a
comment where the HTML parser does, at -->, at --!> and at once after <!-->
or <!--->, and it continues after the content of script, style, textarea
and title where the HTML parser ends it as text; after an svg or math start
tag, and always for xmp, iframe, noembed, noframes and noscript, it also
scans that content as markup on its own, so a comment or quoted value that
the content opens does not hide later markup. Each mutant names the
violation text it must produce, and every clause, listed style-write
spelling, SVG presentation attribute, HTML presentational attribute, size
property, color function and size keyword has one; a separate subtest
requires the listed exceptions, such as fill none in rendered HTML, a
breakpoint length in a media attribute and a percentage in every layout
property and in -webkit-transform, to stay admitted. Further tests require
every static view to inline the token file's exact bytes, the workspace to
link /assets/tokens.css before workspace.css and the route to serve those
bytes, and browserdoc to emit one style element that opens with them.

Re-pinned expected values, each from test output after isolating its cause:
- scripts/proof-lookup-structure.test.mjs declares a second stream delta for
  the three HTML rows of the frozen lookup corpus: the content of the only
  style element is the stylesheet with the pinned SHA-256, and replacing it
  with the declared previous stylesheet, then inverting the card-group
  delta, reproduces the frozen stream. A moved style element, a second style
  element and a changed byte outside the style each break conservation.
- The coverage relation parity digests are re-captured; with the previous
  style content put back, all eight previous digests recur, so the style
  element's content is their only change.
- proofkit/cli-contract.v2.json carries the new native source digest of
  internal/command/requirementbrowser, whose non-test Go files changed, and
  the generated projections follow. The public CLI ABI hash and the
  requirement-browser-server input observation of the compact v2 wire
  corpus change only through that digest: substituting the previous digest
  and its derived contract hashes reproduces both previous values. No
  contractId changes, because no wire shape or specified meaning changes.
- The workspace asset route table gains the four rows of assets/tokens.css.

The visual reference controls follow the moved palette: the one-unit text
color control mutates the text color token in a copied token stylesheet,
the routed unchanged control serves unchanged copies of both stylesheets,
and the other controls still mutate workspace.css.
tests/visual/references.json is unchanged.

REQ-PROOFKIT-QUALITY-028 states the token owner, its two consumers, the
grammar's definitions and rules, and the controls' stylesheets, and gains
four non-claims: the font-face check identifies a face through the family
name that the recorded font configuration resolves, so it does not detect a
configuration that redirects a named Liberation face; the recorded font
configuration hash binds tests/visual/fonts.conf, not the system and user
configuration that it includes, and the engine fixture checks only
FONTCONFIG_FILE in the worker's environment; the grammar test's limits;
and that its script checks are lexical, so a write reached through a
computed, concatenated or escaped name, reflection such as Reflect.set, an
alias or a non-literal key, or through a property, method or attribute that
the test does not list, or a listed name or call spelled other than in the
form it matches (with whitespace, a line break or a comment between its
tokens, as an optional call, with a further argument, a prefixed qualified
name or a comma in its namespace argument, or as a shorthand object
property), is not claimed to be detected. The binding carries
the same non-claims and binds the token owner, route and grammar tests,
which the coverage inventory requires; the overview line describes the
token owner. CONTRIBUTING.md names the pixel-identity non-claim that the
re-admission rule refers to, its CPU models and its four copies, and says
that the gate's record identifies an Arm Neoverse N2 runner by implementer
and part while the Apple model comes from the host. No Go package enters the
browser-check closure, so the witness plan is unchanged.

Proof on this tree:
A local npm run check sequence on darwin/arm64 passed (exit 0) on tree
80fedbb with Node 26.11.1, npm 12.2.0, CPython 3.15.0 and Go 1.27.3,
with no Go test verdict replayed from the test cache: 561/561 runtime
browser cases in Chromium, Firefox and WebKit with zero retries. One
recorded deviation: the host firewall blocks Go-built binaries from the
network, so go:vulncheck ran against a vulnerability-database snapshot
fetched with curl (modified 2026-10-21T00:27:29Z): no vulnerabilities. An
earlier full gate on this tree failed two runtime browser tests on 30 s
timeouts while the host load average exceeded 180 on 8 CPUs; this rerun at
moderate load passed.
Base automatically changed from chain/rp-npm-absence-race to main October 25, 2026 23:32
@iperev
iperev marked this pull request as draft October 25, 2026 23:32
@iperev
iperev marked this pull request as ready for review October 25, 2026 23:51
@iperev
iperev merged commit 2113388 into main Oct 25, 2026
15 checks passed
@iperev
iperev deleted the chain/v2-design-tokens branch October 25, 2026 23:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant