diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 837752cf..76d53ed7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,13 +48,13 @@ jobs: with: name: colors-release-${{ github.sha }}-attempt-${{ github.run_attempt }} path: ${{ runner.temp }}/node-floor-artifact - - name: packed public runtime on declared Node floor + - name: packed public entrypoints on declared Node floor env: ARTIFACT_DIR: ${{ runner.temp }}/node-floor-artifact run: | mapfile -t tarballs < <(find "$ARTIFACT_DIR" -type f -name '*.tgz' -print) test "${#tarballs[@]}" -eq 1 - node scripts/verify-package-release.mjs --runtime-smoke "${tarballs[0]}" + node scripts/verify-package-release.mjs --package-smoke "${tarballs[0]}" msrv: name: MSRV workspace check @@ -241,11 +241,16 @@ jobs: text=True, )) packages = {package["name"]: package for package in metadata["packages"]} - consumers = ( + direct_core_consumers = ( "labcolors-wasm", "labcolors-ffi", "labcolors-conformance", ) + protocol_consumers = ( + "labcolors-compiler", + "labcolors-ffi", + "labcolors-conformance", + ) core = packages["labcolors-core"] if core["features"].get("default") != [ "wcag22-feasibility", @@ -269,7 +274,7 @@ jobs: f"{protocol_core['features']}" ) - for consumer in consumers: + for consumer in direct_core_consumers: core_dependencies = [ dependency for dependency in packages[consumer]["dependencies"] @@ -285,6 +290,7 @@ jobs: f"{consumer} direct Core edge must own no optional capability, got " f"{core_dependency['features']}" ) + for consumer in protocol_consumers: protocol_dependencies = [ dependency for dependency in packages[consumer]["dependencies"] @@ -305,6 +311,47 @@ jobs: f"{consumer} resolved the Core-only explicit-domain capability" ) + runtime_dependencies = packages["labcolors-wasm"]["dependencies"] + if any(dependency["name"] == "labcolors-protocol" for dependency in runtime_dependencies): + raise SystemExit("labcolors-wasm must not depend on labcolors-protocol") + compiler_dependencies = packages["labcolors-compiler"]["dependencies"] + if any( + dependency["name"] in {"labcolors-core", "labcolors-wasm"} + for dependency in compiler_dependencies + ): + raise SystemExit("labcolors-compiler must be a thin protocol-only adapter") + + runtime_tree = subprocess.check_output( + [ + "cargo", "tree", "-p", "labcolors-wasm", + "--target", "wasm32-unknown-unknown", + "--edges", "normal", "-e", "features", + ], + text=True, + ) + for forbidden in ( + "labcolors-protocol", + 'labcolors-core feature "wcag22-feasibility"', + 'labcolors-core feature "wcag22-explicit-feasibility"', + ): + if forbidden in runtime_tree: + raise SystemExit(f"runtime role resolved forbidden capability: {forbidden}") + + compiler_tree = subprocess.check_output( + [ + "cargo", "tree", "-p", "labcolors-compiler", + "--target", "wasm32-unknown-unknown", + "--edges", "normal", "-e", "features", + ], + text=True, + ) + if "labcolors-wasm" in compiler_tree: + raise SystemExit("compiler role resolved the theme/runtime engine") + if 'labcolors-core feature "wcag22-feasibility"' not in compiler_tree: + raise SystemExit("compiler role lacks protocol-owned wcag22-feasibility") + if 'labcolors-core feature "wcag22-explicit-feasibility"' in compiler_tree: + raise SystemExit("compiler role resolved the Core-only explicit capability") + print("core capability projection: PASS") PY - name: reject WCAG22 source-route redirects and proof vacuums @@ -321,6 +368,13 @@ jobs: run: python3 scripts/verify_wcag22_explicit_selection_identity.py --self-test - name: validate historical and current WCAG22 feasibility benchmark evidence run: | + sha256sum --check --strict <<'SHA256' + 7e9ffcbdd9d5d50fe681f511c34fc5c5dd270e9c475ce23ae56e9776922a3c5e crates/labcolors-core/contracts/wcag22-feasibility-benchmark-v1.json + d8d5c7f3eda834bca9912d835fe3ada13d9dcd5a11cb47a131736716b0b51202 crates/labcolors-core/contracts/wcag22-feasibility-benchmark-v2.json + 46ec939523a9aff4f253c4c74e997dfd95812a694b2507fae885ff60244ade3a crates/labcolors-core/contracts/wcag22-feasibility-benchmark-v3.json + 3c257c336bc403eee933990fd7188a3b0a6e89d0cbc983aff18846ef76206275 crates/labcolors-core/contracts/wcag22-feasibility-benchmark-v4.json + SHA256 + historical_artifact="crates/labcolors-core/contracts/wcag22-feasibility-benchmark-v1.json" historical_protocol=( --admit-revision fea99a1ea4148a5a72423c88220655f7f84213fe @@ -385,15 +439,40 @@ jobs: --self-test ) cleanup_history + + v3_artifact="crates/labcolors-core/contracts/wcag22-feasibility-benchmark-v3.json" + v3_snapshot=10c44ef0f4248d0390aa339e81c05a6d5e41996f + v3_protocol=( + --admit-rustc-release 1.96.0 + --admit-cargo-release 1.96.0 + --admit-rustc-binary-sha256 c5922366bfe3d6d028a65d626f4e629b3adad066995cf0b60c8a4b617bba5ffe + --admit-cargo-binary-sha256 fec239e6b74df873f54ef52912bfcfcc8d8414bc14a7ae1e0be80460bae72841 + --admit-benchmark-binary-sha256 6ac07bad81a204ee8fcee8f94a3c445f881d1ca10edaf4cc4a86a5db0b232e3a + --admit-target-triple aarch64-apple-darwin + --admit-target-arch aarch64 + --admit-target-os macos + --admit-pointer-width-bits 64 + --admit-package-version 0.2.0 + --admit-sample-count 5 + ) + git worktree add --detach "$historical_root" "$v3_snapshot" + ( + cd "$historical_root" + python3 scripts/check_wcag22_feasibility_benchmark.py \ + "$v3_artifact" "${v3_protocol[@]}" \ + --artifact-sha256 46ec939523a9aff4f253c4c74e997dfd95812a694b2507fae885ff60244ade3a \ + --self-test + ) + cleanup_history trap - EXIT - current_artifact="crates/labcolors-core/contracts/wcag22-feasibility-benchmark-v3.json" + current_artifact="crates/labcolors-core/contracts/wcag22-feasibility-benchmark-v4.json" current_protocol=( --admit-rustc-release 1.96.0 --admit-cargo-release 1.96.0 --admit-rustc-binary-sha256 c5922366bfe3d6d028a65d626f4e629b3adad066995cf0b60c8a4b617bba5ffe --admit-cargo-binary-sha256 fec239e6b74df873f54ef52912bfcfcc8d8414bc14a7ae1e0be80460bae72841 - --admit-benchmark-binary-sha256 6ac07bad81a204ee8fcee8f94a3c445f881d1ca10edaf4cc4a86a5db0b232e3a + --admit-benchmark-binary-sha256 69fe95cea34c845478c0a3c260e3e4459f1bc09d76857b74d5edb50fd923410a --admit-target-triple aarch64-apple-darwin --admit-target-arch aarch64 --admit-target-os macos @@ -403,7 +482,7 @@ jobs: ) python3 scripts/check_wcag22_feasibility_benchmark.py \ "$current_artifact" "${current_protocol[@]}" \ - --artifact-sha256 46ec939523a9aff4f253c4c74e997dfd95812a694b2507fae885ff60244ade3a \ + --artifact-sha256 3c257c336bc403eee933990fd7188a3b0a6e89d0cbc983aff18846ef76206275 \ --self-test audit: @@ -497,45 +576,49 @@ jobs: run: | cargo install wasm-pack --version 0.13.1 --locked echo "$CARGO_HOME/bin" >> "$GITHUB_PATH" - - name: wasm-pack build (release) - # The release bundle: panic=abort, no panic hook, wasm-opt -Oz. This is - # the artifact @labpics/colors ships, so the size measured below is real. + - name: wasm-pack build (runtime + compiler release roles) + # Each execution role is a separate Cargo root and physical artifact; + # building them in separate invocations prevents feature unification. # Rust error locations otherwise embed the self-hosted runner's mutable # workspace/CARGO_HOME roots and make identical source hash differently. run: | export CARGO_ENCODED_RUSTFLAGS="--remap-path-prefix=$GITHUB_WORKSPACE=/workspace/lab-colors"$'\x1f'"--remap-path-prefix=$CARGO_HOME=/cargo-home" wasm-pack build crates/labcolors-wasm --release --target web --out-dir ../../packages/colors/pkg --out-name labcolors --locked + wasm-pack build crates/labcolors-compiler --release --target web --out-dir ../../packages/colors/compiler --out-name labcolors_compiler --locked - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: node-version: ${{ env.NODE_TOOLCHAIN }} cache: npm cache-dependency-path: packages/colors/package-lock.json - - name: "verify committed #296-B canonical whole-call WASM boundary evidence" - # The admitted JSON binds exact deterministic bytes/shapes and the - # reviewed Uint8Array package root to this built Linux x64 WASM. Fresh - # latency/maxRSS/pages remain observations only. + - name: verify committed #296-C1 canonical whole-call compiler evidence working-directory: packages/colors run: >- node bench/wcag22-feasibility-boundary.bench.mjs - --verify - - name: independently fingerprint the exact #296-B WASM + --verify bench/wcag22-feasibility-wasm-boundary-v3.json + - name: independently fingerprint both execution-role WASM artifacts run: | - bytes="$(wc -c < packages/colors/pkg/labcolors_bg.wasm | tr -d '[:space:]')" - sha256="$(sha256sum packages/colors/pkg/labcolors_bg.wasm | cut -d ' ' -f1)" - echo "canonical candidate raw=${bytes}B sha256=${sha256}" - - name: "upload exact #296-B verified whole-call evidence" + for artifact in \ + packages/colors/pkg/labcolors_bg.wasm \ + packages/colors/compiler/labcolors_compiler_bg.wasm + do + bytes="$(wc -c < "$artifact" | tr -d '[:space:]')" + sha256="$(sha256sum "$artifact" | cut -d ' ' -f1)" + echo "canonical candidate artifact=${artifact} raw=${bytes}B sha256=${sha256}" + done + sha256sum packages/colors/bench/wcag22-feasibility-wasm-boundary-v3.json + - name: upload exact #296-C1 whole-call evidence uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: wcag22-feasibility-wasm-boundary-${{ github.sha }}-attempt-${{ github.run_attempt }} path: | - packages/colors/bench/wcag22-feasibility-wasm-boundary-v2.json + packages/colors/bench/wcag22-feasibility-wasm-boundary-v3.json packages/colors/pkg/labcolors_bg.wasm + packages/colors/compiler/labcolors_compiler_bg.wasm if-no-files-found: error retention-days: 30 - - name: enforce measured WASM raw-byte budget - # Issue #296 owns the current exact Linux x64 measurement and zero-headroom - # ceiling. Issues #284/#295 remain immutable historical inputs. - # gzip is transport diagnostics only. + - name: enforce measured WASM role budgets + # Runtime and compiler have independent exact Linux x64 zero-headroom + # ratchets. gzip remains transport diagnostics only. run: node scripts/check-wasm-size-budget.mjs - name: "@labpics/colors: typecheck + runtime tests" # Now that pkg/ exists (built above), the package's public types resolve @@ -690,7 +773,9 @@ jobs: - name: wasm-pack test (headless chrome) # Chrome is provisioned above; the parity smoke runs against native # resolve_set inside the same wasm runtime. D1 default from the chapter. - run: wasm-pack test --headless --chrome --chromedriver "$CHROMEDRIVER_PATH" crates/labcolors-wasm --locked + run: | + wasm-pack test --headless --chrome --chromedriver "$CHROMEDRIVER_PATH" crates/labcolors-wasm --locked + wasm-pack test --headless --chrome --chromedriver "$CHROMEDRIVER_PATH" crates/labcolors-compiler --locked docs-drift: name: docs-drift (нейминг-канон) runs-on: [self-hosted, Linux, X64] diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 700e58e0..f22d322c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -274,6 +274,15 @@ jobs: throw new Error(`release artifact rejected: ${message}`); } + function exactKeys(value, expected, label) { + const actual = value && typeof value === "object" && !Array.isArray(value) + ? Object.keys(value).sort() + : []; + if (actual.join(",") !== [...expected].sort().join(",")) { + fail(`${label} has a non-canonical shape`); + } + } + function walk(directory, files = []) { for (const entry of readdirSync(directory, { withFileTypes: true })) { const path = join(directory, entry.name); @@ -313,13 +322,14 @@ jobs: const manifest = JSON.parse(readFileSync(manifests[0], "utf8")); const expectedVersion = expectedTag.slice("colors-v".length); - if (manifest.schemaVersion !== 2) fail(`manifest schema ${manifest.schemaVersion}`); + if (manifest.schemaVersion !== 3) fail(`manifest schema ${manifest.schemaVersion}`); if (manifest.sourceSha !== expectedSha) { fail(`manifest sourceSha ${manifest.sourceSha} != ${expectedSha}`); } if (manifest.npm !== expectedVersion) { fail(`manifest npm ${manifest.npm} != tag version ${expectedVersion}`); } + exactKeys(manifest.artifacts, ["tarball", "wasm", "buildMetadata"], "manifest artifacts"); const tarball = tarballs[0]; const bytes = readFileSync(tarball); @@ -335,6 +345,82 @@ jobs: fail(`tarball sha256 ${digest} != manifest ${evidence.sha256}`); } + const expectedWasm = [ + ["runtime", "pkg/labcolors_bg.wasm"], + ["compiler", "compiler/labcolors_compiler_bg.wasm"], + ]; + const wasmEvidence = manifest.artifacts?.wasm; + if (!Array.isArray(wasmEvidence) || wasmEvidence.length !== expectedWasm.length) { + fail("manifest must bind exactly the runtime and compiler WASM roles"); + } + for (let index = 0; index < expectedWasm.length; index += 1) { + const [role, path] = expectedWasm[index]; + const record = wasmEvidence[index]; + const keys = record && typeof record === "object" ? Object.keys(record) : []; + if (keys.join(",") !== "role,path,bytes,sha256") { + fail(`${role} WASM record has a non-canonical shape`); + } + if (record.role !== role || record.path !== path) { + fail(`WASM role ${index} must be ${role} at ${path}`); + } + const packedWasm = execFileSync("tar", ["-xOzf", tarball, `package/${path}`]); + const packedDigest = createHash("sha256").update(packedWasm).digest("hex"); + if (record.bytes !== packedWasm.length || record.sha256 !== packedDigest) { + fail(`${role} WASM record does not match the exact tarball bytes`); + } + } + + const metadataEvidence = manifest.artifacts.buildMetadata; + exactKeys(metadataEvidence, ["path", "bytes", "sha256"], "build metadata record"); + if (metadataEvidence.path !== "build-metadata.json") { + fail(`build metadata path ${metadataEvidence.path}`); + } + const packedMetadata = execFileSync( + "tar", + ["-xOzf", tarball, "package/build-metadata.json"], + ); + const packedMetadataDigest = createHash("sha256").update(packedMetadata).digest("hex"); + if ( + metadataEvidence.bytes !== packedMetadata.length || + metadataEvidence.sha256 !== packedMetadataDigest + ) { + fail("build metadata record does not match the exact tarball bytes"); + } + const metadata = JSON.parse(packedMetadata.toString("utf8")); + exactKeys( + metadata, + ["schemaVersion", "package", "sourceSha", "coreVersion", "conformance", "wasm"], + "packed build metadata", + ); + exactKeys(metadata.package, ["name", "version"], "packed build metadata package"); + exactKeys( + metadata.conformance, + ["packVersion", "packDigest", "manifestSha256", "familySetSha256"], + "packed build metadata conformance", + ); + if ( + metadata.schemaVersion !== 2 || + metadata.package.name !== "@labpics/colors" || + metadata.package.version !== expectedVersion || + metadata.sourceSha !== expectedSha || + metadata.coreVersion !== manifest.core || + metadata.conformance.packVersion !== manifest.conformance?.packVersion || + metadata.conformance.packDigest !== manifest.conformance?.packDigest || + metadata.conformance.manifestSha256 !== manifest.conformance?.manifestSha256 || + metadata.conformance.familySetSha256 !== manifest.conformance?.familySetSha256 || + !Array.isArray(metadata.wasm) || + metadata.wasm.length !== wasmEvidence.length + ) { + fail("packed build metadata does not bind the release identity and conformance"); + } + for (let index = 0; index < wasmEvidence.length; index += 1) { + const metadataWasm = metadata.wasm[index]; + exactKeys(metadataWasm, ["role", "path", "bytes", "sha256"], "metadata WASM record"); + if (JSON.stringify(metadataWasm) !== JSON.stringify(wasmEvidence[index])) { + fail(`metadata WASM role ${index} differs from the release manifest`); + } + } + const packedPackage = JSON.parse( execFileSync("tar", ["-xOzf", tarball, "package/package.json"], { encoding: "utf8", diff --git a/CHANGELOG.md b/CHANGELOG.md index 676e0611..180d5b7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,8 +24,8 @@ Migration-note: [exact alpha / typed Glow](docs/migrations/exact-alpha-glow.md), - npm package несёт byte-exact profile/table/proof в `evidence/`; release verifier и clean-install gate повторно проверяют их хэши и содержимое. - `labcolors-protocol` задаёт единственную versioned bytes→Core→wire границу - complete-feasibility. npm принимает только `Uint8Array`, Swift — `Data` или - `[UInt8]`; + complete-feasibility. `@labpics/colors/compiler` принимает только + `Uint8Array`, Swift — `Data` или `[UInt8]`; обе поверхности сохраняют `Success(Feasible | Infeasible | NotEvaluated)` либо typed `Failure` и не воспроизводят математику Core. - Rust Core принимает также непустой явный конечный набор пар «opaque ID + @@ -41,7 +41,14 @@ Migration-note: [exact alpha / typed Glow](docs/migrations/exact-alpha-glow.md), typed conflict/resource failures и opaque-ID law. Шесть прежних family остаются byte-identical. -### Breaking (Rust API) +### Несовместимые изменения (npm API) + +- Complete-feasibility API и его request/outcome types перенесены из package + root в `@labpics/colors/compiler`. Offline compiler загружает отдельный WASM + через `@labpics/colors/compiler/wasm`; runtime dependency cone больше не + содержит feasibility protocol. + +### Несовместимые изменения (Rust API) - Удалены `classify_at_least_v1`, `AtLeastDecisionV1` и `DecisionGuaranteeV1`: сравнительная «сила гарантии» как данные допускала lossy-схлопывание @@ -78,24 +85,24 @@ Migration-note: [exact alpha / typed Glow](docs/migrations/exact-alpha-glow.md), capsules. Profile V1, proof ID `wcag22-srgb8-full-domain-q55-v1` и package path `evidence/wcag22-srgb8-q55-proof-v1.json` не меняются: это отдельные version domains, а доказанная математика и finite artifact прежние. -- WASM size history стала append-only: immutable V1 сохраняет допуск #284 - (`454385 B`), V2 — transport #295 (`521240 B` / `d37841…9ca0`), V3 — - Core-срез #296-A (`521231 B` / `779379…e029`), а текущий V4 — #296-B - (`520920 B` / `c179f4…f94ed`). Каждый допуск имеет нулевой headroom и точную - ссылку на неизменяемый V1 build recipe; V1/V2/V3 не переписаны. Whole-call V2 - сохраняет детерминированную request/outcome-проекцию 10 крайних форм × 5 - свежих процессов из V1 и привязан к текущему native admission; latency, - process maxRSS и WASM pages остаются наблюдениями, не SLO. +- WASM size history стала role-aware: V5 задаёт независимые exact Linux-x64 + size/SHA и рецепты `runtime`/`compiler` с нулевым headroom, не переписывая + V1–V4. Whole-call V3 проверяет dedicated compiler entry, связывает его с V5 + и native admission V4 и сохраняет детерминированную request/outcome-проекцию. + `initSync`, прогретая операция, process maxRSS и WASM pages остаются + наблюдениями, не SLO. - Native feasibility admission также append-only относительно принятого - `main`: V1/V2 проверяются в исторических snapshots, а текущий V3 связывает + `main`: V1–V3 проверяются в исторических snapshots, а текущий V4 связывает artifact SHA - `46ec939523a9aff4f253c4c74e997dfd95812a694b2507fae885ff60244ade3a` + `3c257c336bc403eee933990fd7188a3b0a6e89d0cbc983aff18846ef76206275` с одним точным dependency cone. Source-bound recorder проверяет Git objects, SHA-256 verifier/subject-файлов и точный `Cargo.lock` до сборки и после запуска; fresh target, пустая Cargo-config hierarchy и закрытая среда исключают ambient profile/flags/wrappers. Receipt фиксирует Rust/Cargo 1.96.0, SHA-256 обоих toolchain executables и реально запущенного benchmark binary, - явный feature set и explicit-empty compiler overrides; 71 негативная мутация + явный feature set и explicit-empty compiler overrides; V3/V4 сохраняют одну + deterministic scenario/identity-проекцию, поэтому C1 меняет только workspace + provenance и admission machinery, а не конечный алгоритм; 71 негативная мутация проверяет fail-closed границы. Сырые наблюдения сохранены без timing threshold, а промежуточные draft- артефакты не становятся публичной историей. @@ -104,9 +111,11 @@ Migration-note: [exact alpha / typed Glow](docs/migrations/exact-alpha-glow.md), закономерно изменён. `manifest.numericalCapabilities` зеркалит single public V2 core manifest (coverage `migrated-sites-only-v1`, FNV-1a-32 drift-checksum над canonical length-prefixed preimage). -- Release manifest schema v2: секция `numericalSites` заменена на - `numericalCapabilities`; release verifier и Swift conformance-тесты - пересчитывают capability checksum независимо от Rust-кода. +- Release manifest schema v3 сохраняет введённую в V2 секцию + `numericalCapabilities` и добавляет упорядоченные exact records обеих + WASM-ролей и build metadata. Publish read-back перепроверяет их по байтам + tarball; Swift conformance-тесты по-прежнему независимо пересчитывают + capability checksum. - Добавлен компилируемый numerical plan (`compile_numerical_plan_v1`) с канонической invocation identity и checksum — типизированная проекция того, какие site/mode заявляет сборка. diff --git a/Cargo.lock b/Cargo.lock index bd2faf41..8152afa0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -493,6 +493,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "labcolors-compiler" +version = "0.2.0" +dependencies = [ + "js-sys", + "labcolors-protocol", + "serde", + "serde_json", + "wasm-bindgen", + "wasm-bindgen-test", +] + [[package]] name = "labcolors-conformance" version = "0.2.0" @@ -542,7 +554,6 @@ dependencies = [ "js-sys", "labcolors-conformance", "labcolors-core", - "labcolors-protocol", "serde", "serde_json", "thiserror", diff --git a/Cargo.toml b/Cargo.toml index e64e7a0e..28645855 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ repository = "https://github.com/Labpics-Team/lab-colors" rust-version = "1.85" # Centralised dependency pins. `labcolors-core` stays zero-RUNTIME-deps (issue #29): -# wasm-bindgen lives ONLY in `labcolors-wasm`, which references it from here. +# only the runtime and offline-compiler WASM edge crates use this wasm-bindgen pin. # `proptest` is a DEV-only dependency of `labcolors-core` (property/fuzz invariants); # it never enters the runtime graph — proven by the supply-chain guards in # `tests/s2b_baseline_guards.rs` (`supply_chain_proptest_*`). diff --git a/README.md b/README.md index 679fd744..c234110c 100644 --- a/README.md +++ b/README.md @@ -31,10 +31,13 @@ NamedRoleTable source-over/screen операции несут проверяемый профиль и `bit-exact` сертификат; Glow требует явный decision profile и может завершиться типизированным `Indeterminate` без CSS fallback. -- **Полная проверка конечного домена.** Клиент может объявить opaque relations, - точные соседние sRGB8-цвета и критерии WCAG 2.2; Core полностью перечислит - зарегистрированную neutral-axis и вернёт packed feasible partition с - доказательством. Это проверка выполнимости, а не скрытая политика выбора. +- **Полная проверка конечного домена.** В compiler-контракте V1 клиент объявляет + opaque occurrence relations, точные соседние sRGB8-цвета и критерии WCAG 2.2, + а домен фиксирован зарегистрированной `srgb8-neutral-axis-v1`. Core полностью + перечисляет его и возвращает packed feasible partition с доказательством. В + npm это offline-операция `@labpics/colors/compiler` с отдельным WASM, а не + часть browser runtime; явный клиентский домен пока остаётся Core-only. Это + проверка выполнимости, а не скрытая политика выбора. - **Непрерывные семейства.** `ColorCurve` и реализации `NeutralCurve`/`AccentCurve` доступны как низкоуровневые вычислительные примитивы. - **Браузерное применение.** `applyTheme`, `watchTheme`, `adaptTheme` и `effectiveBackground` связывают результат WASM с локальной областью DOM. @@ -49,7 +52,7 @@ NamedRoleTable - Автоматический выбор человечески «лучшего», «чистого», «похожего на бренд» или культурно правильного цвета не является частью обязательного базового resolve. - P3, HDR, индивидуальное восприятие и неизвестное вмешательство user agent нельзя молча сводить к sRGB. -## Быстрый старт +## Быстрый старт в браузере Установите пакет: diff --git a/crates/labcolors-compiler/Cargo.toml b/crates/labcolors-compiler/Cargo.toml new file mode 100644 index 00000000..f325db78 --- /dev/null +++ b/crates/labcolors-compiler/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "labcolors-compiler" +publish = false +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true +description = "Offline WASM compiler operations for @labpics/colors/compiler." + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +labcolors-protocol = { path = "../labcolors-protocol" } +wasm-bindgen = { workspace = true } +js-sys = "0.3" + +[dev-dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1" +wasm-bindgen-test = "0.3" + +[package.metadata.wasm-pack.profile.release] +wasm-opt = ["-Oz", "--enable-bulk-memory", "--enable-nontrapping-float-to-int"] diff --git a/crates/labcolors-compiler/src/lib.rs b/crates/labcolors-compiler/src/lib.rs new file mode 100644 index 00000000..d097ecc6 --- /dev/null +++ b/crates/labcolors-compiler/src/lib.rs @@ -0,0 +1,275 @@ +//! Offline compiler boundary for `@labpics/colors/compiler`. +//! +//! This crate is intentionally a mechanical WASM shell over the versioned +//! protocol. It owns no theme vocabulary, runtime state, colour solver or wire +//! projection; those remain in `labcolors-core` and `labcolors-protocol`. + +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(typescript_custom_section)] +const TS_COMPILER_TYPES: &'static str = r##" +import type { Wcag22CriterionV1 } from "../wcag22.js"; + +/** + * Exact non-negative `u64` emitted as canonical decimal JSON text. + * + * Output-only branding prevents TypeScript from pretending that an arbitrary + * integer-looking string has passed the Rust range/canonicality check. + */ +declare const decimalU64V1Brand: unique symbol; +export type DecimalU64V1 = string & { + readonly [decimalU64V1Brand]: "DecimalU64V1"; +}; + +/** One exact final encoded-sRGB8 colour. */ +export type Srgb8BytesV1 = readonly [number, number, number]; + +/** One exact SHA-256 digest or 256-bit LSB0 partition. */ +export type Bytes32V1 = readonly [ + number, number, number, number, number, number, number, number, + number, number, number, number, number, number, number, number, + number, number, number, number, number, number, number, number, + number, number, number, number, number, number, number, number, +]; + +export interface Wcag22FeasibilityApplicableRelationV1 { + readonly relationId: string; + readonly occurrenceId: string; + readonly kind: "applicable"; + readonly criterion: Wcag22CriterionV1; + readonly adjacent: ReadonlyArray; +} + +export interface Wcag22FeasibilityNotApplicableRelationV1 { + readonly relationId: string; + readonly occurrenceId: string; + readonly kind: "notApplicable"; + readonly reasonId: string; +} + +export type Wcag22FeasibilityRelationV1 = + | Wcag22FeasibilityApplicableRelationV1 + | Wcag22FeasibilityNotApplicableRelationV1; + +/** Strict decoded form of the UTF-8 JSON accepted by the byte API. */ +export interface Wcag22FeasibilityRequestV1 { + readonly schemaVersion: 1; + readonly domainId: "srgb8-neutral-axis-v1"; + readonly resourceProfileId: "compile-v1"; + readonly relations: ReadonlyArray; +} + +export interface Wcag22FeasibilityProofV1 { + readonly evaluationId: Bytes32V1; + readonly resourceProfileId: "compile-v1"; + readonly domainId: "srgb8-neutral-axis-v1"; + readonly domainDigest: Bytes32V1; + readonly domainCount: DecimalU64V1; + readonly domainFirst: Srgb8BytesV1; + readonly domainLast: Srgb8BytesV1; + readonly relationSetDigest: Bytes32V1; + readonly canonicalRelations: DecimalU64V1; + readonly applicableRelations: DecimalU64V1; + readonly notApplicableRelations: DecimalU64V1; + readonly applicableEdges: DecimalU64V1; + readonly logicalAssessments: DecimalU64V1; + readonly matrixDigest: Bytes32V1; + /** Exact 256-bit candidate partition, candidate-index LSB0. */ + readonly partition: Bytes32V1; + readonly wcag22ProfileId: "wcag22-srgb8-contrast-v1"; + readonly artifactId: "wcag22-srgb8-luminance-q55-v1"; + readonly boundId: "wcag22-srgb8-outward-q55-v1"; + readonly proofId: "wcag22-srgb8-full-domain-q55-v1"; + readonly proofSha256: Bytes32V1; +} + +export interface Wcag22FeasibilityEvaluatedV1 { + /** The complete registered domain in Core-owned candidate order, once. */ + readonly domain: ReadonlyArray; + /** Canonical declarations, once; no per-cell relation duplication. */ + readonly relations: ReadonlyArray; + /** Candidate-major failure bits at `candidate * E + edge`, packed LSB0. */ + readonly failureMatrix: ReadonlyArray; + readonly proof: Wcag22FeasibilityProofV1; +} + +export interface Wcag22FeasibilityNotEvaluatedResultV1 { + readonly domainId: "srgb8-neutral-axis-v1"; + readonly domainDigest: Bytes32V1; + readonly relationSetDigest: Bytes32V1; + readonly resourceProfileId: "compile-v1"; + readonly relations: ReadonlyArray; +} + +export type Wcag22FeasibilityV1 = + | { readonly status: "feasible"; readonly result: Wcag22FeasibilityEvaluatedV1 } + | { readonly status: "infeasible"; readonly result: Wcag22FeasibilityEvaluatedV1 } + | { readonly status: "notEvaluated"; readonly result: Wcag22FeasibilityNotEvaluatedResultV1 }; + +export type Wcag22FeasibilityTransportErrorV1 = + | { + readonly code: "envelopeTooLarge"; + readonly requestedBytes: DecimalU64V1; + readonly limitBytes: DecimalU64V1; + } + | { readonly code: "invalidUtf8" } + | { + readonly code: "malformedEnvelope"; + readonly class: "syntax" | "shape" | "endOfInput" | "io"; + } + | { readonly code: "unsupportedSchemaVersion"; readonly received: number } + | { readonly code: "unsupportedDomainId"; readonly received: string } + | { readonly code: "unsupportedResourceProfileId"; readonly received: string } + | { readonly code: "unsupportedCriterion"; readonly received: string } + | { readonly code: "emptyNotApplicableReason" }; + +export type Wcag22FeasibilityInvalidRequestV1 = + | { readonly code: "emptyRelationId" } + | { readonly code: "emptyOccurrenceId" } + | { readonly code: "emptyRelations" } + | { readonly code: "emptyAdjacentSet"; readonly relationId: string } + | { readonly code: "conflictingRelationId"; readonly relationId: string } + | { readonly code: "arithmeticOverflow" }; + +export type Wcag22FeasibilityAtomicErrorV1 = + | { readonly code: "invalidSrgb8"; readonly field: string; readonly reason: string } + | { readonly code: "emptyNotApplicableReason" } + | { + readonly code: "artifactInvariantViolation"; + readonly criterion: Wcag22CriterionV1; + readonly foreground: Srgb8BytesV1; + readonly background: Srgb8BytesV1; + } + | { readonly code: "evidenceRegistryMismatch"; readonly message: string }; + +export type Wcag22FeasibilityEvaluatorInvariantV1 = + | { readonly code: "source"; readonly details: Wcag22FeasibilityAtomicErrorV1 } + | { readonly code: "unexpectedNotEvaluated" } + | { readonly code: "inputMismatch" } + | { readonly code: "criterionMismatch" } + | { readonly code: "evidenceMismatch" }; + +export type Wcag22FeasibilityCompilerInvariantV1 = + | { readonly code: "layoutMismatch" } + | { + readonly code: "assessmentCardinalityMismatch"; + readonly expected: DecimalU64V1; + readonly observed: DecimalU64V1; + } + | { + readonly code: "candidateCardinalityMismatch"; + readonly expected: DecimalU64V1; + readonly observed: DecimalU64V1; + } + | { readonly code: "decisionStorageRejectedCell" } + | { readonly code: "decisionStorageRejectedPartition" } + | { readonly code: "completeResultMismatch" }; + +export type Wcag22FeasibilityResourceDimensionV1 = + | "rawRelations" + | "rawAdjacentEntries" + | "opaqueUtf8Bytes" + | "canonicalRelations" + | "applicableEdges" + | "logicalAssessments" + | "packedResultBytes"; + +export type Wcag22FeasibilityCoreErrorV1 = + | { + readonly code: "invalidRequest"; + readonly details: Wcag22FeasibilityInvalidRequestV1; + } + | { + readonly code: "resourceLimitExceeded"; + readonly details: { + readonly profileId: "compile-v1"; + readonly dimension: Wcag22FeasibilityResourceDimensionV1; + readonly requested: DecimalU64V1; + readonly limit: DecimalU64V1; + }; + } + | { + readonly code: "allocationFailed"; + readonly details: { + readonly profileId: "compile-v1"; + readonly requestedBytes: DecimalU64V1; + }; + } + | { + readonly code: "evaluatorInvariantViolation"; + readonly details: { + readonly candidate: Srgb8BytesV1; + readonly relationId: string; + readonly adjacent: Srgb8BytesV1; + readonly violation: Wcag22FeasibilityEvaluatorInvariantV1; + }; + } + | { + readonly code: "compilerInvariantViolation"; + readonly details: Wcag22FeasibilityCompilerInvariantV1; + }; + +export type Wcag22FeasibilityProtocolErrorV1 = + | { readonly source: "transport"; readonly error: Wcag22FeasibilityTransportErrorV1 } + | { readonly source: "core"; readonly error: Wcag22FeasibilityCoreErrorV1 } + | { readonly source: "incompatibleCoreContract" }; + +export type Wcag22FeasibilityOutcomeV1 = + | { + readonly schemaVersion: 1; + readonly outcome: "success"; + readonly feasibility: Wcag22FeasibilityV1; + } + | { + readonly schemaVersion: 1; + readonly outcome: "failure"; + readonly error: Wcag22FeasibilityProtocolErrorV1; + }; +"##; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "Wcag22FeasibilityOutcomeV1")] + pub type JsWcag22FeasibilityOutcomeV1; +} + +const WASM_MAX_ENVELOPE_BYTES_V1: u32 = { + assert!(labcolors_protocol::MAX_ENVELOPE_BYTES_V1 <= u32::MAX as u64); + labcolors_protocol::MAX_ENVELOPE_BYTES_V1 as u32 +}; + +/// Exact protocol-owned ceiling exposed as a JavaScript `number`. +#[wasm_bindgen(js_name = wcag22FeasibilityMaxRequestBytesV1)] +pub fn wcag22_feasibility_max_request_bytes_v1() -> u32 { + WASM_MAX_ENVELOPE_BYTES_V1 +} + +/// Evaluate one strict V1 UTF-8 JSON byte envelope. +#[wasm_bindgen(js_name = evaluateWcag22FeasibilityV1)] +pub fn evaluate_wcag22_feasibility_v1( + request: &[u8], +) -> Result { + protocol_outcome_to_js(labcolors_protocol::evaluate_wcag22_feasibility_v1(request)) +} + +/// Construct the canonical oversize failure without copying a rejected input. +#[wasm_bindgen(js_name = wcag22FeasibilityEnvelopeTooLargeV1)] +pub fn wcag22_feasibility_envelope_too_large_v1( + requested_bytes: u64, +) -> Result { + protocol_outcome_to_js(labcolors_protocol::envelope_too_large_outcome_v1( + requested_bytes, + )) +} + +fn protocol_outcome_to_js( + outcome: labcolors_protocol::ProtocolOutcomeV1, +) -> Result { + let encoded = labcolors_protocol::encode_outcome_v1(&outcome) + .map_err(|error| JsError::new(&format!("protocol encoding failed: {error}")))?; + let json = std::str::from_utf8(&encoded) + .map_err(|_| JsError::new("protocol emitted non-UTF-8 JSON"))?; + let parsed = + js_sys::JSON::parse(json).map_err(|_| JsError::new("protocol JSON did not parse"))?; + Ok(parsed.unchecked_into()) +} diff --git a/crates/labcolors-compiler/tests/wasm_parity.rs b/crates/labcolors-compiler/tests/wasm_parity.rs new file mode 100644 index 00000000..069cabe7 --- /dev/null +++ b/crates/labcolors-compiler/tests/wasm_parity.rs @@ -0,0 +1,61 @@ +#![cfg(target_arch = "wasm32")] + +use labcolors_compiler::{ + evaluate_wcag22_feasibility_v1, wcag22_feasibility_envelope_too_large_v1, + wcag22_feasibility_max_request_bytes_v1, +}; +use serde::Deserialize; +use wasm_bindgen::JsValue; +use wasm_bindgen_test::*; + +wasm_bindgen_test_configure!(run_in_browser); + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct Vector { + case_id: String, + request_json: String, + outcome_json: String, +} + +fn json_text(value: &JsValue) -> String { + js_sys::JSON::stringify(value) + .expect("value is JSON-serializable") + .as_string() + .expect("JSON.stringify returns text") +} + +#[wasm_bindgen_test] +fn compiler_replays_the_canonical_protocol_family_byte_exactly() { + let vectors: Vec = serde_json::from_str(include_str!( + "../../../conformance/vectors/wcag22-feasibility.json" + )) + .expect("canonical family parses"); + assert!(!vectors.is_empty(), "anti-vacuum: family is non-empty"); + + for vector in vectors { + let outcome: JsValue = evaluate_wcag22_feasibility_v1(vector.request_json.as_bytes()) + .expect("canonical projection") + .into(); + assert_eq!( + json_text(&outcome), + vector.outcome_json, + "{}", + vector.case_id + ); + } +} + +#[wasm_bindgen_test] +fn compiler_rechecks_its_protocol_owned_envelope_ceiling() { + let limit = wcag22_feasibility_max_request_bytes_v1(); + assert_eq!(u64::from(limit), labcolors_protocol::MAX_ENVELOPE_BYTES_V1); + + let raw: JsValue = evaluate_wcag22_feasibility_v1(&vec![b' '; limit as usize + 1]) + .expect("oversize failure is data") + .into(); + let scalar: JsValue = wcag22_feasibility_envelope_too_large_v1(u64::from(limit) + 1) + .expect("scalar oversize projection") + .into(); + assert_eq!(json_text(&raw), json_text(&scalar)); +} diff --git a/crates/labcolors-core/benches/wcag22_feasibility_admission.rs b/crates/labcolors-core/benches/wcag22_feasibility_admission.rs index cb8b1966..02182e98 100644 --- a/crates/labcolors-core/benches/wcag22_feasibility_admission.rs +++ b/crates/labcolors-core/benches/wcag22_feasibility_admission.rs @@ -13,7 +13,7 @@ //! //! ```text //! python3 scripts/check_wcag22_feasibility_benchmark.py \ -//! /private/tmp/labcolors-wcag22-feasibility-admission-raw-v3.json \ +//! /private/tmp/labcolors-wcag22-feasibility-admission-raw-v4.json \ //! --record --record-toolchain 1.96.0 --record-sample-count 5 //! ``` @@ -39,8 +39,8 @@ use labcolors_core::wcag22_feasibility::{ #[path = "../src/sha256.rs"] mod subject_sha256; -const ARTIFACT_ID: &str = "wcag22-feasibility-admission-raw-v3"; -const DEFAULT_OUTPUT_FILENAME: &str = "labcolors-wcag22-feasibility-admission-raw-v3.json"; +const ARTIFACT_ID: &str = "wcag22-feasibility-admission-raw-v4"; +const DEFAULT_OUTPUT_FILENAME: &str = "labcolors-wcag22-feasibility-admission-raw-v4.json"; const CANDIDATE_COUNT: u64 = 256; const PAGE_BYTES: u64 = 65_536; const DECISION_SLOT_BYTES: u64 = 32; diff --git a/crates/labcolors-core/contracts/wcag22-feasibility-benchmark-v4.json b/crates/labcolors-core/contracts/wcag22-feasibility-benchmark-v4.json new file mode 100644 index 00000000..56180a4c --- /dev/null +++ b/crates/labcolors-core/contracts/wcag22-feasibility-benchmark-v4.json @@ -0,0 +1,1072 @@ +{ + "schemaVersion": 1, + "artifactId": "wcag22-feasibility-admission-raw-v4", + "recordProvenance": { + "recipeId": "closed-cargo-bench-v1", + "sourceSnapshotSha256": "6dc79d4257c23f9580308b65bde8c9637ec025c1d61ac86c1585872dc9330f2d", + "benchmarkBinarySha256": "69fe95cea34c845478c0a3c260e3e4459f1bc09d76857b74d5edb50fd923410a" + }, + "claimBoundary": "native-process-observations-and-page-slot-arithmetic-only", + "notMeasured": [ + "webassembly-runtime-memory", + "serialized-output-size", + "client-latency" + ], + "environment": { + "execution": "native-process", + "targetArch": "aarch64", + "targetOs": "macos", + "pointerWidthBits": 64, + "debugAssertions": false, + "packageVersion": "0.2.0", + "allocator": "std::alloc::System", + "allocatorInstrumentationIncludedInElapsedTime": true, + "timer": "std::time::Instant", + "measurementThreads": 1, + "requestConstructionMeasured": false, + "rustcVerbose": "rustc 1.96.0 (ac68faa20 2026-05-25)\nbinary: rustc\ncommit-hash: ac68faa20c58cbccd01ee7208bf3b6e93a7d7f96\ncommit-date: 2026-05-25\nhost: aarch64-apple-darwin\nrelease: 1.96.0\nLLVM version: 22.1.2", + "cargoVerbose": "cargo 1.96.0 (30a34c682 2026-05-25)\nrelease: 1.96.0\ncommit-hash: 30a34c6821b57de0aaec83a901aca39f88f6778c\ncommit-date: 2026-05-25\nhost: aarch64-apple-darwin\nlibgit2: 1.9.2 (sys:0.20.4 vendored)\nlibcurl: 8.7.1 (sys:0.4.87+curl-8.19.0 system ssl:(SecureTransport) LibreSSL/3.3.6)\nssl: OpenSSL 3.5.4 30 Sep 2025\nos: Mac OS 26.2.0 [64-bit]", + "activeCoreFeatures": [ + "wcag22-feasibility", + "wcag22-explicit-feasibility" + ], + "explicitEmptyBuildInputs": [ + "CARGO_ENCODED_RUSTFLAGS", + "RUSTC_WRAPPER", + "RUSTC_WORKSPACE_WRAPPER" + ], + "rustcBinarySha256": "c5922366bfe3d6d028a65d626f4e629b3adad066995cf0b60c8a4b617bba5ffe", + "cargoBinarySha256": "fec239e6b74df873f54ef52912bfcfcc8d8414bc14a7ae1e0be80460bae72841", + "sourceConeClean": true, + "sampleCountExplicit": true, + "sourceObjects": { + "workspaceCargo": { + "path": "Cargo.toml", + "gitObject": "2864585584e98c8d99802a3eb440a2d2cee85c7f" + }, + "workspaceLock": { + "path": "Cargo.lock", + "gitObject": "8152afa0fb6656fbe0a367880d5f7a3b1122c09b" + }, + "coreCargo": { + "path": "crates/labcolors-core/Cargo.toml", + "gitObject": "4e9c0893754f1a6d131d98f85f0101902f0991c2" + }, + "coreSourceTree": { + "path": "crates/labcolors-core/src", + "gitObject": "676cde66a135def2819b8467c35adfb7c7404e63" + }, + "wcag22Srgb8Contract": { + "path": "crates/labcolors-core/contracts/wcag22-srgb8-v1.json", + "gitObject": "7b3932335398bdbc0760136904cb245305749b09" + }, + "wcag22Q55ProofContract": { + "path": "crates/labcolors-core/contracts/wcag22-srgb8-q55-proof-v1.json", + "gitObject": "ddce1912e8e06ce6bd55c9854acf27a28a31b134" + }, + "benchmarkHarness": { + "path": "crates/labcolors-core/benches/wcag22_feasibility_admission.rs", + "gitObject": "02182e98ea901d53d04d0fb24942252f563ebfa7" + }, + "benchmarkChecker": { + "path": "scripts/check_wcag22_feasibility_benchmark.py", + "gitObject": "c07ec8029b5c7c6b53398d4ed6e778322bd2dbea" + } + } + }, + "warmupSamples": 0, + "scenarioOrder": "as-emitted", + "sampleCount": 5, + "admissionStatus": "measurement-only-unless-admission-check-passes", + "hardSlo": { + "class": "deterministic-work-and-storage", + "logicalAssessmentLaw": "W=256E", + "packedStorageLaw": "B=0 if A=0, otherwise B=32(E+1)", + "partialTerminalAllowed": false, + "timingThresholdNs": null, + "allRequiredShapesMustComplete": true + }, + "boundedEnvelopeModel": { + "scope": "product-policy-capacity-arithmetic-not-total-memory", + "referenceBoundedBytes": 65536, + "candidateCount": 256, + "decisionSlotBytes": 32, + "partitionBytes": 32, + "reservedPartitionSlots": 1, + "maximumCardinality": 2047, + "maximumLogicalAssessments": 524032, + "maximumPackedResultBytes": 65536 + }, + "subjectManifest": [ + { + "path": "Cargo.toml", + "sha256": "fe8cee2f18a9df79d2de8d6ca6ce968237463ae732792114516d13b2c39d6da4" + }, + { + "path": "crates/labcolors-core/src/lib.rs", + "sha256": "a98651f9b4ab45237fbc04bda90cc260b98e6caf6b99fada63407745cdbd3d64" + }, + { + "path": "crates/labcolors-core/src/wcag22_feasibility.rs", + "sha256": "f57f4d2008880911fdb83e99598744c57cbc66286464dc2296ff31b5b495d96e" + }, + { + "path": "crates/labcolors-core/src/wcag22_feasibility/explicit.rs", + "sha256": "ead9f24e830661037bb723d14d03c152fbedb2a1074a8ec0591153484f8db112" + }, + { + "path": "crates/labcolors-core/src/srgb8.rs", + "sha256": "9a37ea3f25f7ab3e2ae064e5a5b5c7d51a880a537c297169d6014fa32e593e17" + }, + { + "path": "crates/labcolors-core/src/sha256.rs", + "sha256": "09827c91997db66ade30ec7b68b422b4204004296e17b2ee6de49af9ea5b002a" + }, + { + "path": "crates/labcolors-core/src/wcag22.rs", + "sha256": "3e648301d01515f221a783625fa18f41859989f00ceda1cfc3360205d29ab852" + }, + { + "path": "crates/labcolors-core/src/wcag22/kernel.rs", + "sha256": "c97980c1ca2c7ea9cabff9c8d2fb7282773cca180ae15948391c29c9d6196040" + }, + { + "path": "crates/labcolors-core/src/wcag22/q55_data.rs", + "sha256": "af4d23d6b70c45ce6efa839e7dda4bb0a61f6aae43cb805af6fa9b29e6c3bae2" + }, + { + "path": "crates/labcolors-core/src/wcag22_evidence.rs", + "sha256": "3c5a75b07254c6071a64700af208a64987d0f0ea9698eadc54a9e74585ce1f72" + }, + { + "path": "crates/labcolors-core/src/numerics.rs", + "sha256": "ef10c32534f8e3af7c895aaf465a142184ed651d11634d6c0bbb60db4ff2304f" + }, + { + "path": "crates/labcolors-core/contracts/wcag22-srgb8-v1.json", + "sha256": "b4bb7e5f17a99f2c911fdbe3da23a48b049277b796291094950f14680cc3cc7b" + }, + { + "path": "crates/labcolors-core/contracts/wcag22-srgb8-q55-proof-v1.json", + "sha256": "d269e9de689009bb955788bf8762fce56680bf616fc0459b6526a367875a6a08" + }, + { + "path": "crates/labcolors-core/Cargo.toml", + "sha256": "46cf9970ab05d821187b30ff98f0fc9ab070ef546f82817a49d76f6e473e80ff" + }, + { + "path": "Cargo.lock", + "sha256": "2bba351e22e0264fceb3ae9153bd2d7eb465b19d6051fc482a60314483b17e6d" + }, + { + "path": "crates/labcolors-core/benches/wcag22_feasibility_admission.rs", + "sha256": "d28b360c7c93a784c9e4734334148930936ba9ee47b12745164de69ef8fa4f4d" + }, + { + "path": "scripts/check_wcag22_feasibility_benchmark.py", + "sha256": "546a90598a2c7fdd12e7a98c12a7c1dbc89d572c500701987140261a1ac2233e" + } + ], + "profileLimits": { + "profileId": "compile-v1", + "rawRelations": 2047, + "rawAdjacentEntries": 2047, + "opaqueUtf8Bytes": 65536, + "canonicalRelations": 2047, + "applicableEdges": 2047, + "logicalAssessments": 524032, + "packedResultBytes": 65536 + }, + "scenarios": [ + { + "name": "minimum-evaluated", + "shape": { + "rawRelations": 1, + "rawAdjacentEntries": 1, + "opaqueUtf8Bytes": 2, + "canonicalRelations": 1, + "applicableRelations": 1, + "applicableEdges": 1 + }, + "expected": { + "terminal": "feasible", + "logicalAssessments": 256, + "packedResultBytes": 64, + "feasibleCandidates": 7 + }, + "observedIdentity": { + "terminal": "feasible", + "domainDigestSha256": "9634ac326979b23c2103ffcd92a2b890427ea8914a97b264b0c73409640f8466", + "relationSetDigestSha256": "770f11c7b289f17427541399fa49f4e747a4e84d04a3689e3a7e8f0cea968ecc", + "evaluationIdSha256": "c2ff3ed8394f040a1e01ccf92c07442f8fb06f2e8fcb735412c8a4803a81c5db", + "logicalAssessments": 256, + "assessmentIteratorLen": 256, + "derivedPackedResultBytes": 64, + "feasibleCandidates": 7 + }, + "samples": [ + { + "index": 0, + "elapsedNs": 39334, + "allocationCalls": 1, + "allocatedBytes": 64, + "deallocationCalls": 0, + "deallocatedBytes": 0, + "baselineLiveBytes": 3560, + "endLiveBytes": 3624, + "peakLiveBytes": 3624, + "peakAdditionalLiveBytes": 64 + }, + { + "index": 1, + "elapsedNs": 18417, + "allocationCalls": 1, + "allocatedBytes": 64, + "deallocationCalls": 0, + "deallocatedBytes": 0, + "baselineLiveBytes": 3560, + "endLiveBytes": 3624, + "peakLiveBytes": 3624, + "peakAdditionalLiveBytes": 64 + }, + { + "index": 2, + "elapsedNs": 18042, + "allocationCalls": 1, + "allocatedBytes": 64, + "deallocationCalls": 0, + "deallocatedBytes": 0, + "baselineLiveBytes": 3560, + "endLiveBytes": 3624, + "peakLiveBytes": 3624, + "peakAdditionalLiveBytes": 64 + }, + { + "index": 3, + "elapsedNs": 17916, + "allocationCalls": 1, + "allocatedBytes": 64, + "deallocationCalls": 0, + "deallocatedBytes": 0, + "baselineLiveBytes": 3560, + "endLiveBytes": 3624, + "peakLiveBytes": 3624, + "peakAdditionalLiveBytes": 64 + }, + { + "index": 4, + "elapsedNs": 17917, + "allocationCalls": 1, + "allocatedBytes": 64, + "deallocationCalls": 0, + "deallocatedBytes": 0, + "baselineLiveBytes": 3560, + "endLiveBytes": 3624, + "peakLiveBytes": 3624, + "peakAdditionalLiveBytes": 64 + } + ] + }, + { + "name": "maximum-applicable-edges", + "shape": { + "rawRelations": 1, + "rawAdjacentEntries": 2047, + "opaqueUtf8Bytes": 19, + "canonicalRelations": 1, + "applicableRelations": 1, + "applicableEdges": 2047 + }, + "expected": { + "terminal": "infeasible", + "logicalAssessments": 524032, + "packedResultBytes": 65536, + "feasibleCandidates": 0 + }, + "observedIdentity": { + "terminal": "infeasible", + "domainDigestSha256": "9634ac326979b23c2103ffcd92a2b890427ea8914a97b264b0c73409640f8466", + "relationSetDigestSha256": "3498263d9b09b28b10b0a76147bff13bef9844f44b4fe70a3585e6404efac947", + "evaluationIdSha256": "b6011501962a5f2eb55817d2ddab783f37d0eef036ace829bec2675511af394d", + "logicalAssessments": 524032, + "assessmentIteratorLen": 524032, + "derivedPackedResultBytes": 65536, + "feasibleCandidates": 0 + }, + "samples": [ + { + "index": 0, + "elapsedNs": 14313042, + "allocationCalls": 1, + "allocatedBytes": 65536, + "deallocationCalls": 0, + "deallocatedBytes": 0, + "baselineLiveBytes": 10115, + "endLiveBytes": 75651, + "peakLiveBytes": 75651, + "peakAdditionalLiveBytes": 65536 + }, + { + "index": 1, + "elapsedNs": 14298958, + "allocationCalls": 1, + "allocatedBytes": 65536, + "deallocationCalls": 0, + "deallocatedBytes": 0, + "baselineLiveBytes": 10115, + "endLiveBytes": 75651, + "peakLiveBytes": 75651, + "peakAdditionalLiveBytes": 65536 + }, + { + "index": 2, + "elapsedNs": 15975250, + "allocationCalls": 1, + "allocatedBytes": 65536, + "deallocationCalls": 0, + "deallocatedBytes": 0, + "baselineLiveBytes": 10115, + "endLiveBytes": 75651, + "peakLiveBytes": 75651, + "peakAdditionalLiveBytes": 65536 + }, + { + "index": 3, + "elapsedNs": 14321292, + "allocationCalls": 1, + "allocatedBytes": 65536, + "deallocationCalls": 0, + "deallocatedBytes": 0, + "baselineLiveBytes": 10115, + "endLiveBytes": 75651, + "peakLiveBytes": 75651, + "peakAdditionalLiveBytes": 65536 + }, + { + "index": 4, + "elapsedNs": 14414875, + "allocationCalls": 1, + "allocatedBytes": 65536, + "deallocationCalls": 0, + "deallocatedBytes": 0, + "baselineLiveBytes": 10115, + "endLiveBytes": 75651, + "peakLiveBytes": 75651, + "peakAdditionalLiveBytes": 65536 + } + ] + }, + { + "name": "maximum-raw-duplicate-relations", + "shape": { + "rawRelations": 2047, + "rawAdjacentEntries": 2047, + "opaqueUtf8Bytes": 26611, + "canonicalRelations": 1, + "applicableRelations": 1, + "applicableEdges": 1 + }, + "expected": { + "terminal": "feasible", + "logicalAssessments": 256, + "packedResultBytes": 64, + "feasibleCandidates": 7 + }, + "observedIdentity": { + "terminal": "feasible", + "domainDigestSha256": "9634ac326979b23c2103ffcd92a2b890427ea8914a97b264b0c73409640f8466", + "relationSetDigestSha256": "2d8c3d7dce6f7caee600851331aabbd45692a0cb530f1d58f08afb6f0a9aacc2", + "evaluationIdSha256": "83fa34dd186f1692438b5594fcae93b5d10746891e7e400b62f063b4f459cc1a", + "logicalAssessments": 256, + "assessmentIteratorLen": 256, + "derivedPackedResultBytes": 64, + "feasibleCandidates": 7 + }, + "samples": [ + { + "index": 0, + "elapsedNs": 137000, + "allocationCalls": 1, + "allocatedBytes": 64, + "deallocationCalls": 4092, + "deallocatedBytes": 14322, + "baselineLiveBytes": 166005, + "endLiveBytes": 151747, + "peakLiveBytes": 166005, + "peakAdditionalLiveBytes": 0 + }, + { + "index": 1, + "elapsedNs": 137458, + "allocationCalls": 1, + "allocatedBytes": 64, + "deallocationCalls": 4092, + "deallocatedBytes": 14322, + "baselineLiveBytes": 166005, + "endLiveBytes": 151747, + "peakLiveBytes": 166005, + "peakAdditionalLiveBytes": 0 + }, + { + "index": 2, + "elapsedNs": 136708, + "allocationCalls": 1, + "allocatedBytes": 64, + "deallocationCalls": 4092, + "deallocatedBytes": 14322, + "baselineLiveBytes": 166005, + "endLiveBytes": 151747, + "peakLiveBytes": 166005, + "peakAdditionalLiveBytes": 0 + }, + { + "index": 3, + "elapsedNs": 136541, + "allocationCalls": 1, + "allocatedBytes": 64, + "deallocationCalls": 4092, + "deallocatedBytes": 14322, + "baselineLiveBytes": 166005, + "endLiveBytes": 151747, + "peakLiveBytes": 166005, + "peakAdditionalLiveBytes": 0 + }, + { + "index": 4, + "elapsedNs": 136417, + "allocationCalls": 1, + "allocatedBytes": 64, + "deallocationCalls": 4092, + "deallocatedBytes": 14322, + "baselineLiveBytes": 166005, + "endLiveBytes": 151747, + "peakLiveBytes": 166005, + "peakAdditionalLiveBytes": 0 + } + ] + }, + { + "name": "maximum-raw-adjacent-duplicates", + "shape": { + "rawRelations": 1, + "rawAdjacentEntries": 2047, + "opaqueUtf8Bytes": 2, + "canonicalRelations": 1, + "applicableRelations": 1, + "applicableEdges": 1 + }, + "expected": { + "terminal": "feasible", + "logicalAssessments": 256, + "packedResultBytes": 64, + "feasibleCandidates": 7 + }, + "observedIdentity": { + "terminal": "feasible", + "domainDigestSha256": "9634ac326979b23c2103ffcd92a2b890427ea8914a97b264b0c73409640f8466", + "relationSetDigestSha256": "770f11c7b289f17427541399fa49f4e747a4e84d04a3689e3a7e8f0cea968ecc", + "evaluationIdSha256": "c2ff3ed8394f040a1e01ccf92c07442f8fb06f2e8fcb735412c8a4803a81c5db", + "logicalAssessments": 256, + "assessmentIteratorLen": 256, + "derivedPackedResultBytes": 64, + "feasibleCandidates": 7 + }, + "samples": [ + { + "index": 0, + "elapsedNs": 30541, + "allocationCalls": 1, + "allocatedBytes": 64, + "deallocationCalls": 0, + "deallocatedBytes": 0, + "baselineLiveBytes": 10898, + "endLiveBytes": 10962, + "peakLiveBytes": 10962, + "peakAdditionalLiveBytes": 64 + }, + { + "index": 1, + "elapsedNs": 30875, + "allocationCalls": 1, + "allocatedBytes": 64, + "deallocationCalls": 0, + "deallocatedBytes": 0, + "baselineLiveBytes": 10898, + "endLiveBytes": 10962, + "peakLiveBytes": 10962, + "peakAdditionalLiveBytes": 64 + }, + { + "index": 2, + "elapsedNs": 30625, + "allocationCalls": 1, + "allocatedBytes": 64, + "deallocationCalls": 0, + "deallocatedBytes": 0, + "baselineLiveBytes": 10898, + "endLiveBytes": 10962, + "peakLiveBytes": 10962, + "peakAdditionalLiveBytes": 64 + }, + { + "index": 3, + "elapsedNs": 30583, + "allocationCalls": 1, + "allocatedBytes": 64, + "deallocationCalls": 0, + "deallocatedBytes": 0, + "baselineLiveBytes": 10898, + "endLiveBytes": 10962, + "peakLiveBytes": 10962, + "peakAdditionalLiveBytes": 64 + }, + { + "index": 4, + "elapsedNs": 30625, + "allocationCalls": 1, + "allocatedBytes": 64, + "deallocationCalls": 0, + "deallocatedBytes": 0, + "baselineLiveBytes": 10898, + "endLiveBytes": 10962, + "peakLiveBytes": 10962, + "peakAdditionalLiveBytes": 64 + } + ] + }, + { + "name": "maximum-canonical-applicable-relations", + "shape": { + "rawRelations": 2047, + "rawAdjacentEntries": 2047, + "opaqueUtf8Bytes": 20470, + "canonicalRelations": 2047, + "applicableRelations": 2047, + "applicableEdges": 2047 + }, + "expected": { + "terminal": "feasible", + "logicalAssessments": 524032, + "packedResultBytes": 65536, + "feasibleCandidates": 7 + }, + "observedIdentity": { + "terminal": "feasible", + "domainDigestSha256": "9634ac326979b23c2103ffcd92a2b890427ea8914a97b264b0c73409640f8466", + "relationSetDigestSha256": "2c034157301d1ebd68d207201ff4f18e38400a1bddea7824a77079a0ed96d41c", + "evaluationIdSha256": "db69a53550117574de7167b6f40dcfb8e213f5710dcd6ef16d6487259bd6bdc5", + "logicalAssessments": 524032, + "assessmentIteratorLen": 524032, + "derivedPackedResultBytes": 65536, + "feasibleCandidates": 7 + }, + "samples": [ + { + "index": 0, + "elapsedNs": 15489708, + "allocationCalls": 1, + "allocatedBytes": 65536, + "deallocationCalls": 0, + "deallocatedBytes": 0, + "baselineLiveBytes": 224089, + "endLiveBytes": 289625, + "peakLiveBytes": 289625, + "peakAdditionalLiveBytes": 65536 + }, + { + "index": 1, + "elapsedNs": 15401625, + "allocationCalls": 1, + "allocatedBytes": 65536, + "deallocationCalls": 0, + "deallocatedBytes": 0, + "baselineLiveBytes": 224089, + "endLiveBytes": 289625, + "peakLiveBytes": 289625, + "peakAdditionalLiveBytes": 65536 + }, + { + "index": 2, + "elapsedNs": 15559250, + "allocationCalls": 1, + "allocatedBytes": 65536, + "deallocationCalls": 0, + "deallocatedBytes": 0, + "baselineLiveBytes": 224089, + "endLiveBytes": 289625, + "peakLiveBytes": 289625, + "peakAdditionalLiveBytes": 65536 + }, + { + "index": 3, + "elapsedNs": 15334250, + "allocationCalls": 1, + "allocatedBytes": 65536, + "deallocationCalls": 0, + "deallocatedBytes": 0, + "baselineLiveBytes": 224089, + "endLiveBytes": 289625, + "peakLiveBytes": 289625, + "peakAdditionalLiveBytes": 65536 + }, + { + "index": 4, + "elapsedNs": 15591750, + "allocationCalls": 1, + "allocatedBytes": 65536, + "deallocationCalls": 0, + "deallocatedBytes": 0, + "baselineLiveBytes": 224089, + "endLiveBytes": 289625, + "peakLiveBytes": 289625, + "peakAdditionalLiveBytes": 65536 + } + ] + }, + { + "name": "maximum-combined-applicable-envelope", + "shape": { + "rawRelations": 2047, + "rawAdjacentEntries": 2047, + "opaqueUtf8Bytes": 65536, + "canonicalRelations": 2047, + "applicableRelations": 2047, + "applicableEdges": 2047 + }, + "expected": { + "terminal": "feasible", + "logicalAssessments": 524032, + "packedResultBytes": 65536, + "feasibleCandidates": 7 + }, + "observedIdentity": { + "terminal": "feasible", + "domainDigestSha256": "9634ac326979b23c2103ffcd92a2b890427ea8914a97b264b0c73409640f8466", + "relationSetDigestSha256": "647fbd1e61b7b10a58a32f00c6f9f2c1fd4deca9e685e6f682f26ec792dc5a84", + "evaluationIdSha256": "1d71ce6dea709b3977c252e814d0981dfeda2d3d1c42e9c2a1ff60848b4c118a", + "logicalAssessments": 524032, + "assessmentIteratorLen": 524032, + "derivedPackedResultBytes": 65536, + "feasibleCandidates": 7 + }, + "samples": [ + { + "index": 0, + "elapsedNs": 16643209, + "allocationCalls": 1, + "allocatedBytes": 65536, + "deallocationCalls": 0, + "deallocatedBytes": 0, + "baselineLiveBytes": 263414, + "endLiveBytes": 328950, + "peakLiveBytes": 328950, + "peakAdditionalLiveBytes": 65536 + }, + { + "index": 1, + "elapsedNs": 16068417, + "allocationCalls": 1, + "allocatedBytes": 65536, + "deallocationCalls": 0, + "deallocatedBytes": 0, + "baselineLiveBytes": 263414, + "endLiveBytes": 328950, + "peakLiveBytes": 328950, + "peakAdditionalLiveBytes": 65536 + }, + { + "index": 2, + "elapsedNs": 17495375, + "allocationCalls": 1, + "allocatedBytes": 65536, + "deallocationCalls": 0, + "deallocatedBytes": 0, + "baselineLiveBytes": 263414, + "endLiveBytes": 328950, + "peakLiveBytes": 328950, + "peakAdditionalLiveBytes": 65536 + }, + { + "index": 3, + "elapsedNs": 15681958, + "allocationCalls": 1, + "allocatedBytes": 65536, + "deallocationCalls": 0, + "deallocatedBytes": 0, + "baselineLiveBytes": 263414, + "endLiveBytes": 328950, + "peakLiveBytes": 328950, + "peakAdditionalLiveBytes": 65536 + }, + { + "index": 4, + "elapsedNs": 15630500, + "allocationCalls": 1, + "allocatedBytes": 65536, + "deallocationCalls": 0, + "deallocatedBytes": 0, + "baselineLiveBytes": 263414, + "endLiveBytes": 328950, + "peakLiveBytes": 328950, + "peakAdditionalLiveBytes": 65536 + } + ] + }, + { + "name": "maximum-canonical-not-applicable-relations", + "shape": { + "rawRelations": 2047, + "rawAdjacentEntries": 0, + "opaqueUtf8Bytes": 36846, + "canonicalRelations": 2047, + "applicableRelations": 0, + "applicableEdges": 0 + }, + "expected": { + "terminal": "not-evaluated", + "logicalAssessments": 0, + "packedResultBytes": 0, + "feasibleCandidates": null + }, + "observedIdentity": { + "terminal": "not-evaluated", + "domainDigestSha256": "9634ac326979b23c2103ffcd92a2b890427ea8914a97b264b0c73409640f8466", + "relationSetDigestSha256": "7e5f031ce0c8ef68c6667d9ae05bcf9818d8d34b1095fdbbc2dd7bc8f8748089", + "evaluationIdSha256": null, + "logicalAssessments": 0, + "assessmentIteratorLen": 0, + "derivedPackedResultBytes": 0, + "feasibleCandidates": null + }, + "samples": [ + { + "index": 0, + "elapsedNs": 605584, + "allocationCalls": 0, + "allocatedBytes": 0, + "deallocationCalls": 0, + "deallocatedBytes": 0, + "baselineLiveBytes": 235124, + "endLiveBytes": 235124, + "peakLiveBytes": 235124, + "peakAdditionalLiveBytes": 0 + }, + { + "index": 1, + "elapsedNs": 635791, + "allocationCalls": 0, + "allocatedBytes": 0, + "deallocationCalls": 0, + "deallocatedBytes": 0, + "baselineLiveBytes": 235124, + "endLiveBytes": 235124, + "peakLiveBytes": 235124, + "peakAdditionalLiveBytes": 0 + }, + { + "index": 2, + "elapsedNs": 625958, + "allocationCalls": 0, + "allocatedBytes": 0, + "deallocationCalls": 0, + "deallocatedBytes": 0, + "baselineLiveBytes": 235124, + "endLiveBytes": 235124, + "peakLiveBytes": 235124, + "peakAdditionalLiveBytes": 0 + }, + { + "index": 3, + "elapsedNs": 618917, + "allocationCalls": 0, + "allocatedBytes": 0, + "deallocationCalls": 0, + "deallocatedBytes": 0, + "baselineLiveBytes": 235124, + "endLiveBytes": 235124, + "peakLiveBytes": 235124, + "peakAdditionalLiveBytes": 0 + }, + { + "index": 4, + "elapsedNs": 636000, + "allocationCalls": 0, + "allocatedBytes": 0, + "deallocationCalls": 0, + "deallocatedBytes": 0, + "baselineLiveBytes": 235124, + "endLiveBytes": 235124, + "peakLiveBytes": 235124, + "peakAdditionalLiveBytes": 0 + } + ] + }, + { + "name": "maximum-combined-not-applicable-envelope", + "shape": { + "rawRelations": 2047, + "rawAdjacentEntries": 0, + "opaqueUtf8Bytes": 65536, + "canonicalRelations": 2047, + "applicableRelations": 0, + "applicableEdges": 0 + }, + "expected": { + "terminal": "not-evaluated", + "logicalAssessments": 0, + "packedResultBytes": 0, + "feasibleCandidates": null + }, + "observedIdentity": { + "terminal": "not-evaluated", + "domainDigestSha256": "9634ac326979b23c2103ffcd92a2b890427ea8914a97b264b0c73409640f8466", + "relationSetDigestSha256": "68d40f750b9e1e7520290ce21a5077b5eb19d44308dd5c44076a080486182bc2", + "evaluationIdSha256": null, + "logicalAssessments": 0, + "assessmentIteratorLen": 0, + "derivedPackedResultBytes": 0, + "feasibleCandidates": null + }, + "samples": [ + { + "index": 0, + "elapsedNs": 780583, + "allocationCalls": 0, + "allocatedBytes": 0, + "deallocationCalls": 0, + "deallocatedBytes": 0, + "baselineLiveBytes": 284652, + "endLiveBytes": 284652, + "peakLiveBytes": 284652, + "peakAdditionalLiveBytes": 0 + }, + { + "index": 1, + "elapsedNs": 782000, + "allocationCalls": 0, + "allocatedBytes": 0, + "deallocationCalls": 0, + "deallocatedBytes": 0, + "baselineLiveBytes": 284652, + "endLiveBytes": 284652, + "peakLiveBytes": 284652, + "peakAdditionalLiveBytes": 0 + }, + { + "index": 2, + "elapsedNs": 785584, + "allocationCalls": 0, + "allocatedBytes": 0, + "deallocationCalls": 0, + "deallocatedBytes": 0, + "baselineLiveBytes": 284652, + "endLiveBytes": 284652, + "peakLiveBytes": 284652, + "peakAdditionalLiveBytes": 0 + }, + { + "index": 3, + "elapsedNs": 781000, + "allocationCalls": 0, + "allocatedBytes": 0, + "deallocationCalls": 0, + "deallocatedBytes": 0, + "baselineLiveBytes": 284652, + "endLiveBytes": 284652, + "peakLiveBytes": 284652, + "peakAdditionalLiveBytes": 0 + }, + { + "index": 4, + "elapsedNs": 782458, + "allocationCalls": 0, + "allocatedBytes": 0, + "deallocationCalls": 0, + "deallocatedBytes": 0, + "baselineLiveBytes": 284652, + "endLiveBytes": 284652, + "peakLiveBytes": 284652, + "peakAdditionalLiveBytes": 0 + } + ] + }, + { + "name": "maximum-mixed-relations", + "shape": { + "rawRelations": 2047, + "rawAdjacentEntries": 1023, + "opaqueUtf8Bytes": 28662, + "canonicalRelations": 2047, + "applicableRelations": 1023, + "applicableEdges": 1023 + }, + "expected": { + "terminal": "feasible", + "logicalAssessments": 261888, + "packedResultBytes": 32768, + "feasibleCandidates": 7 + }, + "observedIdentity": { + "terminal": "feasible", + "domainDigestSha256": "9634ac326979b23c2103ffcd92a2b890427ea8914a97b264b0c73409640f8466", + "relationSetDigestSha256": "dbe4493668b49a058babc3188b84fc40f321bfd80d8bef22ec9d63f775033eed", + "evaluationIdSha256": "ab188519042404430aa7e71b4766ebf3b64f22fd88cff7898e7bf7ae65b298c2", + "logicalAssessments": 261888, + "assessmentIteratorLen": 261888, + "derivedPackedResultBytes": 32768, + "feasibleCandidates": 7 + }, + "samples": [ + { + "index": 0, + "elapsedNs": 8321542, + "allocationCalls": 1, + "allocatedBytes": 32768, + "deallocationCalls": 0, + "deallocatedBytes": 0, + "baselineLiveBytes": 230809, + "endLiveBytes": 263577, + "peakLiveBytes": 263577, + "peakAdditionalLiveBytes": 32768 + }, + { + "index": 1, + "elapsedNs": 8139333, + "allocationCalls": 1, + "allocatedBytes": 32768, + "deallocationCalls": 0, + "deallocatedBytes": 0, + "baselineLiveBytes": 230809, + "endLiveBytes": 263577, + "peakLiveBytes": 263577, + "peakAdditionalLiveBytes": 32768 + }, + { + "index": 2, + "elapsedNs": 8166875, + "allocationCalls": 1, + "allocatedBytes": 32768, + "deallocationCalls": 0, + "deallocatedBytes": 0, + "baselineLiveBytes": 230809, + "endLiveBytes": 263577, + "peakLiveBytes": 263577, + "peakAdditionalLiveBytes": 32768 + }, + { + "index": 3, + "elapsedNs": 8553417, + "allocationCalls": 1, + "allocatedBytes": 32768, + "deallocationCalls": 0, + "deallocatedBytes": 0, + "baselineLiveBytes": 230809, + "endLiveBytes": 263577, + "peakLiveBytes": 263577, + "peakAdditionalLiveBytes": 32768 + }, + { + "index": 4, + "elapsedNs": 8190000, + "allocationCalls": 1, + "allocatedBytes": 32768, + "deallocationCalls": 0, + "deallocatedBytes": 0, + "baselineLiveBytes": 230809, + "endLiveBytes": 263577, + "peakLiveBytes": 263577, + "peakAdditionalLiveBytes": 32768 + } + ] + }, + { + "name": "maximum-opaque-utf8-bytes", + "shape": { + "rawRelations": 1, + "rawAdjacentEntries": 0, + "opaqueUtf8Bytes": 65536, + "canonicalRelations": 1, + "applicableRelations": 0, + "applicableEdges": 0 + }, + "expected": { + "terminal": "not-evaluated", + "logicalAssessments": 0, + "packedResultBytes": 0, + "feasibleCandidates": null + }, + "observedIdentity": { + "terminal": "not-evaluated", + "domainDigestSha256": "9634ac326979b23c2103ffcd92a2b890427ea8914a97b264b0c73409640f8466", + "relationSetDigestSha256": "d89fd489c0f419f8e6ef2ac97f164d2ee1b4b20dc961895da8061688aa24cc2c", + "evaluationIdSha256": null, + "logicalAssessments": 0, + "assessmentIteratorLen": 0, + "derivedPackedResultBytes": 0, + "feasibleCandidates": null + }, + "samples": [ + { + "index": 0, + "elapsedNs": 334875, + "allocationCalls": 0, + "allocatedBytes": 0, + "deallocationCalls": 0, + "deallocatedBytes": 0, + "baselineLiveBytes": 72691, + "endLiveBytes": 72691, + "peakLiveBytes": 72691, + "peakAdditionalLiveBytes": 0 + }, + { + "index": 1, + "elapsedNs": 334584, + "allocationCalls": 0, + "allocatedBytes": 0, + "deallocationCalls": 0, + "deallocatedBytes": 0, + "baselineLiveBytes": 72691, + "endLiveBytes": 72691, + "peakLiveBytes": 72691, + "peakAdditionalLiveBytes": 0 + }, + { + "index": 2, + "elapsedNs": 335208, + "allocationCalls": 0, + "allocatedBytes": 0, + "deallocationCalls": 0, + "deallocatedBytes": 0, + "baselineLiveBytes": 72691, + "endLiveBytes": 72691, + "peakLiveBytes": 72691, + "peakAdditionalLiveBytes": 0 + }, + { + "index": 3, + "elapsedNs": 334334, + "allocationCalls": 0, + "allocatedBytes": 0, + "deallocationCalls": 0, + "deallocatedBytes": 0, + "baselineLiveBytes": 72691, + "endLiveBytes": 72691, + "peakLiveBytes": 72691, + "peakAdditionalLiveBytes": 0 + }, + { + "index": 4, + "elapsedNs": 335458, + "allocationCalls": 0, + "allocatedBytes": 0, + "deallocationCalls": 0, + "deallocatedBytes": 0, + "baselineLiveBytes": 72691, + "endLiveBytes": 72691, + "peakLiveBytes": 72691, + "peakAdditionalLiveBytes": 0 + } + ] + } + ] +} diff --git a/crates/labcolors-wasm/Cargo.toml b/crates/labcolors-wasm/Cargo.toml index 670d7f02..3532e899 100644 --- a/crates/labcolors-wasm/Cargo.toml +++ b/crates/labcolors-wasm/Cargo.toml @@ -15,14 +15,12 @@ description = "WASM bindings for the labcolors-core contrast engine, packaged as crate-type = ["cdylib", "rlib"] [dependencies] -# Direct Core use stays isolated from optional compiler capabilities. The -# versioned protocol crate owns only the registered-domain WCAG22 projection. +# Runtime owns only point evaluation and the adaptive theme engine. Offline +# compiler operations have a separate Cargo root and physical WASM artifact. labcolors-core = { path = "../labcolors-core", default-features = false } -labcolors-protocol = { path = "../labcolors-protocol" } wasm-bindgen = { workspace = true } # js-sys ships with the wasm-bindgen toolchain (no new third-party tree). It is -# used by existing JavaScript boundary code and to parse protocol-owned JSON; -# the protocol crate, not this adapter, owns serde projection. +# used by existing JavaScript boundary code. js-sys = "0.3" # Derive-only: thiserror is a proc-macro with no runtime code, so it adds # nothing to the WASM bundle while giving matchable, well-described errors. diff --git a/crates/labcolors-wasm/src/lib.rs b/crates/labcolors-wasm/src/lib.rs index b8a5c803..e2d69df4 100644 --- a/crates/labcolors-wasm/src/lib.rs +++ b/crates/labcolors-wasm/src/lib.rs @@ -38,6 +38,8 @@ use crate::error::BindingError; /// full typing without a hand-written `.d.ts`. #[wasm_bindgen(typescript_custom_section)] const TS_RESULT_TYPES: &'static str = r##" +import type { Wcag22CriterionV1 } from "../wcag22.js"; + /** The stable theme contract. `-ic` variants apply increased contrast; all four spellings are fully supported. */ export type ThemeName = "light" | "dark" | "light-ic" | "dark-ic"; @@ -463,11 +465,6 @@ export interface NumericalCapabilityManifestV2 { readonly checksum: string; } -export type Wcag22CriterionV1 = - | "sc-1.4.3-text-default" - | "sc-1.4.3-text-large-scale" - | "sc-1.4.11-ui-component-or-state" - | "sc-1.4.11-graphical-object"; export type Wcag22DecisionV1 = "pass" | "fail"; export interface Wcag22Q55BoundsV1 { /** Decimal u64 string: Q55 values exceed JavaScript's safe integer range. */ @@ -499,222 +496,6 @@ export interface Wcag22AssessmentV1 { }; } -/** - * Exact non-negative `u64` emitted as canonical decimal JSON text. - * - * Output-only branding prevents TypeScript from pretending that an arbitrary - * integer-looking string has passed the Rust range/canonicality check. - */ -declare const decimalU64V1Brand: unique symbol; -export type DecimalU64V1 = string & { - readonly [decimalU64V1Brand]: "DecimalU64V1"; -}; - -/** One exact final encoded-sRGB8 colour. */ -export type Srgb8BytesV1 = readonly [number, number, number]; - -/** One exact SHA-256 digest or 256-bit LSB0 partition. */ -export type Bytes32V1 = readonly [ - number, number, number, number, number, number, number, number, - number, number, number, number, number, number, number, number, - number, number, number, number, number, number, number, number, - number, number, number, number, number, number, number, number, -]; - -export interface Wcag22FeasibilityApplicableRelationV1 { - readonly relationId: string; - readonly occurrenceId: string; - readonly kind: "applicable"; - readonly criterion: Wcag22CriterionV1; - readonly adjacent: ReadonlyArray; -} - -export interface Wcag22FeasibilityNotApplicableRelationV1 { - readonly relationId: string; - readonly occurrenceId: string; - readonly kind: "notApplicable"; - readonly reasonId: string; -} - -export type Wcag22FeasibilityRelationV1 = - | Wcag22FeasibilityApplicableRelationV1 - | Wcag22FeasibilityNotApplicableRelationV1; - -/** Strict decoded form of the UTF-8 JSON accepted by the byte API. */ -export interface Wcag22FeasibilityRequestV1 { - readonly schemaVersion: 1; - readonly domainId: "srgb8-neutral-axis-v1"; - readonly resourceProfileId: "compile-v1"; - readonly relations: ReadonlyArray; -} - -export interface Wcag22FeasibilityProofV1 { - readonly evaluationId: Bytes32V1; - readonly resourceProfileId: "compile-v1"; - readonly domainId: "srgb8-neutral-axis-v1"; - readonly domainDigest: Bytes32V1; - readonly domainCount: DecimalU64V1; - readonly domainFirst: Srgb8BytesV1; - readonly domainLast: Srgb8BytesV1; - readonly relationSetDigest: Bytes32V1; - readonly canonicalRelations: DecimalU64V1; - readonly applicableRelations: DecimalU64V1; - readonly notApplicableRelations: DecimalU64V1; - readonly applicableEdges: DecimalU64V1; - readonly logicalAssessments: DecimalU64V1; - readonly matrixDigest: Bytes32V1; - /** Exact 256-bit candidate partition, candidate-index LSB0. */ - readonly partition: Bytes32V1; - readonly wcag22ProfileId: "wcag22-srgb8-contrast-v1"; - readonly artifactId: "wcag22-srgb8-luminance-q55-v1"; - readonly boundId: "wcag22-srgb8-outward-q55-v1"; - readonly proofId: "wcag22-srgb8-full-domain-q55-v1"; - readonly proofSha256: Bytes32V1; -} - -export interface Wcag22FeasibilityEvaluatedV1 { - /** The complete registered domain in Core-owned candidate order, once. */ - readonly domain: ReadonlyArray; - /** Canonical declarations, once; no per-cell relation duplication. */ - readonly relations: ReadonlyArray; - /** Candidate-major failure bits at `candidate * E + edge`, packed LSB0. */ - readonly failureMatrix: ReadonlyArray; - readonly proof: Wcag22FeasibilityProofV1; -} - -export interface Wcag22FeasibilityNotEvaluatedResultV1 { - readonly domainId: "srgb8-neutral-axis-v1"; - readonly domainDigest: Bytes32V1; - readonly relationSetDigest: Bytes32V1; - readonly resourceProfileId: "compile-v1"; - readonly relations: ReadonlyArray; -} - -export type Wcag22FeasibilityV1 = - | { readonly status: "feasible"; readonly result: Wcag22FeasibilityEvaluatedV1 } - | { readonly status: "infeasible"; readonly result: Wcag22FeasibilityEvaluatedV1 } - | { readonly status: "notEvaluated"; readonly result: Wcag22FeasibilityNotEvaluatedResultV1 }; - -export type Wcag22FeasibilityTransportErrorV1 = - | { - readonly code: "envelopeTooLarge"; - readonly requestedBytes: DecimalU64V1; - readonly limitBytes: DecimalU64V1; - } - | { readonly code: "invalidUtf8" } - | { - readonly code: "malformedEnvelope"; - readonly class: "syntax" | "shape" | "endOfInput" | "io"; - } - | { readonly code: "unsupportedSchemaVersion"; readonly received: number } - | { readonly code: "unsupportedDomainId"; readonly received: string } - | { readonly code: "unsupportedResourceProfileId"; readonly received: string } - | { readonly code: "unsupportedCriterion"; readonly received: string } - | { readonly code: "emptyNotApplicableReason" }; - -export type Wcag22FeasibilityInvalidRequestV1 = - | { readonly code: "emptyRelationId" } - | { readonly code: "emptyOccurrenceId" } - | { readonly code: "emptyRelations" } - | { readonly code: "emptyAdjacentSet"; readonly relationId: string } - | { readonly code: "conflictingRelationId"; readonly relationId: string } - | { readonly code: "arithmeticOverflow" }; - -export type Wcag22FeasibilityAtomicErrorV1 = - | { readonly code: "invalidSrgb8"; readonly field: string; readonly reason: string } - | { readonly code: "emptyNotApplicableReason" } - | { - readonly code: "artifactInvariantViolation"; - readonly criterion: Wcag22CriterionV1; - readonly foreground: Srgb8BytesV1; - readonly background: Srgb8BytesV1; - } - | { readonly code: "evidenceRegistryMismatch"; readonly message: string }; - -export type Wcag22FeasibilityEvaluatorInvariantV1 = - | { readonly code: "source"; readonly details: Wcag22FeasibilityAtomicErrorV1 } - | { readonly code: "unexpectedNotEvaluated" } - | { readonly code: "inputMismatch" } - | { readonly code: "criterionMismatch" } - | { readonly code: "evidenceMismatch" }; - -export type Wcag22FeasibilityCompilerInvariantV1 = - | { readonly code: "layoutMismatch" } - | { - readonly code: "assessmentCardinalityMismatch"; - readonly expected: DecimalU64V1; - readonly observed: DecimalU64V1; - } - | { - readonly code: "candidateCardinalityMismatch"; - readonly expected: DecimalU64V1; - readonly observed: DecimalU64V1; - } - | { readonly code: "decisionStorageRejectedCell" } - | { readonly code: "decisionStorageRejectedPartition" } - | { readonly code: "completeResultMismatch" }; - -export type Wcag22FeasibilityResourceDimensionV1 = - | "rawRelations" - | "rawAdjacentEntries" - | "opaqueUtf8Bytes" - | "canonicalRelations" - | "applicableEdges" - | "logicalAssessments" - | "packedResultBytes"; - -export type Wcag22FeasibilityCoreErrorV1 = - | { - readonly code: "invalidRequest"; - readonly details: Wcag22FeasibilityInvalidRequestV1; - } - | { - readonly code: "resourceLimitExceeded"; - readonly details: { - readonly profileId: "compile-v1"; - readonly dimension: Wcag22FeasibilityResourceDimensionV1; - readonly requested: DecimalU64V1; - readonly limit: DecimalU64V1; - }; - } - | { - readonly code: "allocationFailed"; - readonly details: { - readonly profileId: "compile-v1"; - readonly requestedBytes: DecimalU64V1; - }; - } - | { - readonly code: "evaluatorInvariantViolation"; - readonly details: { - readonly candidate: Srgb8BytesV1; - readonly relationId: string; - readonly adjacent: Srgb8BytesV1; - readonly violation: Wcag22FeasibilityEvaluatorInvariantV1; - }; - } - | { - readonly code: "compilerInvariantViolation"; - readonly details: Wcag22FeasibilityCompilerInvariantV1; - }; - -export type Wcag22FeasibilityProtocolErrorV1 = - | { readonly source: "transport"; readonly error: Wcag22FeasibilityTransportErrorV1 } - | { readonly source: "core"; readonly error: Wcag22FeasibilityCoreErrorV1 } - | { readonly source: "incompatibleCoreContract" }; - -export type Wcag22FeasibilityOutcomeV1 = - | { - readonly schemaVersion: 1; - readonly outcome: "success"; - readonly feasibility: Wcag22FeasibilityV1; - } - | { - readonly schemaVersion: 1; - readonly outcome: "failure"; - readonly error: Wcag22FeasibilityProtocolErrorV1; - }; - /** The full result of resolving one background under one theme. */ export interface ResolvedTheme { readonly theme: ThemeName; @@ -747,8 +528,6 @@ extern "C" { #[wasm_bindgen(typescript_type = "Wcag22AssessmentV1")] pub type JsWcag22Assessment; - #[wasm_bindgen(typescript_type = "Wcag22FeasibilityOutcomeV1")] - pub type JsWcag22FeasibilityOutcomeV1; } /// Единственный public numerical capability manifest: proof-capable V2. @@ -805,61 +584,6 @@ pub fn evaluate_wcag22( Ok(parsed.unchecked_into()) } -const WASM_MAX_ENVELOPE_BYTES_V1: u32 = { - assert!(labcolors_protocol::MAX_ENVELOPE_BYTES_V1 <= u32::MAX as u64); - labcolors_protocol::MAX_ENVELOPE_BYTES_V1 as u32 -}; - -/// Exact protocol-owned ceiling exposed as a JavaScript `number`. -#[wasm_bindgen(js_name = wcag22FeasibilityMaxRequestBytesV1)] -pub fn wcag22_feasibility_max_request_bytes_v1() -> u32 { - WASM_MAX_ENVELOPE_BYTES_V1 -} - -/// Evaluate one exact V1 UTF-8 JSON byte envelope. -/// -/// Transport and Core failures are returned inside the typed outcome. Only an -/// impossible canonical-encoding/JSON.parse failure rejects with `JsError`. -#[wasm_bindgen(js_name = evaluateWcag22FeasibilityV1)] -pub fn evaluate_wcag22_feasibility_v1( - request: &[u8], -) -> Result { - protocol_outcome_to_js(labcolors_protocol::evaluate_wcag22_feasibility_v1(request)) -} - -/// Build the same canonical typed oversize failure without copying the raw -/// request into WebAssembly. The package-root host wrapper calls this scalar -/// helper after its avoidable-copy preflight; Rust still rechecks raw calls. -#[wasm_bindgen(js_name = wcag22FeasibilityEnvelopeTooLargeV1)] -pub fn wcag22_feasibility_envelope_too_large_v1( - requested_bytes: u64, -) -> Result { - protocol_outcome_to_js(labcolors_protocol::envelope_too_large_outcome_v1( - requested_bytes, - )) -} - -fn protocol_outcome_to_js( - outcome: labcolors_protocol::ProtocolOutcomeV1, -) -> Result { - let encoded = labcolors_protocol::encode_outcome_v1(&outcome).map_err(|error| { - to_js_error(BindingError::Internal { - reason: format!("WCAG22 feasibility protocol encoding failed: {error}"), - }) - })?; - let json = std::str::from_utf8(&encoded).map_err(|_| { - to_js_error(BindingError::Internal { - reason: "WCAG22 feasibility protocol emitted non-UTF-8 JSON".to_string(), - }) - })?; - let parsed = js_sys::JSON::parse(json).map_err(|_| { - to_js_error(BindingError::Internal { - reason: "WCAG22 feasibility protocol JSON did not parse".to_string(), - }) - })?; - Ok(parsed.unchecked_into()) -} - /// A contrast engine over a consumer-supplied design system. Construct with /// [`LabColors::new`], load a config with [`loadConfig`](LabColors::load_config), /// then call [`resolve_theme`](LabColors::resolve_theme) many times; identical @@ -1064,6 +788,10 @@ mod native_contract_tests { .expect("custom TypeScript section is extractable") } + fn shared_wcag22_types() -> &'static str { + include_str!("../../../packages/colors/wcag22.d.ts") + } + fn string_union<'a>(types: &'a str, name: &str) -> Vec<&'a str> { let declaration = format!("export type {name} ="); types @@ -1101,7 +829,7 @@ mod native_contract_tests { #[test] fn generated_wcag22_criterion_type_equals_the_core_wire_menu() { - let declared = string_union(custom_types(), "Wcag22CriterionV1"); + let declared = string_union(shared_wcag22_types(), "Wcag22CriterionV1"); let declared_set: std::collections::HashSet<&str> = declared.iter().copied().collect(); let core_set: std::collections::HashSet<&str> = labcolors_core::wcag22::Wcag22CriterionV1::ALL diff --git a/crates/labcolors-wasm/tests/wasm_parity.rs b/crates/labcolors-wasm/tests/wasm_parity.rs index 27903fc1..6ef1db4c 100644 --- a/crates/labcolors-wasm/tests/wasm_parity.rs +++ b/crates/labcolors-wasm/tests/wasm_parity.rs @@ -17,11 +17,8 @@ use labcolors_conformance::{ use labcolors_core::config::ThemeConfig; use labcolors_core::semantic::NamedRoleTable; use labcolors_core::{BgInput, Resolved, ViewingConditions, resolve_named_set}; +use labcolors_wasm::LabColors; use labcolors_wasm::config_dto::ConfigDto; -use labcolors_wasm::{ - LabColors, evaluate_wcag22_feasibility_v1, wcag22_feasibility_envelope_too_large_v1, - wcag22_feasibility_max_request_bytes_v1, -}; use wasm_bindgen::JsValue; use wasm_bindgen_test::*; @@ -108,64 +105,6 @@ fn json_text(value: &JsValue) -> String { .expect("JSON.stringify returns text") } -fn protocol_request(relations: Vec) -> Vec { - let request = labcolors_protocol::RequestV1::try_new( - labcolors_protocol::DomainIdV1::Srgb8NeutralAxis, - relations, - labcolors_protocol::ResourceProfileIdV1::Compile, - ) - .expect("protocol request is locally valid"); - labcolors_protocol::encode_request_v1(&request).expect("canonical request encoding") -} - -fn applicable_relation( - relation_id: &str, - occurrence_id: &str, - criterion: labcolors_protocol::Wcag22CriterionV1, - adjacent: Vec<[u8; 3]>, -) -> labcolors_protocol::RelationV1 { - labcolors_protocol::RelationV1::applicable(relation_id, occurrence_id, criterion, adjacent) - .expect("applicable relation is locally valid") -} - -fn not_applicable_relation( - relation_id: &str, - occurrence_id: &str, - reason_id: &str, -) -> labcolors_protocol::RelationV1 { - labcolors_protocol::RelationV1::not_applicable(relation_id, occurrence_id, reason_id) - .expect("NotApplicable relation is locally valid") -} - -fn evaluate_protocol_bytes(raw: &[u8]) -> JsValue { - evaluate_wcag22_feasibility_v1(raw) - .expect("canonical protocol projection cannot fail") - .into() -} - -fn feasibility_result(outcome: &JsValue, expected_status: &str) -> JsValue { - assert_eq!(get_str(outcome, "outcome").as_deref(), Some("success")); - let feasibility = get_obj(outcome, "feasibility"); - assert_eq!( - get_str(&feasibility, "status").as_deref(), - Some(expected_status) - ); - get_obj(&feasibility, "result") -} - -fn partition_count(outcome: &JsValue, expected_status: &str) -> u32 { - let result = feasibility_result(outcome, expected_status); - let proof = get_obj(&result, "proof"); - get_array(&proof, "partition") - .iter() - .map(|value| { - let byte = value.as_f64().expect("packed byte is numeric"); - assert!(byte.fract() == 0.0 && (0.0..=255.0).contains(&byte)); - (byte as u8).count_ones() - }) - .sum() -} - /// Read the `message` of a rejected `JsError`. A `JsError` crosses as a JS /// `Error` object, so the human text (carrying our stable code) is its /// `.message` property, not the value's own string form. @@ -293,13 +232,6 @@ fn committed_conformance_pack_replays_in_wasm32() { assert_eq!(wcag22_feasibility.len(), fresh.wcag22_feasibility.len()); for (committed, actual) in wcag22_feasibility.iter().zip(&fresh.wcag22_feasibility) { assert_eq!(actual, committed, "feasibility protocol fixture drift"); - let projected = evaluate_protocol_bytes(committed.request_json.as_bytes()); - assert_eq!( - json_text(&projected), - committed.outcome_json, - "{}: public WASM protocol projection drift", - committed.case_id - ); } let committed_manifest: Manifest = @@ -851,232 +783,3 @@ fn config_boundary_two_configs_diverge() { .unwrap_or_default(); assert!(msg.contains("invalid_config"), "код в сообщении: {msg}"); } - -#[wasm_bindgen_test] -fn feasibility_exact_oracle_counts_cross_the_wasm_boundary() { - use labcolors_protocol::Wcag22CriterionV1::{Sc143TextDefault, Sc1411UiComponentOrState}; - let cases = [ - (Sc143TextDefault, vec![[0x76; 3]], "feasible", 7), - (Sc143TextDefault, vec![[0; 3], [255; 3]], "feasible", 2), - ( - Sc143TextDefault, - vec![[0; 3], [255; 3], [0x76; 3]], - "infeasible", - 0, - ), - (Sc1411UiComponentOrState, vec![[0x76; 3]], "feasible", 92), - ( - Sc1411UiComponentOrState, - vec![[0; 3], [255; 3]], - "feasible", - 59, - ), - ]; - for (index, (criterion, adjacent, status, expected)) in cases.into_iter().enumerate() { - let raw = protocol_request(vec![applicable_relation( - &format!("relation-{index}"), - "occurrence", - criterion, - adjacent, - )]); - let outcome = evaluate_protocol_bytes(&raw); - assert_eq!(partition_count(&outcome, status), expected, "case {index}"); - - let result = feasibility_result(&outcome, status); - let domain = get_array(&result, "domain"); - assert_eq!(domain.length(), 256, "Core-owned domain crosses once"); - assert_eq!(json_text(&domain.get(0)), "[0,0,0]"); - assert_eq!(json_text(&domain.get(255)), "[255,255,255]"); - let proof = get_obj(&result, "proof"); - let edges = get_str(&proof, "applicableEdges") - .expect("exact decimal edge count") - .parse::() - .expect("edge count parses"); - assert_eq!(get_array(&result, "failureMatrix").length(), 32 * edges); - assert_eq!(get_array(&proof, "partition").length(), 32); - let wire = json_text(&outcome); - for forbidden in [ - "feasibleCandidates", - "infeasibleCandidates", - "cells", - "assessments", - ] { - assert!( - !wire.contains(forbidden), - "forbidden proportional field {forbidden}" - ); - } - } -} - -#[wasm_bindgen_test] -fn feasibility_preserves_mixed_and_declaration_only_terminals() { - let mixed = protocol_request(vec![ - applicable_relation( - "applicable", - "shared-occurrence", - labcolors_protocol::Wcag22CriterionV1::Sc143TextDefault, - vec![[0x76; 3]], - ), - not_applicable_relation("declared-na", "shared-occurrence", "client-reason"), - ]); - let mixed = evaluate_protocol_bytes(&mixed); - let mixed_result = feasibility_result(&mixed, "feasible"); - assert_eq!(get_array(&mixed_result, "relations").length(), 2); - let proof = get_obj(&mixed_result, "proof"); - assert_eq!(get_str(&proof, "applicableRelations").as_deref(), Some("1")); - assert_eq!( - get_str(&proof, "notApplicableRelations").as_deref(), - Some("1") - ); - - let declarations = protocol_request(vec![not_applicable_relation( - "declared-na", - "occurrence", - "client-reason", - )]); - let declarations = evaluate_protocol_bytes(&declarations); - let result = feasibility_result(&declarations, "notEvaluated"); - assert_eq!(get_array(&result, "relations").length(), 1); - assert!( - js_sys::Reflect::get(&result, &JsValue::from_str("proof")) - .expect("property lookup") - .is_undefined() - ); - assert!( - js_sys::Reflect::get(&result, &JsValue::from_str("failureMatrix")) - .expect("property lookup") - .is_undefined() - ); - assert!( - js_sys::Reflect::get(&result, &JsValue::from_str("domain")) - .expect("property lookup") - .is_undefined() - ); -} - -fn assert_failure_code(outcome: &JsValue, source: &str, code: &str) -> JsValue { - assert_eq!(get_str(outcome, "outcome").as_deref(), Some("failure")); - assert!( - js_sys::Reflect::get(outcome, &JsValue::from_str("feasibility")) - .expect("property lookup") - .is_undefined() - ); - let error = get_obj(outcome, "error"); - assert_eq!(get_str(&error, "source").as_deref(), Some(source)); - let detail = get_obj(&error, "error"); - assert_eq!(get_str(&detail, "code").as_deref(), Some(code)); - detail -} - -#[wasm_bindgen_test] -fn feasibility_transport_and_core_failures_are_typed_data() { - let invalid_utf8 = evaluate_protocol_bytes(&[0xff]); - assert_failure_code(&invalid_utf8, "transport", "invalidUtf8"); - - for malformed in [ - br#"{"schemaVersion":1,"domainId":"srgb8-neutral-axis-v1","resourceProfileId":"compile-v1","relations":[],"unknown":true}"#.as_slice(), - br#"{"schemaVersion":1,"schemaVersion":1,"domainId":"srgb8-neutral-axis-v1","resourceProfileId":"compile-v1","relations":[]}"#.as_slice(), - ] { - let outcome = evaluate_protocol_bytes(malformed); - assert_failure_code(&outcome, "transport", "malformedEnvelope"); - } - - let conflict = protocol_request(vec![ - applicable_relation( - "same-id", - "first", - labcolors_protocol::Wcag22CriterionV1::Sc143TextDefault, - vec![[0; 3]], - ), - applicable_relation( - "same-id", - "second", - labcolors_protocol::Wcag22CriterionV1::Sc143TextDefault, - vec![[0; 3]], - ), - ]); - let conflict = evaluate_protocol_bytes(&conflict); - let error = assert_failure_code(&conflict, "core", "invalidRequest"); - let details = get_obj(&error, "details"); - assert_eq!( - get_str(&details, "code").as_deref(), - Some("conflictingRelationId") - ); - assert_eq!(get_str(&details, "relationId").as_deref(), Some("same-id")); - - let repeated = applicable_relation( - "duplicate", - "occurrence", - labcolors_protocol::Wcag22CriterionV1::Sc143TextDefault, - vec![[0; 3]], - ); - let resource = protocol_request(vec![repeated; 2_048]); - let resource = evaluate_protocol_bytes(&resource); - let error = assert_failure_code(&resource, "core", "resourceLimitExceeded"); - let details = get_obj(&error, "details"); - assert_eq!( - get_str(&details, "dimension").as_deref(), - Some("rawRelations") - ); - assert_eq!(get_str(&details, "requested").as_deref(), Some("2048")); - assert_eq!(get_str(&details, "limit").as_deref(), Some("2047")); -} - -#[wasm_bindgen_test] -fn feasibility_opaque_identity_does_not_change_physical_result() { - let outcome = |relation_id: &str| { - let raw = protocol_request(vec![applicable_relation( - relation_id, - "occurrence", - labcolors_protocol::Wcag22CriterionV1::Sc143TextDefault, - vec![[0x76; 3]], - )]); - evaluate_protocol_bytes(&raw) - }; - let first = outcome("first-id"); - let second = outcome("second-id"); - let first_result = feasibility_result(&first, "feasible"); - let second_result = feasibility_result(&second, "feasible"); - assert_eq!( - json_text(&get_obj(&first_result, "failureMatrix")), - json_text(&get_obj(&second_result, "failureMatrix")) - ); - let first_proof = get_obj(&first_result, "proof"); - let second_proof = get_obj(&second_result, "proof"); - assert_eq!( - json_text(&get_obj(&first_proof, "partition")), - json_text(&get_obj(&second_proof, "partition")) - ); - assert_ne!( - json_text(&get_obj(&first_proof, "relationSetDigest")), - json_text(&get_obj(&second_proof, "relationSetDigest")) - ); - assert_ne!( - json_text(&get_obj(&first_proof, "evaluationId")), - json_text(&get_obj(&second_proof, "evaluationId")) - ); -} - -#[wasm_bindgen_test] -fn feasibility_raw_boundary_rechecks_the_exact_protocol_ceiling() { - let limit = wcag22_feasibility_max_request_bytes_v1(); - assert_eq!(u64::from(limit), labcolors_protocol::MAX_ENVELOPE_BYTES_V1); - let oversized = vec![b' '; limit as usize + 1]; - let raw = evaluate_protocol_bytes(&oversized); - let scalar: JsValue = wcag22_feasibility_envelope_too_large_v1(u64::from(limit) + 1) - .expect("scalar protocol projection") - .into(); - assert_eq!(json_text(&raw), json_text(&scalar)); - let error = assert_failure_code(&raw, "transport", "envelopeTooLarge"); - let requested_text = (u64::from(limit) + 1).to_string(); - let limit_text = u64::from(limit).to_string(); - assert_eq!( - get_str(&error, "requestedBytes").as_deref(), - Some(requested_text.as_str()) - ); - assert_eq!( - get_str(&error, "limitBytes").as_deref(), - Some(limit_text.as_str()) - ); -} diff --git a/docs/NAMING.md b/docs/NAMING.md index 707f2ef8..e4e50e39 100644 --- a/docs/NAMING.md +++ b/docs/NAMING.md @@ -13,13 +13,13 @@ | Метрика | N | | --- | --- | -| членов workspace (Cargo.toml `members`, глоб развёрнут по ФС) | 6 | -| крейтов семейства в crates/ | 5 | -| экспорт-субпутей package.json @labpics/colors | 8 | +| членов workspace (Cargo.toml `members`, глоб развёрнут по ФС) | 7 | +| крейтов семейства в crates/ | 6 | +| экспорт-субпутей package.json @labpics/colors | 10 | | python-скриптов scripts/*.py | 10 | | маркдаун-доков docs/**/*.md (включая этот канон) | 12 | | векторов conformance/vectors/*.json (включая manifest) | 8 | -| файлов вне закона имён | 4 | +| исходных/объявленных publish-файлов вне закона имён | 10 | ## Общие принципы (эталон lab-icons) @@ -40,7 +40,8 @@ - Имя крейта = `labcolors-<роль>`, kebab-case; роль — одно слово: `labcolors-core`, `labcolors-protocol`, `labcolors-conformance`, - `labcolors-ffi`, `labcolors-wasm`. Директория `crates/<имя крейта>`. + `labcolors-ffi`, `labcolors-wasm`, `labcolors-compiler`. Директория + `crates/<имя крейта>`. - Члены workspace объявлены в корневом Cargo.toml (`members`); глоб `crates/*` разворачивается по ФС, harness-члены вне crates/ перечислены поимённо (experiments/psychophysics — имя пакета без префикса: не публикуется, @@ -51,14 +52,19 @@ ### npm-пакет @labpics/colors -- Субпуть = домен, kebab-case: `./apply-theme`, `./watch-theme`, - `./adapt-theme`, `./effective-bg`. Каждый субпуть разрешается в исходник - packages/colors (exports без кода запрещены — сверяет typecheck пакета). +- Исходный JS/TS-субпуть = домен, kebab-case: `./apply-theme`, `./watch-theme`, + `./adapt-theme`, `./effective-bg`, `./compiler`. Каждый такой субпуть + разрешается в исходник packages/colors; целостность экспортов проверяет + release-контур. Служебные и артефактные субпути ниже имеют собственные + правила и не обязаны разрешаться в JS/TS-исходник. - Служебный субпуть `./package.json` — стандарт npm, разрешён законом. - Служебный субпуть `./build-metadata.json` — versioned machine-readable build metadata опубликованных байтов; расширение фиксирует JSON-формат контракта. - Артефактный субпуть `./pkg/labcolors_bg.wasm` — см. «Известные отступления». -- Файлы пакета — kebab-case (`apply-theme.js` + `apply-theme.d.ts`). +- Артефактный субпуть `./compiler/wasm` — см. «Известные отступления». +- Рукописные файлы пакета — kebab-case (`apply-theme.js` + + `apply-theme.d.ts`); объявленные generated outputs перечислены в + «Известных отступлениях» и проверяются по `package.json#files` даже до сборки. ### Python-скрипты (эталоны) @@ -111,6 +117,17 @@ - `./pkg/labcolors_bg.wasm` — субпуть-артефакт wasm-pack: имя `labcolors_bg.wasm` генерирует wasm-bindgen из `--out-name labcolors`, snake_case продиктован тулингом и руками не переименовывается. +- `./compiler/wasm` — вложенный служебный субпуть физического compiler-WASM; + вложенность сохраняет одну публичную роль `compiler`, а имя артефакта скрыто + за стабильным export map. +- `packages/colors/pkg/labcolors_bg.wasm` и + `packages/colors/pkg/labcolors_bg.wasm.d.ts` — runtime-артефакты wasm-bindgen; + snake_case задаёт `--out-name labcolors`. +- `packages/colors/compiler/labcolors_compiler.js`, + `packages/colors/compiler/labcolors_compiler.d.ts`, + `packages/colors/compiler/labcolors_compiler_bg.wasm` и + `packages/colors/compiler/labcolors_compiler_bg.wasm.d.ts` — отдельная + generated compiler-группа; snake_case задаёт `--out-name labcolors_compiler`. - `./build-metadata.json` — служебный JSON-контракт build metadata; расширение намеренно остаётся частью subpath, чтобы формат был явным и потребитель не принимал его за исполняемый JS entrypoint. diff --git a/docs/migrations/exact-alpha-glow.md b/docs/migrations/exact-alpha-glow.md index d5c15878..de99b4a3 100644 --- a/docs/migrations/exact-alpha-glow.md +++ b/docs/migrations/exact-alpha-glow.md @@ -460,9 +460,31 @@ boundary-адаптер, поэтому для JS/TS-потребителей и pack 4 сохранены. Новый corpus фиксирует versioned request/outcome bytes, packed LSB0 evidence, все три feasibility-терминала и типизированные conflict/resource error paths. -- npm добавляет `evaluateWcag22Feasibility(Uint8Array)` и два root-типа — - request/outcome. Вложенные wire-типы остаются деталями исчерпывающего outcome, - а raw wasm-bindgen ABI не экспортируется из package root. +- `evaluateWcag22Feasibility(Uint8Array)`, `wcag22FeasibilityMaxBytes()` и + request/outcome types перенесены из package root в + `@labpics/colors/compiler`. Compiler загружает собственный WASM через + `@labpics/colors/compiler/wasm`; runtime WASM больше не содержит feasibility + protocol. Raw wasm-bindgen ABI обеих ролей остаётся приватным. +- Инициализация теперь также разделена по execution-role: прежний root + `await init()` инициализирует только runtime. Перед первым compiler-вызовом + отдельно импортируйте `init` (или `initSync`) из `@labpics/colors/compiler` + и инициализируйте его собственный WASM; иначе compiler API fail-fast, а не + использует скрытый runtime fallback. В браузере перенесите compiler import, + init и evaluate в dedicated module Worker; UI thread загружает только + runtime. + + ```ts + const compiler = new Worker( + new URL("./color-compiler.worker.ts", import.meta.url), + { type: "module" }, + ); + ``` + + Worker-модуль инициализирует `@labpics/colors/compiler`, регистрирует handler + и только затем отправляет `ready`; main thread передаёт запрос после этого + сигнала, как показано в package README. Для Node offline tooling можно + вызывать entry напрямую, передав ему байты compiler WASM. Один и тот же + модуль для двух ролей не подходит. - Feasibility полностью перечисляет зарегистрированный домен и не выбирает цвет. Selection policy, брендовая близость, polarity и appearance-scoring не являются скрытой частью этого migration-контракта. diff --git a/docs/verification-map.md b/docs/verification-map.md index 9a47bdb3..afbf10c6 100644 --- a/docs/verification-map.md +++ b/docs/verification-map.md @@ -90,7 +90,7 @@ | право выпускать терминальное доказательство связано с фактической типизированной строкой WCAG registry | скомпилированная Rust-проба читает действующую строку; Python канонизирует 10 значимых для выпуска полей через length-prefix/SHA-256; 10 мутаций полей + 2 мутации hex/count транспорта обязаны отказать | независимая локальная привязка допуска (в proof всего 15 негативных контролей) | | один вердикт и его доказательство сохраняются через Core → FFI/WASM → JS/Swift/контракт соответствия | `wcag22_transport_*`, `wasm_parity`, `wcag22.test.mjs`, Swift conformance, зафиксированный набор из шести `wcag22.json`; release verifier повторно проверяет байты доказательств | дифференциальный межграничный оракул | -## Конечная компиляция и явный выбор WCAG 2.2 — `wcag22_feasibility.rs` (#295, #296-A/B) +## Конечная компиляция и явный выбор WCAG 2.2 — `wcag22_feasibility.rs` (#295, #296-A/B/C1) Модуль канонизирует непрозрачные клиентские декларации и полностью перечисляет либо зарегистрированную нейтральную ось, либо явно объявленный клиентом конечный @@ -113,11 +113,11 @@ Core-only; эти evidence не расширяются на его отсутс Protocol/WASM/FFI/npm/Swift проекции. | transport V1 принимает только strict UTF-8 JSON bytes, сначала применяет выведенный предел envelope, затем сохраняет полную Core-алгебру как `Success(feasibility) \| Failure(error)` | `labcolors-protocol`: exact-limit witness, limit+1 decoder-spy, strict-schema/error-algebra tests и compile-fail запрет forged outcome | литеральная грамматика + Core resource-profile SSOT + тип-уровневый негативный контроль | -| WASM/npm не повторяет Core-математику, preflight-ит `Uint8Array.byteLength` до избегаемой ABI-копии и переносит domain/relations один раз с candidate-major LSB0 matrix | `wasm_parity::committed_conformance_pack_replays_in_wasm32`; feasibility boundary tests; `wcag22-feasibility.test.mjs` с независимым LSB0 reduction и mutation subject | дифференциальный public-boundary replay pack 5 + независимый packed consumer | +| offline npm compiler проходит `labcolors-protocol → labcolors-compiler → @labpics/colors/compiler`, не повторяет Core-математику, preflight-ит intrinsic `Uint8Array.byteLength` до избегаемой ABI-копии и переносит domain/relations один раз; runtime dependency cone не содержит protocol | compiler `wasm_parity`; role-isolation dependency tests; hostile-view и feasibility boundary tests; `wcag22-feasibility.test.mjs` с независимым LSB0 reduction и mutation subject | дифференциальный compiler-boundary replay pack 5 + независимый packed consumer | | UniFFI/Swift вызывает тот же protocol byte path, preflight-ит `Data`/`[UInt8]` до сырой FFI-копии и исчерпывающе декодирует terminal/error algebra | FFI mechanical-shell tests; Swift pack-5 replay, limit+1 bridge spy, structural mutation tests и extreme-shape whole-call observations | побайтный FFI/protocol differential + независимый Swift packed consumer | | conformance pack 5 добавляет только feasibility-family; прежние шесть family остаются byte-identical, а 13 новых outcomes воспроизводятся canonical protocol encoder побайтно | `pack_v5_contract`; `reference_runner::protocol_reproduces_committed_wcag22_feasibility_exactly`; release verifier/clean-install replay | SHA-256 immutable-family guards + дифференциальный protocol/public-package replay | -| исторические native-допуски V1/V2 остаются неизменными; V3 заново измеряет текущий neutral-вход через общий finite-domain kernel | V1 checker исполняется в чистом измеренном snapshot `6001cf4`, его applicability verifier — в `94efeee`; V2 побайтно проверяется сохранённым checker-ом из snapshot `4afe61b`; V3 с artifact SHA `46ec939523a9aff4f253c4c74e997dfd95812a694b2507fae885ff60244ade3a` создаёт только source-bound recorder: один exact dependency cone проверяется до fresh build и после запуска, Cargo-config hierarchy пуста, среда закрыта, toolchain и реально запущенный benchmark binary связаны SHA-256; Rust/Cargo 1.96.0, exact features и compiler overrides фиксированы | fail-closed strict-JSON parser, точная сверка dependency cone и recorder receipt, 71 V3 checker-мутация и сырые native-наблюдения без timing threshold | -| канонический транспортный WASM-путь воспроизводим побайтно и завершает все граничные формы полного вызова | append-only size V4 фиксирует `520920 B` и SHA `c179f42cd90c24699167ee78b4080c80fb38247c54953e7dc020483f6fcf94ed`, полученные канонической Linux x64-сборкой, без запаса и с запретом ослабить V3; V1/V2/V3 неизменны; полный вызов V2 содержит 10 объявленных в `SCENARIO_IDS` форм × 5 выборок в новых процессах, где число выборок наследуется из native admission V3, точные хеши запросов/результатов и привязки исходников, pack и toolchain; формы заимствуют у native только ресурсную геометрию, а транспортные payload могут отличаться, поэтому производные терминальные счётчики между двумя harness не обязаны совпадать | `check-wasm-size-budget.mjs`, тесты `WCAG22 WASM budget history is exact, append-only, and acyclic`, `whole-call evidence history is exact and deterministic` и `canonical whole-call artifact schema accepts all immutable scenarios`, независимые Linux `wc`/`sha256sum` + повторный package-root verifier; размер применим только к зафиксированному артефакту и рецепту, набор форм — только к перечисленным сценариям, а число выборок влияет лишь на наблюдения времени/maxRSS/pages, которые не являются порогами | +| исторические native-допуски V1–V3 остаются неизменными; V4 заново измеряет тот же neutral-вход после C1 workspace split | V1 checker исполняется в чистом измеренном snapshot `6001cf4`, его applicability verifier — в `94efeee`; V2 побайтно проверяется сохранённым checker-ом из snapshot `4afe61b`; V3 replay-ится в merged Slice-B snapshot `10c44ef`; текущий V4 с artifact SHA `3c257c336bc403eee933990fd7188a3b0a6e89d0cbc983aff18846ef76206275` создаёт только source-bound recorder: exact dependency cone проверяется до fresh build и после запуска, Cargo-config hierarchy пуста, среда закрыта, toolchain и реально запущенный benchmark binary связаны SHA-256; deterministic scenario/identity-проекция V3/V4 одинакова, а изменившийся subject-набор ограничен workspace/admission machinery | fail-closed strict-JSON parser, точная сверка dependency cone и recorder receipt, 71 checker-мутация и сырые native-наблюдения без timing threshold | +| runtime и compiler WASM воспроизводимы как разные execution-role; compiler завершает все объявленные граничные формы полного вызова | append-only size V5 связывает независимые exact Linux x64 size/SHA и рецепты обеих ролей с нулевым запасом, не переписывая V1–V4; whole-call V3 связан с `compiler.js`, compiler glue/WASM, V5 compiler recipe и native admission V4, сохраняет exact request/outcome projection и отдельно наблюдает `initSync` и прогретую операцию | `check-wasm-size-budget.mjs`, role-budget mutation tests и whole-call V3 verifier; независимые Linux `wc`/`sha256sum`; размер применим только к точным артефактам/рецептам, формы — только к `SCENARIO_IDS`, а timing/maxRSS/pages не являются порогами | Здесь `E` — число канонических применимых рёбер «связь × сосед» (`E∈{0,1,…,2047}`), `A` — число применимых связей (`A∈{0,1,…,E}`; diff --git a/packages/colors/README.md b/packages/colors/README.md index 9089e635..e39b34bc 100644 --- a/packages/colors/README.md +++ b/packages/colors/README.md @@ -1,6 +1,6 @@ # @labpics/colors -Агностичный контраст-движок для дизайн-систем. Получает фоновый цвет и тему — возвращает полный набор цветовых ролей **вашей** системы. Словарь ролей не встроен в пакет: его задаёт конфиг дизайн-системы (`ThemeConfig`), загружаемый через `loadConfig`; имена вида `--lab-label-primary`, `--lab-border-base` в примерах ниже — из конфига дизайн-системы labui. CSS-переменные несут готовое значение `oklch(L% C H)` (для полупрозрачных ролей — `oklch(L% C H / A)`); сырой `#RRGGBB` остаётся данными роли (`roles.<ключ>.hex`). Ядро написано на Rust и скомпилировано в WebAssembly; пакет не имеет runtime-зависимостей. +Агностичный контраст-движок для дизайн-систем. Получает фоновый цвет и тему — возвращает полный набор цветовых ролей **вашей** системы. Словарь ролей не встроен в пакет: его задаёт конфиг дизайн-системы (`ThemeConfig`), загружаемый через `loadConfig`; имена вида `--lab-label-primary`, `--lab-border-base` в примерах ниже — из конфига дизайн-системы labui. CSS-переменные несут готовое значение `oklch(L% C H)` (для полупрозрачных ролей — `oklch(L% C H / A)`); сырой `#RRGGBB` остаётся данными роли (`roles.<ключ>.hex`). Пакет не имеет runtime-зависимостей и разделён на две WASM-роли: runtime resolver в корне и offline compiler в `@labpics/colors/compiler`. Ядро возвращает **данные**, не затрагивает DOM. Три вспомогательные функции переводят эти данные в живые CSS-переменные: `applyTheme` (разовое применение), `watchTheme` (реактивное — обновляется при изменении фона) и `adaptTheme` (плавная адаптация для фона, меняющегося каждый кадр). @@ -24,20 +24,21 @@ npm install @labpics/colors При сборке из монорепо: ```sh -npm run build # → pkg/ (wasm + JS-обёртка + .d.ts) +npm run build # → pkg/ (runtime) + compiler/ (offline compiler) ``` Пакет экспортирует `@labpics/colors/build-metadata.json` — self-declared machine-readable metadata конкретной сборки: npm/core versions, exact source SHA, digest и SHA-256 conformance manifest/family set, а также размер и SHA-256 -WASM-байтов. Release-gate сверяет объект целиком с исходными файлами и повторяет +упорядоченных `runtime`/`compiler` WASM-артефактов. Release-gate сверяет schema 2 +целиком с исходными файлами и повторяет проверку после чистой установки tarball. Это integrity metadata внутри артефакта, не криптографическая provenance/Sigstore-аттестация, не runtime telemetry и не сетевой запрос. --- -## Как использовать +## Как использовать в браузере ### Разовое применение @@ -116,6 +117,29 @@ adaptTheme(hero, { }); ``` +### Инициализация в Node + +Node получает локальные WASM-байты явно; нулевая форма `init()` предназначена +для браузерного loader-а. Runtime и compiler инициализируются независимо: + +```ts +import { readFile } from "node:fs/promises"; +import { createRequire } from "node:module"; +import initRuntime from "@labpics/colors"; +import initCompiler from "@labpics/colors/compiler"; + +const require = createRequire(import.meta.url); +const runtimeWasm = await readFile( + require.resolve("@labpics/colors/pkg/labcolors_bg.wasm"), +); +const compilerWasm = await readFile( + require.resolve("@labpics/colors/compiler/wasm"), +); + +await initRuntime({ module_or_path: runtimeWasm }); +await initCompiler({ module_or_path: compilerWasm }); +``` + --- ## Темы @@ -276,23 +300,47 @@ diagnostics и legacy `wcagRatio` не могут изменить этот ве --- -### `evaluateWcag22Feasibility(request): Wcag22FeasibilityOutcomeV1` +### Offline compiler: `evaluateWcag22Feasibility(request)` Полностью перебирает зарегистрированную конечную ось sRGB8 против всех явно объявленных клиентом связей. Операция отвечает только на вопрос «какие кандидаты проходят все эти ограничения?». Она не выбирает лучший цвет, не угадывает применимость и не понимает семантику ID. -Эта npm-граница принимает только зарегистрированную нейтральную ось V1. Явные -клиентские наборы sRGB8 доступны в Rust Core и этим transport API не принимаются. +Операция экспортируется только из `@labpics/colors/compiler` и загружает +отдельный compiler WASM; package root остаётся runtime API. Эта npm-граница +принимает только зарегистрированную нейтральную ось V1. Явные клиентские наборы +sRGB8 доступны в Rust Core и этим transport API не принимаются. + +В браузере compiler принадлежит offline/Worker execution class: main thread +импортирует только runtime, а dedicated module Worker владеет compiler WASM и +полным вызовом. Node build-tooling может вызывать тот же entry напрямую. ```ts -import init, { +// color-compiler.worker.ts +import initCompiler, { evaluateWcag22Feasibility, type Wcag22FeasibilityRequestV1, -} from "@labpics/colors"; +} from "@labpics/colors/compiler"; -await init(); +await initCompiler(); + +self.addEventListener( + "message", + ({ data }: MessageEvent) => { + const bytes = new TextEncoder().encode(JSON.stringify(data)); + self.postMessage(evaluateWcag22Feasibility(bytes)); + }, +); +self.postMessage({ type: "ready" } as const); +``` + +```ts +// build-colors.ts — main thread не импортирует runtime-код compiler-а +import type { + Wcag22FeasibilityOutcomeV1, + Wcag22FeasibilityRequestV1, +} from "@labpics/colors/compiler"; const request: Wcag22FeasibilityRequestV1 = { schemaVersion: 1, @@ -307,8 +355,23 @@ const request: Wcag22FeasibilityRequestV1 = { }], }; -const bytes = new TextEncoder().encode(JSON.stringify(request)); -const outcome = evaluateWcag22Feasibility(bytes); +const outcome = await new Promise((resolve, reject) => { + const worker = new Worker(new URL("./color-compiler.worker.ts", import.meta.url), { + type: "module", + }); + worker.addEventListener("message", ({ data }) => { + if (data?.type === "ready") { + worker.postMessage(request); + return; + } + worker.terminate(); + resolve(data); + }); + worker.addEventListener("error", (event) => { + worker.terminate(); + reject(event.error ?? new Error(event.message)); + }, { once: true }); +}); if (outcome.outcome === "success") { // feasible | infeasible | notEvaluated @@ -321,7 +384,7 @@ if (outcome.outcome === "success") { Вход — только настоящий `Uint8Array` со strict JSON V1; иной JavaScript-тип детерминированно отклоняется `TypeError` до чтения WASM-owned ceiling и копии. -Граница размера выведена из грамматики и resource profile; после `init()` её +Граница размера выведена из грамматики и resource profile; после `initCompiler()` её возвращает `wcag22FeasibilityMaxBytes()`. Package wrapper проверяет `byteLength` до избежимой копии в WASM, а Rust повторяет авторитетную проверку. Выход хранит домен и канонические связи по одному разу, а решения — в candidate-major LSB0 @@ -461,34 +524,25 @@ replacement принадлежит #283. ## Размер бандла -Raw-размер WASM — hard gate с append-only историей. Неизменяемый -`bench/wasm-size-budget-v1.json` сохраняет допуск #284 (`454385 B`), а -`bench/wasm-size-budget-v2.json` отдельно допускает полный transport #295: -ровно `521240 B`, SHA-256 -`d37841bfb2615d05c8366b08dcc7e5aed1bbd3cf27c3db67896108c5ec9c9ca0`. -`bench/wasm-size-budget-v3.json` повторно допускает Core-срез #296-A: -ровно `521231 B`, SHA-256 -`779379e914909ff1ddbb5afdd6554d026b586f3c71ef6b2cfeba3468bf93e029`. -Текущий `bench/wasm-size-budget-v4.json` допускает #296-B: ровно `520920 B`, -SHA-256 `c179f42cd90c24699167ee78b4080c80fb38247c54953e7dc020483f6fcf94ed`. -Каждый ceiling равен своему каноническому Linux-x64 измерению, произвольного -запаса нет; V1/V2/V3 остаются побайтно неизменными, а V4 не может ослабить V3. - -Release-equivalent Linux x64 CI требует одновременно точный размер и SHA. -Текущий `bench/wcag22-feasibility-wasm-boundary-v2.json` фиксирует 10 крайних -whole-call форм × 5 свежих процессов, request/outcome bytes, packed shape и -привязки Core/pack/toolchain; его детерминированная проекция побайтно совпадает -с неизменяемым V1. Время, process maxRSS и страницы WASM остаются -наблюдениями без выдуманного production-порога. На других host-платформах -checker сообщает только raw/gzip/SHA-диагностику: host-native bytes не выдаются -за канонический release artifact. Сборка remap-ит mutable workspace и Cargo -registry roots в стабильные виртуальные пути; `gzip -9` остаётся диагностикой, -а не второй константой допуска. - -Это весь движок: CAM16, солверы контраста, лестницы и граница конфига. `.wasm` -поставляется отдельным ассетом. Будет ли его загрузка критическим путём первого -рендера, определяет интеграция: до первого `resolveTheme` инициализация обязана -завершиться; приложение может preload/кэшировать модуль. JS-хелперы +Raw-размер WASM — hard gate с append-only историей. Текущий +`bench/wasm-size-budget-v5.json` содержит независимые exact Linux-x64 +size/SHA-бюджеты с нулевым headroom для `runtime` и `compiler`; V1–V4 остаются +неизменяемой историей прежнего единого артефакта. Release-equivalent CI требует +точного совпадения обеих ролей и их рецептов сборки. На других host-платформах +checker сообщает только raw/gzip/SHA-диагностику и не выдаёт локальные байты за +канонический release artifact. + +`bench/wcag22-feasibility-wasm-boundary-v3.json` фиксирует compiler-entry +whole-call формы в свежих процессах и связывает их с Core admission V4, V5 +compiler recipe, package entry/glue и точным compiler WASM. Время `initSync`, +время прогретого вызова, process maxRSS и страницы WASM — наблюдения без +production-порога. `gzip -9` также остаётся диагностикой, а не второй +константой допуска. + +Runtime и offline compiler поставляются двумя независимо загружаемыми +`.wasm`-ассетами. Будет ли runtime-загрузка критическим путём первого рендера, +определяет интеграция: до первого `resolveTheme` инициализация обязана +завершиться; compiler можно не загружать в пользовательской сессии. JS-хелперы (`applyTheme`, `watchTheme`, `adaptTheme`, `effectiveBackground`) имеют именованные экспорты и допускают tree-shaking, но их размер также следует мерить сборкой, а не описывать приблизительно. @@ -503,9 +557,14 @@ headless Chrome из CI. ESM/URL-обёртка генерируется wasm-pa ### Для аудиторов цепочки поставки - **Build metadata:** экспорт `@labpics/colors/build-metadata.json` декларирует - source SHA и conformance pack для установленного WASM; verifier отклоняет - любое лишнее, отсутствующее или несовпадающее поле. Объект не подписан и не - заменяет отключённую npm/Sigstore provenance-аттестацию. -- **Network access (Socket и др.):** единственный `fetch` в пакете (`pkg/labcolors.js`) загружает СОБСТВЕННЫЙ `.wasm`-файл пакета при `init(url)` — стандартный лоадер wasm-bindgen. Ни внешних адресов, ни отправки данных, ни исполнения при импорте. В node-пути (передача байтов) `fetch` не вызывается. -- **Bundlephobia `BuildError`:** их webpack-конвейер не умеет `.wasm`-ассеты («loader customization needed») — так падает почти любой WASM-пакет. Реальный размер конкретного коммита показывает CI-шаг `report bundle size (gzip)`. + source SHA, conformance pack и обе WASM-роли; verifier отклоняет любое лишнее, + отсутствующее или несовпадающее поле и перечитывает metadata из tarball. + Объект не подписан и не заменяет отключённую npm/Sigstore provenance-аттестацию. +- **Network access (Socket и др.):** оба generated loader-а умеют получить свой + `.wasm` через `fetch`, только когда интеграция передала URL или использовала + browser default. В пакете нет внешнего endpoint, отправки данных или исполнения + при импорте; Node-smoke передаёт локальные байты. +- **Bundlephobia `BuildError`:** их webpack-конвейер не умеет `.wasm`-ассеты + («loader customization needed»). Канонические размеры проверяет CI-gate + `enforce measured WASM role budgets`; gzip публикуется только как диагностика. - **Zero runtime JS-dependencies:** npm-поле `dependencies` пусто — транзитивной JS/npm-цепочки поставки нет. Rust-крейты сборки (`serde`, `serde_json` и др.) компилируются ВНУТРЬ `.wasm` (учтены CI-замером); их цепочка аудируется на стороне сборки — `cargo audit` (RustSec) в CI lab-colors. diff --git a/packages/colors/bench/wasm-size-budget-v5.json b/packages/colors/bench/wasm-size-budget-v5.json new file mode 100644 index 00000000..9d8c8742 --- /dev/null +++ b/packages/colors/bench/wasm-size-budget-v5.json @@ -0,0 +1,54 @@ +{ + "schemaVersion": 4, + "budgetId": "labcolors-wasm-roles-issue-296-c1-v5", + "predecessor": { + "path": "packages/colors/bench/wasm-size-budget-v4.json", + "fileSha256": "c34fc10404dc7057a53a28592d18342078b5cd0e5dcaa888db482abf3f5fb23c" + }, + "toolchainSource": { + "path": "packages/colors/bench/wasm-size-budget-v1.json", + "fileSha256": "4f7340fc8cfd0ccb97377c385f2f8d8e7a9ef2c5ba96177f518c5d07de2825e1" + }, + "buildRecipes": { + "runtime": { + "command": "CARGO_ENCODED_RUSTFLAGS= wasm-pack build crates/labcolors-wasm --release --target web --out-dir ../../packages/colors/pkg --out-name labcolors --locked", + "recipeSha256": "0ea74cb070e0a5facb7280f6124930a0bb673ee4dcee9c99fff110db6c9389d4" + }, + "compiler": { + "command": "CARGO_ENCODED_RUSTFLAGS= wasm-pack build crates/labcolors-compiler --release --target web --out-dir ../../packages/colors/compiler --out-name labcolors_compiler --locked", + "recipeSha256": "ce53cea5f579c512a6d2f0c3348f250ac0a5e03206de55e7979c8eae1403be8f" + } + }, + "roles": { + "runtime": { + "artifact": "packages/colors/pkg/labcolors_bg.wasm", + "measurement": { + "issue": 296, + "slice": "C1", + "measurementPlatform": "linux-x64", + "rawBytes": 454385, + "sha256": "8cd65f001d4bb4b8ddead9084e705a64bee14cd796c7bc6ebeb2f2687aa5fdba" + }, + "policy": { + "maxRawBytes": 454385, + "derivation": "exact-accepted-issue-296-slice-c1-runtime-measurement", + "gzip": "diagnostic-only" + } + }, + "compiler": { + "artifact": "packages/colors/compiler/labcolors_compiler_bg.wasm", + "measurement": { + "issue": 296, + "slice": "C1", + "measurementPlatform": "linux-x64", + "rawBytes": 175212, + "sha256": "3a552ce43ada7d0b10e90a23b4a7e50a4ecad77a446374b98ca8ee6b5c6a2a45" + }, + "policy": { + "maxRawBytes": 175212, + "derivation": "exact-accepted-issue-296-slice-c1-compiler-first-admission", + "gzip": "diagnostic-only" + } + } + } +} diff --git a/packages/colors/bench/wcag22-feasibility-boundary.bench.mjs b/packages/colors/bench/wcag22-feasibility-boundary.bench.mjs index 1a6f684c..f18adf44 100644 --- a/packages/colors/bench/wcag22-feasibility-boundary.bench.mjs +++ b/packages/colors/bench/wcag22-feasibility-boundary.bench.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node -// Whole-call #295 boundary evidence through the built package-root API. +// Whole-call #296-C1 evidence through the dedicated compiler entry. // `--emit` prints diagnostic JSON with explicit canonical eligibility; `--record PATH` // admits only the pinned Linux x64/Node/toolchain context and never overwrites; // `--verify [PATH]` binds the exact WASM and reruns every structural law. @@ -21,39 +21,30 @@ const packageRoot = process.env.LABCOLORS_BOUNDARY_PACKAGE_ROOT : canonicalPackageRoot; const canonicalPackageInput = packageRoot === canonicalPackageRoot; const repoRoot = resolve(canonicalPackageRoot, "../.."); -const packageEntry = resolve(packageRoot, "index.js"); +const packageEntry = resolve(packageRoot, "compiler.js"); const packageManifestPath = resolve(packageRoot, "package.json"); -const wasmGluePath = resolve(packageRoot, "pkg/labcolors.js"); -const wasmPath = resolve(packageRoot, "pkg/labcolors_bg.wasm"); -const eagerRuntimeModules = { - adaptTheme: [resolve(packageRoot, "adapt-theme.js"), "packages/colors/adapt-theme.js"], - applyTheme: [resolve(packageRoot, "apply-theme.js"), "packages/colors/apply-theme.js"], - effectiveBackground: [ - resolve(packageRoot, "effective-bg.js"), - "packages/colors/effective-bg.js", - ], - watchTheme: [resolve(packageRoot, "watch-theme.js"), "packages/colors/watch-theme.js"], -}; -const runtimeSourceIds = [ - ...Object.keys(eagerRuntimeModules), +const wasmGluePath = resolve(packageRoot, "compiler/labcolors_compiler.js"); +const wasmPath = resolve(packageRoot, "compiler/labcolors_compiler_bg.wasm"); +const compilerSourceIds = [ + "compilerEntry", "harness", "packageManifest", - "packageRoot", "wasmGlue", ]; const coreAdmissionPath = resolve( repoRoot, - "crates/labcolors-core/contracts/wcag22-feasibility-benchmark-v3.json", + "crates/labcolors-core/contracts/wcag22-feasibility-benchmark-v4.json", ); const packOraclePath = resolve(repoRoot, "conformance/vectors/wcag22-feasibility.json"); const conformanceManifestPath = resolve(repoRoot, "conformance/vectors/manifest.json"); -const wasmToolchainPath = resolve(here, "wasm-size-budget-v1.json"); +const wasmBudgetPath = resolve(here, "wasm-size-budget-v5.json"); +const wasmToolchainSourcePath = resolve(here, "wasm-size-budget-v1.json"); const ciWorkflowPath = resolve(repoRoot, ".github/workflows/ci.yml"); -const defaultMeasurementPath = resolve(here, "wcag22-feasibility-wasm-boundary-v2.json"); +const defaultMeasurementPath = resolve(here, "wcag22-feasibility-wasm-boundary-v3.json"); const pageBytes = 65_536; const candidateCount = 256; -export const MEASUREMENT_ARTIFACT_ID = "wcag22-feasibility-wasm-whole-call-v2"; +export const MEASUREMENT_ARTIFACT_ID = "wcag22-feasibility-wasm-whole-call-v3"; export const SCENARIO_IDS = Object.freeze([ "minimum-evaluated", "maximum-canonical-applicable-relations", @@ -87,7 +78,11 @@ const hardGates = [ "no-proportional-dto", ]; const memoryClaim = - "process maxRSS is total-process high-water including V8; post-call WASM pages are linear-memory high-water observations; neither is total operation memory"; + "process maxRSS values are total-process high-water including V8 and prior warm-up/observer allocations; after-init and warm-call WASM pages are linear-memory high-water observations; neither is total operation memory"; +const initSyncScope = + "initSync-from-in-memory-compiler-wasm-includes-wasm-bindgen-startup-excludes-io-and-js-module-import"; +const operationScope = + "second-identical-operation-after-one-unmeasured-warm-up-whose-result-graph-is-not-retained-by-harness"; const proportionalKeys = new Set([ "assessments", "cells", @@ -155,24 +150,51 @@ function readJsonWithBytes(path, label) { function sourceContracts() { const core = readJsonWithBytes(coreAdmissionPath, "Core admission artifact"); - const toolchain = readJsonWithBytes(wasmToolchainPath, "WASM toolchain artifact"); + const budget = readJsonWithBytes(wasmBudgetPath, "WASM role budget artifact"); + const toolchain = readJsonWithBytes( + wasmToolchainSourcePath, + "WASM toolchain source artifact", + ); if ( core.value?.schemaVersion !== 1 || - core.value?.artifactId !== "wcag22-feasibility-admission-raw-v3" || + core.value?.artifactId !== "wcag22-feasibility-admission-raw-v4" || core.value?.profileLimits?.profileId !== "compile-v1" ) { fail("unsupported Core admission artifact identity"); } + if ( + budget.value?.schemaVersion !== 4 || + budget.value?.budgetId !== "labcolors-wasm-roles-issue-296-c1-v5" || + sha256(budget.bytes) !== + "e4b53a2eb976a8c66827a559cb81232e359b734dbfb14725da215cb496ff5d59" || + budget.value?.toolchainSource?.path !== + "packages/colors/bench/wasm-size-budget-v1.json" || + budget.value?.toolchainSource?.fileSha256 !== sha256(toolchain.bytes) || + budget.value?.roles?.compiler?.artifact !== + "packages/colors/compiler/labcolors_compiler_bg.wasm" + ) { + fail("unsupported canonical WASM role budget identity"); + } if ( toolchain.value?.schemaVersion !== 2 || toolchain.value?.budgetId !== "labcolors-wasm-raw-issue-284-v1" ) { fail("unsupported canonical WASM toolchain artifact identity"); } - return { core, toolchain }; + const compilerRecipe = toolchainRecipe( + toolchain.value, + budget.value.buildRecipes?.compiler?.command, + ); + if ( + budget.value.buildRecipes?.compiler?.recipeSha256 !== + sha256(Buffer.from(JSON.stringify(compilerRecipe), "utf8")) + ) { + fail("compiler build recipe does not bind the canonical toolchain source"); + } + return { budget, core, toolchain }; } -function toolchainRecipe(toolchain) { +function toolchainRecipe(toolchain, command) { const measurement = toolchain.measurement; const recipe = { rustToolchain: measurement?.rustToolchain, @@ -185,7 +207,7 @@ function toolchainRecipe(toolchain) { wasmOptVersion: measurement?.wasmOptVersion, measurementPlatform: measurement?.measurementPlatform, rustPathRemap: measurement?.rustPathRemap, - command: measurement?.command, + command, }; if ( Object.values(recipe).some((value) => value === undefined) || @@ -196,7 +218,7 @@ function toolchainRecipe(toolchain) { return recipe; } -function runtimeSourceBindings() { +function compilerSourceBindings() { const binding = (path, repositoryPath, label) => { let bytes; try { @@ -206,13 +228,12 @@ function runtimeSourceBindings() { } return { path: repositoryPath, sha256: sha256(bytes) }; }; - const eagerModules = Object.fromEntries( - Object.entries(eagerRuntimeModules).map(([sourceId, [path, repositoryPath]]) => [ - sourceId, - binding(path, repositoryPath, `${sourceId} eager runtime module`), - ]), - ); return { + compilerEntry: binding( + packageEntry, + "packages/colors/compiler.js", + "compiler entry module", + ), harness: binding( harnessPath, "packages/colors/bench/wcag22-feasibility-boundary.bench.mjs", @@ -223,13 +244,11 @@ function runtimeSourceBindings() { "packages/colors/package.json", "package manifest", ), - packageRoot: binding(packageEntry, "packages/colors/index.js", "package-root module"), wasmGlue: binding( wasmGluePath, - "packages/colors/pkg/labcolors.js", + "packages/colors/compiler/labcolors_compiler.js", "wasm-bindgen JS glue", ), - ...eagerModules, }; } @@ -441,23 +460,31 @@ function validateSample(sample, shape, scenarioId, limits, sampleIndex, label) { exactKeys( sample, [ + "initSyncElapsedNs", "elapsedNs", "outcomeBytes", "outcomeSha256", "processMaxRssKiBAfter", + "processMaxRssKiBAfterInit", "processMaxRssKiBBefore", + "processMaxRssKiBBeforeInit", "requestBytes", "requestSha256", "sampleIndex", "summary", "wasmMemoryBytesAfter", + "wasmMemoryBytesAfterInit", "wasmMemoryBytesBefore", "wasmMemoryPagesAfter", + "wasmMemoryPagesAfterInit", "wasmMemoryPagesBefore", ], label, ); if (sample.sampleIndex !== sampleIndex) fail(`${label}.sampleIndex is not contiguous`); + decimal(sample.initSyncElapsedNs, `${label}.initSyncElapsedNs`, { + positive: true, + }); decimal(sample.elapsedNs, `${label}.elapsedNs`, { positive: true }); positiveSafeInteger(sample.requestBytes, `${label}.requestBytes`); positiveSafeInteger(sample.outcomeBytes, `${label}.outcomeBytes`); @@ -466,17 +493,26 @@ function validateSample(sample, shape, scenarioId, limits, sampleIndex, label) { for (const field of [ "processMaxRssKiBBefore", "processMaxRssKiBAfter", + "processMaxRssKiBBeforeInit", + "processMaxRssKiBAfterInit", "wasmMemoryBytesBefore", "wasmMemoryBytesAfter", + "wasmMemoryBytesAfterInit", "wasmMemoryPagesBefore", "wasmMemoryPagesAfter", + "wasmMemoryPagesAfterInit", ]) { nonNegativeSafeInteger(sample[field], `${label}.${field}`); } if ( + sample.processMaxRssKiBAfterInit < sample.processMaxRssKiBBeforeInit || + sample.processMaxRssKiBBefore < sample.processMaxRssKiBAfterInit || sample.processMaxRssKiBAfter < sample.processMaxRssKiBBefore || + sample.wasmMemoryBytesBefore < sample.wasmMemoryBytesAfterInit || sample.wasmMemoryBytesAfter < sample.wasmMemoryBytesBefore || + sample.wasmMemoryPagesBefore < sample.wasmMemoryPagesAfterInit || sample.wasmMemoryPagesAfter < sample.wasmMemoryPagesBefore || + sample.wasmMemoryBytesAfterInit !== sample.wasmMemoryPagesAfterInit * pageBytes || sample.wasmMemoryBytesBefore !== sample.wasmMemoryPagesBefore * pageBytes || sample.wasmMemoryBytesAfter !== sample.wasmMemoryPagesAfter * pageBytes ) { @@ -490,14 +526,14 @@ export function validateMeasurementArtifact( artifact, { requireCanonicalArtifact = true } = {}, ) { - const { core, toolchain } = sourceContracts(); + const { budget, core, toolchain } = sourceContracts(); const profileLimits = coreLimits(core.value); const expectedSampleCount = requiredSampleCount(core.value); exactKeys(artifact, artifactTopKeys, "artifact"); if ( artifact.schemaVersion !== 1 || artifact.artifactId !== MEASUREMENT_ARTIFACT_ID || - artifact.claimBoundary !== "canonical-wasm-package-root-whole-call-observations-only" + artifact.claimBoundary !== "canonical-wasm-compiler-entry-whole-call-observations-only" ) { fail("artifact identity or claim boundary drifted"); } @@ -511,7 +547,8 @@ export function validateMeasurementArtifact( artifact.claims.admission !== "canonical-linux-x64-exact-wasm-only" || !isDeepStrictEqual(artifact.claims.hardGates, hardGates) || artifact.claims.timingThresholdNs !== null || - artifact.claims.latency !== "observation-only-no-production-threshold" || + artifact.claims.latency !== + "init-sync-and-warm-operation-observations-only-no-production-threshold" || artifact.claims.memory !== memoryClaim ) { fail("claims add a guessed threshold, inflate memory meaning or weaken hard gates"); @@ -522,9 +559,11 @@ export function validateMeasurementArtifact( [ "canonicalCandidate", "cargoProfile", + "initSyncScope", "execution", "nodeVersion", - "packageRootApi", + "operationScope", + "publicEntry", "platform", "requestConstructionMeasured", "rustToolchain", @@ -540,10 +579,12 @@ export function validateMeasurementArtifact( const measuredToolchain = toolchain.value.measurement; if ( artifact.environment.execution !== "fresh-node-child-process-per-sample" || + artifact.environment.initSyncScope !== initSyncScope || + artifact.environment.operationScope !== operationScope || artifact.environment.sampleCount !== expectedSampleCount || artifact.environment.requestConstructionMeasured !== false || artifact.environment.timer !== "process.hrtime.bigint" || - artifact.environment.packageRootApi !== "packages/colors/index.js" || + artifact.environment.publicEntry !== "packages/colors/compiler.js" || !/^v\d+\.\d+\.\d+$/u.test(artifact.environment.nodeVersion ?? "") || (requireCanonicalArtifact && artifact.environment.nodeVersion !== canonicalNodeVersion()) || @@ -566,7 +607,7 @@ export function validateMeasurementArtifact( exactKeys( artifact.bindings, - ["coreAdmission", "packOracle", "runtimeSources", "wasm", "wasmToolchain"], + ["compilerSources", "coreAdmission", "packOracle", "wasm", "wasmBudget"], "bindings", ); exactKeys( @@ -576,7 +617,7 @@ export function validateMeasurementArtifact( ); if ( artifact.bindings.coreAdmission.path !== - "crates/labcolors-core/contracts/wcag22-feasibility-benchmark-v3.json" || + "crates/labcolors-core/contracts/wcag22-feasibility-benchmark-v4.json" || artifact.bindings.coreAdmission.schemaVersion !== core.value.schemaVersion || artifact.bindings.coreAdmission.artifactId !== core.value.artifactId || artifact.bindings.coreAdmission.profileId !== core.value.profileLimits.profileId || @@ -613,43 +654,56 @@ export function validateMeasurementArtifact( ) { fail("pack-5 LSB0 oracle binding drifted"); } - const sources = runtimeSourceBindings(); + const sources = compilerSourceBindings(); exactKeys( - artifact.bindings.runtimeSources, - runtimeSourceIds, - "bindings.runtimeSources", + artifact.bindings.compilerSources, + compilerSourceIds, + "bindings.compilerSources", ); - for (const sourceId of runtimeSourceIds) { + for (const sourceId of compilerSourceIds) { exactKeys( - artifact.bindings.runtimeSources[sourceId], + artifact.bindings.compilerSources[sourceId], ["path", "sha256"], - `bindings.runtimeSources.${sourceId}`, + `bindings.compilerSources.${sourceId}`, ); - if (!isDeepStrictEqual(artifact.bindings.runtimeSources[sourceId], sources[sourceId])) { - fail(`runtime source binding ${sourceId} drifted`); + if (!isDeepStrictEqual(artifact.bindings.compilerSources[sourceId], sources[sourceId])) { + fail(`compiler source binding ${sourceId} drifted`); } } exactKeys( - artifact.bindings.wasmToolchain, - ["budgetId", "path", "recipeSha256", "schemaVersion"], - "bindings.wasmToolchain", + artifact.bindings.wasmBudget, + ["budgetId", "fileSha256", "path", "recipeSha256", "role", "schemaVersion"], + "bindings.wasmBudget", ); if ( - artifact.bindings.wasmToolchain.path !== - "packages/colors/bench/wasm-size-budget-v1.json" || - artifact.bindings.wasmToolchain.schemaVersion !== toolchain.value.schemaVersion || - artifact.bindings.wasmToolchain.budgetId !== toolchain.value.budgetId || - artifact.bindings.wasmToolchain.recipeSha256 !== - sha256(Buffer.from(JSON.stringify(toolchainRecipe(toolchain.value)), "utf8")) + artifact.bindings.wasmBudget.path !== + "packages/colors/bench/wasm-size-budget-v5.json" || + artifact.bindings.wasmBudget.schemaVersion !== budget.value.schemaVersion || + artifact.bindings.wasmBudget.budgetId !== budget.value.budgetId || + artifact.bindings.wasmBudget.fileSha256 !== sha256(budget.bytes) || + artifact.bindings.wasmBudget.role !== "compiler" || + artifact.bindings.wasmBudget.recipeSha256 !== + budget.value.buildRecipes.compiler.recipeSha256 ) { - fail("canonical WASM toolchain binding drifted"); + fail("canonical compiler WASM budget binding drifted"); } exactKeys(artifact.bindings.wasm, ["bytes", "path", "sha256"], "bindings.wasm"); - if (artifact.bindings.wasm.path !== "packages/colors/pkg/labcolors_bg.wasm") { + if ( + artifact.bindings.wasm.path !== + "packages/colors/compiler/labcolors_compiler_bg.wasm" + ) { fail("WASM binding path drifted"); } positiveSafeInteger(artifact.bindings.wasm.bytes, "bindings.wasm.bytes"); digest(artifact.bindings.wasm.sha256, "bindings.wasm.sha256"); + const compilerMeasurement = budget.value.roles.compiler.measurement; + if ( + requireCanonicalArtifact && + (artifact.bindings.wasm.bytes !== compilerMeasurement.rawBytes || + artifact.bindings.wasm.sha256 !== compilerMeasurement.sha256) + ) { + fail("canonical compiler WASM bytes differ from the bound role budget"); + } exactKeys( artifact.limits, @@ -992,6 +1046,15 @@ function summarizeOutcome(outcome, scenarioId) { }; } +function observeOutcome(outcome, scenarioId) { + const bytes = new TextEncoder().encode(JSON.stringify(outcome)); + return { + outcomeBytes: bytes.byteLength, + outcomeSha256: sha256(bytes), + summary: summarizeOutcome(outcome, scenarioId), + }; +} + function wasmMemory(initOutput) { const memory = initOutput?.memory; if (!(memory instanceof WebAssembly.Memory)) { @@ -1005,39 +1068,55 @@ function wasmMemory(initOutput) { async function measureOneSample(scenarioId) { const { core } = sourceContracts(); const wasmBytes = readFileSync(wasmPath); - const rootApi = await import(pathToFileURL(packageEntry).href); - const initOutput = rootApi.initSync({ module: wasmBytes }); - const publicMax = rootApi.wcag22FeasibilityMaxBytes(); + const compilerApi = await import(pathToFileURL(packageEntry).href); + const beforeInitRss = process.resourceUsage().maxRSS; + const initStarted = process.hrtime.bigint(); + const initOutput = compilerApi.initSync({ module: wasmBytes }); + const initSyncElapsedNs = process.hrtime.bigint() - initStarted; + const afterInitMemory = wasmMemory(initOutput); + const afterInitRss = process.resourceUsage().maxRSS; + const publicMax = compilerApi.wcag22FeasibilityMaxBytes(); positiveSafeInteger(publicMax, "public max getter"); const limits = { maxRequestBytes: publicMax, ...coreLimits(core.value) }; const built = buildScenario(scenarioId, limits); const requestSha256 = sha256(built.bytes); + const warmupObservation = observeOutcome( + compilerApi.evaluateWcag22Feasibility(built.bytes), + scenarioId, + ); const beforeRss = process.resourceUsage().maxRSS; const beforeMemory = wasmMemory(initOutput); const started = process.hrtime.bigint(); - const outcome = rootApi.evaluateWcag22Feasibility(built.bytes); + const outcome = compilerApi.evaluateWcag22Feasibility(built.bytes); const elapsedNs = process.hrtime.bigint() - started; const afterMemory = wasmMemory(initOutput); const afterRss = process.resourceUsage().maxRSS; - - const outcomeBytes = new TextEncoder().encode(JSON.stringify(outcome)); + const outcomeObservation = observeOutcome(outcome, scenarioId); + if (!isDeepStrictEqual(warmupObservation, outcomeObservation)) { + fail("warm-up and measured operation returned different outcomes"); + } return { scenarioId, maxRequestBytes: publicMax, shape: built.shape, sample: { sampleIndex: 0, + initSyncElapsedNs: initSyncElapsedNs.toString(), elapsedNs: elapsedNs.toString(), requestBytes: built.bytes.byteLength, requestSha256, - outcomeBytes: outcomeBytes.byteLength, - outcomeSha256: sha256(outcomeBytes), - summary: summarizeOutcome(outcome, scenarioId), + outcomeBytes: outcomeObservation.outcomeBytes, + outcomeSha256: outcomeObservation.outcomeSha256, + summary: outcomeObservation.summary, + processMaxRssKiBBeforeInit: beforeInitRss, + processMaxRssKiBAfterInit: afterInitRss, processMaxRssKiBBefore: beforeRss, processMaxRssKiBAfter: afterRss, + wasmMemoryBytesAfterInit: afterInitMemory.bytes, wasmMemoryBytesBefore: beforeMemory.bytes, wasmMemoryBytesAfter: afterMemory.bytes, + wasmMemoryPagesAfterInit: afterInitMemory.pages, wasmMemoryPagesBefore: beforeMemory.pages, wasmMemoryPagesAfter: afterMemory.pages, }, @@ -1064,11 +1143,11 @@ function childSample(scenarioId) { } function measurementArtifact() { - const { core, toolchain } = sourceContracts(); + const { budget, core, toolchain } = sourceContracts(); const profileLimits = coreLimits(core.value); const measuredSampleCount = requiredSampleCount(core.value); const oracle = packOracle(); - const sources = runtimeSourceBindings(); + const sources = compilerSourceBindings(); const wasm = readFileSync(wasmPath); if (wasm.length < 8 || !wasm.subarray(0, 4).equals(Buffer.from([0, 97, 115, 109]))) { fail("built package WASM is absent or malformed"); @@ -1100,22 +1179,25 @@ function measurementArtifact() { const artifact = { schemaVersion: 1, artifactId: MEASUREMENT_ARTIFACT_ID, - claimBoundary: "canonical-wasm-package-root-whole-call-observations-only", + claimBoundary: "canonical-wasm-compiler-entry-whole-call-observations-only", claims: { admission: "canonical-linux-x64-exact-wasm-only", hardGates, timingThresholdNs: null, - latency: "observation-only-no-production-threshold", + latency: + "init-sync-and-warm-operation-observations-only-no-production-threshold", memory: memoryClaim, }, environment: { execution: "fresh-node-child-process-per-sample", + initSyncScope, + operationScope, platform, nodeVersion: process.version, sampleCount: measuredSampleCount, requestConstructionMeasured: false, timer: "process.hrtime.bigint", - packageRootApi: "packages/colors/index.js", + publicEntry: "packages/colors/compiler.js", canonicalCandidate: platform === "linux-x64" && canonicalPackageInput && @@ -1129,7 +1211,7 @@ function measurementArtifact() { }, bindings: { coreAdmission: { - path: "crates/labcolors-core/contracts/wcag22-feasibility-benchmark-v3.json", + path: "crates/labcolors-core/contracts/wcag22-feasibility-benchmark-v4.json", schemaVersion: core.value.schemaVersion, artifactId: core.value.artifactId, profileId: core.value.profileLimits.profileId, @@ -1146,17 +1228,17 @@ function measurementArtifact() { packVersion: oracle.packVersion, packDigest: oracle.packDigest, }, - runtimeSources: sources, - wasmToolchain: { - path: "packages/colors/bench/wasm-size-budget-v1.json", - schemaVersion: toolchain.value.schemaVersion, - budgetId: toolchain.value.budgetId, - recipeSha256: sha256( - Buffer.from(JSON.stringify(toolchainRecipe(toolchain.value)), "utf8"), - ), + compilerSources: sources, + wasmBudget: { + path: "packages/colors/bench/wasm-size-budget-v5.json", + schemaVersion: budget.value.schemaVersion, + budgetId: budget.value.budgetId, + fileSha256: sha256(budget.bytes), + role: "compiler", + recipeSha256: budget.value.buildRecipes.compiler.recipeSha256, }, wasm: { - path: "packages/colors/pkg/labcolors_bg.wasm", + path: "packages/colors/compiler/labcolors_compiler_bg.wasm", bytes: wasm.length, sha256: sha256(wasm), }, diff --git a/packages/colors/bench/wcag22-feasibility-wasm-boundary-v3.json b/packages/colors/bench/wcag22-feasibility-wasm-boundary-v3.json new file mode 100644 index 00000000..0b85f96d --- /dev/null +++ b/packages/colors/bench/wcag22-feasibility-wasm-boundary-v3.json @@ -0,0 +1 @@ +{"schemaVersion":1,"artifactId":"wcag22-feasibility-wasm-whole-call-v3","claimBoundary":"canonical-wasm-compiler-entry-whole-call-observations-only","claims":{"admission":"canonical-linux-x64-exact-wasm-only","hardGates":["completion","request-and-outcome-bytes","sha256-binding","terminal-algebra","packed-shape","candidate-major-lsb0-pack-oracle","no-proportional-dto"],"timingThresholdNs":null,"latency":"init-sync-and-warm-operation-observations-only-no-production-threshold","memory":"process maxRSS values are total-process high-water including V8 and prior warm-up/observer allocations; after-init and warm-call WASM pages are linear-memory high-water observations; neither is total operation memory"},"environment":{"execution":"fresh-node-child-process-per-sample","initSyncScope":"initSync-from-in-memory-compiler-wasm-includes-wasm-bindgen-startup-excludes-io-and-js-module-import","operationScope":"second-identical-operation-after-one-unmeasured-warm-up-whose-result-graph-is-not-retained-by-harness","platform":"linux-x64","nodeVersion":"v24.14.0","sampleCount":5,"requestConstructionMeasured":false,"timer":"process.hrtime.bigint","publicEntry":"packages/colors/compiler.js","canonicalCandidate":true,"rustToolchain":"1.96.0","wasmPack":"0.13.1","wasmBindgen":"0.2.126","target":"wasm32-unknown-unknown","cargoProfile":"release","wasmOpt":"-Oz"},"bindings":{"coreAdmission":{"path":"crates/labcolors-core/contracts/wcag22-feasibility-benchmark-v4.json","schemaVersion":1,"artifactId":"wcag22-feasibility-admission-raw-v4","profileId":"compile-v1","sha256":"3c257c336bc403eee933990fd7188a3b0a6e89d0cbc983aff18846ef76206275"},"packOracle":{"path":"conformance/vectors/wcag22-feasibility.json","sha256":"ae2caec47a7b650e73b8d4029a69b4e401dfb7cc199db579c0f95106eebe8dc3","caseId":"text-default-seven","requestSha256":"8da46d4cb835c4e3c89b8f56ec4025457e0e4308ed28adcfa7beb134858e3d6f","outcomeSha256":"9dbe5499abfd07cd915da6da0ca1578278aebc33b0f24c41c6318858026ff442","manifestPath":"conformance/vectors/manifest.json","manifestSha256":"6b873967ba648b4ec160fd3e909a3359d6ffcb0f129bf38711aa74f3dff5a63d","packVersion":"5.0.0","packDigest":"48d00cd5"},"compilerSources":{"compilerEntry":{"path":"packages/colors/compiler.js","sha256":"6fc5a3ebf670dcd624cfe410a6bb3870ccd4e4ed8e2ba9529fb4110b1332f3ba"},"harness":{"path":"packages/colors/bench/wcag22-feasibility-boundary.bench.mjs","sha256":"8c64d53d44720cf5253cace8d32150fac162dc738b043d90d2af2d4723b8edb1"},"packageManifest":{"path":"packages/colors/package.json","sha256":"0c4927abe3ae2ac6919e73ea76b59dd7a17ada0d04c8ec2f95000ad2cfeae865"},"wasmGlue":{"path":"packages/colors/compiler/labcolors_compiler.js","sha256":"b714a18d83c173ba3b491392440ff16233e6fdac055ec3243f18b40338a93535"}},"wasmBudget":{"path":"packages/colors/bench/wasm-size-budget-v5.json","schemaVersion":4,"budgetId":"labcolors-wasm-roles-issue-296-c1-v5","fileSha256":"e4b53a2eb976a8c66827a559cb81232e359b734dbfb14725da215cb496ff5d59","role":"compiler","recipeSha256":"ce53cea5f579c512a6d2f0c3348f250ac0a5e03206de55e7979c8eae1403be8f"},"wasm":{"path":"packages/colors/compiler/labcolors_compiler_bg.wasm","bytes":175212,"sha256":"3a552ce43ada7d0b10e90a23b4a7e50a4ecad77a446374b98ca8ee6b5c6a2a45"}},"limits":{"maxRequestBytes":657380,"profileId":"compile-v1","rawRelations":2047,"rawAdjacentEntries":2047,"opaqueUtf8Bytes":65536,"canonicalRelations":2047,"applicableEdges":2047,"logicalAssessments":524032,"packedResultBytes":65536},"scenarios":[{"scenarioId":"minimum-evaluated","shape":{"rawRelations":1,"rawAdjacentEntries":1,"opaqueUtf8Bytes":2,"canonicalRelations":1,"applicableRelations":1,"applicableEdges":1},"samples":[{"sampleIndex":0,"initSyncElapsedNs":"14685428","elapsedNs":"413204","requestBytes":222,"requestSha256":"21ff2cc1a9b3da86e633f999a5c8417f0be059c499fc9114490e274b319d2ee7","outcomeBytes":4823,"outcomeSha256":"a6196a5aaec38b74029c0d6f5016b9249008d7cd9544c3d5cf5de9932770e090","summary":{"outcome":"success","terminal":"feasible","domainCount":"256","canonicalRelations":"1","applicableRelations":"1","notApplicableRelations":"0","applicableEdges":"1","logicalAssessments":"256","failureMatrixBytes":32,"partitionBytes":32,"feasibleCandidates":7,"lsb0PartitionMatchesMatrix":true,"lsb0PackOracleMatches":true,"proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":50608,"processMaxRssKiBAfterInit":51904,"processMaxRssKiBBefore":54916,"processMaxRssKiBAfter":54916,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":1179648,"wasmMemoryBytesAfter":1179648,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":18,"wasmMemoryPagesAfter":18},{"sampleIndex":1,"initSyncElapsedNs":"1775968","elapsedNs":"468587","requestBytes":222,"requestSha256":"21ff2cc1a9b3da86e633f999a5c8417f0be059c499fc9114490e274b319d2ee7","outcomeBytes":4823,"outcomeSha256":"a6196a5aaec38b74029c0d6f5016b9249008d7cd9544c3d5cf5de9932770e090","summary":{"outcome":"success","terminal":"feasible","domainCount":"256","canonicalRelations":"1","applicableRelations":"1","notApplicableRelations":"0","applicableEdges":"1","logicalAssessments":"256","failureMatrixBytes":32,"partitionBytes":32,"feasibleCandidates":7,"lsb0PartitionMatchesMatrix":true,"lsb0PackOracleMatches":true,"proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":50896,"processMaxRssKiBAfterInit":52108,"processMaxRssKiBBefore":54932,"processMaxRssKiBAfter":54932,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":1179648,"wasmMemoryBytesAfter":1179648,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":18,"wasmMemoryPagesAfter":18},{"sampleIndex":2,"initSyncElapsedNs":"999768","elapsedNs":"402668","requestBytes":222,"requestSha256":"21ff2cc1a9b3da86e633f999a5c8417f0be059c499fc9114490e274b319d2ee7","outcomeBytes":4823,"outcomeSha256":"a6196a5aaec38b74029c0d6f5016b9249008d7cd9544c3d5cf5de9932770e090","summary":{"outcome":"success","terminal":"feasible","domainCount":"256","canonicalRelations":"1","applicableRelations":"1","notApplicableRelations":"0","applicableEdges":"1","logicalAssessments":"256","failureMatrixBytes":32,"partitionBytes":32,"feasibleCandidates":7,"lsb0PartitionMatchesMatrix":true,"lsb0PackOracleMatches":true,"proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":50380,"processMaxRssKiBAfterInit":51464,"processMaxRssKiBBefore":54544,"processMaxRssKiBAfter":54544,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":1179648,"wasmMemoryBytesAfter":1179648,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":18,"wasmMemoryPagesAfter":18},{"sampleIndex":3,"initSyncElapsedNs":"1351148","elapsedNs":"390128","requestBytes":222,"requestSha256":"21ff2cc1a9b3da86e633f999a5c8417f0be059c499fc9114490e274b319d2ee7","outcomeBytes":4823,"outcomeSha256":"a6196a5aaec38b74029c0d6f5016b9249008d7cd9544c3d5cf5de9932770e090","summary":{"outcome":"success","terminal":"feasible","domainCount":"256","canonicalRelations":"1","applicableRelations":"1","notApplicableRelations":"0","applicableEdges":"1","logicalAssessments":"256","failureMatrixBytes":32,"partitionBytes":32,"feasibleCandidates":7,"lsb0PartitionMatchesMatrix":true,"lsb0PackOracleMatches":true,"proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":50600,"processMaxRssKiBAfterInit":51684,"processMaxRssKiBBefore":54608,"processMaxRssKiBAfter":54608,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":1179648,"wasmMemoryBytesAfter":1179648,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":18,"wasmMemoryPagesAfter":18},{"sampleIndex":4,"initSyncElapsedNs":"928420","elapsedNs":"363558","requestBytes":222,"requestSha256":"21ff2cc1a9b3da86e633f999a5c8417f0be059c499fc9114490e274b319d2ee7","outcomeBytes":4823,"outcomeSha256":"a6196a5aaec38b74029c0d6f5016b9249008d7cd9544c3d5cf5de9932770e090","summary":{"outcome":"success","terminal":"feasible","domainCount":"256","canonicalRelations":"1","applicableRelations":"1","notApplicableRelations":"0","applicableEdges":"1","logicalAssessments":"256","failureMatrixBytes":32,"partitionBytes":32,"feasibleCandidates":7,"lsb0PartitionMatchesMatrix":true,"lsb0PackOracleMatches":true,"proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":50872,"processMaxRssKiBAfterInit":52084,"processMaxRssKiBBefore":54704,"processMaxRssKiBAfter":54704,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":1179648,"wasmMemoryBytesAfter":1179648,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":18,"wasmMemoryPagesAfter":18}]},{"scenarioId":"maximum-canonical-applicable-relations","shape":{"rawRelations":2047,"rawAdjacentEntries":2047,"opaqueUtf8Bytes":20470,"canonicalRelations":2047,"applicableRelations":2047,"applicableEdges":2047},"samples":[{"sampleIndex":0,"initSyncElapsedNs":"1603609","elapsedNs":"106458130","requestBytes":264164,"requestSha256":"83955d22987a29e4df0c91647cecadcb3a7e7f5bc06010e0b05519fac4b938d2","outcomeBytes":527090,"outcomeSha256":"dedc563b2a3f7bee7a0f37c5976af853814ac4e9ce12f27efaa3ec8661a79b1c","summary":{"outcome":"success","terminal":"feasible","domainCount":"256","canonicalRelations":"2047","applicableRelations":"2047","notApplicableRelations":"0","applicableEdges":"2047","logicalAssessments":"524032","failureMatrixBytes":65504,"partitionBytes":32,"feasibleCandidates":7,"lsb0PartitionMatchesMatrix":true,"proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":50764,"processMaxRssKiBAfterInit":51972,"processMaxRssKiBBefore":76636,"processMaxRssKiBAfter":77660,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":4194304,"wasmMemoryBytesAfter":4194304,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":64,"wasmMemoryPagesAfter":64},{"sampleIndex":1,"initSyncElapsedNs":"1231909","elapsedNs":"109174048","requestBytes":264164,"requestSha256":"83955d22987a29e4df0c91647cecadcb3a7e7f5bc06010e0b05519fac4b938d2","outcomeBytes":527090,"outcomeSha256":"dedc563b2a3f7bee7a0f37c5976af853814ac4e9ce12f27efaa3ec8661a79b1c","summary":{"outcome":"success","terminal":"feasible","domainCount":"256","canonicalRelations":"2047","applicableRelations":"2047","notApplicableRelations":"0","applicableEdges":"2047","logicalAssessments":"524032","failureMatrixBytes":65504,"partitionBytes":32,"feasibleCandidates":7,"lsb0PartitionMatchesMatrix":true,"proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":50928,"processMaxRssKiBAfterInit":52012,"processMaxRssKiBBefore":75660,"processMaxRssKiBAfter":76684,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":4194304,"wasmMemoryBytesAfter":4194304,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":64,"wasmMemoryPagesAfter":64},{"sampleIndex":2,"initSyncElapsedNs":"1276146","elapsedNs":"153455519","requestBytes":264164,"requestSha256":"83955d22987a29e4df0c91647cecadcb3a7e7f5bc06010e0b05519fac4b938d2","outcomeBytes":527090,"outcomeSha256":"dedc563b2a3f7bee7a0f37c5976af853814ac4e9ce12f27efaa3ec8661a79b1c","summary":{"outcome":"success","terminal":"feasible","domainCount":"256","canonicalRelations":"2047","applicableRelations":"2047","notApplicableRelations":"0","applicableEdges":"2047","logicalAssessments":"524032","failureMatrixBytes":65504,"partitionBytes":32,"feasibleCandidates":7,"lsb0PartitionMatchesMatrix":true,"proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":50832,"processMaxRssKiBAfterInit":51916,"processMaxRssKiBBefore":71080,"processMaxRssKiBAfter":72748,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":4194304,"wasmMemoryBytesAfter":4194304,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":64,"wasmMemoryPagesAfter":64},{"sampleIndex":3,"initSyncElapsedNs":"5495232","elapsedNs":"237415485","requestBytes":264164,"requestSha256":"83955d22987a29e4df0c91647cecadcb3a7e7f5bc06010e0b05519fac4b938d2","outcomeBytes":527090,"outcomeSha256":"dedc563b2a3f7bee7a0f37c5976af853814ac4e9ce12f27efaa3ec8661a79b1c","summary":{"outcome":"success","terminal":"feasible","domainCount":"256","canonicalRelations":"2047","applicableRelations":"2047","notApplicableRelations":"0","applicableEdges":"2047","logicalAssessments":"524032","failureMatrixBytes":65504,"partitionBytes":32,"feasibleCandidates":7,"lsb0PartitionMatchesMatrix":true,"proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":50912,"processMaxRssKiBAfterInit":52124,"processMaxRssKiBBefore":72916,"processMaxRssKiBAfter":74324,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":4194304,"wasmMemoryBytesAfter":4194304,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":64,"wasmMemoryPagesAfter":64},{"sampleIndex":4,"initSyncElapsedNs":"1317296","elapsedNs":"157489365","requestBytes":264164,"requestSha256":"83955d22987a29e4df0c91647cecadcb3a7e7f5bc06010e0b05519fac4b938d2","outcomeBytes":527090,"outcomeSha256":"dedc563b2a3f7bee7a0f37c5976af853814ac4e9ce12f27efaa3ec8661a79b1c","summary":{"outcome":"success","terminal":"feasible","domainCount":"256","canonicalRelations":"2047","applicableRelations":"2047","notApplicableRelations":"0","applicableEdges":"2047","logicalAssessments":"524032","failureMatrixBytes":65504,"partitionBytes":32,"feasibleCandidates":7,"lsb0PartitionMatchesMatrix":true,"proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":50940,"processMaxRssKiBAfterInit":52024,"processMaxRssKiBBefore":73512,"processMaxRssKiBAfter":75304,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":4194304,"wasmMemoryBytesAfter":4194304,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":64,"wasmMemoryPagesAfter":64}]},{"scenarioId":"maximum-applicable-edges","shape":{"rawRelations":1,"rawAdjacentEntries":2047,"opaqueUtf8Bytes":19,"canonicalRelations":1,"applicableRelations":1,"applicableEdges":2047},"samples":[{"sampleIndex":0,"initSyncElapsedNs":"977795","elapsedNs":"65994862","requestBytes":23909,"requestSha256":"9a60a035743121bc4b79660f3a76f75ff1d3da2c5b3a243449442f972e8af9ae","outcomeBytes":269795,"outcomeSha256":"7f08a3d65184a40adc8de04edc352511130fe6fc8e223729996a11cd18bc622c","summary":{"outcome":"success","terminal":"infeasible","domainCount":"256","canonicalRelations":"1","applicableRelations":"1","notApplicableRelations":"0","applicableEdges":"2047","logicalAssessments":"524032","failureMatrixBytes":65504,"partitionBytes":32,"feasibleCandidates":0,"lsb0PartitionMatchesMatrix":true,"proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":50584,"processMaxRssKiBAfterInit":51540,"processMaxRssKiBBefore":67604,"processMaxRssKiBAfter":68756,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":2162688,"wasmMemoryBytesAfter":2162688,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":33,"wasmMemoryPagesAfter":33},{"sampleIndex":1,"initSyncElapsedNs":"1984073","elapsedNs":"205404558","requestBytes":23909,"requestSha256":"9a60a035743121bc4b79660f3a76f75ff1d3da2c5b3a243449442f972e8af9ae","outcomeBytes":269795,"outcomeSha256":"7f08a3d65184a40adc8de04edc352511130fe6fc8e223729996a11cd18bc622c","summary":{"outcome":"success","terminal":"infeasible","domainCount":"256","canonicalRelations":"1","applicableRelations":"1","notApplicableRelations":"0","applicableEdges":"2047","logicalAssessments":"524032","failureMatrixBytes":65504,"partitionBytes":32,"feasibleCandidates":0,"lsb0PartitionMatchesMatrix":true,"proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":50684,"processMaxRssKiBAfterInit":51896,"processMaxRssKiBBefore":66356,"processMaxRssKiBAfter":67636,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":2162688,"wasmMemoryBytesAfter":2162688,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":33,"wasmMemoryPagesAfter":33},{"sampleIndex":2,"initSyncElapsedNs":"7424521","elapsedNs":"130475227","requestBytes":23909,"requestSha256":"9a60a035743121bc4b79660f3a76f75ff1d3da2c5b3a243449442f972e8af9ae","outcomeBytes":269795,"outcomeSha256":"7f08a3d65184a40adc8de04edc352511130fe6fc8e223729996a11cd18bc622c","summary":{"outcome":"success","terminal":"infeasible","domainCount":"256","canonicalRelations":"1","applicableRelations":"1","notApplicableRelations":"0","applicableEdges":"2047","logicalAssessments":"524032","failureMatrixBytes":65504,"partitionBytes":32,"feasibleCandidates":0,"lsb0PartitionMatchesMatrix":true,"proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":50912,"processMaxRssKiBAfterInit":51996,"processMaxRssKiBBefore":66784,"processMaxRssKiBAfter":68064,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":2162688,"wasmMemoryBytesAfter":2162688,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":33,"wasmMemoryPagesAfter":33},{"sampleIndex":3,"initSyncElapsedNs":"1607456","elapsedNs":"130210698","requestBytes":23909,"requestSha256":"9a60a035743121bc4b79660f3a76f75ff1d3da2c5b3a243449442f972e8af9ae","outcomeBytes":269795,"outcomeSha256":"7f08a3d65184a40adc8de04edc352511130fe6fc8e223729996a11cd18bc622c","summary":{"outcome":"success","terminal":"infeasible","domainCount":"256","canonicalRelations":"1","applicableRelations":"1","notApplicableRelations":"0","applicableEdges":"2047","logicalAssessments":"524032","failureMatrixBytes":65504,"partitionBytes":32,"feasibleCandidates":0,"lsb0PartitionMatchesMatrix":true,"proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":50624,"processMaxRssKiBAfterInit":51836,"processMaxRssKiBBefore":66772,"processMaxRssKiBAfter":68052,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":2162688,"wasmMemoryBytesAfter":2162688,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":33,"wasmMemoryPagesAfter":33},{"sampleIndex":4,"initSyncElapsedNs":"7220103","elapsedNs":"124067531","requestBytes":23909,"requestSha256":"9a60a035743121bc4b79660f3a76f75ff1d3da2c5b3a243449442f972e8af9ae","outcomeBytes":269795,"outcomeSha256":"7f08a3d65184a40adc8de04edc352511130fe6fc8e223729996a11cd18bc622c","summary":{"outcome":"success","terminal":"infeasible","domainCount":"256","canonicalRelations":"1","applicableRelations":"1","notApplicableRelations":"0","applicableEdges":"2047","logicalAssessments":"524032","failureMatrixBytes":65504,"partitionBytes":32,"feasibleCandidates":0,"lsb0PartitionMatchesMatrix":true,"proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":50832,"processMaxRssKiBAfterInit":52172,"processMaxRssKiBBefore":66972,"processMaxRssKiBAfter":68508,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":2162688,"wasmMemoryBytesAfter":2162688,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":33,"wasmMemoryPagesAfter":33}]},{"scenarioId":"maximum-opaque-utf8-bytes","shape":{"rawRelations":1,"rawAdjacentEntries":0,"opaqueUtf8Bytes":65536,"canonicalRelations":1,"applicableRelations":0,"applicableEdges":0},"samples":[{"sampleIndex":0,"initSyncElapsedNs":"1154342","elapsedNs":"2915259","requestBytes":65710,"requestSha256":"4c2106974c30967456ac4d92bced32418cb0cb9b8f9ba97e53f19e0f629cf1f2","outcomeBytes":66050,"outcomeSha256":"4651c1ed3d6d43ef8d0eeaf78bb745d4b58ba7d160877df4a23433b233869f18","summary":{"outcome":"success","terminal":"notEvaluated","canonicalRelations":"1","applicableRelations":"0","notApplicableRelations":"1","numericalEvidencePresent":false,"proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":50624,"processMaxRssKiBAfterInit":51836,"processMaxRssKiBBefore":58276,"processMaxRssKiBAfter":59084,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":1507328,"wasmMemoryBytesAfter":1507328,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":23,"wasmMemoryPagesAfter":23},{"sampleIndex":1,"initSyncElapsedNs":"1101382","elapsedNs":"15272411","requestBytes":65710,"requestSha256":"4c2106974c30967456ac4d92bced32418cb0cb9b8f9ba97e53f19e0f629cf1f2","outcomeBytes":66050,"outcomeSha256":"4651c1ed3d6d43ef8d0eeaf78bb745d4b58ba7d160877df4a23433b233869f18","summary":{"outcome":"success","terminal":"notEvaluated","canonicalRelations":"1","applicableRelations":"0","notApplicableRelations":"1","numericalEvidencePresent":false,"proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":51016,"processMaxRssKiBAfterInit":52100,"processMaxRssKiBBefore":58192,"processMaxRssKiBAfter":59344,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":1507328,"wasmMemoryBytesAfter":1507328,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":23,"wasmMemoryPagesAfter":23},{"sampleIndex":2,"initSyncElapsedNs":"931434","elapsedNs":"9451740","requestBytes":65710,"requestSha256":"4c2106974c30967456ac4d92bced32418cb0cb9b8f9ba97e53f19e0f629cf1f2","outcomeBytes":66050,"outcomeSha256":"4651c1ed3d6d43ef8d0eeaf78bb745d4b58ba7d160877df4a23433b233869f18","summary":{"outcome":"success","terminal":"notEvaluated","canonicalRelations":"1","applicableRelations":"0","notApplicableRelations":"1","numericalEvidencePresent":false,"proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":50692,"processMaxRssKiBAfterInit":51900,"processMaxRssKiBBefore":58336,"processMaxRssKiBAfter":58976,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":1507328,"wasmMemoryBytesAfter":1507328,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":23,"wasmMemoryPagesAfter":23},{"sampleIndex":3,"initSyncElapsedNs":"18817802","elapsedNs":"1922780","requestBytes":65710,"requestSha256":"4c2106974c30967456ac4d92bced32418cb0cb9b8f9ba97e53f19e0f629cf1f2","outcomeBytes":66050,"outcomeSha256":"4651c1ed3d6d43ef8d0eeaf78bb745d4b58ba7d160877df4a23433b233869f18","summary":{"outcome":"success","terminal":"notEvaluated","canonicalRelations":"1","applicableRelations":"0","notApplicableRelations":"1","numericalEvidencePresent":false,"proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":50916,"processMaxRssKiBAfterInit":52000,"processMaxRssKiBBefore":58472,"processMaxRssKiBAfter":59296,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":1507328,"wasmMemoryBytesAfter":1507328,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":23,"wasmMemoryPagesAfter":23},{"sampleIndex":4,"initSyncElapsedNs":"2179510","elapsedNs":"2110154","requestBytes":65710,"requestSha256":"4c2106974c30967456ac4d92bced32418cb0cb9b8f9ba97e53f19e0f629cf1f2","outcomeBytes":66050,"outcomeSha256":"4651c1ed3d6d43ef8d0eeaf78bb745d4b58ba7d160877df4a23433b233869f18","summary":{"outcome":"success","terminal":"notEvaluated","canonicalRelations":"1","applicableRelations":"0","notApplicableRelations":"1","numericalEvidencePresent":false,"proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":50592,"processMaxRssKiBAfterInit":51676,"processMaxRssKiBBefore":58196,"processMaxRssKiBAfter":58892,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":1507328,"wasmMemoryBytesAfter":1507328,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":23,"wasmMemoryPagesAfter":23}]},{"scenarioId":"maximum-canonical-not-applicable-relations","shape":{"rawRelations":2047,"rawAdjacentEntries":0,"opaqueUtf8Bytes":36846,"canonicalRelations":2047,"applicableRelations":0,"applicableEdges":0},"samples":[{"sampleIndex":0,"initSyncElapsedNs":"969883","elapsedNs":"16474917","requestBytes":186378,"requestSha256":"b92c8700a4f6f6979e9476d56e57166618971aaecdbf4445ccd6aac124f821b7","outcomeBytes":186719,"outcomeSha256":"bf21a25ad92cfefad9c7116c16f918d97dab5e18e610a21482b26fdf5a948d69","summary":{"outcome":"success","terminal":"notEvaluated","canonicalRelations":"2047","applicableRelations":"0","notApplicableRelations":"2047","numericalEvidencePresent":false,"proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":50600,"processMaxRssKiBAfterInit":51680,"processMaxRssKiBBefore":70040,"processMaxRssKiBAfter":70040,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":2097152,"wasmMemoryBytesAfter":2097152,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":32,"wasmMemoryPagesAfter":32},{"sampleIndex":1,"initSyncElapsedNs":"1749369","elapsedNs":"7938518","requestBytes":186378,"requestSha256":"b92c8700a4f6f6979e9476d56e57166618971aaecdbf4445ccd6aac124f821b7","outcomeBytes":186719,"outcomeSha256":"bf21a25ad92cfefad9c7116c16f918d97dab5e18e610a21482b26fdf5a948d69","summary":{"outcome":"success","terminal":"notEvaluated","canonicalRelations":"2047","applicableRelations":"0","notApplicableRelations":"2047","numericalEvidencePresent":false,"proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":50824,"processMaxRssKiBAfterInit":51780,"processMaxRssKiBBefore":69052,"processMaxRssKiBAfter":70460,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":2097152,"wasmMemoryBytesAfter":2097152,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":32,"wasmMemoryPagesAfter":32},{"sampleIndex":2,"initSyncElapsedNs":"1055283","elapsedNs":"7439374","requestBytes":186378,"requestSha256":"b92c8700a4f6f6979e9476d56e57166618971aaecdbf4445ccd6aac124f821b7","outcomeBytes":186719,"outcomeSha256":"bf21a25ad92cfefad9c7116c16f918d97dab5e18e610a21482b26fdf5a948d69","summary":{"outcome":"success","terminal":"notEvaluated","canonicalRelations":"2047","applicableRelations":"0","notApplicableRelations":"2047","numericalEvidencePresent":false,"proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":50540,"processMaxRssKiBAfterInit":51752,"processMaxRssKiBBefore":67416,"processMaxRssKiBAfter":67508,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":2097152,"wasmMemoryBytesAfter":2097152,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":32,"wasmMemoryPagesAfter":32},{"sampleIndex":3,"initSyncElapsedNs":"1287452","elapsedNs":"20630456","requestBytes":186378,"requestSha256":"b92c8700a4f6f6979e9476d56e57166618971aaecdbf4445ccd6aac124f821b7","outcomeBytes":186719,"outcomeSha256":"bf21a25ad92cfefad9c7116c16f918d97dab5e18e610a21482b26fdf5a948d69","summary":{"outcome":"success","terminal":"notEvaluated","canonicalRelations":"2047","applicableRelations":"0","notApplicableRelations":"2047","numericalEvidencePresent":false,"proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":50836,"processMaxRssKiBAfterInit":51920,"processMaxRssKiBBefore":67944,"processMaxRssKiBAfter":69864,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":2097152,"wasmMemoryBytesAfter":2097152,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":32,"wasmMemoryPagesAfter":32},{"sampleIndex":4,"initSyncElapsedNs":"1188573","elapsedNs":"7993420","requestBytes":186378,"requestSha256":"b92c8700a4f6f6979e9476d56e57166618971aaecdbf4445ccd6aac124f821b7","outcomeBytes":186719,"outcomeSha256":"bf21a25ad92cfefad9c7116c16f918d97dab5e18e610a21482b26fdf5a948d69","summary":{"outcome":"success","terminal":"notEvaluated","canonicalRelations":"2047","applicableRelations":"0","notApplicableRelations":"2047","numericalEvidencePresent":false,"proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":50720,"processMaxRssKiBAfterInit":51804,"processMaxRssKiBBefore":68032,"processMaxRssKiBAfter":69440,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":2097152,"wasmMemoryBytesAfter":2097152,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":32,"wasmMemoryPagesAfter":32}]},{"scenarioId":"maximum-combined-not-applicable-envelope","shape":{"rawRelations":2047,"rawAdjacentEntries":0,"opaqueUtf8Bytes":65536,"canonicalRelations":2047,"applicableRelations":0,"applicableEdges":0},"samples":[{"sampleIndex":0,"initSyncElapsedNs":"1429917","elapsedNs":"13577746","requestBytes":542748,"requestSha256":"25ecc80095ed218f461bfe5283fa19d3e440eb603d49e8b65b65f5ec57a92ece","outcomeBytes":543093,"outcomeSha256":"dab7eda461b67a099559b2419b077c541991f62263f5e393322f278164835c33","summary":{"outcome":"success","terminal":"notEvaluated","canonicalRelations":"2047","applicableRelations":"0","notApplicableRelations":"2047","numericalEvidencePresent":false,"proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":51084,"processMaxRssKiBAfterInit":52040,"processMaxRssKiBBefore":73380,"processMaxRssKiBAfter":74276,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":4128768,"wasmMemoryBytesAfter":4128768,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":63,"wasmMemoryPagesAfter":63},{"sampleIndex":1,"initSyncElapsedNs":"982703","elapsedNs":"15226223","requestBytes":542748,"requestSha256":"25ecc80095ed218f461bfe5283fa19d3e440eb603d49e8b65b65f5ec57a92ece","outcomeBytes":543093,"outcomeSha256":"dab7eda461b67a099559b2419b077c541991f62263f5e393322f278164835c33","summary":{"outcome":"success","terminal":"notEvaluated","canonicalRelations":"2047","applicableRelations":"0","notApplicableRelations":"2047","numericalEvidencePresent":false,"proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":50732,"processMaxRssKiBAfterInit":51816,"processMaxRssKiBBefore":74128,"processMaxRssKiBAfter":74128,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":4128768,"wasmMemoryBytesAfter":4128768,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":63,"wasmMemoryPagesAfter":63},{"sampleIndex":2,"initSyncElapsedNs":"1087301","elapsedNs":"13834021","requestBytes":542748,"requestSha256":"25ecc80095ed218f461bfe5283fa19d3e440eb603d49e8b65b65f5ec57a92ece","outcomeBytes":543093,"outcomeSha256":"dab7eda461b67a099559b2419b077c541991f62263f5e393322f278164835c33","summary":{"outcome":"success","terminal":"notEvaluated","canonicalRelations":"2047","applicableRelations":"0","notApplicableRelations":"2047","numericalEvidencePresent":false,"proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":50680,"processMaxRssKiBAfterInit":51764,"processMaxRssKiBBefore":72452,"processMaxRssKiBAfter":72452,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":4128768,"wasmMemoryBytesAfter":4128768,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":63,"wasmMemoryPagesAfter":63},{"sampleIndex":3,"initSyncElapsedNs":"5999833","elapsedNs":"14952049","requestBytes":542748,"requestSha256":"25ecc80095ed218f461bfe5283fa19d3e440eb603d49e8b65b65f5ec57a92ece","outcomeBytes":543093,"outcomeSha256":"dab7eda461b67a099559b2419b077c541991f62263f5e393322f278164835c33","summary":{"outcome":"success","terminal":"notEvaluated","canonicalRelations":"2047","applicableRelations":"0","notApplicableRelations":"2047","numericalEvidencePresent":false,"proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":50748,"processMaxRssKiBAfterInit":51960,"processMaxRssKiBBefore":74544,"processMaxRssKiBAfter":74672,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":4128768,"wasmMemoryBytesAfter":4128768,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":63,"wasmMemoryPagesAfter":63},{"sampleIndex":4,"initSyncElapsedNs":"982973","elapsedNs":"23650303","requestBytes":542748,"requestSha256":"25ecc80095ed218f461bfe5283fa19d3e440eb603d49e8b65b65f5ec57a92ece","outcomeBytes":543093,"outcomeSha256":"dab7eda461b67a099559b2419b077c541991f62263f5e393322f278164835c33","summary":{"outcome":"success","terminal":"notEvaluated","canonicalRelations":"2047","applicableRelations":"0","notApplicableRelations":"2047","numericalEvidencePresent":false,"proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":50764,"processMaxRssKiBAfterInit":51848,"processMaxRssKiBBefore":72208,"processMaxRssKiBAfter":73104,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":4128768,"wasmMemoryBytesAfter":4128768,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":63,"wasmMemoryPagesAfter":63}]},{"scenarioId":"maximum-combined-applicable-envelope","shape":{"rawRelations":2047,"rawAdjacentEntries":2047,"opaqueUtf8Bytes":65536,"canonicalRelations":2047,"applicableRelations":2047,"applicableEdges":2047},"samples":[{"sampleIndex":0,"initSyncElapsedNs":"4179065","elapsedNs":"192891198","requestBytes":657380,"requestSha256":"53bbc4fa41ce062a2056151b8e5f7ae68a8d81b122e6178bc919aa749b9898c3","outcomeBytes":847668,"outcomeSha256":"885a51a2136c2a09189bd5c88c35b886d343f99917a3c55f7231b2b37203e270","summary":{"outcome":"success","terminal":"feasible","domainCount":"256","canonicalRelations":"2047","applicableRelations":"2047","notApplicableRelations":"0","applicableEdges":"2047","logicalAssessments":"524032","failureMatrixBytes":65504,"partitionBytes":32,"feasibleCandidates":149,"lsb0PartitionMatchesMatrix":true,"proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":50664,"processMaxRssKiBAfterInit":51876,"processMaxRssKiBBefore":75808,"processMaxRssKiBAfter":77216,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":4653056,"wasmMemoryBytesAfter":4653056,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":71,"wasmMemoryPagesAfter":71},{"sampleIndex":1,"initSyncElapsedNs":"1084476","elapsedNs":"202202076","requestBytes":657380,"requestSha256":"53bbc4fa41ce062a2056151b8e5f7ae68a8d81b122e6178bc919aa749b9898c3","outcomeBytes":847668,"outcomeSha256":"885a51a2136c2a09189bd5c88c35b886d343f99917a3c55f7231b2b37203e270","summary":{"outcome":"success","terminal":"feasible","domainCount":"256","canonicalRelations":"2047","applicableRelations":"2047","notApplicableRelations":"0","applicableEdges":"2047","logicalAssessments":"524032","failureMatrixBytes":65504,"partitionBytes":32,"feasibleCandidates":149,"lsb0PartitionMatchesMatrix":true,"proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":50492,"processMaxRssKiBAfterInit":51704,"processMaxRssKiBBefore":77080,"processMaxRssKiBAfter":78616,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":4653056,"wasmMemoryBytesAfter":4653056,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":71,"wasmMemoryPagesAfter":71},{"sampleIndex":2,"initSyncElapsedNs":"1810172","elapsedNs":"167308929","requestBytes":657380,"requestSha256":"53bbc4fa41ce062a2056151b8e5f7ae68a8d81b122e6178bc919aa749b9898c3","outcomeBytes":847668,"outcomeSha256":"885a51a2136c2a09189bd5c88c35b886d343f99917a3c55f7231b2b37203e270","summary":{"outcome":"success","terminal":"feasible","domainCount":"256","canonicalRelations":"2047","applicableRelations":"2047","notApplicableRelations":"0","applicableEdges":"2047","logicalAssessments":"524032","failureMatrixBytes":65504,"partitionBytes":32,"feasibleCandidates":149,"lsb0PartitionMatchesMatrix":true,"proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":52800,"processMaxRssKiBAfterInit":53884,"processMaxRssKiBBefore":79348,"processMaxRssKiBAfter":80628,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":4653056,"wasmMemoryBytesAfter":4653056,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":71,"wasmMemoryPagesAfter":71},{"sampleIndex":3,"initSyncElapsedNs":"1407043","elapsedNs":"127140196","requestBytes":657380,"requestSha256":"53bbc4fa41ce062a2056151b8e5f7ae68a8d81b122e6178bc919aa749b9898c3","outcomeBytes":847668,"outcomeSha256":"885a51a2136c2a09189bd5c88c35b886d343f99917a3c55f7231b2b37203e270","summary":{"outcome":"success","terminal":"feasible","domainCount":"256","canonicalRelations":"2047","applicableRelations":"2047","notApplicableRelations":"0","applicableEdges":"2047","logicalAssessments":"524032","failureMatrixBytes":65504,"partitionBytes":32,"feasibleCandidates":149,"lsb0PartitionMatchesMatrix":true,"proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":50840,"processMaxRssKiBAfterInit":52052,"processMaxRssKiBBefore":76512,"processMaxRssKiBAfter":77920,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":4653056,"wasmMemoryBytesAfter":4653056,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":71,"wasmMemoryPagesAfter":71},{"sampleIndex":4,"initSyncElapsedNs":"1036284","elapsedNs":"109859312","requestBytes":657380,"requestSha256":"53bbc4fa41ce062a2056151b8e5f7ae68a8d81b122e6178bc919aa749b9898c3","outcomeBytes":847668,"outcomeSha256":"885a51a2136c2a09189bd5c88c35b886d343f99917a3c55f7231b2b37203e270","summary":{"outcome":"success","terminal":"feasible","domainCount":"256","canonicalRelations":"2047","applicableRelations":"2047","notApplicableRelations":"0","applicableEdges":"2047","logicalAssessments":"524032","failureMatrixBytes":65504,"partitionBytes":32,"feasibleCandidates":149,"lsb0PartitionMatchesMatrix":true,"proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":50932,"processMaxRssKiBAfterInit":52144,"processMaxRssKiBBefore":77268,"processMaxRssKiBAfter":78676,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":4653056,"wasmMemoryBytesAfter":4653056,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":71,"wasmMemoryPagesAfter":71}]},{"scenarioId":"maximum-raw-duplicate-relations","shape":{"rawRelations":2047,"rawAdjacentEntries":2047,"opaqueUtf8Bytes":26611,"canonicalRelations":1,"applicableRelations":1,"applicableEdges":1},"samples":[{"sampleIndex":0,"initSyncElapsedNs":"972117","elapsedNs":"13229099","requestBytes":270305,"requestSha256":"fc5f72ce71f8f9d81164dbe7632ac3345fca31c2eb677db9709c45bd7a04077a","outcomeBytes":4837,"outcomeSha256":"ef8e4cf8ac7580773571fa354b6b2eed38e4ca60d32780b0ba86e2be8cf6bcf2","summary":{"outcome":"success","terminal":"feasible","domainCount":"256","canonicalRelations":"1","applicableRelations":"1","notApplicableRelations":"0","applicableEdges":"1","logicalAssessments":"256","failureMatrixBytes":32,"partitionBytes":32,"feasibleCandidates":7,"lsb0PartitionMatchesMatrix":true,"proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":50572,"processMaxRssKiBAfterInit":51908,"processMaxRssKiBBefore":64856,"processMaxRssKiBAfter":65640,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":1703936,"wasmMemoryBytesAfter":1703936,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":26,"wasmMemoryPagesAfter":26},{"sampleIndex":1,"initSyncElapsedNs":"1464239","elapsedNs":"9793445","requestBytes":270305,"requestSha256":"fc5f72ce71f8f9d81164dbe7632ac3345fca31c2eb677db9709c45bd7a04077a","outcomeBytes":4837,"outcomeSha256":"ef8e4cf8ac7580773571fa354b6b2eed38e4ca60d32780b0ba86e2be8cf6bcf2","summary":{"outcome":"success","terminal":"feasible","domainCount":"256","canonicalRelations":"1","applicableRelations":"1","notApplicableRelations":"0","applicableEdges":"1","logicalAssessments":"256","failureMatrixBytes":32,"partitionBytes":32,"feasibleCandidates":7,"lsb0PartitionMatchesMatrix":true,"proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":50668,"processMaxRssKiBAfterInit":51752,"processMaxRssKiBBefore":67824,"processMaxRssKiBAfter":67824,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":1703936,"wasmMemoryBytesAfter":1703936,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":26,"wasmMemoryPagesAfter":26},{"sampleIndex":2,"initSyncElapsedNs":"1775939","elapsedNs":"9078837","requestBytes":270305,"requestSha256":"fc5f72ce71f8f9d81164dbe7632ac3345fca31c2eb677db9709c45bd7a04077a","outcomeBytes":4837,"outcomeSha256":"ef8e4cf8ac7580773571fa354b6b2eed38e4ca60d32780b0ba86e2be8cf6bcf2","summary":{"outcome":"success","terminal":"feasible","domainCount":"256","canonicalRelations":"1","applicableRelations":"1","notApplicableRelations":"0","applicableEdges":"1","logicalAssessments":"256","failureMatrixBytes":32,"partitionBytes":32,"feasibleCandidates":7,"lsb0PartitionMatchesMatrix":true,"proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":50664,"processMaxRssKiBAfterInit":51876,"processMaxRssKiBBefore":66484,"processMaxRssKiBAfter":66740,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":1703936,"wasmMemoryBytesAfter":1703936,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":26,"wasmMemoryPagesAfter":26},{"sampleIndex":3,"initSyncElapsedNs":"1751974","elapsedNs":"8689241","requestBytes":270305,"requestSha256":"fc5f72ce71f8f9d81164dbe7632ac3345fca31c2eb677db9709c45bd7a04077a","outcomeBytes":4837,"outcomeSha256":"ef8e4cf8ac7580773571fa354b6b2eed38e4ca60d32780b0ba86e2be8cf6bcf2","summary":{"outcome":"success","terminal":"feasible","domainCount":"256","canonicalRelations":"1","applicableRelations":"1","notApplicableRelations":"0","applicableEdges":"1","logicalAssessments":"256","failureMatrixBytes":32,"partitionBytes":32,"feasibleCandidates":7,"lsb0PartitionMatchesMatrix":true,"proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":50536,"processMaxRssKiBAfterInit":51620,"processMaxRssKiBBefore":65280,"processMaxRssKiBAfter":66560,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":1703936,"wasmMemoryBytesAfter":1703936,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":26,"wasmMemoryPagesAfter":26},{"sampleIndex":4,"initSyncElapsedNs":"3739121","elapsedNs":"4403494","requestBytes":270305,"requestSha256":"fc5f72ce71f8f9d81164dbe7632ac3345fca31c2eb677db9709c45bd7a04077a","outcomeBytes":4837,"outcomeSha256":"ef8e4cf8ac7580773571fa354b6b2eed38e4ca60d32780b0ba86e2be8cf6bcf2","summary":{"outcome":"success","terminal":"feasible","domainCount":"256","canonicalRelations":"1","applicableRelations":"1","notApplicableRelations":"0","applicableEdges":"1","logicalAssessments":"256","failureMatrixBytes":32,"partitionBytes":32,"feasibleCandidates":7,"lsb0PartitionMatchesMatrix":true,"proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":50316,"processMaxRssKiBAfterInit":51528,"processMaxRssKiBBefore":64660,"processMaxRssKiBAfter":65044,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":1703936,"wasmMemoryBytesAfter":1703936,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":26,"wasmMemoryPagesAfter":26}]},{"scenarioId":"maximum-raw-adjacent-duplicates","shape":{"rawRelations":1,"rawAdjacentEntries":2047,"opaqueUtf8Bytes":2,"canonicalRelations":1,"applicableRelations":1,"applicableEdges":1},"samples":[{"sampleIndex":0,"initSyncElapsedNs":"5150901","elapsedNs":"1576279","requestBytes":28866,"requestSha256":"495db8cf72c85e1811f87180cefb4a90c8d64be1277ad4ed41562a2e97034979","outcomeBytes":4823,"outcomeSha256":"a6196a5aaec38b74029c0d6f5016b9249008d7cd9544c3d5cf5de9932770e090","summary":{"outcome":"success","terminal":"feasible","domainCount":"256","canonicalRelations":"1","applicableRelations":"1","notApplicableRelations":"0","applicableEdges":"1","logicalAssessments":"256","failureMatrixBytes":32,"partitionBytes":32,"feasibleCandidates":7,"lsb0PartitionMatchesMatrix":true,"proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":50916,"processMaxRssKiBAfterInit":52000,"processMaxRssKiBBefore":58928,"processMaxRssKiBAfter":58928,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":1376256,"wasmMemoryBytesAfter":1376256,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":21,"wasmMemoryPagesAfter":21},{"sampleIndex":1,"initSyncElapsedNs":"1123004","elapsedNs":"1633043","requestBytes":28866,"requestSha256":"495db8cf72c85e1811f87180cefb4a90c8d64be1277ad4ed41562a2e97034979","outcomeBytes":4823,"outcomeSha256":"a6196a5aaec38b74029c0d6f5016b9249008d7cd9544c3d5cf5de9932770e090","summary":{"outcome":"success","terminal":"feasible","domainCount":"256","canonicalRelations":"1","applicableRelations":"1","notApplicableRelations":"0","applicableEdges":"1","logicalAssessments":"256","failureMatrixBytes":32,"partitionBytes":32,"feasibleCandidates":7,"lsb0PartitionMatchesMatrix":true,"proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":50740,"processMaxRssKiBAfterInit":51952,"processMaxRssKiBBefore":59016,"processMaxRssKiBAfter":59016,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":1376256,"wasmMemoryBytesAfter":1376256,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":21,"wasmMemoryPagesAfter":21},{"sampleIndex":2,"initSyncElapsedNs":"1380212","elapsedNs":"5935626","requestBytes":28866,"requestSha256":"495db8cf72c85e1811f87180cefb4a90c8d64be1277ad4ed41562a2e97034979","outcomeBytes":4823,"outcomeSha256":"a6196a5aaec38b74029c0d6f5016b9249008d7cd9544c3d5cf5de9932770e090","summary":{"outcome":"success","terminal":"feasible","domainCount":"256","canonicalRelations":"1","applicableRelations":"1","notApplicableRelations":"0","applicableEdges":"1","logicalAssessments":"256","failureMatrixBytes":32,"partitionBytes":32,"feasibleCandidates":7,"lsb0PartitionMatchesMatrix":true,"proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":50672,"processMaxRssKiBAfterInit":51756,"processMaxRssKiBBefore":58988,"processMaxRssKiBAfter":58988,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":1376256,"wasmMemoryBytesAfter":1376256,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":21,"wasmMemoryPagesAfter":21},{"sampleIndex":3,"initSyncElapsedNs":"1361444","elapsedNs":"1947789","requestBytes":28866,"requestSha256":"495db8cf72c85e1811f87180cefb4a90c8d64be1277ad4ed41562a2e97034979","outcomeBytes":4823,"outcomeSha256":"a6196a5aaec38b74029c0d6f5016b9249008d7cd9544c3d5cf5de9932770e090","summary":{"outcome":"success","terminal":"feasible","domainCount":"256","canonicalRelations":"1","applicableRelations":"1","notApplicableRelations":"0","applicableEdges":"1","logicalAssessments":"256","failureMatrixBytes":32,"partitionBytes":32,"feasibleCandidates":7,"lsb0PartitionMatchesMatrix":true,"proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":50784,"processMaxRssKiBAfterInit":51868,"processMaxRssKiBBefore":59308,"processMaxRssKiBAfter":59308,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":1376256,"wasmMemoryBytesAfter":1376256,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":21,"wasmMemoryPagesAfter":21},{"sampleIndex":4,"initSyncElapsedNs":"1212630","elapsedNs":"1822149","requestBytes":28866,"requestSha256":"495db8cf72c85e1811f87180cefb4a90c8d64be1277ad4ed41562a2e97034979","outcomeBytes":4823,"outcomeSha256":"a6196a5aaec38b74029c0d6f5016b9249008d7cd9544c3d5cf5de9932770e090","summary":{"outcome":"success","terminal":"feasible","domainCount":"256","canonicalRelations":"1","applicableRelations":"1","notApplicableRelations":"0","applicableEdges":"1","logicalAssessments":"256","failureMatrixBytes":32,"partitionBytes":32,"feasibleCandidates":7,"lsb0PartitionMatchesMatrix":true,"proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":50592,"processMaxRssKiBAfterInit":51676,"processMaxRssKiBBefore":59468,"processMaxRssKiBAfter":59468,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":1376256,"wasmMemoryBytesAfter":1376256,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":21,"wasmMemoryPagesAfter":21}]},{"scenarioId":"transport-limit-plus-one","shape":{"rawRelations":2047,"rawAdjacentEntries":2047,"opaqueUtf8Bytes":65536,"canonicalRelations":2047,"applicableRelations":2047,"applicableEdges":2047},"samples":[{"sampleIndex":0,"initSyncElapsedNs":"1172209","elapsedNs":"41082","requestBytes":657381,"requestSha256":"8569362aca144e7f9b4f83af9f9b91ef6287768b91afef3a2feb4d9d2e27606d","outcomeBytes":154,"outcomeSha256":"1a074b69790ebf713dcd65a76ddc630a75ce996ae01404a552583a2211b5e125","summary":{"outcome":"failure","source":"transport","code":"envelopeTooLarge","requestedBytes":"657381","limitBytes":"657380","proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":50912,"processMaxRssKiBAfterInit":51996,"processMaxRssKiBBefore":91884,"processMaxRssKiBAfter":91884,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":1179648,"wasmMemoryBytesAfter":1179648,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":18,"wasmMemoryPagesAfter":18},{"sampleIndex":1,"initSyncElapsedNs":"993519","elapsedNs":"53339","requestBytes":657381,"requestSha256":"8569362aca144e7f9b4f83af9f9b91ef6287768b91afef3a2feb4d9d2e27606d","outcomeBytes":154,"outcomeSha256":"1a074b69790ebf713dcd65a76ddc630a75ce996ae01404a552583a2211b5e125","summary":{"outcome":"failure","source":"transport","code":"envelopeTooLarge","requestedBytes":"657381","limitBytes":"657380","proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":50692,"processMaxRssKiBAfterInit":51904,"processMaxRssKiBBefore":92052,"processMaxRssKiBAfter":92052,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":1179648,"wasmMemoryBytesAfter":1179648,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":18,"wasmMemoryPagesAfter":18},{"sampleIndex":2,"initSyncElapsedNs":"1088391","elapsedNs":"60380","requestBytes":657381,"requestSha256":"8569362aca144e7f9b4f83af9f9b91ef6287768b91afef3a2feb4d9d2e27606d","outcomeBytes":154,"outcomeSha256":"1a074b69790ebf713dcd65a76ddc630a75ce996ae01404a552583a2211b5e125","summary":{"outcome":"failure","source":"transport","code":"envelopeTooLarge","requestedBytes":"657381","limitBytes":"657380","proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":50696,"processMaxRssKiBAfterInit":51652,"processMaxRssKiBBefore":95728,"processMaxRssKiBAfter":95728,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":1179648,"wasmMemoryBytesAfter":1179648,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":18,"wasmMemoryPagesAfter":18},{"sampleIndex":3,"initSyncElapsedNs":"1064647","elapsedNs":"42765","requestBytes":657381,"requestSha256":"8569362aca144e7f9b4f83af9f9b91ef6287768b91afef3a2feb4d9d2e27606d","outcomeBytes":154,"outcomeSha256":"1a074b69790ebf713dcd65a76ddc630a75ce996ae01404a552583a2211b5e125","summary":{"outcome":"failure","source":"transport","code":"envelopeTooLarge","requestedBytes":"657381","limitBytes":"657380","proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":50676,"processMaxRssKiBAfterInit":51888,"processMaxRssKiBBefore":91996,"processMaxRssKiBAfter":91996,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":1179648,"wasmMemoryBytesAfter":1179648,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":18,"wasmMemoryPagesAfter":18},{"sampleIndex":4,"initSyncElapsedNs":"1201633","elapsedNs":"205840","requestBytes":657381,"requestSha256":"8569362aca144e7f9b4f83af9f9b91ef6287768b91afef3a2feb4d9d2e27606d","outcomeBytes":154,"outcomeSha256":"1a074b69790ebf713dcd65a76ddc630a75ce996ae01404a552583a2211b5e125","summary":{"outcome":"failure","source":"transport","code":"envelopeTooLarge","requestedBytes":"657381","limitBytes":"657380","proportionalFieldsPresent":false},"processMaxRssKiBBeforeInit":52712,"processMaxRssKiBAfterInit":53920,"processMaxRssKiBBefore":92744,"processMaxRssKiBAfter":92744,"wasmMemoryBytesAfterInit":1114112,"wasmMemoryBytesBefore":1179648,"wasmMemoryBytesAfter":1179648,"wasmMemoryPagesAfterInit":17,"wasmMemoryPagesBefore":18,"wasmMemoryPagesAfter":18}]}]} diff --git a/packages/colors/compiler.d.ts b/packages/colors/compiler.d.ts new file mode 100644 index 00000000..7280b5ff --- /dev/null +++ b/packages/colors/compiler.d.ts @@ -0,0 +1,22 @@ +/// + +import type { Wcag22FeasibilityOutcomeV1 } from "./compiler/labcolors_compiler.js"; + +export { + default, + default as init, + initSync, +} from "./compiler/labcolors_compiler.js"; + +/** Exact derived V1 request ceiling, available after compiler WASM initialization. */ +export declare function wcag22FeasibilityMaxBytes(): number; + +/** Evaluate one strict V1 UTF-8 JSON byte envelope; protocol failures are data. */ +export declare function evaluateWcag22Feasibility( + request: Uint8Array, +): Wcag22FeasibilityOutcomeV1; + +export type { + Wcag22FeasibilityOutcomeV1, + Wcag22FeasibilityRequestV1, +} from "./compiler/labcolors_compiler.js"; diff --git a/packages/colors/compiler.js b/packages/colors/compiler.js new file mode 100644 index 00000000..03d7cc62 --- /dev/null +++ b/packages/colors/compiler.js @@ -0,0 +1,71 @@ +// Offline compiler entry for @labpics/colors/compiler. + +import { + evaluateWcag22FeasibilityV1 as evaluateWcag22FeasibilityRawV1, + wcag22FeasibilityEnvelopeTooLargeV1, + wcag22FeasibilityMaxRequestBytesV1, +} from "./compiler/labcolors_compiler.js"; + +const typedArrayPrototype = Object.getPrototypeOf(Uint8Array.prototype); +const typedArrayTag = Object.getOwnPropertyDescriptor( + typedArrayPrototype, + Symbol.toStringTag, +).get; +const typedArrayByteLength = Object.getOwnPropertyDescriptor( + typedArrayPrototype, + "byteLength", +).get; +const typedArrayBuffer = Object.getOwnPropertyDescriptor( + typedArrayPrototype, + "buffer", +).get; +const typedArrayByteOffset = Object.getOwnPropertyDescriptor( + typedArrayPrototype, + "byteOffset", +).get; +const Uint8ArrayConstructor = Uint8Array; + +function hasUint8ArrayBrand(value) { + return ArrayBuffer.isView(value) && typedArrayTag.call(value) === "Uint8Array"; +} + +export { + default, + default as init, + initSync, +} from "./compiler/labcolors_compiler.js"; + +/** Exact derived V1 request ceiling, available after compiler WASM initialization. */ +export function wcag22FeasibilityMaxBytes() { + return wcag22FeasibilityMaxRequestBytesV1(); +} + +/** + * Evaluate one strict V1 UTF-8 JSON envelope. + * + * The host rejects the wrong input type and oversized views before wasm-bindgen + * can copy them. Rust repeats the authoritative envelope check. + * + * @param {Uint8Array} request + */ +export function evaluateWcag22Feasibility(request) { + if (!hasUint8ArrayBrand(request)) { + throw new TypeError("evaluateWcag22Feasibility request must be a Uint8Array"); + } + const snapshotBytes = typedArrayByteLength.call(request); + let canonicalRequest; + try { + canonicalRequest = new Uint8ArrayConstructor( + typedArrayBuffer.call(request), + typedArrayByteOffset.call(request), + snapshotBytes, + ); + } catch { + throw new TypeError("evaluateWcag22Feasibility request must be a live Uint8Array"); + } + const requestedBytes = typedArrayByteLength.call(canonicalRequest); + if (requestedBytes > wcag22FeasibilityMaxBytes()) { + return wcag22FeasibilityEnvelopeTooLargeV1(BigInt(requestedBytes)); + } + return evaluateWcag22FeasibilityRawV1(canonicalRequest); +} diff --git a/packages/colors/index.d.ts b/packages/colors/index.d.ts index ca88d945..105bc130 100644 --- a/packages/colors/index.d.ts +++ b/packages/colors/index.d.ts @@ -6,11 +6,8 @@ // `RoleResult` union and the `LabColors` engine) and the vanilla `applyTheme` // helper, so a consumer gets full typing from the package root. -import type { - Wcag22AssessmentV1, - Wcag22CriterionV1, - Wcag22FeasibilityOutcomeV1, -} from "./pkg/labcolors.js"; +import type { Wcag22AssessmentV1 } from "./pkg/labcolors.js"; +import type { Wcag22CriterionV1 } from "./wcag22.js"; export { default, @@ -27,14 +24,6 @@ export declare function evaluateWcag22( criterion: Wcag22CriterionV1, ): Wcag22AssessmentV1; -/** Exact derived V1 request ceiling, available after WASM initialization. */ -export declare function wcag22FeasibilityMaxBytes(): number; - -/** Evaluate one strict V1 UTF-8 JSON byte envelope; protocol failures are data. */ -export declare function evaluateWcag22Feasibility( - request: Uint8Array, -): Wcag22FeasibilityOutcomeV1; - // Curated public schema/result surface. wasm-bindgen's InitOutput and raw // __wbg_* ABI helpers remain implementation details. export type { @@ -78,13 +67,11 @@ export type { ResolvedTheme, NumericalCapabilitySiteV2, NumericalCapabilityManifestV2, - Wcag22CriterionV1, Wcag22DecisionV1, Wcag22Q55BoundsV1, Wcag22AssessmentV1, - Wcag22FeasibilityRequestV1, - Wcag22FeasibilityOutcomeV1, } from "./pkg/labcolors.js"; +export type { Wcag22CriterionV1 } from "./wcag22.js"; export { applyTheme } from "./apply-theme.js"; export { watchTheme } from "./watch-theme.js"; diff --git a/packages/colors/index.js b/packages/colors/index.js index 6f936ae1..8450127e 100644 --- a/packages/colors/index.js +++ b/packages/colors/index.js @@ -3,23 +3,8 @@ // Re-exports the wasm-bindgen surface (the default `init` loader, `initSync`, // and the `LabColors` engine class) plus the vanilla DOM runtime helpers: // `applyTheme` (one-shot apply), `watchTheme` (reactive sync), and the -// effective-background resolver. The wasm glue is the generated `pkg/` artifact -// (built by `npm run build`). - -import { - evaluateWcag22FeasibilityV1 as evaluateWcag22FeasibilityRawV1, - wcag22FeasibilityEnvelopeTooLargeV1, - wcag22FeasibilityMaxRequestBytesV1, -} from "./pkg/labcolors.js"; - -const typedArrayTag = Object.getOwnPropertyDescriptor( - Object.getPrototypeOf(Uint8Array.prototype), - Symbol.toStringTag, -).get; - -function hasUint8ArrayBrand(value) { - return ArrayBuffer.isView(value) && typedArrayTag.call(value) === "Uint8Array"; -} +// effective-background resolver. Offline compiler operations live exclusively +// at `@labpics/colors/compiler` and have a separate WASM artifact. export { default, @@ -30,33 +15,6 @@ export { numericalCapabilityManifest, } from "./pkg/labcolors.js"; -/** Exact derived V1 request ceiling, available after WASM initialization. */ -export function wcag22FeasibilityMaxBytes() { - return wcag22FeasibilityMaxRequestBytesV1(); -} - -/** - * Evaluate one strict V1 UTF-8 JSON envelope. - * - * The host checks the typed array's byte length before wasm-bindgen performs - * its avoidable input copy. Rust repeats the authoritative check. For the - * declared Uint8Array input, envelope, resource and Core failures are returned - * as typed outcome data. Any other JavaScript value throws a deterministic - * TypeError before the host reads the WASM-owned ceiling or copies input. - * - * @param {Uint8Array} request - */ -export function evaluateWcag22Feasibility(request) { - if (!hasUint8ArrayBrand(request)) { - throw new TypeError("evaluateWcag22Feasibility request must be a Uint8Array"); - } - const requestedBytes = request.byteLength; - if (requestedBytes > wcag22FeasibilityMaxBytes()) { - return wcag22FeasibilityEnvelopeTooLargeV1(BigInt(requestedBytes)); - } - return evaluateWcag22FeasibilityRawV1(request); -} - export { applyTheme } from "./apply-theme.js"; export { watchTheme } from "./watch-theme.js"; export { adaptTheme } from "./adapt-theme.js"; diff --git a/packages/colors/package.json b/packages/colors/package.json index 4937a01c..8ed7bc6c 100644 --- a/packages/colors/package.json +++ b/packages/colors/package.json @@ -19,6 +19,11 @@ "types": "./index.d.ts", "default": "./index.js" }, + "./compiler": { + "types": "./compiler.d.ts", + "default": "./compiler.js" + }, + "./compiler/wasm": "./compiler/labcolors_compiler_bg.wasm", "./apply-theme": { "types": "./apply-theme.d.ts", "default": "./apply-theme.js" @@ -44,6 +49,9 @@ "build-metadata.json", "index.js", "index.d.ts", + "compiler.js", + "compiler.d.ts", + "wcag22.d.ts", "apply-theme.js", "apply-theme.d.ts", "watch-theme.js", @@ -58,11 +66,15 @@ "pkg/labcolors.js", "pkg/labcolors.d.ts", "pkg/labcolors_bg.wasm", - "pkg/labcolors_bg.wasm.d.ts" + "pkg/labcolors_bg.wasm.d.ts", + "compiler/labcolors_compiler.js", + "compiler/labcolors_compiler.d.ts", + "compiler/labcolors_compiler_bg.wasm", + "compiler/labcolors_compiler_bg.wasm.d.ts" ], "scripts": { - "//build": "wasm-pack resolves --out-dir relative to the CRATE dir (crates/labcolors-wasm), not the cwd, so the ../../packages path is correct and unambiguous regardless of where npm runs it.", - "build": "wasm-pack build ../../crates/labcolors-wasm --release --target web --out-dir ../../packages/colors/pkg --out-name labcolors --locked", + "//build": "Each execution role has its own Cargo root and output directory; wasm-pack resolves --out-dir relative to that crate.", + "build": "wasm-pack build ../../crates/labcolors-wasm --release --target web --out-dir ../../packages/colors/pkg --out-name labcolors --locked && wasm-pack build ../../crates/labcolors-compiler --release --target web --out-dir ../../packages/colors/compiler --out-name labcolors_compiler --locked", "typecheck": "tsc --noEmit -p tsconfig.json", "test": "node --test", "prepack": "npm run build && node ../../scripts/prepare-npm-package.mjs", diff --git a/packages/colors/test/compiler-boundary.test.mjs b/packages/colors/test/compiler-boundary.test.mjs new file mode 100644 index 00000000..f5bd0972 --- /dev/null +++ b/packages/colors/test/compiler-boundary.test.mjs @@ -0,0 +1,74 @@ +import assert from "node:assert/strict"; +import { existsSync, readFileSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), "../../.."); +const read = (...parts) => readFileSync(join(root, ...parts), "utf8"); + +test("npm exposes one compiler subpath without a package-root alias", () => { + const packageJson = JSON.parse(read("packages", "colors", "package.json")); + + assert.deepEqual(packageJson.exports["./compiler"], { + types: "./compiler.d.ts", + default: "./compiler.js", + }); + assert.equal( + packageJson.exports["./compiler/wasm"], + "./compiler/labcolors_compiler_bg.wasm", + ); + for (const artifact of [ + "compiler.js", + "compiler.d.ts", + "compiler/labcolors_compiler.js", + "compiler/labcolors_compiler.d.ts", + "compiler/labcolors_compiler_bg.wasm", + "compiler/labcolors_compiler_bg.wasm.d.ts", + ]) { + assert.ok(packageJson.files.includes(artifact), `npm files omits ${artifact}`); + } +}); + +test("package root cannot resolve or name the offline compiler surface", () => { + const rootJavaScript = read("packages", "colors", "index.js"); + const rootDeclarations = read("packages", "colors", "index.d.ts"); + + for (const source of [rootJavaScript, rootDeclarations]) { + assert.doesNotMatch(source, /Feasibility|feasibility|labcolors_compiler/u); + } + assert.doesNotMatch(rootJavaScript, /\.\/compiler(?:\.js|\/)/u); + assert.doesNotMatch(rootDeclarations, /\.\/compiler(?:\.js|\/)/u); +}); + +test("runtime and compiler have disjoint normal dependency graphs", () => { + const compilerManifestPath = join( + root, + "crates", + "labcolors-compiler", + "Cargo.toml", + ); + assert.ok(existsSync(compilerManifestPath), "thin compiler WASM crate is missing"); + + const runtimeManifest = read("crates", "labcolors-wasm", "Cargo.toml"); + const compilerManifest = read("crates", "labcolors-compiler", "Cargo.toml"); + assert.doesNotMatch(runtimeManifest, /labcolors-protocol/u); + assert.match( + compilerManifest, + /labcolors-protocol = \{ path = "\.\.\/labcolors-protocol" \}/u, + ); + assert.doesNotMatch(compilerManifest, /labcolors-wasm|labcolors-core/u); +}); + +test("the package build invokes wasm-pack once per physical role", () => { + const packageJson = JSON.parse(read("packages", "colors", "package.json")); + const build = packageJson.scripts.build; + const invocations = build.match(/wasm-pack build/gu) ?? []; + + assert.equal(invocations.length, 2); + assert.match(build, /crates\/labcolors-wasm[\s\S]*--out-dir \.\.\/\.\.\/packages\/colors\/pkg/u); + assert.match( + build, + /crates\/labcolors-compiler[\s\S]*--out-dir \.\.\/\.\.\/packages\/colors\/compiler/u, + ); +}); diff --git a/packages/colors/test/release-contract.test.mjs b/packages/colors/test/release-contract.test.mjs index 93d43cb0..f7cd89c9 100644 --- a/packages/colors/test/release-contract.test.mjs +++ b/packages/colors/test/release-contract.test.mjs @@ -121,12 +121,13 @@ test("every workspace package inherits the declared MSRV", () => { } }); -test("WCAG22 feasibility projects only the registered-domain capability through transports", () => { +test("runtime and compiler resolve disjoint Core capability graphs", () => { const isolatedCoreEdge = /labcolors-core = \{ path = "\.\.\/labcolors-core", default-features = false \}/u; const protocolEdge = /labcolors-protocol = \{ path = "\.\.\/labcolors-protocol" \}/u; const protocolManifest = read("crates", "labcolors-protocol", "Cargo.toml"); const wasmManifest = read("crates", "labcolors-wasm", "Cargo.toml"); + const compilerManifest = read("crates", "labcolors-compiler", "Cargo.toml"); const ffiManifest = read("crates", "labcolors-ffi", "Cargo.toml"); const conformanceManifest = read("crates", "labcolors-conformance", "Cargo.toml"); @@ -134,7 +135,11 @@ test("WCAG22 feasibility projects only the registered-domain capability through protocolManifest, /labcolors-core = \{ path = "\.\.\/labcolors-core", default-features = false, features = \["wcag22-feasibility"\] \}/u, ); - for (const manifest of [wasmManifest, ffiManifest, conformanceManifest]) { + assert.match(wasmManifest, isolatedCoreEdge); + assert.doesNotMatch(wasmManifest, /labcolors-protocol/u); + assert.match(compilerManifest, protocolEdge); + assert.doesNotMatch(compilerManifest, /labcolors-core|labcolors-wasm/u); + for (const manifest of [ffiManifest, conformanceManifest]) { assert.match(manifest, isolatedCoreEdge); assert.match(manifest, protocolEdge); assert.doesNotMatch(manifest, /features = \["wcag22-feasibility"\]/u); @@ -145,18 +150,29 @@ test("WCAG22 feasibility projects only the registered-domain capability through ci, "name: prove core capability projection boundary", ); - const declaredConsumers = projection.match( - /consumers = \(\n(?(?: "[^"]+",\n)+)\)/u, + const declaredDirectCore = projection.match( + /direct_core_consumers = \(\n(?(?: "[^"]+",\n)+)\)/u, )?.groups?.items; - assert.ok(declaredConsumers, "CI must declare one consumer SSOT"); + const declaredProtocol = projection.match( + /protocol_consumers = \(\n(?(?: "[^"]+",\n)+)\)/u, + )?.groups?.items; + assert.ok(declaredDirectCore, "CI must declare one direct-Core consumer SSOT"); + assert.ok(declaredProtocol, "CI must declare one Protocol consumer SSOT"); assert.deepEqual( - [...declaredConsumers.matchAll(/"([^"]+)"/gu)].map((match) => match[1]), + [...declaredDirectCore.matchAll(/"([^"]+)"/gu)].map((match) => match[1]), ["labcolors-wasm", "labcolors-ffi", "labcolors-conformance"], ); + assert.deepEqual( + [...declaredProtocol.matchAll(/"([^"]+)"/gu)].map((match) => match[1]), + ["labcolors-compiler", "labcolors-ffi", "labcolors-conformance"], + ); + assert.equal( + projection.match(/for consumer in direct_core_consumers:/gu)?.length, + 1, + ); assert.equal( - projection.match(/for consumer in consumers:/gu)?.length, + projection.match(/for consumer in protocol_consumers:/gu)?.length, 1, - "the dependency and feature-tree checks must share one consumer loop", ); assert.match( projection, @@ -167,7 +183,11 @@ test("WCAG22 feasibility projects only the registered-domain capability through assert.match(projection, /dependency\["name"\] == "labcolors-protocol"/u); assert.match( projection, - /\["cargo", "tree", "-p", consumer, "--edges", "normal", "-e", "features"\]/u, + /"cargo", "tree", "-p", "labcolors-wasm",[\s\S]*?"--target", "wasm32-unknown-unknown"/u, + ); + assert.match( + projection, + /"cargo", "tree", "-p", "labcolors-compiler",[\s\S]*?"--target", "wasm32-unknown-unknown"/u, ); assert.match( projection, @@ -204,7 +224,7 @@ test("MSRV and packaged Rust crate gates are executable CI contracts", () => { assert.match(ci, /^\s*NODE_CONSUMER_FLOOR: 22\.11\.0$/m); assert.match( ci, - /^\s*node-consumer-floor:[\s\S]*needs: wasm[\s\S]*node-version: \$\{\{ env\.NODE_CONSUMER_FLOOR \}\}[\s\S]*actions\/download-artifact@[0-9a-f]{40}[\s\S]*--runtime-smoke/m, + /^\s*node-consumer-floor:[\s\S]*needs: wasm[\s\S]*node-version: \$\{\{ env\.NODE_CONSUMER_FLOOR \}\}[\s\S]*actions\/download-artifact@[0-9a-f]{40}[\s\S]*--package-smoke/m, ); assert.match(ci, /^\s*CHROME_FOR_TESTING_VERSION: 150\.0\.7871\.115$/m); assert.match( @@ -722,16 +742,58 @@ test("publish artifact validator executes and rejects identity or byte drift", ( join(payload, "package.json"), `${JSON.stringify({ name: "@labpics/colors", version: "0.10.0" })}\n`, ); + const runtimeWasm = Buffer.from([0, 97, 115, 109, 1, 0, 0, 0]); + const compilerWasm = Buffer.from([0, 97, 115, 109, 1, 0, 0, 1]); + mkdirSync(join(payload, "pkg")); + mkdirSync(join(payload, "compiler")); + writeFileSync(join(payload, "pkg", "labcolors_bg.wasm"), runtimeWasm); + writeFileSync( + join(payload, "compiler", "labcolors_compiler_bg.wasm"), + compilerWasm, + ); + + const expectedSha = "a".repeat(40); + const conformance = { + packVersion: "5.0.0", + packDigest: "12345678", + manifestSha256: "c".repeat(64), + familySetSha256: "d".repeat(64), + }; + const wasmEvidence = [ + { + role: "runtime", + path: "pkg/labcolors_bg.wasm", + bytes: runtimeWasm.length, + sha256: createHash("sha256").update(runtimeWasm).digest("hex"), + }, + { + role: "compiler", + path: "compiler/labcolors_compiler_bg.wasm", + bytes: compilerWasm.length, + sha256: createHash("sha256").update(compilerWasm).digest("hex"), + }, + ]; + const buildMetadata = { + schemaVersion: 2, + package: { name: "@labpics/colors", version: "0.10.0" }, + sourceSha: expectedSha, + coreVersion: "0.2.0", + conformance, + wasm: wasmEvidence, + }; + const metadataPath = join(payload, "build-metadata.json"); + const metadataBytes = Buffer.from(`${JSON.stringify(buildMetadata)}\n`); + writeFileSync(metadataPath, metadataBytes); const tarball = join(artifact, "labpics-colors-0.10.0.tgz"); execFileSync("tar", ["-czf", tarball, "-C", join(temporary, "payload"), "package"]); const bytes = readFileSync(tarball); - const expectedSha = "a".repeat(40); + const manifest = { - // Release-manifest schema v2: numericalCapabilities вместо numericalSites - // (см. verify-package-release.mjs); validator publish-workflow пиняет 2. - schemaVersion: 2, + schemaVersion: 3, npm: "0.10.0", + core: "0.2.0", + conformance, sourceSha: expectedSha, artifacts: { tarball: { @@ -739,6 +801,12 @@ test("publish artifact validator executes and rejects identity or byte drift", ( bytes: bytes.length, sha256: createHash("sha256").update(bytes).digest("hex"), }, + wasm: structuredClone(wasmEvidence), + buildMetadata: { + path: "build-metadata.json", + bytes: metadataBytes.length, + sha256: createHash("sha256").update(metadataBytes).digest("hex"), + }, }, }; const manifestPath = join(artifact, "release-manifest.json"); @@ -775,6 +843,38 @@ test("publish artifact validator executes and rejects identity or byte drift", ( manifest.artifacts.tarball.sha256 = "0".repeat(64); writeFileSync(manifestPath, `${JSON.stringify(manifest)}\n`); assert.throws(execute, /Command failed/u); + + manifest.artifacts.tarball.sha256 = createHash("sha256").update(bytes).digest("hex"); + manifest.artifacts.wasm[1].bytes += 1; + writeFileSync(manifestPath, `${JSON.stringify(manifest)}\n`); + assert.throws(execute, /Command failed/u); + + manifest.artifacts.wasm[1].bytes -= 1; + manifest.artifacts.wasm.reverse(); + writeFileSync(manifestPath, `${JSON.stringify(manifest)}\n`); + assert.throws(execute, /Command failed/u); + + manifest.artifacts.wasm.reverse(); + manifest.artifacts.buildMetadata.sha256 = "0".repeat(64); + writeFileSync(manifestPath, `${JSON.stringify(manifest)}\n`); + assert.throws(execute, /Command failed/u); + + const tamperedMetadataBytes = Buffer.from( + `${JSON.stringify({ ...buildMetadata, sourceSha: "b".repeat(40) })}\n`, + ); + writeFileSync(metadataPath, tamperedMetadataBytes); + execFileSync("tar", ["-czf", tarball, "-C", join(temporary, "payload"), "package"]); + const tamperedTarball = readFileSync(tarball); + manifest.artifacts.tarball.bytes = tamperedTarball.length; + manifest.artifacts.tarball.sha256 = createHash("sha256") + .update(tamperedTarball) + .digest("hex"); + manifest.artifacts.buildMetadata.bytes = tamperedMetadataBytes.length; + manifest.artifacts.buildMetadata.sha256 = createHash("sha256") + .update(tamperedMetadataBytes) + .digest("hex"); + writeFileSync(manifestPath, `${JSON.stringify(manifest)}\n`); + assert.throws(execute, /Command failed/u); } finally { rmSync(temporary, { recursive: true, force: true }); } @@ -966,6 +1066,7 @@ test("release evidence carries the versioned WCAG22 feasibility operation", () = ); assert.match(verifier, /evaluateWcag22Feasibility/u); assert.match(verifier, /wcag22FeasibilityMaxBytes/u); + assert.match(verifier, /from "@labpics\/colors\/compiler"/u); assert.match(verifier, /type Wcag22FeasibilityRequestV1/u); assert.match(verifier, /type Wcag22FeasibilityOutcomeV1/u); assert.match(verifier, /get\("text-default-seven"\)\?\.vector/u); @@ -974,168 +1075,121 @@ test("release evidence carries the versioned WCAG22 feasibility operation", () = /JSON\.stringify\(evaluateWcag22Feasibility\(feasibilityRequest\)\)[\s\S]*?feasibilityFixture\.outcomeJson/u, ); assert.equal( - verifier.match(/writeFile\(runtimePath, runtimeSmokeSource\(feasibilityFixture\)\)/gu)?.length, - 2, - "clean-install and Node-floor smokes must execute the same canonical fixture", + verifier.match(/compilerSmokeSource\(feasibilityFixture\)/gu)?.length, + 4, + "clean-install, role-isolation and Node-floor smokes must execute the same canonical fixture", + ); + const compilerSmoke = verifier.slice( + verifier.indexOf("function compilerSmokeSource"), + verifier.indexOf("function typeSmokeSource"), ); assert.ok( - verifier.indexOf("await init({ module_or_path:") < - verifier.indexOf("wcag22FeasibilityMaxBytes()"), - "clean smoke must import safely and call the getter only after WASM init", + compilerSmoke.indexOf("await init({ module_or_path:") < + compilerSmoke.indexOf("wcag22FeasibilityMaxBytes()"), + "compiler smoke must import safely and call the getter only after its WASM init", ); + assert.match(verifier, /verifyPackedRoleIsolation/u); + assert.match(verifier, /await rm\(resolve\(installed, "compiler"\)/u); + assert.match(verifier, /await rm\(resolve\(installed, "pkg"\)/u); assert.match(verifier, /@ts-expect-error byte API rejects strings/u); assert.match(verifier, /case "notEvaluated"/u); assert.match(verifier, /case "incompatibleCoreContract"/u); }); -test("WCAG22 WASM budget history is exact, append-only, and acyclic", async () => { - const v1Path = join(root, "packages", "colors", "bench", "wasm-size-budget-v1.json"); - const v2Path = join(root, "packages", "colors", "bench", "wasm-size-budget-v2.json"); - const v3Path = join(root, "packages", "colors", "bench", "wasm-size-budget-v3.json"); - const v4Path = join(root, "packages", "colors", "bench", "wasm-size-budget-v4.json"); +test("WCAG22 WASM role budgets are exact, append-only, and acyclic", async () => { + const bench = join(root, "packages", "colors", "bench"); + const paths = Object.fromEntries( + [1, 2, 3, 4, 5].map((version) => [ + `v${version}`, + join(bench, `wasm-size-budget-v${version}.json`), + ]), + ); const checkerPath = join(root, "scripts", "check-wasm-size-budget.mjs"); const sha256 = (value) => createHash("sha256").update(value).digest("hex"); const canonicalJson = (value) => `${JSON.stringify(value, null, 2)}\n`; - - const v1Bytes = readFileSync(v1Path); - const v1 = JSON.parse(v1Bytes); - assert.equal( - sha256(v1Bytes), - "4f7340fc8cfd0ccb97377c385f2f8d8e7a9ef2c5ba96177f518c5d07de2825e1", - "the immutable #284 evidence and build recipe must remain byte-identical", - ); - const recipe = { - rustToolchain: v1.measurement.rustToolchain, - rustcCommit: v1.measurement.rustcCommit, - wasmPack: v1.measurement.wasmPack, - wasmBindgen: v1.measurement.wasmBindgen, - target: v1.measurement.target, - cargoProfile: v1.measurement.cargoProfile, - wasmOpt: v1.measurement.wasmOpt, - wasmOptVersion: v1.measurement.wasmOptVersion, - measurementPlatform: v1.measurement.measurementPlatform, - rustPathRemap: v1.measurement.rustPathRemap, - command: v1.measurement.command, + const expectedHashes = { + v1: "4f7340fc8cfd0ccb97377c385f2f8d8e7a9ef2c5ba96177f518c5d07de2825e1", + v2: "713ccc314b3e6f638d87a54716d665d52f77c86f34a2b6edefe0a354a499d8b1", + v3: "d7937612e4c33574a8af28845bb1dd30cca86fc39fc0206cac4c377de77fec15", + v4: "c34fc10404dc7057a53a28592d18342078b5cd0e5dcaa888db482abf3f5fb23c", + v5: "e4b53a2eb976a8c66827a559cb81232e359b734dbfb14725da215cb496ff5d59", }; - assert.equal( - sha256(JSON.stringify(recipe)), - "0ea74cb070e0a5facb7280f6124930a0bb673ee4dcee9c99fff110db6c9389d4", - ); - assert.deepEqual(v1.measurement.rustPathRemap, [ - "GITHUB_WORKSPACE=/workspace/lab-colors", - "CARGO_HOME=/cargo-home", - ]); + const documents = {}; + for (const version of Object.keys(paths)) { + const bytes = readFileSync(paths[version]); + const value = JSON.parse(bytes); + documents[version] = value; + assert.equal(sha256(bytes), expectedHashes[version], `${version} byte identity drifted`); + if (version !== "v1") assert.equal(bytes.toString("utf8"), canonicalJson(value)); + } - const v2Bytes = readFileSync(v2Path); - const v2 = JSON.parse(v2Bytes); - assert.equal(v2Bytes.toString("utf8"), canonicalJson(v2)); - assert.equal( - sha256(v2Bytes), - "713ccc314b3e6f638d87a54716d665d52f77c86f34a2b6edefe0a354a499d8b1", - "the admitted v2 document must be byte-immutable", - ); - assert.deepEqual(Object.keys(v2), [ + const { v1, v2, v3, v4, v5 } = documents; + assert.equal(v1.budgetId, "labcolors-wasm-raw-issue-284-v1"); + assert.equal(v2.budgetId, "labcolors-wasm-raw-issue-295-v2"); + assert.equal(v3.budgetId, "labcolors-wasm-raw-issue-296-v3"); + assert.equal(v4.budgetId, "labcolors-wasm-raw-issue-296-v4"); + assert.deepEqual(Object.keys(v5), [ "schemaVersion", "budgetId", - "artifact", - "buildRecipe", - "measurement", - "policy", + "predecessor", + "toolchainSource", + "buildRecipes", + "roles", ]); - assert.deepEqual(Object.keys(v2.buildRecipe), ["path", "fileSha256", "recipeSha256"]); - assert.deepEqual(Object.keys(v2.measurement), [ - "issue", - "measurementPlatform", - "rawBytes", - "sha256", - ]); - assert.deepEqual(Object.keys(v2.policy), ["maxRawBytes", "derivation", "gzip"]); - assert.equal(v2.schemaVersion, 3); - assert.equal(v2.budgetId, "labcolors-wasm-raw-issue-295-v2"); - assert.equal(v2.artifact, "packages/colors/pkg/labcolors_bg.wasm"); - assert.deepEqual(v2.buildRecipe, { - path: "packages/colors/bench/wasm-size-budget-v1.json", - fileSha256: "4f7340fc8cfd0ccb97377c385f2f8d8e7a9ef2c5ba96177f518c5d07de2825e1", - recipeSha256: "0ea74cb070e0a5facb7280f6124930a0bb673ee4dcee9c99fff110db6c9389d4", - }); - assert.deepEqual(v2.measurement, { - issue: 295, - measurementPlatform: "linux-x64", - rawBytes: 521240, - sha256: "d37841bfb2615d05c8366b08dcc7e5aed1bbd3cf27c3db67896108c5ec9c9ca0", + assert.equal(v5.schemaVersion, 4); + assert.equal(v5.budgetId, "labcolors-wasm-roles-issue-296-c1-v5"); + assert.deepEqual(v5.predecessor, { + path: "packages/colors/bench/wasm-size-budget-v4.json", + fileSha256: expectedHashes.v4, }); - assert.deepEqual(v2.policy, { - maxRawBytes: 521240, - derivation: "exact-accepted-issue-295-slice-b-measurement", - gzip: "diagnostic-only", + assert.deepEqual(v5.toolchainSource, { + path: "packages/colors/bench/wasm-size-budget-v1.json", + fileSha256: expectedHashes.v1, }); - - const v3Bytes = readFileSync(v3Path); - const v3 = JSON.parse(v3Bytes); - assert.equal(v3Bytes.toString("utf8"), canonicalJson(v3)); + assert.deepEqual(Object.keys(v5.buildRecipes), ["runtime", "compiler"]); + assert.deepEqual(Object.keys(v5.roles), ["runtime", "compiler"]); assert.equal( - sha256(v3Bytes), - "d7937612e4c33574a8af28845bb1dd30cca86fc39fc0206cac4c377de77fec15", - "the admitted v3 document must be byte-immutable", - ); - assert.deepEqual(Object.keys(v3), Object.keys(v2)); - assert.deepEqual(Object.keys(v3.buildRecipe), Object.keys(v2.buildRecipe)); - assert.deepEqual(Object.keys(v3.measurement), Object.keys(v2.measurement)); - assert.deepEqual(Object.keys(v3.policy), Object.keys(v2.policy)); - assert.equal(v3.schemaVersion, 3); - assert.equal(v3.budgetId, "labcolors-wasm-raw-issue-296-v3"); - assert.equal(v3.artifact, "packages/colors/pkg/labcolors_bg.wasm"); - assert.deepEqual(v3.buildRecipe, v2.buildRecipe); - assert.deepEqual(v3.measurement, { + v5.buildRecipes.runtime.recipeSha256, + "0ea74cb070e0a5facb7280f6124930a0bb673ee4dcee9c99fff110db6c9389d4", + ); + assert.equal( + v5.buildRecipes.compiler.recipeSha256, + "ce53cea5f579c512a6d2f0c3348f250ac0a5e03206de55e7979c8eae1403be8f", + ); + assert.match(v5.buildRecipes.runtime.command, /crates\/labcolors-wasm/u); + assert.match(v5.buildRecipes.compiler.command, /crates\/labcolors-compiler/u); + assert.deepEqual(v5.roles.runtime.measurement, { issue: 296, + slice: "C1", measurementPlatform: "linux-x64", - rawBytes: 521231, - sha256: "779379e914909ff1ddbb5afdd6554d026b586f3c71ef6b2cfeba3468bf93e029", - }); - assert.deepEqual(v3.policy, { - maxRawBytes: 521231, - derivation: "exact-accepted-issue-296-slice-a-measurement", - gzip: "diagnostic-only", + rawBytes: 454385, + sha256: "8cd65f001d4bb4b8ddead9084e705a64bee14cd796c7bc6ebeb2f2687aa5fdba", }); - - const v4Bytes = readFileSync(v4Path); - const v4 = JSON.parse(v4Bytes); - assert.equal(v4Bytes.toString("utf8"), canonicalJson(v4)); - assert.equal( - sha256(v4Bytes), - "c34fc10404dc7057a53a28592d18342078b5cd0e5dcaa888db482abf3f5fb23c", - "the admitted v4 document must be byte-immutable", - ); - assert.deepEqual(Object.keys(v4), Object.keys(v3)); - assert.deepEqual(Object.keys(v4.buildRecipe), Object.keys(v3.buildRecipe)); - assert.deepEqual(Object.keys(v4.measurement), Object.keys(v3.measurement)); - assert.deepEqual(Object.keys(v4.policy), Object.keys(v3.policy)); - assert.equal(v4.schemaVersion, 3); - assert.equal(v4.budgetId, "labcolors-wasm-raw-issue-296-v4"); - assert.equal(v4.artifact, "packages/colors/pkg/labcolors_bg.wasm"); - assert.deepEqual(v4.buildRecipe, v3.buildRecipe); - assert.deepEqual(v4.measurement, { + assert.deepEqual(v5.roles.compiler.measurement, { issue: 296, + slice: "C1", measurementPlatform: "linux-x64", - rawBytes: 520920, - sha256: "c179f42cd90c24699167ee78b4080c80fb38247c54953e7dc020483f6fcf94ed", - }); - assert.deepEqual(v4.policy, { - maxRawBytes: 520920, - derivation: "exact-accepted-issue-296-slice-b-measurement", - gzip: "diagnostic-only", + rawBytes: 175212, + sha256: "3a552ce43ada7d0b10e90a23b4a7e50a4ecad77a446374b98ca8ee6b5c6a2a45", }); - assert.ok(v4.policy.maxRawBytes <= v3.policy.maxRawBytes, "the v4 ratchet may only tighten"); + assert.equal(v5.roles.runtime.policy.maxRawBytes, v5.roles.runtime.measurement.rawBytes); + assert.equal(v5.roles.compiler.policy.maxRawBytes, v5.roles.compiler.measurement.rawBytes); + assert.ok(v5.roles.runtime.policy.maxRawBytes <= v1.policy.maxRawBytes); + assert.ok(v5.roles.runtime.policy.maxRawBytes <= v4.policy.maxRawBytes); const checker = await import( new URL("../../../scripts/check-wasm-size-budget.mjs", import.meta.url) ); - assert.equal(checker.DEFAULT_BUDGET, v4Path); - assert.equal(checker.V1_FILE_SHA256, v4.buildRecipe.fileSha256); - assert.equal(checker.V1_RECIPE_SHA256, v4.buildRecipe.recipeSha256); - assert.equal(checker.V2_FILE_SHA256, sha256(v2Bytes)); - assert.equal(checker.V3_FILE_SHA256, sha256(v3Bytes)); - assert.equal(checker.V4_FILE_SHA256, sha256(v4Bytes)); + assert.equal(checker.DEFAULT_BUDGET, paths.v5); + for (const version of [1, 2, 3, 4, 5]) { + assert.equal(checker[`V${version}_FILE_SHA256`], expectedHashes[`v${version}`]); + } + assert.equal(checker.V1_RECIPE_SHA256, v5.buildRecipes.runtime.recipeSha256); + assert.doesNotMatch( + read("scripts", "check-wasm-size-budget.mjs"), + /wcag22-feasibility-wasm-boundary-v1\.json/u, + "whole-call evidence must not become a size-budget dependency", + ); const wholeCallSource = read( "packages", @@ -1143,100 +1197,116 @@ test("WCAG22 WASM budget history is exact, append-only, and acyclic", async () = "bench", "wcag22-feasibility-boundary.bench.mjs", ); - assert.match(wholeCallSource, /wasmToolchainPath = resolve\(here, "wasm-size-budget-v1\.json"\)/u); - assert.doesNotMatch(wholeCallSource, /wasm-size-budget-v[234]\.json/u); - assert.doesNotMatch( - read("scripts", "check-wasm-size-budget.mjs"), - /wcag22-feasibility-wasm-boundary-v1\.json/u, - "the sibling whole-call artifact must not become a size-budget dependency", - ); + assert.match(wholeCallSource, /wasmBudgetPath = resolve\(here, "wasm-size-budget-v5\.json"\)/u); const ci = read(".github", "workflows", "ci.yml"); - assert.match(ci, /name: enforce measured WASM raw-byte budget/u); + assert.match(ci, /name: enforce measured WASM role budgets/u); assert.match(ci, /run: node scripts\/check-wasm-size-budget\.mjs/u); - assert.doesNotMatch(ci, /Not a hard gate yet/u); const wasmJob = ci.match(/\n wasm:\n(?[\s\S]*?)(?=\n [a-z][a-z0-9_-]*:\n)/u)?.groups?.body; assert.ok(wasmJob, "CI must contain a bounded wasm job"); assert.match(wasmJob, /runs-on: \[self-hosted, Linux, X64\]/u); - assert.match(wasmJob, /CARGO_ENCODED_RUSTFLAGS/u); assert.match(wasmJob, /GITHUB_WORKSPACE=\/workspace\/lab-colors/u); assert.match(wasmJob, /CARGO_HOME=\/cargo-home/u); - const builtWasm = readFileSync( - join(root, "packages", "colors", "pkg", "labcolors_bg.wasm"), - ).toString("latin1"); - assert.match(builtWasm, /\/cargo-home\/registry\/src\//u); - assert.doesNotMatch(builtWasm, /\/(?:Users|home)\/[^\0]*?\/\.cargo\/registry\/src\//u); - assert.doesNotMatch( - builtWasm, - /\/opt\/actions-runner\/[^\0]*?\/cargo-wasm\/registry\/src\//u, - ); + for (const [role, path] of [ + ["runtime", join(root, "packages", "colors", "pkg", "labcolors_bg.wasm")], + ["compiler", join(root, "packages", "colors", "compiler", "labcolors_compiler_bg.wasm")], + ]) { + const builtBytes = readFileSync(path); + // The exact V5 digest selects the pinned Linux build; developer builds retain their own host paths. + if (sha256(builtBytes) !== v5.roles[role].measurement.sha256) continue; + const builtWasm = builtBytes.toString("latin1"); + assert.match(builtWasm, /\/cargo-home\/registry\/src\//u); + assert.doesNotMatch(builtWasm, /\/(?:Users|home)\/[^\0]*?\/\.cargo\/registry\/src\//u); + assert.doesNotMatch(builtWasm, /\/opt\/actions-runner\/[^\0]*?\/cargo-wasm\/registry\/src\//u); + } - const temporary = mkdtempSync(join(tmpdir(), "labcolors-wasm-budget-v4-")); + const temporary = mkdtempSync(join(tmpdir(), "labcolors-wasm-role-budget-v5-")); try { - const wasmPath = join(temporary, "fixture.wasm"); + const runtimePath = join(temporary, "runtime.wasm"); + const compilerPath = join(temporary, "compiler.wasm"); const fixtureBudgetPath = join(temporary, "budget.json"); - const bytes = Buffer.alloc(16); - bytes.set([0x00, 0x61, 0x73, 0x6d]); - const fixture = structuredClone(v4); - fixture.measurement.rawBytes = bytes.length; - fixture.measurement.sha256 = sha256(bytes); - fixture.policy.maxRawBytes = bytes.length; - writeFileSync(wasmPath, bytes); + const runtimeBytes = Buffer.alloc(16); + const compilerBytes = Buffer.alloc(17); + runtimeBytes.set([0x00, 0x61, 0x73, 0x6d]); + compilerBytes.set([0x00, 0x61, 0x73, 0x6d]); + const fixture = structuredClone(v5); + for (const [role, bytes] of [["runtime", runtimeBytes], ["compiler", compilerBytes]]) { + fixture.roles[role].measurement.rawBytes = bytes.length; + fixture.roles[role].measurement.sha256 = sha256(bytes); + fixture.roles[role].policy.maxRawBytes = bytes.length; + } + writeFileSync(runtimePath, runtimeBytes); + writeFileSync(compilerPath, compilerBytes); writeFileSync(fixtureBudgetPath, canonicalJson(fixture)); + assert.doesNotThrow(() => + checker.parseBudgetDocument(readFileSync(fixtureBudgetPath), fixtureBudgetPath) + ); const run = () => execFileSync( process.execPath, - [checkerPath, "--wasm", wasmPath, "--budget", fixtureBudgetPath], + [ + checkerPath, + "--budget", + fixtureBudgetPath, + "--runtime-wasm", + runtimePath, + "--compiler-wasm", + compilerPath, + ], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }, ); - assert.match( - run(), - /WASM size budget (?:PASS|DIAGNOSTIC) raw=16B .*artifact-sha=match/u, - ); + const output = run(); + assert.match(output, /role=runtime raw=16B .*artifact-sha=match/u); + assert.match(output, /role=compiler raw=17B .*artifact-sha=match/u); const schemaMutations = [ - ["schema rollback", (value) => { value.schemaVersion = 2; }], + ["schema rollback", (value) => { value.schemaVersion = 3; }], ["identity drift", (value) => { value.budgetId = "other"; }], - ["artifact path drift", (value) => { value.artifact = "other.wasm"; }], - ["missing field", (value) => { delete value.artifact; }], - ["unknown field", (value) => { value.unknown = true; }], - ["recipe path drift", (value) => { value.buildRecipe.path = "other.json"; }], - ["v1 file drift", (value) => { value.buildRecipe.fileSha256 = "0".repeat(64); }], - ["recipe drift", (value) => { value.buildRecipe.recipeSha256 = "0".repeat(64); }], - ["missing recipe field", (value) => { delete value.buildRecipe.recipeSha256; }], - ["measurement issue drift", (value) => { value.measurement.issue = 295; }], - ["measurement platform drift", (value) => { - value.measurement.measurementPlatform = "darwin-arm64"; + ["predecessor path", (value) => { value.predecessor.path = "other.json"; }], + ["predecessor hash", (value) => { value.predecessor.fileSha256 = "0".repeat(64); }], + ["toolchain path", (value) => { value.toolchainSource.path = "other.json"; }], + ["toolchain hash", (value) => { value.toolchainSource.fileSha256 = "0".repeat(64); }], + ["missing compiler recipe", (value) => { delete value.buildRecipes.compiler; }], + ["extra recipe", (value) => { value.buildRecipes.other = value.buildRecipes.runtime; }], + ["compiler uses runtime command", (value) => { + value.buildRecipes.compiler.command = value.buildRecipes.runtime.command; }], - ["unknown measurement field", (value) => { value.measurement.unknown = true; }], - ["zero bytes", (value) => { value.measurement.rawBytes = 0; }], - ["fractional bytes", (value) => { value.measurement.rawBytes = 1.5; }], - ["unsafe bytes", (value) => { - value.measurement.rawBytes = Number.MAX_SAFE_INTEGER + 1; + ["compiler recipe digest", (value) => { + value.buildRecipes.compiler.recipeSha256 = "0".repeat(64); }], - ["invalid SHA", (value) => { value.measurement.sha256 = "0"; }], - ["uppercase SHA", (value) => { value.measurement.sha256 = "A".repeat(64); }], - ["ceiling plus one", (value) => { value.policy.maxRawBytes += 1; }], - ["ceiling minus one", (value) => { value.policy.maxRawBytes -= 1; }], - ["derivation drift", (value) => { value.policy.derivation = "guessed"; }], - ["gzip gate", (value) => { value.policy.gzip = 123; }], - ["missing policy field", (value) => { delete value.policy.gzip; }], - ["whole-call cycle", (value) => { value.wholeCallArtifact = "forbidden"; }], - ["ratchet regression", (value) => { - value.measurement.rawBytes = v3.policy.maxRawBytes + 1; - value.policy.maxRawBytes = v3.policy.maxRawBytes + 1; + ["missing compiler role", (value) => { delete value.roles.compiler; }], + ["extra role", (value) => { value.roles.other = value.roles.runtime; }], + ["swapped artifacts", (value) => { + [value.roles.runtime.artifact, value.roles.compiler.artifact] = + [value.roles.compiler.artifact, value.roles.runtime.artifact]; + }], + ["measurement issue", (value) => { value.roles.runtime.measurement.issue = 295; }], + ["measurement slice", (value) => { value.roles.compiler.measurement.slice = "B"; }], + ["measurement platform", (value) => { + value.roles.runtime.measurement.measurementPlatform = "darwin-arm64"; }], - ["key reorder", (value) => ({ + ["zero bytes", (value) => { value.roles.compiler.measurement.rawBytes = 0; }], + ["fractional bytes", (value) => { value.roles.runtime.measurement.rawBytes = 1.5; }], + ["invalid SHA", (value) => { value.roles.compiler.measurement.sha256 = "0"; }], + ["ceiling mismatch", (value) => { value.roles.runtime.policy.maxRawBytes += 1; }], + ["derivation drift", (value) => { value.roles.compiler.policy.derivation = "guessed"; }], + ["gzip gate", (value) => { value.roles.runtime.policy.gzip = "gate"; }], + ["same-capability regression", (value) => { + value.roles.runtime.measurement.rawBytes = v1.policy.maxRawBytes + 1; + value.roles.runtime.policy.maxRawBytes = v1.policy.maxRawBytes + 1; + }], + ["whole-call cycle", (value) => { value.wholeCallArtifact = "forbidden"; }], + ["top-level key reorder", (value) => ({ budgetId: value.budgetId, schemaVersion: value.schemaVersion, - artifact: value.artifact, - buildRecipe: value.buildRecipe, - measurement: value.measurement, - policy: value.policy, + predecessor: value.predecessor, + toolchainSource: value.toolchainSource, + buildRecipes: value.buildRecipes, + roles: value.roles, })], ]; - assert.equal(schemaMutations.length, 25, "v4 schema mutation set changed"); + assert.equal(schemaMutations.length, 25, "v5 schema mutation set changed"); for (const [name, mutate] of schemaMutations) { const invalid = structuredClone(fixture); const result = mutate(invalid) ?? invalid; @@ -1249,63 +1319,49 @@ test("WCAG22 WASM budget history is exact, append-only, and acyclic", async () = writeFileSync( fixtureBudgetPath, canonicalJson(fixture).replace( - ' "schemaVersion": 3,\n', - ' "schemaVersion": 3,\n "schemaVersion": 3,\n', + ' "schemaVersion": 4,\n', + ' "schemaVersion": 4,\n "schemaVersion": 4,\n', ), ); assert.throws(run, /canonical JSON/u, "duplicate JSON fields must fail"); - const canonical = checker.evaluateWasmBudget(fixture, bytes, "linux-x64"); - assert.equal(canonical.status, "PASS"); - assert.equal(canonical.artifactSha, "match"); - - const sameSizeMutation = Buffer.from(bytes); - sameSizeMutation[15] = 1; - assert.throws( - () => checker.evaluateWasmBudget(fixture, sameSizeMutation, "linux-x64"), - /SHA-256 mismatch/u, - "same-size byte drift must fail", - ); - assert.throws( - () => checker.evaluateWasmBudget( - fixture, - Buffer.concat([bytes, Buffer.from([0])]), - "linux-x64", - ), - /length mismatch/u, - "append must fail", - ); - assert.throws( - () => checker.evaluateWasmBudget(fixture, bytes.subarray(0, 15), "linux-x64"), - /length mismatch/u, - "truncate must fail", - ); - assert.equal( - checker.evaluateWasmBudget(fixture, sameSizeMutation, "darwin-arm64").status, - "DIAGNOSTIC", - "non-canonical hosts report evidence without admitting it", - ); - assert.equal( - checker.evaluateWasmBudget( - fixture, - Buffer.concat([bytes, Buffer.from([0])]), - "darwin-arm64", - ).status, - "DIAGNOSTIC", - "non-canonical growth remains diagnostic", - ); - assert.equal( - checker.evaluateWasmBudget(fixture, bytes.subarray(0, 15), "darwin-arm64").status, - "DIAGNOSTIC", - "non-canonical shrink remains diagnostic", - ); + for (const [role, bytes] of [["runtime", runtimeBytes], ["compiler", compilerBytes]]) { + const record = fixture.roles[role]; + const canonical = checker.evaluateWasmBudget(role, record, bytes, "linux-x64"); + assert.equal(canonical.status, "PASS"); + assert.equal(canonical.artifactSha, "match"); + const sameSizeMutation = Buffer.from(bytes); + sameSizeMutation[sameSizeMutation.length - 1] = 1; + assert.throws( + () => checker.evaluateWasmBudget(role, record, sameSizeMutation, "linux-x64"), + /SHA-256 mismatch/u, + ); + assert.throws( + () => checker.evaluateWasmBudget( + role, + record, + Buffer.concat([bytes, Buffer.from([0])]), + "linux-x64", + ), + /length mismatch/u, + ); + assert.throws( + () => checker.evaluateWasmBudget(role, record, bytes.subarray(0, -1), "linux-x64"), + /length mismatch/u, + ); + assert.equal( + checker.evaluateWasmBudget(role, record, sameSizeMutation, "darwin-arm64").status, + "DIAGNOSTIC", + ); + } const coordinatedMutation = structuredClone(fixture); - coordinatedMutation.measurement.sha256 = sha256(sameSizeMutation); - const coordinatedBytes = Buffer.from(canonicalJson(coordinatedMutation)); + coordinatedMutation.roles.compiler.measurement.sha256 = sha256( + Buffer.from(compilerBytes).fill(1, compilerBytes.length - 1), + ); assert.throws( - () => checker.parseBudgetDocument(coordinatedBytes, v4Path), - /immutable v4 file SHA-256 mismatch/u, + () => checker.parseBudgetDocument(Buffer.from(canonicalJson(coordinatedMutation)), paths.v5), + /immutable v5 file SHA-256 mismatch/u, "coordinated artifact and document drift must still fail the default identity", ); } finally { @@ -1313,7 +1369,7 @@ test("WCAG22 WASM budget history is exact, append-only, and acyclic", async () = } }); -test("feasibility benchmark keeps V1/V2 history and admits exact V3 Core subjects", () => { +test("feasibility benchmark keeps V1-V3 history and admits exact V4 Core subjects", () => { const contractNames = readdirSync(join( root, "crates", @@ -1324,7 +1380,28 @@ test("feasibility benchmark keeps V1/V2 history and admits exact V3 Core subject "wcag22-feasibility-benchmark-v1.json", "wcag22-feasibility-benchmark-v2.json", "wcag22-feasibility-benchmark-v3.json", + "wcag22-feasibility-benchmark-v4.json", + ]); + const immutableArtifactHashes = new Map([ + ["wcag22-feasibility-benchmark-v1.json", "7e9ffcbdd9d5d50fe681f511c34fc5c5dd270e9c475ce23ae56e9776922a3c5e"], + ["wcag22-feasibility-benchmark-v2.json", "d8d5c7f3eda834bca9912d835fe3ada13d9dcd5a11cb47a131736716b0b51202"], + ["wcag22-feasibility-benchmark-v3.json", "46ec939523a9aff4f253c4c74e997dfd95812a694b2507fae885ff60244ade3a"], + ["wcag22-feasibility-benchmark-v4.json", "3c257c336bc403eee933990fd7188a3b0a6e89d0cbc983aff18846ef76206275"], ]); + for (const [name, expectedSha256] of immutableArtifactHashes) { + const bytes = readFileSync(join( + root, + "crates", + "labcolors-core", + "contracts", + name, + )); + assert.equal( + createHash("sha256").update(bytes).digest("hex"), + expectedSha256, + `${name} is append-only evidence and must stay byte-exact`, + ); + } const benchmarkChecker = join( root, @@ -1336,18 +1413,61 @@ test("feasibility benchmark keeps V1/V2 history and admits exact V3 Core subject "crates", "labcolors-core", "contracts", - "wcag22-feasibility-benchmark-v3.json", + "wcag22-feasibility-benchmark-v4.json", )); const canonicalPayload = JSON.parse(canonicalArtifact.toString("utf8")); + const historicalV3Payload = JSON.parse(read( + "crates", + "labcolors-core", + "contracts", + "wcag22-feasibility-benchmark-v3.json", + )); + const identityProjection = (payload) => ({ + boundedEnvelopeModel: payload.boundedEnvelopeModel, + profileLimits: payload.profileLimits, + scenarioOrder: payload.scenarioOrder, + scenarios: payload.scenarios.map((scenario) => ({ + name: scenario.name, + shape: scenario.shape, + expected: scenario.expected, + observedIdentity: scenario.observedIdentity, + })), + }); + assert.deepEqual( + identityProjection(canonicalPayload), + identityProjection(historicalV3Payload), + "C1 may change provenance and observations, not the admitted finite algorithm", + ); + const subjectsByPath = (payload) => new Map( + payload.subjectManifest.map((subject) => [subject.path, subject.sha256]), + ); + const historicalSubjects = subjectsByPath(historicalV3Payload); + const canonicalSubjects = subjectsByPath(canonicalPayload); + const subjectPaths = [...new Set([ + ...historicalSubjects.keys(), + ...canonicalSubjects.keys(), + ])].sort(); + assert.deepEqual( + subjectPaths.filter( + (path) => historicalSubjects.get(path) !== canonicalSubjects.get(path), + ), + [ + "Cargo.lock", + "Cargo.toml", + "crates/labcolors-core/benches/wcag22_feasibility_admission.rs", + "scripts/check_wcag22_feasibility_benchmark.py", + ], + "V4 source drift must be exactly the C1 workspace and admission machinery", + ); assert.equal( "gitRevision" in canonicalPayload.environment, false, - "durable V3 must not claim an ephemeral measurement commit", + "durable V4 must not claim an ephemeral measurement commit", ); assert.equal( "gitTree" in canonicalPayload.environment, false, - "durable V3 must use its exact source-object cone as the provenance SSOT", + "durable V4 must use its exact source-object cone as the provenance SSOT", ); assert.deepEqual( canonicalPayload.environment.explicitEmptyBuildInputs, @@ -1375,12 +1495,12 @@ test("feasibility benchmark keeps V1/V2 history and admits exact V3 Core subject assert.equal( "rustFlags" in canonicalPayload.environment, false, - "V3 must not collapse absent and explicitly empty RUSTFLAGS", + "V4 must not collapse absent and explicitly empty RUSTFLAGS", ); assert.equal( "cargoEncodedRustflags" in canonicalPayload.environment, false, - "V3 must represent source presence instead of only its empty value", + "V4 must represent source presence instead of only its empty value", ); const rustcRelease = canonicalPayload.environment.rustcVerbose .match(/^rustc ([^ ]+) /u)?.[1]; @@ -1461,15 +1581,20 @@ test("feasibility benchmark keeps V1/V2 history and admits exact V3 Core subject assert.match(error.stderr, /unrecognized arguments: --admit-revision/u); return true; }, - "V3 must not accept unverifiable whole-commit provenance", + "V4 must not accept unverifiable whole-commit provenance", ); } finally { rmSync(temporary, { recursive: true, force: true }); } const ci = read(".github", "workflows", "ci.yml"); - const unmergedDraftAdmission = - /v3_snapshot=|b777b1d95dd7693220621600dd49042a2046dab5|5781d4ab84b39a585d437e8e04604b25ef891cf1|5e5fdb34586452f3171b20113ab6f6a9412bcd82|ff2ed3c522192fe7c1e1492d59a466dd78c90ba2d5a243474cd4073f93362f53|e701d2e5ea8db96e446f6ac428b44374cd219caf09711bcac109639fbb405efd|d7f0f1c3ef0810eb5e3a8aecfcb0b67be7603ee9a6b23f8401c2284c5532bace|feasibility-benchmark-v4|admission-raw-v4/u; + assert.match( + ci, + /sha256sum --check --strict <<'SHA256'[\s\S]*?7e9ffcbdd9d5d50fe681f511c34fc5c5dd270e9c475ce23ae56e9776922a3c5e crates\/labcolors-core\/contracts\/wcag22-feasibility-benchmark-v1\.json[\s\S]*?d8d5c7f3eda834bca9912d835fe3ada13d9dcd5a11cb47a131736716b0b51202 crates\/labcolors-core\/contracts\/wcag22-feasibility-benchmark-v2\.json[\s\S]*?46ec939523a9aff4f253c4c74e997dfd95812a694b2507fae885ff60244ade3a crates\/labcolors-core\/contracts\/wcag22-feasibility-benchmark-v3\.json[\s\S]*?3c257c336bc403eee933990fd7188a3b0a6e89d0cbc983aff18846ef76206275 crates\/labcolors-core\/contracts\/wcag22-feasibility-benchmark-v4\.json[\s\S]*?SHA256/u, + "CI must pin current-checkout bytes before historical replay", + ); + const rejectedDraftFingerprints = + /b777b1d95dd7693220621600dd49042a2046dab5|5781d4ab84b39a585d437e8e04604b25ef891cf1|5e5fdb34586452f3171b20113ab6f6a9412bcd82|ff2ed3c522192fe7c1e1492d59a466dd78c90ba2d5a243474cd4073f93362f53|e701d2e5ea8db96e446f6ac428b44374cd219caf09711bcac109639fbb405efd|d7f0f1c3ef0810eb5e3a8aecfcb0b67be7603ee9a6b23f8401c2284c5532bace/u; for (const [path, source] of [ [".github/workflows/ci.yml", ci], ["CHANGELOG.md", read("CHANGELOG.md")], @@ -1486,7 +1611,7 @@ test("feasibility benchmark keeps V1/V2 history and admits exact V3 Core subject ]) { assert.doesNotMatch( source, - unmergedDraftAdmission, + rejectedDraftFingerprints, `${path} must not retain an unmerged draft admission`, ); } @@ -1507,18 +1632,23 @@ test("feasibility benchmark keeps V1/V2 history and admits exact V3 Core subject ); assert.match( ci, - /trap - EXIT[\s\S]*?current_artifact="crates\/labcolors-core\/contracts\/wcag22-feasibility-benchmark-v3\.json"[\s\S]*?current_protocol=\([\s\S]*?--admit-rustc-release 1\.96\.0[\s\S]*?--admit-cargo-release 1\.96\.0[\s\S]*?--admit-rustc-binary-sha256 [0-9a-f]{64}[\s\S]*?--admit-cargo-binary-sha256 [0-9a-f]{64}[\s\S]*?--admit-benchmark-binary-sha256 [0-9a-f]{64}[\s\S]*?python3 scripts\/check_wcag22_feasibility_benchmark\.py[\s\S]*?--artifact-sha256 46ec939523a9aff4f253c4c74e997dfd95812a694b2507fae885ff60244ade3a[\s\S]*?--self-test/u, - "V3 must bind the current generic kernel without an intermediate worktree", + /v3_artifact="crates\/labcolors-core\/contracts\/wcag22-feasibility-benchmark-v3\.json"[\s\S]*?v3_snapshot=10c44ef0f4248d0390aa339e81c05a6d5e41996f[\s\S]*?--admit-benchmark-binary-sha256 6ac07bad81a204ee8fcee8f94a3c445f881d1ca10edaf4cc4a86a5db0b232e3a[\s\S]*?git worktree add --detach "\$historical_root" "\$v3_snapshot"[\s\S]*?python3 scripts\/check_wcag22_feasibility_benchmark\.py[\s\S]*?--artifact-sha256 46ec939523a9aff4f253c4c74e997dfd95812a694b2507fae885ff60244ade3a[\s\S]*?--self-test/u, + "V3 must replay through the exact merged Slice-B snapshot", + ); + assert.match( + ci, + /trap - EXIT[\s\S]*?current_artifact="crates\/labcolors-core\/contracts\/wcag22-feasibility-benchmark-v4\.json"[\s\S]*?current_protocol=\([\s\S]*?--admit-rustc-release 1\.96\.0[\s\S]*?--admit-cargo-release 1\.96\.0[\s\S]*?--admit-rustc-binary-sha256 c5922366bfe3d6d028a65d626f4e629b3adad066995cf0b60c8a4b617bba5ffe[\s\S]*?--admit-cargo-binary-sha256 fec239e6b74df873f54ef52912bfcfcc8d8414bc14a7ae1e0be80460bae72841[\s\S]*?--admit-benchmark-binary-sha256 69fe95cea34c845478c0a3c260e3e4459f1bc09d76857b74d5edb50fd923410a[\s\S]*?python3 scripts\/check_wcag22_feasibility_benchmark\.py[\s\S]*?--artifact-sha256 3c257c336bc403eee933990fd7188a3b0a6e89d0cbc983aff18846ef76206275[\s\S]*?--self-test/u, + "V4 must bind the current C1 source cone without an intermediate worktree", ); assert.equal( ci.match(/python3 scripts\/check_wcag22_feasibility_benchmark\.py/gu)?.length, - 3, - "CI must validate exactly two historical and one current benchmark artifact", + 4, + "CI must validate exactly three historical and one current benchmark artifact", ); assert.equal( ci.match(/git worktree add --detach/gu)?.length, - 3, - "only the three main-reachable historical verifier snapshots may use worktrees", + 4, + "only the four main-reachable historical verifier snapshots may use worktrees", ); }); @@ -1693,13 +1823,21 @@ test("published build metadata binds source, conformance, and WASM inputs", () = assert.match(prepare, /packDigest: conformance\.packDigest/); assert.match(prepare, /manifestSha256: sha256\(Buffer\.from\(conformanceSource\)\)/); assert.match(prepare, /familySetSha256: sha256\(Buffer\.concat\(familyBytes\)\)/); - assert.match(prepare, /wasm: \{ bytes: wasm\.length, sha256: sha256\(wasm\) \}/); + assert.match(prepare, /schemaVersion: 2/u); + assert.match(prepare, /role: "runtime"[\s\S]*?path: "pkg\/labcolors_bg\.wasm"/u); + assert.match( + prepare, + /role: "compiler"[\s\S]*?path: "compiler\/labcolors_compiler_bg\.wasm"/u, + ); + assert.match(prepare, /bytes: runtimeWasm\.length/u); + assert.match(prepare, /bytes: compilerWasm\.length/u); const verifier = read("scripts", "verify-package-release.mjs"); assert.match(verifier, /import \{ workspaceVersion \} from "\.\/cargo-workspace\.mjs";/); assert.match(verifier, /function validateBuildMetadata/); assert.match(verifier, /isDeepStrictEqual\(metadata, expected\)/); assert.match(verifier, /require\.resolve\("@labpics\/colors\/build-metadata\.json"\)/); + assert.match(verifier, /require\.resolve\("@labpics\/colors\/compiler\/wasm"\)/); assert.match(verifier, /installedBuildMetadata/); assert.match(verifier, /isDeepStrictEqual\(installedBuildMetadata, expectedBuildMetadata\)/); assert.match(verifier, /"--offline"/); @@ -1710,10 +1848,12 @@ test("published build metadata binds source, conformance, and WASM inputs", () = assert.match(verifier, /"--lib",\s+"ES2022,DOM"/u); assert.doesNotMatch(verifier, /ES2022,DOM,ESNext\.Disposable/u); assert.match(verifier, /libraries: \["ES2022", "DOM"\]/u); - assert.match(verifier, /artifacts: \{ tarball, wasm, buildMetadata \}/); + assert.match(verifier, /role: "runtime", \.\.\.wasm\.runtime/u); + assert.match(verifier, /role: "compiler", \.\.\.wasm\.compiler/u); + assert.match(verifier, /buildMetadata,/u); }); -test("package root curates public types while keeping feasibility internals private", () => { +test("runtime and compiler declarations expose disjoint curated type surfaces", () => { const wasmSource = read("crates", "labcolors-wasm", "src", "lib.rs"); const customSection = wasmSource.match( /const TS_RESULT_TYPES: &'static str = r##"([\s\S]*?)"##;/u, @@ -1724,6 +1864,7 @@ test("package root curates public types while keeping feasibility internals priv ].map((match) => match[1]); assert.ok(generatedNames.length > 10, "anti-vacuum: custom type surface is non-trivial"); assert.equal(new Set(generatedNames).size, generatedNames.length, "duplicate custom type name"); + assert.doesNotMatch(customSection, /Feasibility|feasibility/u); const rootDeclarations = read("packages", "colors", "index.d.ts"); assert.match( @@ -1765,6 +1906,32 @@ test("package root curates public types while keeping feasibility internals priv const exportedNames = [...rootTypes.matchAll(/^\s{2}([A-Za-z][A-Za-z0-9_]*),$/gmu)].map( (match) => match[1], ); + assert.deepEqual( + [...exportedNames].sort(), + [...generatedNames].sort(), + "root types must equal the runtime generated surface exactly", + ); + assert.doesNotMatch(rootDeclarations, /Feasibility|feasibility/u); + assert.match(rootDeclarations, /export type \{ Wcag22CriterionV1 \} from "\.\/wcag22\.js"/u); + + const compilerSource = read("crates", "labcolors-compiler", "src", "lib.rs"); + const compilerCustomSection = compilerSource.match( + /const TS_COMPILER_TYPES: &'static str = r##"([\s\S]*?)"##;/u, + )?.[1]; + assert.ok(compilerCustomSection, "compiler custom TypeScript section not found"); + const compilerGeneratedNames = [ + ...compilerCustomSection.matchAll( + /^export\s+(?:type|interface)\s+([A-Za-z][A-Za-z0-9_]*)/gmu, + ), + ].map((match) => match[1]); + const compilerDeclarations = read("packages", "colors", "compiler.d.ts"); + const compilerPublicTypes = compilerDeclarations.match( + /export type \{([\s\S]*?)\} from "\.\/compiler\/labcolors_compiler\.js";/u, + )?.[1]; + assert.ok(compilerPublicTypes, "curated compiler type export block not found"); + const compilerExportedNames = [ + ...compilerPublicTypes.matchAll(/^\s{2}([A-Za-z][A-Za-z0-9_]*),$/gmu), + ].map((match) => match[1]); const feasibilityInternals = new Set([ "Bytes32V1", "DecimalU64V1", @@ -1786,21 +1953,25 @@ test("package root curates public types while keeping feasibility internals priv "Wcag22FeasibilityV1", ]); for (const name of feasibilityInternals) { - assert.ok(generatedNames.includes(name), `generated declarations omit ${name}`); - assert.ok(!exportedNames.includes(name), `package root leaks internal ${name}`); + assert.ok(compilerGeneratedNames.includes(name), `compiler declarations omit ${name}`); + assert.ok(!compilerExportedNames.includes(name), `compiler entry leaks internal ${name}`); } for (const publicName of [ "Wcag22FeasibilityRequestV1", "Wcag22FeasibilityOutcomeV1", ]) { - assert.ok(exportedNames.includes(publicName), `package root omits ${publicName}`); + assert.ok(compilerExportedNames.includes(publicName), `compiler entry omits ${publicName}`); } - assert.deepEqual( - [...exportedNames].sort(), - generatedNames.filter((name) => !feasibilityInternals.has(name)).sort(), - "root types must equal the reviewed public subset exactly", - ); + assert.deepEqual(compilerExportedNames.sort(), [ + "Wcag22FeasibilityOutcomeV1", + "Wcag22FeasibilityRequestV1", + ]); assert.doesNotMatch(rootTypes, /InitOutput|__wbg_/u, "raw wasm ABI must stay private"); + assert.doesNotMatch( + compilerPublicTypes, + /InitOutput|__wbg_/u, + "raw compiler WASM ABI must stay private", + ); }); test("public declarations compile at the documented minimum TypeScript version", () => { diff --git a/packages/colors/test/release-provenance.test.mjs b/packages/colors/test/release-provenance.test.mjs index e0fbe77e..f6eb86c3 100644 --- a/packages/colors/test/release-provenance.test.mjs +++ b/packages/colors/test/release-provenance.test.mjs @@ -157,10 +157,21 @@ test("build metadata exact validator rejects one-field tampering", async () => { manifestSha256: "2".repeat(64), familySetSha256: "3".repeat(64), }, - wasm: { bytes: 123, sha256: "4".repeat(64) }, + wasm: { + runtime: { + path: "pkg/labcolors_bg.wasm", + bytes: 123, + sha256: "4".repeat(64), + }, + compiler: { + path: "compiler/labcolors_compiler_bg.wasm", + bytes: 45, + sha256: "5".repeat(64), + }, + }, }; const metadata = { - schemaVersion: 1, + schemaVersion: 2, package: { ...context.packageJson }, sourceSha: context.source, coreVersion: context.coreVersion, @@ -170,12 +181,15 @@ test("build metadata exact validator rejects one-field tampering", async () => { manifestSha256: context.conformanceEvidence.manifestSha256, familySetSha256: context.conformanceEvidence.familySetSha256, }, - wasm: { ...context.wasm }, + wasm: [ + { role: "runtime", ...context.wasm.runtime }, + { role: "compiler", ...context.wasm.compiler }, + ], }; assert.doesNotThrow(() => validateBuildMetadata(metadata, context)); const tampered = structuredClone(metadata); - tampered.wasm.sha256 = "5".repeat(64); + tampered.wasm[1].sha256 = "6".repeat(64); assert.throws( () => validateBuildMetadata(tampered, context), /does not exactly bind the release inputs/, diff --git a/packages/colors/test/wcag22-feasibility-boundary.test.mjs b/packages/colors/test/wcag22-feasibility-boundary.test.mjs index 65f00797..bc50d436 100644 --- a/packages/colors/test/wcag22-feasibility-boundary.test.mjs +++ b/packages/colors/test/wcag22-feasibility-boundary.test.mjs @@ -15,7 +15,7 @@ const here = dirname(fileURLToPath(import.meta.url)); const root = resolve(here, "../../.."); const corePath = resolve( root, - "crates/labcolors-core/contracts/wcag22-feasibility-benchmark-v3.json", + "crates/labcolors-core/contracts/wcag22-feasibility-benchmark-v4.json", ); const toolchainPath = resolve(root, "packages/colors/bench/wasm-size-budget-v1.json"); const harnessPath = resolve( @@ -25,17 +25,14 @@ const harnessPath = resolve( const packPath = resolve(root, "conformance/vectors/wcag22-feasibility.json"); const conformanceManifestPath = resolve(root, "conformance/vectors/manifest.json"); const packageManifestPath = resolve(root, "packages/colors/package.json"); -const packageRootPath = resolve(root, "packages/colors/index.js"); -const wasmGluePath = resolve(root, "packages/colors/pkg/labcolors.js"); -const eagerRuntimePaths = { - adaptTheme: "adapt-theme.js", - applyTheme: "apply-theme.js", - effectiveBackground: "effective-bg.js", - watchTheme: "watch-theme.js", -}; +const compilerEntryPath = resolve(root, "packages/colors/compiler.js"); +const wasmGluePath = resolve(root, "packages/colors/compiler/labcolors_compiler.js"); +const wasmBudgetPath = resolve(root, "packages/colors/bench/wasm-size-budget-v5.json"); const coreBytes = readFileSync(corePath); const core = JSON.parse(coreBytes); const toolchain = JSON.parse(readFileSync(toolchainPath)); +const wasmBudgetBytes = readFileSync(wasmBudgetPath); +const wasmBudget = JSON.parse(wasmBudgetBytes); const packBytes = readFileSync(packPath); const pack = JSON.parse(packBytes); const conformanceManifestBytes = readFileSync(conformanceManifestPath); @@ -59,13 +56,13 @@ function toolchainRecipe() { wasmOptVersion: measurement.wasmOptVersion, measurementPlatform: measurement.measurementPlatform, rustPathRemap: measurement.rustPathRemap, - command: measurement.command, + command: wasmBudget.buildRecipes.compiler.command, }; } function maxRequestBytes() { // Independent test oracle only. Production measurement takes this value - // exclusively from the initialized package-root getter, then binds the exact + // exclusively from the initialized compiler-entry getter, then binds the exact // WASM plus limit/limit+1 witnesses; this mirror mutation-kills a fabricated // artifact limit without making JS a second production authority. const limits = core.profileLimits; @@ -146,16 +143,21 @@ function fixture() { shape, samples: Array.from({ length: core.sampleCount }, (_, sampleIndex) => ({ sampleIndex, + initSyncElapsedNs: String(sampleIndex + 1), elapsedNs: String(sampleIndex + 1), requestBytes, requestSha256, outcomeBytes: 200 + scenarioIndex, outcomeSha256, summary: structuredClone(summary), + processMaxRssKiBBeforeInit: 900, + processMaxRssKiBAfterInit: 950, processMaxRssKiBBefore: 1_000, processMaxRssKiBAfter: 1_001 + sampleIndex, + wasmMemoryBytesAfterInit: 65_536, wasmMemoryBytesBefore: 65_536, wasmMemoryBytesAfter: 65_536 * 2, + wasmMemoryPagesAfterInit: 1, wasmMemoryPagesBefore: 1, wasmMemoryPagesAfter: 2, })), @@ -164,7 +166,7 @@ function fixture() { return { schemaVersion: 1, artifactId: MEASUREMENT_ARTIFACT_ID, - claimBoundary: "canonical-wasm-package-root-whole-call-observations-only", + claimBoundary: "canonical-wasm-compiler-entry-whole-call-observations-only", claims: { admission: "canonical-linux-x64-exact-wasm-only", hardGates: [ @@ -177,18 +179,22 @@ function fixture() { "no-proportional-dto", ], timingThresholdNs: null, - latency: "observation-only-no-production-threshold", + latency: "init-sync-and-warm-operation-observations-only-no-production-threshold", memory: - "process maxRSS is total-process high-water including V8; post-call WASM pages are linear-memory high-water observations; neither is total operation memory", + "process maxRSS values are total-process high-water including V8 and prior warm-up/observer allocations; after-init and warm-call WASM pages are linear-memory high-water observations; neither is total operation memory", }, environment: { execution: "fresh-node-child-process-per-sample", + initSyncScope: + "initSync-from-in-memory-compiler-wasm-includes-wasm-bindgen-startup-excludes-io-and-js-module-import", + operationScope: + "second-identical-operation-after-one-unmeasured-warm-up-whose-result-graph-is-not-retained-by-harness", platform: "linux-x64", nodeVersion: canonicalNodeVersion, sampleCount: core.sampleCount, requestConstructionMeasured: false, timer: "process.hrtime.bigint", - packageRootApi: "packages/colors/index.js", + publicEntry: "packages/colors/compiler.js", canonicalCandidate: true, rustToolchain: toolchain.measurement.rustToolchain, wasmPack: toolchain.measurement.wasmPack, @@ -199,7 +205,7 @@ function fixture() { }, bindings: { coreAdmission: { - path: "crates/labcolors-core/contracts/wcag22-feasibility-benchmark-v3.json", + path: "crates/labcolors-core/contracts/wcag22-feasibility-benchmark-v4.json", schemaVersion: core.schemaVersion, artifactId: core.artifactId, profileId: core.profileLimits.profileId, @@ -216,7 +222,7 @@ function fixture() { packVersion: conformanceManifest.packVersion, packDigest: conformanceManifest.packDigest, }, - runtimeSources: { + compilerSources: { harness: { path: "packages/colors/bench/wcag22-feasibility-boundary.bench.mjs", sha256: sha256(readFileSync(harnessPath)), @@ -225,34 +231,27 @@ function fixture() { path: "packages/colors/package.json", sha256: sha256(readFileSync(packageManifestPath)), }, - packageRoot: { - path: "packages/colors/index.js", - sha256: sha256(readFileSync(packageRootPath)), + compilerEntry: { + path: "packages/colors/compiler.js", + sha256: sha256(readFileSync(compilerEntryPath)), }, wasmGlue: { - path: "packages/colors/pkg/labcolors.js", + path: "packages/colors/compiler/labcolors_compiler.js", sha256: sha256(readFileSync(wasmGluePath)), }, - ...Object.fromEntries( - Object.entries(eagerRuntimePaths).map(([sourceId, filename]) => [ - sourceId, - { - path: `packages/colors/${filename}`, - sha256: sha256(readFileSync(resolve(root, "packages/colors", filename))), - }, - ]), - ), }, - wasmToolchain: { - path: "packages/colors/bench/wasm-size-budget-v1.json", - schemaVersion: toolchain.schemaVersion, - budgetId: toolchain.budgetId, - recipeSha256: sha256(Buffer.from(JSON.stringify(toolchainRecipe()), "utf8")), + wasmBudget: { + path: "packages/colors/bench/wasm-size-budget-v5.json", + schemaVersion: wasmBudget.schemaVersion, + budgetId: wasmBudget.budgetId, + fileSha256: sha256(wasmBudgetBytes), + role: "compiler", + recipeSha256: wasmBudget.buildRecipes.compiler.recipeSha256, }, wasm: { - path: "packages/colors/pkg/labcolors_bg.wasm", - bytes: 500_000, - sha256: "a".repeat(64), + path: "packages/colors/compiler/labcolors_compiler_bg.wasm", + bytes: wasmBudget.roles.compiler.measurement.rawBytes, + sha256: wasmBudget.roles.compiler.measurement.sha256, }, }, limits, @@ -269,6 +268,10 @@ test("whole-call evidence history is exact and deterministic", () => { root, "packages/colors/bench/wcag22-feasibility-wasm-boundary-v2.json", )); + const v3Bytes = readFileSync(resolve( + root, + "packages/colors/bench/wcag22-feasibility-wasm-boundary-v3.json", + )); assert.equal( sha256(v1Bytes), "8281f372cf635174fa3cedf828a96b48a023c413f43245cfc7001d9b83ff1790", @@ -277,10 +280,16 @@ test("whole-call evidence history is exact and deterministic", () => { sha256(v2Bytes), "3b4ec73fc09eeee03a96fa785fe7c4c6af419965b74b9e454f1378cf3170d888", ); + assert.equal( + sha256(v3Bytes), + "60e0b0f621fb4e0fcc5c57c527a8f1bf11487ee34581c98239b1ac6c31e6de86", + ); const v1 = JSON.parse(v1Bytes); const v2 = JSON.parse(v2Bytes); + const v3 = JSON.parse(v3Bytes); assert.equal(v1.artifactId, "wcag22-feasibility-wasm-whole-call-v1"); - assert.equal(v2.artifactId, MEASUREMENT_ARTIFACT_ID); + assert.equal(v2.artifactId, "wcag22-feasibility-wasm-whole-call-v2"); + assert.equal(v3.artifactId, "wcag22-feasibility-wasm-whole-call-v3"); assert.deepEqual(v2.bindings.coreAdmission, { path: "crates/labcolors-core/contracts/wcag22-feasibility-benchmark-v3.json", schemaVersion: 1, @@ -293,6 +302,18 @@ test("whole-call evidence history is exact and deterministic", () => { bytes: 520920, sha256: "c179f42cd90c24699167ee78b4080c80fb38247c54953e7dc020483f6fcf94ed", }); + assert.deepEqual(v3.bindings.coreAdmission, { + path: "crates/labcolors-core/contracts/wcag22-feasibility-benchmark-v4.json", + schemaVersion: 1, + artifactId: "wcag22-feasibility-admission-raw-v4", + profileId: "compile-v1", + sha256: "3c257c336bc403eee933990fd7188a3b0a6e89d0cbc983aff18846ef76206275", + }); + assert.deepEqual(v3.bindings.wasm, { + path: "packages/colors/compiler/labcolors_compiler_bg.wasm", + bytes: 175212, + sha256: "3a552ce43ada7d0b10e90a23b4a7e50a4ecad77a446374b98ca8ee6b5c6a2a45", + }); const deterministicProjection = (artifact) => ({ limits: artifact.limits, @@ -310,6 +331,7 @@ test("whole-call evidence history is exact and deterministic", () => { })), }); assert.deepEqual(deterministicProjection(v2), deterministicProjection(v1)); + assert.deepEqual(deterministicProjection(v3), deterministicProjection(v2)); }); test("canonical whole-call artifact schema accepts all immutable scenarios", () => { @@ -349,16 +371,32 @@ test("whole-call checker mutation-kills missing evidence and inflated claims", ( artifact.scenarios[0].samples.pop(); }], ["stale measurement harness", (artifact) => { - artifact.bindings.runtimeSources.harness.sha256 = "b".repeat(64); + artifact.bindings.compilerSources.harness.sha256 = "b".repeat(64); }], ["stale package root", (artifact) => { - artifact.bindings.runtimeSources.packageRoot.sha256 = "b".repeat(64); + artifact.bindings.compilerSources.compilerEntry.sha256 = "b".repeat(64); }], ["stale generated glue", (artifact) => { - artifact.bindings.runtimeSources.wasmGlue.sha256 = "b".repeat(64); + artifact.bindings.compilerSources.wasmGlue.sha256 = "b".repeat(64); + }], + ["runtime source inserted", (artifact) => { + artifact.bindings.compilerSources.packageRoot = { + path: "packages/colors/index.js", + sha256: "b".repeat(64), + }; + }], + ["stale role budget", (artifact) => { + artifact.bindings.wasmBudget.fileSha256 = "b".repeat(64); + }], + ["wrong execution role", (artifact) => { + artifact.bindings.wasmBudget.role = "runtime"; + }], + ["wrong compiler recipe", (artifact) => { + artifact.bindings.wasmBudget.recipeSha256 = "b".repeat(64); }], ]; - assert.equal(mutations.length, 13, "anti-vacuum mutation set changed"); + assert.doesNotThrow(() => validateMeasurementArtifact(fixture())); + assert.equal(mutations.length, 17, "anti-vacuum mutation set changed"); for (const [name, mutate] of mutations) { const artifact = fixture(); mutate(artifact); @@ -370,11 +408,12 @@ test("whole-call checker mutation-kills missing evidence and inflated claims", ( } }); -test("final CI verifies and uploads committed Linux evidence before the size gate", () => { +test("CI verifies committed whole-call evidence before the size gate", () => { const ci = ciSource; const harness = "node bench/wcag22-feasibility-boundary.bench.mjs"; - const verify = "\n --verify"; - const fingerprint = "sha256sum packages/colors/pkg/labcolors_bg.wasm"; + const evidence = "bench/wcag22-feasibility-wasm-boundary-v3.json"; + const verify = `--verify ${evidence}`; + const fingerprint = "name: independently fingerprint both execution-role WASM artifacts"; const upload = "actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02"; const sizeGate = "node scripts/check-wasm-size-budget.mjs"; @@ -383,13 +422,13 @@ test("final CI verifies and uploads committed Linux evidence before the size gat const fingerprintIndex = ci.indexOf(fingerprint, verifyIndex); const uploadIndex = ci.indexOf(upload, verifyIndex); const sizeGateIndex = ci.indexOf(sizeGate); - assert.match(ci, /verify committed #296-B canonical whole-call WASM boundary evidence/u); + assert.match(ci, /verify committed #296-C1 canonical whole-call compiler evidence/u); assert.match( ci, - /name: "upload exact #296-B verified whole-call evidence"/u, + /name: upload exact #296-C1 whole-call evidence/u, ); - assert.ok(harnessIndex >= 0, "the package-root harness must run in CI"); - assert.ok(verifyIndex > harnessIndex, "CI must rerun the committed evidence verifier"); + assert.ok(harnessIndex >= 0, "the compiler-entry harness must run in CI"); + assert.ok(verifyIndex > harnessIndex, "CI must verify the committed evidence"); assert.ok( fingerprintIndex > verifyIndex, "an independent system tool must fingerprint the verified WASM", @@ -401,9 +440,13 @@ test("final CI verifies and uploads committed Linux evidence before the size gat ); assert.match( ci, - /path: \|[\s\S]*?packages\/colors\/bench\/wcag22-feasibility-wasm-boundary-v2\.json[\s\S]*?packages\/colors\/pkg\/labcolors_bg\.wasm/u, + /path: \|[\s\S]*?packages\/colors\/bench\/wcag22-feasibility-wasm-boundary-v3\.json[\s\S]*?packages\/colors\/pkg\/labcolors_bg\.wasm[\s\S]*?packages\/colors\/compiler\/labcolors_compiler_bg\.wasm/u, + ); + assert.doesNotMatch( + ci, + /wcag22-feasibility-boundary\.bench\.mjs --record/u, + "CI must never mint a new whole-call truth from the revision under test", ); - assert.doesNotMatch(ci, /--record/u, "candidate-recording mode must not survive admission"); assert.match(ci, /if-no-files-found: error/u); assert.match( readFileSync(resolve(root, "packages/colors/bench/wcag22-feasibility-boundary.bench.mjs"), "utf8"), diff --git a/packages/colors/test/wcag22-feasibility.test.mjs b/packages/colors/test/wcag22-feasibility.test.mjs index 9a060b7b..111847dc 100644 --- a/packages/colors/test/wcag22-feasibility.test.mjs +++ b/packages/colors/test/wcag22-feasibility.test.mjs @@ -8,28 +8,28 @@ import { test } from "node:test"; import { pathToFileURL } from "node:url"; import { runInNewContext } from "node:vm"; -const packageRoot = new URL("../", import.meta.url); const require = createRequire(import.meta.url); -async function importRootWithInstrumentedWasm(t) { - const fixture = await mkdtemp(join(tmpdir(), "labcolors-feasibility-host-")); +async function importCompilerWithInstrumentedWasm(t) { + const fixture = await mkdtemp(join(tmpdir(), "labcolors-compiler-host-")); t.after(() => rm(fixture, { recursive: true, force: true })); - await mkdir(join(fixture, "pkg")); + await mkdir(join(fixture, "compiler")); await writeFile(join(fixture, "package.json"), '{"type":"module"}\n'); - await writeFile(join(fixture, "index.js"), await readFile(new URL("../index.js", import.meta.url))); await writeFile( - join(fixture, "pkg/labcolors.js"), + join(fixture, "compiler.js"), + await readFile(new URL("../compiler.js", import.meta.url)), + ); + await writeFile( + join(fixture, "compiler/labcolors_compiler.js"), ` globalThis.__labcolorsFeasibilityCalls = { evaluate: [], max: [], oversize: [] }; let initialized = false; export default async function init() { initialized = true; } export function initSync() { initialized = true; } -export class LabColors {} -export function evaluateWcag22() {} -export function numericalCapabilityManifest() {} export function wcag22FeasibilityMaxRequestBytesV1() { if (!initialized) throw new Error("WASM not initialized"); globalThis.__labcolorsFeasibilityCalls.max.push(true); + globalThis.__labcolorsFeasibilityBeforeForward?.(); return 657380; } export function evaluateWcag22FeasibilityV1(request) { @@ -55,45 +55,113 @@ export function wcag22FeasibilityEnvelopeTooLargeV1(requestedBytes) { } `, ); - for (const [file, exports] of [ - ["apply-theme.js", "export function applyTheme() {}\n"], - ["watch-theme.js", "export function watchTheme() {}\n"], - ["adapt-theme.js", "export function adaptTheme() {}\n"], - [ - "effective-bg.js", - "export function effectiveBackground() {}\nexport function parseCssColor() {}\nexport function compositeOver() {}\nexport function compositeStackToHex() {}\nexport function toHex() {}\nexport function oklabLerp() {}\n", - ], - ]) { - await writeFile(join(fixture, file), exports); - } - return import(`${pathToFileURL(join(fixture, "index.js")).href}?case=${Date.now()}`); + return import(`${pathToFileURL(join(fixture, "compiler.js")).href}?case=${Date.now()}`); } -test("package root rejects an oversized envelope before the avoidable WASM copy", async (t) => { - const root = await importRootWithInstrumentedWasm(t); +test("compiler rejects an oversized envelope before the avoidable WASM copy", async (t) => { + const compiler = await importCompilerWithInstrumentedWasm(t); assert.deepEqual( globalThis.__labcolorsFeasibilityCalls.max, [], "import must not touch uninitialized WASM", ); - root.initSync(); - assert.equal(root.wcag22FeasibilityMaxBytes(), 657380); + compiler.initSync(); + assert.equal(compiler.wcag22FeasibilityMaxBytes(), 657380); - const oversized = new Uint8Array(root.wcag22FeasibilityMaxBytes() + 1); - const outcome = root.evaluateWcag22Feasibility(oversized); + const oversized = new Uint8Array(compiler.wcag22FeasibilityMaxBytes() + 1); + const outcome = compiler.evaluateWcag22Feasibility(oversized); assert.equal(outcome.outcome, "failure"); assert.equal(outcome.error.error.code, "envelopeTooLarge"); assert.deepEqual(globalThis.__labcolorsFeasibilityCalls.evaluate, []); assert.deepEqual(globalThis.__labcolorsFeasibilityCalls.oversize, [657381n]); - const exactLimit = new Uint8Array(root.wcag22FeasibilityMaxBytes()); - assert.equal(root.evaluateWcag22Feasibility(exactLimit).outcome, "success"); + const exactLimit = new Uint8Array(compiler.wcag22FeasibilityMaxBytes()); + assert.equal(compiler.evaluateWcag22Feasibility(exactLimit).outcome, "success"); assert.equal(globalThis.__labcolorsFeasibilityCalls.evaluate.length, 1); - assert.strictEqual(globalThis.__labcolorsFeasibilityCalls.evaluate[0], exactLimit); + assert.notStrictEqual(globalThis.__labcolorsFeasibilityCalls.evaluate[0], exactLimit); + assert.equal(globalThis.__labcolorsFeasibilityCalls.evaluate[0].buffer, exactLimit.buffer); }); -test("package root rejects non-Uint8Array inputs before touching WASM", async (t) => { - const root = await importRootWithInstrumentedWasm(t); +test("compiler measures the intrinsic Uint8Array length, not an own-property spoof", async (t) => { + const compiler = await importCompilerWithInstrumentedWasm(t); + compiler.initSync(); + const oversized = new Uint8Array(compiler.wcag22FeasibilityMaxBytes() + 1); + Object.defineProperty(oversized, "byteLength", { value: 0 }); + + const outcome = compiler.evaluateWcag22Feasibility(oversized); + assert.equal(outcome.error.error.code, "envelopeTooLarge"); + assert.deepEqual(globalThis.__labcolorsFeasibilityCalls.evaluate, []); + assert.deepEqual(globalThis.__labcolorsFeasibilityCalls.oversize, [657381n]); +}); + +test("compiler does not pass a spoofed Uint8Array length to generated glue", async (t) => { + const compiler = await importCompilerWithInstrumentedWasm(t); + compiler.initSync(); + const request = new Uint8Array([1, 2, 3]); + Object.defineProperty(request, "length", { value: 0 }); + + assert.equal(compiler.evaluateWcag22Feasibility(request).outcome, "success"); + const [forwarded] = globalThis.__labcolorsFeasibilityCalls.evaluate; + assert.equal(forwarded.length, 3); + assert.deepEqual([...forwarded], [1, 2, 3]); +}); + +test("compiler normalizes hostile subclasses and rejects detached views before WASM", async (t) => { + const compiler = await importCompilerWithInstrumentedWasm(t); + compiler.initSync(); + + class HostileUint8Array extends Uint8Array {} + Object.defineProperty(HostileUint8Array.prototype, "length", { + get: () => 0, + }); + const hostile = new HostileUint8Array([4, 5]); + assert.equal(compiler.evaluateWcag22Feasibility(hostile).outcome, "success"); + assert.deepEqual([...globalThis.__labcolorsFeasibilityCalls.evaluate[0]], [4, 5]); + + const detached = new Uint8Array([6]); + structuredClone(detached.buffer, { transfer: [detached.buffer] }); + assert.throws( + () => compiler.evaluateWcag22Feasibility(detached), + /request must be a live Uint8Array/u, + ); + + const normal = new Uint8Array([7]); + assert.equal(compiler.evaluateWcag22Feasibility(normal).outcome, "success"); + assert.deepEqual([...globalThis.__labcolorsFeasibilityCalls.evaluate[1]], [7]); +}); + +test("compiler rejects a detached view before requiring initialized WASM", async (t) => { + const compiler = await importCompilerWithInstrumentedWasm(t); + const detached = new Uint8Array([1]); + structuredClone(detached.buffer, { transfer: [detached.buffer] }); + + assert.throws( + () => compiler.evaluateWcag22Feasibility(detached), + /request must be a live Uint8Array/u, + ); + assert.deepEqual(globalThis.__labcolorsFeasibilityCalls, { + evaluate: [], + max: [], + oversize: [], + }); +}); + +test("compiler freezes a length-tracking shared view at the checked byte length", async (t) => { + const compiler = await importCompilerWithInstrumentedWasm(t); + compiler.initSync(); + const buffer = new SharedArrayBuffer(1, { maxByteLength: 2 }); + const request = new Uint8Array(buffer); + globalThis.__labcolorsFeasibilityBeforeForward = () => buffer.grow(2); + t.after(() => { delete globalThis.__labcolorsFeasibilityBeforeForward; }); + + assert.equal(compiler.evaluateWcag22Feasibility(request).outcome, "success"); + const [forwarded] = globalThis.__labcolorsFeasibilityCalls.evaluate; + assert.equal(buffer.byteLength, 2); + assert.equal(forwarded.length, 1); +}); + +test("compiler rejects non-Uint8Array inputs before touching WASM", async (t) => { + const compiler = await importCompilerWithInstrumentedWasm(t); const invalidInputs = [ ["Array", []], ["Int8Array", new Int8Array()], @@ -107,7 +175,7 @@ test("package root rejects non-Uint8Array inputs before touching WASM", async (t for (const [label, input] of invalidInputs) { assert.throws( - () => root.evaluateWcag22Feasibility(input), + () => compiler.evaluateWcag22Feasibility(input), { name: "TypeError", message: "evaluateWcag22Feasibility request must be a Uint8Array", @@ -121,15 +189,16 @@ test("package root rejects non-Uint8Array inputs before touching WASM", async (t oversize: [], }); - root.initSync(); + compiler.initSync(); const crossRealm = runInNewContext("new Uint8Array([123])"); - assert.equal(root.evaluateWcag22Feasibility(crossRealm).outcome, "success"); - assert.strictEqual(globalThis.__labcolorsFeasibilityCalls.evaluate[0], crossRealm); + assert.equal(compiler.evaluateWcag22Feasibility(crossRealm).outcome, "success"); + assert.deepEqual([...globalThis.__labcolorsFeasibilityCalls.evaluate[0]], [123]); + assert.equal(globalThis.__labcolorsFeasibilityCalls.evaluate[0].buffer, crossRealm.buffer); }); -test("package root derives the envelope ceiling from WASM instead of copying a literal", async () => { - const source = await readFile(new URL("../index.js", import.meta.url), "utf8"); - const declarations = await readFile(new URL("../index.d.ts", import.meta.url), "utf8"); +test("compiler derives the envelope ceiling from WASM instead of copying a literal", async () => { + const source = await readFile(new URL("../compiler.js", import.meta.url), "utf8"); + const declarations = await readFile(new URL("../compiler.d.ts", import.meta.url), "utf8"); assert.doesNotMatch(source, /657380/u); assert.match(source, /wcag22FeasibilityMaxBytes/u); assert.match(source, /wcag22FeasibilityMaxRequestBytesV1/u); @@ -147,7 +216,7 @@ test("package root derives the envelope ceiling from WASM instead of copying a l assert.doesNotMatch( declarations, new RegExp(`\\b${internal},`, "u"), - `${internal} must not widen the curated package-root type menu`, + `${internal} must not widen the curated compiler type menu`, ); } }); @@ -157,10 +226,13 @@ test("feasibility TypeScript is exhaustive and excludes forged/proportional stat t.after(() => rm(fixture, { recursive: true, force: true })); let declarations; try { - declarations = await readFile(new URL("../pkg/labcolors.d.ts", import.meta.url), "utf8"); + declarations = await readFile( + new URL("../compiler/labcolors_compiler.d.ts", import.meta.url), + "utf8", + ); } catch (error) { if (error?.code === "ENOENT") { - throw new Error("pkg/labcolors.d.ts is required; run `npm run build` before tests", { + throw new Error("compiler declarations are required; run `npm run build` before tests", { cause: error, }); } @@ -171,7 +243,12 @@ test("feasibility TypeScript is exhaustive and excludes forged/proportional stat /export function evaluateWcag22FeasibilityV1\(request: Uint8Array\): Wcag22FeasibilityOutcomeV1;/u, "wasm-bindgen must publish the reviewed byte API declaration", ); - await writeFile(join(fixture, "labcolors.d.ts"), declarations); + await mkdir(join(fixture, "compiler")); + await writeFile(join(fixture, "compiler", "labcolors_compiler.d.ts"), declarations); + await writeFile( + join(fixture, "wcag22.d.ts"), + await readFile(new URL("../wcag22.d.ts", import.meta.url)), + ); await writeFile( join(fixture, "consumer.ts"), ` @@ -181,7 +258,7 @@ import { type Wcag22FeasibilityEvaluatedV1, type Wcag22FeasibilityOutcomeV1, type Wcag22FeasibilityRequestV1, -} from "./labcolors.js"; +} from "./compiler/labcolors_compiler.js"; const request: Wcag22FeasibilityRequestV1 = { schemaVersion: 1, @@ -300,14 +377,16 @@ function assertPackedPartition(result) { } } -test("built package root replays pack 5 through an independent packed consumer", async () => { - const glueUrl = new URL("../pkg/labcolors.js", import.meta.url); - const wasmBytes = await readFile(new URL("../pkg/labcolors_bg.wasm", import.meta.url)); +test("built compiler replays pack 5 through an independent packed consumer", async () => { + const glueUrl = new URL("../compiler/labcolors_compiler.js", import.meta.url); + const wasmBytes = await readFile( + new URL("../compiler/labcolors_compiler_bg.wasm", import.meta.url), + ); const raw = await import(glueUrl.href); raw.initSync({ module: new WebAssembly.Module(wasmBytes) }); - const root = await import(`../index.js?feasibility=${Date.now()}`); + const compiler = await import(`../compiler.js?feasibility=${Date.now()}`); assert.equal( - root.wcag22FeasibilityMaxBytes(), + compiler.wcag22FeasibilityMaxBytes(), raw.wcag22FeasibilityMaxRequestBytesV1(), ); @@ -329,7 +408,7 @@ test("built package root replays pack 5 through an independent packed consumer", const encoder = new TextEncoder(); let mutationSubject; for (const vector of vectors) { - const outcome = root.evaluateWcag22Feasibility(encoder.encode(vector.requestJson)); + const outcome = compiler.evaluateWcag22Feasibility(encoder.encode(vector.requestJson)); assert.equal(JSON.stringify(outcome), vector.outcomeJson, `${vector.caseId}: wire drift`); if (outcome.outcome !== "success" || outcome.feasibility.status === "notEvaluated") { continue; diff --git a/packages/colors/wcag22.d.ts b/packages/colors/wcag22.d.ts new file mode 100644 index 00000000..56b6e9e5 --- /dev/null +++ b/packages/colors/wcag22.d.ts @@ -0,0 +1,6 @@ +/** Core-owned exact final-sRGB8 criterion menu shared by both execution roles. */ +export type Wcag22CriterionV1 = + | "sc-1.4.3-text-default" + | "sc-1.4.3-text-large-scale" + | "sc-1.4.11-ui-component-or-state" + | "sc-1.4.11-graphical-object"; diff --git a/scripts/check-wasm-size-budget.mjs b/scripts/check-wasm-size-budget.mjs index d452866c..68ec4f16 100644 --- a/scripts/check-wasm-size-budget.mjs +++ b/scripts/check-wasm-size-budget.mjs @@ -7,16 +7,15 @@ import { fileURLToPath } from "node:url"; import { gzipSync } from "node:zlib"; const SCRIPT_PATH = fileURLToPath(import.meta.url); -const SCRIPT_DIR = dirname(SCRIPT_PATH); -const REPO_ROOT = resolve(SCRIPT_DIR, ".."); -const DEFAULT_WASM = resolve(REPO_ROOT, "packages/colors/pkg/labcolors_bg.wasm"); +const REPO_ROOT = resolve(dirname(SCRIPT_PATH), ".."); const V1_PATH = resolve(REPO_ROOT, "packages/colors/bench/wasm-size-budget-v1.json"); const V2_PATH = resolve(REPO_ROOT, "packages/colors/bench/wasm-size-budget-v2.json"); const V3_PATH = resolve(REPO_ROOT, "packages/colors/bench/wasm-size-budget-v3.json"); +const V4_PATH = resolve(REPO_ROOT, "packages/colors/bench/wasm-size-budget-v4.json"); export const DEFAULT_BUDGET = resolve( REPO_ROOT, - "packages/colors/bench/wasm-size-budget-v4.json", + "packages/colors/bench/wasm-size-budget-v5.json", ); export const V1_FILE_SHA256 = "4f7340fc8cfd0ccb97377c385f2f8d8e7a9ef2c5ba96177f518c5d07de2825e1"; @@ -28,10 +27,29 @@ export const V3_FILE_SHA256 = "d7937612e4c33574a8af28845bb1dd30cca86fc39fc0206cac4c377de77fec15"; export const V4_FILE_SHA256 = "c34fc10404dc7057a53a28592d18342078b5cd0e5dcaa888db482abf3f5fb23c"; +export const V5_FILE_SHA256 = + "e4b53a2eb976a8c66827a559cb81232e359b734dbfb14725da215cb496ff5d59"; const V1_REPOSITORY_PATH = "packages/colors/bench/wasm-size-budget-v1.json"; -const V4_BUDGET_ID = "labcolors-wasm-raw-issue-296-v4"; -const WASM_REPOSITORY_PATH = "packages/colors/pkg/labcolors_bg.wasm"; +const V4_REPOSITORY_PATH = "packages/colors/bench/wasm-size-budget-v4.json"; +const V5_BUDGET_ID = "labcolors-wasm-roles-issue-296-c1-v5"; +const ROLE_ORDER = ["runtime", "compiler"]; +const ROLE_SPECS = { + runtime: { + artifact: "packages/colors/pkg/labcolors_bg.wasm", + command: + "CARGO_ENCODED_RUSTFLAGS= wasm-pack build crates/labcolors-wasm --release --target web --out-dir ../../packages/colors/pkg --out-name labcolors --locked", + recipeSha256: V1_RECIPE_SHA256, + derivation: "exact-accepted-issue-296-slice-c1-runtime-measurement", + }, + compiler: { + artifact: "packages/colors/compiler/labcolors_compiler_bg.wasm", + command: + "CARGO_ENCODED_RUSTFLAGS= wasm-pack build crates/labcolors-compiler --release --target web --out-dir ../../packages/colors/compiler --out-name labcolors_compiler --locked", + recipeSha256: "ce53cea5f579c512a6d2f0c3348f250ac0a5e03206de55e7979c8eae1403be8f", + derivation: "exact-accepted-issue-296-slice-c1-compiler-first-admission", + }, +}; function fail(message) { throw new Error(`WASM size budget: ${message}`); @@ -66,7 +84,7 @@ function positiveSafeInteger(value, label) { } } -function toolchainRecipe(v1) { +function roleRecipe(v1, command) { const measurement = v1.measurement; return { rustToolchain: measurement?.rustToolchain, @@ -79,136 +97,143 @@ function toolchainRecipe(v1) { wasmOptVersion: measurement?.wasmOptVersion, measurementPlatform: measurement?.measurementPlatform, rustPathRemap: measurement?.rustPathRemap, - command: measurement?.command, + command, }; } -function readImmutableBudget(path, expectedSha256, version, budgetId) { +function readImmutableJson(path, expectedSha256, label) { let bytes; try { bytes = readFileSync(path); } catch (error) { - fail(`cannot read immutable ${version} budget ${path}: ${error.message}`); + fail(`cannot read immutable ${label} ${path}: ${error.message}`); } const actualSha256 = sha256(bytes); if (actualSha256 !== expectedSha256) { fail( - `immutable ${version} file SHA-256 mismatch: ` + + `immutable ${label} file SHA-256 mismatch: ` + `expected=${expectedSha256} actual=${actualSha256}`, ); } - let budget; try { - budget = JSON.parse(bytes.toString("utf8")); + return JSON.parse(bytes.toString("utf8")); } catch (error) { - fail(`immutable ${version} budget is not JSON: ${error.message}`); - } - if (budget?.schemaVersion !== 3 || budget?.budgetId !== budgetId) { - fail(`immutable ${version} budget identity drifted`); + fail(`immutable ${label} is not JSON: ${error.message}`); } - return budget; } function verifyImmutableHistory() { - let bytes; - try { - bytes = readFileSync(V1_PATH); - } catch (error) { - fail(`cannot read immutable build recipe ${V1_PATH}: ${error.message}`); - } - if (sha256(bytes) !== V1_FILE_SHA256) { - fail(`immutable v1 file SHA-256 mismatch: expected=${V1_FILE_SHA256} actual=${sha256(bytes)}`); - } - - let v1; - try { - v1 = JSON.parse(bytes.toString("utf8")); - } catch (error) { - fail(`immutable v1 build recipe is not JSON: ${error.message}`); - } - if ( - v1?.schemaVersion !== 2 || - v1?.budgetId !== "labcolors-wasm-raw-issue-284-v1" - ) { + const v1 = readImmutableJson(V1_PATH, V1_FILE_SHA256, "v1"); + if (v1?.schemaVersion !== 2 || v1?.budgetId !== "labcolors-wasm-raw-issue-284-v1") { fail("immutable v1 build recipe identity drifted"); } - const actualRecipeSha256 = sha256(JSON.stringify(toolchainRecipe(v1))); - if (actualRecipeSha256 !== V1_RECIPE_SHA256) { - fail( - `immutable v1 toolchain recipe SHA-256 mismatch: ` + - `expected=${V1_RECIPE_SHA256} actual=${actualRecipeSha256}`, - ); + if (sha256(JSON.stringify(roleRecipe(v1, v1.measurement.command))) !== V1_RECIPE_SHA256) { + fail("immutable v1 runtime recipe projection drifted"); } - readImmutableBudget( - V2_PATH, - V2_FILE_SHA256, - "v2", - "labcolors-wasm-raw-issue-295-v2", - ); - return readImmutableBudget( - V3_PATH, - V3_FILE_SHA256, - "v3", - "labcolors-wasm-raw-issue-296-v3", - ); + const historical = [ + [V2_PATH, V2_FILE_SHA256, "v2", "labcolors-wasm-raw-issue-295-v2"], + [V3_PATH, V3_FILE_SHA256, "v3", "labcolors-wasm-raw-issue-296-v3"], + [V4_PATH, V4_FILE_SHA256, "v4", "labcolors-wasm-raw-issue-296-v4"], + ]; + let v4; + for (const [path, digest, label, budgetId] of historical) { + const value = readImmutableJson(path, digest, label); + if (value?.schemaVersion !== 3 || value?.budgetId !== budgetId) { + fail(`immutable ${label} budget identity drifted`); + } + if (label === "v4") v4 = value; + } + return { v1, v4 }; } function validateBudgetValue(budget) { exactKeys( budget, - ["schemaVersion", "budgetId", "artifact", "buildRecipe", "measurement", "policy"], + [ + "schemaVersion", + "budgetId", + "predecessor", + "toolchainSource", + "buildRecipes", + "roles", + ], "budget", ); - if (budget.schemaVersion !== 3) fail("supported schemaVersion is exactly 3"); - if (budget.budgetId !== V4_BUDGET_ID) fail(`budgetId must be ${V4_BUDGET_ID}`); - if (budget.artifact !== WASM_REPOSITORY_PATH) { - fail(`artifact must be ${WASM_REPOSITORY_PATH}`); - } + if (budget.schemaVersion !== 4) fail("supported schemaVersion is exactly 4"); + if (budget.budgetId !== V5_BUDGET_ID) fail(`budgetId must be ${V5_BUDGET_ID}`); - exactKeys( - budget.buildRecipe, - ["path", "fileSha256", "recipeSha256"], - "buildRecipe", - ); - if (budget.buildRecipe.path !== V1_REPOSITORY_PATH) { - fail(`buildRecipe.path must be ${V1_REPOSITORY_PATH}`); - } - if (budget.buildRecipe.fileSha256 !== V1_FILE_SHA256) { - fail("buildRecipe.fileSha256 must bind the immutable v1 file"); - } - if (budget.buildRecipe.recipeSha256 !== V1_RECIPE_SHA256) { - fail("buildRecipe.recipeSha256 must bind the canonical v1 toolchain projection"); + exactKeys(budget.predecessor, ["path", "fileSha256"], "predecessor"); + if ( + budget.predecessor.path !== V4_REPOSITORY_PATH || + budget.predecessor.fileSha256 !== V4_FILE_SHA256 + ) { + fail("predecessor must bind the immutable v4 document"); } - exactKeys( - budget.measurement, - ["issue", "measurementPlatform", "rawBytes", "sha256"], - "measurement", - ); - if (budget.measurement.issue !== 296) fail("measurement must cite Issue #296"); - if (budget.measurement.measurementPlatform !== "linux-x64") { - fail("measurement.measurementPlatform must be canonical linux-x64"); + exactKeys(budget.toolchainSource, ["path", "fileSha256"], "toolchainSource"); + if ( + budget.toolchainSource.path !== V1_REPOSITORY_PATH || + budget.toolchainSource.fileSha256 !== V1_FILE_SHA256 + ) { + fail("toolchainSource must bind the immutable v1 document"); } - positiveSafeInteger(budget.measurement.rawBytes, "measurement.rawBytes"); - lowercaseDigest(budget.measurement.sha256, "measurement.sha256"); - exactKeys(budget.policy, ["maxRawBytes", "derivation", "gzip"], "policy"); - positiveSafeInteger(budget.policy.maxRawBytes, "policy.maxRawBytes"); - if (budget.policy.maxRawBytes !== budget.measurement.rawBytes) { - fail("current ceiling must equal the exact accepted measurement (zero arbitrary headroom)"); - } - if (budget.policy.derivation !== "exact-accepted-issue-296-slice-b-measurement") { - fail("policy.derivation must cite the exact accepted Issue #296 Slice B measurement"); - } - if (budget.policy.gzip !== "diagnostic-only") { - fail("gzip must remain diagnostic-only across implementations"); + exactKeys(budget.buildRecipes, ROLE_ORDER, "buildRecipes"); + exactKeys(budget.roles, ROLE_ORDER, "roles"); + const { v1, v4 } = verifyImmutableHistory(); + + for (const role of ROLE_ORDER) { + const spec = ROLE_SPECS[role]; + const recipe = budget.buildRecipes[role]; + exactKeys(recipe, ["command", "recipeSha256"], `buildRecipes.${role}`); + if (recipe.command !== spec.command) fail(`${role} build command drifted`); + lowercaseDigest(recipe.recipeSha256, `buildRecipes.${role}.recipeSha256`); + const actualRecipeSha256 = sha256(JSON.stringify(roleRecipe(v1, recipe.command))); + if ( + recipe.recipeSha256 !== spec.recipeSha256 || + recipe.recipeSha256 !== actualRecipeSha256 + ) { + fail(`${role} build recipe SHA-256 does not bind the declared command and toolchain`); + } + + const record = budget.roles[role]; + exactKeys(record, ["artifact", "measurement", "policy"], `roles.${role}`); + if (record.artifact !== spec.artifact) { + fail(`roles.${role}.artifact must be ${spec.artifact}`); + } + exactKeys( + record.measurement, + ["issue", "slice", "measurementPlatform", "rawBytes", "sha256"], + `roles.${role}.measurement`, + ); + if (record.measurement.issue !== 296 || record.measurement.slice !== "C1") { + fail(`roles.${role}.measurement must cite Issue #296 Slice C1`); + } + if (record.measurement.measurementPlatform !== "linux-x64") { + fail(`roles.${role}.measurement must use canonical linux-x64`); + } + positiveSafeInteger(record.measurement.rawBytes, `roles.${role}.measurement.rawBytes`); + lowercaseDigest(record.measurement.sha256, `roles.${role}.measurement.sha256`); + + exactKeys(record.policy, ["maxRawBytes", "derivation", "gzip"], `roles.${role}.policy`); + positiveSafeInteger(record.policy.maxRawBytes, `roles.${role}.policy.maxRawBytes`); + if (record.policy.maxRawBytes !== record.measurement.rawBytes) { + fail(`${role} ceiling must equal its exact measurement (zero arbitrary headroom)`); + } + if (record.policy.derivation !== spec.derivation) { + fail(`${role} policy derivation drifted`); + } + if (record.policy.gzip !== "diagnostic-only") { + fail(`${role} gzip measurement must remain diagnostic-only`); + } } - const previous = verifyImmutableHistory(); - positiveSafeInteger(previous.policy?.maxRawBytes, "immutable v3 policy.maxRawBytes"); - if (budget.policy.maxRawBytes > previous.policy.maxRawBytes) { - fail("current ceiling must not exceed the immutable v3 ratchet"); + if (budget.roles.runtime.policy.maxRawBytes > v1.policy.maxRawBytes) { + fail("runtime role must not exceed the immutable same-capability pre-compiler ceiling"); + } + if (budget.roles.runtime.policy.maxRawBytes > v4.policy.maxRawBytes) { + fail("runtime role must not regress the immutable immediate predecessor ceiling"); } } @@ -227,10 +252,10 @@ export function parseBudgetDocument(bytes, budgetPath) { validateBudgetValue(budget); if (resolve(budgetPath) === DEFAULT_BUDGET) { const actualFileSha256 = sha256(document); - if (actualFileSha256 !== V4_FILE_SHA256) { + if (actualFileSha256 !== V5_FILE_SHA256) { fail( - `immutable v4 file SHA-256 mismatch: ` + - `expected=${V4_FILE_SHA256} actual=${actualFileSha256}`, + `immutable v5 file SHA-256 mismatch: ` + + `expected=${V5_FILE_SHA256} actual=${actualFileSha256}`, ); } } @@ -238,63 +263,50 @@ export function parseBudgetDocument(bytes, budgetPath) { } function readBudget(path) { - let bytes; try { - bytes = readFileSync(path); + return parseBudgetDocument(readFileSync(path), path); } catch (error) { + if (error instanceof Error && error.message.startsWith("WASM size budget:")) throw error; fail(`cannot read ${path}: ${error.message}`); } - return parseBudgetDocument(bytes, path); -} - -function pathsFromArgs(args) { - let wasm = DEFAULT_WASM; - let budget = DEFAULT_BUDGET; - for (let index = 0; index < args.length; index += 2) { - const flag = args[index]; - const value = args[index + 1]; - if (value === undefined) fail(`${flag ?? "argument"} requires a path`); - if (flag === "--wasm") wasm = resolve(value); - else if (flag === "--budget") budget = resolve(value); - else fail(`unknown argument ${flag}`); - } - return { wasm, budget }; } -export function evaluateWasmBudget(budget, wasm, currentPlatform) { +export function evaluateWasmBudget(role, record, wasm, currentPlatform) { + if (!ROLE_ORDER.includes(role)) fail(`unknown execution role ${role}`); const bytes = Buffer.isBuffer(wasm) ? wasm : Buffer.from(wasm); if ( bytes.length < 8 || !bytes.subarray(0, 4).equals(Buffer.from([0, 97, 115, 109])) ) { - fail("artifact is not a WebAssembly binary"); + fail(`${role} artifact is not a WebAssembly binary`); } const rawBytes = bytes.length; const gzipBytes = gzipSync(bytes, { level: 9 }).length; const artifactSha256 = sha256(bytes); - const artifactSha = artifactSha256 === budget.measurement.sha256 ? "match" : "different"; - const isCanonicalPlatform = currentPlatform === budget.measurement.measurementPlatform; - if (isCanonicalPlatform && rawBytes !== budget.measurement.rawBytes) { + const artifactSha = artifactSha256 === record.measurement.sha256 ? "match" : "different"; + const isCanonicalPlatform = currentPlatform === record.measurement.measurementPlatform; + if (isCanonicalPlatform && rawBytes !== record.measurement.rawBytes) { fail( - `exact artifact length mismatch on ${currentPlatform}: ` + - `expected=${budget.measurement.rawBytes}B actual=${rawBytes}B; ` + + `${role} exact artifact length mismatch on ${currentPlatform}: ` + + `expected=${record.measurement.rawBytes}B actual=${rawBytes}B; ` + `gzip=${gzipBytes}B diagnostic-only sha256=${artifactSha256}`, ); } if (isCanonicalPlatform && artifactSha !== "match") { fail( - `exact artifact SHA-256 mismatch on ${currentPlatform}: ` + - `expected=${budget.measurement.sha256} actual=${artifactSha256}; ` + + `${role} exact artifact SHA-256 mismatch on ${currentPlatform}: ` + + `expected=${record.measurement.sha256} actual=${artifactSha256}; ` + `raw=${rawBytes}B gzip=${gzipBytes}B diagnostic-only`, ); } return { + role, status: isCanonicalPlatform ? "PASS" : "DIAGNOSTIC", rawBytes, - maxRawBytes: budget.policy.maxRawBytes, - deltaBytes: rawBytes - budget.policy.maxRawBytes, + maxRawBytes: record.policy.maxRawBytes, + deltaBytes: rawBytes - record.policy.maxRawBytes, gzipBytes, currentPlatform, artifactSha, @@ -305,29 +317,50 @@ export function evaluateWasmBudget(budget, wasm, currentPlatform) { function formatResult(result, artifact) { const delta = `${result.deltaBytes >= 0 ? "+" : ""}${result.deltaBytes}`; return ( - `WASM size budget ${result.status} raw=${result.rawBytes}B ` + + `WASM size budget ${result.status} role=${result.role} raw=${result.rawBytes}B ` + `ceiling=${result.maxRawBytes}B delta=${delta}B gzip=${result.gzipBytes}B ` + `diagnostic-only platform=${result.currentPlatform} artifact=${artifact} ` + `artifact-sha=${result.artifactSha} recipe-sha=match` ); } +function pathsFromArgs(args) { + const paths = { + budget: DEFAULT_BUDGET, + runtime: resolve(REPO_ROOT, ROLE_SPECS.runtime.artifact), + compiler: resolve(REPO_ROOT, ROLE_SPECS.compiler.artifact), + }; + for (let index = 0; index < args.length; index += 2) { + const flag = args[index]; + const value = args[index + 1]; + if (value === undefined) fail(`${flag ?? "argument"} requires a path`); + if (flag === "--budget") paths.budget = resolve(value); + else if (flag === "--runtime-wasm") paths.runtime = resolve(value); + else if (flag === "--compiler-wasm") paths.compiler = resolve(value); + else fail(`unknown argument ${flag}`); + } + return paths; +} + function main(args) { - const { wasm: wasmPath, budget: budgetPath } = pathsFromArgs(args); - const budget = readBudget(budgetPath); - let wasm; - try { - wasm = readFileSync(wasmPath); - } catch (error) { - fail(`cannot read ${wasmPath}: ${error.message}`); + const paths = pathsFromArgs(args); + const budget = readBudget(paths.budget); + for (const role of ROLE_ORDER) { + let wasm; + try { + wasm = readFileSync(paths[role]); + } catch (error) { + fail(`cannot read ${role} artifact ${paths[role]}: ${error.message}`); + } + const result = evaluateWasmBudget( + role, + budget.roles[role], + wasm, + `${process.platform}-${process.arch}`, + ); + const artifact = relative(REPO_ROOT, paths[role]).replaceAll("\\", "/"); + console.log(formatResult(result, artifact)); } - const artifact = relative(REPO_ROOT, wasmPath).replaceAll("\\", "/"); - const result = evaluateWasmBudget( - budget, - wasm, - `${process.platform}-${process.arch}`, - ); - console.log(formatResult(result, artifact)); } if (process.argv[1] !== undefined && resolve(process.argv[1]) === SCRIPT_PATH) { diff --git a/scripts/check_wcag22_feasibility_benchmark.py b/scripts/check_wcag22_feasibility_benchmark.py index 277d6feb..c07ec802 100644 --- a/scripts/check_wcag22_feasibility_benchmark.py +++ b/scripts/check_wcag22_feasibility_benchmark.py @@ -29,7 +29,7 @@ DEFAULT_ARTIFACT = Path( - "/private/tmp/labcolors-wcag22-feasibility-admission-raw-v3.json" + "/private/tmp/labcolors-wcag22-feasibility-admission-raw-v4.json" ) HEX_256 = re.compile(r"[0-9a-f]{64}") GIT_OBJECT = re.compile(r"[0-9a-f]{40}") @@ -821,7 +821,7 @@ def check_environment( require(isinstance(environment, dict) and environment.get("execution") == "native-process", "environment must identify native-process execution") require(set(environment) == ENVIRONMENT_FIELDS, - "environment fields drifted from the exact V3 schema") + "environment fields drifted from the exact V4 schema") require(environment.get("allocator") == "std::alloc::System", "allocator provenance must identify the measured global allocator") require(environment.get("allocatorInstrumentationIncludedInElapsedTime") is True, @@ -911,7 +911,7 @@ def check( source_before = dependency_cone_snapshot() require(isinstance(payload, dict), "artifact root must be an object") require(payload.get("schemaVersion") == 1, "unsupported benchmark schemaVersion") - require(payload.get("artifactId") == "wcag22-feasibility-admission-raw-v3", + require(payload.get("artifactId") == "wcag22-feasibility-admission-raw-v4", "unexpected benchmark artifactId") require( payload.get("claimBoundary") diff --git a/scripts/docs-drift.test.mjs b/scripts/docs-drift.test.mjs index 5b7b7524..b943f81d 100644 --- a/scripts/docs-drift.test.mjs +++ b/scripts/docs-drift.test.mjs @@ -7,9 +7,10 @@ */ import assert from 'node:assert/strict'; -import { readFileSync } from 'node:fs'; +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { test } from 'node:test'; import { join } from 'node:path'; +import { tmpdir } from 'node:os'; import { ROOT, @@ -17,6 +18,7 @@ import { crateName, fileStem, lawForFile, + nonLawFiles, workspaceCrates, workspaceMembers, } from './naming-inventory.mjs'; @@ -69,6 +71,69 @@ test('lawForFile: по-доменные законы', () => { assert.ok(!lawForFile('crates/x/tests/data/labui.config.prod.json')); }); +test('generated WASM names come from package files without hiding undeclared source', () => { + const root = mkdtempSync(join(tmpdir(), 'labcolors-naming-generated-')); + try { + const compiler = join(root, 'packages', 'colors', 'compiler'); + const runtime = join(root, 'packages', 'colors', 'pkg'); + mkdirSync(compiler, { recursive: true }); + mkdirSync(runtime, { recursive: true }); + writeFileSync( + join(root, 'packages', 'colors', 'package.json'), + JSON.stringify({ + files: [ + 'compiler/labcolors_compiler.js', + 'compiler/labcolors_compiler_bg.wasm', + 'compiler/not_built_generated_bg.wasm', + 'pkg/labcolors.js', + ], + }), + ); + writeFileSync(join(compiler, 'labcolors_compiler.js'), 'generated'); + writeFileSync(join(compiler, 'labcolors_compiler_bg.wasm'), 'generated'); + writeFileSync(join(compiler, 'hand_written_bad.js'), 'source'); + writeFileSync(join(runtime, 'labcolors.js'), 'generated'); + writeFileSync(join(runtime, 'hand_written_runtime_bad.js'), 'source'); + assert.deepEqual(nonLawFiles(root), [ + 'packages/colors/compiler/hand_written_bad.js', + 'packages/colors/compiler/labcolors_compiler.js', + 'packages/colors/compiler/labcolors_compiler_bg.wasm', + 'packages/colors/compiler/not_built_generated_bg.wasm', + 'packages/colors/pkg/hand_written_runtime_bad.js', + ]); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test('generated package directory entries are naming roots, not blind spots', () => { + const root = mkdtempSync(join(tmpdir(), 'labcolors-naming-generated-roots-')); + try { + const compiler = join(root, 'packages', 'colors', 'compiler'); + const runtime = join(root, 'packages', 'colors', 'pkg'); + mkdirSync(compiler, { recursive: true }); + mkdirSync(runtime, { recursive: true }); + writeFileSync( + join(root, 'packages', 'colors', 'package.json'), + JSON.stringify({ files: ['compiler', 'pkg'] }), + ); + writeFileSync(join(compiler, 'labcolors_compiler.js'), 'generated'); + writeFileSync(join(runtime, 'labcolors.js'), 'generated'); + writeFileSync(join(runtime, '.hidden_runtime_bad.js'), 'extra'); + mkdirSync(join(runtime, 'dist')); + writeFileSync(join(runtime, 'dist', 'nested_runtime_bad.js'), 'extra'); + writeFileSync(join(runtime, 'unexpected_runtime_bad.js'), 'extra'); + assert.deepEqual(nonLawFiles(root), [ + 'packages/colors/compiler/labcolors_compiler.js', + 'packages/colors/pkg/.hidden_runtime_bad.js', + 'packages/colors/pkg/dist/nested_runtime_bad.js', + 'packages/colors/pkg/unexpected_runtime_bad.js', + ]); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + test('workspaceMembers разворачивает глоб crates/* по ФС', () => { const members = workspaceMembers(ROOT); assert.ok(members.includes('crates/labcolors-core')); @@ -298,6 +363,8 @@ test('breaking exact-alpha/glow контракт имеет migration и не о 'appearanceDiagnosticProfile', 'selectionDiagnosticProfile', 'resolve_alpha_analog_hex', + 'dedicated module Worker', + 'двух ролей не подходит', 'Rollback', ]) { assert.ok(migration.includes(required), `migration не содержит ${required}`); @@ -307,6 +374,32 @@ test('breaking exact-alpha/glow контракт имеет migration и не о assert.doesNotMatch(adr, /referenceProfile/); assert.doesNotMatch(readme, /\bdiagnosticProfile\b/); assert.doesNotMatch(adr, /\bdiagnosticProfile\b/); + assert.match( + readme, + /new Worker\(new URL\("\.\/color-compiler\.worker\.ts"[\s\S]*?worker\.terminate\(\)/, + ); + const workerReady = readme.indexOf('self.postMessage({ type: "ready" } as const)'); + const mainMessageHandler = readme.indexOf( + 'worker.addEventListener("message"', + workerReady, + ); + const readyBranch = readme.indexOf( + 'if (data?.type === "ready")', + mainMessageHandler, + ); + const requestPost = readme.indexOf('worker.postMessage(request)', readyBranch); + const outcomeResolve = readme.indexOf('resolve(data)', requestPost); + assert.ok(workerReady >= 0, 'compiler Worker не сообщает ready после init'); + assert.ok(mainMessageHandler > workerReady, 'main listener должен ждать ready'); + assert.ok(readyBranch > mainMessageHandler, 'main не различает ready и outcome'); + assert.ok(requestPost > readyBranch, 'request нельзя отправлять до ready'); + assert.ok(outcomeResolve > requestPost, 'listener обязан дождаться outcome после ready'); + assert.doesNotMatch( + readme.slice(mainMessageHandler, outcomeResolve), + /\{ once: true \}/, + 'ready не должен снимать listener до получения outcome', + ); + assert.doesNotMatch(migration, /Promise\.all\(\[initRuntime\(\), initCompiler\(\)\]\)/); }); test('exact-alpha migration экранирует absolute-value pipes внутри Markdown tables', () => { diff --git a/scripts/naming-inventory.mjs b/scripts/naming-inventory.mjs index d4d4d7ae..364acba5 100644 --- a/scripts/naming-inventory.mjs +++ b/scripts/naming-inventory.mjs @@ -26,17 +26,23 @@ import { fileURLToPath, pathToFileURL } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); export const ROOT = join(__dirname, '..'); -/** Генерируемое/чужое — не инвентарь. `pkg` — артефакт wasm-pack. */ +/** Генерируемое/чужое вне публичного package-контракта — не инвентарь. */ const SKIP_DIRS = new Set([ 'node_modules', 'target', '.git', - 'pkg', 'dist', '.build', 'coverage', ]); +// Directory entries in package.json#files own their full tree. Source-only +// build-directory and dotfile skips must not create publish blind spots. +const PACKAGE_ROOT_WALK = Object.freeze({ + includeDotEntries: true, + skipDirs: new Set(['node_modules', '.git']), +}); + /** Директории, по которым бежит скан закона имён. */ export const SCAN_TOPS = [ 'crates', @@ -57,13 +63,14 @@ const TOOL_FIXED = new Set([ 'LICENSE', ]); -/** Рекурсивный список файлов (POSIX-пути относительно base, дотфайлы мимо). */ -export function walk(dir, out = [], base = dir) { +/** Рекурсивный список файлов (POSIX-пути относительно base). */ +export function walk(dir, out = [], base = dir, options = {}) { if (!existsSync(dir)) return out; + const { includeDotEntries = false, skipDirs = SKIP_DIRS } = options; for (const e of readdirSync(dir, { withFileTypes: true })) { - if (e.name.startsWith('.')) continue; + if (!includeDotEntries && e.name.startsWith('.')) continue; if (e.isDirectory()) { - if (!SKIP_DIRS.has(e.name)) walk(join(dir, e.name), out, base); + if (!skipDirs.has(e.name)) walk(join(dir, e.name), out, base, options); } else { out.push(relative(base, join(dir, e.name)).replaceAll('\\', '/')); } @@ -188,13 +195,34 @@ export function lawForFile(path) { /** Файлы SCAN_TOPS вне по-доменного закона имён. */ export function nonLawFiles(root = ROOT) { - const bad = []; + const packageRoot = join(root, 'packages', 'colors'); + const packageJson = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8')); + const generatedPackageFiles = new Set(); + for (const declaredPath of packageJson.files ?? []) { + const packagePath = declaredPath.replace(/^\.\//, '').replace(/\/+$/, ''); + if (packagePath === 'pkg' || packagePath === 'compiler') { + const generatedRoot = join(packageRoot, packagePath); + for (const file of walk(generatedRoot, [], generatedRoot, PACKAGE_ROOT_WALK)) { + generatedPackageFiles.add(`packages/colors/${packagePath}/${file}`); + } + } else if (/^(?:pkg|compiler)\//.test(packagePath)) { + generatedPackageFiles.add(`packages/colors/${packagePath}`); + } + } + const bad = new Set(); for (const top of SCAN_TOPS) { for (const f of walk(join(root, top))) { - if (!lawForFile(f)) bad.push(`${top}/${f}`); + const repositoryPath = `${top}/${f}`; + // Declared generated outputs are package-contract facts, not source-tree + // facts. An undeclared file beside them still passes through the law. + if (generatedPackageFiles.has(repositoryPath)) continue; + if (!lawForFile(f)) bad.add(repositoryPath); } } - return bad.sort(); + for (const path of generatedPackageFiles) { + if (!lawForFile(path)) bad.add(path); + } + return [...bad].sort(); } /* ------------------------------------------------------------------ */ diff --git a/scripts/prepare-npm-package.mjs b/scripts/prepare-npm-package.mjs index cd3fca61..e0d643c9 100644 --- a/scripts/prepare-npm-package.mjs +++ b/scripts/prepare-npm-package.mjs @@ -110,19 +110,27 @@ export async function prepareNpmPackage() { } } - const [packageJsonSource, cargoSource, conformanceSource, wasm, ...familyBytes] = + const [ + packageJsonSource, + cargoSource, + conformanceSource, + runtimeWasm, + compilerWasm, + ...familyBytes + ] = await Promise.all([ readFile(resolve(PACKAGE_DIR, "package.json"), "utf8"), readFile(resolve(REPO_ROOT, "Cargo.toml"), "utf8"), readFile(resolve(CONFORMANCE_DIR, "manifest.json"), "utf8"), readFile(resolve(PACKAGE_DIR, "pkg/labcolors_bg.wasm")), + readFile(resolve(PACKAGE_DIR, "compiler/labcolors_compiler_bg.wasm")), ...CONFORMANCE_FILES.map((file) => readFile(resolve(CONFORMANCE_DIR, file))), ]); const packageJson = JSON.parse(packageJsonSource); const conformance = JSON.parse(conformanceSource); const coreVersion = workspaceVersion(cargoSource); const metadata = { - schemaVersion: 1, + schemaVersion: 2, package: { name: packageJson.name, version: packageJson.version }, sourceSha, coreVersion, @@ -132,7 +140,20 @@ export async function prepareNpmPackage() { manifestSha256: sha256(Buffer.from(conformanceSource)), familySetSha256: sha256(Buffer.concat(familyBytes)), }, - wasm: { bytes: wasm.length, sha256: sha256(wasm) }, + wasm: [ + { + role: "runtime", + path: "pkg/labcolors_bg.wasm", + bytes: runtimeWasm.length, + sha256: sha256(runtimeWasm), + }, + { + role: "compiler", + path: "compiler/labcolors_compiler_bg.wasm", + bytes: compilerWasm.length, + sha256: sha256(compilerWasm), + }, + ], }; await atomicWrite(BUILD_METADATA, `${JSON.stringify(metadata, null, 2)}\n`); diff --git a/scripts/verify-package-release.mjs b/scripts/verify-package-release.mjs index ebe2de3d..c766e5a0 100644 --- a/scripts/verify-package-release.mjs +++ b/scripts/verify-package-release.mjs @@ -39,7 +39,8 @@ const CONFORMANCE_FAMILY_FILES = [ "wcag22.json", "wcag22-feasibility.json", ]; -const WASM_PATH = resolve(PACKAGE_DIR, "pkg/labcolors_bg.wasm"); +const RUNTIME_WASM_PATH = resolve(PACKAGE_DIR, "pkg/labcolors_bg.wasm"); +const COMPILER_WASM_PATH = resolve(PACKAGE_DIR, "compiler/labcolors_compiler_bg.wasm"); const REQUIRED_PACK_FILES = ["package.json", "README.md", "LICENSE"]; const FORBIDDEN_PACK_SEGMENTS = new Set([ @@ -248,7 +249,7 @@ export function validateBuildMetadata( { packageJson, source, coreVersion, conformanceEvidence, wasm }, ) { const expected = { - schemaVersion: 1, + schemaVersion: 2, package: { name: packageJson.name, version: packageJson.version }, sourceSha: source, coreVersion, @@ -258,7 +259,10 @@ export function validateBuildMetadata( manifestSha256: conformanceEvidence.manifestSha256, familySetSha256: conformanceEvidence.familySetSha256, }, - wasm: { bytes: wasm.bytes, sha256: wasm.sha256 }, + wasm: [ + { role: "runtime", ...wasm.runtime }, + { role: "compiler", ...wasm.compiler }, + ], }; if (!isDeepStrictEqual(metadata, expected)) { fail( @@ -1247,7 +1251,7 @@ async function wcag22FeasibilitySmokeFixture() { }; } -function runtimeSmokeSource(feasibilityFixture) { +function runtimeSmokeSource() { return String.raw` import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; @@ -1256,9 +1260,7 @@ import { createRequire } from "node:module"; import init, { LabColors, evaluateWcag22, - evaluateWcag22Feasibility, numericalCapabilityManifest, - wcag22FeasibilityMaxBytes, } from "@labpics/colors"; const require = createRequire(import.meta.url); @@ -1273,17 +1275,12 @@ assert.deepEqual(metadata.package, { }); assert.match(metadata.sourceSha, /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/u); assert.match(metadata.coreVersion, /^\d+\.\d+\.\d+$/u); -assert.equal(metadata.wasm.bytes, (await readFile(wasmPath)).length); +assert.deepEqual(metadata.wasm.map(({ role }) => role), ["runtime", "compiler"]); +const runtimeWasm = metadata.wasm.find(({ role }) => role === "runtime"); +assert.deepEqual(runtimeWasm.path, "pkg/labcolors_bg.wasm"); +assert.equal(runtimeWasm.bytes, (await readFile(wasmPath)).length); await init({ module_or_path: await readFile(wasmPath) }); -const feasibilityFixture = ${JSON.stringify(feasibilityFixture)}; -const feasibilityRequest = new TextEncoder().encode(feasibilityFixture.requestJson); -assert.ok(feasibilityRequest.byteLength <= wcag22FeasibilityMaxBytes()); -assert.equal( - JSON.stringify(evaluateWcag22Feasibility(feasibilityRequest)), - feasibilityFixture.outcomeJson, -); - const capability = numericalCapabilityManifest(); assert.equal(capability.schemaVersion, 2); assert.ok(capability.sites.some((site) => @@ -1465,14 +1462,42 @@ for (const key of [ `; } +function compilerSmokeSource(feasibilityFixture) { + return String.raw` +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import { createRequire } from "node:module"; + +import init, { + evaluateWcag22Feasibility, + wcag22FeasibilityMaxBytes, +} from "@labpics/colors/compiler"; + +const require = createRequire(import.meta.url); +const wasmPath = require.resolve("@labpics/colors/compiler/wasm"); +const metadataPath = require.resolve("@labpics/colors/build-metadata.json"); +const metadata = JSON.parse(await readFile(metadataPath, "utf8")); +const compilerWasm = metadata.wasm.find(({ role }) => role === "compiler"); +assert.deepEqual(compilerWasm.path, "compiler/labcolors_compiler_bg.wasm"); +assert.equal(compilerWasm.bytes, (await readFile(wasmPath)).length); +await init({ module_or_path: await readFile(wasmPath) }); + +const feasibilityFixture = ${JSON.stringify(feasibilityFixture)}; +const feasibilityRequest = new TextEncoder().encode(feasibilityFixture.requestJson); +assert.ok(feasibilityRequest.byteLength <= wcag22FeasibilityMaxBytes()); +assert.equal( + JSON.stringify(evaluateWcag22Feasibility(feasibilityRequest)), + feasibilityFixture.outcomeJson, +); +`; +} + function typeSmokeSource() { return String.raw` import init, { LabColors, evaluateWcag22, - evaluateWcag22Feasibility, numericalCapabilityManifest, - wcag22FeasibilityMaxBytes, type GlowDecisionGuaranteeV1, type GlowDeterminateRole, type GlowDeterminateRoleBase, @@ -1488,9 +1513,13 @@ import init, { type TranslucentRole, type Wcag22AssessmentV1, type Wcag22CriterionV1, +} from "@labpics/colors"; +import { + evaluateWcag22Feasibility, + wcag22FeasibilityMaxBytes, type Wcag22FeasibilityOutcomeV1, type Wcag22FeasibilityRequestV1, -} from "@labpics/colors"; +} from "@labpics/colors/compiler"; import { applyTheme } from "@labpics/colors/apply-theme"; import { watchTheme, @@ -1863,9 +1892,22 @@ async function verifyCleanConsumer( "clean-installed package", ); - const installedWasm = await readFile(resolve(installed, "pkg/labcolors_bg.wasm")); - if (sha256(installedWasm) !== expectedBuildMetadata.wasm.sha256) { - fail("clean-installed WASM bytes differ from the packed release input"); + const expectedWasm = new Map( + expectedBuildMetadata.wasm.map((artifact) => [artifact.role, artifact]), + ); + for (const [role, path] of [ + ["runtime", "pkg/labcolors_bg.wasm"], + ["compiler", "compiler/labcolors_compiler_bg.wasm"], + ]) { + const expected = expectedWasm.get(role); + const installedWasm = await readFile(resolve(installed, path)); + if ( + expected?.path !== path || + expected.bytes !== installedWasm.length || + expected.sha256 !== sha256(installedWasm) + ) { + fail(`clean-installed ${role} WASM differs from the packed release input`); + } } const installedBuildMetadata = await readJson(resolve(installed, "build-metadata.json")); if (!isDeepStrictEqual(installedBuildMetadata, expectedBuildMetadata)) { @@ -1873,12 +1915,15 @@ async function verifyCleanConsumer( } const feasibilityFixture = await wcag22FeasibilitySmokeFixture(); - const runtimePath = resolve(consumer, "smoke.mjs"); + const runtimePath = resolve(consumer, "runtime-smoke.mjs"); + const compilerPath = resolve(consumer, "compiler-smoke.mjs"); const typesPath = resolve(consumer, "smoke.ts"); - await writeFile(runtimePath, runtimeSmokeSource(feasibilityFixture)); + await writeFile(runtimePath, runtimeSmokeSource()); + await writeFile(compilerPath, compilerSmokeSource(feasibilityFixture)); await writeFile(typesPath, typeSmokeSource()); command(process.execPath, [runtimePath], consumer); + command(process.execPath, [compilerPath], consumer); for (const compiler of typescriptCompilers) { command( process.execPath, @@ -1907,6 +1952,62 @@ async function verifyCleanConsumer( consumer, ); } + + await verifyPackedRoleIsolation( + tarballPath, + packageJson, + "runtime", + runtimeSmokeSource(), + ); + await verifyPackedRoleIsolation( + tarballPath, + packageJson, + "compiler", + compilerSmokeSource(feasibilityFixture), + ); + } finally { + await rm(consumer, { recursive: true, force: true }); + } +} + +async function verifyPackedRoleIsolation(tarballPath, packageJson, role, smokeSource) { + const consumer = await mkdtemp(join(tmpdir(), `labcolors-${role}-isolation-`)); + try { + await writeFile( + join(consumer, "package.json"), + `${JSON.stringify({ private: true, type: "module" }, null, 2)}\n`, + ); + const installed = resolve(consumer, "node_modules", ...packageJson.name.split("/")); + await mkdir(installed, { recursive: true }); + command("tar", ["-xzf", tarballPath, "--strip-components=1", "-C", installed]); + + if (role === "runtime") { + await rm(resolve(installed, "compiler"), { recursive: true, force: true }); + await rm(resolve(installed, "compiler.js"), { force: true }); + await rm(resolve(installed, "compiler.d.ts"), { force: true }); + } else if (role === "compiler") { + await rm(resolve(installed, "pkg"), { recursive: true, force: true }); + for (const file of [ + "index.js", + "index.d.ts", + "apply-theme.js", + "apply-theme.d.ts", + "watch-theme.js", + "watch-theme.d.ts", + "adapt-theme.js", + "adapt-theme.d.ts", + "effective-bg.js", + "effective-bg.d.ts", + ]) { + await rm(resolve(installed, file), { force: true }); + } + } else { + fail(`unknown isolated execution role: ${role}`); + } + + const smokePath = resolve(consumer, "smoke.mjs"); + await writeFile(smokePath, smokeSource); + command(process.execPath, [smokePath], consumer); } finally { await rm(consumer, { recursive: true, force: true }); } @@ -1915,9 +2016,9 @@ async function verifyCleanConsumer( // Execute the same packed-package runtime smoke under the caller's Node binary. // CI uses this to prove the public consumer floor independently from the pinned // release packer. -export async function smokePackedRuntime(tarballPath) { +export async function smokePackedPackage(tarballPath) { const tarball = resolve(tarballPath); - const consumer = await mkdtemp(join(tmpdir(), "labcolors-runtime-smoke-")); + const consumer = await mkdtemp(join(tmpdir(), "labcolors-package-smoke-")); try { await writeFile( join(consumer, "package.json"), @@ -1936,10 +2037,13 @@ export async function smokePackedRuntime(tarballPath) { ], consumer, ); - const feasibilityFixture = await wcag22FeasibilitySmokeFixture(); const runtimePath = resolve(consumer, "smoke.mjs"); - await writeFile(runtimePath, runtimeSmokeSource(feasibilityFixture)); + const compilerPath = resolve(consumer, "compiler-smoke.mjs"); + const feasibilityFixture = await wcag22FeasibilitySmokeFixture(); + await writeFile(runtimePath, runtimeSmokeSource()); + await writeFile(compilerPath, compilerSmokeSource(feasibilityFixture)); command(process.execPath, [runtimePath], consumer); + command(process.execPath, [compilerPath], consumer); } finally { await rm(consumer, { recursive: true, force: true }); } @@ -1979,11 +2083,18 @@ export async function verifyPackageRelease() { } const conformanceEvidence = await validateConformance(conformance); - const wasmBytes = await readFile(WASM_PATH); - if (wasmBytes.length < 8 || !wasmBytes.subarray(0, 4).equals(Buffer.from([0, 97, 115, 109]))) { - fail("pkg/labcolors_bg.wasm is absent or has no WebAssembly magic header"); + const wasmPaths = { + runtime: [RUNTIME_WASM_PATH, "pkg/labcolors_bg.wasm"], + compiler: [COMPILER_WASM_PATH, "compiler/labcolors_compiler_bg.wasm"], + }; + const wasm = {}; + for (const [role, [path, displayPath]] of Object.entries(wasmPaths)) { + const bytes = await readFile(path); + if (bytes.length < 8 || !bytes.subarray(0, 4).equals(Buffer.from([0, 97, 115, 109]))) { + fail(`${displayPath} is absent or has no WebAssembly magic header`); + } + wasm[role] = await hashedArtifact(path, displayPath); } - const wasm = await hashedArtifact(WASM_PATH, "pkg/labcolors_bg.wasm"); const buildMetadataValue = await readJson(BUILD_METADATA); validateBuildMetadata(buildMetadataValue, { packageJson, @@ -2033,11 +2144,9 @@ export async function verifyPackageRelease() { ); const manifest = { - // Схема release-manifest v2: numericalSites (pack 2.x, прозаические - // research-поля) заменён на numericalCapabilities — typed capability - // projection ядра с независимо пересчитанным checksum. Read-back в - // publish-workflow пиняет ровно эту версию. - schemaVersion: 2, + // V3 makes the two execution-role WASM records explicit. The publish + // read-back validates both records against bytes inside the exact tarball. + schemaVersion: 3, npm: packageJson.version, core: coreVersion, wire: { @@ -2086,7 +2195,14 @@ export async function verifyPackageRelease() { "spatial-glow-field", "display-p3", ], - artifacts: { tarball, wasm, buildMetadata }, + artifacts: { + tarball, + wasm: [ + { role: "runtime", ...wasm.runtime }, + { role: "compiler", ...wasm.compiler }, + ], + buildMetadata, + }, }; await writeFile(RELEASE_MANIFEST, `${JSON.stringify(manifest, null, 2)}\n`); @@ -2108,18 +2224,18 @@ const invokedDirectly = process.argv[1] !== undefined && resolve(process.argv[1]) === fileURLToPath(import.meta.url); if (invokedDirectly) { - const runtimeSmokeIndex = process.argv.indexOf("--runtime-smoke"); - const action = runtimeSmokeIndex >= 0 + const packageSmokeIndex = process.argv.indexOf("--package-smoke"); + const action = packageSmokeIndex >= 0 ? (() => { - const tarball = process.argv[runtimeSmokeIndex + 1]; - if (!tarball) fail("--runtime-smoke requires a tarball path"); - return smokePackedRuntime(tarball).then(() => ({ runtimeSmoke: tarball })); + const tarball = process.argv[packageSmokeIndex + 1]; + if (!tarball) fail("--package-smoke requires a tarball path"); + return smokePackedPackage(tarball).then(() => ({ packageSmoke: tarball })); })() : verifyPackageRelease(); action .then(async ({ manifest, tarball }) => { - if (runtimeSmokeIndex >= 0) { - console.log(`runtime smoke passed: ${resolve(process.argv[runtimeSmokeIndex + 1])}`); + if (packageSmokeIndex >= 0) { + console.log(`package smoke passed: ${resolve(process.argv[packageSmokeIndex + 1])}`); return; } await writeGithubOutputs({ manifest, tarball });