From d6348fc556f15844d5f56315d826ff452bb3e6dc Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Sun, 19 Jul 2026 11:16:16 +0300 Subject: [PATCH 1/2] =?UTF-8?q?F1:=20=D1=81=D0=B2=D1=8F=D0=B7=D0=B0=D1=82?= =?UTF-8?q?=D1=8C=20evaluator=20=D1=81=20=D0=B2=D0=B8=D0=B4=D0=B8=D0=BC?= =?UTF-8?q?=D1=8B=D0=BC=20occurrence?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/labcolors-core/src/appearance.rs | 54 +++- crates/labcolors-core/src/constraint_tests.rs | 298 ++++++++++++++++++ crates/labcolors-core/src/constraints/mod.rs | 123 ++++++++ .../labcolors-core/src/constraints/wcag22.rs | 29 ++ crates/labcolors-core/src/lib.rs | 5 + crates/labcolors-core/src/semantic.rs | 14 +- 6 files changed, 514 insertions(+), 9 deletions(-) create mode 100644 crates/labcolors-core/src/constraint_tests.rs create mode 100644 crates/labcolors-core/src/constraints/mod.rs create mode 100644 crates/labcolors-core/src/constraints/wcag22.rs diff --git a/crates/labcolors-core/src/appearance.rs b/crates/labcolors-core/src/appearance.rs index 1201d1ac..d5a013fe 100644 --- a/crates/labcolors-core/src/appearance.rs +++ b/crates/labcolors-core/src/appearance.rs @@ -785,7 +785,7 @@ impl PointOpacityOverSurfaceV1 { source: [u8; 3], opacity: f64, backdrop: [u8; 3], - ) -> Result<[u8; 3], PointOpacityError> { + ) -> Result { crate::alpha::validate_alpha(opacity).map_err(|message| PointOpacityError { message })?; let opacity = if opacity == 0.0 { 0.0 } else { opacity }; let mut paints = [None; 2]; @@ -805,8 +805,8 @@ impl PointOpacityOverSurfaceV1 { &mut surfaces, &mut occurrences, ); - Ok(surfaces[1].unwrap_or_else(|| { - unreachable!("compiler-verified point program materializes its output Surface") + Ok(occurrences[0].unwrap_or_else(|| { + unreachable!("compiler-verified point program materializes its Occurrence") })) } } @@ -996,12 +996,60 @@ impl ResolvedOccurrence { self.visible } + #[cfg(test)] + pub(crate) fn modeled_srgb8_point(&self) -> ModeledSrgb8PointOccurrence { + ModeledSrgb8PointOccurrence { + visible: self.visible, + backdrop: self.backdrop, + } + } + + #[cfg(test)] + pub(crate) fn visible_point_binding(&self) -> VisiblePointBindingV1 { + VisiblePointBindingV1(self.certificate) + } + #[cfg(test)] pub(crate) fn certificate(&self) -> &SourceOverCertificateV1 { &self.certificate } } +/// Owned point-target для evaluator-ов финального видимого результата. +/// Ссылки на occurrence/certificate здесь нет: evaluator структурно не может +/// подменить скомпозитированный stimulus authored source-цветом. +#[derive(Debug, Clone, Copy)] +#[cfg(test)] +pub(crate) struct ModeledSrgb8PointOccurrence { + visible: [u8; 3], + backdrop: [u8; 3], +} + +#[cfg(test)] +impl ModeledSrgb8PointOccurrence { + pub(crate) fn visible(self) -> [u8; 3] { + self.visible + } + + pub(crate) fn backdrop(self) -> [u8; 3] { + self.backdrop + } +} + +/// Physical identity modeled point-occurrence, связанная с exact source-over +/// proof. Assessment не может пережить смену Paint/Surface/alpha лишь потому, +/// что финальные байты случайно совпали. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg(test)] +pub(crate) struct VisiblePointBindingV1(SourceOverCertificateV1); + +#[cfg(test)] +impl VisiblePointBindingV1 { + pub(crate) fn certificate(self) -> SourceOverCertificateV1 { + self.0 + } +} + /// Полный атомарный результат evaluate в каноническом typed-ID порядке. #[cfg(test)] #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/crates/labcolors-core/src/constraint_tests.rs b/crates/labcolors-core/src/constraint_tests.rs new file mode 100644 index 00000000..838e83f2 --- /dev/null +++ b/crates/labcolors-core/src/constraint_tests.rs @@ -0,0 +1,298 @@ +use proptest::prelude::*; + +use crate::appearance::{ + AppearanceBindings, AppearanceGraphSpec, ColorInputId, CompositionProfileV1, OccurrenceId, + OccurrenceSpec, OpacityInputId, PaintId, PaintSpec, PointOpacityOverSurfaceV1, SurfaceId, + SurfaceSpec, +}; +use crate::constraints::{BoundAssessment, Wcag22Srgb8V1, assess}; +use crate::wcag22::{ + Wcag22ApplicableDecisionV1, Wcag22AssessmentV1, Wcag22CriterionV1, Wcag22ProfileIdV1, + evaluate_wcag22_srgb8, wcag22_profile_v1, +}; + +fn point_occurrence( + source: [u8; 3], + opacity: f64, + backdrop: [u8; 3], +) -> crate::appearance::ResolvedOccurrence { + PointOpacityOverSurfaceV1::evaluate(source, opacity, backdrop) + .unwrap_or_else(|error| panic!("valid point occurrence rejected: {}", error.message())) +} + +fn wcag_assessment( + source: [u8; 3], + opacity: f64, + backdrop: [u8; 3], + criterion: Wcag22CriterionV1, +) -> BoundAssessment +{ + let occurrence = point_occurrence(source, opacity, backdrop); + assess(&occurrence, &Wcag22Srgb8V1, criterion) + .expect("proof-bound WCAG evaluator must decide every admitted sRGB8 pair") +} + +#[test] +fn wcag_reads_final_visible_occurrence_in_measurement_order() { + let report = wcag_assessment( + [0, 0, 0], + 0.5, + [255, 255, 255], + Wcag22CriterionV1::Sc143TextDefault, + ); + + let Wcag22AssessmentV1::Evaluated { + measurement, + decision, + .. + } = report.assessment() + else { + panic!("required invocation cannot become NotEvaluated"); + }; + assert_eq!(measurement.foreground, [128, 128, 128]); + assert_eq!(measurement.background, [255, 255, 255]); + assert_eq!(*decision, Wcag22ApplicableDecisionV1::Fail); + assert_eq!( + report.binding().certificate().occurrence(), + OccurrenceId::new(0) + ); + + let subject_decision = evaluate_wcag22_srgb8( + [0, 0, 0], + [255, 255, 255], + Wcag22CriterionV1::Sc143TextDefault, + ) + .expect("control pair is in domain"); + assert!(matches!( + subject_decision, + Wcag22AssessmentV1::Evaluated { + decision: Wcag22ApplicableDecisionV1::Pass, + .. + } + )); +} + +#[test] +fn modeled_target_preserves_chromatic_channel_order() { + let occurrence = point_occurrence([0x12, 0x34, 0xAB], 1.0, [0xF0, 0x40, 0x20]); + let target = occurrence.modeled_srgb8_point(); + + assert_eq!(target.visible(), [0x12, 0x34, 0xAB]); + assert_eq!(target.backdrop(), [0xF0, 0x40, 0x20]); +} + +#[test] +fn modeled_target_has_no_binding_or_source_capability() { + let source = include_str!("appearance.rs"); + let (_, target_tail) = source + .split_once("pub(crate) struct ModeledSrgb8PointOccurrence {") + .expect("modeled target declaration"); + let (target_surface, _) = target_tail + .split_once("/// Physical identity modeled point-occurrence") + .expect("binding declaration follows modeled target"); + + for forbidden in [ + "ResolvedOccurrence", + "VisiblePointBindingV1", + "SourceOverCertificateV1", + "fn binding", + "certificate:", + ] { + assert!( + !target_surface.contains(forbidden), + "evaluator target leaked forbidden capability {forbidden}" + ); + } +} + +fn two_equal_physical_occurrences() -> [crate::appearance::ResolvedOccurrence; 2] { + let source = ColorInputId::new(0); + let backdrop = ColorInputId::new(1); + let opacity = OpacityInputId::new(0); + let solid = PaintId::new(0); + let translucent = PaintId::new(1); + let backdrop_surface = SurfaceId::new(0); + let first_surface = SurfaceId::new(1); + let second_surface = SurfaceId::new(2); + let first = OccurrenceId::new(0); + let second = OccurrenceId::new(1); + let graph = AppearanceGraphSpec::new( + vec![source, backdrop], + vec![opacity], + vec![ + PaintSpec::Solid { + id: solid, + color: source, + }, + PaintSpec::Opacity { + id: translucent, + source: solid, + opacity, + }, + ], + vec![ + SurfaceSpec::Input { + id: backdrop_surface, + color: backdrop, + }, + SurfaceSpec::FromOccurrence { + id: first_surface, + occurrence: first, + }, + SurfaceSpec::FromOccurrence { + id: second_surface, + occurrence: second, + }, + ], + vec![ + OccurrenceSpec { + id: first, + subject: translucent, + against: backdrop_surface, + profile: CompositionProfileV1::EncodedSrgb8SourceOverV1, + }, + OccurrenceSpec { + id: second, + subject: translucent, + against: backdrop_surface, + profile: CompositionProfileV1::EncodedSrgb8SourceOverV1, + }, + ], + ) + .compile() + .expect("acyclic typed graph must compile"); + let evaluation = graph + .evaluate(&AppearanceBindings::new( + vec![(source, [0, 64, 255]), (backdrop, [255, 255, 255])], + vec![(opacity, 0.5)], + )) + .expect("complete bindings must evaluate"); + [ + *evaluation.occurrence(first).expect("first occurrence"), + *evaluation.occurrence(second).expect("second occurrence"), + ] +} + +#[test] +fn equal_physics_under_distinct_occurrence_ids_keeps_distinct_bindings() { + let [first, second] = two_equal_physical_occurrences(); + let first_report = assess( + &first, + &Wcag22Srgb8V1, + Wcag22CriterionV1::Sc1411UiComponentOrState, + ) + .expect("first assessment"); + let second_report = assess( + &second, + &Wcag22Srgb8V1, + Wcag22CriterionV1::Sc1411UiComponentOrState, + ) + .expect("second assessment"); + + assert_eq!(first_report.assessment(), second_report.assessment()); + assert_ne!(first_report.binding(), second_report.binding()); +} + +#[test] +fn same_ids_and_final_pair_do_not_erase_subject_or_alpha_provenance() { + let transparent_black = wcag_assessment( + [0, 0, 0], + 0.0, + [255, 255, 255], + Wcag22CriterionV1::Sc143TextDefault, + ); + let opaque_white = wcag_assessment( + [255, 255, 255], + 1.0, + [255, 255, 255], + Wcag22CriterionV1::Sc143TextDefault, + ); + + assert_eq!(transparent_black.assessment(), opaque_white.assessment()); + assert_ne!(transparent_black.binding(), opaque_white.binding()); +} + +proptest! { + #[test] + fn bound_wcag_adapter_matches_standalone_final_pair( + source in any::<[u8; 3]>(), + backdrop in any::<[u8; 3]>(), + opacity_step in 0_u16..=1024, + ) { + let opacity = f64::from(opacity_step) / 1024.0; + let occurrence = point_occurrence(source, opacity, backdrop); + let final_visible = occurrence.visible(); + let final_backdrop = occurrence.backdrop(); + let target = occurrence.modeled_srgb8_point(); + for criterion in Wcag22CriterionV1::ALL { + let report = assess(&occurrence, &Wcag22Srgb8V1, criterion) + .expect("finite WCAG table must decide every admitted pair"); + let standalone = evaluate_wcag22_srgb8(final_visible, final_backdrop, criterion) + .expect("same admitted pair must be decided by standalone evaluator"); + + prop_assert_eq!(target.visible(), final_visible); + prop_assert_eq!(target.backdrop(), final_backdrop); + prop_assert_eq!(report.assessment(), &standalone); + prop_assert_eq!(report.binding(), &occurrence.visible_point_binding()); + } + } +} + +#[test] +fn required_criterion_is_not_replaced_by_one_hardcoded_threshold() { + let text = wcag_assessment( + [138, 138, 138], + 1.0, + [255, 255, 255], + Wcag22CriterionV1::Sc143TextDefault, + ); + let large_text = wcag_assessment( + [138, 138, 138], + 1.0, + [255, 255, 255], + Wcag22CriterionV1::Sc143TextLargeScale, + ); + + assert!(matches!( + text.assessment(), + Wcag22AssessmentV1::Evaluated { + decision: Wcag22ApplicableDecisionV1::Fail, + .. + } + )); + assert!(matches!( + large_text.assessment(), + Wcag22AssessmentV1::Evaluated { + decision: Wcag22ApplicableDecisionV1::Pass, + .. + } + )); +} + +#[test] +fn bound_report_release_matches_assessment_and_registry() { + let report = wcag_assessment( + [0, 0, 0], + 1.0, + [255, 255, 255], + Wcag22CriterionV1::Sc143TextDefault, + ); + let Wcag22AssessmentV1::Evaluated { profile_id, .. } = report.assessment() else { + panic!("required invocation cannot become NotEvaluated"); + }; + + assert_eq!(report.release(), profile_id); + assert_eq!(*report.release(), wcag22_profile_v1().profile_id); +} + +#[test] +fn wcag_adapter_contains_delegation_not_a_second_formula() { + let source = include_str!("constraints/wcag22.rs"); + assert_eq!(source.matches("evaluate_wcag22_srgb8(").count(), 1); + for duplicated_math in ["4.5", "3.0", "luminance", "Q55", "powf"] { + assert!( + !source.contains(duplicated_math), + "adapter duplicated WCAG math marker {duplicated_math}" + ); + } +} diff --git a/crates/labcolors-core/src/constraints/mod.rs b/crates/labcolors-core/src/constraints/mod.rs new file mode 100644 index 00000000..05052106 --- /dev/null +++ b/crates/labcolors-core/src/constraints/mod.rs @@ -0,0 +1,123 @@ +//! Приватная typed-связка physical target и evaluator-а. +//! +//! Модуль не является public registry: он лишь гарантирует, что assessment +//! сохраняет identity физического evidence и release реально вызванного +//! evaluator-а. + +use crate::appearance::{ModeledSrgb8PointOccurrence, ResolvedOccurrence, VisiblePointBindingV1}; + +mod wcag22; + +pub(crate) use wcag22::Wcag22Srgb8V1; + +/// Marker-ы недоступны внешним crate-ам: новые target/evaluator families +/// добавляются только вместе с code-owned physical adapter-ом. +mod private { + pub trait EvaluatorSealed {} +} + +pub(crate) trait Evaluator: private::EvaluatorSealed { + type Invocation; + type Release; + type Assessment; + type Error; + + fn release(&self) -> Self::Release; + + fn evaluate( + &self, + target: &Target, + invocation: Self::Invocation, + ) -> Result; +} + +/// Assessment вместе с exact physical binding и evaluator release. +/// Поля закрыты, чтобы genuine result нельзя было пересвязать вручную. +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct BoundAssessment { + binding: Binding, + release: Release, + assessment: Assessment, +} + +impl BoundAssessment { + pub(crate) fn binding(&self) -> &Binding { + &self.binding + } + + pub(crate) fn release(&self) -> &Release { + &self.release + } + + pub(crate) fn assessment(&self) -> &Assessment { + &self.assessment + } +} + +pub(crate) type AssessmentResult = Result< + BoundAssessment< + VisiblePointBindingV1, + >::Release, + >::Assessment, + >, + >::Error, +>; + +pub(crate) fn assess( + source: &ResolvedOccurrence, + evaluator: &Evaluation, + invocation: Evaluation::Invocation, +) -> AssessmentResult +where + Evaluation: Evaluator, +{ + let target = source.modeled_srgb8_point(); + let binding = source.visible_point_binding(); + let assessment = evaluator.evaluate(&target, invocation)?; + Ok(BoundAssessment { + binding, + release: evaluator.release(), + assessment, + }) +} + +#[cfg(test)] +mod tests { + use super::{Evaluator, ModeledSrgb8PointOccurrence, assess, private}; + use crate::appearance::PointOpacityOverSurfaceV1; + + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + struct SentinelError; + + struct FailingEvaluator; + + impl private::EvaluatorSealed for FailingEvaluator {} + + impl Evaluator for FailingEvaluator { + type Invocation = (); + type Release = &'static str; + type Assessment = (); + type Error = SentinelError; + + fn release(&self) -> Self::Release { + "sentinel-v1" + } + + fn evaluate( + &self, + _target: &ModeledSrgb8PointOccurrence, + _invocation: Self::Invocation, + ) -> Result { + Err(SentinelError) + } + } + + #[test] + fn evaluator_error_is_returned_without_report_or_fallback() { + let occurrence = PointOpacityOverSurfaceV1::evaluate([1, 2, 3], 0.5, [4, 5, 6]) + .unwrap_or_else(|error| panic!("valid point occurrence rejected: {}", error.message())); + let error = assess(&occurrence, &FailingEvaluator, ()) + .expect_err("binder must preserve evaluator failure"); + assert_eq!(error, SentinelError); + } +} diff --git a/crates/labcolors-core/src/constraints/wcag22.rs b/crates/labcolors-core/src/constraints/wcag22.rs new file mode 100644 index 00000000..798cad59 --- /dev/null +++ b/crates/labcolors-core/src/constraints/wcag22.rs @@ -0,0 +1,29 @@ +use crate::appearance::ModeledSrgb8PointOccurrence; +use crate::constraints::{Evaluator, private}; +use crate::wcag22::{ + Wcag22AssessmentV1, Wcag22CriterionV1, Wcag22EvaluationErrorV1, Wcag22ProfileIdV1, + evaluate_wcag22_srgb8, wcag22_profile_v1, +}; + +pub(crate) struct Wcag22Srgb8V1; + +impl private::EvaluatorSealed for Wcag22Srgb8V1 {} + +impl Evaluator for Wcag22Srgb8V1 { + type Invocation = Wcag22CriterionV1; + type Release = Wcag22ProfileIdV1; + type Assessment = Wcag22AssessmentV1; + type Error = Wcag22EvaluationErrorV1; + + fn release(&self) -> Self::Release { + wcag22_profile_v1().profile_id + } + + fn evaluate( + &self, + target: &ModeledSrgb8PointOccurrence, + invocation: Self::Invocation, + ) -> Result { + evaluate_wcag22_srgb8(target.visible(), target.backdrop(), invocation) + } +} diff --git a/crates/labcolors-core/src/lib.rs b/crates/labcolors-core/src/lib.rs index c113be45..16c9c3f2 100644 --- a/crates/labcolors-core/src/lib.rs +++ b/crates/labcolors-core/src/lib.rs @@ -15,6 +15,8 @@ pub(crate) mod accent_balance; pub mod alpha; pub(crate) mod appearance; pub mod config; +#[cfg(test)] +pub(crate) mod constraints; pub mod glow; pub mod hash; pub mod ladder; @@ -46,6 +48,9 @@ mod agnostic_gates; #[cfg(test)] mod appearance_graph_tests; +#[cfg(test)] +mod constraint_tests; + #[cfg(test)] mod wcag22_tests; diff --git a/crates/labcolors-core/src/semantic.rs b/crates/labcolors-core/src/semantic.rs index 381c8209..ea4bb9de 100644 --- a/crates/labcolors-core/src/semantic.rs +++ b/crates/labcolors-core/src/semantic.rs @@ -2667,13 +2667,15 @@ pub(crate) fn resolve_pair_label( ))); } }; - let surface_rgb = match PointOpacityOverSurfaceV1::evaluate(source_rgb, alpha, context_rgb) { - Ok(surface) => surface, - Err(error) => { - return pair_label_opacity_input_error(error.message()); - } - }; + let surface_occurrence = + match PointOpacityOverSurfaceV1::evaluate(source_rgb, alpha, context_rgb) { + Ok(occurrence) => occurrence, + Err(error) => { + return pair_label_opacity_input_error(error.message()); + } + }; // Финальные байты реально собранной surface становятся контекстом solve. + let surface_rgb = surface_occurrence.visible(); let surface_hex = crate::alpha::hex_from_srgb8(surface_rgb); let Ok(surface_bg) = BgInput::solid(&surface_hex) else { // Композит 8-битных каналов всегда в кубе — недостижимо, но честнее From 309f2d714a40695ccc7c48c7e94d9557fcd1d862 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Sun, 19 Jul 2026 11:44:17 +0300 Subject: [PATCH 2/2] test: ratchet F1a WASM snapshot --- .../colors/bench/wasm-size-budget-v19.json | 33 ++++++++++++ .../colors/test/release-contract.test.mjs | 51 +++++++++++++++---- scripts/check-wasm-size-budget.mjs | 40 +++++++++------ 3 files changed, 97 insertions(+), 27 deletions(-) create mode 100644 packages/colors/bench/wasm-size-budget-v19.json diff --git a/packages/colors/bench/wasm-size-budget-v19.json b/packages/colors/bench/wasm-size-budget-v19.json new file mode 100644 index 00000000..4bef91ac --- /dev/null +++ b/packages/colors/bench/wasm-size-budget-v19.json @@ -0,0 +1,33 @@ +{ + "schemaVersion": 8, + "budgetId": "labcolors-wasm-runtime-f1a-visible-assessment-v19", + "predecessor": { + "path": "packages/colors/bench/wasm-size-budget-v18.json", + "fileSha256": "2ee1e5b4472aa393eb399e3839ab9ac3cfb832a69fa8b704c3fa4854bc1e11f8" + }, + "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" + } + }, + "roles": { + "runtime": { + "artifact": "packages/colors/pkg/labcolors_bg.wasm", + "measurement": { + "source": "github-actions-run-29679743144", + "measurementPlatform": "linux-x64", + "rawBytes": 385589 + }, + "policy": { + "maxRawBytes": 385589, + "basis": "accepted-f1a-visible-assessment-snapshot", + "gzip": "diagnostic-only" + } + } + } +} diff --git a/packages/colors/test/release-contract.test.mjs b/packages/colors/test/release-contract.test.mjs index 0d75cc5f..61131ec8 100644 --- a/packages/colors/test/release-contract.test.mjs +++ b/packages/colors/test/release-contract.test.mjs @@ -1187,7 +1187,7 @@ test("release evidence carries no trace of the excised offline line", () => { 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, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18].map((version) => [ + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19].map((version) => [ `v${version}`, join(bench, `wasm-size-budget-v${version}.json`), ]), @@ -1214,6 +1214,7 @@ test("WASM role size budgets are exact, append-only, and acyclic", async () => { v16: "9a9ab7050a3f6b103905a817b9d675650d04ddab3524a3fc5aa3d0f5d6cab9c3", v17: "8da5e810394ee8fea8491bcd8aff7f3023c267bf31bb5ea4c4b843ef459e7906", v18: "2ee1e5b4472aa393eb399e3839ab9ac3cfb832a69fa8b704c3fa4854bc1e11f8", + v19: "b96d65b06f9bcd7ec568be73bdf5aaea0ed5616f8c104ce6fa33cdf38e7290e2", }; const documents = {}; for (const version of Object.keys(paths)) { @@ -1226,6 +1227,7 @@ test("WASM role size budgets are exact, append-only, and acyclic", async () => { const { v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, + v19, } = documents; assert.equal(v1.budgetId, "labcolors-wasm-raw-issue-284-v1"); assert.equal(v2.budgetId, "labcolors-wasm-raw-issue-295-v2"); @@ -1624,11 +1626,38 @@ test("WASM role size budgets are exact, append-only, and acyclic", async () => { "F0a must lower the exact runtime ratchet by the canonical Linux delta", ); + // V19 (F1a proof-precondition): production point-adapter сохраняет полный + // ResolvedOccurrence; run 29679743144 измерил весь Linux artifact на 21B + // меньше v18. Это дельта снапшота, не стоимость отдельного изменения. + assert.equal(v19.schemaVersion, 8); + assert.equal(v19.budgetId, "labcolors-wasm-runtime-f1a-visible-assessment-v19"); + assert.deepEqual(v19.predecessor, { + path: "packages/colors/bench/wasm-size-budget-v18.json", + fileSha256: expectedHashes.v18, + }); + assert.deepEqual(v19.toolchainSource, v18.toolchainSource); + assert.deepEqual(v19.buildRecipes, v18.buildRecipes); + assert.deepEqual(v19.roles.runtime.measurement, { + source: "github-actions-run-29679743144", + measurementPlatform: "linux-x64", + rawBytes: 385589, + }); + assert.deepEqual(v19.roles.runtime.policy, { + maxRawBytes: 385589, + basis: "accepted-f1a-visible-assessment-snapshot", + gzip: "diagnostic-only", + }); + assert.equal( + v18.roles.runtime.policy.maxRawBytes - v19.roles.runtime.policy.maxRawBytes, + 21, + "F1a proof-precondition must lower the exact runtime ratchet", + ); + const checker = await import( new URL("../../../scripts/check-wasm-size-budget.mjs", import.meta.url) ); - assert.equal(checker.DEFAULT_BUDGET, paths.v18); - for (const version of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18]) { + assert.equal(checker.DEFAULT_BUDGET, paths.v19); + for (const version of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]) { assert.equal(checker[`V${version}_FILE_SHA256`], expectedHashes[`v${version}`]); } assert.equal(checker.V1_RECIPE_SHA256, v5.buildRecipes.runtime.recipeSha256); @@ -1672,7 +1701,7 @@ test("WASM role size budgets are exact, append-only, and acyclic", async () => { return `"--remap-path-prefix=\$${mapping.slice(0, separator)}=${mapping.slice(separator + 1)}"`; }) .join("$'\\x1f'")}`; - const runtimeCommand = v18.buildRecipes.runtime.command; + const runtimeCommand = v19.buildRecipes.runtime.command; assert.ok(runtimeCommand.startsWith(recipePrefix)); const expectedBuild = runtimeCommand.slice(recipePrefix.length); const expectedDiffBlock = [ @@ -1749,13 +1778,13 @@ test("WASM role size budgets are exact, append-only, and acyclic", async () => { assert.notEqual(pathBypass, repetition, "path mutation must bite the live guard"); assert.throws(() => assertRepeatabilityContract(pathBypass)); - const temporary = mkdtempSync(join(tmpdir(), "labcolors-wasm-runtime-budget-v18-")); + const temporary = mkdtempSync(join(tmpdir(), "labcolors-wasm-runtime-budget-v19-")); try { const runtimePath = join(temporary, "runtime.wasm"); const fixtureBudgetPath = join(temporary, "budget.json"); const runtimeBytes = Buffer.alloc(16); runtimeBytes.set([0x00, 0x61, 0x73, 0x6d]); - const fixture = structuredClone(v18); + const fixture = structuredClone(v19); fixture.roles.runtime.measurement.rawBytes = runtimeBytes.length; fixture.roles.runtime.policy.maxRawBytes = runtimeBytes.length; writeFileSync(runtimePath, runtimeBytes); @@ -1828,7 +1857,7 @@ test("WASM role size budgets are exact, append-only, and acyclic", async () => { // acceptedCeiling-закона. Чекер обязан отклонить его всё равно: // рост сверх принятого снапшота требует НОВОЙ версии бюджета, // а не правки текущей. - const ceiling = v18.roles.runtime.policy.maxRawBytes; + const ceiling = v19.roles.runtime.policy.maxRawBytes; value.roles.runtime.measurement.rawBytes = ceiling + 1; value.roles.runtime.policy.maxRawBytes = ceiling + 1; }], @@ -1842,7 +1871,7 @@ test("WASM role size budgets are exact, append-only, and acyclic", async () => { roles: value.roles, })], ]; - assert.equal(schemaMutations.length, 24, "v18 schema mutation set changed"); + assert.equal(schemaMutations.length, 24, "v19 schema mutation set changed"); for (const [name, mutate] of schemaMutations) { const invalid = structuredClone(fixture); const result = mutate(invalid) ?? invalid; @@ -1861,7 +1890,7 @@ test("WASM role size budgets are exact, append-only, and acyclic", async () => { writeFileSync(fixtureBudgetPath, canonicalJson(schemaFirst)); assert.throws( () => runWith(fixtureBudgetPath, join(temporary, "missing-runtime.wasm")), - /budgetId must be labcolors-wasm-runtime-f0a-point-render-spine-v18/u, + /budgetId must be labcolors-wasm-runtime-f1a-visible-assessment-v19/u, "CLI must reject the budget schema before reading a missing artifact", ); @@ -1917,8 +1946,8 @@ test("WASM role size budgets are exact, append-only, and acyclic", async () => { coordinatedMutation.roles.runtime.measurement.rawBytes -= 1; coordinatedMutation.roles.runtime.policy.maxRawBytes -= 1; assert.throws( - () => checker.parseBudgetDocument(Buffer.from(canonicalJson(coordinatedMutation)), paths.v18), - /current v18 file SHA-256 mismatch/u, + () => checker.parseBudgetDocument(Buffer.from(canonicalJson(coordinatedMutation)), paths.v19), + /current v19 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 60e55eda..b8088b04 100644 --- a/scripts/check-wasm-size-budget.mjs +++ b/scripts/check-wasm-size-budget.mjs @@ -25,10 +25,11 @@ const V14_PATH = resolve(REPO_ROOT, "packages/colors/bench/wasm-size-budget-v14. const V15_PATH = resolve(REPO_ROOT, "packages/colors/bench/wasm-size-budget-v15.json"); const V16_PATH = resolve(REPO_ROOT, "packages/colors/bench/wasm-size-budget-v16.json"); const V17_PATH = resolve(REPO_ROOT, "packages/colors/bench/wasm-size-budget-v17.json"); +const V18_PATH = resolve(REPO_ROOT, "packages/colors/bench/wasm-size-budget-v18.json"); export const DEFAULT_BUDGET = resolve( REPO_ROOT, - "packages/colors/bench/wasm-size-budget-v18.json", + "packages/colors/bench/wasm-size-budget-v19.json", ); export const V1_FILE_SHA256 = "4f7340fc8cfd0ccb97377c385f2f8d8e7a9ef2c5ba96177f518c5d07de2825e1"; @@ -58,7 +59,7 @@ export const V12_FILE_SHA256 = "925452113b18b63137b9dae4786e3a8f7ba098eb47a2631a97107fbd52aa9a95"; // V*_FILE_SHA256 — SHA-256 канонических байтов версионных budget-файлов в // packages/colors/bench/. Версии v1..v(N−1) неизменяемы навсегда (история -// ратчета); константа текущей версии (сейчас v18) ротируется ТОЛЬКО вместе с +// ратчета); константа текущей версии (сейчас v19) ротируется ТОЛЬКО вместе с // принятием нового снапшота: следующий bump добавляет v(N+1)-пару, а v(N) // уходит в неизменяемый список. Дрейф любого файла к его пину = отказ чекера. export const V13_FILE_SHA256 = @@ -73,9 +74,11 @@ export const V17_FILE_SHA256 = "8da5e810394ee8fea8491bcd8aff7f3023c267bf31bb5ea4c4b843ef459e7906"; export const V18_FILE_SHA256 = "2ee1e5b4472aa393eb399e3839ab9ac3cfb832a69fa8b704c3fa4854bc1e11f8"; +export const V19_FILE_SHA256 = + "b96d65b06f9bcd7ec568be73bdf5aaea0ed5616f8c104ce6fa33cdf38e7290e2"; const V1_REPOSITORY_PATH = "packages/colors/bench/wasm-size-budget-v1.json"; -const V17_REPOSITORY_PATH = "packages/colors/bench/wasm-size-budget-v17.json"; +const V18_REPOSITORY_PATH = "packages/colors/bench/wasm-size-budget-v18.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"; @@ -90,6 +93,7 @@ const V15_BUDGET_ID = "labcolors-wasm-runtime-c6-validation-srgb-ssot-v15"; const V16_BUDGET_ID = "labcolors-wasm-runtime-q0-output-conflict-v16"; const V17_BUDGET_ID = "labcolors-wasm-runtime-q0-review-hardening-v17"; const V18_BUDGET_ID = "labcolors-wasm-runtime-f0a-point-render-spine-v18"; +const V19_BUDGET_ID = "labcolors-wasm-runtime-f1a-visible-assessment-v19"; const ROLE_ORDER = ["runtime"]; const ROLE_SPECS = { runtime: { @@ -97,11 +101,11 @@ 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, - // Pinned Linux run 29675926449 измерил весь F0a snapshot на 46497B меньше - // v17. Это дельта целого артефакта, не причинная оценка отдельных узлов. - basis: "accepted-f0a-point-render-spine-snapshot", - measurementSource: "github-actions-run-29675926449", - acceptedCeiling: 385610, + // Pinned Linux run 29679743144 измерил весь F1a proof-precondition snapshot + // на 21B меньше v18. Это дельта целого артефакта, не причинная оценка узла. + basis: "accepted-f1a-visible-assessment-snapshot", + measurementSource: "github-actions-run-29679743144", + acceptedCeiling: 385589, }, }; @@ -250,7 +254,11 @@ function verifyImmutableHistory() { if (v17?.schemaVersion !== 8 || v17?.budgetId !== V17_BUDGET_ID) { fail("immutable v17 budget identity drifted"); } - return { v1, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17 }; + const v18 = readImmutableJson(V18_PATH, V18_FILE_SHA256, "v18"); + if (v18?.schemaVersion !== 8 || v18?.budgetId !== V18_BUDGET_ID) { + fail("immutable v18 budget identity drifted"); + } + return { v1, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18 }; } function validateBudgetValue(budget) { @@ -267,14 +275,14 @@ function validateBudgetValue(budget) { "budget", ); if (budget.schemaVersion !== 8) fail("supported schemaVersion is exactly 8"); - if (budget.budgetId !== V18_BUDGET_ID) fail(`budgetId must be ${V18_BUDGET_ID}`); + if (budget.budgetId !== V19_BUDGET_ID) fail(`budgetId must be ${V19_BUDGET_ID}`); exactKeys(budget.predecessor, ["path", "fileSha256"], "predecessor"); if ( - budget.predecessor.path !== V17_REPOSITORY_PATH || - budget.predecessor.fileSha256 !== V17_FILE_SHA256 + budget.predecessor.path !== V18_REPOSITORY_PATH || + budget.predecessor.fileSha256 !== V18_FILE_SHA256 ) { - fail("predecessor must bind the immutable v17 document"); + fail("predecessor must bind the immutable v18 document"); } exactKeys(budget.toolchainSource, ["path", "fileSha256"], "toolchainSource"); @@ -357,10 +365,10 @@ export function parseBudgetDocument(bytes, budgetPath) { validateBudgetValue(budget); if (resolve(budgetPath) === DEFAULT_BUDGET) { const actualFileSha256 = sha256(document); - if (actualFileSha256 !== V18_FILE_SHA256) { + if (actualFileSha256 !== V19_FILE_SHA256) { fail( - `current v18 file SHA-256 mismatch: ` + - `expected=${V18_FILE_SHA256} actual=${actualFileSha256}`, + `current v19 file SHA-256 mismatch: ` + + `expected=${V19_FILE_SHA256} actual=${actualFileSha256}`, ); } }