From cea960ebc13a056b8a504012555846278bedd92e Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Mon, 20 Jul 2026 03:18:48 +0300 Subject: [PATCH 1/8] =?UTF-8?q?V1b:=20=D1=80=D0=B0=D0=B7=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=B8=D0=B7=D0=BC=D0=B5=D1=80=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=B8=20hard-=D1=80=D0=B5=D1=88=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/labcolors-core/src/alpha.rs | 68 +++- crates/labcolors-core/src/analog.rs | 160 +++++---- crates/labcolors-core/src/constraint_tests.rs | 313 +++++++++++++----- .../labcolors-core/src/constraints/exact.rs | 65 ++-- crates/labcolors-core/src/constraints/mod.rs | 254 ++++++++++---- .../labcolors-core/src/constraints/wcag22.rs | 172 +++++++++- crates/labcolors-core/src/recheck.rs | 112 +++---- crates/labcolors-core/src/recheck_tests.rs | 60 ++-- crates/labcolors-core/src/semantic.rs | 11 +- 9 files changed, 843 insertions(+), 372 deletions(-) diff --git a/crates/labcolors-core/src/alpha.rs b/crates/labcolors-core/src/alpha.rs index 680c3dae..c11c2d03 100644 --- a/crates/labcolors-core/src/alpha.rs +++ b/crates/labcolors-core/src/alpha.rs @@ -455,10 +455,33 @@ pub fn resolve_alpha_analog_hex( target, requested_alpha, backdrop, - )?; + ) + .map_err(resolve_verified_error_message)?; Ok((verified.tint().to_hex(), verified.alpha())) } +fn resolve_verified_error_message(error: crate::analog::ResolveVerifiedErrorV1) -> String { + match error { + crate::analog::ResolveVerifiedErrorV1::Proposal(error) => match error { + crate::analog::AlphaAnalogProposalErrorV1::InvalidRequestedAlpha { bits } => { + let requested_alpha = f64::from_bits(bits); + format!("requested_alpha вне конечного [0,1]: {requested_alpha}") + } + crate::analog::AlphaAnalogProposalErrorV1::DerivedAlphaOutsideUnitInterval => { + "выведенная alpha вышла из конечного [0,1]".to_owned() + } + crate::analog::AlphaAnalogProposalErrorV1::MissingTintAtFirstAlpha => { + "первая sRGB8-alpha не дала допустимый byte-тинт".to_owned() + } + }, + crate::analog::ResolveVerifiedErrorV1::ConstraintViolation(witness) => format!( + "alpha-analog не воспроизвёл sRGB8-цель: target={:?}, actual={:?}", + witness.violation().target().bytes(), + witness.violation().actual().bytes() + ), + } +} + #[cfg(test)] mod tests { use super::*; @@ -578,7 +601,10 @@ mod tests { backdrop, ) .unwrap_or_else(|error| { - panic!("solid={solid}, bg={bg}, requested={requested_alpha}: {error}") + panic!( + "solid={solid}, bg={bg}, requested={requested_alpha}: {}", + resolve_verified_error_message(error) + ) }); let tint = verified.tint().bytes(); let actual_alpha = verified.alpha(); @@ -985,6 +1011,44 @@ mod tests { } } + #[test] + fn public_hex_boundary_stringifies_typed_proposal_failure() { + let error = resolve_alpha_analog_hex("#000000", -0.25, "#FFFFFF") + .expect_err("public hex boundary must preserve proposal rejection"); + assert!(error.contains("requested_alpha вне конечного [0,1]")); + } + + #[test] + fn public_string_boundary_omits_authored_routing_identity() { + let message_for = |declaration_ordinal| { + let error = crate::analog::ExactAlphaProgramV1::evaluate( + crate::analog::AuthoredAlphaBindingIdV1::Named { + declaration_ordinal, + }, + crate::Srgb8::new([0; 3]), + crate::Srgb8::new([255; 3]), + crate::composition::AdmittedOpacityV1::new(0.5).unwrap(), + crate::Srgb8::new([0; 3]), + ) + .expect_err("control candidate must violate exact identity"); + let witness = error; + resolve_verified_error_message( + crate::analog::ResolveVerifiedErrorV1::ConstraintViolation(witness), + ) + }; + + let first = message_for(2); + let second = message_for(9); + assert_eq!(first, second, "routing identity must remain typed-only"); + assert_eq!( + first, + "alpha-analog не воспроизвёл sRGB8-цель: target=[0, 0, 0], actual=[128, 128, 128]" + ); + for forbidden in ["Standalone", "Named", "ordinal", "declaration_ordinal"] { + assert!(!first.contains(forbidden)); + } + } + /// Домен ядра закреплён: внегамутные и неконечные входы отвергаются /// (молчаливый ответ на мусор был бы ложным обещанием разрешимости). #[test] diff --git a/crates/labcolors-core/src/analog.rs b/crates/labcolors-core/src/analog.rs index 6b402c3f..3365a9cf 100644 --- a/crates/labcolors-core/src/analog.rs +++ b/crates/labcolors-core/src/analog.rs @@ -2,19 +2,19 @@ //! //! Proposal выбирает `(tint, alpha)`, но не сертифицирует себя. Этот модуль //! материализует ровно один финальный occurrence общей point-программой, -//! применяет exact identity constraint и только после PASS создаёт verified +//! применяет exact identity constraint и только после `Pass` создаёт verified //! value. Никакого результата с частичным evidence при mismatch не существует. use crate::Srgb8; use crate::appearance::{ PhysicalProgramIdentityV1, PointOpacityOverSurfaceV1, ProgramOccurrenceBindingV1, - SourceOverCertificateV1, VisiblePointBindingV1, + SourceOverCertificateV1, }; use crate::composition::AdmittedOpacityV1; use crate::constraints::{ - BoundAssessment, BoundVerdict, ExactConstraintIdentityV1, ExactIdentityAssessmentV1, - ExactIdentityCapabilityV1, ExactIdentityMismatchV1, ExactIdentityReleaseV1, - ExactSrgb8IdentityV1, assess, + ExactConstraintIdentityV1, ExactIdentityCapabilityV1, ExactIdentityReleaseV1, + ExactPassEvidenceV1, ExactSrgb8IdentityV1, ExactViolationEvidenceV1, HardDecision, + assess_visible_point_hard, }; /// Opaque identity authored invocation-а. Standalone helper не притворяется @@ -41,14 +41,7 @@ pub(crate) struct ExactIdentityEvidenceV1 { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) struct VerifiedAlphaAnalogV1 { authored: AuthoredAlphaBindingIdV1, - assessment: BoundAssessment< - VisiblePointBindingV1, - ExactConstraintIdentityV1, - ExactIdentityReleaseV1, - ExactIdentityCapabilityV1, - Srgb8, - ExactIdentityAssessmentV1, - >, + evidence: ExactPassEvidenceV1, } impl VerifiedAlphaAnalogV1 { @@ -61,22 +54,22 @@ impl VerifiedAlphaAnalogV1 { } pub(crate) fn certificate(&self) -> &SourceOverCertificateV1 { - self.assessment.binding().occurrence_ref() + self.evidence.binding().occurrence_ref() } pub(crate) fn evidence(&self) -> ExactIdentityEvidenceV1 { - let binding = *self.assessment.binding(); + let binding = *self.evidence.binding(); let occurrence = binding.occurrence(); ExactIdentityEvidenceV1 { physical: ExactAlphaProgramV1::physical_identity(), authored: self.authored, - constraint: *self.assessment.identity(), - capability: *self.assessment.capability(), - release: *self.assessment.release(), + constraint: *self.evidence.identity(), + capability: *self.evidence.capability(), + release: *self.evidence.release(), program_occurrence: binding.program_occurrence(), occurrence, - target: *self.assessment.invocation(), - actual: Srgb8::new(occurrence.output_rgb()), + target: self.evidence.target(), + actual: self.evidence.actual(), } } } @@ -202,18 +195,21 @@ fn propose( target: Srgb8, requested_alpha: f64, backdrop: Srgb8, -) -> Result<(Srgb8, AdmittedOpacityV1), String> { - let requested = AdmittedOpacityV1::new(requested_alpha) - .map_err(|_| format!("requested_alpha вне конечного [0,1]: {requested_alpha}"))?; +) -> Result<(Srgb8, AdmittedOpacityV1), AlphaAnalogProposalErrorV1> { + let requested = AdmittedOpacityV1::new(requested_alpha).map_err(|_| { + AlphaAnalogProposalErrorV1::InvalidRequestedAlpha { + bits: requested_alpha.to_bits(), + } + })?; if let Some(tint) = tint_at_alpha(target, requested.value(), backdrop) { return Ok((tint, requested)); } let alpha = AdmittedOpacityV1::new(first_alpha(target, backdrop)) - .map_err(|_| "выведенная alpha вышла из конечного [0,1]".to_owned())?; + .map_err(|_| AlphaAnalogProposalErrorV1::DerivedAlphaOutsideUnitInterval)?; debug_assert!(alpha.value() > requested.value()); let tint = tint_at_alpha(target, alpha.value(), backdrop) - .ok_or_else(|| "первая sRGB8-alpha не дала допустимый byte-тинт".to_owned())?; + .ok_or(AlphaAnalogProposalErrorV1::MissingTintAtFirstAlpha)?; Ok((tint, alpha)) } @@ -223,26 +219,39 @@ pub(crate) fn resolve_verified( target: Srgb8, requested_alpha: f64, backdrop: Srgb8, -) -> Result { - let (tint, alpha) = propose(target, requested_alpha, backdrop)?; +) -> Result { + let (tint, alpha) = + propose(target, requested_alpha, backdrop).map_err(ResolveVerifiedErrorV1::Proposal)?; ExactAlphaProgramV1::evaluate(authored, target, tint, alpha, backdrop) - .map_err(|error| error.message()) + .map_err(ResolveVerifiedErrorV1::ConstraintViolation) } #[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) enum ExactAlphaProgramErrorV1 { - IdentityMismatch(ExactIdentityMismatchV1), +pub(crate) enum ResolveVerifiedErrorV1 { + Proposal(AlphaAnalogProposalErrorV1), + // Нынешний exact byte-grid proposal не может попасть сюда, но coordinator + // не имеет права стирать authored witness, если его построитель изменится. + ConstraintViolation(AlphaAnalogViolationV1), } -impl ExactAlphaProgramErrorV1 { - pub(crate) fn message(&self) -> String { - match self { - Self::IdentityMismatch(mismatch) => format!( - "alpha-analog не воспроизвёл sRGB8-цель: target={:?}, actual={:?}", - mismatch.target().bytes(), - mismatch.actual().bytes() - ), - } +/// Typed отказ proposal до materialization occurrence. Binary64 хранится +/// битами, чтобы transport-строка оставалась обязанностью публичной границы. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum AlphaAnalogProposalErrorV1 { + InvalidRequestedAlpha { bits: u64 }, + DerivedAlphaOutsideUnitInterval, + MissingTintAtFirstAlpha, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct AlphaAnalogViolationV1 { + authored: AuthoredAlphaBindingIdV1, + violation: ExactViolationEvidenceV1, +} + +impl AlphaAnalogViolationV1 { + pub(crate) const fn violation(&self) -> &ExactViolationEvidenceV1 { + &self.violation } } @@ -261,21 +270,20 @@ impl ExactAlphaProgramV1 { tint: Srgb8, alpha: AdmittedOpacityV1, backdrop: Srgb8, - ) -> Result { + ) -> Result { let occurrence = PointOpacityOverSurfaceV1::evaluate_admitted(tint.bytes(), alpha, backdrop.bytes()); - let assessment = match assess(&occurrence, &ExactSrgb8IdentityV1, target) { - BoundVerdict::Pass(assessment) => assessment, - BoundVerdict::Fail(failure) => { - return Err(ExactAlphaProgramErrorV1::IdentityMismatch( - failure.into_outcome(), - )); + let evidence = match assess_visible_point_hard(&occurrence, &ExactSrgb8IdentityV1, target) { + Ok(HardDecision::Pass(evidence)) => evidence, + Ok(HardDecision::Violation(violation)) => { + return Err(AlphaAnalogViolationV1 { + authored, + violation, + }); } + Err(error) => match error {}, }; - let verified = VerifiedAlphaAnalogV1 { - authored, - assessment, - }; + let verified = VerifiedAlphaAnalogV1 { authored, evidence }; debug_assert_eq!( verified.evidence().actual, Srgb8::new(verified.certificate().output_rgb()) @@ -366,17 +374,20 @@ mod tests { let target = Srgb8::new([1, 0, 0]); let backdrop = Srgb8::new([0; 3]); for requested_alpha in [-0.25, 1.25] { - let error = resolve_verified( + let error: ResolveVerifiedErrorV1 = resolve_verified( AuthoredAlphaBindingIdV1::Standalone, target, requested_alpha, backdrop, ) .expect_err("invalid alpha must not be silently moved onto the exact frontier"); - assert!( - error.contains("requested_alpha вне конечного [0,1]"), - "unexpected rejection boundary: {error}" - ); + let ResolveVerifiedErrorV1::Proposal( + AlphaAnalogProposalErrorV1::InvalidRequestedAlpha { bits }, + ) = error + else { + panic!("invalid requested alpha must fail at the proposal boundary") + }; + assert_eq!(bits, requested_alpha.to_bits()); } } @@ -392,16 +403,37 @@ mod tests { Srgb8::new([0, 0, 0]), ) .expect_err("wrong final bytes must not mint VerifiedAlphaAnalogV1"); - let message = error.message(); - assert!(message.contains("target=[0, 0, 0]"), "{message}"); - assert!(message.contains("actual=[128, 128, 128]"), "{message}"); - - let ExactAlphaProgramErrorV1::IdentityMismatch(mismatch) = error; - assert_eq!(mismatch.target(), target); - assert_eq!(mismatch.actual(), Srgb8::new([128, 128, 128])); + let witness = error; + fn requires_violation(_: ExactViolationEvidenceV1) {} + assert_eq!(witness.authored, AuthoredAlphaBindingIdV1::Standalone); + assert_eq!(witness.violation().target(), target); + assert_eq!(witness.violation().actual(), Srgb8::new([128; 3])); + requires_violation(witness.violation); assert_eq!(crate::composition::source_over_evaluation_count(), 1); } + #[test] + fn equal_violation_physics_under_distinct_named_bindings_keeps_distinct_witnesses() { + let evaluate = |declaration_ordinal| { + ExactAlphaProgramV1::evaluate( + AuthoredAlphaBindingIdV1::Named { + declaration_ordinal, + }, + Srgb8::new([0, 0, 0]), + Srgb8::new([255, 255, 255]), + admitted(0.5), + Srgb8::new([0, 0, 0]), + ) + .expect_err("control candidate must violate exact identity") + }; + let first = evaluate(2); + let second = evaluate(9); + + assert_eq!(first.violation(), second.violation()); + assert_ne!(first.authored, second.authored); + assert_ne!(first, second); + } + #[test] fn exact_evidence_keeps_physics_and_authored_routing_separate() { let verified = ExactAlphaProgramV1::evaluate( @@ -414,6 +446,8 @@ mod tests { Srgb8::new([255, 255, 255]), ) .unwrap(); + fn requires_pass(_: ExactPassEvidenceV1) {} + requires_pass(verified.evidence); let evidence = verified.evidence(); assert_eq!( @@ -427,7 +461,7 @@ mod tests { assert_eq!(evidence.target, evidence.actual); assert_eq!( evidence.program_occurrence, - verified.assessment.binding().program_occurrence() + verified.evidence.binding().program_occurrence() ); assert_eq!(evidence.occurrence.output_rgb(), evidence.actual.bytes()); assert_eq!( diff --git a/crates/labcolors-core/src/constraint_tests.rs b/crates/labcolors-core/src/constraint_tests.rs index a4274f89..9127f5e1 100644 --- a/crates/labcolors-core/src/constraint_tests.rs +++ b/crates/labcolors-core/src/constraint_tests.rs @@ -1,13 +1,17 @@ use proptest::prelude::*; +use crate::Srgb8; use crate::appearance::{ AppearanceBindings, AppearanceGraphSpec, ColorInputId, CompositionProfileV1, OccurrenceId, OccurrenceSpec, OpacityInputId, PaintId, PaintSpec, PointOpacityOverSurfaceV1, SurfaceId, - SurfaceInputPortId, SurfaceSpec, + SurfaceInputPortId, SurfaceSpec, VisiblePointBindingV1, }; use crate::constraints::{ - BoundAssessment, BoundVerdict, Wcag22Srgb8CapabilityV1, Wcag22Srgb8EvaluatorIdentityV1, - Wcag22Srgb8V1, assess, + ApplicableWcag22EvaluationErrorV1, ApplicableWcag22MeasurementV1, ClassifiedMeasurement, + Evaluator, ExactIdentityPassV1, ExactPassEvidenceV1, ExactSrgb8IdentityV1, + ExactViolationEvidenceV1, HardDecision, VisiblePointPassEvidence, + VisiblePointViolationEvidence, Wcag22PassV1, Wcag22Srgb8V1, Wcag22ViolationV1, + assess_visible_point_hard, }; use crate::wcag22::{ Wcag22ApplicableDecisionV1, Wcag22AssessmentV1, Wcag22CriterionV1, Wcag22ProfileIdV1, @@ -23,49 +27,189 @@ fn point_occurrence( .unwrap_or_else(|error| panic!("valid point occurrence rejected: {}", error.message())) } -fn wcag_assessment( +type Wcag22HardResultV1 = Result< + HardDecision< + VisiblePointPassEvidence, + VisiblePointViolationEvidence, + >, + ApplicableWcag22EvaluationErrorV1, +>; + +fn wcag_outcome( source: [u8; 3], opacity: f64, backdrop: [u8; 3], criterion: Wcag22CriterionV1, -) -> BoundAssessment< - crate::appearance::VisiblePointBindingV1, - Wcag22Srgb8EvaluatorIdentityV1, - Wcag22ProfileIdV1, - Wcag22Srgb8CapabilityV1, - Wcag22CriterionV1, - Wcag22AssessmentV1, -> { +) -> Wcag22HardResultV1 { let occurrence = point_occurrence(source, opacity, backdrop); - let BoundVerdict::Pass(assessment) = assess(&occurrence, &Wcag22Srgb8V1, criterion) else { - panic!("proof-bound WCAG evaluator must decide every admitted sRGB8 pair"); + assess_visible_point_hard(&occurrence, &Wcag22Srgb8V1, criterion) +} + +fn wcag_parts( + outcome: &Wcag22HardResultV1, +) -> Result< + ( + &ApplicableWcag22MeasurementV1, + &VisiblePointBindingV1, + &Wcag22CriterionV1, + &Wcag22ProfileIdV1, + ), + &ApplicableWcag22EvaluationErrorV1, +> { + match outcome { + Ok(HardDecision::Pass(evidence)) => Ok(( + evidence.measurement().value(), + evidence.binding(), + evidence.invocation(), + evidence.release(), + )), + Ok(HardDecision::Violation(evidence)) => Ok(( + evidence.measurement().value(), + evidence.binding(), + evidence.invocation(), + evidence.release(), + )), + Err(error) => Err(error), + } +} + +#[test] +fn wcag_808080_on_white_is_a_typed_hard_violation() { + let occurrence = point_occurrence([0x80; 3], 1.0, [0xFF; 3]); + let Ok(HardDecision::Violation(violation)) = assess_visible_point_hard( + &occurrence, + &Wcag22Srgb8V1, + Wcag22CriterionV1::Sc143TextDefault, + ) else { + panic!("#808080/#FFFFFF must violate SC 1.4.3 default text"); + }; + + fn requires_wcag_violation( + _: &ClassifiedMeasurement, + ) { + } + let retained = violation.measurement().value(); + assert_eq!(retained.decision(), Wcag22ApplicableDecisionV1::Fail); + assert_eq!(retained.measurement().foreground, [0x80; 3]); + assert_eq!(retained.measurement().background, [0xFF; 3]); + requires_wcag_violation(violation.measurement()); +} + +#[test] +fn wcag_black_on_white_is_a_typed_hard_pass() { + let occurrence = point_occurrence([0x00; 3], 1.0, [0xFF; 3]); + let Ok(HardDecision::Pass(pass)) = assess_visible_point_hard( + &occurrence, + &Wcag22Srgb8V1, + Wcag22CriterionV1::Sc143TextDefault, + ) else { + panic!("#000000/#FFFFFF must pass SC 1.4.3 default text"); + }; + + fn requires_wcag_pass(_: &ClassifiedMeasurement) {} + assert_eq!( + pass.measurement().value().decision(), + Wcag22ApplicableDecisionV1::Pass + ); + requires_wcag_pass(pass.measurement()); +} + +#[test] +fn wcag_pass_and_violation_payloads_are_type_incompatible() { + assert_ne!( + core::any::TypeId::of::(), + core::any::TypeId::of::() + ); +} + +#[test] +fn exact_mismatch_is_total_evaluation_and_constraint_violation() { + let occurrence = point_occurrence([128, 128, 128], 1.0, [255, 255, 255]); + let Ok(HardDecision::Violation(violation)) = assess_visible_point_hard( + &occurrence, + &ExactSrgb8IdentityV1, + Srgb8::new([127, 128, 128]), + ) else { + panic!("exact mismatch was incorrectly classified as pass"); + }; + + fn requires_violation(_: ExactViolationEvidenceV1) {} + assert_eq!(violation.target(), Srgb8::new([127, 128, 128])); + assert_eq!(violation.actual(), Srgb8::new([128; 3])); + requires_violation(violation); +} + +#[test] +fn exact_match_is_refined_to_the_distinct_pass_type() { + let occurrence = point_occurrence([128, 128, 128], 1.0, [255, 255, 255]); + let Ok(HardDecision::Pass(pass)) = + assess_visible_point_hard(&occurrence, &ExactSrgb8IdentityV1, Srgb8::new([128; 3])) + else { + panic!("exact equality was incorrectly refined as a violation"); + }; + + fn requires_pass(_: ExactPassEvidenceV1) {} + fn requires_pass_classification( + _: &crate::constraints::ClassifiedMeasurement, + ) { + } + assert_eq!(*pass.invocation(), Srgb8::new([128; 3])); + assert_eq!(pass.binding().occurrence().output_rgb(), [128; 3]); + requires_pass_classification(pass.measurement()); + requires_pass(pass); +} + +#[test] +fn same_raw_808080_measurement_is_classified_only_against_the_invocation() { + let occurrence = point_occurrence([0x80; 3], 1.0, [0xFF; 3]); + let Ok(HardDecision::Pass(pass)) = + assess_visible_point_hard(&occurrence, &ExactSrgb8IdentityV1, Srgb8::new([0x80; 3])) + else { + panic!("#808080 must pass its identical invocation"); }; - assessment + let Ok(HardDecision::Violation(violation)) = assess_visible_point_hard( + &occurrence, + &ExactSrgb8IdentityV1, + Srgb8::new([0x7F, 0x80, 0x80]), + ) else { + panic!("one-byte target mismatch must be a violation"); + }; + + assert_eq!(pass.actual(), Srgb8::new([0x80; 3])); + assert_eq!(violation.actual(), pass.actual()); + assert_ne!(violation.target(), pass.target()); +} + +#[test] +fn exact_evaluator_emits_only_raw_actual_measurement() { + let occurrence = point_occurrence([0x80; 3], 1.0, [0xFF; 3]); + let modeled = occurrence.modeled_srgb8_point(); + let first: Result = + ExactSrgb8IdentityV1.evaluate(&modeled, &Srgb8::new([0x80; 3])); + let second: Result = + ExactSrgb8IdentityV1.evaluate(&modeled, &Srgb8::new([0x7F, 0x80, 0x80])); + + assert_eq!(first.unwrap(), Srgb8::new([0x80; 3])); + assert_eq!(second.unwrap(), Srgb8::new([0x80; 3])); } #[test] fn wcag_reads_final_visible_occurrence_in_measurement_order() { - let report = wcag_assessment( + let report = wcag_outcome( [0, 0, 0], 0.5, [255, 255, 255], Wcag22CriterionV1::Sc143TextDefault, ); - let Wcag22AssessmentV1::Evaluated { - measurement, - decision, - .. - } = report.outcome() - 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.invocation(), &Wcag22CriterionV1::Sc143TextDefault); + let (measurement, binding, invocation, _) = wcag_parts(&report) + .expect("proof-bound WCAG evaluator must measure every admitted sRGB8 pair"); + assert_eq!(measurement.measurement().foreground, [128, 128, 128]); + assert_eq!(measurement.measurement().background, [255, 255, 255]); + assert_eq!(measurement.decision(), Wcag22ApplicableDecisionV1::Fail); + assert_eq!(invocation, &Wcag22CriterionV1::Sc143TextDefault); assert_eq!( - report.binding().program_occurrence().occurrence(), + binding.program_occurrence().occurrence(), OccurrenceId::new(0) ); @@ -190,42 +334,46 @@ fn two_equal_physical_occurrences() -> [crate::appearance::ResolvedOccurrence; 2 #[test] fn equal_physics_under_distinct_occurrence_ids_keeps_distinct_bindings() { let [first, second] = two_equal_physical_occurrences(); - let BoundVerdict::Pass(first_report) = assess( + let first_report = assess_visible_point_hard( &first, &Wcag22Srgb8V1, Wcag22CriterionV1::Sc1411UiComponentOrState, - ) else { - panic!("first assessment failed"); - }; - let BoundVerdict::Pass(second_report) = assess( + ); + let second_report = assess_visible_point_hard( &second, &Wcag22Srgb8V1, Wcag22CriterionV1::Sc1411UiComponentOrState, - ) else { - panic!("second assessment failed"); - }; + ); + let (first_measurement, first_binding, _, _) = + wcag_parts(&first_report).expect("first measurement failed"); + let (second_measurement, second_binding, _, _) = + wcag_parts(&second_report).expect("second measurement failed"); - assert_eq!(first_report.outcome(), second_report.outcome()); - assert_ne!(first_report.binding(), second_report.binding()); + assert_eq!(first_measurement, second_measurement); + assert_ne!(first_binding, second_binding); } #[test] fn same_ids_and_final_pair_do_not_erase_subject_or_alpha_provenance() { - let transparent_black = wcag_assessment( + let transparent_black = wcag_outcome( [0, 0, 0], 0.0, [255, 255, 255], Wcag22CriterionV1::Sc143TextDefault, ); - let opaque_white = wcag_assessment( + let opaque_white = wcag_outcome( [255, 255, 255], 1.0, [255, 255, 255], Wcag22CriterionV1::Sc143TextDefault, ); + let (transparent_measurement, transparent_binding, _, _) = + wcag_parts(&transparent_black).expect("transparent occurrence must evaluate"); + let (opaque_measurement, opaque_binding, _, _) = + wcag_parts(&opaque_white).expect("opaque occurrence must evaluate"); - assert_eq!(transparent_black.outcome(), opaque_white.outcome()); - assert_ne!(transparent_black.binding(), opaque_white.binding()); + assert_eq!(transparent_measurement, opaque_measurement); + assert_ne!(transparent_binding, opaque_binding); } proptest! { @@ -241,84 +389,67 @@ proptest! { let final_backdrop = occurrence.backdrop(); let target = occurrence.modeled_srgb8_point(); for criterion in Wcag22CriterionV1::ALL { - let BoundVerdict::Pass(report) = assess(&occurrence, &Wcag22Srgb8V1, criterion) else { - return Err(TestCaseError::fail("finite WCAG table rejected an admitted pair")); - }; + let report = assess_visible_point_hard(&occurrence, &Wcag22Srgb8V1, criterion); + let (bound, binding, invocation, release) = wcag_parts(&report) + .map_err(|_| TestCaseError::fail("finite WCAG table rejected an admitted pair"))?; let standalone = evaluate_wcag22_srgb8(final_visible, final_backdrop, criterion) .expect("same admitted pair must be decided by standalone evaluator"); + let Wcag22AssessmentV1::Evaluated { + profile_id, + criterion: standalone_criterion, + measurement, + decision, + evidence, + } = standalone else { + return Err(TestCaseError::fail("explicit criterion became report-only")); + }; prop_assert_eq!(target.visible(), final_visible); prop_assert_eq!(target.backdrop(), final_backdrop); - prop_assert_eq!(report.outcome(), &standalone); - prop_assert_eq!(report.binding(), &occurrence.visible_point_binding()); + prop_assert_eq!(bound.profile_id(), profile_id); + prop_assert_eq!(bound.criterion(), standalone_criterion); + prop_assert_eq!(bound.measurement(), &measurement); + prop_assert_eq!(bound.decision(), decision); + prop_assert_eq!(bound.evidence(), &evidence); + prop_assert_eq!(invocation, &criterion); + prop_assert_eq!(release, &profile_id); + prop_assert_eq!(binding, &occurrence.visible_point_binding()); } } } #[test] fn required_criterion_is_not_replaced_by_one_hardcoded_threshold() { - let text = wcag_assessment( + let text = wcag_outcome( [138, 138, 138], 1.0, [255, 255, 255], Wcag22CriterionV1::Sc143TextDefault, ); - let large_text = wcag_assessment( + let large_text = wcag_outcome( [138, 138, 138], 1.0, [255, 255, 255], Wcag22CriterionV1::Sc143TextLargeScale, ); + let (text, _, _, _) = wcag_parts(&text).expect("text measurement failed"); + let (large_text, _, _, _) = wcag_parts(&large_text).expect("large-text measurement failed"); - assert!(matches!( - text.outcome(), - Wcag22AssessmentV1::Evaluated { - decision: Wcag22ApplicableDecisionV1::Fail, - .. - } - )); - assert!(matches!( - large_text.outcome(), - Wcag22AssessmentV1::Evaluated { - decision: Wcag22ApplicableDecisionV1::Pass, - .. - } - )); + assert_eq!(text.decision(), Wcag22ApplicableDecisionV1::Fail); + assert_eq!(large_text.decision(), Wcag22ApplicableDecisionV1::Pass); } #[test] fn bound_report_release_matches_assessment_and_registry() { - let report = wcag_assessment( + let report = wcag_outcome( [0, 0, 0], 1.0, [255, 255, 255], Wcag22CriterionV1::Sc143TextDefault, ); - let Wcag22AssessmentV1::Evaluated { profile_id, .. } = report.outcome() 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}" - ); - } -} + let (measurement, _, _, release) = + wcag_parts(&report).expect("required criterion must evaluate"); -#[test] -fn exact_success_type_cannot_represent_a_target_actual_mismatch() { - let source = include_str!("constraints/exact.rs"); - assert!(source.contains("pub(crate) struct ExactIdentityAssessmentV1(());")); - assert!(!source.contains("pub(crate) struct ExactIdentityAssessmentV1;")); - assert_eq!(source.matches("ExactIdentityAssessmentV1(())").count(), 2); - assert!(!source.contains("matched: Srgb8")); + assert_eq!(release, &measurement.profile_id()); + assert_eq!(*release, wcag22_profile_v1().profile_id); } diff --git a/crates/labcolors-core/src/constraints/exact.rs b/crates/labcolors-core/src/constraints/exact.rs index fb94abea..4675d9ab 100644 --- a/crates/labcolors-core/src/constraints/exact.rs +++ b/crates/labcolors-core/src/constraints/exact.rs @@ -1,6 +1,10 @@ use crate::Srgb8; use crate::appearance::ModeledSrgb8PointOccurrence; -use crate::constraints::{Evaluator, private}; +use crate::constraints::{ + Evaluator, HardClassifier, HardDecision, VisiblePointPassEvidence, + VisiblePointViolationEvidence, private, +}; +use core::convert::Infallible; /// Структурная identity общего exact-закона финального point occurrence. /// Она не содержит client ID, target bytes или выбранную alpha: эти значения @@ -23,29 +27,16 @@ pub(crate) enum ExactIdentityCapabilityV1 { FinalOccurrenceSrgb8IdentityV1, } -/// PASS-marker без дублирования bytes: invocation хранит target, а physical -/// binding — actual. Создать marker может только sealed evaluator после exact -/// equality, поэтому несовпадающая success-пара непредставима и в памяти. +/// Закрытые ZST payload-типы делают Pass и Violation несовместимыми, но не +/// позволяют classifier-у вернуть другое measurement. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) struct ExactIdentityAssessmentV1(()); +pub(crate) struct ExactIdentityPassV1(()); -/// Типизированный отказ exact-гейта. Он несёт только диагностическую пару и -/// никогда не выдаёт частично «проверенный» occurrence/evidence. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) struct ExactIdentityMismatchV1 { - target: Srgb8, - actual: Srgb8, -} - -impl ExactIdentityMismatchV1 { - pub(crate) const fn target(self) -> Srgb8 { - self.target - } +pub(crate) struct ExactIdentityViolationV1(()); - pub(crate) const fn actual(self) -> Srgb8 { - self.actual - } -} +pub(crate) type ExactPassEvidenceV1 = VisiblePointPassEvidence; +pub(crate) type ExactViolationEvidenceV1 = VisiblePointViolationEvidence; pub(crate) struct ExactSrgb8IdentityV1; @@ -55,14 +46,15 @@ impl ExactSrgb8IdentityV1 { } impl private::EvaluatorSealed for ExactSrgb8IdentityV1 {} +impl private::HardClassifierSealed for ExactSrgb8IdentityV1 {} impl Evaluator for ExactSrgb8IdentityV1 { type Invocation = Srgb8; type Identity = ExactConstraintIdentityV1; type Release = ExactIdentityReleaseV1; type Capability = ExactIdentityCapabilityV1; - type Assessment = ExactIdentityAssessmentV1; - type Error = ExactIdentityMismatchV1; + type Measurement = Srgb8; + type Error = Infallible; fn identity(&self) -> Self::Identity { Self::IDENTITY @@ -79,15 +71,26 @@ impl Evaluator for ExactSrgb8IdentityV1 { fn evaluate( &self, occurrence: &ModeledSrgb8PointOccurrence, - target: &Self::Invocation, - ) -> Result { - let actual = Srgb8::new(occurrence.visible()); - if actual != *target { - return Err(ExactIdentityMismatchV1 { - target: *target, - actual, - }); + _invocation: &Self::Invocation, + ) -> Result { + Ok(Srgb8::new(occurrence.visible())) + } +} + +impl HardClassifier for ExactSrgb8IdentityV1 { + type Pass = ExactIdentityPassV1; + type Violation = ExactIdentityViolationV1; + + fn classify( + &self, + invocation: &Srgb8, + measurement: &Srgb8, + ) -> HardDecision { + let actual = *measurement; + if actual == *invocation { + HardDecision::Pass(ExactIdentityPassV1(())) + } else { + HardDecision::Violation(ExactIdentityViolationV1(())) } - Ok(ExactIdentityAssessmentV1(())) } } diff --git a/crates/labcolors-core/src/constraints/mod.rs b/crates/labcolors-core/src/constraints/mod.rs index f055c487..83e39ec3 100644 --- a/crates/labcolors-core/src/constraints/mod.rs +++ b/crates/labcolors-core/src/constraints/mod.rs @@ -1,27 +1,36 @@ -//! Приватная typed-связка physical target и evaluator-а. +//! Приватная typed-связка physical measurement, hard-classifier и evidence. //! -//! Модуль не является public registry: он лишь гарантирует, что assessment -//! сохраняет identity физического evidence и release реально вызванного +//! Evaluator только измеряет modeled occurrence. Hard verdict появляется один +//! раз в sealed classifier-е, после чего source-specific binder атомарно +//! связывает результат с physical occurrence и metadata реально вызванного //! evaluator-а. +use crate::Srgb8; use crate::appearance::{ModeledSrgb8PointOccurrence, ResolvedOccurrence, VisiblePointBindingV1}; mod exact; pub(crate) use exact::{ - ExactConstraintIdentityV1, ExactIdentityAssessmentV1, ExactIdentityCapabilityV1, - ExactIdentityMismatchV1, ExactIdentityReleaseV1, ExactSrgb8IdentityV1, + ExactConstraintIdentityV1, ExactIdentityCapabilityV1, ExactIdentityReleaseV1, + ExactPassEvidenceV1, ExactSrgb8IdentityV1, ExactViolationEvidenceV1, }; +#[cfg(test)] +pub(crate) use exact::ExactIdentityPassV1; + #[cfg(test)] mod wcag22; #[cfg(test)] -pub(crate) use wcag22::{Wcag22Srgb8CapabilityV1, Wcag22Srgb8EvaluatorIdentityV1, Wcag22Srgb8V1}; +pub(crate) use wcag22::{ + ApplicableWcag22EvaluationErrorV1, ApplicableWcag22MeasurementV1, Wcag22PassV1, Wcag22Srgb8V1, + Wcag22ViolationV1, +}; -/// Marker-ы недоступны внешним crate-ам: новые target/evaluator families +/// Seals недоступны внешним crate-ам: новые evaluator/classifier families /// добавляются только вместе с code-owned physical adapter-ом. mod private { pub trait EvaluatorSealed {} + pub trait HardClassifierSealed {} } pub(crate) trait Evaluator: private::EvaluatorSealed { @@ -29,7 +38,7 @@ pub(crate) trait Evaluator: private::EvaluatorSealed { type Identity; type Release; type Capability; - type Assessment; + type Measurement; type Error; fn identity(&self) -> Self::Identity; @@ -42,24 +51,24 @@ pub(crate) trait Evaluator: private::EvaluatorSealed { &self, target: &Target, invocation: &Self::Invocation, - ) -> Result; + ) -> Result; } -/// Один outcome вместе с exact physical binding и metadata действительно -/// вызванного evaluator-а. Один и тот же carrier используется для PASS и FAIL, -/// поэтому отказ не теряет provenance и не восстанавливает её вручную. +/// Одно измерение вместе с exact physical binding и metadata действительно +/// вызванного evaluator-а. Поля закрыты: binding создаёт только адаптер того +/// physical source, из которого одновременно получены target и certificate. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) struct BoundEvidence { +pub(crate) struct BoundEvidence { binding: Binding, identity: Identity, release: Release, capability: Capability, invocation: Invocation, - outcome: Outcome, + measurement: Measurement, } -impl - BoundEvidence +impl + BoundEvidence { pub(crate) fn binding(&self) -> &Binding { &self.binding @@ -81,124 +90,227 @@ impl &self.invocation } - pub(crate) fn outcome(&self) -> &Outcome { - &self.outcome + #[cfg(test)] + pub(crate) fn measurement(&self) -> &Measurement { + &self.measurement } +} - pub(crate) fn into_outcome(self) -> Outcome { - self.outcome - } +/// Raw measurement, доказанно отнесённое classifier-ом ровно к одному +/// несовместимому исходу. Закрытый payload подтверждает решение classifier-а, +/// но не может заменить исходное measurement. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct ClassifiedMeasurement { + measurement: Measurement, + classification: Classification, } -pub(crate) type BoundAssessment = - BoundEvidence; +impl ClassifiedMeasurement { + fn new(measurement: Measurement, classification: Classification) -> Self { + Self { + measurement, + classification, + } + } -pub(crate) type BoundFailure = - BoundEvidence; + pub(crate) fn value(&self) -> &Measurement { + &self.measurement + } +} +/// Два несовместимых hard-решения после успешного измерения. Ошибка evaluator-а +/// остаётся внешним `Result::Err`: отдельный `Fault` появится только вместе с +/// первым реальным fallible consumer и его исполняемым контрактом. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum BoundVerdict +pub(crate) enum HardDecision { + Pass(Pass), + Violation(Violation), +} + +/// Sealed hard-classifier — единственный слой, которому разрешено превращать +/// raw measurement и invocation в Pass/Violation. +pub(crate) trait HardClassifier: + private::HardClassifierSealed { - Pass(BoundAssessment), - Fail(BoundFailure), + type Pass; + type Violation; + + fn classify( + &self, + invocation: &Invocation, + measurement: &Measurement, + ) -> HardDecision; } -pub(crate) type AssessmentResult = BoundVerdict< +type PointInvocation = + >::Invocation; +type PointMeasurement = + >::Measurement; +type PointPass = + , PointMeasurement>>::Pass; +type PointViolation = , + PointMeasurement, +>>::Violation; + +type BoundVisiblePointMeasurement = BoundEvidence< VisiblePointBindingV1, >::Identity, >::Release, >::Capability, >::Invocation, - >::Assessment, - >::Error, + Measurement, >; -pub(crate) fn assess( +pub(crate) type VisiblePointPassEvidence = BoundVisiblePointMeasurement< + Evaluation, + ClassifiedMeasurement, PointPass>, +>; + +pub(crate) type VisiblePointViolationEvidence = BoundVisiblePointMeasurement< + Evaluation, + ClassifiedMeasurement, PointViolation>, +>; + +/// Единственный binder hard-classifier-а для final visible point. Modeled +/// target и binding берутся из одного occurrence; metadata связывается только +/// после успешного measurement и классификации. +pub(crate) fn assess_visible_point_hard( source: &ResolvedOccurrence, evaluator: &Evaluation, - invocation: Evaluation::Invocation, -) -> AssessmentResult + invocation: PointInvocation, +) -> Result< + HardDecision, VisiblePointViolationEvidence>, + >::Error, +> where - Evaluation: Evaluator, + Evaluation: Evaluator + + HardClassifier, PointMeasurement>, { let target = source.modeled_srgb8_point(); let binding = source.visible_point_binding(); - let verdict = evaluator.evaluate(&target, &invocation); + let measurement = evaluator.evaluate(&target, &invocation)?; + let classification = evaluator.classify(&invocation, &measurement); let identity = evaluator.identity(); let release = evaluator.release(); let capability = evaluator.capability(); - match verdict { - Ok(outcome) => BoundVerdict::Pass(BoundEvidence { + + Ok(match classification { + HardDecision::Pass(payload) => HardDecision::Pass(BoundEvidence { binding, identity, release, capability, invocation, - outcome, + measurement: ClassifiedMeasurement::new(measurement, payload), }), - Err(outcome) => BoundVerdict::Fail(BoundEvidence { + HardDecision::Violation(payload) => HardDecision::Violation(BoundEvidence { binding, identity, release, capability, invocation, - outcome, + measurement: ClassifiedMeasurement::new(measurement, payload), }), + }) +} + +impl + BoundEvidence< + Binding, + Identity, + Release, + Capability, + Srgb8, + ClassifiedMeasurement, + > +{ + pub(crate) fn target(&self) -> Srgb8 { + self.invocation + } + + pub(crate) fn actual(&self) -> Srgb8 { + *self.measurement.value() } } #[cfg(test)] mod tests { - use super::{BoundVerdict, Evaluator, ModeledSrgb8PointOccurrence, assess, private}; + use super::{ + Evaluator, HardClassifier, HardDecision, ModeledSrgb8PointOccurrence, + assess_visible_point_hard, private, + }; + use crate::Srgb8; use crate::appearance::PointOpacityOverSurfaceV1; + use core::convert::Infallible; #[derive(Debug, Clone, Copy, PartialEq, Eq)] - struct SentinelError; + struct SentinelPass(()); - struct FailingEvaluator; + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + struct SentinelViolation(()); - impl private::EvaluatorSealed for FailingEvaluator {} + struct SubstitutionAttemptEvaluator { + measured: Srgb8, + attempted_replacement: Srgb8, + } - impl Evaluator for FailingEvaluator { - type Invocation = (); - type Identity = &'static str; - type Release = &'static str; - type Capability = &'static str; - type Assessment = (); - type Error = SentinelError; + impl private::EvaluatorSealed for SubstitutionAttemptEvaluator {} + impl private::HardClassifierSealed for SubstitutionAttemptEvaluator {} - fn identity(&self) -> Self::Identity { - "sentinel-law" - } + impl Evaluator for SubstitutionAttemptEvaluator { + type Invocation = Srgb8; + type Identity = (); + type Release = (); + type Capability = (); + type Measurement = Srgb8; + type Error = Infallible; - fn release(&self) -> Self::Release { - "sentinel-v1" - } + fn identity(&self) {} - fn capability(&self) -> Self::Capability { - "sentinel-point" - } + fn release(&self) {} + + fn capability(&self) {} fn evaluate( &self, _target: &ModeledSrgb8PointOccurrence, _invocation: &Self::Invocation, - ) -> Result { - Err(SentinelError) + ) -> Result { + Ok(self.measured) + } + } + + impl HardClassifier for SubstitutionAttemptEvaluator { + type Pass = SentinelPass; + type Violation = SentinelViolation; + + fn classify( + &self, + _invocation: &Srgb8, + measurement: &Srgb8, + ) -> HardDecision { + assert_eq!(*measurement, self.measured); + let _forbidden_substitute = self.attempted_replacement; + HardDecision::Pass(SentinelPass(())) } } #[test] - fn evaluator_error_is_returned_without_report_or_fallback() { + fn classifier_payload_cannot_replace_the_evaluator_measurement() { let occurrence = PointOpacityOverSurfaceV1::evaluate([1, 2, 3], 0.5, [4, 5, 6]) .unwrap_or_else(|error| panic!("valid point occurrence rejected: {}", error.message())); - let BoundVerdict::Fail(error) = assess(&occurrence, &FailingEvaluator, ()) else { - panic!("failing evaluator unexpectedly passed"); + let evaluator = SubstitutionAttemptEvaluator { + measured: Srgb8::new([0x80; 3]), + attempted_replacement: Srgb8::new([0x00; 3]), }; - assert_eq!(error.outcome(), &SentinelError); - assert_eq!(error.identity(), &"sentinel-law"); - assert_eq!(error.release(), &"sentinel-v1"); - assert_eq!(error.capability(), &"sentinel-point"); - assert_eq!(error.invocation(), &()); + let Ok(HardDecision::Pass(evidence)) = + assess_visible_point_hard(&occurrence, &evaluator, Srgb8::new([0x80; 3])) + else { + panic!("control classifier must return Pass"); + }; + + assert_eq!(evidence.actual(), evaluator.measured); + assert_ne!(evidence.actual(), evaluator.attempted_replacement); } } diff --git a/crates/labcolors-core/src/constraints/wcag22.rs b/crates/labcolors-core/src/constraints/wcag22.rs index c2c029f8..f866eef4 100644 --- a/crates/labcolors-core/src/constraints/wcag22.rs +++ b/crates/labcolors-core/src/constraints/wcag22.rs @@ -1,7 +1,9 @@ use crate::appearance::ModeledSrgb8PointOccurrence; -use crate::constraints::{Evaluator, private}; +use crate::constraints::{Evaluator, HardClassifier, HardDecision, private}; +use crate::numerics::NumericalDecisionEvidenceV1; use crate::wcag22::{ - Wcag22AssessmentV1, Wcag22CriterionV1, Wcag22EvaluationErrorV1, Wcag22ProfileIdV1, + Wcag22ApplicableDecisionV1, Wcag22AssessmentV1, Wcag22ClientDeclaredNotApplicableV1, + Wcag22CriterionV1, Wcag22EvaluationErrorV1, Wcag22MeasurementV1, Wcag22ProfileIdV1, evaluate_wcag22_srgb8, wcag22_profile_v1, }; @@ -13,15 +15,107 @@ pub(crate) struct Wcag22Srgb8CapabilityV1; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) struct Wcag22Srgb8EvaluatorIdentityV1; +/// Applicable-only WCAG measurement. Private fields make report-only +/// `NotEvaluated` and a mismatched criterion unrepresentable after refinement. +#[derive(Debug, Clone, PartialEq)] +pub(crate) struct ApplicableWcag22MeasurementV1 { + profile_id: Wcag22ProfileIdV1, + criterion: Wcag22CriterionV1, + measurement: Wcag22MeasurementV1, + decision: Wcag22ApplicableDecisionV1, + evidence: NumericalDecisionEvidenceV1, +} + +impl ApplicableWcag22MeasurementV1 { + pub(crate) const fn profile_id(&self) -> Wcag22ProfileIdV1 { + self.profile_id + } + + pub(crate) const fn criterion(&self) -> Wcag22CriterionV1 { + self.criterion + } + + pub(crate) const fn measurement(&self) -> &Wcag22MeasurementV1 { + &self.measurement + } + + pub(crate) const fn decision(&self) -> Wcag22ApplicableDecisionV1 { + self.decision + } + + pub(crate) const fn evidence(&self) -> &NumericalDecisionEvidenceV1 { + &self.evidence + } +} + +/// Refinement faults are data, never a colour verdict. `Kernel` preserves the +/// real evaluator error; the other variants reject report/protocol states +/// before the hard classifier can run. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum ApplicableWcag22EvaluationErrorV1 { + Kernel(Wcag22EvaluationErrorV1), + ReportOnly { + profile_id: Wcag22ProfileIdV1, + declaration: Wcag22ClientDeclaredNotApplicableV1, + }, + CriterionMismatch { + requested: Wcag22CriterionV1, + evaluated: Wcag22CriterionV1, + }, +} + +fn refine_applicable_measurement( + requested: Wcag22CriterionV1, + assessment: Wcag22AssessmentV1, +) -> Result { + match assessment { + Wcag22AssessmentV1::Evaluated { + profile_id, + criterion, + measurement, + decision, + evidence, + } => { + if criterion != requested { + return Err(ApplicableWcag22EvaluationErrorV1::CriterionMismatch { + requested, + evaluated: criterion, + }); + } + Ok(ApplicableWcag22MeasurementV1 { + profile_id, + criterion, + measurement, + decision, + evidence, + }) + } + Wcag22AssessmentV1::NotEvaluated { + profile_id, + declaration, + } => Err(ApplicableWcag22EvaluationErrorV1::ReportOnly { + profile_id, + declaration, + }), + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct Wcag22PassV1(()); + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct Wcag22ViolationV1(()); + impl private::EvaluatorSealed for Wcag22Srgb8V1 {} +impl private::HardClassifierSealed for Wcag22Srgb8V1 {} impl Evaluator for Wcag22Srgb8V1 { type Invocation = Wcag22CriterionV1; type Identity = Wcag22Srgb8EvaluatorIdentityV1; type Release = Wcag22ProfileIdV1; type Capability = Wcag22Srgb8CapabilityV1; - type Assessment = Wcag22AssessmentV1; - type Error = Wcag22EvaluationErrorV1; + type Measurement = ApplicableWcag22MeasurementV1; + type Error = ApplicableWcag22EvaluationErrorV1; fn identity(&self) -> Self::Identity { Wcag22Srgb8EvaluatorIdentityV1 @@ -39,7 +133,73 @@ impl Evaluator for Wcag22Srgb8V1 { &self, target: &ModeledSrgb8PointOccurrence, invocation: &Self::Invocation, - ) -> Result { - evaluate_wcag22_srgb8(target.visible(), target.backdrop(), *invocation) + ) -> Result { + let assessment = evaluate_wcag22_srgb8(target.visible(), target.backdrop(), *invocation) + .map_err(ApplicableWcag22EvaluationErrorV1::Kernel)?; + refine_applicable_measurement(*invocation, assessment) + } +} + +impl HardClassifier for Wcag22Srgb8V1 { + type Pass = Wcag22PassV1; + type Violation = Wcag22ViolationV1; + + fn classify( + &self, + _invocation: &Wcag22CriterionV1, + measurement: &ApplicableWcag22MeasurementV1, + ) -> HardDecision { + match measurement.decision { + Wcag22ApplicableDecisionV1::Pass => HardDecision::Pass(Wcag22PassV1(())), + Wcag22ApplicableDecisionV1::Fail => HardDecision::Violation(Wcag22ViolationV1(())), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn genuine_not_evaluated_is_rejected_before_classification() { + let declaration = Wcag22ClientDeclaredNotApplicableV1::try_new("decorative-divider") + .expect("non-empty client declaration must be valid"); + let assessment = Wcag22AssessmentV1::NotEvaluated { + profile_id: wcag22_profile_v1().profile_id, + declaration, + }; + + let error = refine_applicable_measurement(Wcag22CriterionV1::Sc143TextDefault, assessment) + .expect_err("report-only assessment cannot reach a hard classifier"); + let ApplicableWcag22EvaluationErrorV1::ReportOnly { + profile_id, + declaration, + } = error + else { + panic!("NotEvaluated must retain its report-only payload"); + }; + + assert_eq!(profile_id, wcag22_profile_v1().profile_id); + assert_eq!(declaration.reason_id(), "decorative-divider"); + } + + #[test] + fn evaluated_criterion_mismatch_is_a_typed_refinement_error() { + let assessment = + evaluate_wcag22_srgb8([0; 3], [0xFF; 3], Wcag22CriterionV1::Sc143TextLargeScale) + .expect("control pair must evaluate"); + + let error = refine_applicable_measurement(Wcag22CriterionV1::Sc143TextDefault, assessment) + .expect_err("assessment for another criterion must be rejected"); + let ApplicableWcag22EvaluationErrorV1::CriterionMismatch { + requested, + evaluated, + } = error + else { + panic!("criterion mismatch must not be reclassified"); + }; + + assert_eq!(requested, Wcag22CriterionV1::Sc143TextDefault); + assert_eq!(evaluated, Wcag22CriterionV1::Sc143TextLargeScale); } } diff --git a/crates/labcolors-core/src/recheck.rs b/crates/labcolors-core/src/recheck.rs index 819e0d2b..fd6a06b8 100644 --- a/crates/labcolors-core/src/recheck.rs +++ b/crates/labcolors-core/src/recheck.rs @@ -11,37 +11,19 @@ use crate::Srgb8; use crate::appearance::{ OccurrenceId, PaintId, PhysicalProgramIdentityV1, PointOpacityOverSurfaceV1, - SourceOverCertificateV1, SurfaceInputPortId, VisiblePointBindingV1, + SourceOverCertificateV1, SurfaceInputPortId, }; use crate::composition::{AdmittedOpacityV1, OpacityAdmissionErrorV1}; use crate::constraints::{ - BoundAssessment, BoundFailure, BoundVerdict, ExactConstraintIdentityV1, - ExactIdentityAssessmentV1, ExactIdentityCapabilityV1, ExactIdentityMismatchV1, - ExactIdentityReleaseV1, ExactSrgb8IdentityV1, assess, + ExactConstraintIdentityV1, ExactIdentityCapabilityV1, ExactIdentityReleaseV1, + ExactPassEvidenceV1, ExactSrgb8IdentityV1, ExactViolationEvidenceV1, HardDecision, + assess_visible_point_hard, }; use crate::observation::{ ObservationSnapshot, ObservationState, ObservationStreamId, ObservedScenarioSet, PriorObservation, Revision, ScenarioId, }; -type ExactBoundAssessmentV1 = BoundAssessment< - VisiblePointBindingV1, - ExactConstraintIdentityV1, - ExactIdentityReleaseV1, - ExactIdentityCapabilityV1, - Srgb8, - ExactIdentityAssessmentV1, ->; - -type ExactBoundFailureV1 = BoundFailure< - VisiblePointBindingV1, - ExactConstraintIdentityV1, - ExactIdentityReleaseV1, - ExactIdentityCapabilityV1, - Srgb8, - ExactIdentityMismatchV1, ->; - /// Один immutable exact evaluator invocation, связанный с authored occurrence. /// Identity/release/capability не дублируются здесь: proof получает их только /// из binder-а действительно вызванного evaluator-а. @@ -213,32 +195,36 @@ impl CompiledFixedRecheckV1 { paint.opacity, backdrop.bytes(), ); - let assessment = - match assess(&occurrence, &ExactSrgb8IdentityV1, requirement.invocation) { - BoundVerdict::Pass(assessment) => assessment, - BoundVerdict::Fail(verdict) => { - return Ok(FinalRecheckOutcomeV1::Infeasible(InfeasibleRecheckV1 { - occurrence: requirement.occurrence, - surface: requirement.surface, - physical_program: self.physical_program, - paint, - observation: FrozenObservationV1 { - stream, - revision, - schema: schema.to_vec().into_boxed_slice(), - set: set.clone(), - }, - case_index, - verdict, - })); - } - }; + let evidence = match assess_visible_point_hard( + &occurrence, + &ExactSrgb8IdentityV1, + requirement.invocation, + ) { + Ok(HardDecision::Pass(evidence)) => evidence, + Ok(HardDecision::Violation(evidence)) => { + return Ok(FinalRecheckOutcomeV1::Violation(ExactViolationRecheckV1 { + occurrence: requirement.occurrence, + surface: requirement.surface, + physical_program: self.physical_program, + paint, + observation: FrozenObservationV1 { + stream, + revision, + schema: schema.to_vec().into_boxed_slice(), + set: set.clone(), + }, + case_index, + evidence, + })); + } + Err(error) => match error {}, + }; occurrences.push(ExactOccurrenceEvidenceV1 { physical_program: self.physical_program, occurrence: requirement.occurrence, surface: requirement.surface, case_index, - assessment, + evidence, }); } } @@ -331,7 +317,7 @@ pub(crate) struct ExactOccurrenceEvidenceV1 { occurrence: OccurrenceId, surface: SurfaceInputPortId, case_index: usize, - assessment: ExactBoundAssessmentV1, + evidence: ExactPassEvidenceV1, } impl ExactOccurrenceEvidenceV1 { @@ -350,35 +336,35 @@ impl ExactOccurrenceEvidenceV1 { pub(crate) fn program_occurrence_binding( &self, ) -> crate::appearance::ProgramOccurrenceBindingV1 { - self.assessment.binding().program_occurrence() + self.evidence.binding().program_occurrence() } pub(crate) fn constraint(&self) -> ExactConstraintIdentityV1 { - *self.assessment.identity() + *self.evidence.identity() } pub(crate) fn release(&self) -> ExactIdentityReleaseV1 { - *self.assessment.release() + *self.evidence.release() } pub(crate) fn capability(&self) -> ExactIdentityCapabilityV1 { - *self.assessment.capability() + *self.evidence.capability() } pub(crate) fn invocation(&self) -> Srgb8 { - *self.assessment.invocation() + *self.evidence.invocation() } pub(crate) fn target(&self) -> Srgb8 { - *self.assessment.invocation() + self.evidence.target() } pub(crate) fn actual(&self) -> Srgb8 { - Srgb8::new(self.assessment.binding().occurrence().output_rgb()) + self.evidence.actual() } pub(crate) fn physical_certificate(&self) -> SourceOverCertificateV1 { - self.assessment.binding().occurrence() + self.evidence.binding().occurrence() } } @@ -389,17 +375,17 @@ pub(crate) struct WaitingRecheckV1 { } #[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) struct InfeasibleRecheckV1 { +pub(crate) struct ExactViolationRecheckV1 { occurrence: OccurrenceId, surface: SurfaceInputPortId, physical_program: PhysicalProgramIdentityV1, paint: EncodedPaintCandidateV1, observation: FrozenObservationV1, case_index: usize, - verdict: ExactBoundFailureV1, + evidence: ExactViolationEvidenceV1, } -impl InfeasibleRecheckV1 { +impl ExactViolationRecheckV1 { pub(crate) const fn occurrence(&self) -> OccurrenceId { self.occurrence } @@ -421,31 +407,31 @@ impl InfeasibleRecheckV1 { } pub(crate) fn physical_certificate(&self) -> SourceOverCertificateV1 { - self.verdict.binding().occurrence() + self.evidence.binding().occurrence() } pub(crate) fn invocation(&self) -> Srgb8 { - *self.verdict.invocation() + *self.evidence.invocation() } pub(crate) fn constraint(&self) -> ExactConstraintIdentityV1 { - *self.verdict.identity() + *self.evidence.identity() } pub(crate) fn release(&self) -> ExactIdentityReleaseV1 { - *self.verdict.release() + *self.evidence.release() } pub(crate) fn capability(&self) -> ExactIdentityCapabilityV1 { - *self.verdict.capability() + *self.evidence.capability() } pub(crate) fn target(&self) -> Srgb8 { - self.verdict.outcome().target() + self.evidence.target() } pub(crate) fn actual(&self) -> Srgb8 { - self.verdict.outcome().actual() + self.evidence.actual() } } @@ -574,7 +560,7 @@ impl RevisionBoundRecheckV1 { pub(crate) enum FinalRecheckOutcomeV1 { Waiting(WaitingRecheckV1), Stale(StaleRecheckV1), - Infeasible(InfeasibleRecheckV1), + Violation(ExactViolationRecheckV1), Verified(RevisionBoundRecheckV1), } diff --git a/crates/labcolors-core/src/recheck_tests.rs b/crates/labcolors-core/src/recheck_tests.rs index 2eb5219d..e602831a 100644 --- a/crates/labcolors-core/src/recheck_tests.rs +++ b/crates/labcolors-core/src/recheck_tests.rs @@ -127,7 +127,7 @@ fn fixed_candidate_is_verified_only_after_every_final_occurrence_passes() { } #[test] -fn any_failed_case_returns_one_infeasible_outcome_without_partial_verified_value() { +fn any_exact_violation_returns_one_violation_without_partial_verified_value() { let plan = one_occurrence([64, 64, 64]); let state = ready_state( STREAM, @@ -140,40 +140,40 @@ fn any_failed_case_returns_one_infeasible_outcome_without_partial_verified_value ); let candidate = EncodedPaintCandidateV1::new(PAINT, Srgb8::new([0; 3]), 0.25).unwrap(); - let FinalRecheckOutcomeV1::Infeasible(failure) = plan.recheck(&state, candidate).unwrap() + let FinalRecheckOutcomeV1::Violation(violation) = plan.recheck(&state, candidate).unwrap() else { panic!("one failing final occurrence must reject the whole fixed candidate"); }; - assert_eq!(failure.occurrence(), OCCURRENCE_A); - assert_eq!(failure.surface(), SURFACE_A); - assert_eq!(failure.provenance(), &[ScenarioId::new(1)]); + assert_eq!(violation.occurrence(), OCCURRENCE_A); + assert_eq!(violation.surface(), SURFACE_A); + assert_eq!(violation.provenance(), &[ScenarioId::new(1)]); assert_eq!( - failure.physical_program(), + violation.physical_program(), crate::appearance::PhysicalProgramIdentityV1::SolidOpacityOverSurfaceEncodedSrgb8V1 ); - assert_eq!(failure.target(), Srgb8::new([64; 3])); - assert_ne!(failure.actual(), failure.target()); + assert_eq!(violation.target(), Srgb8::new([64; 3])); + assert_ne!(violation.actual(), violation.target()); assert_eq!( - Srgb8::new(failure.physical_certificate().output_rgb()), - failure.actual() + Srgb8::new(violation.physical_certificate().output_rgb()), + violation.actual() ); - assert_eq!(failure.invocation(), failure.target()); + assert_eq!(violation.invocation(), violation.target()); assert_eq!( - failure.constraint(), + violation.constraint(), crate::constraints::ExactConstraintIdentityV1::FinalSrgb8IdentityV1 ); assert_eq!( - failure.release(), + violation.release(), crate::constraints::ExactIdentityReleaseV1::V1 ); assert_eq!( - failure.capability(), + violation.capability(), crate::constraints::ExactIdentityCapabilityV1::FinalOccurrenceSrgb8IdentityV1 ); } #[test] -fn waiting_stale_infeasible_verified_and_hold_are_distinct() { +fn waiting_stale_violation_verified_and_hold_are_distinct() { let plan = one_occurrence([10, 20, 30]); let candidate = opaque([10, 20, 30]); let mut state = ObservationState::new(STREAM, vec![SURFACE_A]).unwrap(); @@ -456,28 +456,6 @@ fn singleton_recheck_is_differentially_equal_to_g1a_and_point_program() { assert_eq!(evidence.physical_certificate(), *g1a.certificate()); } -#[test] -fn production_g1a_uses_the_same_bound_evaluator_instead_of_manual_evidence() { - let analog = include_str!("analog.rs"); - assert_eq!( - analog - .matches("assess(&occurrence, &ExactSrgb8IdentityV1") - .count(), - 1 - ); - for forbidden in [ - "ExactSrgb8IdentityV1::evaluate", - "enum ExactIdentityReleaseV1", - "enum ExactIdentityCapabilityV1", - "fn constraint_identity", - ] { - assert!( - !analog.contains(forbidden), - "G1a still bypasses F1a: {forbidden}" - ); - } -} - proptest! { #[test] fn every_physical_case_is_required_not_just_any_case( @@ -503,7 +481,7 @@ proptest! { prop_assert!(matches!( plan.recheck(&state, candidate), - Ok(FinalRecheckOutcomeV1::Infeasible(_)) + Ok(FinalRecheckOutcomeV1::Violation(_)) )); } @@ -667,7 +645,7 @@ fn canonical_tuple_cannot_be_reinterpreted_under_a_different_surface_schema() { } #[test] -fn infeasible_witness_keeps_the_authored_paint_identity() { +fn violation_witness_keeps_the_authored_paint_identity() { let second_paint = PaintId::new(8); let requirement = || { vec![ExactOccurrenceRequirementV1::new( @@ -688,12 +666,12 @@ fn infeasible_witness_keeps_the_authored_paint_identity() { let second_candidate = EncodedPaintCandidateV1::new(second_paint, Srgb8::new([0; 3]), 0.25).unwrap(); - let FinalRecheckOutcomeV1::Infeasible(first) = + let FinalRecheckOutcomeV1::Violation(first) = first_plan.recheck(&state, first_candidate).unwrap() else { panic!("control candidate must fail"); }; - let FinalRecheckOutcomeV1::Infeasible(second) = + let FinalRecheckOutcomeV1::Violation(second) = second_plan.recheck(&state, second_candidate).unwrap() else { panic!("control candidate must fail"); diff --git a/crates/labcolors-core/src/semantic.rs b/crates/labcolors-core/src/semantic.rs index bc9df369..e5e4d8be 100644 --- a/crates/labcolors-core/src/semantic.rs +++ b/crates/labcolors-core/src/semantic.rs @@ -2784,10 +2784,13 @@ fn resolve_rgba_inverted_with_binding( let verified = match crate::analog::resolve_verified(authored, target, requested_alpha, backdrop) { Ok(verified) => verified, - Err(error) => { - return Err(SolveFailure::InternalInvariant(format!( - "validated alpha-analog resolver violated its total-domain contract: {error}" - ))); + Err(_error) => { + // Здесь вход уже прошёл admission; typed witness остаётся в + // analog boundary, а публичная semantic-ошибка не сериализует + // authored routing или внутреннюю структуру evidence. + return Err(SolveFailure::InternalInvariant( + "validated alpha-analog resolver violated its total-domain contract".into(), + )); } }; let actual_alpha = verified.alpha(); From 39d00141cb5ce4cc0c4e14fda6352ec5baab0cad Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Mon, 20 Jul 2026 07:01:40 +0300 Subject: [PATCH 2/8] =?UTF-8?q?chore:=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BF?= =?UTF-8?q?=D1=80=D0=B8=D0=B2=D1=8F=D0=B7=D0=B0=D1=82=D1=8C=20WASM-=D1=80?= =?UTF-8?q?=D0=B0=D1=82=D1=87=D0=B5=D1=82=20=D0=BA=20V1b?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/colors/bench/wasm.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/colors/bench/wasm.json b/packages/colors/bench/wasm.json index c8a47a55..8a0289db 100644 --- a/packages/colors/bench/wasm.json +++ b/packages/colors/bench/wasm.json @@ -19,13 +19,13 @@ "command": "CARGO_ENCODED_RUSTFLAGS= wasm-pack build crates/labcolors-wasm --release --target web --out-dir ../../packages/colors/pkg --out-name labcolors --locked" }, "measurement": { - "source": "github-actions-run-29701262743", + "source": "github-actions-run-29709392606", "platform": "linux-x64", - "rawBytes": 387058 + "rawBytes": 386488 }, "policy": { - "maxRawBytes": 387058, - "basis": "c8b-canonical-point-compositor-exact-head", + "maxRawBytes": 386488, + "basis": "v1b-evaluator-classifier-exact-head", "gzip": "diagnostic-only" } } From b9196bdb886169867b2c3d6521c85a981763fd03 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Mon, 20 Jul 2026 07:03:29 +0300 Subject: [PATCH 3/8] =?UTF-8?q?chore:=20=D0=B7=D0=B0=D0=BA=D1=80=D0=B5?= =?UTF-8?q?=D0=BF=D0=B8=D1=82=D1=8C=20SHA=20=D0=BD=D0=BE=D0=B2=D0=BE=D0=B3?= =?UTF-8?q?=D0=BE=20WASM-=D0=B1=D1=8E=D0=B4=D0=B6=D0=B5=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/check-wasm-size-budget.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/check-wasm-size-budget.mjs b/scripts/check-wasm-size-budget.mjs index 30e840a3..8dd2cf19 100644 --- a/scripts/check-wasm-size-budget.mjs +++ b/scripts/check-wasm-size-budget.mjs @@ -14,7 +14,7 @@ export const DEFAULT_BUDGET = resolve( "packages/colors/bench/wasm.json", ); export const WASM_BUDGET_FILE_SHA256 = - "e01d8055e884bad5377af58cdc6aa1bf232a3f30f93abc250220d4947a7187c9"; + "d4d6e57d23703cdaa6016c1b9579004c2a83e0cad953809d49d5c82bfa7e2d70"; const SCHEMA_VERSION = 1; const CANONICAL_ARTIFACT = "packages/colors/pkg/labcolors_bg.wasm"; From 377bed6c3657762d219850857c1446ff2c5e15e9 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Mon, 20 Jul 2026 07:12:49 +0300 Subject: [PATCH 4/8] =?UTF-8?q?ci:=20=D0=B2=D1=80=D0=B5=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D0=BD=D0=BE=20=D0=B8=D0=B7=D0=BC=D0=B5=D1=80=D0=B8=D1=82=D1=8C?= =?UTF-8?q?=20canonical=20WASM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/wasm-measure-temporary.yml | 58 ++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 .github/workflows/wasm-measure-temporary.yml diff --git a/.github/workflows/wasm-measure-temporary.yml b/.github/workflows/wasm-measure-temporary.yml new file mode 100644 index 00000000..845a6de7 --- /dev/null +++ b/.github/workflows/wasm-measure-temporary.yml @@ -0,0 +1,58 @@ +name: Temporary WASM measurement + +on: + push: + branches: + - agent/v1b-evaluator-classifier + +permissions: + contents: read + +env: + RUST_TOOLCHAIN: 1.96.0 + +jobs: + measure: + name: Measure canonical WASM + runs-on: [self-hosted, Linux, X64] + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + - name: Isolate toolchain state + run: | + echo "RUSTUP_HOME=$RUNNER_TEMP/rustup-$GITHUB_JOB" >> "$GITHUB_ENV" + echo "CARGO_HOME=$RUNNER_TEMP/cargo-$GITHUB_JOB" >> "$GITHUB_ENV" + echo "WASM_PACK_CACHE=$RUNNER_TEMP/wasm-pack-$GITHUB_JOB" >> "$GITHUB_ENV" + echo "TMPDIR=$RUNNER_TEMP/tmp-$GITHUB_JOB" >> "$GITHUB_ENV" + mkdir -p "$RUNNER_TEMP/tmp-$GITHUB_JOB" "$RUNNER_TEMP/wasm-pack-$GITHUB_JOB" + - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # master @ 2026-03-27 + with: + toolchain: ${{ env.RUST_TOOLCHAIN }} + targets: wasm32-unknown-unknown + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 + - name: Install wasm-pack + run: | + cargo install wasm-pack --version 0.13.1 --locked >"$RUNNER_TEMP/wasm-pack-install.log" 2>&1 + echo "$CARGO_HOME/bin" >> "$GITHUB_PATH" + - name: Build and record exact artifact + run: | + set -euo pipefail + export CARGO_ENCODED_RUSTFLAGS="--remap-path-prefix=$GITHUB_WORKSPACE=/workspace/lab-colors"$'\x1f'"--remap-path-prefix=$CARGO_HOME=/cargo-home" + rm -rf packages/colors/pkg + wasm-pack build crates/labcolors-wasm --release --target web --out-dir ../../packages/colors/pkg --out-name labcolors --locked >"$RUNNER_TEMP/wasm-build.log" 2>&1 + wasm=packages/colors/pkg/labcolors_bg.wasm + { + printf 'rawBytes=' + wc -c < "$wasm" | tr -d '[:space:]' + printf '\nsha256=' + sha256sum "$wasm" | cut -d' ' -f1 + printf '\n' + } | tee "$RUNNER_TEMP/wasm-measurement.txt" + - name: Upload exact measurement + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: wasm-measurement-${{ github.sha }} + path: ${{ runner.temp }}/wasm-measurement.txt + if-no-files-found: error + retention-days: 1 From 8282eea8f1af11bfca613ddedc86200fbde8c50d Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Mon, 20 Jul 2026 07:15:52 +0300 Subject: [PATCH 5/8] =?UTF-8?q?ci:=20=D0=B7=D0=B0=D0=BF=D1=83=D1=81=D1=82?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=B2=D1=80=D0=B5=D0=BC=D0=B5=D0=BD=D0=BD?= =?UTF-8?q?=D0=BE=D0=B5=20=D0=B8=D0=B7=D0=BC=D0=B5=D1=80=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D0=B2=20PR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/wasm-measure-temporary.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/wasm-measure-temporary.yml b/.github/workflows/wasm-measure-temporary.yml index 845a6de7..6ed04288 100644 --- a/.github/workflows/wasm-measure-temporary.yml +++ b/.github/workflows/wasm-measure-temporary.yml @@ -4,6 +4,7 @@ on: push: branches: - agent/v1b-evaluator-classifier + pull_request: permissions: contents: read @@ -15,6 +16,9 @@ jobs: measure: name: Measure canonical WASM runs-on: [self-hosted, Linux, X64] + if: >- + github.event_name != 'pull_request' || + github.event.pull_request.head.repo.full_name == github.repository steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: From 2b736e53613f0e40d0f7c4130fd9a19eb9801803 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Mon, 20 Jul 2026 07:22:01 +0300 Subject: [PATCH 6/8] =?UTF-8?q?chore:=20=D0=B7=D0=B0=D0=BA=D1=80=D0=B5?= =?UTF-8?q?=D0=BF=D0=B8=D1=82=D1=8C=20canonical=20WASM=20=D0=BF=D0=BE?= =?UTF-8?q?=D1=81=D0=BB=D0=B5=20docs-=D1=81=D1=80=D0=B5=D0=B7=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/colors/bench/wasm.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/colors/bench/wasm.json b/packages/colors/bench/wasm.json index 8a0289db..a86a8338 100644 --- a/packages/colors/bench/wasm.json +++ b/packages/colors/bench/wasm.json @@ -19,13 +19,13 @@ "command": "CARGO_ENCODED_RUSTFLAGS= wasm-pack build crates/labcolors-wasm --release --target web --out-dir ../../packages/colors/pkg --out-name labcolors --locked" }, "measurement": { - "source": "github-actions-run-29709392606", + "source": "github-actions-run-29716449361", "platform": "linux-x64", - "rawBytes": 386488 + "rawBytes": 386547 }, "policy": { - "maxRawBytes": 386488, - "basis": "v1b-evaluator-classifier-exact-head", + "maxRawBytes": 386547, + "basis": "v1b-after-docs-exact-head", "gzip": "diagnostic-only" } } From 87f56bde531005cbc9cc0ae5ca2be3b41e87966c Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Mon, 20 Jul 2026 07:22:54 +0300 Subject: [PATCH 7/8] =?UTF-8?q?chore:=20=D0=B7=D0=B0=D0=BA=D1=80=D0=B5?= =?UTF-8?q?=D0=BF=D0=B8=D1=82=D1=8C=20SHA=20canonical=20WASM-=D0=B1=D1=8E?= =?UTF-8?q?=D0=B4=D0=B6=D0=B5=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/check-wasm-size-budget.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/check-wasm-size-budget.mjs b/scripts/check-wasm-size-budget.mjs index 8dd2cf19..0827d509 100644 --- a/scripts/check-wasm-size-budget.mjs +++ b/scripts/check-wasm-size-budget.mjs @@ -14,7 +14,7 @@ export const DEFAULT_BUDGET = resolve( "packages/colors/bench/wasm.json", ); export const WASM_BUDGET_FILE_SHA256 = - "d4d6e57d23703cdaa6016c1b9579004c2a83e0cad953809d49d5c82bfa7e2d70"; + "1195cf28878d0e717af7d56c51a77621dcb9b060478965180cfc66befea3c3c5"; const SCHEMA_VERSION = 1; const CANONICAL_ARTIFACT = "packages/colors/pkg/labcolors_bg.wasm"; From 1aec4817ea30565577a120605f8b0dee9d9e37d5 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Mon, 20 Jul 2026 07:23:05 +0300 Subject: [PATCH 8/8] =?UTF-8?q?ci:=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20=D0=B2=D1=80=D0=B5=D0=BC=D0=B5=D0=BD=D0=BD=D0=BE=D0=B5?= =?UTF-8?q?=20WASM-=D0=B8=D0=B7=D0=BC=D0=B5=D1=80=D0=B5=D0=BD=D0=B8=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/wasm-measure-temporary.yml | 62 -------------------- 1 file changed, 62 deletions(-) delete mode 100644 .github/workflows/wasm-measure-temporary.yml diff --git a/.github/workflows/wasm-measure-temporary.yml b/.github/workflows/wasm-measure-temporary.yml deleted file mode 100644 index 6ed04288..00000000 --- a/.github/workflows/wasm-measure-temporary.yml +++ /dev/null @@ -1,62 +0,0 @@ -name: Temporary WASM measurement - -on: - push: - branches: - - agent/v1b-evaluator-classifier - pull_request: - -permissions: - contents: read - -env: - RUST_TOOLCHAIN: 1.96.0 - -jobs: - measure: - name: Measure canonical WASM - runs-on: [self-hosted, Linux, X64] - if: >- - github.event_name != 'pull_request' || - github.event.pull_request.head.repo.full_name == github.repository - steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - persist-credentials: false - - name: Isolate toolchain state - run: | - echo "RUSTUP_HOME=$RUNNER_TEMP/rustup-$GITHUB_JOB" >> "$GITHUB_ENV" - echo "CARGO_HOME=$RUNNER_TEMP/cargo-$GITHUB_JOB" >> "$GITHUB_ENV" - echo "WASM_PACK_CACHE=$RUNNER_TEMP/wasm-pack-$GITHUB_JOB" >> "$GITHUB_ENV" - echo "TMPDIR=$RUNNER_TEMP/tmp-$GITHUB_JOB" >> "$GITHUB_ENV" - mkdir -p "$RUNNER_TEMP/tmp-$GITHUB_JOB" "$RUNNER_TEMP/wasm-pack-$GITHUB_JOB" - - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # master @ 2026-03-27 - with: - toolchain: ${{ env.RUST_TOOLCHAIN }} - targets: wasm32-unknown-unknown - - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - - name: Install wasm-pack - run: | - cargo install wasm-pack --version 0.13.1 --locked >"$RUNNER_TEMP/wasm-pack-install.log" 2>&1 - echo "$CARGO_HOME/bin" >> "$GITHUB_PATH" - - name: Build and record exact artifact - run: | - set -euo pipefail - export CARGO_ENCODED_RUSTFLAGS="--remap-path-prefix=$GITHUB_WORKSPACE=/workspace/lab-colors"$'\x1f'"--remap-path-prefix=$CARGO_HOME=/cargo-home" - rm -rf packages/colors/pkg - wasm-pack build crates/labcolors-wasm --release --target web --out-dir ../../packages/colors/pkg --out-name labcolors --locked >"$RUNNER_TEMP/wasm-build.log" 2>&1 - wasm=packages/colors/pkg/labcolors_bg.wasm - { - printf 'rawBytes=' - wc -c < "$wasm" | tr -d '[:space:]' - printf '\nsha256=' - sha256sum "$wasm" | cut -d' ' -f1 - printf '\n' - } | tee "$RUNNER_TEMP/wasm-measurement.txt" - - name: Upload exact measurement - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 - with: - name: wasm-measurement-${{ github.sha }} - path: ${{ runner.temp }}/wasm-measurement.txt - if-no-files-found: error - retention-days: 1