Wave A: public-name check, WebAssembly determinism in CI, decode-path reporting, and pin by default #616
Merged
dcharlot
merged 29 commits from 2026-11-04 20:25:43 -05:00
wai/wave-a into main 2026-11-04
watch-together: the page makes its audio context before its join; a slot is set apart only when the machine ran nothing of the page
All checks were successful
wai-quantum docs, course and locales / every command and snippet in the docs · linux (pull_request) Successful in 21m22s
WAI mbt2018_mean integer decode (regenerated vectors) / regenerate mbt2018_mean integer vectors → Rust decode, required (pull_request) Successful in 7m27s
WAI Studio / Studio in Chromium and Firefox · macos (pull_request) Successful in 10m15s
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (pull_request) Successful in 11m59s
WAI HTTP delivery / HTTP corpus · falsify patches 1/3 (pull_request) Successful in 6m48s
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Successful in 6m44s
wai-web reach builds / wai-web reach builds · this tree's wai-web, every rung on its engine line (pull_request) Successful in 9m5s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 33m23s
WAI HTTP delivery / HTTP corpus · falsify patches 2/3 (pull_request) Successful in 7m11s
WAI Studio / Studio in WebKit · linux (pull_request) Successful in 18m7s
WAI HTTP delivery / HTTP corpus · verified both ways, and the differential (pull_request) Successful in 2m19s
WAI HTTP delivery / cache rules · controls and labelled caches (pull_request) Successful in 1m20s
wai-ops integer operators / linux-aarch64 (pull_request) Successful in 1m2s
wai-pack / wai-pack · linux-arm64 (pull_request) Successful in 1m45s
WAI HTTP delivery / HTTP corpus · falsify mutations 2/3 (pull_request) Successful in 32m36s
wai-pack / wai-pack · the floor through a media prober (pull_request) Successful in 32s
wai-pack / wai-pack · dependency licences (pull_request) Successful in 16s
wai-pack / wai-pack · independent verifier (Python) (pull_request) Successful in 7m5s
wai-quantum npu-cores member / member · npu-cores absent (linux-arm64) (pull_request) Successful in 27s
wai-quantum HIP member / member · HIP absent (linux-arm64) (pull_request) Successful in 58s
WAI HTTP delivery / HTTP corpus · falsify mutations 1/3 (pull_request) Successful in 39m55s
wai-quantum pulse and dynamics / the same bits · linux-arm64 (pull_request) Successful in 1m24s
WAI HTTP delivery / HTTP corpus · falsify mutations 0/3 (pull_request) Successful in 40m34s
wai-quantum tensor-engine member / member · tensor engine absent (linux-arm64) (pull_request) Successful in 20s
wai-quantum variational and algorithm library / parameters, gradients, optimisers, VQE, QAOA and builders · linux-arm64, release (pull_request) Successful in 2m40s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (pull_request) Successful in 2m39s
wai-quantum extension points / the registry and the example · linux-arm64 (pull_request) Successful in 4m1s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 11m3s
wai-vector vector graphics / linux-aarch64 (pull_request) Successful in 4m17s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Successful in 1h10m6s
The watch-together example failed twice on the macOS runner for #616, on a loaded host (load
average about 50, then about 30). Each time the first page in the browser, Ada's, signed its
first presentation statement after its join, at 3.83 s and then 3.44 s on the room clock, and
then none in the slots from 4 s and 5 s (and 6 s the second time), while it was still entering
(begin()): it followed the log from 7.49 s, then 8.49 s, 3.7 s and 5.0 s after its first
statement, where Ben's page took 1.2 s and 1.0 s. Ada's page was silent at least 2.17 s, then
at least 3.56 s, with nothing refused, and was never taken for gone. What held it on the runner
was not measured.
What was measured:
- On the CI host, with CI jobs running (load average 30 to 39), in a browser launched as the
example's test launches it (headless, the same flags, at nice 10), on a blank page with
nothing else in it: the first audio context in a fresh browser held its page's thread 172 to
192 ms (3 runs), for the constructor call alone; a second page's took 3.8 to 4.0 ms; neither
page's thread had a gap over 60 ms in the 1.5 s after. That is a tenth or less of Ada's
silences. Moving the context before the join takes about 0.2 s, as measured there, out of the
stretch in which the page must state; what held Ada's page for the rest is unexplained. The
rest of begin() (its audio worklet, its relay session, its WebAssembly, its first draws, the
camera and microphone) was not measured there, nor an audio context made beside that work.
- On a laptop, in 25 runs of the example: the first page's audio context 94 to 128 ms, the
second's 0.8 to 1.4 ms.
- On a laptop, on main, two injected faults each leave empty the slots just after the first
page's first statement, as in CI: the audio-context call held 2.5 s (a busy wait in it; the
slots from 1 s and 2 s, 1 run), and the first page's renderer process stopped 2.5 s from its
join (SIGSTOP; the slot from 2 s, 2 of 2 runs). Neither was measured on the CI host.
This change is not shown to make the example pass 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 and not
that one, the check below still fails (its heartbeat ticking), and what it prints does not tell
those two apart.
1. The page makes the composition's audio context before it enters the co-watch log (app.js
enter(), the new Compositor.openAudio()), since from that join it states in every slot
(co-watch §7.3), so that hold is not in the stretch in which it must state. The rest of the
entry (begin()) still runs after the join. begin() takes the compositor the entry made; in
any other room it makes one, as before. The snapshot carries when the context was made, on
the room clock, and how long making it held the thread (cowatch.audio_open).
2. A compositor made for an entry that failed is closed when the next entry makes its own
(Compositor.close(): its mix timer, its worklet node, its drawing, its frames and its audio
context). Before, it stayed, its audio context open and its mix timer running.
3. The statement loop records its stalls (cowatch.js run(), snapshot().stalls): each gap over
half a slot between two of its passes, `{ from, to }` on the page's estimate of the room
clock, with how long its wait and its signing took in it, on that clock (`timer_us`,
`signing_us`); the rest of a stall is the loop's own work. It states every half slot while
it runs, so it leaves a slot empty only inside such a stall. Its wait is a method (`wait`),
and roomTime(ms) gives the room clock at an instant of the page's clock. The snapshot
carries the loop's wait (`pass_ms`) and the page's clock beside the room clock (`wall_ms`).
The co-watch rule and §7.3 are unchanged.
4. The example's slot check (co-watch §7.3: a statement in every slot while present) sets an
empty slot apart only if the machine ran nothing of the page in the slot's second half: a
stall of the loop spans that half; a heartbeat worker that the test starts in each page,
ticking every 20 ms, did not tick in it either (to within a pass at each end); and the stall
is the loop's wait and its signing but for at most one pass of its own work. Otherwise the
check fails and says why: no stall of the loop (a statement lost); the heartbeat ticking (the
page's thread did not run its loop while the worker ran: the page's own work holding the
thread, or the host running the worker and not the page's thread, which the check does not
tell apart); or more than a pass of the loop's own work (its own code, an await outside its
measures, or the thread taken off the processor inside it). On a host that runs the page's
threads unevenly, the check can fail in these ways with no defect in the page. Pass or fail,
it prints each page's audio-context time, its loop's stalls and its heartbeat's silences over
100 ms. They show whether the page's thread or the whole page was held, not what held it.
Unit tests (cowatch.test.mjs) run the loop on a room clock the test moves: no stall and every
slot held while it runs; a thread held while the loop waited, recorded once with all of the
stall its wait, and the only empty slots those it spans; a gap just over half a slot recorded
and one of half a slot not; a stall in a pass's own work recorded with a wait of one pass, the
rest neither wait nor signing.
Verified on a laptop with this change on main (152aab5c7), in a browser launched as the
example's test launches it and niced:
- From an archive build.sh made, with the WebAssembly build-web.sh made from crates this change
does not touch, run-examples.sh browser passes video-room, watch-together, messaging,
remote-desktop and package-and-deliver in each of 2 runs, and watch-together alone 2 more.
- With this room client served beside an earlier archive's binaries (the same Rust but for the
relay's DVR change in the commit before), watch-together passes 3 of 3, and the faults below
ran on that set. In each passing run the first page's audio context was made before its join
and Ada's loop had no stall. Ben's deliberate hold, a synchronous request, shows as a stall of
3.8 to 4.2 s with his heartbeat never silent.
- The room client's unit tests pass under node --test (77).
- Falsified, each fault put into a scratch copy of the room client, 2 runs each unless said:
the audio context made in begin() again and that call held 2.5 s (a busy wait), the check
fails: no statement in the slots from 1 s and 2 s (Ada) and 4 s and 5 s (Ben), each in a
stall of the loop with its heartbeat ticking, the context printed as made after the join in
2.5 to 2.6 s. A 2.5 s busy wait just after the join, or 4 s after it, fails it the same way:
the slots it spans, with the heartbeat ticking. Every statement of the third whole slot after
each join dropped fails it (no stall of its loop). One 1.5 s await in the loop's own work
fails it (heartbeat ticking, 1.5 s neither its wait nor its signing); the same await inside
the loop's wait fails it on the heartbeat alone. The page's renderer process stopped 2.5 s
(SIGSTOP) at Ada's join, or 2.5 s after it, or Ben's 1 s after his (1 run): it passes, the
slots inside the stop set apart; the heartbeat was silent and the loop stalled over the stop,
to within 25 ms. The stop at Ada's join and a 2.5 s busy wait 4 s after it (1 run): the
stop's slot is set apart and the busy wait's fail.
- In the unit tests, the wait measured on another clock than the stall, no stall recorded, a
stall recorded only past a whole slot, and the wait left out of the record each fail one or
two of the stall tests.
- A video room: one page publishes; the other presses Watch, its first relay connection is
refused (a fault the probe injects), and it presses Watch again. Then that page holds one
audio context open (the first closed) and one mix timer, and mixes 50 blocks of 20 ms a
second. On main it holds two open and two timers; with close() doing nothing, two open. The
same in a watch room: one open with this change, two on main.
- Not shown: that the example passes on the CI host.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
joule-moq-relay DVR: count an eviction under the index lock; the by-age test counts late groups
Under load the relay can take a group whole only after the age window has passed its number, because its streams finish out of order. Such a group is late: it is never stored, and it is counted in groups_late. The by-age test expected six evictions whatever the order, and failed 3 against 6 when three groups came late. It now checks that every group is evicted or late, and that each group stored is counted as it goes. A second run makes the out-of-order case happen on purpose (group 0 and group 2's receipts, then, once the window has passed group 2, group 1 and group 2's objects) and checks three stored, three late. The store also counted an eviction after releasing the index lock, so a reader that found the group gone could read the count one short (5 against 6 with a delay injected there). The count now moves under the same lock as the removal. Co-Authored-By: Claude Opus 5.5 <[email protected]>
wai-web-wasm: the native unit test count includes the decoder entry's set test
All checks were successful
WAI Studio / Studio in Chromium and Firefox · macos (pull_request) Successful in 10m48s
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Successful in 6m49s
WAI mbt2018_mean integer decode (regenerated vectors) / regenerate mbt2018_mean integer vectors → Rust decode, required (pull_request) Successful in 13m1s
WAI Studio / Studio in WebKit · linux (pull_request) Successful in 11m54s
wai-vector vector graphics / independent checker (Python) (pull_request) Successful in 12m11s
WAI HTTP delivery / HTTP corpus · falsify mutations 0/3 (pull_request) Successful in 33m18s
wai-quantum docs, course and locales / every command and snippet in the docs · linux (pull_request) Successful in 38m48s
WAI HTTP delivery / HTTP corpus · falsify patches 0/3 (pull_request) Successful in 5m24s
WAI HTTP delivery / HTTP corpus · falsify patches 1/3 (pull_request) Successful in 6m18s
WAI HTTP delivery / HTTP corpus · falsify patches 2/3 (pull_request) Successful in 5m51s
WAI HTTP delivery / HTTP corpus · verified both ways, and the differential (pull_request) Successful in 2m8s
WAI HTTP delivery / cache rules · controls and labelled caches (pull_request) Successful in 1m32s
WAI HTTP delivery / HTTP corpus · falsify mutations 1/3 (pull_request) Successful in 39m58s
WAI examples / WAI examples · android-app · linux-arm64 (pull_request) Successful in 6m0s
wai-ops integer operators / linux-aarch64 (pull_request) Successful in 2m3s
wai-pack / wai-pack · linux-arm64 (pull_request) Successful in 2m34s
WAI HTTP delivery / HTTP corpus · falsify mutations 2/3 (pull_request) Successful in 40m14s
wai-pack / wai-pack · the floor through a media prober (pull_request) Successful in 52s
wai-pack / wai-pack · dependency licences (pull_request) Successful in 37s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 12m10s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Successful in 1h8m39s
wai-quantum HIP member / member · HIP absent (linux-arm64) (pull_request) Successful in 1m11s
wai-quantum npu-cores member / member · npu-cores absent (linux-arm64) (pull_request) Successful in 54s
wai-quantum tensor-engine member / member · tensor engine absent (linux-arm64) (pull_request) Successful in 37s
wai-quantum pulse and dynamics / the same bits · linux-arm64 (pull_request) Successful in 2m2s
wai-quantum extension points / the registry and the example · linux-arm64 (pull_request) Successful in 3m2s
wai-quantum variational and algorithm library / parameters, gradients, optimisers, VQE, QAOA and builders · linux-arm64, release (pull_request) Successful in 3m0s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (pull_request) Successful in 2m50s
wai-vector vector graphics / linux-aarch64 (pull_request) Successful in 2m47s
wai-pack / wai-pack · independent verifier (Python) (pull_request) Successful in 10m6s
Pin by default added measure::tests::a_decoder_entry_names_the_set_its_decode_read, so the native unit tests step's count check must expect it; the step otherwise fails after every test has passed. Co-Authored-By: Claude Opus 5.5 <[email protected]>
claim-conformance: an unpinned decode's entry names the set it read, and one vector keeps the earlier text's null
Some checks failed
wai-quantum views in agent hosts / views · a scripted host (pull_request) Successful in 7m30s
WAI Studio / Studio in Chromium and Firefox · macos (pull_request) Successful in 9m42s
wai-quantum signal processor / processor and member · signal processor, emulated (pull_request) Successful in 15m43s
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Failing after 5m42s
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (pull_request) Successful in 14m44s
WAI HTTP delivery / HTTP corpus · falsify patches 0/3 (pull_request) Successful in 9m39s
wai-web reach builds / wai-web reach builds · this tree's wai-web, every rung on its engine line (pull_request) Successful in 12m11s
WAI HTTP delivery / HTTP corpus · falsify patches 1/3 (pull_request) Successful in 10m18s
WAI HTTP delivery / HTTP corpus · falsify mutations 0/3 (pull_request) Successful in 31m16s
WAI Studio / Studio in WebKit · linux (pull_request) Successful in 25m3s
WAI HTTP delivery / HTTP corpus · falsify mutations 1/3 (pull_request) Successful in 30m52s
WAI HTTP delivery / HTTP corpus · verified both ways, and the differential (pull_request) Successful in 1m59s
WAI HTTP delivery / cache rules · controls and labelled caches (pull_request) Successful in 1m12s
wai-ops integer operators / linux-aarch64 (pull_request) Successful in 1m2s
WAI HTTP delivery / HTTP corpus · falsify mutations 2/3 (pull_request) Successful in 31m3s
wai-pack / wai-pack · linux-arm64 (pull_request) Successful in 1m35s
wai-pack / wai-pack · the floor through a media prober (pull_request) Successful in 17s
wai-pack / wai-pack · dependency licences (pull_request) Successful in 14s
wai-quantum HIP member / member · HIP absent (linux-arm64) (pull_request) Successful in 50s
WAI examples / WAI examples · android-app · linux-arm64 (pull_request) Successful in 4m44s
wai-quantum npu-cores member / member · npu-cores absent (linux-arm64) (pull_request) Successful in 23s
WAI HTTP delivery / HTTP corpus · falsify patches 2/3 (pull_request) Successful in 9m36s
wai-quantum tensor-engine member / member · tensor engine absent (linux-arm64) (pull_request) Successful in 26s
wai-quantum pulse and dynamics / the same bits · linux-arm64 (pull_request) Successful in 1m17s
wai-quantum extension points / the registry and the example · linux-arm64 (pull_request) Successful in 2m12s
wai-quantum variational and algorithm library / parameters, gradients, optimisers, VQE, QAOA and builders · linux-arm64, release (pull_request) Successful in 2m30s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (pull_request) Successful in 2m15s
wai-pack / wai-pack · independent verifier (Python) (pull_request) Successful in 6m30s
wai-vector vector graphics / linux-aarch64 (pull_request) Successful in 2m21s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Has been cancelled
Two decode-path vectors stated an unpinned NeuralIntegerExact decode with pin and tier null, the form of the earlier text. Under SPEC §3.1, Pin by default, rule 4 and jwp-receipts §2.4.12, a sink names the set it read, by the digest a pin over it names, with the tier none, and null means a decode that read no set or an entry from a sink of the earlier text. session_variant_not_exact_unpinned and session_stage_hash_over_an_unpinned_base now write the entry so, and each is still refused for its own reason: rule 13, and a stage hash over a base whose tier is not decode-equivalence. A vector appended after them, session_stage_hash_over_an_unpinned_base_earlier_text, keeps the null form, which a verifier cannot tell from a decode that read no set, and is refused all the same. The vectors before the decode-path rules are unchanged, as the test that pins them by digest requires. differential.py reaches the tier none too: in its decode-path sweep, in the mutation that puts a path under a pin, and where its stage-bounds sweep unpins the base's entry, which now writes either form. Co-Authored-By: Claude Opus 5.5 <[email protected]>
determinism-tiers §1 and §7.1: each published route described on its own
Two passages followed a published route's limitation with a sentence about WAI's integer networks: in §1, an integerised video codec's accumulators, whose overflow was observed not to happen rather than bounded; in §7.1, a requantisation that was not bit-exact across neural processors. Each passage now describes the route alone. integer-operators §8 and §5.1 specify WAI's bounds and rounding. Co-Authored-By: Claude Opus 5.5 <[email protected]>
CHANGELOG: the pin-by-default entry acknowledges the staged-measure goldens' break
wai breaking reports staged-measure-conformance/goldens.json as a wire break: the unpinned fixture's evaluation was added, and the corpus-wide dedup record, taken over every fixture's objects, counts its objects too, so its totals moved. The entry named no break for it. It now acknowledges it, and says that no other fixture's evaluation changes. Co-Authored-By: Claude Opus 5.5 <[email protected]>
wai: an honest session's base entry names the pin it verified, so its stage hash seals
every_corpus_case_seals_an_honest_session replays each staged case as a sink and records its session, but its decoder entries named no pin and no tier for any base. Since determinism-tiers §3 rule 3, a stage hash over a NeuralIntegerExact base is refused unless every entry naming the base is at decode-equivalence, so the honest sessions of the pinned int_hyper, int_codec and splat cases were refused with session_stage_hash_not_exact. A base's entry now names what the sink's decode states (jwp-receipts §2.4.12): the pin it verified, with the tier that pin declares; for an unpinned decode that read a set, the digest a pin over the bytes read names, with the tier none (SPEC §3.1, Pin by default, as IntParams::read_pin names it); nothing for a decode that read no set. Layer entries name nothing, as before. Co-Authored-By: Claude Opus 5.5 <[email protected]>
wai conform sink: the native sink through the conformance runner, and wai verify --conformant admits a result only for a run that passed
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]>
wai: the page's recorder names the set an unpinned decode read, and a measured title over an unpinned base holds both recorders to it
The browser recorder (wai-web's SessionHandle.decoderFor) still wrote pin and tier null for every decode that verified no pin, so for one unpinned decode the page and the native tool sealed different claims, against SPEC §3.1 rule 4 and jwp-receipts §2.4.12. Every committed fixture pinned its base, which hid it. decoderFor now takes the set the decode read, a trailing argument, apart from whether it verified a pin; StageBase gives both (paramSet, pinned) from staged::decode_base_pinned. A decode that verified no pin is named by the set it read, with the tier none, as stage_measure's recorder names it; a pinned decode given a set other than its pin's is refused. measure_verify.py names the set as the text does: the SHA-256 of a one-file capability's file, otherwise of the wai.prior.set listing of the files read. staged-measure-conformance/fixtures/unpinned is a title over the staged corpus's unpinned wai.neural.int_hyper base (image_int_hyper_unpinned), decoded against the same files the still's pin names, so its decoder entry names that digest; goldens.json regenerated with wai_stage_measure. The parity test and verify_measure_wasm.mjs pass the verified-pin flag apart from the triple's pin, so both recorders and both verifiers are compared on it, and verify_staged_wasm.mjs holds StageBase.pinned and paramSet to every staged case. jwp-receipts §2.4.12 says that an entry from a sink of the earlier text may carry null for an unpinned decode that read a set, which a verifier cannot tell from one that read none. SPEC §3.1 rule 2 says that an unpinned envelope conforms unless §7 requires the pin (replicate_unpinned). DecoderEntry::pin says what it now names. Co-Authored-By: Claude Opus 5.5 <[email protected]>
SPEC §3.1: pin by default; wai verify reports an unpinned envelope, and a decode's record names the set it read
An encoder SHOULD pin the parameter set a payload was coded against wherever a capability that decodes it is registered as decoding against one, whatever the intent (SPEC §3.1, Pin by default). An envelope that pins nothing there still conforms in 1.x; a verifier reports it (prior_unpinned), informatively: Wai::check_unpinned, WaiMulti::check_unpinned and codecs::pin_by_default. wai verify now reads the envelopes under each path, refusing one whose pin breaks SPEC §3.1 and noting prior_unpinned; wai inspect and wai wrap note it too. pin-conformance/unpinned.json holds the vectors, reached by wai_pin_vectors and by pin_verify.py, which reads the classes from the published registry export. A reconstruct-session decoder entry's pin (jwp-receipts §2.4.12) now names the set an unpinned decode read, by the digest a pin over those bytes names, with the tier none. The reference sink's base decode gives it (staged::BaseDecode::set, from IntParams::read_pin), and stage_measure's sessions carry it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
claim-conformance: a decode-path sweep in the differential, and stage hashes over stage sets that pass
The previous change did not reach either rule: a planted regression of rule 14 to `float` alone still left the differential with nothing to report, since the random sessions are refused by an earlier rule first. session_decode_paths now sweeps one picture through one entry over five capabilities (classified at each tier, or not at all), seven terms for the path, three pins and both kinds of decoder, where nothing else decides, so rules 6, 13 and 14 each give the verdict. session_stage_bounds, whose sessions pass every earlier rule, states a stage hash a third of the time, and then half the time unpins the base's entry or names another path on an entry. Co-Authored-By: Claude Opus 5.5 <[email protected]>
claim-conformance: the differential reaches rule 14 and the stage-hash refusal
In the sessions CI's run of the differential generates (seed 1) neither session_tier_exceeds_variant nor session_stage_hash_not_exact was the verdict once. An entry naming a stage set's base almost never had the unit the session names the set at, so session_first_unit_mismatch came first, and a path that is not exact under a pin of decode-equivalence rarely met a capability with no class. Entries naming the base now take that unit, as an honest sink states it, four times in five, and the path defect moves its entry to another capability more often, an unclassified one half the time. Co-Authored-By: Claude Opus 5.5 <[email protected]>
CHANGELOG: the decode-path entry names its breaks, and the research notes record the path
Reason gains three variants and has no #[non_exhaustive], so the API check counts them as a break, and the entry named none. It now acknowledges them, the five neural dispatch functions' new return type (the neural feature is outside the checked API, so the check does not report it), and wai_decoded_t's two new fields. The entry also states the exact path, the per-object stage-hash check, where each decode-path case runs, and that §1 records the five routes while §7.1 cites the two float-decode records. syfai-input's G11 and the September sweep's G9 said the result does not record which path ran. Each gains a dated status note pointing to determinism-tiers §5 and neural::Dispatched::path. Co-Authored-By: Claude Opus 5.5 <[email protected]>
neural: the binding check reads the descriptor, the C ABI reports the path, and CI runs the exact decoder
check_reconstruction_binding compared only the hash, so a binding of another decoderId, format or size than the exact decoder computes was reported as Differs. reconstruction-binding §2 forbids reporting a binding as failed when the verifier decoded some other way than its descriptor says. Such a binding is now BindingCheck::OtherReconstruction, with the path that ran; for the exact decoder that is any decoderId but 2. wai_neural_decode_ex dropped the path and the capability, so a native sink could not state its decoder entry's variant. wai_decoded_t gains `path` (WAI_DECODE_PATH_SOLE, _EXACT or _FLOAT) and `fallback`, 1 when a WAI1 envelope decoded through its fallback. include/wai.h is regenerated. The neural gates counted the two JPEG-AI cases' skips as passes, so CI ran only the one-path case. That step now selects its two committed-input cases by name, one of them a binding over int_mlicpp checked under its descriptor. A new job in the 24.04 image fetches the pinned jpegai/v1 bundle and runs the exact decoder's case with WAI_REQUIRE_SOTA_VECTORS=1, requiring it by name and refusing a skip. The float decode needs the reference-software assets, which no job holds, so its case runs locally only; the test header and determinism-tiers §5 say so. The C ABI step runs the path and fallback test. Co-Authored-By: Claude Opus 5.5 <[email protected]>
jwp-receipts §2.4.12: one exact path for rules 13 and 14 and the stage hash, checked per object
Rule 14 refused only `float` under a pin of decode-equivalence, while rule 13 and the stage-hash check read any variant but null or exact as not exact. Rules 6 and 13 already refuse such a pin for every class the registry gives, so rule 14 reaches only capabilities with no class, and there `fp16` passed. The three now share one predicate, DecoderEntry::exact_path: null, `exact`, or, for a NeuralIntegerExact capability, the lowering its registration names. The last is how opus-dred §7.2's present entry names wai.audio.opus.dred.int's integer path, `dred-int/1`, which rule 13 would otherwise have refused; opus-dred says a session claim of that decode may name it so. The stage-hash refusal is per object: an entry naming the base at a weaker step counts whichever units it presented, so the outcome does not depend on whether the verifier holds the outputs. determinism-tiers §3 rule 3 now says so. The vector session_stage_hash_not_exact moves its fallback decode inside the set, at unit 2, so it is refused under either reading, and the earlier form stays as session_stage_hash_not_exact_outside_the_set, the per-object case. determinism-tiers §3 calls the closed vocabulary a pin's, says rule 3 governs decoded media while generation-equivalence carries its own digest, and says rule 2 is held over the registry class. The README says rule 6 enforces it, with an inter-frame capability's vector, and rule 14 is its per-path complement. Five vectors are appended after the earlier ones, which a test pins by digest: a registered lowering accepted, another capability's lowering refused, `fp16` under decode-equivalence refused, an inter-frame capability over its class refused, and the per-object case. Co-Authored-By: Claude Opus 5.5 <[email protected]>
neural: a decode reports the capability it decoded through and the path that ran
wai.image.jpegai decodes through its exact decoder or through a float decode, and both returned the same result, so neither a receipt nor a binding check could tell which had run. decode_envelope, decode_envelope_strict, decode_multi, decode_multi_strict and decode_payload now return a Dispatched: the output, the capability the payload decoded through (its own or the fallback) and the path, DecodePath::Sole, Exact or Float. DecodePath::variant gives the term a session claim's decoder entry states. check_reconstruction_binding reads the path: over a decode-equivalence capability or the exact decoder it reports whether the hash holds, and over the float decode it reports the binding as not checked, never as failed, as reconstruction-binding §2 requires. The notes in determinism-tiers §5 and reconstruction-binding §2 that the dispatch did not report the path are gone. tests/jpegai_decode_path.rs runs the float path and requires the result, the session claim and the binding check to say so, and the exact decoder likewise. Its JPEG-AI cases need inputs outside the repository and skip without them; the case with one path runs in the neural gates. Co-Authored-By: Claude Opus 5.5 <[email protected]>
determinism-tiers §3: no tier lies between the two, and a session claim's decode path has three refusals
A reconstruction kept within a bound of the exact one between points at which it is made exact again is measured on named devices and a named corpus, and a sink cannot check it from what it received. So §3 says, normatively, that no tier, class or claim states such a bound, and three rules follow: a decode of a NeuralIntegerExact capability in reduced precision or float is not a decode of it; a capability whose payload holds several frame types takes the class of its weakest; and a clip digest, a stage hash or a binding over a sequence covers only frames each decoded at decode-equivalence. SPEC §7 points to it. A reconstruct-session decoder entry's `variant` now names the path that ran: `exact`, `float`, or null for a capability with one path. Rule 13 refuses a NeuralIntegerExact capability named with another path (session_variant_not_exact); rule 14 refuses a float path under a pin of decode-equivalence (session_tier_exceeds_variant); and against the stage claims, a stage hash over a base an entry decoded at a weaker step, or over a base or layer decoded by a path that is not exact, is refused (session_stage_hash_not_exact). claim-conformance gains a vector for each refusal and accepted ones for both of wai.image.jpegai's paths, appended after the earlier vectors, which a test pins by digest. claim_verify.py reads the same rules, and differential.py generates sessions around them. Co-Authored-By: Claude Opus 5.5 <[email protected]>
determinism-tiers §1 and §7.1: the published routes as their bodies state them, and a catalogue-prior record
§1 now records five published routes to a reproducible stage, each from the paper section read. The explicit-parameter route keeps its entropy stage and lets the reconstruction drift: its body says so, and its periodic I-frames re-anchor the picture to one float decode (§3.2, Appendix B.8). Two integer routes join the quantised decoder: a float model run in a 16-bit integer mode, whose accumulators were observed not to overflow (§4.5), and an integer-trained codec (abstract only). One more line reaches no tier: float synchronised on a tested hardware class, whose streams decode only within the GPU groups its tests found (Table IV, §III-B). §7.1 cites the two records of float decode across devices beside WAI's record, and is not a comparison. syfai-input §5 and the September sweep's row of arXiv 2608.00483, G7 and F5 are corrected against the bodies read on 2026-11-03. research/catalogue-prior.md records a linear basis learned on openly licensed films of one studio, measured against AV1: its readings, procedure, decision rule and the time the rule was fixed. The rule refutes a linear or spectral catalogue basis and exact recurrence across titles for a studio's catalogue; a learned prior is not measured. tools/wai_catalogue_prior_verify.py recomputes every BD-rate and verdict from the readings and checks every figure the page quotes, in the conformance workflow. Co-Authored-By: Claude Opus 5.5 <[email protected]>
byte-equality: the disk check writes as well as measures, and covers the conformance matrix too
The disk pre-check read df under the leg's four directories (the workspace, the runner's temporary directory, TMPDIR and HOME). Three gaps remained. 1. On linux-aarch64 df reads the runner VM's disk, not the host's. Run in a job container started as the runner starts one (its image and its two cache mounts), the step's loop put all four directories on the VM's data disk (84 GiB free), while the host volume that holds the disk image had 50 GiB. The image is a 200 GiB file that was already allocated on the host but for 0.7 GiB, so a full host volume could take back at most that much. The step now also writes and syncs 32 MiB once on each filesystem it measures (dd conv=fsync, which the macOS host's dd and the job image's both take), and names a write refused for space, an I/O error, a read-only filesystem or a quota infrastructure. That also catches a disk out of inodes, which df's block count does not show. Another failed write is a warning, not a verdict. 2. The second determinism matrix, wai-conformance.yml's byte-exact job, runs on the same three labels with no disk check. It now has both steps, with the same floor. 3. The floor had no reading behind it. The step's loop, run on 2026-11-04 in each label's job environment: linux-x86_64 201 GiB (one host) and 304 GiB (the other); linux-aarch64 84 GiB; macos 78 GiB under the workspace and the runner's temporary directory, 85 GiB under TMPDIR and HOME. The floor stays at 4 GiB: it fails a leg only on a host within a few GiB of full, and the macOS runner's internal disk, where HOME and TMPDIR sit, has gone from 7 GiB free to 118 MiB within hours, with its jobs failing under HOME. The env comment says so. The class of the failure is still not in the status context: the forge reports `wai cross-architecture byte-equality / <label>` as failed for a full disk as for a divergence. The comment now says so, and why the check is not a job of its own per label: the `linux` label has two hosts, so such a job could measure another host's disk than the leg's. Co-Authored-By: Claude Opus 5.5 <[email protected]>
determinism-tiers §5: CI validates the browser packages; the READMEs run CI's wasmtime
§5 said the browser crate's SIMD validation was done by hand, "which CI does not check". Since "wai-web-wasm: the packages validate with relaxed SIMD and threads off", wai-web-wasm.yml validates both bundles and reach/record/wai_web_bg.wasm with relaxed SIMD and threads disabled. §5 now says so, and says what CI still does not check: that they hold no other SIMD instruction. The changelog gains the CI entry for this branch. The commands that reproduce CI's wasmtime legs ran a stock wasmtime: the WAI Quantum READMEs (English and Chinese), the bit_identity test's header, the WAI README's edge example, and the conformance READMEs (quantum, qasm3, formats, answers, intents). Each now passes -W nan-canonicalization=y -W relaxed-simd-deterministic=y, as CI does. The determinism-cost run scripts under wai/research keep their commands: they record runs already made. The quantum docs check could not see that drift. For a `norun=ci:` block it stripped every leading VAR=value before matching the command against the workflow, and its pattern, \S*, did not read a quoted value. It now reads a quoted or bare value, and when the workflow sets VAR (as an env: key or in a script) the documented value must be one the workflow gives it, so CARGO_TARGET_WASM32_WASIP2_RUNNER=wasmtime is refused against a workflow that sets it to the flagged runner. Co-Authored-By: Claude Opus 5.5 <[email protected]>
CI: a check holds every workflow to the one pinned wasmtime and both its settings
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]>
wai-byte-equality: a full runner disk fails as infrastructure, before the checkout
Main's run 17676 at 2bef11f71 failed on both arm64 legs with "No space left
on device": linux-aarch64 in the checkout's `git fetch`, and macOS in the
toolchain's temporary files. Both runners live on one host, whose disk was
full. No fixture was decoded, yet the cross-architecture matrix read as red.
Each leg now begins, before the checkout, with a step named
"infrastructure: this runner's disk has room for the leg". It measures the
free space under the workspace, the runner's temporary directory, TMPDIR and
HOME. Below a floor of 4 GiB it fails with an error that begins
"infrastructure:" and says that no fixture was decoded. The leg itself writes
under 1 GiB (measured on the build host: about 270 MiB of working tree and a
67 MiB release target), and a cold toolchain install about 1 GiB more.
A disk can also fill while the leg runs, from the jobs that share it. A
second step runs only when a step after the check failed. It measures again,
and names the failure infrastructure when the disk is below the floor, or
says that the failure is not the disk.
Both steps are bash and skip on Windows, as the matrix comment says.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
wai-web-wasm: the packages validate with relaxed SIMD and threads off
determinism-tiers §5 says a WebAssembly decoder at decode-equivalence MUST NOT use relaxed SIMD, and that the browser crate was validated without it by hand, "which CI does not check". CI now checks it. wasm-tools 1.250.0, pinned by version and SHA-256, validates the node bundle and the platform_decode bundle the job builds, and the package of record the site serves (reach/record/wai_web_bg.wasm), with --features=-relaxed-simd,-threads. A relaxed SIMD instruction, a shared memory or an atomic in any of them fails the job. Before the packages, the step shows that it can fail. A planted module with i8x16.relaxed_swizzle and one with a shared memory each validate with the default features, and each is refused with them off for its own feature's reason (the error text is matched). Co-Authored-By: Claude Opus 5.5 <[email protected]>
CI: one pinned wasmtime, run with canonical NaNs and deterministic relaxed SIMD
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]>
sink-energy §4.6: what the third point's tool read, and what its paper leaves unstated
Point 3 said its energy was read by the processor's own counter through a software tool. The paper's tool reads the processor's counter and the graphics processor's, and the paper states neither whether the idle energy is subtracted nor whether Table 4's savings are the measured or the predicted energy of the configurations chosen; the point now says so. Point 2's classifier chooses per rate-factor setting, of which Table V covers four, not per quantisation setting. Co-Authored-By: Claude Opus 5.5 <[email protected]>
check-public-names: the maintainers' product names are checked on every pull request
The private list is a secret, given only to the run on a push to main, so a pull request that named one of the maintainers' products was caught only after it landed. Those names need no secret: --maintainer-products-only applies MAINTAINER_PRODUCTS alone and reads no private list, and the repository checks run it on every change. The default still requires the private list and fails closed without it. The script's header and CONTRIBUTING now say which products the script lists: those the maintainers' public site names. A product not yet announced is named on no site, so the private list holds it until the day it is announced, when it moves to the script. The self-test gains cases for the new mode: a product is a hit, a clean file is clean, the private list is not read, and a missing file fails closed. Two paragraphs are rewrapped to their file's width. Co-Authored-By: Claude Opus 5.5 <[email protected]>
sink-energy §4.6: outside measurements of decode energy, each cited by arXiv identifier and table
A hardware decoder against software decoders of the same format (arXiv 2402.09001, Table IV), fewer frames chosen by the encoder (arXiv 2603.18305, Table V) and a lower resolution or coarser quantiser chosen by the encoder (arXiv 2511.00707, Table 4), each with what was measured and against what. They are informative: no record states them, no rule reads them, and none is a figure of a WAI decoder. The section says how they bear on the text: the two encoder choices cost frames or quality, SPEC §4's energy order already ranks the platform's power-efficient decode first, and a hardware row of this revision reads no media engine, so it and the first point are not like for like. Co-Authored-By: Claude Opus 5.5 <[email protected]>
check-public-names: the products WAI's maintainers sell are on the list
The name check read only the private list, which never held the maintainers' own product names, so a changelog line naming one passed it. Those names are public already, and the private list also serves checks of text about the products, which names them where it should; so they are listed in the script, MAINTAINER_PRODUCTS, in any case, joined or with one hyphen or underscore, the shorter one standing alone. They are applied beside the private list, with the same opt-out, and an unset private list still fails closed. The self-test gains a case for each spelling, a generic phrase that is not a hit, an honoured opt-out, and the unset list. Co-Authored-By: Claude Opus 5.5 <[email protected]>
CHANGELOG: the route record's failure-cost entry names no product
The entry for version 2 of the route record named the product whose proposal it adopted, against CONTRIBUTING's public-text rule. The name is redacted in place with a dated marker, as this changelog's history redacts names, and the sentence keeps its disclosure: the proposal came from a product of WAI's maintainers [name removed 2026-11-03]. Co-Authored-By: Claude Opus 5.5 <[email protected]>