From a16151f03a46eb5ad48a44217c416de76951eba4 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Thu, 16 Jul 2026 20:17:46 +0300 Subject: [PATCH 1/7] refactor: keep appearance graph physical --- crates/labcolors-core/src/appearance.rs | 21 ----- .../src/appearance_graph_tests.rs | 84 ++++++++++++++----- crates/labcolors-core/src/semantic.rs | 29 ++++--- docs/whitepaper.md | 12 +-- 4 files changed, 86 insertions(+), 60 deletions(-) diff --git a/crates/labcolors-core/src/appearance.rs b/crates/labcolors-core/src/appearance.rs index 862d0e24..58b722a6 100644 --- a/crates/labcolors-core/src/appearance.rs +++ b/crates/labcolors-core/src/appearance.rs @@ -87,18 +87,6 @@ pub(crate) enum CompositionProfileV1 { EncodedSrgb8SourceOverV1, } -/// Класс доказательства результата: точная операция объявленного профиля либо -/// охарактеризованное legacy-совместимое поведение. Классы не смешиваются: -/// occurrence, решаемый legacy-солвером, не наследует exact-статус композита. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum EvidenceClass { - /// Точный результат reference-операции в её объявленном конечном домене. - ReferenceExact, - /// Охарактеризованное текущее поведение (см. §5.2 ТЗ #307): сохраняется - /// байт-в-байт, но не объявляется новой научной истиной. - LegacyCompatibility, -} - /// Декларация поверхности: input-слой (цвет из bindings как есть) либо /// source-over композит поверх другой поверхности. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -145,8 +133,6 @@ pub(crate) struct ForegroundOccurrenceSpec { pub(crate) identity_source: ColorInputId, /// Поверхность, против которой foreground реально стоит. pub(crate) against: SurfaceId, - /// Класс доказательства решателя, потребляющего occurrence. - pub(crate) evidence: EvidenceClass, } /// Типизированные ошибки compile/evaluate. Публичный (в пределах crate) вход @@ -467,9 +453,6 @@ pub(crate) struct SourceOverCertificateV1 { pub(crate) opacity_bits: u64, /// Финальные байты результата. pub(crate) output_rgb: [u8; 3], - /// Класс доказательства: всегда [`EvidenceClass::ReferenceExact`] — - /// сертификат существует только для exact-профиля. - pub(crate) evidence: EvidenceClass, } impl SourceOverCertificateV1 { @@ -509,8 +492,6 @@ pub(crate) struct ResolvedOccurrence { pub(crate) against: SurfaceId, /// Финальные вычисленные байты этой поверхности. pub(crate) backdrop: [u8; 3], - /// Класс доказательства решателя-потребителя. - pub(crate) evidence: EvidenceClass, } /// Результат одного evaluate: байты каждой поверхности, occurrences, @@ -688,7 +669,6 @@ impl CompiledAppearanceGraph { opacity_input: opacity, opacity_bits: alpha.to_bits(), output_rgb, - evidence: EvidenceClass::ReferenceExact, }); resolved[index] = Some(output_rgb); } @@ -723,7 +703,6 @@ impl CompiledAppearanceGraph { source: color_value(spec.identity_source), against: spec.against, backdrop, - evidence: spec.evidence, } }) .collect(); diff --git a/crates/labcolors-core/src/appearance_graph_tests.rs b/crates/labcolors-core/src/appearance_graph_tests.rs index a0fd6d4b..eaa1dcce 100644 --- a/crates/labcolors-core/src/appearance_graph_tests.rs +++ b/crates/labcolors-core/src/appearance_graph_tests.rs @@ -10,8 +10,9 @@ use proptest::prelude::*; use crate::appearance::{ - AppearanceBindings, AppearanceGraphSpec, ColorInputId, CompositionProfileV1, EvidenceClass, - ForegroundOccurrenceSpec, GraphError, OccurrenceId, OpacityInputId, SurfaceId, SurfaceSpec, + AppearanceBindings, AppearanceGraphSpec, ColorInputId, CompositionProfileV1, + ForegroundOccurrenceSpec, GraphError, OccurrenceId, OpacityInputId, ResolvedOccurrence, + SourceOverCertificateV1, SurfaceId, SurfaceSpec, }; use crate::solve::Floor; @@ -22,6 +23,45 @@ const CONTEXT_SURFACE: SurfaceId = SurfaceId::new(0); const DERIVED_SURFACE: SurfaceId = SurfaceId::new(1); const FOREGROUND: OccurrenceId = OccurrenceId::new(0); +#[test] +fn occurrence_contract_contains_only_physical_facts() { + let graph = AppearanceGraphSpec::new( + vec![SOURCE, CONTEXT], + vec![], + vec![SurfaceSpec::Input { + id: CONTEXT_SURFACE, + color: CONTEXT, + }], + vec![ForegroundOccurrenceSpec { + id: FOREGROUND, + identity_source: SOURCE, + against: CONTEXT_SURFACE, + }], + ) + .compile() + .unwrap(); + + let rendered = graph + .evaluate(&AppearanceBindings::new( + vec![(SOURCE, [1, 2, 3]), (CONTEXT, [4, 5, 6])], + vec![], + )) + .unwrap(); + + let ResolvedOccurrence { + id, + identity_source, + source, + against, + backdrop, + } = *rendered.occurrence(FOREGROUND).unwrap(); + + assert_eq!( + (id, identity_source, source, against, backdrop), + (FOREGROUND, SOURCE, [1, 2, 3], CONTEXT_SURFACE, [4, 5, 6],) + ); +} + fn atomic_component(surface_declarations_reversed: bool) -> AppearanceGraphSpec { let context = SurfaceSpec::Input { id: CONTEXT_SURFACE, @@ -48,7 +88,6 @@ fn atomic_component(surface_declarations_reversed: bool) -> AppearanceGraphSpec id: FOREGROUND, identity_source: SOURCE, against: DERIVED_SURFACE, - evidence: EvidenceClass::LegacyCompatibility, }], ) } @@ -130,7 +169,6 @@ fn unrelated_opaque_handles_do_not_change_the_physics() { id: other_occurrence, identity_source: other_source, against: other_derived_surface, - evidence: EvidenceClass::LegacyCompatibility, }], ) .compile() @@ -171,7 +209,6 @@ fn graph_rejects_missing_occurrence_backdrop_and_cycles() { id: FOREGROUND, identity_source: SOURCE, against: DERIVED_SURFACE, - evidence: EvidenceClass::LegacyCompatibility, }], ) .compile(); @@ -240,17 +277,27 @@ proptest! { let certificates = rendered.certificates(); prop_assert_eq!(certificates.len(), 1); let certificate = &certificates[0]; - prop_assert_eq!(certificate.profile, CompositionProfileV1::EncodedSrgb8SourceOverV1); - prop_assert_eq!(certificate.evidence, EvidenceClass::ReferenceExact); - prop_assert_eq!(certificate.surface, DERIVED_SURFACE); - prop_assert_eq!(certificate.source_input, SOURCE); - prop_assert_eq!(certificate.source_rgb, source); - prop_assert_eq!(certificate.backdrop_surface, CONTEXT_SURFACE); - prop_assert_eq!(certificate.backdrop_rgb, context); - prop_assert_eq!(certificate.opacity_input, OPACITY); - prop_assert_eq!(certificate.opacity_bits, opacity.to_bits()); - prop_assert_eq!(certificate.output_rgb, rendered.surface_rgb(DERIVED_SURFACE).unwrap()); - prop_assert_eq!(certificate.replay(), Ok(certificate.output_rgb)); + let SourceOverCertificateV1 { + profile, + surface, + source_input, + source_rgb, + backdrop_surface, + backdrop_rgb, + opacity_input, + opacity_bits, + output_rgb, + } = certificate; + prop_assert_eq!(*profile, CompositionProfileV1::EncodedSrgb8SourceOverV1); + prop_assert_eq!(*surface, DERIVED_SURFACE); + prop_assert_eq!(*source_input, SOURCE); + prop_assert_eq!(*source_rgb, source); + prop_assert_eq!(*backdrop_surface, CONTEXT_SURFACE); + prop_assert_eq!(*backdrop_rgb, context); + prop_assert_eq!(*opacity_input, OPACITY); + prop_assert_eq!(*opacity_bits, opacity.to_bits()); + prop_assert_eq!(*output_rgb, rendered.surface_rgb(DERIVED_SURFACE).unwrap()); + prop_assert_eq!(certificate.replay(), Ok(*output_rgb)); } } @@ -308,13 +355,11 @@ fn compile_rejects_duplicate_declarations_with_typed_errors() { id: FOREGROUND, identity_source: SOURCE, against: CONTEXT_SURFACE, - evidence: EvidenceClass::LegacyCompatibility, }, ForegroundOccurrenceSpec { id: FOREGROUND, identity_source: CONTEXT, against: CONTEXT_SURFACE, - evidence: EvidenceClass::LegacyCompatibility, }, ], ) @@ -433,7 +478,6 @@ fn compile_rejects_every_missing_reference_with_typed_errors() { id: FOREGROUND, identity_source: SOURCE, against: CONTEXT_SURFACE, - evidence: EvidenceClass::LegacyCompatibility, }], ) .compile(); @@ -567,7 +611,6 @@ fn occurrence_source_follows_the_declared_identity_edge_not_the_composite_source id: FOREGROUND, identity_source: identity, against: DERIVED_SURFACE, - evidence: EvidenceClass::LegacyCompatibility, }], ) .compile() @@ -587,5 +630,4 @@ fn occurrence_source_follows_the_declared_identity_edge_not_the_composite_source assert_eq!(occurrence.identity_source, identity); assert_eq!(occurrence.source, [111, 112, 113]); assert_ne!(occurrence.source, [10, 20, 30]); - assert_eq!(occurrence.evidence, EvidenceClass::LegacyCompatibility); } diff --git a/crates/labcolors-core/src/semantic.rs b/crates/labcolors-core/src/semantic.rs index 67ef4a8f..0c3b7e0b 100644 --- a/crates/labcolors-core/src/semantic.rs +++ b/crates/labcolors-core/src/semantic.rs @@ -688,13 +688,13 @@ pub enum RoleSpec { /// позиции `fill-*-primary` над фоном резолва), а НЕ против фона страницы /// и НЕ против эмитированного [`PairFill`](Self::PairFill) — у того своя, /// отдельно сдвинутая солид-эмиссия; ребра `PairFill → PairLabel` не - /// существует. Резолв — compatibility-адаптер над одним generic-компонентом - /// appearance-графа (#307): скомпилированный граф точно собирает - /// поверхность и возвращает foreground occurrence против неё, затем - /// оттеночный foreground решается прежним законом (`resolve_hued_anchor…`, - /// статус LegacyCompatibility) — пол поверхности гарантирован по - /// построению, тон клампится (флаг `compressed`) при недостижимости на - /// кривой семьи. + /// существует. Резолв использует один generic-компонент appearance-графа: + /// скомпилированный граф собирает поверхность и возвращает физические факты + /// foreground occurrence против неё. Доказательный статус downstream- + /// резолвера граф не назначает. Differential-тест закрепляет эквивалентность + /// миграционного wiring и outcomes на проверяемом домене, но не является + /// независимым oracle самого solver-а. Тон клампится (флаг `compressed`) + /// при недостижимости на кривой семьи. PairLabel { /// Пер-темный кодированный тинт-якорь семьи (как у лестницы). tint: LadderTint, @@ -2774,7 +2774,7 @@ fn nested_foreground_component() -> Result< &'static crate::appearance::GraphError, > { use crate::appearance::{ - AppearanceGraphSpec, CompiledAppearanceGraph, CompositionProfileV1, EvidenceClass, + AppearanceGraphSpec, CompiledAppearanceGraph, CompositionProfileV1, ForegroundOccurrenceSpec, GraphError, SurfaceSpec, }; static COMPONENT: std::sync::OnceLock> = @@ -2801,7 +2801,6 @@ fn nested_foreground_component() -> Result< id: NESTED_FOREGROUND, identity_source: NESTED_SOURCE, against: NESTED_DERIVED_SURFACE, - evidence: EvidenceClass::LegacyCompatibility, }], ) .compile() @@ -2829,10 +2828,14 @@ fn pair_label_surface_domain_error(error: &str) -> Resolved { /// Поверхность НЕ является эмитированным [`RoleSpec::PairFill`] — у того своя, /// отдельно сдвинутая солид-эмиссия; никакого ребра `PairFill → PairLabel` нет. /// -/// Оттеночный foreground решается ПРЕЖНИМ законом -/// ([`resolve_hued_anchor_from_encoded_source`], статус LegacyCompatibility — -/// не новая научная истина) НА ЭТОЙ ПОВЕРХНОСТИ: её собственный -/// [`ResolveContext`] задаёт полярность/макс-контраст, поэтому WCAG-пол лейбла +/// Оттеночный foreground решается текущим +/// [`resolve_hued_anchor_from_encoded_source`] НА ЭТОЙ ПОВЕРХНОСТИ. Appearance- +/// граф не присваивает этому downstream-решению доказательный статус: он +/// возвращает только source/against/backdrop. Differential закрепляет wiring и +/// outcomes миграции на проверяемом домене, но оба пути используют один solver +/// и потому не образуют независимый oracle его математики. Собственный +/// [`ResolveContext`] поверхности задаёт полярность/макс-контраст, поэтому +/// WCAG-пол лейбла /// гарантирован против той подложки, на которой foreground реально стоит /// (обычные `label-*` роли решаются против страницы, и на тинт-подложке их /// контраст проседает — класс, который закрывает эта роль). Недостижимость пола diff --git a/docs/whitepaper.md b/docs/whitepaper.md index 3ec4cf3a..57b6fd77 100644 --- a/docs/whitepaper.md +++ b/docs/whitepaper.md @@ -210,11 +210,13 @@ task #29). `RoleRecipe::PairLabel` решает лейбл штатным зак ``` Граф не знает клиентских имён (`PairLabel`, `Warning` и т. п.): роль -foreground/фон задаётся только топологией typed handles. Точной (Reference -exact) здесь является **только** композиция поверхности в объявленном -encoded-sRGB8 профиле; сам foreground-solve (LPC/якорная доля/семейная кривая) -остаётся статусом **LegacyCompatibility** — охарактеризованное текущее -поведение, не новая научная истина. Лейбл не несёт typography-фактов, поэтому +foreground/фон задаётся только топологией typed handles. Точность композиции +определяется объявленным encoded-sRGB8 профилем и независимо проверяемым replay- +сертификатом. Foreground occurrence содержит только физические факты +source/against/backdrop; доказательный статус downstream-solve граф не назначает. +Differential закрепляет wiring и outcomes миграции на проверяемом домене; оба +пути используют один downstream solver и не являются независимым oracle его +математики. Лейбл не несёт typography-фактов, поэтому никакой размер/вес текста здесь не учитывается и не обещается. Старая ручная композиция заморожена как test-only differential oracle; матрица 5 семей × 4 режима × 6 фонов + property-тесты доказывают байт-идентичность From 3997a30936de2494d8b27122791a42c3d886644c Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Thu, 16 Jul 2026 22:10:46 +0300 Subject: [PATCH 2/7] test: ratchet smaller runtime wasm --- packages/colors/README.md | 7 ++- .../colors/bench/wasm-size-budget-v7.json | 54 +++++++++++++++++ .../colors/test/release-contract.test.mjs | 58 +++++++++++++++---- scripts/check-wasm-size-budget.mjs | 54 +++++++++++------ 4 files changed, 140 insertions(+), 33 deletions(-) create mode 100644 packages/colors/bench/wasm-size-budget-v7.json diff --git a/packages/colors/README.md b/packages/colors/README.md index d9097473..23e51acf 100644 --- a/packages/colors/README.md +++ b/packages/colors/README.md @@ -621,9 +621,10 @@ WASM и полным вызовом. ## Размер бандла Raw-размер WASM — hard gate с append-only историей. Текущий -`bench/wasm-size-budget-v6.json` содержит exact Linux-x64 size/SHA-бюджеты с -нулевым headroom для `runtime` и `compiler`; V1–V5 остаются -неизменяемой историей прежнего единого артефакта. Release-equivalent CI требует +`bench/wasm-size-budget-v7.json` содержит exact Linux-x64 size/SHA-бюджеты с +нулевым headroom для `runtime` и `compiler`; checker выбирает текущую версию, а +все предыдущие versioned-файлы остаются неизменяемой историей. +Release-equivalent CI требует точного совпадения обеих ролей и их рецептов сборки. На других host-платформах checker сообщает только raw/gzip/SHA-диагностику и не выдаёт локальные байты за канонический release artifact. diff --git a/packages/colors/bench/wasm-size-budget-v7.json b/packages/colors/bench/wasm-size-budget-v7.json new file mode 100644 index 00000000..b1d74eb8 --- /dev/null +++ b/packages/colors/bench/wasm-size-budget-v7.json @@ -0,0 +1,54 @@ +{ + "schemaVersion": 5, + "budgetId": "labcolors-wasm-roles-issue-307-c7a-v7", + "predecessor": { + "path": "packages/colors/bench/wasm-size-budget-v6.json", + "fileSha256": "761af6050031169dac7eafdfadb2db9bbb2023b96ed5ba9d3c5dc966ffeafb32" + }, + "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": 307, + "slice": "C7a", + "measurementPlatform": "linux-x64", + "rawBytes": 454334, + "sha256": "e052ff2413d6da57e745342b45abe9af3719994deface73db19faeba84af91b5" + }, + "policy": { + "maxRawBytes": 454334, + "derivation": "exact-accepted-issue-307-slice-c7a-runtime-measurement", + "gzip": "diagnostic-only" + } + }, + "compiler": { + "artifact": "packages/colors/compiler/labcolors_compiler_bg.wasm", + "measurement": { + "issue": 296, + "slice": "C3", + "measurementPlatform": "linux-x64", + "rawBytes": 229658, + "sha256": "34e2a561862ee06d52d1104f8ba60ccf9967e2e4fd09803d4e75e1966074bc8d" + }, + "policy": { + "maxRawBytes": 229658, + "derivation": "exact-accepted-issue-296-slice-c3-compiler-measurement", + "gzip": "diagnostic-only" + } + } + } +} diff --git a/packages/colors/test/release-contract.test.mjs b/packages/colors/test/release-contract.test.mjs index 6a0e1ac8..89d7d0a2 100644 --- a/packages/colors/test/release-contract.test.mjs +++ b/packages/colors/test/release-contract.test.mjs @@ -1128,10 +1128,10 @@ test("release evidence carries the versioned WCAG22 feasibility operation", () = assert.match(verifier, /case "incompatibleCoreContract"/u); }); -test("WCAG22 WASM role budgets are exact, append-only, and acyclic", async () => { +test("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, 6].map((version) => [ + [1, 2, 3, 4, 5, 6, 7].map((version) => [ `v${version}`, join(bench, `wasm-size-budget-v${version}.json`), ]), @@ -1146,6 +1146,7 @@ test("WCAG22 WASM role budgets are exact, append-only, and acyclic", async () => v4: "c34fc10404dc7057a53a28592d18342078b5cd0e5dcaa888db482abf3f5fb23c", v5: "e4b53a2eb976a8c66827a559cb81232e359b734dbfb14725da215cb496ff5d59", v6: "761af6050031169dac7eafdfadb2db9bbb2023b96ed5ba9d3c5dc966ffeafb32", + v7: "a57e94d4cf6b0df7048ec2d808f78eef301e80064bee0648179af83e729a95c3", }; const documents = {}; for (const version of Object.keys(paths)) { @@ -1156,7 +1157,7 @@ test("WCAG22 WASM role budgets are exact, append-only, and acyclic", async () => if (version !== "v1") assert.equal(bytes.toString("utf8"), canonicalJson(value)); } - const { v1, v2, v3, v4, v5, v6 } = documents; + const { v1, v2, v3, v4, v5, v6, v7 } = 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"); @@ -1237,11 +1238,38 @@ test("WCAG22 WASM role budgets are exact, append-only, and acyclic", async () => assert.ok(v6.roles.runtime.policy.maxRawBytes <= v1.policy.maxRawBytes); assert.ok(v6.roles.runtime.policy.maxRawBytes <= v4.policy.maxRawBytes); + // V7 (#307-C7a): удаление нефизической evidence-оси уменьшает только runtime; + // compiler и оба воспроизводимых build-рецепта остаются байт-идентичными V6. + assert.equal(v7.schemaVersion, 5); + assert.equal(v7.budgetId, "labcolors-wasm-roles-issue-307-c7a-v7"); + assert.deepEqual(v7.predecessor, { + path: "packages/colors/bench/wasm-size-budget-v6.json", + fileSha256: expectedHashes.v6, + }); + assert.deepEqual(v7.toolchainSource, v6.toolchainSource); + assert.deepEqual(v7.buildRecipes, v6.buildRecipes); + assert.deepEqual(v7.roles.runtime.measurement, { + issue: 307, + slice: "C7a", + measurementPlatform: "linux-x64", + rawBytes: 454334, + sha256: "e052ff2413d6da57e745342b45abe9af3719994deface73db19faeba84af91b5", + }); + assert.equal( + v7.roles.runtime.policy.derivation, + "exact-accepted-issue-307-slice-c7a-runtime-measurement", + ); + assert.deepEqual(v7.roles.compiler, v6.roles.compiler); + for (const role of ["runtime", "compiler"]) { + assert.equal(v7.roles[role].policy.maxRawBytes, v7.roles[role].measurement.rawBytes); + assert.ok(v7.roles[role].policy.maxRawBytes <= v6.roles[role].policy.maxRawBytes); + } + const checker = await import( new URL("../../../scripts/check-wasm-size-budget.mjs", import.meta.url) ); - assert.equal(checker.DEFAULT_BUDGET, paths.v6); - for (const version of [1, 2, 3, 4, 5, 6]) { + assert.equal(checker.DEFAULT_BUDGET, paths.v7); + for (const version of [1, 2, 3, 4, 5, 6, 7]) { assert.equal(checker[`V${version}_FILE_SHA256`], expectedHashes[`v${version}`]); } assert.equal(checker.V1_RECIPE_SHA256, v5.buildRecipes.runtime.recipeSha256); @@ -1261,15 +1289,15 @@ test("WCAG22 WASM role budgets are exact, append-only, and acyclic", async () => ["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; + // The current exact digest selects the pinned Linux build; developer builds retain their own host paths. + if (sha256(builtBytes) !== v7.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-role-budget-v5-")); + const temporary = mkdtempSync(join(tmpdir(), "labcolors-wasm-role-budget-v7-")); try { const runtimePath = join(temporary, "runtime.wasm"); const compilerPath = join(temporary, "compiler.wasm"); @@ -1278,7 +1306,7 @@ test("WCAG22 WASM role budgets are exact, append-only, and acyclic", async () => const compilerBytes = Buffer.alloc(17); runtimeBytes.set([0x00, 0x61, 0x73, 0x6d]); compilerBytes.set([0x00, 0x61, 0x73, 0x6d]); - const fixture = structuredClone(v6); + const fixture = structuredClone(v7); for (const [role, bytes] of [["runtime", runtimeBytes], ["compiler", compilerBytes]]) { fixture.roles[role].measurement.rawBytes = bytes.length; fixture.roles[role].measurement.sha256 = sha256(bytes); @@ -1344,6 +1372,12 @@ test("WCAG22 WASM role budgets are exact, append-only, and acyclic", async () => value.roles.runtime.measurement.rawBytes = v1.policy.maxRawBytes + 1; value.roles.runtime.policy.maxRawBytes = v1.policy.maxRawBytes + 1; }], + ["compiler predecessor regression", (value) => { + value.roles.compiler.measurement.rawBytes = + v6.roles.compiler.policy.maxRawBytes + 1; + value.roles.compiler.policy.maxRawBytes = + v6.roles.compiler.policy.maxRawBytes + 1; + }], ["whole-call cycle", (value) => { value.wholeCallArtifact = "forbidden"; }], ["top-level key reorder", (value) => ({ budgetId: value.budgetId, @@ -1354,7 +1388,7 @@ test("WCAG22 WASM role budgets are exact, append-only, and acyclic", async () => roles: value.roles, })], ]; - assert.equal(schemaMutations.length, 25, "v5 schema mutation set changed"); + assert.equal(schemaMutations.length, 26, "v7 schema mutation set changed"); for (const [name, mutate] of schemaMutations) { const invalid = structuredClone(fixture); const result = mutate(invalid) ?? invalid; @@ -1408,8 +1442,8 @@ test("WCAG22 WASM role budgets are exact, append-only, and acyclic", async () => Buffer.from(compilerBytes).fill(1, compilerBytes.length - 1), ); assert.throws( - () => checker.parseBudgetDocument(Buffer.from(canonicalJson(coordinatedMutation)), paths.v6), - /current v6 file SHA-256 mismatch/u, + () => checker.parseBudgetDocument(Buffer.from(canonicalJson(coordinatedMutation)), paths.v7), + /current v7 file SHA-256 mismatch/u, "coordinated artifact and document drift must still fail the default identity", ); } finally { diff --git a/scripts/check-wasm-size-budget.mjs b/scripts/check-wasm-size-budget.mjs index 84fcf813..0271fa0a 100644 --- a/scripts/check-wasm-size-budget.mjs +++ b/scripts/check-wasm-size-budget.mjs @@ -13,10 +13,11 @@ const V2_PATH = resolve(REPO_ROOT, "packages/colors/bench/wasm-size-budget-v2.js 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"); const V5_PATH = resolve(REPO_ROOT, "packages/colors/bench/wasm-size-budget-v5.json"); +const V6_PATH = resolve(REPO_ROOT, "packages/colors/bench/wasm-size-budget-v6.json"); export const DEFAULT_BUDGET = resolve( REPO_ROOT, - "packages/colors/bench/wasm-size-budget-v6.json", + "packages/colors/bench/wasm-size-budget-v7.json", ); export const V1_FILE_SHA256 = "4f7340fc8cfd0ccb97377c385f2f8d8e7a9ef2c5ba96177f518c5d07de2825e1"; @@ -32,11 +33,14 @@ export const V5_FILE_SHA256 = "e4b53a2eb976a8c66827a559cb81232e359b734dbfb14725da215cb496ff5d59"; export const V6_FILE_SHA256 = "761af6050031169dac7eafdfadb2db9bbb2023b96ed5ba9d3c5dc966ffeafb32"; +export const V7_FILE_SHA256 = + "a57e94d4cf6b0df7048ec2d808f78eef301e80064bee0648179af83e729a95c3"; const V1_REPOSITORY_PATH = "packages/colors/bench/wasm-size-budget-v1.json"; -const V5_REPOSITORY_PATH = "packages/colors/bench/wasm-size-budget-v5.json"; +const V6_REPOSITORY_PATH = "packages/colors/bench/wasm-size-budget-v6.json"; const V5_BUDGET_ID = "labcolors-wasm-roles-issue-296-c1-v5"; const V6_BUDGET_ID = "labcolors-wasm-roles-issue-296-c3-v6"; +const V7_BUDGET_ID = "labcolors-wasm-roles-issue-307-c7a-v7"; const ROLE_ORDER = ["runtime", "compiler"]; const ROLE_SPECS = { runtime: { @@ -44,8 +48,9 @@ const ROLE_SPECS = { 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", - measurementSlice: "C1", + derivation: "exact-accepted-issue-307-slice-c7a-runtime-measurement", + measurementIssue: 307, + measurementSlice: "C7a", }, compiler: { artifact: "packages/colors/compiler/labcolors_compiler_bg.wasm", @@ -55,6 +60,7 @@ const ROLE_SPECS = { // C3 публикует атомарную операцию в compiler-роли: рост размера — это // добавленная capability, зафиксированная новым точным измерением. derivation: "exact-accepted-issue-296-slice-c3-compiler-measurement", + measurementIssue: 296, measurementSlice: "C3", }, }; @@ -156,7 +162,11 @@ function verifyImmutableHistory() { if (v5?.schemaVersion !== 4 || v5?.budgetId !== V5_BUDGET_ID) { fail("immutable v5 budget identity drifted"); } - return { v1, v4, v5 }; + const v6 = readImmutableJson(V6_PATH, V6_FILE_SHA256, "v6"); + if (v6?.schemaVersion !== 5 || v6?.budgetId !== V6_BUDGET_ID) { + fail("immutable v6 budget identity drifted"); + } + return { v1, v4, v5, v6 }; } function validateBudgetValue(budget) { @@ -173,14 +183,14 @@ function validateBudgetValue(budget) { "budget", ); if (budget.schemaVersion !== 5) fail("supported schemaVersion is exactly 5"); - if (budget.budgetId !== V6_BUDGET_ID) fail(`budgetId must be ${V6_BUDGET_ID}`); + if (budget.budgetId !== V7_BUDGET_ID) fail(`budgetId must be ${V7_BUDGET_ID}`); exactKeys(budget.predecessor, ["path", "fileSha256"], "predecessor"); if ( - budget.predecessor.path !== V5_REPOSITORY_PATH || - budget.predecessor.fileSha256 !== V5_FILE_SHA256 + budget.predecessor.path !== V6_REPOSITORY_PATH || + budget.predecessor.fileSha256 !== V6_FILE_SHA256 ) { - fail("predecessor must bind the immutable v5 document"); + fail("predecessor must bind the immutable v6 document"); } exactKeys(budget.toolchainSource, ["path", "fileSha256"], "toolchainSource"); @@ -193,7 +203,7 @@ function validateBudgetValue(budget) { exactKeys(budget.buildRecipes, ROLE_ORDER, "buildRecipes"); exactKeys(budget.roles, ROLE_ORDER, "roles"); - const { v1, v4 } = verifyImmutableHistory(); + const { v1, v6 } = verifyImmutableHistory(); for (const role of ROLE_ORDER) { const spec = ROLE_SPECS[role]; @@ -219,8 +229,14 @@ function validateBudgetValue(budget) { ["issue", "slice", "measurementPlatform", "rawBytes", "sha256"], `roles.${role}.measurement`, ); - if (record.measurement.issue !== 296 || record.measurement.slice !== spec.measurementSlice) { - fail(`roles.${role}.measurement must cite Issue #296 Slice ${spec.measurementSlice}`); + if ( + record.measurement.issue !== spec.measurementIssue || + record.measurement.slice !== spec.measurementSlice + ) { + fail( + `roles.${role}.measurement must cite Issue #${spec.measurementIssue} ` + + `Slice ${spec.measurementSlice}`, + ); } if (record.measurement.measurementPlatform !== "linux-x64") { fail(`roles.${role}.measurement must use canonical linux-x64`); @@ -242,10 +258,12 @@ function validateBudgetValue(budget) { } if (budget.roles.runtime.policy.maxRawBytes > v1.policy.maxRawBytes) { - fail("runtime role must not exceed the immutable same-capability pre-compiler ceiling"); + fail("runtime role must not exceed the immutable same-capability initial ceiling"); } - if (budget.roles.runtime.policy.maxRawBytes > v4.policy.maxRawBytes) { - fail("runtime role must not regress the immutable immediate predecessor ceiling"); + for (const role of ROLE_ORDER) { + if (budget.roles[role].policy.maxRawBytes > v6.roles[role].policy.maxRawBytes) { + fail(`${role} role must not regress the immutable immediate predecessor ceiling`); + } } } @@ -264,10 +282,10 @@ export function parseBudgetDocument(bytes, budgetPath) { validateBudgetValue(budget); if (resolve(budgetPath) === DEFAULT_BUDGET) { const actualFileSha256 = sha256(document); - if (actualFileSha256 !== V6_FILE_SHA256) { + if (actualFileSha256 !== V7_FILE_SHA256) { fail( - `current v6 file SHA-256 mismatch: ` + - `expected=${V6_FILE_SHA256} actual=${actualFileSha256}`, + `current v7 file SHA-256 mismatch: ` + + `expected=${V7_FILE_SHA256} actual=${actualFileSha256}`, ); } } From cc5f6bfc24cb30e693c7e8e8d0090b72d54a7193 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Thu, 16 Jul 2026 22:54:05 +0300 Subject: [PATCH 3/7] docs: keep appearance prose in project language --- crates/labcolors-core/src/semantic.rs | 21 +++++++++++---------- docs/whitepaper.md | 17 +++++++++-------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/crates/labcolors-core/src/semantic.rs b/crates/labcolors-core/src/semantic.rs index 0c3b7e0b..0ccd65b8 100644 --- a/crates/labcolors-core/src/semantic.rs +++ b/crates/labcolors-core/src/semantic.rs @@ -690,11 +690,11 @@ pub enum RoleSpec { /// отдельно сдвинутая солид-эмиссия; ребра `PairFill → PairLabel` не /// существует. Резолв использует один generic-компонент appearance-графа: /// скомпилированный граф собирает поверхность и возвращает физические факты - /// foreground occurrence против неё. Доказательный статус downstream- - /// резолвера граф не назначает. Differential-тест закрепляет эквивалентность - /// миграционного wiring и outcomes на проверяемом домене, но не является - /// независимым oracle самого solver-а. Тон клампится (флаг `compressed`) - /// при недостижимости на кривой семьи. + /// foreground occurrence против неё. Доказательный статус последующего + /// резолвера граф не назначает. Дифференциальный тест закрепляет + /// эквивалентность миграционного подключения и результатов на проверяемом + /// домене, но не является независимым эталоном математики самого резолвера. + /// Тон клампится (флаг `compressed`) при недостижимости на кривой семьи. PairLabel { /// Пер-темный кодированный тинт-якорь семьи (как у лестницы). tint: LadderTint, @@ -2830,11 +2830,12 @@ fn pair_label_surface_domain_error(error: &str) -> Resolved { /// /// Оттеночный foreground решается текущим /// [`resolve_hued_anchor_from_encoded_source`] НА ЭТОЙ ПОВЕРХНОСТИ. Appearance- -/// граф не присваивает этому downstream-решению доказательный статус: он -/// возвращает только source/against/backdrop. Differential закрепляет wiring и -/// outcomes миграции на проверяемом домене, но оба пути используют один solver -/// и потому не образуют независимый oracle его математики. Собственный -/// [`ResolveContext`] поверхности задаёт полярность/макс-контраст, поэтому +/// граф не присваивает этому последующему решению доказательный статус: он +/// возвращает только source/against/backdrop. Дифференциальный тест закрепляет +/// подключение и результаты миграции на проверяемом домене, но оба пути +/// используют один резолвер и потому не образуют независимый эталон его +/// математики. Собственный [`ResolveContext`] поверхности задаёт +/// полярность/макс-контраст, поэтому /// WCAG-пол лейбла /// гарантирован против той подложки, на которой foreground реально стоит /// (обычные `label-*` роли решаются против страницы, и на тинт-подложке их diff --git a/docs/whitepaper.md b/docs/whitepaper.md index 57b6fd77..a1077352 100644 --- a/docs/whitepaper.md +++ b/docs/whitepaper.md @@ -209,16 +209,17 @@ task #29). `RoleRecipe::PairLabel` решает лейбл штатным зак → прежний foreground-резолвер ``` -Граф не знает клиентских имён (`PairLabel`, `Warning` и т. п.): роль -foreground/фон задаётся только топологией typed handles. Точность композиции -определяется объявленным encoded-sRGB8 профилем и независимо проверяемым replay- -сертификатом. Foreground occurrence содержит только физические факты -source/against/backdrop; доказательный статус downstream-solve граф не назначает. -Differential закрепляет wiring и outcomes миграции на проверяемом домене; оба -пути используют один downstream solver и не являются независимым oracle его +Граф не знает клиентских имён (`PairLabel`, `Warning` и т. п.): роль переднего +плана и фона задаётся только топологией типизированных handles. Точность +композиции определяется объявленным encoded-sRGB8 профилем и независимо +проверяемым replay-сертификатом. Вхождение переднего плана содержит только +физические факты source/against/backdrop; доказательный статус последующего +решения граф не назначает. Дифференциальный тест закрепляет подключение и +результаты миграции на проверяемом домене; оба пути используют один последующий +резолвер и не являются независимым эталоном его математики. Лейбл не несёт typography-фактов, поэтому никакой размер/вес текста здесь не учитывается и не обещается. Старая ручная -композиция заморожена как test-only differential oracle; матрица +композиция заморожена как тестовый эталон для дифференциального сравнения; матрица 5 семей × 4 режима × 6 фонов + property-тесты доказывают байт-идентичность production-пути ей (`migration_*`, [`pair_label_tests.rs`](../crates/labcolors-core/src/pair_label_tests.rs)). From c12bb175cd4bd5ee5284503d2af29e3a6e2c5406 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Thu, 16 Jul 2026 23:54:10 +0300 Subject: [PATCH 4/7] ci: separate wasm size from artifact identity --- .github/workflows/ci.yml | 39 +++++++- packages/colors/README.md | 14 +-- .../colors/bench/wasm-size-budget-v7.json | 8 +- .../colors/test/release-contract.test.mjs | 99 ++++++++++++------- scripts/check-wasm-size-budget.mjs | 19 +--- 5 files changed, 116 insertions(+), 63 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 87d15910..dac7b426 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -466,15 +466,48 @@ jobs: run: | cargo install wasm-pack --version 0.13.1 --locked echo "$CARGO_HOME/bin" >> "$GITHUB_PATH" - - name: wasm-pack build (runtime + compiler release roles) + - name: reproduce WASM roles from the same source # 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. + # The second build follows `cargo clean`, so equality proves same-source + # reproduction instead of comparing unrelated source commits by SHA. 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 + build_roles() { + 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 + } + + rm -rf packages/colors/pkg packages/colors/compiler + build_roles + + first="$RUNNER_TEMP/wasm-first-$GITHUB_JOB" + rm -rf "$first" + mkdir -p "$first" + cp -a packages/colors/pkg "$first/pkg" + cp -a packages/colors/compiler "$first/compiler" + + cargo clean + rm -rf packages/colors/pkg packages/colors/compiler + build_roles + + diff --no-dereference --recursive "$first/pkg" packages/colors/pkg + diff --no-dereference --recursive "$first/compiler" packages/colors/compiler + + for wasm in \ + packages/colors/pkg/labcolors_bg.wasm \ + packages/colors/compiler/labcolors_compiler_bg.wasm + do + if LC_ALL=C grep -a -q -E '/(Users|home)/[^[:cntrl:]]*/\.cargo/registry/src/|/opt/actions-runner/[^[:cntrl:]]*/cargo-wasm/registry/src/' "$wasm"; then + echo "unmapped build path in $wasm" >&2 + exit 1 + fi + done + sha256sum \ + packages/colors/pkg/labcolors_bg.wasm \ + packages/colors/compiler/labcolors_compiler_bg.wasm - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: node-version: ${{ env.NODE_TOOLCHAIN }} diff --git a/packages/colors/README.md b/packages/colors/README.md index 23e51acf..879478cb 100644 --- a/packages/colors/README.md +++ b/packages/colors/README.md @@ -621,12 +621,14 @@ WASM и полным вызовом. ## Размер бандла Raw-размер WASM — hard gate с append-only историей. Текущий -`bench/wasm-size-budget-v7.json` содержит exact Linux-x64 size/SHA-бюджеты с -нулевым headroom для `runtime` и `compiler`; checker выбирает текущую версию, а -все предыдущие versioned-файлы остаются неизменяемой историей. -Release-equivalent CI требует -точного совпадения обеих ролей и их рецептов сборки. На других host-платформах -checker сообщает только raw/gzip/SHA-диагностику и не выдаёт локальные байты за +`bench/wasm-size-budget-v7.json` содержит exact Linux-x64 size-бюджеты с нулевым +headroom для `runtime` и `compiler`; checker выбирает текущую версию, а все +предыдущие versioned-файлы остаются неизменяемой историей. Size policy не +притворяется идентификатором артефакта: фактический SHA каждой роли вместе с +source SHA записывается в `build-metadata.json` и повторно сверяется с точными +байтами tarball при публикации. Release-equivalent CI требует точного размера +обеих ролей и неизменных рецептов сборки. На других host-платформах checker +сообщает только raw/gzip/SHA-диагностику и не выдаёт локальные байты за канонический release artifact. Runtime и offline compiler поставляются двумя независимо загружаемыми diff --git a/packages/colors/bench/wasm-size-budget-v7.json b/packages/colors/bench/wasm-size-budget-v7.json index b1d74eb8..de769a80 100644 --- a/packages/colors/bench/wasm-size-budget-v7.json +++ b/packages/colors/bench/wasm-size-budget-v7.json @@ -1,5 +1,5 @@ { - "schemaVersion": 5, + "schemaVersion": 6, "budgetId": "labcolors-wasm-roles-issue-307-c7a-v7", "predecessor": { "path": "packages/colors/bench/wasm-size-budget-v6.json", @@ -26,8 +26,7 @@ "issue": 307, "slice": "C7a", "measurementPlatform": "linux-x64", - "rawBytes": 454334, - "sha256": "e052ff2413d6da57e745342b45abe9af3719994deface73db19faeba84af91b5" + "rawBytes": 454334 }, "policy": { "maxRawBytes": 454334, @@ -41,8 +40,7 @@ "issue": 296, "slice": "C3", "measurementPlatform": "linux-x64", - "rawBytes": 229658, - "sha256": "34e2a561862ee06d52d1104f8ba60ccf9967e2e4fd09803d4e75e1966074bc8d" + "rawBytes": 229658 }, "policy": { "maxRawBytes": 229658, diff --git a/packages/colors/test/release-contract.test.mjs b/packages/colors/test/release-contract.test.mjs index 89d7d0a2..bee02b94 100644 --- a/packages/colors/test/release-contract.test.mjs +++ b/packages/colors/test/release-contract.test.mjs @@ -1128,7 +1128,7 @@ test("release evidence carries the versioned WCAG22 feasibility operation", () = assert.match(verifier, /case "incompatibleCoreContract"/u); }); -test("WASM role budgets are exact, append-only, and acyclic", async () => { +test("WASM role size budgets are exact, append-only, and acyclic", async () => { const bench = join(root, "packages", "colors", "bench"); const paths = Object.fromEntries( [1, 2, 3, 4, 5, 6, 7].map((version) => [ @@ -1146,7 +1146,7 @@ test("WASM role budgets are exact, append-only, and acyclic", async () => { v4: "c34fc10404dc7057a53a28592d18342078b5cd0e5dcaa888db482abf3f5fb23c", v5: "e4b53a2eb976a8c66827a559cb81232e359b734dbfb14725da215cb496ff5d59", v6: "761af6050031169dac7eafdfadb2db9bbb2023b96ed5ba9d3c5dc966ffeafb32", - v7: "a57e94d4cf6b0df7048ec2d808f78eef301e80064bee0648179af83e729a95c3", + v7: "01d17c042b7dc36585e9657490048932fdf61d4715099b735aa3bf2d3dc5777e", }; const documents = {}; for (const version of Object.keys(paths)) { @@ -1238,9 +1238,9 @@ test("WASM role budgets are exact, append-only, and acyclic", async () => { assert.ok(v6.roles.runtime.policy.maxRawBytes <= v1.policy.maxRawBytes); assert.ok(v6.roles.runtime.policy.maxRawBytes <= v4.policy.maxRawBytes); - // V7 (#307-C7a): удаление нефизической evidence-оси уменьшает только runtime; - // compiler и оба воспроизводимых build-рецепта остаются байт-идентичными V6. - assert.equal(v7.schemaVersion, 5); + // V7 (#307-C7a): size policy хранит только измеряемую величину. SHA каждого + // конкретного source-коммита принадлежит release provenance, не size budget. + assert.equal(v7.schemaVersion, 6); assert.equal(v7.budgetId, "labcolors-wasm-roles-issue-307-c7a-v7"); assert.deepEqual(v7.predecessor, { path: "packages/colors/bench/wasm-size-budget-v6.json", @@ -1253,13 +1253,18 @@ test("WASM role budgets are exact, append-only, and acyclic", async () => { slice: "C7a", measurementPlatform: "linux-x64", rawBytes: 454334, - sha256: "e052ff2413d6da57e745342b45abe9af3719994deface73db19faeba84af91b5", }); assert.equal( v7.roles.runtime.policy.derivation, "exact-accepted-issue-307-slice-c7a-runtime-measurement", ); - assert.deepEqual(v7.roles.compiler, v6.roles.compiler); + assert.deepEqual(v7.roles.compiler.measurement, { + issue: 296, + slice: "C3", + measurementPlatform: "linux-x64", + rawBytes: 229658, + }); + assert.deepEqual(v7.roles.compiler.policy, v6.roles.compiler.policy); for (const role of ["runtime", "compiler"]) { assert.equal(v7.roles[role].policy.maxRawBytes, v7.roles[role].measurement.rawBytes); assert.ok(v7.roles[role].policy.maxRawBytes <= v6.roles[role].policy.maxRawBytes); @@ -1283,19 +1288,33 @@ test("WASM role budgets are exact, append-only, and acyclic", async () => { assert.match(wasmJob, /runs-on: \[self-hosted, Linux, X64\]/u); assert.match(wasmJob, /GITHUB_WORKSPACE=\/workspace\/lab-colors/u); assert.match(wasmJob, /CARGO_HOME=\/cargo-home/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 current exact digest selects the pinned Linux build; developer builds retain their own host paths. - if (sha256(builtBytes) !== v7.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 reproduction = workflowRunScript( + ci, + "name: reproduce WASM roles from the same source", + ); + assert.equal( + reproduction.match(/wasm-pack build crates\/labcolors-wasm/gu)?.length, + 1, + "runtime build recipe must have one shell SSOT", + ); + assert.equal( + reproduction.match(/wasm-pack build crates\/labcolors-compiler/gu)?.length, + 1, + "compiler build recipe must have one shell SSOT", + ); + assert.equal( + reproduction.match(/^\s*build_roles$/gmu)?.length, + 2, + "the same recipe must run exactly twice", + ); + assert.match(reproduction, /cargo clean/u); + assert.match(reproduction, /cp -a packages\/colors\/pkg/u); + assert.match(reproduction, /cp -a packages\/colors\/compiler/u); + assert.equal( + reproduction.match(/diff --no-dereference --recursive/gu)?.length, + 2, + ); + assert.match(reproduction, /\/opt\/actions-runner\/[^\n]*cargo-wasm\/registry\/src\//u); const temporary = mkdtempSync(join(tmpdir(), "labcolors-wasm-role-budget-v7-")); try { @@ -1309,7 +1328,6 @@ test("WASM role budgets are exact, append-only, and acyclic", async () => { const fixture = structuredClone(v7); 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); @@ -1333,11 +1351,17 @@ test("WASM role budgets are exact, append-only, and acyclic", async () => { { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }, ); 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); + assert.match( + output, + /role=runtime raw=16B .*artifact-sha256=[0-9a-f]{64} declared-recipe-sha=match/u, + ); + assert.match( + output, + /role=compiler raw=17B .*artifact-sha256=[0-9a-f]{64} declared-recipe-sha=match/u, + ); const schemaMutations = [ - ["schema rollback", (value) => { value.schemaVersion = 3; }], + ["schema rollback", (value) => { value.schemaVersion = 5; }], ["identity drift", (value) => { value.budgetId = "other"; }], ["predecessor path", (value) => { value.predecessor.path = "other.json"; }], ["predecessor hash", (value) => { value.predecessor.fileSha256 = "0".repeat(64); }], @@ -1364,7 +1388,9 @@ test("WASM role budgets are exact, append-only, and acyclic", async () => { }], ["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"; }], + ["artifact digest conflation", (value) => { + value.roles.compiler.measurement.sha256 = "0".repeat(64); + }], ["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"; }], @@ -1401,8 +1427,8 @@ test("WASM role budgets are exact, append-only, and acyclic", async () => { writeFileSync( fixtureBudgetPath, canonicalJson(fixture).replace( - ' "schemaVersion": 5,\n', - ' "schemaVersion": 5,\n "schemaVersion": 5,\n', + ' "schemaVersion": 6,\n', + ' "schemaVersion": 6,\n "schemaVersion": 6,\n', ), ); assert.throws(run, /canonical JSON/u, "duplicate JSON fields must fail"); @@ -1411,13 +1437,17 @@ test("WASM role budgets are exact, append-only, and acyclic", async () => { const record = fixture.roles[role]; const canonical = checker.evaluateWasmBudget(role, record, bytes, "linux-x64"); assert.equal(canonical.status, "PASS"); - assert.equal(canonical.artifactSha, "match"); + assert.equal(canonical.artifactSha256, sha256(bytes)); const sameSizeMutation = Buffer.from(bytes); sameSizeMutation[sameSizeMutation.length - 1] = 1; - assert.throws( - () => checker.evaluateWasmBudget(role, record, sameSizeMutation, "linux-x64"), - /SHA-256 mismatch/u, + const sameSize = checker.evaluateWasmBudget( + role, + record, + sameSizeMutation, + "linux-x64", ); + assert.equal(sameSize.status, "PASS"); + assert.notEqual(sameSize.artifactSha256, canonical.artifactSha256); assert.throws( () => checker.evaluateWasmBudget( role, @@ -1438,9 +1468,8 @@ test("WASM role budgets are exact, append-only, and acyclic", async () => { } const coordinatedMutation = structuredClone(fixture); - coordinatedMutation.roles.compiler.measurement.sha256 = sha256( - Buffer.from(compilerBytes).fill(1, compilerBytes.length - 1), - ); + coordinatedMutation.roles.runtime.measurement.rawBytes -= 1; + coordinatedMutation.roles.runtime.policy.maxRawBytes -= 1; assert.throws( () => checker.parseBudgetDocument(Buffer.from(canonicalJson(coordinatedMutation)), paths.v7), /current v7 file SHA-256 mismatch/u, @@ -1632,6 +1661,8 @@ test("published build metadata binds source, conformance, and WASM inputs", () = ); assert.match(prepare, /bytes: runtimeWasm\.length/u); assert.match(prepare, /bytes: compilerWasm\.length/u); + assert.match(prepare, /sha256: sha256\(runtimeWasm\)/u); + assert.match(prepare, /sha256: sha256\(compilerWasm\)/u); const verifier = read("scripts", "verify-package-release.mjs"); assert.match(verifier, /import \{ workspaceVersion \} from "\.\/cargo-workspace\.mjs";/); diff --git a/scripts/check-wasm-size-budget.mjs b/scripts/check-wasm-size-budget.mjs index 0271fa0a..6543bd35 100644 --- a/scripts/check-wasm-size-budget.mjs +++ b/scripts/check-wasm-size-budget.mjs @@ -34,7 +34,7 @@ export const V5_FILE_SHA256 = export const V6_FILE_SHA256 = "761af6050031169dac7eafdfadb2db9bbb2023b96ed5ba9d3c5dc966ffeafb32"; export const V7_FILE_SHA256 = - "a57e94d4cf6b0df7048ec2d808f78eef301e80064bee0648179af83e729a95c3"; + "01d17c042b7dc36585e9657490048932fdf61d4715099b735aa3bf2d3dc5777e"; const V1_REPOSITORY_PATH = "packages/colors/bench/wasm-size-budget-v1.json"; const V6_REPOSITORY_PATH = "packages/colors/bench/wasm-size-budget-v6.json"; @@ -182,7 +182,7 @@ function validateBudgetValue(budget) { ], "budget", ); - if (budget.schemaVersion !== 5) fail("supported schemaVersion is exactly 5"); + if (budget.schemaVersion !== 6) fail("supported schemaVersion is exactly 6"); if (budget.budgetId !== V7_BUDGET_ID) fail(`budgetId must be ${V7_BUDGET_ID}`); exactKeys(budget.predecessor, ["path", "fileSha256"], "predecessor"); @@ -226,7 +226,7 @@ function validateBudgetValue(budget) { } exactKeys( record.measurement, - ["issue", "slice", "measurementPlatform", "rawBytes", "sha256"], + ["issue", "slice", "measurementPlatform", "rawBytes"], `roles.${role}.measurement`, ); if ( @@ -242,7 +242,6 @@ function validateBudgetValue(budget) { 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`); @@ -314,7 +313,6 @@ export function evaluateWasmBudget(role, record, wasm, currentPlatform) { const rawBytes = bytes.length; const gzipBytes = gzipSync(bytes, { level: 9 }).length; const artifactSha256 = sha256(bytes); - const artifactSha = artifactSha256 === record.measurement.sha256 ? "match" : "different"; const isCanonicalPlatform = currentPlatform === record.measurement.measurementPlatform; if (isCanonicalPlatform && rawBytes !== record.measurement.rawBytes) { fail( @@ -323,14 +321,6 @@ export function evaluateWasmBudget(role, record, wasm, currentPlatform) { `gzip=${gzipBytes}B diagnostic-only sha256=${artifactSha256}`, ); } - if (isCanonicalPlatform && artifactSha !== "match") { - fail( - `${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", @@ -339,7 +329,6 @@ export function evaluateWasmBudget(role, record, wasm, currentPlatform) { deltaBytes: rawBytes - record.policy.maxRawBytes, gzipBytes, currentPlatform, - artifactSha, artifactSha256, }; } @@ -350,7 +339,7 @@ function formatResult(result, artifact) { `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` + `artifact-sha256=${result.artifactSha256} declared-recipe-sha=match` ); } From 8b3fe904d9dd34527e10213ad46e5ab51f6f552f Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Fri, 17 Jul 2026 00:14:11 +0300 Subject: [PATCH 5/7] test: make wasm repeatability claims falsifiable --- .github/workflows/ci.yml | 17 +-- packages/colors/README.md | 8 +- .../colors/test/release-contract.test.mjs | 108 +++++++++++++----- scripts/check-wasm-size-budget.mjs | 2 +- 4 files changed, 97 insertions(+), 38 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dac7b426..a7dfcefa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -466,14 +466,15 @@ jobs: run: | cargo install wasm-pack --version 0.13.1 --locked echo "$CARGO_HOME/bin" >> "$GITHUB_PATH" - - name: reproduce WASM roles from the same source + - name: repeat WASM role builds in one pinned job # 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. - # The second build follows `cargo clean`, so equality proves same-source - # reproduction instead of comparing unrelated source commits by SHA. + # The second build follows `cargo clean`; equality checks repeatability + # for this source inside one pinned job, not cross-run identity. run: | + set -euo pipefail export CARGO_ENCODED_RUSTFLAGS="--remap-path-prefix=$GITHUB_WORKSPACE=/workspace/lab-colors"$'\x1f'"--remap-path-prefix=$CARGO_HOME=/cargo-home" build_roles() { wasm-pack build crates/labcolors-wasm --release --target web --out-dir ../../packages/colors/pkg --out-name labcolors --locked @@ -500,10 +501,12 @@ jobs: packages/colors/pkg/labcolors_bg.wasm \ packages/colors/compiler/labcolors_compiler_bg.wasm do - if LC_ALL=C grep -a -q -E '/(Users|home)/[^[:cntrl:]]*/\.cargo/registry/src/|/opt/actions-runner/[^[:cntrl:]]*/cargo-wasm/registry/src/' "$wasm"; then - echo "unmapped build path in $wasm" >&2 - exit 1 - fi + for root in "$GITHUB_WORKSPACE" "$CARGO_HOME" "$RUSTUP_HOME"; do + if LC_ALL=C grep -a -F -q -- "$root/" "$wasm"; then + echo "unmapped build path $root in $wasm" >&2 + exit 1 + fi + done done sha256sum \ packages/colors/pkg/labcolors_bg.wasm \ diff --git a/packages/colors/README.md b/packages/colors/README.md index 879478cb..8671063f 100644 --- a/packages/colors/README.md +++ b/packages/colors/README.md @@ -627,9 +627,11 @@ headroom для `runtime` и `compiler`; checker выбирает текущую притворяется идентификатором артефакта: фактический SHA каждой роли вместе с source SHA записывается в `build-metadata.json` и повторно сверяется с точными байтами tarball при публикации. Release-equivalent CI требует точного размера -обеих ролей и неизменных рецептов сборки. На других host-платформах checker -сообщает только raw/gzip/SHA-диагностику и не выдаёт локальные байты за -канонический release artifact. +обеих ролей и неизменных рецептов сборки. Две чистые сборки дополнительно +сравниваются внутри одного pinned Linux job; это проверка повторяемости в данном +job, а не утверждение о cross-run или cross-machine reproducibility. На других +host-платформах checker сообщает только raw/gzip/SHA-диагностику и не выдаёт +локальные байты за канонический release artifact. Runtime и offline compiler поставляются двумя независимо загружаемыми `.wasm`-ассетами. Будет ли runtime-загрузка критическим путём первого рендера, diff --git a/packages/colors/test/release-contract.test.mjs b/packages/colors/test/release-contract.test.mjs index bee02b94..4ec89795 100644 --- a/packages/colors/test/release-contract.test.mjs +++ b/packages/colors/test/release-contract.test.mjs @@ -1288,33 +1288,82 @@ test("WASM role size budgets are exact, append-only, and acyclic", async () => { assert.match(wasmJob, /runs-on: \[self-hosted, Linux, X64\]/u); assert.match(wasmJob, /GITHUB_WORKSPACE=\/workspace\/lab-colors/u); assert.match(wasmJob, /CARGO_HOME=\/cargo-home/u); - const reproduction = workflowRunScript( + const repetition = workflowRunScript( ci, - "name: reproduce WASM roles from the same source", - ); - assert.equal( - reproduction.match(/wasm-pack build crates\/labcolors-wasm/gu)?.length, - 1, - "runtime build recipe must have one shell SSOT", - ); - assert.equal( - reproduction.match(/wasm-pack build crates\/labcolors-compiler/gu)?.length, - 1, - "compiler build recipe must have one shell SSOT", - ); - assert.equal( - reproduction.match(/^\s*build_roles$/gmu)?.length, - 2, - "the same recipe must run exactly twice", - ); - assert.match(reproduction, /cargo clean/u); - assert.match(reproduction, /cp -a packages\/colors\/pkg/u); - assert.match(reproduction, /cp -a packages\/colors\/compiler/u); - assert.equal( - reproduction.match(/diff --no-dereference --recursive/gu)?.length, - 2, + "name: repeat WASM role builds in one pinned job", + ); + const recipePrefix = "CARGO_ENCODED_RUSTFLAGS= "; + const expectedRemapExport = `export CARGO_ENCODED_RUSTFLAGS=${v1.measurement.rustPathRemap + .map((mapping) => { + const separator = mapping.indexOf("="); + assert.ok(separator > 0, "path remap must name one environment source"); + return `"--remap-path-prefix=\$${mapping.slice(0, separator)}=${mapping.slice(separator + 1)}"`; + }) + .join("$'\\x1f'")}`; + const expectedBuilds = ["runtime", "compiler"].map((role) => { + const command = v7.buildRecipes[role].command; + assert.ok(command.startsWith(recipePrefix)); + return command.slice(recipePrefix.length); + }); + const expectedDiffs = [ + 'diff --no-dereference --recursive "$first/pkg" packages/colors/pkg', + 'diff --no-dereference --recursive "$first/compiler" packages/colors/compiler', + ]; + const assertRepeatabilityContract = (script) => { + assert.match(script, /^set -euo pipefail$/mu); + assert.deepEqual( + script.split("\n").filter((line) => line.startsWith("export CARGO_ENCODED_RUSTFLAGS=")), + [expectedRemapExport], + "the live path-remap command must equal the versioned budget declaration", + ); + const functionBody = script.match( + /(?:^|\n)build_roles\(\) \{\n(?(?: [^\n]+\n)+)\}/u, + )?.groups?.body; + assert.ok(functionBody, "build_roles must be one bounded shell function"); + assert.deepEqual( + functionBody.split("\n").map((line) => line.trim()).filter(Boolean), + expectedBuilds, + "CI build commands must equal the versioned budget recipe commands", + ); + assert.equal( + script.match(/^build_roles$/gmu)?.length, + 2, + "the same recipe must run exactly twice", + ); + assert.match(script, /^cargo clean$/mu); + assert.match(script, /^cp -a packages\/colors\/pkg "\$first\/pkg"$/mu); + assert.match(script, /^cp -a packages\/colors\/compiler "\$first\/compiler"$/mu); + assert.deepEqual( + script.split("\n").filter((line) => line.startsWith("diff ")), + expectedDiffs, + "both output directories must be compared by fail-closed exact commands", + ); + assert.match( + script, + /^ for root in "\$GITHUB_WORKSPACE" "\$CARGO_HOME" "\$RUSTUP_HOME"; do$/mu, + ); + assert.match( + script, + /^ if LC_ALL=C grep -a -F -q -- "\$root\/" "\$wasm"; then$/mu, + ); + }; + assertRepeatabilityContract(repetition); + + const recipeBypass = repetition.replace( + expectedBuilds[0], + `${expectedBuilds[0]} --features unreviewed`, ); - assert.match(reproduction, /\/opt\/actions-runner\/[^\n]*cargo-wasm\/registry\/src\//u); + assert.notEqual(recipeBypass, repetition, "recipe mutation must bite a real command"); + assert.throws(() => assertRepeatabilityContract(recipeBypass)); + + const diffBypass = repetition.replace(expectedDiffs[0], `${expectedDiffs[0]} || true`); + assert.notEqual(diffBypass, repetition, "diff mutation must bite a real command"); + assert.throws(() => assertRepeatabilityContract(diffBypass)); + + const pathCheck = 'if LC_ALL=C grep -a -F -q -- "$root/" "$wasm"; then'; + const pathBypass = repetition.replace(pathCheck, `${pathCheck} : || true`); + assert.notEqual(pathBypass, repetition, "path mutation must bite the live guard"); + assert.throws(() => assertRepeatabilityContract(pathBypass)); const temporary = mkdtempSync(join(tmpdir(), "labcolors-wasm-role-budget-v7-")); try { @@ -1353,11 +1402,16 @@ test("WASM role size budgets are exact, append-only, and acyclic", async () => { const output = run(); assert.match( output, - /role=runtime raw=16B .*artifact-sha256=[0-9a-f]{64} declared-recipe-sha=match/u, + /role=runtime raw=16B .*artifact-sha256=[0-9a-f]{64}/u, ); assert.match( output, - /role=compiler raw=17B .*artifact-sha256=[0-9a-f]{64} declared-recipe-sha=match/u, + /role=compiler raw=17B .*artifact-sha256=[0-9a-f]{64}/u, + ); + assert.doesNotMatch( + output, + /(?:recipe|artifact)-sha(?:256)?=match/u, + "the size checker cannot infer artifact provenance from arbitrary input bytes", ); const schemaMutations = [ diff --git a/scripts/check-wasm-size-budget.mjs b/scripts/check-wasm-size-budget.mjs index 6543bd35..4448ae17 100644 --- a/scripts/check-wasm-size-budget.mjs +++ b/scripts/check-wasm-size-budget.mjs @@ -339,7 +339,7 @@ function formatResult(result, artifact) { `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-sha256=${result.artifactSha256} declared-recipe-sha=match` + `artifact-sha256=${result.artifactSha256}` ); } From 2ee9f248d28453703264c14dc31e4ade6f86a6ed Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Fri, 17 Jul 2026 00:19:05 +0300 Subject: [PATCH 6/7] docs: fix PairLabel wording --- docs/whitepaper.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/whitepaper.md b/docs/whitepaper.md index a1077352..e68c5121 100644 --- a/docs/whitepaper.md +++ b/docs/whitepaper.md @@ -217,8 +217,8 @@ task #29). `RoleRecipe::PairLabel` решает лейбл штатным зак решения граф не назначает. Дифференциальный тест закрепляет подключение и результаты миграции на проверяемом домене; оба пути используют один последующий резолвер и не являются независимым эталоном его -математики. Лейбл не несёт typography-фактов, поэтому -никакой размер/вес текста здесь не учитывается и не обещается. Старая ручная +математики. Лейбл не несёт типографических фактов, поэтому +ни размер, ни вес текста здесь не учитываются и не обещаются. Старая ручная композиция заморожена как тестовый эталон для дифференциального сравнения; матрица 5 семей × 4 режима × 6 фонов + property-тесты доказывают байт-идентичность production-пути ей (`migration_*`, From 67b1cbaa2e9176280201b696f5269ebc00d1c08d Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Fri, 17 Jul 2026 00:27:15 +0300 Subject: [PATCH 7/7] test: close wasm repeatability bypasses --- .github/workflows/ci.yml | 15 +++++-- packages/colors/README.md | 11 ++--- .../colors/test/release-contract.test.mjs | 45 +++++++++++++------ 3 files changed, 49 insertions(+), 22 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a7dfcefa..54a223a2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -466,13 +466,14 @@ jobs: run: | cargo install wasm-pack --version 0.13.1 --locked echo "$CARGO_HOME/bin" >> "$GITHUB_PATH" - - name: repeat WASM role builds in one pinned job + - name: repeat WASM role builds in one toolchain-pinned CI job # 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. # The second build follows `cargo clean`; equality checks repeatability - # for this source inside one pinned job, not cross-run identity. + # for this source inside this toolchain-pinned job, not cross-run or + # cross-host identity. run: | set -euo pipefail export CARGO_ENCODED_RUSTFLAGS="--remap-path-prefix=$GITHUB_WORKSPACE=/workspace/lab-colors"$'\x1f'"--remap-path-prefix=$CARGO_HOME=/cargo-home" @@ -494,8 +495,14 @@ jobs: rm -rf packages/colors/pkg packages/colors/compiler build_roles - diff --no-dereference --recursive "$first/pkg" packages/colors/pkg - diff --no-dereference --recursive "$first/compiler" packages/colors/compiler + if ! diff --no-dereference --recursive "$first/pkg" packages/colors/pkg; then + echo "runtime WASM output changed between builds" >&2 + exit 1 + fi + if ! diff --no-dereference --recursive "$first/compiler" packages/colors/compiler; then + echo "compiler WASM output changed between builds" >&2 + exit 1 + fi for wasm in \ packages/colors/pkg/labcolors_bg.wasm \ diff --git a/packages/colors/README.md b/packages/colors/README.md index 8671063f..707f2f68 100644 --- a/packages/colors/README.md +++ b/packages/colors/README.md @@ -627,11 +627,12 @@ headroom для `runtime` и `compiler`; checker выбирает текущую притворяется идентификатором артефакта: фактический SHA каждой роли вместе с source SHA записывается в `build-metadata.json` и повторно сверяется с точными байтами tarball при публикации. Release-equivalent CI требует точного размера -обеих ролей и неизменных рецептов сборки. Две чистые сборки дополнительно -сравниваются внутри одного pinned Linux job; это проверка повторяемости в данном -job, а не утверждение о cross-run или cross-machine reproducibility. На других -host-платформах checker сообщает только raw/gzip/SHA-диагностику и не выдаёт -локальные байты за канонический release artifact. +обеих ролей и неизменных рецептов сборки. CI собирает роли, выполняет +`cargo clean`, повторяет сборку и сравнивает результаты внутри одного Linux job +с закреплённым toolchain; это проверка повторяемости в данном job, а не +утверждение о cross-run или cross-host reproducibility. На других host-платформах +checker сообщает только raw/gzip/SHA-диагностику и не выдаёт локальные байты за +канонический release artifact. Runtime и offline compiler поставляются двумя независимо загружаемыми `.wasm`-ассетами. Будет ли runtime-загрузка критическим путём первого рендера, diff --git a/packages/colors/test/release-contract.test.mjs b/packages/colors/test/release-contract.test.mjs index 4ec89795..e9d73834 100644 --- a/packages/colors/test/release-contract.test.mjs +++ b/packages/colors/test/release-contract.test.mjs @@ -1290,7 +1290,7 @@ test("WASM role size budgets are exact, append-only, and acyclic", async () => { assert.match(wasmJob, /CARGO_HOME=\/cargo-home/u); const repetition = workflowRunScript( ci, - "name: repeat WASM role builds in one pinned job", + "name: repeat WASM role builds in one toolchain-pinned CI job", ); const recipePrefix = "CARGO_ENCODED_RUSTFLAGS= "; const expectedRemapExport = `export CARGO_ENCODED_RUSTFLAGS=${v1.measurement.rustPathRemap @@ -1309,6 +1309,20 @@ test("WASM role size budgets are exact, append-only, and acyclic", async () => { 'diff --no-dereference --recursive "$first/pkg" packages/colors/pkg', 'diff --no-dereference --recursive "$first/compiler" packages/colors/compiler', ]; + const expectedDiffBlocks = expectedDiffs.map((command, index) => [ + `if ! ${command}; then`, + ` echo "${index === 0 ? "runtime" : "compiler"} WASM output changed between builds" >&2`, + " exit 1", + "fi", + ].join("\n")); + const expectedPathGuard = [ + ' for root in "$GITHUB_WORKSPACE" "$CARGO_HOME" "$RUSTUP_HOME"; do', + ' if LC_ALL=C grep -a -F -q -- "$root/" "$wasm"; then', + ' echo "unmapped build path $root in $wasm" >&2', + " exit 1", + " fi", + " done", + ].join("\n"); const assertRepeatabilityContract = (script) => { assert.match(script, /^set -euo pipefail$/mu); assert.deepEqual( @@ -1334,17 +1348,17 @@ test("WASM role size budgets are exact, append-only, and acyclic", async () => { assert.match(script, /^cp -a packages\/colors\/pkg "\$first\/pkg"$/mu); assert.match(script, /^cp -a packages\/colors\/compiler "\$first\/compiler"$/mu); assert.deepEqual( - script.split("\n").filter((line) => line.startsWith("diff ")), - expectedDiffs, + [...script.matchAll(/^if ! diff[^\n]+\n echo [^\n]+\n exit 1\nfi$/gmu)] + .map((match) => match[0]), + expectedDiffBlocks, "both output directories must be compared by fail-closed exact commands", ); - assert.match( - script, - /^ for root in "\$GITHUB_WORKSPACE" "\$CARGO_HOME" "\$RUSTUP_HOME"; do$/mu, - ); - assert.match( - script, - /^ if LC_ALL=C grep -a -F -q -- "\$root\/" "\$wasm"; then$/mu, + assert.equal( + script.match( + /^ for root in "\$GITHUB_WORKSPACE" "\$CARGO_HOME" "\$RUSTUP_HOME"; do\n if LC_ALL=C grep -a -F -q -- "\$root\/" "\$wasm"; then\n echo "unmapped build path \$root in \$wasm" >&2\n exit 1\n fi\n done$/mu, + )?.[0], + expectedPathGuard, + "host-path rejection must remain one fail-closed exact block", ); }; assertRepeatabilityContract(repetition); @@ -1356,12 +1370,17 @@ test("WASM role size budgets are exact, append-only, and acyclic", async () => { assert.notEqual(recipeBypass, repetition, "recipe mutation must bite a real command"); assert.throws(() => assertRepeatabilityContract(recipeBypass)); - const diffBypass = repetition.replace(expectedDiffs[0], `${expectedDiffs[0]} || true`); + const diffBypass = repetition.replace( + expectedDiffBlocks[0], + expectedDiffBlocks[0].replace(" exit 1", " :"), + ); assert.notEqual(diffBypass, repetition, "diff mutation must bite a real command"); assert.throws(() => assertRepeatabilityContract(diffBypass)); - const pathCheck = 'if LC_ALL=C grep -a -F -q -- "$root/" "$wasm"; then'; - const pathBypass = repetition.replace(pathCheck, `${pathCheck} : || true`); + const pathBypass = repetition.replace( + expectedPathGuard, + expectedPathGuard.replace(" exit 1", " :"), + ); assert.notEqual(pathBypass, repetition, "path mutation must bite the live guard"); assert.throws(() => assertRepeatabilityContract(pathBypass));