It reached main without one just before the guard did, so main's own guard run
fails on it. The same block every other workflow carries.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A newer push supersedes its run instead of queueing another, as main's workflow
guard requires of every workflow.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Round 3, N1 and N2.
N1. The fixed 8192 bytes did not cover fitting a wide residual table:
hist_to_cdf held a u128 frequency per symbol in a vector that doubled when
the escape was pushed (22,492 bytes over 511 symbols), so small clips with
a wide table exceeded the bound (the review: a 1 x 3 clip of two noise
frames at step 1 peaked at 12,670 bytes against 8,450; 371 of 3,000 random
small clips were over).
- hist_to_cdf reserves the escape's slot, so the push never regrows the
vector; the fit over 511 symbols now peaks at 14,332 bytes. Its result
is unchanged: the staged corpus regenerates byte for byte, the 26 golden
payloads hold, and the table and reverse-writer tests pass.
- The fixed term is derived from the widest table, S = 2*255+1 symbols:
its fit, 16*(S+1) + 2*8*S + 4*(S+2) (frequencies with the escape, the
ordering and the stable sort's scratch, the table), and the table's
three copies with the payload's header, 3*4*(S+2) + 64: 24,640 bytes in
all, as expressions in the code and in packaging §5.5.
- tests/int_video_enc_memory.rs adds two tiny clips with a wide table
(1 x 3 noise, two frames, step 1; 2 x 5, four frames, block 5, step 1)
and the review's seeded sweep of 3,000 small clips, run in full in both
profiles.
N2. working_bytes formed its figure in u64 and wrapped on dimensions past
the caps. A clip past the format's caps (which encode refuses before it
allocates) now gives u64::MAX, and every figure is formed in u128 and
saturates; the doc comment states it, and a unit test holds the review's
three inputs.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Fix round, items 4 and 3.
Item 4 (decision: build it). §5.6 restores the design's rule for choosing
q per piece from q_candidates under max_sse_px_milli: the candidates read
as a set, encoded from the largest down, the first with
floor(SSE*1000/(3whn)) <= max_sse_px_milli chosen, so candidates that meet
the bound alike are decided by their step; where none meets it, the
smallest. wai_pack::encode::choose_q is the function the packer's base
encode will call. Departure from the design's plan: the rule is specified
and built here, ahead of the stage that writes objects, and states its
ties and its no-candidate case, which the design left open. §5.5 now says
what is true today: wai-pack does not write objects yet and will write
these with int_video_enc at the step §5.6 chooses. Goldens:
pack-conformance/qchoice, every piece of every fixture under two bounds
(one splitting pieces between steps 8 and 4, one that most pieces meet at
no step), with each candidate's SSE, the choice and the chosen payload's
BLAKE3.
Item 3. pack_verify.py no longer trusts a self-consistent closed loop: from
each golden payload's decode it recomputes every motion vector by the §5.5
search (its order and tie-break) on the source and the previous decoded
frame, every coded residual (the stream's symbols) as
quantize(source - prediction), and the residual table and offset as the
§5.5 fit of those residuals. For the step goldens it runs an encoder of its
own (search, quantization, reconstruction) to recompute every candidate's
SSE and the choice.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Fix round, item 5. The encoder held a residual per sample (i64) and the
rANS encoder buffered a 12-byte symbol per sample: about 22 times the clip
(3.03 GB on a 134 MB clip). Now:
- rans.rs: the encoder's state step and the stream's finish are shared
functions; RansEncoder keeps its API and its bytes; RansReverseEncoder
takes values last-decoded first and holds only the words it emits. A
test feeds both the same values, escapes included, and requires equal
bytes (it also runs in the cross-architecture byte-exact crate, which
includes rans.rs).
- int_video_enc: pass 1 finds the vectors and builds the reconstruction,
its SSE and the residuals' counts (every residual is in -255..=255);
pass 2 derives each residual again and codes it last first into the
reverse writer. The finished stream is built at its exact length.
- int_video_enc::working_bytes states what encode holds beyond its input,
beside the decoder's DeclaredCost (packaging §5.5 states it too), and
tests/int_video_enc_memory.rs, a live-bytes allocator, checks it over
noise at step 1, a smooth pan, block 1 and one frame, in both CI
profiles of wai-conformance.
The bytes are unchanged: the staged corpus, whose bases the zero-motion
writer codes, regenerates byte for byte, and so do the 26 golden payloads.
The review's 134 MB clip now peaks at about 340 MB resident, its input
included.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Fix round, item 8. deny.toml's MPL-2.0 exception named cbindgen at any
version and in any role: a crate that linked cbindgen as a normal
dependency passed the check (the review's probe). The exception now names
[email protected], and the licences job asserts that cargo tree -e normal
finds no path from wai-pack to cbindgen, while the build edge is found.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Fix round, item 9. The streaming segmenter kept every frame's score, so its
memory grew with the stream. Scores are now an opt-in batch output
(Segmenter::with_scores, which segment() uses); a streaming segmenter holds
at most 2L + 2 frame records and the window, and Segmenter::retained says
how much. A test pushes 60,000 frames and checks the bound at every one.
segment_diff.py (from the review's segdiff) compares the streaming
segmenter, through the wai-pack segment-json aid, with pack_verify.py's
on-demand form on seeded random clips, and checks the min and max bounds of
every piece; CI runs it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Fix round, item 10. A component and a rung could share an id, so a
reference (a lead, a rung's component, refines, a rule's ladder) could
name two things. Every id is now unique across the five kinds together
(policy-duplicate-id), in both implementations and §3.4(2), with a corpus
case of a component and a rung both named v. The corpus README notes that
two golden payloads are byte-identical because their frames are.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Fix round, item 7. The relative test of wai.pack.scenecut/1 applies only to
a window of 3 or more scores, so a cut_window of 1 or 2 left
cut_rel_permille inert while the policy stated it. The range is now 3 to
64 (policy-value), in both implementations and §3.3, with a corpus case.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Fix round, item 6. The example named wai.audio.int_layered, which SPEC §5
does not register yet, so a packager refused it (capability-unknown). The
audio component's base is now wai.audio.flac, and the rungs that named or
refined int_layered are gone. int_layered's parameters leave the schema
and §3.3's table, with a note that the string is reserved until it is
registered. Both implementations now require every policy of the policy
corpus to be accepted, rungs included, not only parsed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Fix round, item 2. Policy::parse_bytes reads a policy as bytes; the CLI and
the corpus use it (no lossy decode). Before the value kinds, and before
anything else, these are policy-toml in both implementations:
- bytes that are not UTF-8;
- a byte-order mark (the house rule: UTF-8 with no byte-order mark);
- not TOML 1.0, an integer outside TOML's 64-bit range among it
(pack_verify.py's reader takes any size, so it now checks the range);
- arrays and tables nested past 32, the document's table at depth 1: a
pass of its own before the value-kind walk, so a nesting fault outranks
a float found earlier in key order, as the Rust parser's own nesting
limit already made it. pack_verify.py also reads a RecursionError from
its TOML reader as policy-toml.
packaging §3.1 and §3.2 state it. Corpus: a byte-order mark, bytes that are
not UTF-8, an integer at 2^63, nesting to depth 33 and 80. policy_diff.py
(from the review's byte-level cases and seeded policies) compares every
verdict and digest of wai-pack policy-verdict with pack_verify.py's; CI
runs it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Fix round, item 1. align != 2 * channels was u16 arithmetic: 32768 channels
panicked a debug build and, with an alignment of 0, a release build accepted
it; 32769 with 2 likewise. The check is now channels.checked_mul(2) ==
Some(align), as the spec and pack_verify.py read it. A block and a Y4M frame
grow with the bytes read, never to a size the file declares, and the corpus
check reads WAV data in bounded blocks: a 4 GiB data claim over 4 bytes
allocates about 4 bytes.
- Corpus: channels 32768 (alignment 0) and 32769 (alignment 2), and a
4 GiB data claim.
- Release builds of wai-pack check overflow (profile.release), so CI's
release tests catch this class.
- tests/source_alloc.rs bounds what a reader allocates for such files.
- source_diff.py (from the review's differential, seeded): Y4M and WAV
mutations, every verdict of wai-pack source-verdict equal to
pack_verify.py's; CI runs it against debug and release builds.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Every wai/*-conformance directory states its decoded outputs' SHA-256 in a
sha256.json sidecar or is excluded with a reason. pack-conformance states
each decoded frame's SHA-256 in encode/expected.json, as staged-conformance
does in its case files; its other vectors have no decoded output.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
§3.6 states the capability rules a packager refuses a policy by, in order,
read from SPEC §5's registry. §5.5 defers int_motion's payload to the pin
SPEC §5 already holds (integer-payloads §3: WIV1, keyframe 0), so packaging
adds no annex of its own, and states the reference encoder's choices:
motion search order and tie-break, the rounding of the quantizer, the
residual table's arithmetic, and the one-frame header. The corpus README
and the CHANGELOG follow; the workflow also fires on the registry export
pack_verify.py reads.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
capcheck.rs: for each rung, in order, capability-unknown (SPEC §5 does
not register it), capability-not-payload (a record or a derivation),
capability-candidate (no pinned payload format), capability-role (a base
or floor that is not standalone media, a refinement that is not
refinement-only), and under royalty_free_only license-unclassified or
license-refused with the class's label, read from wai's codecs registry.
A packager's verdict on a policy is the policy's refusal, then the first
rung's; the refusal corpus gains a case for each that a registered string
can reach.
pack-conformance/encode: a WIV1 payload of int_video_enc for every piece of
every fixture (its first frames), under a whole-pel and a half-pel
parameter set, with its BLAKE3, the SHA-256 of every decoded frame and the
SSE against the source. wai-pack regenerates each byte for byte, decodes
it through decode_int and recomputes the SSE; pack_verify.py decodes each
with the integer-payload corpus's own WIV1 decoder, recomputes the SSE from
the fixture it regenerates, and applies R6 from the published registry
export.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
src/int_video_enc.rs (feature video_int) codes RGB8 frames as a WIV1
keyframe-0 payload: integer block motion search over the decoder's own
prediction from the previous frame as decoded (dy then dx ascending, least
SAD, ties to the shorter vector, then the earlier one), whole- or half-pel;
residuals quantized half away from zero in a closed loop; one residual table
for the clip by the rule link 18's writer used. The caps, the motion-vector
field's included, are checked before anything is allocated, and the SSE of
the reconstruction is returned with it.
int_payload::encode_wiv1_zero_motion keeps its signature and its bytes: it
is now encode() at block max(h, w), search 0, whole-pel. The staged corpus,
whose bases it writes, regenerates byte for byte. int_video::predict is
public, so the encoder predicts with the decoder's own function.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
W4-2 makes int_video_enc the one WIV1 encoder, and link 18's
int_payload::encode_wiv1_zero_motion becomes a thin case of it. That function
exists only on link 18, so its branch is merged here rather than a second
encoder written beside it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
# Conflicts:
# wai/CHANGELOG.md
extensions/packaging.md, first sections: the policy schema
wai.pack.policy/1 with every member's default and range, the order a
policy is read in and the name of every refusal, the policy digest over
the effective policy's RFC 8785 form; closed pieces, the plan digest,
wai.pack.scenecut/1 with its lookahead clauses, the unit conversions,
alignment to the lead component, classification, and the Y4M and WAV
sources. Sections 5 to 11 are reserved for the stages that write objects.
.github/workflows/wai-pack.yml: on each architecture, clippy with float
arithmetic denied, the tests in release with a floor per target, and the
corpus recomputed; the Python verifier; and the dependency licences
against an allow-list.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
So the canonical JSON's string form (RFC 8785: no escape for a non-ASCII
character, the short escape for a tab) and TOML's unicode escapes are part
of what the two implementations must agree on.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The corpus check compared the committed digests of each equal pair, which
an implementation that had diverged would still match.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A new crate beside wai-rs, with its own workspace and lock: the reference
packager of the packaging extension, built so far as far as a plan.
- policy.rs: wai.pack.policy/1 read in three stages (value kinds a canonical
JSON cannot carry; the schema with every default written in; the
constraints across members), each refusal named. The effective policy's
canonical JSON goes through wai::jcs, and its digest is
BLAKE3("wai:pack-policy\x01" || JCS).
- source/: Y4M to RGB8 (BT.709 limited range, integer), WAV PCM s16le.
- scenecut.rs: wai.pack.scenecut/1, one streaming segmenter with a bounded
lookahead and the return and revisit clauses, so live and on-demand
packing cut alike at equal lookahead; plan digest.
- classify.rs: static, synthetic, high motion, natural.
- fixture.rs: integer-generated Y4M fixtures (cuts-01, kinds-01).
- conformance.rs and wai-pack conformance regen|check.
pack-conformance/ holds the policy digests and equal-spelling pairs, a
refusal case per reason, the unit and colour conversions, and the scene-cut
vectors; pack_verify.py recomputes every one of them from the rules, with
its own TOML schema, JCS, readers, fixture generator and segmenter. It
found one defect while it was written: a millisecond bound converted to an
extreme timebase wrapped past 2^64; both conversions now saturate.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
RFC 8785 over WAI's signing subset moves from claim_receipt to src/jcs.rs,
compiled under every feature set (it needs serde_json alone), so a packaging
policy's digest canonicalises through the same function claims, key logs and
energy figures use. Behaviour is unchanged: null admitted, integers up to
2^53 - 1, everything else refused. claim_receipt::jcs keeps its signature and
maps the refusal to malformed, as before.
Co-Authored-By: Claude Opus 5.5 <[email protected]>