wai-pack: the packaging policy, scene cuts, and the one integer video encoder #168

Merged
dcharlot merged 26 commits from wai/w4-wai-pack into main 2026-10-06 03:52:48 -04:00
Owner

wai-pack is the segmenter and packager, and this pull request adds its first two parts: the policy that drives it, and the one encoder for WAI's integer video payload.

  • The policy. wai.pack.policy/1 is a TOML 1.0 document that is read in three stages, each refusal named.

    • It is read from bytes. Invalid UTF-8, a byte-order mark, an integer outside 64 bits or nesting deeper than 32 is policy-toml.
    • Ids share one namespace.
    • Its digest is BLAKE3 over its JCS form, so two spellings of one policy have one digest.
  • Sources and scene cuts.

    • Y4M and WAV readers refuse a header that lies, and never size a buffer from a header.
    • A streaming segmenter finds cuts with lookahead, with a return clause and a revisit clause, so a flash or a cut back does not start a piece. Its state is bounded by its window and lookahead.
  • The integer video encoder (int_video_enc.rs) writes the pinned WIV1 payload. It is the only WIV1 encoder; the zero-motion writer is a case of it, and its bytes are unchanged.

    • Integer motion search, closed-loop quantisation and a two-pass rANS writer.
    • Caps are checked before anything is allocated.
    • A stated working-set bound, which a test measures.
  • The step q is chosen per piece from the policy's candidates under its distortion bound (packaging §5.6). wai-pack does not write objects yet; the base encode will call this rule.

  • Capability refusals. A packager refuses a candidate, unknown or non-payload capability, a wrong role, and an unclassified licence.

  • The corpus (pack-conformance/) holds policies, sources, scene cuts and golden payloads, and pack_verify.py re-derives all of it independently:

    • every motion vector and residual;
    • the table fit;
    • the choice of q.

    Seeded differentials compare Rust and Python on policies, sources and scene cuts.

  • CI (wai-pack.yml):

    • clippy, release tests with overflow checks on, and a test-count floor;
    • the independent verifier;
    • a licence check with cargo-deny, plus an assertion that the build-time-only exception never becomes a runtime dependency.

Also: energy-hardware.yml declares its concurrency group. It reached main just before the workflow guard did, so main's own workflow-hygiene run fails on it.

Text: extensions/packaging.md §1–§4, §3.6, §5.5, §5.6 and §12. The remaining sections are reserved headings until the packer writes objects.

Checked locally on macOS arm64: the link gate, pack_verify.py, the wai-pack conformance check, the three differentials, the staged corpus, clippy, cargo-deny and the public-text checks. The forge CI runs on this pull request.

🤖 Generated with Claude Code

