All checks were successful
joule-moq-remote host agent / joule-moq-remote · does this change touch it (pull_request) Successful in 6s
joule-moq-room room service / joule-moq-room · does this change touch it (pull_request) Successful in 7s
joule-moq-web browser client / joule-moq-web · does this change touch it (pull_request) Successful in 6s
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 6s
wai-app and Create / wai-app · does this change touch it (pull_request) Successful in 6s
wai-audit collector and player integration / does this change touch it (pull_request) Successful in 6s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (pull_request) Successful in 6s
WAI operation catalogue / does this change touch it (pull_request) Successful in 6s
WAI conformance / does this change touch it (pull_request) Successful in 6s
WAI energy receipts / does this change touch it (pull_request) Successful in 5s
WAI examples / WAI examples · does this change touch them (pull_request) Successful in 6s
wai-fleet service / does this change touch it (pull_request) Successful in 6s
WAI JPEG-AI whole decode (pinned inputs, required) / does this change touch it (pull_request) Successful in 7s
wai-ledger service / does this change touch it (pull_request) Successful in 6s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (pull_request) Successful in 1m25s
wai-pack / wai-pack · does this change touch it (pull_request) Successful in 6s
wai-quantum for JavaScript / does this change touch it (pull_request) Successful in 6s
wai-quantum studio and desktop / does this change touch it (pull_request) Successful in 6s
wai-sink-android native sink library / does this change touch it (pull_request) Successful in 6s
wai-sink-swift native sink package / does this change touch it (pull_request) Successful in 6s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (pull_request) Successful in 1m57s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 2m7s
WAI Studio / does this change touch it (pull_request) Successful in 6s
wai-web reach builds / wai-web reach · does this change touch it (pull_request) Successful in 6s
wai-web wasm32 / does this change touch it (pull_request) Successful in 6s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (pull_request) Successful in 29s
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (pull_request) Successful in 2m1s
WAI conformance / fast integer kernels, every x86_64 kernel set (AVX-512 silicon) (pull_request) Successful in 2m59s
wai-audit collector and player integration / collector · linux (pull_request) Successful in 1m1s
WAI conformance / cross-arch byte-equality · macos (pull_request) Successful in 26s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 4m19s
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Successful in 16s
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Successful in 1m4s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (pull_request) Successful in 1m15s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (pull_request) Successful in 5m15s
joule-moq-room room service / joule-moq-room, every target (pull_request) Successful in 6m19s
wai-fleet service / service · macos (pull_request) Successful in 1m2s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (pull_request) Successful in 2m46s
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Successful in 1m43s
wai-ledger service / service · macos (pull_request) Successful in 1m24s
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Successful in 1m2s
WAI operation catalogue / the catalogue and every door · linux (pull_request) Successful in 4m18s
wai-quantum studio and desktop / desktop app · windows (pull_request) Successful in 9m19s
wai-pack / wai-pack · macos (pull_request) Successful in 3m8s
WAI conformance / cross-arch byte-equality · linux (pull_request) Successful in 30s
WAI energy receipts / energy receipts · macos (pull_request) Successful in 7m49s
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Successful in 3m23s
WAI conformance / faces from keypoints under wasm32-wasip2 (wasmtime) (pull_request) Successful in 1m39s
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Successful in 3m9s
wai-fleet service / service · linux (pull_request) Successful in 2m5s
wai-quantum studio and desktop / desktop app · macos (pull_request) Successful in 3m28s
wai-ledger service / service · linux (pull_request) Successful in 1m43s
wai-pack / wai-pack · linux (pull_request) Successful in 1m55s
wai-quantum for JavaScript / javascript · linux (pull_request) Successful in 3m0s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (pull_request) Successful in 1m10s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Successful in 16m12s
WAI conformance / each feature of the independent verifiers' steps compiles alone (pull_request) Successful in 11m32s
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (pull_request) Successful in 4m51s
WAI examples / WAI examples · the archive, the readiness check, the browser and Apple · macos (pull_request) Successful in 13m12s
WAI conformance / cargo-class corpora · macos (pull_request) Successful in 15m2s
wai-quantum studio and desktop / studio · browser (pull_request) Successful in 4m3s
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Successful in 16m34s
wai-web reach builds / wai-web reach builds · the package of record, every output as recorded (pull_request) Successful in 2m40s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 17m22s
wai-pack / wai-pack · linux-arm64 (pull_request) Successful in 1m44s
wai-quantum studio and desktop / desktop app · linux (pull_request) Successful in 5m46s
wai-pack / wai-pack · the floor through a media prober (pull_request) Successful in 34s
wai-pack / wai-pack · dependency licences (pull_request) Successful in 17s
WAI examples / WAI examples · portable · linux (pull_request) Successful in 9m28s
WAI examples / WAI examples · android-app · linux-arm64 (pull_request) Successful in 5m7s
WAI conformance / cargo-class corpora · linux (pull_request) Successful in 14m49s
wai-web reach builds / wai-web reach builds · this tree's wai-web, every rung on its engine line (pull_request) Successful in 4m34s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (pull_request) Successful in 2m16s
WAI JPEG-AI whole decode (pinned inputs, required) / fetch pinned JPEG-AI inputs → whole decode + pixel hash, required (pull_request) Successful in 11m4s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 13m6s
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Successful in 5m58s
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (pull_request) Successful in 10m55s
WAI Studio / Studio in Chromium and Firefox · macos (pull_request) Successful in 9m51s
WAI Studio / Studio in WebKit · linux (pull_request) Successful in 10m15s
wai-pack / wai-pack · independent verifier (Python) (pull_request) Successful in 9m21s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 30m14s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Successful in 1h4m7s
Every change to WAI added an entry at the head of wai/CHANGELOG.md, and every new module appended to the hand-kept lists of .github/workflows/wai-conformance.yml. So any two branches conflicted on those lines and had to merge one after another. This moves both into files a change adds. - Changelog entries: a change adds wai/changes/<yyyy-mm-dd>-<slug>.md holding the entry text. scripts/changelog-assemble.py renders the entries into CHANGELOG.md's Unreleased section for a release, in a stable order. scripts/check-changelog-fragment.py, run by repo-checks on every pull request, fails a pull request that changes wai/ and adds or edits no entry file directly in wai/changes/ (a release passes, and so does a commit with the trailer line "Changelog: none"). A pull request opened before the check reached main passes as it is; one opened after it that adds its entry to the head of Unreleased fails and is told where the entry goes. wai breaking reads breaking changes from entry files too. - The conformance workflow: each per-module verifier step is a file in .github/wai-conformance/verifiers/, each corpus a file in corpora/, and the integer-kernel tests carry a tag line. scripts/wai-conformance-lists.py derives every list the workflow kept by hand (WAI_FEATURES, the union --bin and --test lists, the features-alone lists, the kernel and corpus steps) from those files when the workflow runs. Its `selftest --exact` holds the derived lists to the hand lists of the commit before this one, and its dry-run prints every command each job runs: the dry-runs before and after this change differ only in the gate's path list, which gains the two paths this change adds. - What stays shared, and how to insert there, is written in wai/CONTRIBUTING.md and wai/changes/README.md: wai-rs Cargo.toml features and [[test]] entries, lib.rs, registry rows, wai/.gitattributes and the live corpus table take an insert beside the related entry, never at the end. Co-Authored-By: Claude Opus 5.5 <[email protected]>
221 lines
12 KiB
Rust
221 lines
12 KiB
Rust
// wai-conformance: int-kernels
|
||
//! `wai.generate.spectral` — **generation-equivalence**: a deterministic integer
|
||
//! generator produces byte-identical bytes on every machine from the same
|
||
//! `(model, prompt, seed, steps)`, so its hash is portable (the create-side of
|
||
//! "conformance is a hash"). Committed vectors under `tests/vectors/generate/`:
|
||
//! `generate.json` (96×96), `sizes.json` (256×128 and 640×320), and `instr-v0.json`,
|
||
//! the instruction object of form v0 (SPEC §5) with the SHA-256 of each of its four
|
||
//! listed sizes. Nothing skips except the two larger listed sizes in a debug build,
|
||
//! which the release step of the conformance workflow runs; the objects a sink
|
||
//! refuses are run through `wai` in `tests/cli_generate_score.rs`.
|
||
#![cfg(feature = "generate")]
|
||
|
||
use std::path::PathBuf;
|
||
|
||
use wai::generate::{fnv1a, generate, SpectralModel};
|
||
|
||
#[path = "support/sha256_sidecar.rs"]
|
||
mod sha256_sidecar;
|
||
|
||
fn dir() -> PathBuf {
|
||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/vectors/generate")
|
||
}
|
||
fn jload(name: &str) -> serde_json::Value {
|
||
serde_json::from_slice(&std::fs::read(dir().join(name)).unwrap()).unwrap()
|
||
}
|
||
fn hash(rgb: &[u8]) -> String {
|
||
format!("{:016x}", fnv1a(rgb))
|
||
}
|
||
|
||
#[test]
|
||
fn generation_is_deterministic_and_pinned() {
|
||
let m = SpectralModel::from_value(&jload("generate_spectral_model.json"));
|
||
let pin = jload("generate.json");
|
||
let (prompt, seed, steps, h, w) = (
|
||
pin["prompt"].as_str().unwrap(),
|
||
pin["seed"].as_u64().unwrap(),
|
||
pin["steps"].as_u64().unwrap() as usize,
|
||
pin["h"].as_u64().unwrap() as usize,
|
||
pin["w"].as_u64().unwrap() as usize,
|
||
);
|
||
|
||
// 1. PIN: the committed (prompt, seed, steps) reproduces the committed hash,
|
||
// byte-for-byte — the portable receipt of a generated image.
|
||
let rgb = generate(&m, prompt, seed, steps, h, w);
|
||
assert_eq!(rgb.len(), h * w * 3);
|
||
let got = hash(&rgb);
|
||
println!("generate.spectral: {h}x{w} '{prompt}' seed={seed} steps={steps} -> fnv1a {got}");
|
||
assert_eq!(got, pin["rgb_hash"].as_str().unwrap(), "pinned generation must reproduce the committed hash");
|
||
sha256_sidecar::one(&dir(), "generate", "RGB8: rows top to bottom, pixels left to right, R, G, B bytes", &rgb);
|
||
|
||
// 2. DETERMINISM: regenerating the same inputs is byte-identical.
|
||
assert_eq!(generate(&m, prompt, seed, steps, h, w), rgb, "regeneration must be byte-identical");
|
||
|
||
// 3. SENSITIVITY: a different prompt or seed draws a different image (the
|
||
// generation is conditioned, not constant).
|
||
assert_ne!(hash(&generate(&m, "a different prompt", seed, steps, h, w)), got, "prompt must change the output");
|
||
assert_ne!(hash(&generate(&m, prompt, seed ^ 1, steps, h, w)), got, "seed must change the output");
|
||
assert_ne!(hash(&generate(&m, prompt, seed, steps + 1, h, w)), got, "steps must change the output");
|
||
}
|
||
|
||
fn model() -> SpectralModel {
|
||
SpectralModel::from_value(&jload("generate_spectral_model.json"))
|
||
}
|
||
|
||
/// `sizes.json`: the committed vector's prompt, seed and steps at 256×128 and 640×320,
|
||
/// non-square, so the `(h, w)` order of `generate` is pinned too. Each SHA-256 was
|
||
/// made natively by `wai generate` and equals the shared browser binding's output.
|
||
#[test]
|
||
fn sizes_reproduce_their_sha256() {
|
||
let (m, pin) = (model(), jload("sizes.json"));
|
||
let model_bytes = std::fs::read(dir().join("generate_spectral_model.json")).unwrap();
|
||
assert_eq!(sha256_sidecar::sha256_hex(&model_bytes), pin["model_sha256"].as_str().unwrap(), "the committed model");
|
||
let (prompt, seed, steps) =
|
||
(pin["prompt"].as_str().unwrap(), pin["seed"].as_u64().unwrap(), pin["steps"].as_u64().unwrap() as usize);
|
||
let sizes = pin["sizes"].as_array().unwrap();
|
||
assert_eq!(sizes.len(), 2);
|
||
for s in sizes {
|
||
let (w, h) = (s["w"].as_u64().unwrap() as usize, s["h"].as_u64().unwrap() as usize);
|
||
assert_ne!(w, h, "a non-square size pins the order of w and h");
|
||
let rgb = generate(&m, prompt, seed, steps, h, w);
|
||
assert_eq!(rgb.len(), w * h * 3);
|
||
assert_eq!(sha256_sidecar::sha256_hex(&rgb), s["sha256"].as_str().unwrap(), "{w}x{h}");
|
||
// h and w swapped make another picture.
|
||
assert_ne!(generate(&m, prompt, seed, steps, w, h), rgb, "{w}x{h}: swapping w and h");
|
||
}
|
||
}
|
||
|
||
/// The members of the vector's instruction object, read with serde_json, and the
|
||
/// object written again in form v0's canonical serialization (SPEC §5, *Payload of
|
||
/// `wai.generate.spectral`*), stated here independently of `wai`'s reader.
|
||
fn instr_v0() -> (serde_json::Value, Vec<(usize, usize, String)>) {
|
||
let v = jload("instr-v0.json");
|
||
let object = v["object"].as_str().unwrap();
|
||
let o: serde_json::Value = serde_json::from_str(object).unwrap();
|
||
let sizes: Vec<String> = o["sizes"].as_array().unwrap().iter()
|
||
.map(|p| format!("[{},{}]", p[0].as_u64().unwrap(), p[1].as_u64().unwrap())).collect();
|
||
let canonical = format!(
|
||
"{{\"model\":\"{}\",\"prompt\":\"{}\",\"seed\":{},\"sizes\":[{}],\"steps\":{}}}",
|
||
o["model"].as_str().unwrap(), o["prompt"].as_str().unwrap(), o["seed"].as_u64().unwrap(),
|
||
sizes.join(","), o["steps"].as_u64().unwrap());
|
||
assert_eq!(canonical, object, "the vector's object is in canonical form");
|
||
let outputs = v["outputs"].as_array().unwrap().iter().map(|e| (
|
||
e["w"].as_u64().unwrap() as usize, e["h"].as_u64().unwrap() as usize, e["sha256"].as_str().unwrap().to_string(),
|
||
)).collect();
|
||
(o, outputs)
|
||
}
|
||
|
||
fn make_listed(min_pixels: usize, max_pixels: usize) -> usize {
|
||
let (o, outputs) = instr_v0();
|
||
let model_bytes = std::fs::read(dir().join(jload("instr-v0.json")["model_file"].as_str().unwrap())).unwrap();
|
||
assert_eq!(format!("sha256:{}", sha256_sidecar::sha256_hex(&model_bytes)), o["model"].as_str().unwrap(),
|
||
"the object names the committed model by its SHA-256");
|
||
let listed: Vec<(usize, usize)> = o["sizes"].as_array().unwrap().iter()
|
||
.map(|p| (p[0].as_u64().unwrap() as usize, p[1].as_u64().unwrap() as usize)).collect();
|
||
assert_eq!(listed, outputs.iter().map(|(w, h, _)| (*w, *h)).collect::<Vec<_>>(), "one output per listed size");
|
||
let m = SpectralModel::from_value(&serde_json::from_slice(&model_bytes).unwrap());
|
||
let (prompt, seed, steps) =
|
||
(o["prompt"].as_str().unwrap(), o["seed"].as_u64().unwrap(), o["steps"].as_u64().unwrap() as usize);
|
||
let mut made = 0;
|
||
for (w, h, sha) in &outputs {
|
||
if !(min_pixels..=max_pixels).contains(&(w * h)) {
|
||
continue;
|
||
}
|
||
let rgb = generate(&m, prompt, seed, steps, *h, *w);
|
||
assert_eq!(rgb.len(), w * h * 3);
|
||
assert_eq!(&sha256_sidecar::sha256_hex(&rgb), sha, "{w}x{h}");
|
||
made += 1;
|
||
}
|
||
made
|
||
}
|
||
|
||
/// `instr-v0.json` at its two smaller listed sizes. Its 256×128 and 640×320 outputs are
|
||
/// `sizes.json`'s: the object's prompt, seed and steps are the committed vector's.
|
||
#[test]
|
||
fn instruction_object_v0_small_sizes_reproduce() {
|
||
assert_eq!(make_listed(0, 640 * 320), 2);
|
||
let (_, outputs) = instr_v0();
|
||
let sizes = jload("sizes.json");
|
||
for (s, (w, h, sha)) in sizes["sizes"].as_array().unwrap().iter().zip(&outputs) {
|
||
assert_eq!((s["w"].as_u64().unwrap() as usize, s["h"].as_u64().unwrap() as usize, s["sha256"].as_str().unwrap()),
|
||
(*w, *h, sha.as_str()));
|
||
}
|
||
}
|
||
|
||
/// `instr-v0.json` at 1280×720 and 1920×1080: under a second in a release build, many
|
||
/// seconds in a debug one, so the release step of the conformance workflow runs it.
|
||
#[test]
|
||
#[cfg_attr(debug_assertions, ignore = "1280x720 and 1920x1080 take seconds in a debug build; the release step runs them")]
|
||
fn instruction_object_v0_large_sizes_reproduce() {
|
||
assert_eq!(make_listed(640 * 320 + 1, usize::MAX), 2);
|
||
}
|
||
|
||
/// SPEC §5, *Payload of `wai.generate.spectral`*, stated here a second time and
|
||
/// independently of `wai`'s reader: the bytes are one JSON object with exactly the
|
||
/// five members, each value in range, and the bytes are exactly that object written
|
||
/// canonically (members in ascending byte order, no whitespace, integers in decimal,
|
||
/// the prompt unescaped). A member given twice fails the last test, since the object
|
||
/// written again names each member once. `Some(object)` when the bytes are form v0.
|
||
fn form_v0(bytes: &[u8]) -> Option<serde_json::Value> {
|
||
let o: serde_json::Value = serde_json::from_slice(bytes).ok()?;
|
||
let m = o.as_object()?;
|
||
let keys: Vec<&str> = m.keys().map(String::as_str).collect();
|
||
let mut sorted = keys.clone();
|
||
sorted.sort();
|
||
if sorted != ["model", "prompt", "seed", "sizes", "steps"] {
|
||
return None;
|
||
}
|
||
let hex = o["model"].as_str()?.strip_prefix("sha256:")?;
|
||
let hex_ok = hex.len() == 64 && hex.bytes().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b));
|
||
let prompt = o["prompt"].as_str()?;
|
||
let prompt_ok = prompt.len() <= 256 && prompt.bytes().all(|b| (0x20..=0x7e).contains(&b) && b != b'"' && b != b'\\');
|
||
let seed = o["seed"].as_u64()?;
|
||
let steps = o["steps"].as_u64()?;
|
||
let sizes: Vec<(u64, u64)> = o["sizes"].as_array()?.iter()
|
||
.map(|p| p.as_array().filter(|a| a.len() == 2).and_then(|a| Some((a[0].as_u64()?, a[1].as_u64()?))))
|
||
.collect::<Option<_>>()?;
|
||
let sizes_ok = (1..=16).contains(&sizes.len())
|
||
&& sizes.iter().all(|&(w, h)| (1..=4096).contains(&w) && (1..=4096).contains(&h) && w * h <= 1 << 23)
|
||
&& sizes.windows(2).all(|p| p[0] < p[1]);
|
||
if !(hex_ok && prompt_ok && seed <= u32::MAX as u64 && (1..=64).contains(&steps) && sizes_ok) {
|
||
return None;
|
||
}
|
||
let listed: Vec<String> = sizes.iter().map(|(w, h)| format!("[{w},{h}]")).collect();
|
||
let canonical = format!("{{\"model\":\"sha256:{hex}\",\"prompt\":\"{prompt}\",\"seed\":{seed},\"sizes\":[{}],\"steps\":{steps}}}",
|
||
listed.join(","));
|
||
(canonical.as_bytes() == bytes).then_some(o)
|
||
}
|
||
|
||
/// `instr-v0.json`, read by the statement above: its object and every object it lists
|
||
/// as accepted (one at each inclusive bound) are form v0, and each accepted object's
|
||
/// first listed size, made by the reference generator, has the SHA-256 the vector
|
||
/// states; every object it lists as refused is not form v0. `wai`'s own reader is held
|
||
/// to the same lists, with the reason for each refusal, in `tests/cli_generate_score.rs`.
|
||
#[test]
|
||
fn instruction_object_v0_bounds_and_refusals() {
|
||
let v = jload("instr-v0.json");
|
||
let model_bytes = std::fs::read(dir().join(v["model_file"].as_str().unwrap())).unwrap();
|
||
let m = SpectralModel::from_value(&serde_json::from_slice(&model_bytes).unwrap());
|
||
let object = form_v0(v["object"].as_str().unwrap().as_bytes()).expect("the vector's object is form v0");
|
||
let accepted = v["accepted"].as_array().unwrap();
|
||
assert!(accepted.len() >= 12);
|
||
for c in accepted {
|
||
let why = c["why"].as_str().unwrap();
|
||
let o = form_v0(c["object"].as_str().unwrap().as_bytes()).unwrap_or_else(|| panic!("{why}: not form v0"));
|
||
assert_eq!(o["model"], object["model"], "{why}: the committed model");
|
||
let first = &o["sizes"][0];
|
||
for e in c["outputs"].as_array().unwrap() {
|
||
let (w, h) = (e["w"].as_u64().unwrap() as usize, e["h"].as_u64().unwrap() as usize);
|
||
assert_eq!((first[0].as_u64(), first[1].as_u64()), (Some(w as u64), Some(h as u64)), "{why}: the first listed size");
|
||
let rgb = generate(&m, o["prompt"].as_str().unwrap(), o["seed"].as_u64().unwrap(), o["steps"].as_u64().unwrap() as usize, h, w);
|
||
assert_eq!(rgb.len(), w * h * 3);
|
||
assert_eq!(&sha256_sidecar::sha256_hex(&rgb), e["sha256"].as_str().unwrap(), "{why}: {w}x{h}");
|
||
}
|
||
}
|
||
let refused = v["refused"].as_array().unwrap();
|
||
assert!(refused.len() >= 30);
|
||
for c in refused {
|
||
let why = c["why"].as_str().unwrap();
|
||
assert!(form_v0(c["object"].as_str().unwrap().as_bytes()).is_none(), "{why}: the statement of form v0 accepts it");
|
||
}
|
||
}
|