Wave A: public-name check, WebAssembly determinism in CI, decode-path reporting, and pin by default #616
Loading…
Reference in a new issue
No description provided.
Delete branch "wai/wave-a"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What this is
Four pieces of reviewed work, put on one linear branch by cherry-picking, with no merge commits. Two small fixes follow them; the combined checks found both. Each fix is its own commit.
This stacks on #614 (
test/timing-under-load, the CI timing fixes), which sits on main after #611. Until #614 merges, this PR's diff against main also shows #614's commits. Merge #614 first.The four items
1. Public-text hygiene (
wai/rd-hygiene)scripts/check-public-names.shnow holds the names of the maintainers' own products in the script. A changelog line naming one of them had passed, because the private list never held them. A new--maintainer-products-onlymode applies that list alone and reads no secret, so the repository checks run it on every pull request. The full check, with the private list, still runs on the push to main and still fails closed without the list.sink-energy§4.6, Outside measurements of decode energy (informative), cites each measurement to its paper and table, with what each one measured and what its paper leaves unstated.Verified: the public-text self-test, the
--maintainer-products-onlyrun and the full check with the private list. All three are clean.2. WebAssembly determinism in CI (
ci/wasm-gate)scripts/check-wasmtime-pin.pyfails any workflow that pins another release, installs the runtime another way, or runs it without both settings. The READMEs' commands pass the same settings.wai-webbundles and the package of record the site serves) are validated with relaxed SIMD and threads turned off (determinism-tiers§5).wai-byte-equality.yml, and thebyte-exactmatrix inwai-conformance.yml) first measures the free space on its disks and writes a test file to each. A disk below 4 GiB, or one that refuses the write, fails the leg as an infrastructure failure, not as a divergence. If a later step fails, the disks are measured again.Verified:
3. Decode-path close-off (
wai/determinism-closeoff)wai_decoded_tcarries the path and whether the fallback was used.determinism-tiers§3 says that no tier lies between the two existing tiers. A session claim states the decode path, and three refusals follow:session_variant_not_exact,session_tier_exceeds_variantandsession_stage_hash_not_exact. The claim corpus gains vectors for these, and the differential now reaches each new rule.wai.image.jpegaidecoder on pinned, fetched inputs, and a skip counts as a failure.Verified:
claim_verify.pypasses.differential.pyagrees with both builds over the sessions CI generates (seed 1).WAI_REQUIRE_SOTA_VECTORS=1: the exact decoder on the fetched inputs, with no skip, and the cases that use committed inputs.wai breaking --againstmain's tip reports every break as acknowledged.4. Pin by default (
wai/pin-by-default)prior_unpinned) for information only.wai verifyreads envelopes, andwai inspectandwai wrapnote the finding.none. The native recorder and the page's recorder both do this, and a new fixture holds both of them to it.wai conform sinkruns the native sink through the conformance runner.wai verify --conformantadmits a result only for a run that passed.That branch's real tip is carried. Its last commits were planted falsifications, each reverted by the next, with no net change, and they are not included.
Verified:
cli_dx verify_reports_an_unpinned_envelopepasses.wai_pin_vectors verifyandpin_verify.pypass.cli_breaking,cli_conformandcli_conform_sinkspass.wai ops export --checkandactions.mjs --checkpass.wai_stage_measure verifyandmeasure_verify.pypass.Two fixes found by the combined checks
These problems already existed on the branches above; combining them did not cause them. Each branch's own gates did not run the step that finds its problem.
wai: an honest session's base entry names the pin it verified, so its stage hash sealsdeterminism-tiers§3, rule 3) madestaged::claim_tests::every_corpus_case_seals_an_honest_sessionfail. The test's replayed sink named no pin or tier for any base, so the honest sessions of the pinned cases were refused. This also fails onwai/determinism-closeoffalone.none.CHANGELOG: the pin-by-default entry acknowledges the staged-measure goldens' breakwai breakingreportedstaged-measure-conformance/goldens.jsonas an unacknowledged wire break. The new fixture's evaluation was added, and the corpus-wide dedup totals moved.Conflicts resolved
wai/CHANGELOG.md: every entry is kept once, newest at the top.jwp-receipts§2.4.12: thevariantrow comes from the decode-path work, and thepinandtierrows come from pin by default.Final review before push
A review of every commit message and the whole diff, for public history, led to these changes. The code tree is unchanged by the message edits.
Verified after these changes: the documentation checks, the catalogue-prior verifier, the workflow step check and YAML parse,
wai breakingagainst #614's tip with every break acknowledged, the build, and the page recorder's set test.Two tests that failed this pull request under load
Two checks failed on this pull request in code it does not change: the relay's DVR test on the linux-arm64 runner, and the watch-together example on the macOS runner (twice). Two commits on top fix them:
joule-moq-relay DVR: count an eviction under the index lock; the by-age test counts late groupswatch-together: the page makes its audio context before its join; a slot is set apart only when the machine ran nothing of the pageEach check still fails on the defect it was written to catch. No bound was widened and no retry was added.
1. The DVR window by age (
joule-moq-relay)What it asserted. The relay's store keeps groups for a 2 s window.
a_window_by_age_empties_as_its_groups_pass_itpublishes three groups on a media track and its receipts track, six groups in all, waits until the store holds none, and assertedgroups_evicted == 6. It then publishes a fourth group and fetches from group 0: what aged out must come back as one unknown range, and every group after it whole.What failed. On the linux-arm64 runner the assertion failed with 3 against 6. The run printed no other count.
The cause, as measured. The store takes a group only once it is whole: its objects and its receipts. A group that becomes whole after the window has already passed its number is late. The store counts it in
groups_lateand never stores it, by design. The relay reads each stream on its own task, so on a loaded host groups can become whole out of order. The test assumed an order the relay does not promise. On a build host other than the CI runner, forcing that order gives 3 groups stored and 3 late, and the old assertion fails with 3 against 6 in each run. The runner printed nogroups_late, so the claim that its three missing evictions were late groups comes from this reproduction. It was not read on the runner.A second defect turned up along the way. The store removed a group from its index under the index lock but counted the eviction after releasing it, so a reader that found the group gone could read the count one short. With 300 ms injected between the removal and the count, the in-order test failed with 5 against 6.
The fix.
groups_evicted + groups_late == 6), and every stored group is counted when it leaves (groups_evicted == groups_written). Each prints all the store's counts when it fails.a_window_by_age_passes_groups_taken_out_of_order, forces the loaded-host order and checks 3 stored and 3 late, then runs the same fetch checks.Falsified on that build host:
2. watch-together: a statement in every slot (co-watch §7.3)
What it asserted. Every slot wholly inside a page's presence holds a presentation statement from that page. Presence runs from the page's join to the slot before the last statement in the log, and for Ben's first stretch it ends at his deliberate hold.
What failed. The check failed in two runs on the macOS runner, at a host load average of about 50 and then about 30. Both times the first page in the browser, Ada's, made its first statement after its join, at 3.83 s and then 3.44 s on the room clock. It then made none in the slots from 4 s and 5 s, and in the second run from 6 s as well. During that time it was still entering the room and had not yet followed the log. It followed the log from 7.49 s and 8.49 s, which is 3.7 s and 5.0 s after its first statement. Ben's page took 1.2 s and 1.0 s for the same step. Ada's page was silent for at least 2.17 s and then at least 3.56 s. Nothing was refused, and the room never took the page for gone. What held the page on the runner was not measured.
What was measured.
The fix.
The audio context before the join. The page makes the composition's audio context before it joins the co-watch log, through
Compositor.openAudio()inenter(), so that hold is no longer inside the stretch in which the page must state. By the blank-page measurement on the CI host, that is about 0.2 s. The rest of the entry still runs after the join.A failed entry's compositor is closed. When an entry fails, the next entry closes its compositor (
Compositor.close()). Before, a second entry left two audio contexts open and two mix timers running.The statement loop records its stalls. Each gap of more than half a slot between two of the loop's passes is recorded on the room clock, with how long its wait and its signing took.
A narrower excuse in the example's check. An empty slot is set apart only if the machine ran nothing of the page in the slot's second half, which means all three of these hold:
Any other empty slot fails, and the check says why:
Pass or fail, the check prints each page's audio-context time, the loop's stalls and the heartbeat's silences.
What this does not show. It is not shown that the example now passes on the CI host. If what held Ada's page there was the page's own work on its thread, or the host running the page's other threads but not that one, the check still fails, with its heartbeat ticking. Its printout does not tell those two cases apart. It does show whether the page's thread alone or the whole page was held, and for how long.
Falsified on a laptop, with each fault put into a scratch copy of the room client:
Checked on this branch's tip
The two commits sit on this pull request's head as they were checked on main. The seven files they change are byte for byte the files checked there, and the branch merges onto main with no conflict. The tip, this pull request's changes included, was checked off the CI hosts:
cargo test -p joule-moq-relay --test dvrpasses all 10 tests on a Linux build host (Rust 1.98.0, this branch's toolchain), both by-age tests among them.node --testoverweb/room/*.test.mjsandweb/moq/*.test.mjspasses 77 of 77, andcowatch.test.mjs, run as the conformance workflow runs it, passes 15 of 15.build-web.shmade the room client from this tree, on the Linux build host.build.shbuilt and packed the archive from this tree. The readiness check passed from the unpacked archive in an empty directory:wai hello,wai verify, andwai dev --checkwith 7 of 7 checks.run-examples.sh browserpassed video-room, watch-together, messaging, remote-desktop and package-and-deliver, and watch-together alone passed 2 more runs: 3 of 3 in all, each with 12 of 12 checks. In each watch-together run:None of this ran on the CI hosts. Whether the watch-together example now passes on the macOS runner under load is for this pull request's next CI run to show.
Repository checks run locally
Section references, SPEC references, tests in CI, orphan sources, comparative prose and vanity metrics all pass, and so does the public-name check with the private list.
🤖 Generated with Claude Code
Step 5 of `a_gpu_span_is_a_sole_users_rail_or_an_exact_zero_or_no_figure` burns this process's GPU across a second child's exit and asserted matches!(refused, Err(GpuClientVanished { pid, .. }) if pid == second_pid) It failed on the macOS energy job four times since 2026-10-11 (jobs 21067, 36264, 75996, 76205), each time naming another process's pid. The meter was right each time; the assertion assumed the child's client is the only one that can vanish in that ~0.3 s span, and the build host is shared: Playwright's headless browsers, other test processes and mediaanalysisd open, close and reset GPU clients all the time (the same run's 1.5 s burn had already retried twice for two other pids). `apportion` checks this process's own clients first, then the others, each pass walking the start snapshot in order and naming the first client that is gone or went back. The registry lists clients oldest first, so any older client of another process's that vanished in the same span is named before the child's. Every such client is a true witness: its time over the span is lost, so `all_ns`, and the 99% sole-user test, cannot be decided. The step now proves what it is for, whatever else the host does: - the refusal is `GpuClientVanished`, never a figure; - the client it names was at the start under that pid and is gone or went back at the end (the check `until_no_client_vanished` already made, now one helper); - it is exactly the one the rule picks: this process's own vanished client first, else the first vanished in the start snapshot; - every client of the second child's is gone from the end snapshot; - with the host's other vanished clients taken out of the real start snapshot, the child's exit alone still refuses the span, naming the child; so the check cannot pass only because unrelated churn refused it. When another client was named it says which. The step is not retried: there is nothing to wait out, and a retry would hide a regression in the rule. The tie-break gets a unit test of its own over built snapshots (two other processes' clients vanish: the first at the start is named, in either order, gone or gone back alike; this process's own vanished client is named even when it comes last), and `GpuClientVanished`'s doc states it. The meter is unchanged. Verified on the build host (load 95-160): clippy -D warnings on jouleclaw-energy with `--features ioreport` and without; the CI step (`cargo test -p jouleclaw-energy --features ioreport`, the liveness line printed) passed; process_gpu passed 5 of 5 on the host as it is and 6 of 6 beside a churner opening an idle GPU client for 1.5 s every 2.5 s, which failed the old assertion 2 of 6. In one of those six the refusal named a churner's client, as in CI, and every check above held. Falsified: dropping a vanished other-process client instead of refusing fails step 5 (it gave GpuShared) and two unit tests; walking the start snapshot last-first fails the new unit test alone. Co-Authored-By: Claude Opus 5.5 <[email protected]>The macOS energy job's process_gpu test gave up when a GPU client vanished in six spans in a row: its checks needed a span in which no client of the shared host vanished, and on a host whose other sessions open and close GPU clients every second there may be none. Reproduced on the build host beside a churner of short-lived GPU clients of our own: at one every 0.75 s, 4 of 4 runs gave up; at one every 1.5 s each run took 2-4 tries. Two other failures had the same root: the oracle's sum over every client gave up when one went back inside its window (job 27254, the `.expect` on `oracle_times`), and fell below the counter's when a client came and went inside the span (job 48857, the every-client assertion). No span now needs to be free of churn. Each span of the counter's or the span meter's is bracketed by `ioreg` listings of the registry, read independently of the counter: before it, around its work and after it. `classify` sorts every client against them. A registry entry once destroyed never comes back and its ID is not reused, so: - a client two reads list is in every read between them, under one pid; anything else is a contradiction and fails the test: a client the end snapshot lacks while `ioreg` lists it after (step 5's own check, now on every span), one the start lacks while `ioreg` listed it before and after, one both snapshots hold that `ioreg` did not list around the work, one gone inside the work and back in the end snapshot; - a client's time goes down only where a queue of it closed, which the listings show as a fall, so each counter reading lies between the listings either side of it, or on one side of a fall across it; a reading the registry does not explain voids the span; - a client the counter saw vanish is corroborated if `ioreg` listed it under the same pid (gone) or showed its fall inside the span (gone back); one `ioreg` never listed voids the span; - a client only `ioreg` listed inside the span, or a fall it showed where the counter saw an advance, is GPU time the counter's snapshots cannot hold, counted as hidden, a lower bound; - this process's own clients must be clean, or the span is void. A void span is taken again after 0.5, 1, 2, 4, 8, 8 ... s, and none the registry vouches for in 90 s fails as `process-gpu INCONCLUSIVE`, with each reason's count: not a verdict, and not a pass. A refusal must be the registry's: a corroborated vanished client refuses the span, naming the client the meter's rule picks (`expected_refusal`, factored out of step 5), and with none the span is not refused. The checks then run on the view, the snapshots with the vanished clients taken out of both, which can only raise this process's share. `agrees_with_oracle` checks client by client. This process's GPU time equals `ioreg`'s exactly, as before. The clients whose time went down nowhere advanced at most the oracle's advance plus what the gap can hold (their lower bound is now the sorting's, per client). The meter's every-client time is the sum over the view's clients. The rail is checked against the test's own IOReport read on every span whose snapshots both read it current, shared ones included (it had been on sole-user spans only), and a figure's rail is the snapshots' own advance. A figure must be a sole user's share of the time the counter saw plus the time hidden from it. `oracle_times` and its give-up are gone. Steps 4c and 6 take the span meter's span through `finish_showing_gpu` (the previous commit) and check that it is the GPU counter's figure, or the CPU alone with the counter's reason, over the meter's own snapshots, where the test had picked a span out of six by the text of its reason. A corroborated refusal is accepted there as the CPU alone, `HwShunt`, `Partial`, with CPU energy. Step 5's span-meter span gives the counter's reason over its own snapshots. The idle spans of steps 3 and 4a, and 4c's CPU-only span, also assert that `ioreg` saw no GPU time of this process's inside them. After the liveness askers, the test waits, at most 3 s and never failing, for this process's own clients to settle. the_span_classifier_sorts_every_client drives `classify` over six built reads: clean; gone and gone back, with the registry vouching and without; made and destroyed inside the span (48857's shape); a fall inside the work that the counter saw as an advance (27254's); falls across each snapshot inside the span and outside it; under- and over-reads; this process's own client not clean; each contradiction and a pid mismatch; `expected_refusal`'s order; and the 99% gate with hidden time. standards-build.yml: the jouleclaw-energy step also greps `process-gpu: oracle-checked:`, which step 2 prints once its burn was checked against the oracle. MEASURED_ENERGY.md says what the test checks. Verified on the build host, at this commit's jouleclaw files: clippy -D warnings on jouleclaw-energy with `--features ioreport` and without; the macOS energy job's jouleclaw-energy step as standards-build.yml runs it (under `bash -eo pipefail`, both greps), 5 of 5, three of them beside a churner of short-lived GPU clients of our own every 0.75 s; and the process_gpu test 16 of 16, 4 quiet and 4 beside a churner every 3, 1.5 and 0.75 s, with no void span, no INCONCLUSIVE and no contradiction, every vanished client corroborated on the first attempt. A read-only walk of the registry the way the counter walks it, every 5 ms for 60 s beside a churner every 0.2 s (268 clients made and destroyed), found no walk lacking a client the walks either side of it held and no child iterator invalid at the end of a walk (phantom_walks 0, invalid_at_end 0, of 7266 walks): a contradiction is a fault in a read, not churn. Falsified, each failing as named: a start or end walk dropping a client, or giving it another pid, and a phantom client in both snapshots (contradictions); the counter skipping another process's vanished client, or giving a figure across a gone one ("the registry vouches for ... but the counter gave Ok"); refusing a client that did not vanish, or naming another than the rule's; this process's time and every client's time each 1 ns off; the end snapshot misstating when it read; the rail under-read by 10%, and a figure's rail not the snapshots' advance; the sole-user gate at 1%; the span meter's figure, or its reason, not the counter's over its own snapshots; GPU work in an idle span; the CI grep's line renamed; and the classifier's stretch rule, fit rule, fall placement, the per-pid oracle, the refusal order and the hidden-time gate, each broken in turn. Two persistent faults end INCONCLUSIVE, as designed: (c) a 10% under-read of another process's client in every snapshot, "process-gpu INCONCLUSIVE: 1.5 s burn: no span in 92 s (12 spans) could be checked against the oracle: a counter reading out of the registry's order ×12; ...", and (d) a phantom client in every start snapshot, "... a vanished client the registry never listed ×12; ...". Co-Authored-By: Claude Opus 5.5 <[email protected]>The macOS browser e2e job failed its cowatch scenario five times with "cowatch: both play from Ada's control: not within 60000 ms (last: false)" (jobs 67776, 70090, 73183, 76976, 77089). It is the take-for-gone chain b01681570 records for watch-together, with the stall in Ada's own entering: `begin()` posted nothing for longer than the room's 4 s silence limit on the loaded host, the room took her for gone, and the old page, entering again, sent an event signed before its new join, refused `time_reversed`, and stopped. This branch's page recovers (b01681570, 4cc9a3e1b), but the scenario still failed: its check that the service carried every event the pages posted required the service to have refused nothing, and the service counts each `not_present` a page taken for gone gets before it enters again. On the build host under load, 8 of 11 runs of the branch head failed on exactly that check, with {"not_present":1} or {"not_present":2}, every page having entered again and played. The check now takes the design at work for what it is. The service's refusals must all be `not_present`, as many as the pages count themselves taken for gone (`gone`); each page must have entered again as often (`rejoins`, at most three), had no other event refused (`post_refused`), and not stopped. The pages' counts are read either side of the service's until the two readings agree. That check and the closing one print each page's gone, rejoins, resigned and dropped. Each control's instant is now read from the room's log, as the room carried it: a page that entered again signs its controls again, no earlier than its new join plus the lead, so the instant can be later than the one first signed. The waits for each control use it, and the play's line reports it, with the instant first signed when it moved. Before, a seek moved that way could pass its wait before it applied, and the position check after it then failed. The scenario does not hold a page on purpose: the watch-together example does that every run, and this scenario stays the room as people use it. Co-Authored-By: Claude Opus 5.5 <[email protected]>Step 4's burner child burned for at most 60 s, its wall time capped there whatever it was asked, while steps 4b and 4c now each take spans again for up to 90 s (`BUDGET`) where the old test allowed six quick tries. A long run of void spans could outlast the child; its exit then read as a corroborated vanish, the view was no longer shared, and the test failed with messages that look like a meter defect ("a shared GPU gave Ok(..)", "the child did not contend", "the child's client is not in the start snapshot") instead of INCONCLUSIVE. - The child burns for the seconds it is asked, of wall time, and step 4 asks for 2 × `BUDGET` + 60 s, longer than 4a, 4b and 4c can take between them; step 5 still kills it. - Right after 4b's and 4c's spans, the test fails `process-gpu INCONCLUSIVE: ... the contending child exited (<status>) while its spans were taken` if the child is gone. - The child is held in `Burning`, which kills and reaps it on drop: std's `Child` does neither, and a failed check would otherwise leave a GPU burner on the shared host for the rest of its life. A child whose parent is gone (killed outright, dropping nothing) stops burning too. Co-Authored-By: Claude Opus 5.5 <[email protected]>WAI's WebAssembly legs ran two stock wasmtime versions: 49.0.2 in the quantum and face install steps, and 38.0.3 in wai-ops.yml and wai-vector.yml (wai-ops runs the wasm32-wasip2 decode determinism-tiers §5 cites). A stock wasmtime leaves NaN canonicalisation off and lets each relaxed SIMD instruction take the host's own behaviour, so the bits of a NaN a module reads back, and the result of a relaxed instruction, were the engine's choice. Every install step now pins wasmtime 50.0.1 by version and SHA-256 (the x86_64 Linux release archive, 685466455dd4...98a5, the digest the release lists), and every run passes -W nan-canonicalization=y -W relaxed-simd-deterministic=y wasmtime's two determinism settings: every NaN a float instruction makes is the canonical one, and each relaxed SIMD instruction has one behaviour on every host. The flag names are 50.0.1's own, read from that release's `wasmtime run -W help` on the build host. The two 38.0.3 steps gain the architecture check and the digest the others already had. Both settings took effect on the build host (arm64, the 50.0.1 release for that platform). f32.add of a NaN with payload 0x200001 and 1.0 gives 0x7fe00001 without the flags and the canonical 0x7fc00000 with them. The fixed relaxed behaviour is the engine's, not always the one WebAssembly's deterministic profile prescribes: f32x4.relaxed_madd(1+2^-12, 1+2^-12, -(1+2^-11)) gives the fused 2^-24 with and without the flag, where the profile's choice (parameter 0) is the unfused fadd(fmul), which gives 0. The workflows' comment says so. Co-Authored-By: Claude Opus 5.5 <[email protected]>The install steps' comment said every WAI workflow that runs wasmtime pins one version and passes both determinism settings, but nothing enforced it: a step that ran a stock or differently pinned wasmtime would have merged green. scripts/check-wasmtime-pin.py now fails, in the repository checks, any workflow that 1. downloads a wasmtime release other than 50.0.1, or checks it against any SHA-256 but that archive's; 2. installs wasmtime another way (cargo install, the install script, an action); 3. runs wasmtime (run, serve, compile, or a module given to it) or names it as a CARGO_TARGET_*_RUNNER without -W nan-canonicalization=y and -W relaxed-simd-deterministic=y. It also fails when it finds no download or no run, so a check that looked in the wrong place cannot pass. On this tree it finds the downloads and runs it should. Its self-test (scripts/check-wasmtime-pin-selftest.sh) passes a scratch workflow shaped like these and fails each way of breaking it that the self-test lists, matching the reason in the output. The comment's relaxed_madd claim was measured on arm64 only. It now rests on both architectures, measured on an arm64 macOS host and an x86_64 Linux host with a small probe (wasmtime 50.0.1's release for the host, checked against the digest the release lists): host Darwin arm64; wasmtime 50.0.1 (c307ec81d 2026-10-29) f32.add(bits 0x7fa00001, 1.0) [no flags]: 0x7fe00001 f32x4.relaxed_madd(1+2^-12, 1+2^-12, -(1+2^-11)) [no flags]: 0x33800000 f32.add(bits 0x7fa00001, 1.0) [both flags]: 0x7fc00000 f32x4.relaxed_madd(1+2^-12, 1+2^-12, -(1+2^-11)) [both flags]: 0x33800000 an unknown -W option is refused: error: failed to parse -W / --wasm option: unknown option: no-such-option host Linux x86_64; wasmtime 50.0.1 (c307ec81d 2026-10-29) (the same five lines, bit for bit) 0x33800000 is 2^-24, the fused result; the unfused fadd(fmul) gives 0. So the NaN setting takes effect on both, and relaxed_madd is fused on both with and without the relaxed setting. That matches 50.0.1's source (the wasmtime-internal-cranelift crate, read from the registry): the translator lowers F32x4/F64x2RelaxedMadd through fma_f32x4/fma_f64x2 (src/translate/code_translator.rs L2530-2539), which emit a native fma on a host that has one, a fused library call under relaxed-simd-deterministic on a host that has none, and the unfused fmul and fadd only on a host with no FMA and the setting off (src/func_environ.rs L5907-5925). The setting is what makes the result fused on every host. And since an unknown -W option stops wasmtime, a run that exits 0 with the two flags has parsed them. Co-Authored-By: Claude Opus 5.5 <[email protected]>conformance-results §4.1: a relying party that admits an implementation on a conform-result/1 record requires the record accepted, its run passed (not_conformant), its suite one whose corpus the party holds (suite_unknown), and its corpus digest that corpus's (corpus_mismatch). wai verify --conformant [--root <wai-dir>] applies the rules against a checkout; without it a record of a run that did not pass verifies as before. A sink kind: wai conform sink --suite native-sink gives a sink each session of live-conformance/native-sink/ with its answers withheld, and each configuration the corpus states a session refuses; wai_native_sink_vectors answers [--c-abi] is the reference sink, through its module or through the C interface the platform integrations link. wai-breaking.yml's runner job runs the native sink both ways and the score decoder, each record admitted against the checkout's corpus, and a planted sink that presents an object whose digest does not match: its run must exit 1 with tampered-fragment failed, and admission must refuse its record at rule 2. tests/cli_conform_sinks.rs holds the same, and corpus_mismatch and suite_unknown; it has its [[test]] entry with its required features, so check-tests-in-ci.py passes. The binaries are built with the test step's features, so cargo reuses what that step built. The browser sink has no sink suite, and §7 says that §4.1 has only this reference implementation, with no admission vectors. The catalogue's receipt.verify (the new options, and the envelopes the pin-by-default commit added) and conformance.run (the sink kind, the {case} placeholder) are exported again, and Studio's actions regenerated from the export: actions.mjs --check had failed since that commit. c_abi::take reads and frees no null buffer. Co-Authored-By: Claude Opus 5.5 <[email protected]>