wai-pack is the segmenter and packager, and this pull request adds its first two parts: the policy that drives it, and the one encoder for WAI's integer video payload. - **The policy.** `wai.pack.policy/1` is a TOML 1.0 document that is read in three stages, each refusal named. - It is read from bytes. Invalid UTF-8, a byte-order mark, an integer outside 64 bits or nesting deeper than 32 is `policy-toml`. - Ids share one namespace. - Its digest is BLAKE3 over its JCS form, so two spellings of one policy have one digest. - **Sources and scene cuts.** - Y4M and WAV readers refuse a header that lies, and never size a buffer from a header. - A streaming segmenter finds cuts with lookahead, with a return clause and a revisit clause, so a flash or a cut back does not start a piece. Its state is bounded by its window and lookahead. - **The integer video encoder** (`int_video_enc.rs`) writes the pinned `WIV1` payload. It is the only WIV1 encoder; the zero-motion writer is a case of it, and its bytes are unchanged. - Integer motion search, closed-loop quantisation and a two-pass rANS writer. - Caps are checked before anything is allocated. - A stated working-set bound, which a test measures. - **The step `q`** is chosen per piece from the policy's candidates under its distortion bound (packaging §5.6). wai-pack does not write objects yet; the base encode will call this rule. - **Capability refusals.** A packager refuses a candidate, unknown or non-payload capability, a wrong role, and an unclassified licence. - **The corpus** (`pack-conformance/`) holds policies, sources, scene cuts and golden payloads, and `pack_verify.py` re-derives all of it independently: - every motion vector and residual; - the table fit; - the choice of `q`. Seeded differentials compare Rust and Python on policies, sources and scene cuts. - **CI** (`wai-pack.yml`): - clippy, release tests with overflow checks on, and a test-count floor; - the independent verifier; - a licence check with cargo-deny, plus an assertion that the build-time-only exception never becomes a runtime dependency. **Also:** `energy-hardware.yml` declares its concurrency group. It reached main just before the workflow guard did, so main's own `workflow-hygiene` run fails on it. **Text:** `extensions/packaging.md` §1–§4, §3.6, §5.5, §5.6 and §12. The remaining sections are reserved headings until the packer writes objects. **Checked locally on macOS arm64:** the link gate, `pack_verify.py`, the wai-pack conformance check, the three differentials, the staged corpus, clippy, cargo-deny and the public-text checks. The forge CI runs on this pull request. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
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]>
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]>
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]>
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]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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]>
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
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]>
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]>
§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]>
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]>
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]>
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 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 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 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 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 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 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]>
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]>
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]>
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]>
ci: energy-hardware.yml declares its concurrency group
Some checks failed
standards workspaces build + test / joule-ui-rs (pull_request) Successful in 1m8s
standards workspaces build + test / joulehook-rs (pull_request) Successful in 1m9s
standards workspaces build + test / proof-rs (pull_request) Successful in 1m12s
standards workspaces build + test / sandbox-rs (pull_request) Successful in 2m18s
standards workspaces build + test / sense-rs (pull_request) Successful in 1m4s
standards workspaces build + test / smart-byte-rs (pull_request) Successful in 3m39s
wai cross-architecture byte-equality / linux-x86_64 (pull_request) Successful in 27s
WAI conformance / cargo-class corpora · linux (pull_request) Successful in 4m42s
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Successful in 13s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Successful in 2m11s
WAI conformance / cross-arch byte-equality · linux (pull_request) Successful in 23s
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Successful in 45s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 1m31s
wai formal verification (integer core) / kani (pull_request) Successful in 5m25s
standards workspaces build + test / jouledesk-rs (pull_request) Successful in 3m50s
WAI JPEG-AI whole decode (pinned inputs, required) / fetch pinned JPEG-AI inputs → whole decode + pixel hash, required (pull_request) Successful in 1m0s
WAI neural sink gates / model-free refusals of the neural sink (pull_request) Successful in 1m1s
wai-pack / wai-pack · linux (pull_request) Successful in 42s
wai-pack / wai-pack · independent verifier (Python) (pull_request) Successful in 59s
standards workspaces build + test / macOS energy backends (pull_request) Failing after 4m31s
wai-pack / wai-pack · dependency licences (pull_request) Successful in 1m9s
WAI section references / cited sections exist (pull_request) Successful in 9s
wai cross-architecture byte-equality / macos-arm64 (reference) (pull_request) Successful in 34s
WAI mbt2018_mean integer decode (regenerated vectors) / regenerate mbt2018_mean integer vectors → Rust decode, required (pull_request) Successful in 3m52s
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Successful in 1m59s
workflow hygiene / every workflow supersedes its own queue (pull_request) Successful in 3s
WAI conformance / cargo-class corpora · macos (pull_request) Successful in 8m24s
WAI conformance / cross-arch byte-equality · macos (pull_request) Successful in 34s
WAI energy receipts / energy receipts · macos (pull_request) Successful in 3m7s
wai-pack / wai-pack · macos (pull_request) Successful in 1m20s
8b39a21e9a
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]>
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Transaction-Science/open-standards!168
No description provided.