From fade33a2a2d39af28cdffcb9611bb1adc8b6c75a Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Fri, 24 Jul 2026 20:51:37 +0300 Subject: [PATCH 1/4] core: bind Program content identity --- .../labcolors-core/src/constraints/exact.rs | 20 +- crates/labcolors-core/src/constraints/mod.rs | 66 +- .../labcolors-core/src/constraints/wcag22.rs | 19 +- .../src/generic_boundary_tests.rs | 36 +- crates/labcolors-core/src/lib.rs | 4 + crates/labcolors-core/src/program_identity.rs | 1723 +++++++++++++++++ .../src/program_identity_tests.rs | 1189 ++++++++++++ .../src/program_joint_integration_tests.rs | 10 +- crates/labcolors-core/src/program_session.rs | 126 +- crates/labcolors-core/src/sha256.rs | 31 +- 10 files changed, 3159 insertions(+), 65 deletions(-) create mode 100644 crates/labcolors-core/src/program_identity.rs create mode 100644 crates/labcolors-core/src/program_identity_tests.rs diff --git a/crates/labcolors-core/src/constraints/exact.rs b/crates/labcolors-core/src/constraints/exact.rs index 34fed4fb..fa5f458d 100644 --- a/crates/labcolors-core/src/constraints/exact.rs +++ b/crates/labcolors-core/src/constraints/exact.rs @@ -1,7 +1,8 @@ use crate::Srgb8; use crate::appearance::ModeledSrgb8PointOccurrence; use crate::constraints::{ - Evaluator, HardClassifier, HardDecision, ProgramPointTargetV1, VisiblePointPassEvidence, + Evaluator, HardClassifier, HardDecision, ProgramConstraintContentV1, + ProgramPointEvaluatorContentV1, ProgramPointTargetV1, VisiblePointPassEvidence, VisiblePointViolationEvidence, private, }; use core::convert::Infallible; @@ -12,12 +13,16 @@ use core::convert::Infallible; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum ExactConstraintIdentityV1 { FinalSrgb8IdentityV1, + #[cfg(test)] + MutationSentinelV1, } /// Версия формулы exact byte-identity evaluator-а. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum ExactIdentityReleaseV1 { V1, + #[cfg(test)] + MutationSentinelV1, } /// Узкая capability evaluator-а: только финальный modeled point occurrence в @@ -25,6 +30,8 @@ pub(crate) enum ExactIdentityReleaseV1 { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum ExactIdentityCapabilityV1 { FinalOccurrenceSrgb8IdentityV1, + #[cfg(test)] + MutationSentinelV1, } /// Закрытые ZST payload-типы делают Pass и Violation несовместимыми, но не @@ -111,6 +118,17 @@ impl Evaluator for ExactSrgb8IdentityV1 { } } +impl ProgramPointEvaluatorContentV1 for ExactSrgb8IdentityV1 { + fn program_constraint_content_v1(&self, invocation: Srgb8) -> ProgramConstraintContentV1 { + ProgramConstraintContentV1::ExactSrgb8 { + identity: >::identity(self), + release: >::release(self), + capability: >::capability(self), + expected: invocation, + } + } +} + impl HardClassifier for ExactSrgb8IdentityV1 { type Pass = ExactIdentityPassV1; type Violation = ExactIdentityViolationV1; diff --git a/crates/labcolors-core/src/constraints/mod.rs b/crates/labcolors-core/src/constraints/mod.rs index fc56ab48..4aae4880 100644 --- a/crates/labcolors-core/src/constraints/mod.rs +++ b/crates/labcolors-core/src/constraints/mod.rs @@ -8,6 +8,7 @@ use crate::Srgb8; use crate::appearance::{ModeledSrgb8PointOccurrence, ResolvedOccurrence, VisiblePointBindingV1}; use crate::lcs_occurrence::ModeledLcsOccurrenceV1; +use crate::wcag22::{Wcag22CriterionV1, Wcag22ProfileIdV1}; mod exact; pub(crate) use exact::{ @@ -20,7 +21,7 @@ pub(crate) use exact::ExactIdentityPassV1; mod wcag22; -pub(crate) use wcag22::Wcag22Srgb8V1; +pub(crate) use wcag22::{Wcag22Srgb8CapabilityV1, Wcag22Srgb8EvaluatorIdentityV1, Wcag22Srgb8V1}; #[cfg(test)] pub(crate) use wcag22::{ @@ -266,6 +267,7 @@ pub(crate) trait ProgramPointEvaluatorV1: Sized + Evaluator + HardClassifier, ProgramPointMeasurement> + + ProgramPointEvaluatorContentV1 { } @@ -273,9 +275,44 @@ impl ProgramPointEvaluatorV1 for Evaluation where Evaluation: Sized + Evaluator + HardClassifier, ProgramPointMeasurement> + + ProgramPointEvaluatorContentV1 { } +/// Полное code-owned описание одного evaluator invocation для compile identity. +/// +/// Здесь намеренно нет авторского constraint ID. Метаданные берутся из того же +/// закрытого определения evaluator-а, которое связывает runtime evidence, и не +/// могут разойтись с его identity, release или capability. Добавление либо +/// изменение production evaluator-а остаётся явной сменой схемы. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ProgramConstraintContentV1 { + ExactSrgb8 { + identity: ExactConstraintIdentityV1, + release: ExactIdentityReleaseV1, + capability: ExactIdentityCapabilityV1, + expected: Srgb8, + }, + Wcag22Srgb8 { + identity: Wcag22Srgb8EvaluatorIdentityV1, + release: Wcag22ProfileIdV1, + capability: Wcag22Srgb8CapabilityV1, + criterion: Wcag22CriterionV1, + }, + #[cfg(test)] + FinalRecheckMutantExactSrgb8 { expected: Srgb8 }, +} + +/// Внутрикрейтное описание generic test seam с одним evaluator-ом. Package +/// Program использует закрытое heterogeneous-множество, поэтому клиент не +/// может подменить descriptor. +pub(crate) trait ProgramPointEvaluatorContentV1: Evaluator { + fn program_constraint_content_v1( + &self, + invocation: ProgramPointInvocation, + ) -> ProgramConstraintContentV1; +} + #[cfg(test)] impl Evaluator for CountingProgramWcag22Srgb8V1 { type Invocation = >::Invocation; @@ -313,6 +350,21 @@ impl Evaluator for CountingProgramWcag22Srgb8V1 { } } +#[cfg(test)] +impl ProgramPointEvaluatorContentV1 for CountingProgramWcag22Srgb8V1 { + fn program_constraint_content_v1( + &self, + invocation: ProgramPointInvocation, + ) -> ProgramConstraintContentV1 { + ProgramConstraintContentV1::Wcag22Srgb8 { + identity: self.identity(), + release: self.release(), + capability: self.capability(), + criterion: invocation, + } + } +} + #[cfg(test)] impl HardClassifier< @@ -375,6 +427,18 @@ impl Evaluator for FinalRecheckMutantProgramEvaluatorV1 { } } +#[cfg(test)] +impl ProgramPointEvaluatorContentV1 for FinalRecheckMutantProgramEvaluatorV1 { + fn program_constraint_content_v1( + &self, + invocation: ProgramPointInvocation, + ) -> ProgramConstraintContentV1 { + ProgramConstraintContentV1::FinalRecheckMutantExactSrgb8 { + expected: invocation, + } + } +} + #[cfg(test)] impl HardClassifier for FinalRecheckMutantProgramEvaluatorV1 { type Pass = MutantExactPassV1; diff --git a/crates/labcolors-core/src/constraints/wcag22.rs b/crates/labcolors-core/src/constraints/wcag22.rs index d57d854a..c4b28a87 100644 --- a/crates/labcolors-core/src/constraints/wcag22.rs +++ b/crates/labcolors-core/src/constraints/wcag22.rs @@ -1,5 +1,8 @@ use crate::appearance::ModeledSrgb8PointOccurrence; -use crate::constraints::{Evaluator, HardClassifier, HardDecision, ProgramPointTargetV1, private}; +use crate::constraints::{ + Evaluator, HardClassifier, HardDecision, ProgramConstraintContentV1, + ProgramPointEvaluatorContentV1, ProgramPointTargetV1, private, +}; use crate::numerics::NumericalDecisionEvidenceV1; use crate::wcag22::{ Wcag22ApplicableDecisionV1, Wcag22AssessmentV1, Wcag22ClientDeclaredNotApplicableV1, @@ -175,6 +178,20 @@ impl Evaluator for Wcag22Srgb8V1 { } } +impl ProgramPointEvaluatorContentV1 for Wcag22Srgb8V1 { + fn program_constraint_content_v1( + &self, + invocation: Wcag22CriterionV1, + ) -> ProgramConstraintContentV1 { + ProgramConstraintContentV1::Wcag22Srgb8 { + identity: >::identity(self), + release: >::release(self), + capability: >::capability(self), + criterion: invocation, + } + } +} + impl HardClassifier for Wcag22Srgb8V1 { type Pass = Wcag22PassV1; type Violation = Wcag22ViolationV1; diff --git a/crates/labcolors-core/src/generic_boundary_tests.rs b/crates/labcolors-core/src/generic_boundary_tests.rs index 53457a93..f563773f 100644 --- a/crates/labcolors-core/src/generic_boundary_tests.rs +++ b/crates/labcolors-core/src/generic_boundary_tests.rs @@ -8,13 +8,15 @@ const OBSERVATION_SOURCE: &str = include_str!("observation.rs"); const OUTPUT_PROJECTION_SOURCE: &str = include_str!("output_projection.rs"); const PACKAGE_BRIDGE_SOURCE: &str = include_str!("package_bridge.rs"); const POINT_SUPPORT_SOURCE: &str = include_str!("point_support.rs"); +const PROGRAM_IDENTITY_SOURCE: &str = include_str!("program_identity.rs"); const PROGRAM_SESSION_SOURCE: &str = include_str!("program_session.rs"); const SESSION_SOURCE: &str = include_str!("session.rs"); const WCAG22_CONSTRAINT_SOURCE: &str = include_str!("constraints/wcag22.rs"); -const GENERIC_SOURCES: [(&str, &str); 3] = [ +const GENERIC_SOURCES: [(&str, &str); 4] = [ ("appearance.rs", APPEARANCE_SOURCE), ("lcs_occurrence.rs", LCS_OCCURRENCE_SOURCE), + ("program_identity.rs", PROGRAM_IDENTITY_SOURCE), ("program_session.rs", PROGRAM_SESSION_SOURCE), ]; @@ -541,7 +543,7 @@ fn program_session_owns_context_bound_lcs_evidence_and_one_session_scratch_cache "let outputs = compile_outputs(", ); for required in [ - "compile_constraints::(&graph, &all_occurrence_contexts, program.constraints)?", + "compile_constraints::(&graph, &all_occurrence_contexts, &program.constraints)?", "compact_constraint_contexts(&all_occurrence_contexts, &mut constraints)?", ] { assert!( @@ -587,6 +589,36 @@ fn program_session_owns_context_bound_lcs_evidence_and_one_session_scratch_cache } } +#[test] +fn cold_program_normalization_reuses_owned_unordered_buffers() { + let compiler = PROGRAM_SESSION_SOURCE + .split_whitespace() + .collect::>() + .join(" "); + for required in [ + "authored_targets: &mut [Target]", + "authored_selection: Option<&mut DeclaredJointSelectionV1>", + "let TargetDomainV1::Finite(candidates) = &mut target.domain", + "authored_state .choices .sort_unstable_by_key", + "authored: &mut [OutputBinding]", + ] { + assert!( + compiler.contains(required), + "cold Program compilation must normalize owned buffers in place; missing `{required}`", + ); + } + for forbidden in [ + "candidates.extend_from_slice(authored_candidates)", + "choices.extend_from_slice(&authored_state.choices)", + "authored.extend_from_slice(authored_outputs)", + ] { + assert!( + !compiler.contains(forbidden), + "cold Program compilation must not restore avoidable shadow copy `{forbidden}`", + ); + } +} + #[test] fn program_lcs_boundary_has_no_legacy_color_or_projection_shortcuts() { for forbidden in [ diff --git a/crates/labcolors-core/src/lib.rs b/crates/labcolors-core/src/lib.rs index ec0b63ea..fa185ec4 100644 --- a/crates/labcolors-core/src/lib.rs +++ b/crates/labcolors-core/src/lib.rs @@ -64,6 +64,7 @@ pub(crate) mod program_session; pub(crate) mod release_registry; pub mod scale; pub mod semantic; +pub(crate) mod sha256; pub mod solve; pub(crate) mod wcag; @@ -102,6 +103,9 @@ mod program_joint_integration_tests; #[cfg(test)] mod program_mixed_evaluator_tests; +#[cfg(test)] +mod program_identity_tests; + #[cfg(test)] mod release_registry_tests; diff --git a/crates/labcolors-core/src/program_identity.rs b/crates/labcolors-core/src/program_identity.rs new file mode 100644 index 00000000..53a1b7f3 --- /dev/null +++ b/crates/labcolors-core/src/program_identity.rs @@ -0,0 +1,1723 @@ +//! Устойчивый к коллизиям адрес исполняемого содержимого принятой Program. +//! +//! Paint/Surface/Occurrence способны образовывать двудольные графы +//! инцидентности, поэтому одного топологического хеша или уточнения разбиения +//! недостаточно. Модуль строит типизированный цветной граф, канонизирует его без +//! opaque ID и хеширует канонический прообраз. + +use super::*; + +const DOMAIN_V1: &[u8] = b"labcolors.program-content-identity.v1\0"; +// Максимальный V1-цвет принадлежит Occurrence: теги вершины, композиции, +// контекста и frame, два binary64-параметра наблюдения и surround. Явная +// граница устраняет аллокацию на каждую вершину и требует пересмотра при +// расширении схемы вместо скрытого runtime-лимита. +const COLOR_CAPACITY: usize = 1 + 1 + 1 + 4 + 8 + 8 + 1; + +mod release_tag { + pub(super) const PROGRAM_SCHEMA_V1: u8 = 1; + pub(super) const DECLARED_TOTAL_ORDER_V1: u8 = 1; + pub(super) const FRESH_FULL_RECHECK_V1: u8 = 1; + pub(super) const ATOMIC_OBSERVATION_GROUP_V1: u8 = 1; + pub(super) const ENCODED_PAINT_EMISSION_V1: u8 = 1; + pub(super) const MODELED_LCS_OCCURRENCE_V1: u8 = 1; + + pub(super) const IEC_SRGB8_D65_OUTPUT_PROFILE_V1: u8 = 1; + pub(super) const IEC_SRGB8_TO_XYZ_D65_TRANSFORM_V1: u8 = 1; + pub(super) const CIE1931_TWO_DEGREE_OBSERVER_V1: u8 = 1; + pub(super) const IEC61966_D65_REFERENCE_WHITE_V1: u8 = 1; + pub(super) const RELATIVE_Y1_SCALE_V1: u8 = 1; + pub(super) const XYZ_FRAME_V1: u8 = 1; + #[cfg(test)] + pub(super) const MUTATION_SENTINEL_FRAME_V1: u8 = 2; + pub(super) const CIECAM16_VIEWING_INPUTS_V1: u8 = 1; + pub(super) const ENCODED_SRGB8_SOURCE_OVER_V1: u8 = 1; + + pub(super) const EXACT_SRGB8_FAMILY_V1: u8 = 1; + pub(super) const EXACT_SRGB8_IDENTITY_V1: u8 = 1; + pub(super) const EXACT_SRGB8_RELEASE_V1: u8 = 1; + pub(super) const EXACT_SRGB8_CAPABILITY_V1: u8 = 1; + #[cfg(test)] + pub(super) const EXACT_SRGB8_IDENTITY_MUTATION_SENTINEL_V1: u8 = 2; + #[cfg(test)] + pub(super) const EXACT_SRGB8_RELEASE_MUTATION_SENTINEL_V1: u8 = 2; + #[cfg(test)] + pub(super) const EXACT_SRGB8_CAPABILITY_MUTATION_SENTINEL_V1: u8 = 2; + pub(super) const WCAG22_SRGB8_FAMILY_V1: u8 = 2; + pub(super) const WCAG22_SRGB8_IDENTITY_V1: u8 = 1; + pub(super) const WCAG22_SRGB8_PROFILE_V1: u8 = 1; + pub(super) const WCAG22_SRGB8_CAPABILITY_V1: u8 = 1; +} + +/// Устойчивый к коллизиям адрес канонизированного содержимого Program V1. +/// +/// SHA-256 не делает адрес инъективным. Адрес не связывает пространства opaque +/// ID и не подтверждает владельца, поколение либо revision. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct ProgramContentIdentityV1([u8; 32]); + +impl ProgramContentIdentityV1 { + pub(crate) const fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +struct VertexColorV1 { + len: u8, + bytes: [u8; COLOR_CAPACITY], +} + +impl VertexColorV1 { + fn new(tag: u8) -> Self { + let mut value = Self { + len: 1, + bytes: [0; COLOR_CAPACITY], + }; + value.bytes[0] = tag; + value + } + + fn push_u8(&mut self, value: u8) -> Result<(), ProgramCompileError> { + let index = usize::from(self.len); + let slot = self + .bytes + .get_mut(index) + .ok_or(ProgramCompileError::InternalInvariant)?; + *slot = value; + self.len = self + .len + .checked_add(1) + .ok_or(ProgramCompileError::InternalInvariant)?; + Ok(()) + } + + fn push_u64(&mut self, value: u64) -> Result<(), ProgramCompileError> { + for byte in value.to_be_bytes() { + self.push_u8(byte)?; + } + Ok(()) + } + + fn push_srgb8(&mut self, value: Srgb8) -> Result<(), ProgramCompileError> { + for byte in value.bytes() { + self.push_u8(byte)?; + } + Ok(()) + } + + fn as_slice(&self) -> &[u8] { + &self.bytes[..usize::from(self.len)] + } +} + +mod vertex_tag { + pub(super) const PROGRAM: u8 = 1; + pub(super) const SOURCE: u8 = 2; + pub(super) const TARGET_FIXED: u8 = 3; + pub(super) const TARGET_FINITE: u8 = 4; + pub(super) const CANDIDATE: u8 = 5; + pub(super) const OPACITY: u8 = 6; + pub(super) const PAINT_SOLID: u8 = 7; + pub(super) const PAINT_OPACITY: u8 = 8; + pub(super) const OBSERVATION_GROUP: u8 = 9; + pub(super) const SURFACE_INPUT_PORT: u8 = 10; + pub(super) const SURFACE_INPUT: u8 = 11; + pub(super) const SURFACE_FROM_OCCURRENCE: u8 = 12; + pub(super) const OCCURRENCE: u8 = 13; + pub(super) const CONSTRAINT_HARD: u8 = 14; + pub(super) const CONSTRAINT_REPORT_ONLY: u8 = 15; + pub(super) const OUTPUT: u8 = 16; + pub(super) const JOINT_SELECTION: u8 = 17; + pub(super) const JOINT_STATE: u8 = 18; + pub(super) const JOINT_CHOICE: u8 = 19; +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[repr(u8)] +enum EdgeRoleV1 { + ProgramMember = 1, + TargetSource = 2, + TargetCandidate = 3, + SolidTarget = 4, + OpacitySourcePaint = 5, + OpacityInput = 6, + ObservationGroupPort = 7, + InputSurfacePort = 8, + DerivedSurfaceOccurrence = 9, + OccurrenceSubjectPaint = 10, + OccurrenceBackdropSurface = 11, + ConstraintOccurrence = 12, + OutputPaint = 13, + SelectionState = 14, + StateChoice = 15, + ChoiceTarget = 16, + ChoiceCandidate = 17, +} + +#[derive(Debug, Clone, Copy)] +struct EdgeV1 { + from: usize, + to: usize, + role: EdgeRoleV1, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +struct ArcV1 { + direction: u8, + role: EdgeRoleV1, + neighbour: usize, +} + +struct CanonicalGraphV1 { + colors: Vec, + adjacency: Vec>, + edge_count: usize, +} + +struct GraphBuilderV1 { + colors: Vec, + edges: Vec, + root: usize, +} + +impl GraphBuilderV1 { + fn new(root: VertexColorV1) -> Result { + let mut colors = Vec::new(); + colors + .try_reserve_exact(1) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + colors.push(root); + Ok(Self { + colors, + edges: Vec::new(), + root: 0, + }) + } + + fn add_member(&mut self, color: VertexColorV1) -> Result { + self.colors + .try_reserve(1) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + let index = self.colors.len(); + self.colors.push(color); + self.add_edge(self.root, index, EdgeRoleV1::ProgramMember)?; + Ok(index) + } + + fn add_edge( + &mut self, + from: usize, + to: usize, + role: EdgeRoleV1, + ) -> Result<(), ProgramCompileError> { + if from >= self.colors.len() || to >= self.colors.len() { + return Err(ProgramCompileError::InternalInvariant); + } + self.edges + .try_reserve(1) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + self.edges.push(EdgeV1 { from, to, role }); + Ok(()) + } + + fn finish(self) -> Result { + let mut degrees = Vec::new(); + degrees + .try_reserve_exact(self.colors.len()) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + degrees.resize(self.colors.len(), 0_usize); + for edge in &self.edges { + degrees[edge.from] = degrees[edge.from] + .checked_add(1) + .ok_or(ProgramCompileError::ResourceExhausted)?; + degrees[edge.to] = degrees[edge.to] + .checked_add(1) + .ok_or(ProgramCompileError::ResourceExhausted)?; + } + + let mut adjacency = Vec::new(); + adjacency + .try_reserve_exact(self.colors.len()) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + for degree in degrees { + let mut arcs = Vec::new(); + arcs.try_reserve_exact(degree) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + adjacency.push(arcs); + } + for edge in &self.edges { + adjacency[edge.from].push(ArcV1 { + direction: 0, + role: edge.role, + neighbour: edge.to, + }); + adjacency[edge.to].push(ArcV1 { + direction: 1, + role: edge.role, + neighbour: edge.from, + }); + } + for arcs in &mut adjacency { + arcs.sort_unstable(); + } + Ok(CanonicalGraphV1 { + colors: self.colors, + adjacency, + edge_count: self.edges.len(), + }) + } +} + +struct IdIndexV1 { + values: Vec<(Key, usize)>, +} + +impl IdIndexV1 +where + Key: Copy + Ord, +{ + fn new() -> Self { + Self { values: Vec::new() } + } + + fn insert(&mut self, key: Key, value: usize) -> Result<(), ProgramCompileError> { + self.values + .try_reserve(1) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + self.values.push((key, value)); + Ok(()) + } + + fn finish(&mut self) -> Result<(), ProgramCompileError> { + self.values.sort_unstable_by_key(|(key, _)| *key); + if self.values.windows(2).any(|pair| pair[0].0 == pair[1].0) { + return Err(ProgramCompileError::InternalInvariant); + } + Ok(()) + } + + fn get(&self, key: Key) -> Result { + let index = self + .values + .binary_search_by_key(&key, |(candidate, _)| *candidate) + .map_err(|_| ProgramCompileError::InternalInvariant)?; + Ok(self.values[index].1) + } +} + +fn program_root_color() -> Result { + let mut color = VertexColorV1::new(vertex_tag::PROGRAM); + // Эти теги связывают адрес с версиями исполняемых законов: схемой Program, + // total-order selection, финальной перепроверкой, атомарным наблюдением, + // encoded Paint emission и формированием modeled LCS. + for release in [ + release_tag::PROGRAM_SCHEMA_V1, + release_tag::DECLARED_TOTAL_ORDER_V1, + release_tag::FRESH_FULL_RECHECK_V1, + release_tag::ATOMIC_OBSERVATION_GROUP_V1, + release_tag::ENCODED_PAINT_EMISSION_V1, + release_tag::MODELED_LCS_OCCURRENCE_V1, + ] { + color.push_u8(release)?; + } + Ok(color) +} + +fn write_signal(color: &mut VertexColorV1, signal: ColorSignal) -> Result<(), ProgramCompileError> { + let profile = match signal.output_profile() { + crate::lcs_occurrence::OutputProfileId::Iec61966Srgb8D65V1 => { + release_tag::IEC_SRGB8_D65_OUTPUT_PROFILE_V1 + } + }; + color.push_u8(profile)?; + color.push_u8(match crate::lcs_occurrence::ADMITTED_SRGB8_TRISTIMULUS_BINDING_V1 + .transform_release() + { + crate::lcs_occurrence::ColorimetricTransformReleaseId::Iec61966Srgb8ToCie1931TwoDegreeXyzD65RelativeY1V1 => { + release_tag::IEC_SRGB8_TO_XYZ_D65_TRANSFORM_V1 + } + })?; + color.push_srgb8(signal.srgb8()) +} + +fn source_color(source: Source) -> Result { + let mut color = VertexColorV1::new(vertex_tag::SOURCE); + write_signal(&mut color, source.signal())?; + Ok(color) +} + +fn candidate_color(candidate: TargetCandidateV1) -> Result { + let mut color = VertexColorV1::new(vertex_tag::CANDIDATE); + write_signal(&mut color, candidate.signal())?; + Ok(color) +} + +fn opacity_color(input: OpacityInput) -> Result { + let admitted = crate::composition::AdmittedOpacityV1::new(input.value()) + .map_err(|_| ProgramCompileError::InternalInvariant)?; + let mut color = VertexColorV1::new(vertex_tag::OPACITY); + color.push_u64(admitted.bits())?; + Ok(color) +} + +fn write_context( + color: &mut VertexColorV1, + context: AppearanceContextId, +) -> Result<(), ProgramCompileError> { + color.push_u8(match context.schema_release() { + crate::lcs_occurrence::AppearanceContextSchemaReleaseId::Ciecam16ViewingInputsV1 => { + release_tag::CIECAM16_VIEWING_INPUTS_V1 + } + })?; + let frame = context.frame(); + color.push_u8(match frame.observer() { + crate::lcs_occurrence::ObserverProfileId::Cie1931TwoDegreeV1 => { + release_tag::CIE1931_TWO_DEGREE_OBSERVER_V1 + } + })?; + color.push_u8(match frame.reference_white() { + crate::lcs_occurrence::ReferenceWhiteId::Iec61966D65ChromaticityV1 => { + release_tag::IEC61966_D65_REFERENCE_WHITE_V1 + } + })?; + color.push_u8(match frame.scale() { + crate::lcs_occurrence::TristimulusScale::RelativeY1 => release_tag::RELATIVE_Y1_SCALE_V1, + })?; + color.push_u8(match frame.release() { + crate::lcs_occurrence::ColorimetricFrameReleaseId::XyzV1 => release_tag::XYZ_FRAME_V1, + #[cfg(test)] + crate::lcs_occurrence::ColorimetricFrameReleaseId::MutationSentinelV1 => { + release_tag::MUTATION_SENTINEL_FRAME_V1 + } + })?; + color.push_u64(context.adapting_luminance_cd_m2().to_bits())?; + color.push_u64(context.background_luminance_ratio().to_bits())?; + color.push_u8(match context.surround_profile() { + crate::lcs_occurrence::SurroundProfileId::AverageV1 => 1, + crate::lcs_occurrence::SurroundProfileId::DimV1 => 2, + crate::lcs_occurrence::SurroundProfileId::DarkV1 => 3, + })?; + Ok(()) +} + +fn occurrence_color(occurrence: Occurrence) -> Result { + let mut color = VertexColorV1::new(vertex_tag::OCCURRENCE); + color.push_u8(match occurrence.composition() { + CompositionProfile::EncodedSrgb8SourceOverV1 => release_tag::ENCODED_SRGB8_SOURCE_OVER_V1, + })?; + write_context(&mut color, occurrence.context())?; + Ok(color) +} + +fn wcag_criterion_tag(criterion: Wcag22CriterionV1) -> u8 { + match criterion { + Wcag22CriterionV1::Sc143TextDefault => 1, + Wcag22CriterionV1::Sc143TextLargeScale => 2, + Wcag22CriterionV1::Sc1411UiComponentOrState => 3, + Wcag22CriterionV1::Sc1411GraphicalObject => 4, + } +} + +fn constraint_color( + mode_tag: u8, + content: ProgramConstraintContentV1, +) -> Result { + let mut color = VertexColorV1::new(mode_tag); + match content { + ProgramConstraintContentV1::ExactSrgb8 { + identity, + release, + capability, + expected, + } => { + color.push_u8(release_tag::EXACT_SRGB8_FAMILY_V1)?; + color.push_u8(match identity { + crate::constraints::ExactConstraintIdentityV1::FinalSrgb8IdentityV1 => { + release_tag::EXACT_SRGB8_IDENTITY_V1 + } + #[cfg(test)] + crate::constraints::ExactConstraintIdentityV1::MutationSentinelV1 => { + release_tag::EXACT_SRGB8_IDENTITY_MUTATION_SENTINEL_V1 + } + })?; + color.push_u8(match release { + crate::constraints::ExactIdentityReleaseV1::V1 => { + release_tag::EXACT_SRGB8_RELEASE_V1 + } + #[cfg(test)] + crate::constraints::ExactIdentityReleaseV1::MutationSentinelV1 => { + release_tag::EXACT_SRGB8_RELEASE_MUTATION_SENTINEL_V1 + } + })?; + color.push_u8(match capability { + crate::constraints::ExactIdentityCapabilityV1::FinalOccurrenceSrgb8IdentityV1 => { + release_tag::EXACT_SRGB8_CAPABILITY_V1 + } + #[cfg(test)] + crate::constraints::ExactIdentityCapabilityV1::MutationSentinelV1 => { + release_tag::EXACT_SRGB8_CAPABILITY_MUTATION_SENTINEL_V1 + } + })?; + color.push_srgb8(expected)?; + } + ProgramConstraintContentV1::Wcag22Srgb8 { + identity, + release, + capability, + criterion, + } => { + color.push_u8(release_tag::WCAG22_SRGB8_FAMILY_V1)?; + color.push_u8(match identity { + crate::constraints::Wcag22Srgb8EvaluatorIdentityV1 => { + release_tag::WCAG22_SRGB8_IDENTITY_V1 + } + })?; + color.push_u8(match release { + crate::wcag22::Wcag22ProfileIdV1::Wcag22Srgb8ContrastV1 => { + release_tag::WCAG22_SRGB8_PROFILE_V1 + } + })?; + color.push_u8(match capability { + crate::constraints::Wcag22Srgb8CapabilityV1 => { + release_tag::WCAG22_SRGB8_CAPABILITY_V1 + } + })?; + color.push_u8(wcag_criterion_tag(criterion))?; + } + #[cfg(test)] + ProgramConstraintContentV1::FinalRecheckMutantExactSrgb8 { expected } => { + for tag in [0xFE_u8, 1, 1, 1] { + color.push_u8(tag)?; + } + color.push_srgb8(expected)?; + } + } + Ok(color) +} + +fn build_graph( + program: &Program, +) -> Result +where + Evaluation: ProgramConstraintEvaluatorSetV1, + ProgramConstraintInvocationOf: Copy, +{ + let mut graph = GraphBuilderV1::new(program_root_color()?)?; + let mut sources = IdIndexV1::new(); + let mut targets = IdIndexV1::new(); + let mut candidates = IdIndexV1::new(); + let mut opacities = IdIndexV1::new(); + let mut paints = IdIndexV1::new(); + let mut ports = IdIndexV1::new(); + let mut surfaces = IdIndexV1::new(); + let mut occurrences = IdIndexV1::new(); + + for source in &program.sources { + sources.insert(source.id(), graph.add_member(source_color(*source)?)?)?; + } + for target in &program.targets { + let target_color = match target.domain() { + TargetDomainV1::Fixed => VertexColorV1::new(vertex_tag::TARGET_FIXED), + TargetDomainV1::Finite(_) => VertexColorV1::new(vertex_tag::TARGET_FINITE), + }; + let target_vertex = graph.add_member(target_color)?; + targets.insert(target.id(), target_vertex)?; + if let TargetDomainV1::Finite(domain) = target.domain() { + for candidate in domain { + let vertex = graph.add_member(candidate_color(*candidate)?)?; + candidates.insert((target.id(), candidate.id()), vertex)?; + } + } + } + for opacity in &program.opacities { + opacities.insert(opacity.id(), graph.add_member(opacity_color(*opacity)?)?)?; + } + for paint in &program.paints { + let (id, tag) = match paint { + Paint::Solid { id, .. } => (*id, vertex_tag::PAINT_SOLID), + Paint::Opacity { id, .. } => (*id, vertex_tag::PAINT_OPACITY), + }; + paints.insert(id, graph.add_member(VertexColorV1::new(tag))?)?; + } + + let group = graph.add_member(VertexColorV1::new(vertex_tag::OBSERVATION_GROUP))?; + for port in &program.observation_group.surface_input_ports { + ports.insert( + *port, + graph.add_member(VertexColorV1::new(vertex_tag::SURFACE_INPUT_PORT))?, + )?; + } + for surface in &program.surfaces { + let (id, tag) = match surface { + Surface::Input { id, .. } => (*id, vertex_tag::SURFACE_INPUT), + Surface::FromOccurrence { id, .. } => (*id, vertex_tag::SURFACE_FROM_OCCURRENCE), + }; + surfaces.insert(id, graph.add_member(VertexColorV1::new(tag))?)?; + } + for occurrence in &program.occurrences { + occurrences.insert( + occurrence.id(), + graph.add_member(occurrence_color(*occurrence)?)?, + )?; + } + + sources.finish()?; + targets.finish()?; + candidates.finish()?; + opacities.finish()?; + paints.finish()?; + ports.finish()?; + surfaces.finish()?; + occurrences.finish()?; + + for target in &program.targets { + let target_vertex = targets.get(target.id())?; + graph.add_edge( + target_vertex, + sources.get(target.source())?, + EdgeRoleV1::TargetSource, + )?; + if let TargetDomainV1::Finite(domain) = target.domain() { + for candidate in domain { + graph.add_edge( + target_vertex, + candidates.get((target.id(), candidate.id()))?, + EdgeRoleV1::TargetCandidate, + )?; + } + } + } + for paint in &program.paints { + match *paint { + Paint::Solid { id, target } => graph.add_edge( + paints.get(id)?, + targets.get(target)?, + EdgeRoleV1::SolidTarget, + )?, + Paint::Opacity { + id, + source, + opacity, + } => { + graph.add_edge( + paints.get(id)?, + paints.get(source)?, + EdgeRoleV1::OpacitySourcePaint, + )?; + graph.add_edge( + paints.get(id)?, + opacities.get(opacity)?, + EdgeRoleV1::OpacityInput, + )?; + } + } + } + for port in &program.observation_group.surface_input_ports { + graph.add_edge(group, ports.get(*port)?, EdgeRoleV1::ObservationGroupPort)?; + } + for surface in &program.surfaces { + match *surface { + Surface::Input { id, input } => graph.add_edge( + surfaces.get(id)?, + ports.get(input)?, + EdgeRoleV1::InputSurfacePort, + )?, + Surface::FromOccurrence { id, occurrence } => graph.add_edge( + surfaces.get(id)?, + occurrences.get(occurrence)?, + EdgeRoleV1::DerivedSurfaceOccurrence, + )?, + } + } + for occurrence in &program.occurrences { + graph.add_edge( + occurrences.get(occurrence.id())?, + paints.get(occurrence.subject())?, + EdgeRoleV1::OccurrenceSubjectPaint, + )?; + graph.add_edge( + occurrences.get(occurrence.id())?, + surfaces.get(occurrence.against())?, + EdgeRoleV1::OccurrenceBackdropSurface, + )?; + } + + for constraint in &program.constraints.hard { + let color = constraint_color( + vertex_tag::CONSTRAINT_HARD, + program.evaluator.constraint_content(constraint.invocation), + )?; + let vertex = graph.add_member(color)?; + graph.add_edge( + vertex, + occurrences.get(constraint.target)?, + EdgeRoleV1::ConstraintOccurrence, + )?; + } + for constraint in &program.constraints.report_only { + let color = constraint_color( + vertex_tag::CONSTRAINT_REPORT_ONLY, + program.evaluator.constraint_content(constraint.invocation), + )?; + let vertex = graph.add_member(color)?; + graph.add_edge( + vertex, + occurrences.get(constraint.target)?, + EdgeRoleV1::ConstraintOccurrence, + )?; + } + for output in &program.outputs { + let vertex = graph.add_member(VertexColorV1::new(vertex_tag::OUTPUT))?; + graph.add_edge(vertex, paints.get(output.paint())?, EdgeRoleV1::OutputPaint)?; + } + + if let Some(selection) = &program.joint_selection { + let selection_vertex = graph.add_member(VertexColorV1::new(vertex_tag::JOINT_SELECTION))?; + for (state_index, state) in selection.states().iter().enumerate() { + let state_index = + u64::try_from(state_index).map_err(|_| ProgramCompileError::ResourceExhausted)?; + let mut state_color = VertexColorV1::new(vertex_tag::JOINT_STATE); + state_color.push_u64(state_index)?; + let state_vertex = graph.add_member(state_color)?; + graph.add_edge(selection_vertex, state_vertex, EdgeRoleV1::SelectionState)?; + for choice in state.choices() { + let choice_vertex = + graph.add_member(VertexColorV1::new(vertex_tag::JOINT_CHOICE))?; + graph.add_edge(state_vertex, choice_vertex, EdgeRoleV1::StateChoice)?; + graph.add_edge( + choice_vertex, + targets.get(choice.target())?, + EdgeRoleV1::ChoiceTarget, + )?; + graph.add_edge( + choice_vertex, + candidates.get((choice.target(), choice.candidate()))?, + EdgeRoleV1::ChoiceCandidate, + )?; + } + } + } + + graph.finish() +} + +struct PartitionV1 { + cells: Vec>, +} + +impl PartitionV1 { + fn initial(graph: &CanonicalGraphV1) -> Result { + let mut order = Vec::new(); + order + .try_reserve_exact(graph.colors.len()) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + order.extend(0..graph.colors.len()); + order.sort_unstable_by(|left, right| { + graph.colors[*left] + .cmp(&graph.colors[*right]) + .then_with(|| left.cmp(right)) + }); + + let mut cells = Vec::new(); + cells + .try_reserve_exact(graph.colors.len()) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + let mut start = 0; + while start < order.len() { + let mut end = start + 1; + while end < order.len() && graph.colors[order[start]] == graph.colors[order[end]] { + end += 1; + } + cells.push(copy_vertices(&order[start..end])?); + start = end; + } + Ok(Self { cells }) + } + + fn is_discrete(&self) -> bool { + self.cells.iter().all(|cell| cell.len() == 1) + } + + fn first_non_singleton(&self) -> Option { + self.cells.iter().position(|cell| cell.len() > 1) + } +} + +fn copy_vertices(source: &[usize]) -> Result, ProgramCompileError> { + let mut copied = Vec::new(); + copied + .try_reserve_exact(source.len()) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + copied.extend_from_slice(source); + Ok(copied) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +struct RefinementAtomV1 { + direction: u8, + role: EdgeRoleV1, + neighbour_cell: usize, +} + +struct RefinementRecordV1 { + vertex: usize, + signature: Vec, +} + +fn refine_partition( + graph: &CanonicalGraphV1, + mut partition: PartitionV1, +) -> Result { + loop { + let mut cell_of = Vec::new(); + cell_of + .try_reserve_exact(graph.colors.len()) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + cell_of.resize(graph.colors.len(), usize::MAX); + for (cell_index, cell) in partition.cells.iter().enumerate() { + for &vertex in cell { + let slot = cell_of + .get_mut(vertex) + .ok_or(ProgramCompileError::InternalInvariant)?; + if *slot != usize::MAX { + return Err(ProgramCompileError::InternalInvariant); + } + *slot = cell_index; + } + } + if cell_of.contains(&usize::MAX) { + return Err(ProgramCompileError::InternalInvariant); + } + + let mut next_cells = Vec::new(); + next_cells + .try_reserve_exact(graph.colors.len()) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + for cell in &partition.cells { + let mut records = Vec::new(); + records + .try_reserve_exact(cell.len()) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + for &vertex in cell { + let arcs = graph + .adjacency + .get(vertex) + .ok_or(ProgramCompileError::InternalInvariant)?; + let mut signature = Vec::new(); + signature + .try_reserve_exact(arcs.len()) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + for arc in arcs { + signature.push(RefinementAtomV1 { + direction: arc.direction, + role: arc.role, + neighbour_cell: cell_of[arc.neighbour], + }); + } + signature.sort_unstable(); + records.push(RefinementRecordV1 { vertex, signature }); + } + records.sort_unstable_by(|left, right| left.signature.cmp(&right.signature)); + + let mut start = 0; + while start < records.len() { + let mut end = start + 1; + while end < records.len() && records[start].signature == records[end].signature { + end += 1; + } + let mut split = Vec::new(); + split + .try_reserve_exact(end - start) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + split.extend(records[start..end].iter().map(|record| record.vertex)); + next_cells.push(split); + start = end; + } + } + + if next_cells.len() == partition.cells.len() { + partition.cells = next_cells; + return Ok(partition); + } + partition.cells = next_cells; + } +} + +fn individualize( + partition: &PartitionV1, + cell_index: usize, + vertex: usize, +) -> Result { + let selected = partition + .cells + .get(cell_index) + .ok_or(ProgramCompileError::InternalInvariant)?; + if selected.len() < 2 || !selected.contains(&vertex) { + return Err(ProgramCompileError::InternalInvariant); + } + + let mut cells = Vec::new(); + cells + .try_reserve_exact( + partition + .cells + .len() + .checked_add(1) + .ok_or(ProgramCompileError::ResourceExhausted)?, + ) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + for (index, cell) in partition.cells.iter().enumerate() { + if index != cell_index { + cells.push(copy_vertices(cell)?); + continue; + } + let mut singleton = Vec::new(); + singleton + .try_reserve_exact(1) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + singleton.push(vertex); + cells.push(singleton); + + let mut remainder = Vec::new(); + remainder + .try_reserve_exact(cell.len() - 1) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + remainder.extend( + cell.iter() + .copied() + .filter(|candidate| *candidate != vertex), + ); + cells.push(remainder); + } + Ok(PartitionV1 { cells }) +} + +struct SearchFrameV1 { + partition: PartitionV1, + branch_cell: Option, + candidates: Vec, + explored_candidates: Vec, + next_candidate: usize, + leaf_pending: bool, +} + +impl SearchFrameV1 { + fn new(partition: PartitionV1) -> Result { + let branch_cell = partition.first_non_singleton(); + let candidates = match branch_cell { + Some(index) => copy_vertices(&partition.cells[index])?, + None => Vec::new(), + }; + Ok(Self { + leaf_pending: branch_cell.is_none(), + partition, + branch_cell, + candidates, + explored_candidates: Vec::new(), + next_candidate: 0, + }) + } +} + +fn push_u64_bytes(output: &mut Vec, value: u64) { + output.extend_from_slice(&value.to_be_bytes()); +} + +fn usize_as_u64(value: usize) -> Result { + u64::try_from(value).map_err(|_| ProgramCompileError::ResourceExhausted) +} + +struct SerializedLeafV1 { + preimage: Vec, + order: Vec, +} + +fn serialize_leaf( + graph: &CanonicalGraphV1, + partition: &PartitionV1, +) -> Result { + if !partition.is_discrete() || partition.cells.len() != graph.colors.len() { + return Err(ProgramCompileError::InternalInvariant); + } + let mut order = Vec::new(); + order + .try_reserve_exact(graph.colors.len()) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + let mut label_of = Vec::new(); + label_of + .try_reserve_exact(graph.colors.len()) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + label_of.resize(graph.colors.len(), usize::MAX); + for (label, cell) in partition.cells.iter().enumerate() { + let [vertex] = cell.as_slice() else { + return Err(ProgramCompileError::InternalInvariant); + }; + label_of[*vertex] = label; + order.push(*vertex); + } + + let edge_bytes = graph + .edge_count + .checked_mul(9) + .ok_or(ProgramCompileError::ResourceExhausted)?; + let color_bytes = graph.colors.iter().try_fold(0_usize, |total, color| { + total + .checked_add(16) + .and_then(|value| value.checked_add(color.as_slice().len())) + .ok_or(ProgramCompileError::ResourceExhausted) + })?; + let capacity = DOMAIN_V1 + .len() + .checked_add(16) + .and_then(|value| value.checked_add(color_bytes)) + .and_then(|value| value.checked_add(edge_bytes)) + .ok_or(ProgramCompileError::ResourceExhausted)?; + let mut output = Vec::new(); + output + .try_reserve_exact(capacity) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + output.extend_from_slice(DOMAIN_V1); + push_u64_bytes(&mut output, usize_as_u64(graph.colors.len())?); + push_u64_bytes(&mut output, usize_as_u64(graph.edge_count)?); + + for cell in &partition.cells { + let vertex = cell[0]; + let color = graph.colors[vertex]; + push_u64_bytes(&mut output, u64::from(color.len)); + output.extend_from_slice(color.as_slice()); + + let outgoing_count = graph.adjacency[vertex] + .iter() + .filter(|arc| arc.direction == 0) + .count(); + push_u64_bytes(&mut output, usize_as_u64(outgoing_count)?); + let mut outgoing = Vec::new(); + outgoing + .try_reserve_exact(outgoing_count) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + outgoing.extend( + graph.adjacency[vertex] + .iter() + .filter(|arc| arc.direction == 0) + .map(|arc| (arc.role, label_of[arc.neighbour])), + ); + outgoing.sort_unstable(); + for (role, target) in outgoing { + output.push(role as u8); + push_u64_bytes(&mut output, usize_as_u64(target)?); + } + } + Ok(SerializedLeafV1 { + preimage: output, + order, + }) +} + +fn equal_leaf_automorphism( + canonical_order: &[usize], + equal_order: &[usize], +) -> Result, ProgramCompileError> { + if canonical_order.len() != equal_order.len() { + return Err(ProgramCompileError::InternalInvariant); + } + let mut permutation = Vec::new(); + permutation + .try_reserve_exact(canonical_order.len()) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + permutation.resize(canonical_order.len(), usize::MAX); + for (&from, &to) in canonical_order.iter().zip(equal_order) { + let slot = permutation + .get_mut(from) + .ok_or(ProgramCompileError::InternalInvariant)?; + if *slot != usize::MAX || to >= canonical_order.len() { + return Err(ProgramCompileError::InternalInvariant); + } + *slot = to; + } + if permutation.contains(&usize::MAX) { + return Err(ProgramCompileError::InternalInvariant); + } + Ok(permutation) +} + +fn automorphism_preserves_partition( + permutation: &[usize], + cell_of: &[usize], +) -> Result { + if permutation.len() != cell_of.len() { + return Err(ProgramCompileError::InternalInvariant); + } + for (vertex, &image) in permutation.iter().enumerate() { + let image_cell = cell_of + .get(image) + .ok_or(ProgramCompileError::InternalInvariant)?; + if cell_of[vertex] != *image_cell { + return Ok(false); + } + } + Ok(true) +} + +/// Проверяет, лежит ли `candidate` в орбите уже исследованной ветви при +/// автоморфизмах, стабилизирующих текущее упорядоченное разбиение. Отсечение +/// точное: отображение сохраняется лишь после совпадения полных сериализаций +/// листьев, доказывающего автоморфизм графа. +fn candidate_is_in_explored_orbit( + partition: &PartitionV1, + explored: &[usize], + candidate: usize, + automorphisms: &[Vec], +) -> Result { + if explored.is_empty() || automorphisms.is_empty() { + return Ok(false); + } + let vertex_count = automorphisms[0].len(); + let mut cell_of = Vec::new(); + cell_of + .try_reserve_exact(vertex_count) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + cell_of.resize(vertex_count, usize::MAX); + for (cell_index, cell) in partition.cells.iter().enumerate() { + for &vertex in cell { + let slot = cell_of + .get_mut(vertex) + .ok_or(ProgramCompileError::InternalInvariant)?; + if *slot != usize::MAX { + return Err(ProgramCompileError::InternalInvariant); + } + *slot = cell_index; + } + } + if cell_of.contains(&usize::MAX) || candidate >= vertex_count { + return Err(ProgramCompileError::InternalInvariant); + } + + let mut stabilizers = Vec::new(); + stabilizers + .try_reserve_exact(automorphisms.len()) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + for (index, permutation) in automorphisms.iter().enumerate() { + if automorphism_preserves_partition(permutation, &cell_of)? { + stabilizers.push(index); + } + } + if stabilizers.is_empty() { + return Ok(false); + } + + let mut seen = Vec::new(); + seen.try_reserve_exact(vertex_count) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + seen.resize(vertex_count, false); + let mut queue = Vec::new(); + queue + .try_reserve_exact(vertex_count) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + for &representative in explored { + let slot = seen + .get_mut(representative) + .ok_or(ProgramCompileError::InternalInvariant)?; + if !*slot { + *slot = true; + queue.push(representative); + } + } + let mut cursor = 0; + while cursor < queue.len() { + let vertex = queue[cursor]; + cursor += 1; + for &index in &stabilizers { + let image = automorphisms[index][vertex]; + let slot = seen + .get_mut(image) + .ok_or(ProgramCompileError::InternalInvariant)?; + if !*slot { + *slot = true; + queue.push(image); + } + } + } + Ok(seen[candidate]) +} + +/// Для одноцветных вершин с одинаковыми полными списками инцидентности +/// транспозиция сохраняет все типизированные рёбра. Это точное дешёвое +/// отсечение обрабатывает повторы до выделения общей перестановки. +fn candidate_is_exact_twin( + graph: &CanonicalGraphV1, + explored: &[usize], + candidate: usize, +) -> Result { + let candidate_color = graph + .colors + .get(candidate) + .ok_or(ProgramCompileError::InternalInvariant)?; + let candidate_arcs = graph + .adjacency + .get(candidate) + .ok_or(ProgramCompileError::InternalInvariant)?; + for &representative in explored { + if graph + .colors + .get(representative) + .ok_or(ProgramCompileError::InternalInvariant)? + == candidate_color + && graph + .adjacency + .get(representative) + .ok_or(ProgramCompileError::InternalInvariant)? + == candidate_arcs + { + return Ok(true); + } + } + Ok(false) +} + +fn canonical_search_impl( + graph: &CanonicalGraphV1, + mut remaining_branch_expansions: Option, +) -> Result<(Vec, usize), ProgramCompileError> { + let initial = refine_partition(graph, PartitionV1::initial(graph)?)?; + let mut stack = Vec::new(); + stack + .try_reserve_exact(graph.colors.len()) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + stack.push(SearchFrameV1::new(initial)?); + let mut best: Option = None; + let mut automorphisms: Vec> = Vec::new(); + let mut leaf_count = 0_usize; + + while !stack.is_empty() { + let leaf = stack + .last() + .map(|frame| frame.leaf_pending) + .ok_or(ProgramCompileError::InternalInvariant)?; + if leaf { + let candidate = { + let frame = stack + .last_mut() + .ok_or(ProgramCompileError::InternalInvariant)?; + frame.leaf_pending = false; + serialize_leaf(graph, &frame.partition)? + }; + stack.pop(); + // Диагностический счётчик не участвует в admission: насыщение не + // влияет на выбранный прообраз даже у недостижимо большого дерева. + leaf_count = leaf_count.saturating_add(1); + match &best { + None => best = Some(candidate), + Some(current) if candidate.preimage < current.preimage => best = Some(candidate), + Some(current) if candidate.preimage == current.preimage => { + let permutation = equal_leaf_automorphism(¤t.order, &candidate.order)?; + // Отсечение не требуется для корректности. Храним не более + // V доказанных автоморфизмов: при длине V это ограничивает + // память O(V²), а остальные ветви исследуются полностью. + if permutation.iter().enumerate().any(|(from, to)| from != *to) + && automorphisms.len() < graph.colors.len() + && !automorphisms.contains(&permutation) + { + automorphisms + .try_reserve(1) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + automorphisms.push(permutation); + } + } + Some(_) => {} + } + continue; + } + + let child = { + let frame = stack + .last_mut() + .ok_or(ProgramCompileError::InternalInvariant)?; + let mut selected = None; + while frame.next_candidate < frame.candidates.len() { + let candidate = frame.candidates[frame.next_candidate]; + frame.next_candidate += 1; + if candidate_is_exact_twin(graph, &frame.explored_candidates, candidate)? + || candidate_is_in_explored_orbit( + &frame.partition, + &frame.explored_candidates, + candidate, + &automorphisms, + )? + { + continue; + } + frame + .explored_candidates + .try_reserve(1) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + frame.explored_candidates.push(candidate); + selected = Some(candidate); + break; + } + match selected { + Some(candidate) => { + if let Some(remaining) = &mut remaining_branch_expansions { + *remaining = remaining + .checked_sub(1) + .ok_or(ProgramCompileError::ResourceExhausted)?; + } + let cell = frame + .branch_cell + .ok_or(ProgramCompileError::InternalInvariant)?; + Some(refine_partition( + graph, + individualize(&frame.partition, cell, candidate)?, + )?) + } + None => None, + } + }; + match child { + Some(partition) => { + stack + .try_reserve(1) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + stack.push(SearchFrameV1::new(partition)?); + } + None => { + stack.pop(); + } + } + } + let best = best.ok_or(ProgramCompileError::InternalInvariant)?; + Ok((best.preimage, leaf_count)) +} + +fn canonical_search(graph: &CanonicalGraphV1) -> Result<(Vec, usize), ProgramCompileError> { + // Число шагов поиска не инвариантно к изоморфизму: после opaque- + // переименования автоморфизм может обнаружиться другой ветвью. Поэтому + // динамический лимит сделал бы допуск зависимым от client-owned ID. Точный + // поиск возвращает только полный прообраз либо типизированный отказ. + canonical_search_impl(graph, None) +} + +#[cfg(test)] +fn canonical_search_with_test_fuel( + graph: &CanonicalGraphV1, + test_fuel: usize, +) -> Result<(Vec, usize), ProgramCompileError> { + // Только тестовая инъекция отказа для проверки атомарности. Это не политика + // допуска: история поиска не инвариантна к alpha-переименованию. + canonical_search_impl(graph, Some(test_fuel)) +} + +fn canonical_preimage(graph: &CanonicalGraphV1) -> Result, ProgramCompileError> { + canonical_search(graph).map(|(preimage, _)| preimage) +} + +pub(super) fn compile_program_content_identity_v1( + program: &Program, +) -> Result +where + Evaluation: ProgramConstraintEvaluatorSetV1, + ProgramConstraintInvocationOf: Copy, +{ + let graph = build_graph(program)?; + let preimage = canonical_preimage(&graph)?; + let digest = crate::sha256::digest(&preimage); + Ok(ProgramContentIdentityV1(*digest.as_bytes())) +} + +#[cfg(test)] +mod tests { + use super::*; + use proptest::prelude::*; + + #[test] + fn evaluator_descriptor_and_certificate_metadata_have_one_source_of_truth() { + let evaluator = crate::constraints::ExactSrgb8IdentityV1; + let expected = Srgb8::new([0x12, 0x34, 0x56]); + let content = evaluator.program_constraint_content_v1(expected); + let ProgramConstraintContentV1::ExactSrgb8 { + identity, + release, + capability, + expected: described_expected, + } = content + else { + panic!("exact evaluator must describe its own exact invocation"); + }; + assert_eq!( + identity, + >::identity( + &evaluator + ) + ); + assert_eq!( + release, + >::release( + &evaluator + ) + ); + assert_eq!( + capability, + >::capability(&evaluator) + ); + assert_eq!(described_expected, expected); + + let baseline = constraint_color(vertex_tag::CONSTRAINT_HARD, content).unwrap(); + for mutant in [ + ProgramConstraintContentV1::ExactSrgb8 { + identity: crate::constraints::ExactConstraintIdentityV1::MutationSentinelV1, + release, + capability, + expected, + }, + ProgramConstraintContentV1::ExactSrgb8 { + identity, + release: crate::constraints::ExactIdentityReleaseV1::MutationSentinelV1, + capability, + expected, + }, + ProgramConstraintContentV1::ExactSrgb8 { + identity, + release, + capability: crate::constraints::ExactIdentityCapabilityV1::MutationSentinelV1, + expected, + }, + ] { + assert_ne!( + constraint_color(vertex_tag::CONSTRAINT_HARD, mutant).unwrap(), + baseline + ); + } + } + + fn context_color(context: AppearanceContextId) -> VertexColorV1 { + let mut color = VertexColorV1::new(vertex_tag::OCCURRENCE); + write_context(&mut color, context).unwrap(); + color + } + + #[test] + fn every_appearance_context_coordinate_and_frame_release_is_content_bound() { + use crate::lcs_occurrence::{ + AdaptingLuminanceCdM2, AppearanceContextSchemaReleaseId, BackgroundLuminanceRatio, + IEC_SRGB_D65_XYZ_FRAME_V1, MUTATION_SENTINEL_XYZ_FRAME_V1, SurroundProfileId, + }; + + let make = |frame, adapting_luminance, background_ratio, surround| { + AppearanceContextId::from_inputs( + AppearanceContextSchemaReleaseId::Ciecam16ViewingInputsV1, + frame, + AdaptingLuminanceCdM2::try_new(adapting_luminance).unwrap(), + BackgroundLuminanceRatio::try_new(background_ratio).unwrap(), + surround, + ) + }; + let baseline = context_color(make( + IEC_SRGB_D65_XYZ_FRAME_V1, + 64.0, + 0.2, + SurroundProfileId::AverageV1, + )); + for mutant in [ + make( + IEC_SRGB_D65_XYZ_FRAME_V1, + 32.0, + 0.2, + SurroundProfileId::AverageV1, + ), + make( + IEC_SRGB_D65_XYZ_FRAME_V1, + 64.0, + 0.1, + SurroundProfileId::AverageV1, + ), + make( + IEC_SRGB_D65_XYZ_FRAME_V1, + 64.0, + 0.2, + SurroundProfileId::DimV1, + ), + make( + MUTATION_SENTINEL_XYZ_FRAME_V1, + 64.0, + 0.2, + SurroundProfileId::AverageV1, + ), + ] { + assert_ne!(context_color(mutant), baseline); + } + } + + #[test] + fn every_wcag_criterion_has_distinct_constraint_content() { + let evaluator = crate::constraints::Wcag22Srgb8V1; + let mut colors = Vec::new(); + for criterion in [ + Wcag22CriterionV1::Sc143TextDefault, + Wcag22CriterionV1::Sc143TextLargeScale, + Wcag22CriterionV1::Sc1411UiComponentOrState, + Wcag22CriterionV1::Sc1411GraphicalObject, + ] { + colors.push( + constraint_color( + vertex_tag::CONSTRAINT_HARD, + evaluator.program_constraint_content_v1(criterion), + ) + .unwrap(), + ); + } + colors.sort_unstable(); + colors.dedup(); + assert_eq!(colors.len(), 4); + } + + fn mapping_preserves_graph( + left: &CanonicalGraphV1, + right: &CanonicalGraphV1, + mapping: &[usize], + ) -> bool { + if left.colors.len() != right.colors.len() || left.edge_count != right.edge_count { + return false; + } + for (vertex, &image) in mapping.iter().enumerate() { + if left.colors[vertex] != right.colors[image] { + return false; + } + let mut left_arcs = left.adjacency[vertex] + .iter() + .map(|arc| (arc.direction, arc.role, mapping[arc.neighbour])) + .collect::>(); + let mut right_arcs = right.adjacency[image] + .iter() + .map(|arc| (arc.direction, arc.role, arc.neighbour)) + .collect::>(); + left_arcs.sort_unstable(); + right_arcs.sort_unstable(); + if left_arcs != right_arcs { + return false; + } + } + true + } + + fn visit_mappings( + left: &CanonicalGraphV1, + right: &CanonicalGraphV1, + mapping: &mut [usize], + cursor: usize, + ) -> bool { + if cursor == mapping.len() { + return mapping_preserves_graph(left, right, mapping); + } + for candidate in cursor..mapping.len() { + mapping.swap(cursor, candidate); + let color_matches = left.colors[cursor] == right.colors[mapping[cursor]]; + if color_matches && visit_mappings(left, right, mapping, cursor + 1) { + mapping.swap(cursor, candidate); + return true; + } + mapping.swap(cursor, candidate); + } + false + } + + fn brute_force_isomorphic(left: &CanonicalGraphV1, right: &CanonicalGraphV1) -> bool { + if left.colors.len() != right.colors.len() { + return false; + } + let mut mapping = (0..left.colors.len()).collect::>(); + visit_mappings(left, right, &mut mapping, 0) + } + + fn tiny_bipartite_graph(edge_mask: u8) -> CanonicalGraphV1 { + let mut graph = GraphBuilderV1::new(VertexColorV1::new(vertex_tag::PROGRAM)).unwrap(); + let sources = [(); 2].map(|()| { + graph + .add_member(VertexColorV1::new(vertex_tag::SOURCE)) + .unwrap() + }); + let targets = [(); 2].map(|()| { + graph + .add_member(VertexColorV1::new(vertex_tag::TARGET_FIXED)) + .unwrap() + }); + for (bit, (target, source)) in [ + (targets[0], sources[0]), + (targets[0], sources[1]), + (targets[1], sources[0]), + (targets[1], sources[1]), + ] + .into_iter() + .enumerate() + { + if edge_mask & (1 << bit) != 0 { + graph + .add_edge(target, source, EdgeRoleV1::TargetSource) + .unwrap(); + } + } + graph.finish().unwrap() + } + + #[test] + fn canonicalizer_matches_an_independent_tiny_isomorphism_oracle() { + let graphs = (0..16).map(tiny_bipartite_graph).collect::>(); + let preimages = graphs + .iter() + .map(|graph| canonical_preimage(graph).unwrap()) + .collect::>(); + + for left in 0..graphs.len() { + for right in 0..graphs.len() { + assert_eq!( + preimages[left] == preimages[right], + brute_force_isomorphic(&graphs[left], &graphs[right]), + "tiny bipartite masks {left:#06b} and {right:#06b}" + ); + } + } + } + + #[test] + fn exact_automorphism_pruning_prevents_factorial_symmetric_search() { + const SYMMETRIC_VERTICES: usize = 12; + let mut graph = GraphBuilderV1::new(VertexColorV1::new(vertex_tag::PROGRAM)).unwrap(); + for _ in 0..SYMMETRIC_VERTICES { + graph + .add_member(VertexColorV1::new(vertex_tag::SOURCE)) + .unwrap(); + } + let graph = graph.finish().unwrap(); + + let (_, leaves) = canonical_search(&graph).unwrap(); + + assert_eq!(leaves, 1, "exact twin pruning visited extra leaves"); + } + + #[test] + fn exhausted_fault_injection_fuel_never_returns_a_partial_preimage() { + let mut graph = GraphBuilderV1::new(VertexColorV1::new(vertex_tag::PROGRAM)).unwrap(); + graph + .add_member(VertexColorV1::new(vertex_tag::SOURCE)) + .unwrap(); + graph + .add_member(VertexColorV1::new(vertex_tag::SOURCE)) + .unwrap(); + let graph = graph.finish().unwrap(); + + assert_eq!( + canonical_search_with_test_fuel(&graph, 0).unwrap_err(), + ProgramCompileError::ResourceExhausted + ); + } + + fn relabelled_budget_graph(permutation: [usize; 9]) -> CanonicalGraphV1 { + const EDGES: [(usize, usize); 16] = [ + (1, 2), + (2, 1), + (3, 4), + (8, 1), + (4, 3), + (1, 8), + (6, 4), + (6, 7), + (7, 6), + (5, 6), + (5, 3), + (8, 2), + (7, 5), + (4, 7), + (3, 5), + (2, 8), + ]; + + let mut graph = GraphBuilderV1::new(VertexColorV1::new(vertex_tag::PROGRAM)).unwrap(); + for _ in 1..permutation.len() { + graph + .add_member(VertexColorV1::new(vertex_tag::SOURCE)) + .unwrap(); + } + for (from, to) in EDGES { + graph + .add_edge(permutation[from], permutation[to], EdgeRoleV1::TargetSource) + .unwrap(); + } + graph.finish().unwrap() + } + + fn permutation_from_keys(keys: [u64; N]) -> Vec { + let mut images = (0..N).collect::>(); + images.sort_unstable_by_key(|index| (keys[*index], *index)); + let mut permutation = vec![0]; + permutation.extend(images.into_iter().map(|index| index + 1)); + permutation + } + + fn small_directed_graph(edge_mask: u16, permutation: &[usize]) -> CanonicalGraphV1 { + let mut graph = GraphBuilderV1::new(VertexColorV1::new(vertex_tag::PROGRAM)).unwrap(); + for _ in 1..permutation.len() { + graph + .add_member(VertexColorV1::new(vertex_tag::SOURCE)) + .unwrap(); + } + let mut bit = 0; + for from in 1..permutation.len() { + for to in 1..permutation.len() { + if from != to && edge_mask & (1 << bit) != 0 { + graph + .add_edge(permutation[from], permutation[to], EdgeRoleV1::TargetSource) + .unwrap(); + } + if from != to { + bit += 1; + } + } + } + graph.finish().unwrap() + } + + #[test] + fn exact_preimage_is_invariant_under_opaque_relabelling() { + let canonical = relabelled_budget_graph([0, 1, 2, 3, 4, 5, 6, 7, 8]); + let renamed = relabelled_budget_graph([0, 1, 2, 6, 3, 5, 4, 7, 8]); + + let (canonical, _) = canonical_search(&canonical).unwrap(); + let (renamed, _) = canonical_search(&renamed).unwrap(); + + assert_eq!(canonical, renamed); + } + + proptest! { + #![proptest_config(ProptestConfig::with_cases(128))] + + #[test] + fn hostile_graph_preimage_is_invariant_for_generated_bijections( + keys in proptest::array::uniform8(any::()), + ) { + let permutation = permutation_from_keys(keys); + let permutation: [usize; 9] = permutation.try_into().unwrap(); + let baseline = relabelled_budget_graph([0, 1, 2, 3, 4, 5, 6, 7, 8]); + let renamed = relabelled_budget_graph(permutation); + + prop_assert_eq!( + canonical_search(&baseline).unwrap().0, + canonical_search(&renamed).unwrap().0, + ); + } + + #[test] + fn small_role_directed_graph_preimage_is_invariant_for_generated_bijections( + edge_mask in 0_u16..=0x0fff, + keys in proptest::array::uniform4(any::()), + ) { + let baseline = small_directed_graph(edge_mask, &[0, 1, 2, 3, 4]); + let renamed = small_directed_graph(edge_mask, &permutation_from_keys(keys)); + + prop_assert_eq!( + canonical_search(&baseline).unwrap().0, + canonical_search(&renamed).unwrap().0, + ); + } + } +} diff --git a/crates/labcolors-core/src/program_identity_tests.rs b/crates/labcolors-core/src/program_identity_tests.rs new file mode 100644 index 00000000..218c0b5e --- /dev/null +++ b/crates/labcolors-core/src/program_identity_tests.rs @@ -0,0 +1,1189 @@ +use crate::Srgb8; +use crate::appearance::{OccurrenceId, OpacityInputId, PaintId, SurfaceId, SurfaceInputPortId}; +use crate::lcs_occurrence::{ + AdaptingLuminanceCdM2, AppearanceContextId, AppearanceContextSchemaReleaseId, + BackgroundLuminanceRatio, ColorSignal, IEC_SRGB_D65_XYZ_FRAME_V1, SurroundProfileId, +}; +use crate::observation::ObservationGroupId; +use crate::program_session::{ + CompositionProfile, ConstraintId, ConstraintInvocation, ConstraintSet, + CoreProgramConstraintInvocationV1, CoreProgramEvaluatorsV1, CoreProgramV1, + DeclaredJointSelectionV1, JointCandidateStateV1, ObservationGroup, Occurrence, OpacityInput, + OutputBinding, OutputSlotId, Paint, Program, Source, SourceId, Surface, Target, + TargetCandidateChoiceV1, TargetCandidateId, TargetCandidateV1, TargetId, +}; +use crate::wcag22::Wcag22CriterionV1; + +fn signal(value: [u8; 3]) -> ColorSignal { + ColorSignal::from_srgb8(Srgb8::new(value)) +} + +fn context(surround: SurroundProfileId) -> AppearanceContextId { + AppearanceContextId::from_inputs( + AppearanceContextSchemaReleaseId::Ciecam16ViewingInputsV1, + IEC_SRGB_D65_XYZ_FRAME_V1, + AdaptingLuminanceCdM2::try_new(64.0).unwrap(), + BackgroundLuminanceRatio::try_new(0.2).unwrap(), + surround, + ) +} + +#[derive(Clone, Copy)] +struct FixedIds { + sources: [SourceId; 2], + targets: [TargetId; 2], + paints: [PaintId; 2], + ports: [SurfaceInputPortId; 2], + surfaces: [SurfaceId; 2], + occurrences: [OccurrenceId; 2], + constraints: [ConstraintId; 2], + outputs: [OutputSlotId; 2], + group: ObservationGroupId, +} + +#[derive(Clone, Copy)] +enum FixedMutation { + None, + SourceSignal, + TargetSource, +} + +fn fixed_program( + ids: FixedIds, + reverse_declarations: bool, + second_signal: Srgb8, + mutation: FixedMutation, +) -> CoreProgramV1 { + let mut sources = vec![ + Source::new(ids.sources[0], signal([0x10, 0x20, 0x30])), + Source::new( + ids.sources[1], + ColorSignal::from_srgb8(if matches!(mutation, FixedMutation::SourceSignal) { + Srgb8::new([0x41, 0x50, 0x60]) + } else { + second_signal + }), + ), + ]; + let mut targets = vec![ + Target::fixed(ids.targets[0], ids.sources[0]), + Target::fixed( + ids.targets[1], + if matches!(mutation, FixedMutation::TargetSource) { + ids.sources[0] + } else { + ids.sources[1] + }, + ), + ]; + let mut paints = vec![ + Paint::Solid { + id: ids.paints[0], + target: ids.targets[0], + }, + Paint::Solid { + id: ids.paints[1], + target: ids.targets[1], + }, + ]; + let mut surfaces = vec![ + Surface::Input { + id: ids.surfaces[0], + input: ids.ports[0], + }, + Surface::Input { + id: ids.surfaces[1], + input: ids.ports[1], + }, + ]; + let mut occurrences = vec![ + Occurrence::new( + ids.occurrences[0], + ids.paints[0], + ids.surfaces[0], + CompositionProfile::EncodedSrgb8SourceOverV1, + context(SurroundProfileId::AverageV1), + ), + Occurrence::new( + ids.occurrences[1], + ids.paints[1], + ids.surfaces[1], + CompositionProfile::EncodedSrgb8SourceOverV1, + context(SurroundProfileId::DimV1), + ), + ]; + let mut hard = vec![ + ConstraintInvocation::hard( + ids.constraints[0], + ids.occurrences[0], + CoreProgramConstraintInvocationV1::ExactSrgb8(Srgb8::new([0x10, 0x20, 0x30])), + ), + ConstraintInvocation::hard( + ids.constraints[1], + ids.occurrences[1], + CoreProgramConstraintInvocationV1::ExactSrgb8(second_signal), + ), + ]; + let mut outputs = vec![ + OutputBinding::new(ids.outputs[0], ids.paints[0]), + OutputBinding::new(ids.outputs[1], ids.paints[1]), + ]; + let mut ports = ids.ports.to_vec(); + + if reverse_declarations { + sources.reverse(); + targets.reverse(); + paints.reverse(); + surfaces.reverse(); + occurrences.reverse(); + hard.reverse(); + outputs.reverse(); + ports.reverse(); + } + + Program::new( + sources, + targets, + ObservationGroup::new(ids.group, ports), + vec![], + paints, + surfaces, + occurrences, + ConstraintSet::new(hard, vec![]), + outputs, + CoreProgramEvaluatorsV1, + ) +} + +#[test] +fn fixed_graph_identity_ignores_opaque_names_and_unordered_declaration_order() { + let canonical = FixedIds { + sources: [SourceId::new(10), SourceId::new(20)], + targets: [TargetId::new(30), TargetId::new(40)], + paints: [PaintId::new(50), PaintId::new(60)], + ports: [SurfaceInputPortId::new(70), SurfaceInputPortId::new(80)], + surfaces: [SurfaceId::new(90), SurfaceId::new(100)], + occurrences: [OccurrenceId::new(110), OccurrenceId::new(120)], + constraints: [ConstraintId::new(130), ConstraintId::new(140)], + outputs: [OutputSlotId::new(150), OutputSlotId::new(160)], + group: ObservationGroupId::new(170), + }; + let renamed = FixedIds { + sources: [SourceId::new(902), SourceId::new(101)], + targets: [TargetId::new(804), TargetId::new(203)], + paints: [PaintId::new(706), PaintId::new(305)], + ports: [SurfaceInputPortId::new(608), SurfaceInputPortId::new(407)], + surfaces: [SurfaceId::new(510), SurfaceId::new(409)], + occurrences: [OccurrenceId::new(312), OccurrenceId::new(211)], + constraints: [ConstraintId::new(114), ConstraintId::new(913)], + outputs: [OutputSlotId::new(816), OutputSlotId::new(715)], + group: ObservationGroupId::new(617), + }; + + let canonical = fixed_program( + canonical, + false, + Srgb8::new([0x40, 0x50, 0x60]), + FixedMutation::None, + ) + .compile() + .unwrap(); + let renamed = fixed_program( + renamed, + true, + Srgb8::new([0x40, 0x50, 0x60]), + FixedMutation::None, + ) + .compile() + .unwrap(); + + assert_eq!(canonical.content_identity(), renamed.content_identity()); +} + +#[test] +fn canonical_v1_digest_is_cross_platform_golden() { + let ids = FixedIds { + sources: [SourceId::new(10), SourceId::new(20)], + targets: [TargetId::new(30), TargetId::new(40)], + paints: [PaintId::new(50), PaintId::new(60)], + ports: [SurfaceInputPortId::new(70), SurfaceInputPortId::new(80)], + surfaces: [SurfaceId::new(90), SurfaceId::new(100)], + occurrences: [OccurrenceId::new(110), OccurrenceId::new(120)], + constraints: [ConstraintId::new(130), ConstraintId::new(140)], + outputs: [OutputSlotId::new(150), OutputSlotId::new(160)], + group: ObservationGroupId::new(170), + }; + let compiled = fixed_program( + ids, + false, + Srgb8::new([0x40, 0x50, 0x60]), + FixedMutation::None, + ) + .compile() + .unwrap(); + + assert_eq!( + compiled.content_identity().as_bytes(), + &[ + 168, 248, 71, 93, 18, 147, 245, 229, 235, 83, 237, 210, 202, 114, 6, 19, 16, 224, 85, + 63, 190, 86, 219, 66, 99, 226, 22, 200, 208, 224, 149, 196, + ] + ); +} + +#[test] +fn source_signal_and_target_source_edge_are_independently_content_bound() { + let ids = FixedIds { + sources: [SourceId::new(10), SourceId::new(20)], + targets: [TargetId::new(30), TargetId::new(40)], + paints: [PaintId::new(50), PaintId::new(60)], + ports: [SurfaceInputPortId::new(70), SurfaceInputPortId::new(80)], + surfaces: [SurfaceId::new(90), SurfaceId::new(100)], + occurrences: [OccurrenceId::new(110), OccurrenceId::new(120)], + constraints: [ConstraintId::new(130), ConstraintId::new(140)], + outputs: [OutputSlotId::new(150), OutputSlotId::new(160)], + group: ObservationGroupId::new(170), + }; + let baseline = fixed_program( + ids, + false, + Srgb8::new([0x40, 0x50, 0x60]), + FixedMutation::None, + ) + .compile() + .unwrap() + .content_identity(); + let changed_signal = fixed_program( + ids, + false, + Srgb8::new([0x40, 0x50, 0x60]), + FixedMutation::SourceSignal, + ) + .compile() + .unwrap() + .content_identity(); + let changed_target_source = fixed_program( + ids, + false, + Srgb8::new([0x40, 0x50, 0x60]), + FixedMutation::TargetSource, + ) + .compile() + .unwrap() + .content_identity(); + + assert_ne!(changed_signal, baseline); + assert_ne!(changed_target_source, baseline); +} + +#[derive(Clone, Copy)] +struct FullIds { + sources: [SourceId; 2], + targets: [TargetId; 2], + candidates: [[TargetCandidateId; 2]; 2], + opacities: [OpacityInputId; 2], + paints: [PaintId; 4], + port: SurfaceInputPortId, + surfaces: [SurfaceId; 2], + occurrences: [OccurrenceId; 3], + constraints: [ConstraintId; 3], + outputs: [OutputSlotId; 2], + group: ObservationGroupId, +} + +#[derive(Debug, Clone, Copy)] +enum FullMutation { + None, + CompleteSchemaGolden, + CandidateSignal, + OpacityValue, + OpacityPositiveZero, + OpacityNegativeZero, + PaintTarget, + OpacitySource, + OpacityInput, + OccurrenceSubject, + Context, + ConstraintTarget, + ConstraintMode, + ConstraintFamily, + ConstraintInvocation, + ConstraintMultiplicity, + OutputBinding, + OutputMultiplicity, +} + +fn full_program(ids: FullIds, reverse_unordered: bool, mutation: FullMutation) -> CoreProgramV1 { + let mut candidate_signals = [ + [signal([0x10, 0x20, 0x30]), signal([0x30, 0x20, 0x10])], + [signal([0x20, 0x60, 0x40]), signal([0x60, 0x40, 0x20])], + ]; + if matches!(mutation, FullMutation::CandidateSignal) { + candidate_signals[1][1] = signal([0x61, 0x40, 0x20]); + } + let mut sources = vec![ + Source::new(ids.sources[0], signal([0x08, 0x10, 0x18])), + Source::new(ids.sources[1], signal([0x18, 0x10, 0x08])), + ]; + let mut targets = (0..2) + .map(|target| { + let mut candidates = (0..2) + .map(|candidate| { + TargetCandidateV1::new( + ids.candidates[target][candidate], + candidate_signals[target][candidate], + ) + }) + .collect::>(); + if reverse_unordered { + candidates.reverse(); + } + Target::finite(ids.targets[target], ids.sources[target], candidates) + }) + .collect::>(); + let mut paints = vec![ + Paint::Solid { + id: ids.paints[0], + target: if matches!(mutation, FullMutation::PaintTarget) { + ids.targets[1] + } else { + ids.targets[0] + }, + }, + Paint::Opacity { + id: ids.paints[1], + source: if matches!(mutation, FullMutation::OpacitySource) { + ids.paints[2] + } else { + ids.paints[0] + }, + opacity: if matches!(mutation, FullMutation::OpacityInput) { + ids.opacities[1] + } else { + ids.opacities[0] + }, + }, + Paint::Solid { + id: ids.paints[2], + target: ids.targets[1], + }, + Paint::Solid { + id: ids.paints[3], + target: ids.targets[0], + }, + ]; + let mut surfaces = vec![ + Surface::Input { + id: ids.surfaces[0], + input: ids.port, + }, + Surface::FromOccurrence { + id: ids.surfaces[1], + occurrence: ids.occurrences[0], + }, + ]; + let mut occurrences = vec![ + Occurrence::new( + ids.occurrences[0], + if matches!(mutation, FullMutation::OccurrenceSubject) { + ids.paints[2] + } else { + ids.paints[1] + }, + ids.surfaces[0], + CompositionProfile::EncodedSrgb8SourceOverV1, + context(SurroundProfileId::AverageV1), + ), + Occurrence::new( + ids.occurrences[1], + ids.paints[2], + ids.surfaces[1], + CompositionProfile::EncodedSrgb8SourceOverV1, + context(if matches!(mutation, FullMutation::Context) { + SurroundProfileId::DarkV1 + } else { + SurroundProfileId::DimV1 + }), + ), + Occurrence::new( + ids.occurrences[2], + ids.paints[3], + ids.surfaces[1], + CompositionProfile::EncodedSrgb8SourceOverV1, + context(SurroundProfileId::DarkV1), + ), + ]; + let mut hard = vec![ConstraintInvocation::hard( + ids.constraints[0], + if matches!(mutation, FullMutation::ConstraintTarget) { + ids.occurrences[1] + } else { + ids.occurrences[0] + }, + CoreProgramConstraintInvocationV1::ExactSrgb8(Srgb8::new([0x11, 0x22, 0x33])), + )]; + let second_invocation = if matches!(mutation, FullMutation::ConstraintFamily) { + CoreProgramConstraintInvocationV1::Wcag22Srgb8(Wcag22CriterionV1::Sc143TextDefault) + } else { + CoreProgramConstraintInvocationV1::ExactSrgb8(Srgb8::new( + if matches!(mutation, FullMutation::ConstraintInvocation) { + [0x44, 0x55, 0x67] + } else { + [0x44, 0x55, 0x66] + }, + )) + }; + let mut report_only = Vec::new(); + if matches!(mutation, FullMutation::ConstraintMode) { + report_only.push(ConstraintInvocation::report_only( + ids.constraints[1], + ids.occurrences[1], + second_invocation, + )); + } else { + hard.push(ConstraintInvocation::hard( + ids.constraints[1], + ids.occurrences[1], + second_invocation, + )); + } + if matches!(mutation, FullMutation::CompleteSchemaGolden) { + report_only.push(ConstraintInvocation::report_only( + ConstraintId::new(1_001), + ids.occurrences[2], + CoreProgramConstraintInvocationV1::Wcag22Srgb8( + Wcag22CriterionV1::Sc1411GraphicalObject, + ), + )); + } + hard.push(ConstraintInvocation::hard( + ids.constraints[2], + ids.occurrences[2], + CoreProgramConstraintInvocationV1::ExactSrgb8(Srgb8::new([0x21, 0x32, 0x43])), + )); + if matches!(mutation, FullMutation::ConstraintMultiplicity) { + hard.push(ConstraintInvocation::hard( + ConstraintId::new(1_000), + ids.occurrences[1], + CoreProgramConstraintInvocationV1::ExactSrgb8(Srgb8::new([0x44, 0x55, 0x66])), + )); + } + let mut outputs = vec![ + OutputBinding::new( + ids.outputs[0], + if matches!(mutation, FullMutation::OutputBinding) { + ids.paints[1] + } else { + ids.paints[2] + }, + ), + OutputBinding::new(ids.outputs[1], ids.paints[3]), + ]; + if matches!(mutation, FullMutation::OutputMultiplicity) { + outputs.push(OutputBinding::new(OutputSlotId::new(1_000), ids.paints[0])); + } + let mut opacities = vec![ + OpacityInput::new( + ids.opacities[0], + match mutation { + FullMutation::OpacityValue => 0.5, + FullMutation::OpacityPositiveZero => 0.0, + FullMutation::OpacityNegativeZero => -0.0, + _ => 0.625, + }, + ), + OpacityInput::new(ids.opacities[1], 0.25), + ]; + if reverse_unordered { + sources.reverse(); + targets.reverse(); + opacities.reverse(); + paints.reverse(); + surfaces.reverse(); + occurrences.reverse(); + hard.reverse(); + report_only.reverse(); + outputs.reverse(); + } + + let mut states = Vec::new(); + for first in 0..2 { + for second in 0..2 { + let mut choices = vec![ + TargetCandidateChoiceV1::new(ids.targets[0], ids.candidates[0][first]), + TargetCandidateChoiceV1::new(ids.targets[1], ids.candidates[1][second]), + ]; + if reverse_unordered { + choices.reverse(); + } + states.push(JointCandidateStateV1::new(choices)); + } + } + + Program::new( + sources, + targets, + ObservationGroup::new(ids.group, vec![ids.port]), + opacities, + paints, + surfaces, + occurrences, + ConstraintSet::new(hard, report_only), + outputs, + CoreProgramEvaluatorsV1, + ) + .with_joint_selection(DeclaredJointSelectionV1::new(states)) +} + +fn canonical_full_ids() -> FullIds { + FullIds { + sources: [SourceId::new(1), SourceId::new(2)], + targets: [TargetId::new(3), TargetId::new(4)], + candidates: [ + [TargetCandidateId::new(5), TargetCandidateId::new(6)], + [TargetCandidateId::new(7), TargetCandidateId::new(8)], + ], + opacities: [OpacityInputId::new(9), OpacityInputId::new(10)], + paints: [ + PaintId::new(11), + PaintId::new(12), + PaintId::new(13), + PaintId::new(14), + ], + port: SurfaceInputPortId::new(15), + surfaces: [SurfaceId::new(16), SurfaceId::new(17)], + occurrences: [ + OccurrenceId::new(18), + OccurrenceId::new(19), + OccurrenceId::new(20), + ], + constraints: [ + ConstraintId::new(21), + ConstraintId::new(22), + ConstraintId::new(23), + ], + outputs: [OutputSlotId::new(24), OutputSlotId::new(25)], + group: ObservationGroupId::new(26), + } +} + +#[test] +fn complete_program_schema_v1_digest_is_cross_platform_golden() { + // Вместе с fixed golden этот Program содержит каждый V1 vertex/edge tag, + // обе constraint families и оба режима. Случайная смена кодировки требует + // явной смены версии, а не тихого перевыпуска прежнего content address. + let compiled = full_program( + canonical_full_ids(), + false, + FullMutation::CompleteSchemaGolden, + ) + .compile() + .unwrap(); + + assert_eq!( + compiled.content_identity().as_bytes(), + &[ + 31, 240, 88, 38, 57, 68, 24, 218, 176, 123, 232, 154, 83, 136, 136, 238, 75, 62, 8, + 163, 188, 120, 229, 152, 163, 217, 101, 60, 245, 191, 167, 219, + ] + ); +} + +#[test] +fn every_typed_opaque_namespace_and_unordered_list_is_alpha_invariant() { + let canonical = canonical_full_ids(); + let renamed = FullIds { + sources: [SourceId::new(2), SourceId::new(1)], + targets: [TargetId::new(2), TargetId::new(1)], + candidates: [ + [TargetCandidateId::new(2), TargetCandidateId::new(1)], + [TargetCandidateId::new(2), TargetCandidateId::new(1)], + ], + opacities: [OpacityInputId::new(2), OpacityInputId::new(1)], + paints: [ + PaintId::new(4), + PaintId::new(3), + PaintId::new(2), + PaintId::new(1), + ], + port: SurfaceInputPortId::new(1), + surfaces: [SurfaceId::new(2), SurfaceId::new(1)], + occurrences: [ + OccurrenceId::new(3), + OccurrenceId::new(2), + OccurrenceId::new(1), + ], + constraints: [ + ConstraintId::new(3), + ConstraintId::new(2), + ConstraintId::new(1), + ], + outputs: [OutputSlotId::new(2), OutputSlotId::new(1)], + group: ObservationGroupId::new(1), + }; + + let canonical = full_program(canonical, false, FullMutation::None) + .compile() + .unwrap(); + let renamed = full_program(renamed, true, FullMutation::None) + .compile() + .unwrap(); + + assert_eq!(canonical.content_identity(), renamed.content_identity()); +} + +#[test] +fn independent_program_content_mutations_change_identity() { + let ids = canonical_full_ids(); + let baseline = full_program(ids, false, FullMutation::None) + .compile() + .unwrap() + .content_identity(); + + for mutation in [ + FullMutation::CandidateSignal, + FullMutation::OpacityValue, + FullMutation::PaintTarget, + FullMutation::OpacitySource, + FullMutation::OpacityInput, + FullMutation::OccurrenceSubject, + FullMutation::Context, + FullMutation::ConstraintTarget, + FullMutation::ConstraintMode, + FullMutation::ConstraintFamily, + FullMutation::ConstraintInvocation, + FullMutation::ConstraintMultiplicity, + FullMutation::OutputBinding, + FullMutation::OutputMultiplicity, + ] { + let compiled = full_program(ids, false, mutation) + .compile() + .unwrap_or_else(|error| panic!("{mutation:?} must remain valid: {error:?}")); + assert_ne!(compiled.content_identity(), baseline, "{mutation:?}"); + } +} + +#[test] +fn signed_zero_opacity_has_one_physical_content_identity() { + let ids = canonical_full_ids(); + + let positive = full_program(ids, false, FullMutation::OpacityPositiveZero) + .compile() + .unwrap(); + let negative = full_program(ids, false, FullMutation::OpacityNegativeZero) + .compile() + .unwrap(); + + assert_eq!(positive.content_identity(), negative.content_identity()); +} + +fn nested_surface_program( + surface_from_second_occurrence: bool, + third_uses_nested_surface: bool, +) -> CoreProgramV1 { + let sources = [SourceId::new(1), SourceId::new(2)]; + let targets = [TargetId::new(3), TargetId::new(4)]; + let paints = [PaintId::new(5), PaintId::new(6)]; + let port = SurfaceInputPortId::new(7); + let surfaces = [SurfaceId::new(8), SurfaceId::new(9)]; + let occurrences = [ + OccurrenceId::new(10), + OccurrenceId::new(11), + OccurrenceId::new(12), + ]; + + Program::new( + vec![ + Source::new(sources[0], signal([0x20, 0x30, 0x40])), + Source::new(sources[1], signal([0x70, 0x60, 0x50])), + ], + vec![ + Target::fixed(targets[0], sources[0]), + Target::fixed(targets[1], sources[1]), + ], + ObservationGroup::new(ObservationGroupId::new(13), vec![port]), + vec![], + vec![ + Paint::Solid { + id: paints[0], + target: targets[0], + }, + Paint::Solid { + id: paints[1], + target: targets[1], + }, + ], + vec![ + Surface::Input { + id: surfaces[0], + input: port, + }, + Surface::FromOccurrence { + id: surfaces[1], + occurrence: occurrences[usize::from(surface_from_second_occurrence)], + }, + ], + vec![ + Occurrence::new( + occurrences[0], + paints[0], + surfaces[0], + CompositionProfile::EncodedSrgb8SourceOverV1, + context(SurroundProfileId::AverageV1), + ), + Occurrence::new( + occurrences[1], + paints[1], + surfaces[0], + CompositionProfile::EncodedSrgb8SourceOverV1, + context(SurroundProfileId::DimV1), + ), + Occurrence::new( + occurrences[2], + paints[0], + surfaces[usize::from(third_uses_nested_surface)], + CompositionProfile::EncodedSrgb8SourceOverV1, + context(SurroundProfileId::DarkV1), + ), + ], + ConstraintSet::new( + occurrences + .iter() + .copied() + .enumerate() + .map(|(index, occurrence)| { + ConstraintInvocation::hard( + ConstraintId::new(14 + index as u32), + occurrence, + CoreProgramConstraintInvocationV1::ExactSrgb8(Srgb8::new([ + 0x20 + index as u8, + 0x30, + 0x40, + ])), + ) + }) + .collect(), + vec![], + ), + vec![ + OutputBinding::new(OutputSlotId::new(17), paints[0]), + OutputBinding::new(OutputSlotId::new(18), paints[1]), + ], + CoreProgramEvaluatorsV1, + ) +} + +#[test] +fn surface_and_occurrence_relations_are_content_bound() { + let baseline = nested_surface_program(false, true) + .compile() + .unwrap() + .content_identity(); + let changed_surface_source = nested_surface_program(true, true) + .compile() + .unwrap() + .content_identity(); + let changed_occurrence_backdrop = nested_surface_program(false, false) + .compile() + .unwrap() + .content_identity(); + + assert_ne!(changed_surface_source, baseline); + assert_ne!(changed_occurrence_backdrop, baseline); +} + +#[derive(Clone, Copy)] +enum SubjectPaintShape { + OpacityFromFirst, + OpacityFromSecond, + Solid, +} + +fn paint_shape_program(shape: SubjectPaintShape) -> CoreProgramV1 { + let sources = [SourceId::new(1), SourceId::new(2)]; + let targets = [TargetId::new(3), TargetId::new(4)]; + let paints = [PaintId::new(5), PaintId::new(6), PaintId::new(7)]; + let opacity = OpacityInputId::new(8); + let port = SurfaceInputPortId::new(9); + let surface = SurfaceId::new(10); + let occurrence = OccurrenceId::new(11); + let subject = match shape { + SubjectPaintShape::OpacityFromFirst => Paint::Opacity { + id: paints[1], + source: paints[0], + opacity, + }, + SubjectPaintShape::OpacityFromSecond => Paint::Opacity { + id: paints[1], + source: paints[2], + opacity, + }, + SubjectPaintShape::Solid => Paint::Solid { + id: paints[1], + target: targets[0], + }, + }; + + Program::new( + vec![ + Source::new(sources[0], signal([0x20, 0x30, 0x40])), + Source::new(sources[1], signal([0x70, 0x60, 0x50])), + ], + vec![ + Target::fixed(targets[0], sources[0]), + Target::fixed(targets[1], sources[1]), + ], + ObservationGroup::new(ObservationGroupId::new(12), vec![port]), + vec![OpacityInput::new(opacity, 0.5)], + vec![ + Paint::Solid { + id: paints[0], + target: targets[0], + }, + subject, + Paint::Solid { + id: paints[2], + target: targets[1], + }, + ], + vec![Surface::Input { + id: surface, + input: port, + }], + vec![Occurrence::new( + occurrence, + paints[1], + surface, + CompositionProfile::EncodedSrgb8SourceOverV1, + context(SurroundProfileId::AverageV1), + )], + ConstraintSet::new( + vec![ConstraintInvocation::hard( + ConstraintId::new(13), + occurrence, + CoreProgramConstraintInvocationV1::ExactSrgb8(Srgb8::new([0x20, 0x30, 0x40])), + )], + vec![], + ), + vec![OutputBinding::new(OutputSlotId::new(14), paints[1])], + CoreProgramEvaluatorsV1, + ) +} + +#[test] +fn paint_variant_and_dependency_edges_are_content_bound() { + let baseline = paint_shape_program(SubjectPaintShape::OpacityFromFirst) + .compile() + .unwrap() + .content_identity(); + let changed_source = paint_shape_program(SubjectPaintShape::OpacityFromSecond) + .compile() + .unwrap() + .content_identity(); + let changed_variant = paint_shape_program(SubjectPaintShape::Solid) + .compile() + .unwrap() + .content_identity(); + + assert_ne!(changed_source, baseline); + assert_ne!(changed_variant, baseline); +} + +fn source_alias_program(shared: bool) -> CoreProgramV1 { + let sources = if shared { + vec![Source::new(SourceId::new(1), signal([0x30, 0x40, 0x50]))] + } else { + vec![ + Source::new(SourceId::new(1), signal([0x30, 0x40, 0x50])), + Source::new(SourceId::new(2), signal([0x30, 0x40, 0x50])), + ] + }; + let targets = [TargetId::new(3), TargetId::new(4)]; + let paints = [PaintId::new(5), PaintId::new(6)]; + let port = SurfaceInputPortId::new(7); + let surface = SurfaceId::new(8); + let occurrences = [OccurrenceId::new(9), OccurrenceId::new(10)]; + Program::new( + sources, + vec![ + Target::fixed(targets[0], SourceId::new(1)), + Target::fixed( + targets[1], + if shared { + SourceId::new(1) + } else { + SourceId::new(2) + }, + ), + ], + ObservationGroup::new(ObservationGroupId::new(11), vec![port]), + vec![], + vec![ + Paint::Solid { + id: paints[0], + target: targets[0], + }, + Paint::Solid { + id: paints[1], + target: targets[1], + }, + ], + vec![Surface::Input { + id: surface, + input: port, + }], + vec![ + Occurrence::new( + occurrences[0], + paints[0], + surface, + CompositionProfile::EncodedSrgb8SourceOverV1, + context(SurroundProfileId::AverageV1), + ), + Occurrence::new( + occurrences[1], + paints[1], + surface, + CompositionProfile::EncodedSrgb8SourceOverV1, + context(SurroundProfileId::AverageV1), + ), + ], + ConstraintSet::new( + vec![ + ConstraintInvocation::hard( + ConstraintId::new(12), + occurrences[0], + CoreProgramConstraintInvocationV1::ExactSrgb8(Srgb8::new([0x30, 0x40, 0x50])), + ), + ConstraintInvocation::hard( + ConstraintId::new(13), + occurrences[1], + CoreProgramConstraintInvocationV1::ExactSrgb8(Srgb8::new([0x30, 0x40, 0x50])), + ), + ], + vec![], + ), + vec![ + OutputBinding::new(OutputSlotId::new(14), paints[0]), + OutputBinding::new(OutputSlotId::new(15), paints[1]), + ], + CoreProgramEvaluatorsV1, + ) +} + +#[test] +fn shared_content_and_equal_duplicated_content_have_distinct_identity() { + let shared = source_alias_program(true).compile().unwrap(); + let duplicated = source_alias_program(false).compile().unwrap(); + + assert_ne!(shared.content_identity(), duplicated.content_identity()); +} + +fn finite_program(reverse_order: bool) -> CoreProgramV1 { + let source = SourceId::new(1); + let target = TargetId::new(2); + let first = TargetCandidateId::new(3); + let second = TargetCandidateId::new(4); + let paint = PaintId::new(5); + let port = SurfaceInputPortId::new(6); + let surface = SurfaceId::new(7); + let occurrence = OccurrenceId::new(8); + let states = [first, second].map(|candidate| { + JointCandidateStateV1::new(vec![TargetCandidateChoiceV1::new(target, candidate)]) + }); + let states = if reverse_order { + vec![states[1].clone(), states[0].clone()] + } else { + states.to_vec() + }; + + Program::new( + vec![Source::new(source, signal([0; 3]))], + vec![Target::finite( + target, + source, + vec![ + TargetCandidateV1::new(first, signal([0; 3])), + TargetCandidateV1::new(second, signal([0xFF; 3])), + ], + )], + ObservationGroup::new(ObservationGroupId::new(9), vec![port]), + vec![], + vec![Paint::Solid { id: paint, target }], + vec![Surface::Input { + id: surface, + input: port, + }], + vec![Occurrence::new( + occurrence, + paint, + surface, + CompositionProfile::EncodedSrgb8SourceOverV1, + context(SurroundProfileId::AverageV1), + )], + ConstraintSet::new( + vec![ConstraintInvocation::hard( + ConstraintId::new(10), + occurrence, + CoreProgramConstraintInvocationV1::ExactSrgb8(Srgb8::new([0; 3])), + )], + vec![], + ), + vec![OutputBinding::new(OutputSlotId::new(11), paint)], + CoreProgramEvaluatorsV1, + ) + .with_joint_selection(DeclaredJointSelectionV1::new(states)) +} + +fn fixed_single_target_program() -> CoreProgramV1 { + let source = SourceId::new(1); + let target = TargetId::new(2); + let paint = PaintId::new(5); + let port = SurfaceInputPortId::new(6); + let surface = SurfaceId::new(7); + let occurrence = OccurrenceId::new(8); + + Program::new( + vec![Source::new(source, signal([0; 3]))], + vec![Target::fixed(target, source)], + ObservationGroup::new(ObservationGroupId::new(9), vec![port]), + vec![], + vec![Paint::Solid { id: paint, target }], + vec![Surface::Input { + id: surface, + input: port, + }], + vec![Occurrence::new( + occurrence, + paint, + surface, + CompositionProfile::EncodedSrgb8SourceOverV1, + context(SurroundProfileId::AverageV1), + )], + ConstraintSet::new( + vec![ConstraintInvocation::hard( + ConstraintId::new(10), + occurrence, + CoreProgramConstraintInvocationV1::ExactSrgb8(Srgb8::new([0; 3])), + )], + vec![], + ), + vec![OutputBinding::new(OutputSlotId::new(11), paint)], + CoreProgramEvaluatorsV1, + ) +} + +#[test] +fn fixed_and_finite_target_domains_have_distinct_identity() { + let fixed = fixed_single_target_program().compile().unwrap(); + let finite = finite_program(false).compile().unwrap(); + + assert_ne!(fixed.content_identity(), finite.content_identity()); +} + +#[test] +fn content_identity_retains_the_explicit_joint_state_order() { + let forward = finite_program(false).compile().unwrap(); + let reversed = finite_program(true).compile().unwrap(); + + assert_ne!(forward.content_identity(), reversed.content_identity()); +} + +#[derive(Clone, Copy)] +enum RegularIncidence { + OneCycle, + TwoCycles, +} + +fn regular_incidence_program(kind: RegularIncidence) -> CoreProgramV1 { + let source = SourceId::new(1); + let target = TargetId::new(2); + let paints = [10, 11, 12, 13].map(PaintId::new); + let ports = [20, 21, 22, 23].map(SurfaceInputPortId::new); + let surfaces = [30, 31, 32, 33].map(SurfaceId::new); + let incidence = match kind { + RegularIncidence::OneCycle => [ + (0, 0), + (0, 1), + (1, 1), + (1, 2), + (2, 2), + (2, 3), + (3, 3), + (3, 0), + ], + RegularIncidence::TwoCycles => [ + (0, 0), + (0, 1), + (1, 0), + (1, 1), + (2, 2), + (2, 3), + (3, 2), + (3, 3), + ], + }; + let occurrences = incidence + .iter() + .enumerate() + .map(|(index, (paint, surface))| { + Occurrence::new( + OccurrenceId::new(40 + index as u32), + paints[*paint], + surfaces[*surface], + CompositionProfile::EncodedSrgb8SourceOverV1, + context(SurroundProfileId::AverageV1), + ) + }) + .collect::>(); + let constraints = occurrences + .iter() + .enumerate() + .map(|(index, occurrence)| { + ConstraintInvocation::hard( + ConstraintId::new(60 + index as u32), + occurrence.id(), + CoreProgramConstraintInvocationV1::ExactSrgb8(Srgb8::new([0x20; 3])), + ) + }) + .collect(); + + Program::new( + vec![Source::new(source, signal([0x20; 3]))], + vec![Target::fixed(target, source)], + ObservationGroup::new(ObservationGroupId::new(3), ports.to_vec()), + vec![], + paints + .iter() + .copied() + .map(|id| Paint::Solid { id, target }) + .collect(), + surfaces + .iter() + .copied() + .zip(ports) + .map(|(id, input)| Surface::Input { id, input }) + .collect(), + occurrences, + ConstraintSet::new(constraints, vec![]), + paints + .iter() + .copied() + .enumerate() + .map(|(index, paint)| OutputBinding::new(OutputSlotId::new(80 + index as u32), paint)) + .collect(), + CoreProgramEvaluatorsV1, + ) +} + +#[test] +fn exact_canon_distinguishes_regular_non_isomorphic_programs() { + let one_cycle = regular_incidence_program(RegularIncidence::OneCycle) + .compile() + .unwrap(); + let two_cycles = regular_incidence_program(RegularIncidence::TwoCycles) + .compile() + .unwrap(); + + assert_ne!(one_cycle.content_identity(), two_cycles.content_identity()); +} diff --git a/crates/labcolors-core/src/program_joint_integration_tests.rs b/crates/labcolors-core/src/program_joint_integration_tests.rs index db1ce5d2..88afd5a1 100644 --- a/crates/labcolors-core/src/program_joint_integration_tests.rs +++ b/crates/labcolors-core/src/program_joint_integration_tests.rs @@ -1152,15 +1152,17 @@ fn equivalent_recompiled_owner_is_a_new_generation_and_cannot_revive_old_session let first_evaluator = CountingProgramWcag22Srgb8V1::default(); let first_calls = first_evaluator.clone(); let mut compiled = counting_fixed_program(first_evaluator); + let first_content_identity = compiled.content_identity(); let mut old_session = compiled.instantiate(STREAM).unwrap(); - assert!(matches!( - old_session.update(update(1, 0x00)).unwrap(), - SessionState::Ready { .. } - )); + let SessionState::Ready { current } = old_session.update(update(1, 0x00)).unwrap() else { + panic!("the first owner must certify its admitted input"); + }; + assert_eq!(current.report().content_identity(), first_content_identity); let replacement_evaluator = CountingProgramWcag22Srgb8V1::default(); let replacement_calls = replacement_evaluator.clone(); compiled = counting_fixed_program(replacement_evaluator); + assert_eq!(compiled.content_identity(), first_content_identity); assert!(matches!( old_session.update(update(2, 0x00)), Err(SessionUpdateError::OwnerExpired), diff --git a/crates/labcolors-core/src/program_session.rs b/crates/labcolors-core/src/program_session.rs index 30c4b217..a755d754 100644 --- a/crates/labcolors-core/src/program_session.rs +++ b/crates/labcolors-core/src/program_session.rs @@ -22,10 +22,11 @@ use crate::appearance::{ }; use crate::composition::CompositionProfileV1; use crate::constraints::{ - Evaluator, ExactSrgb8IdentityV1, HardDecision, ProgramPointAssessmentErrorV1, - ProgramPointEvaluatorV1, ProgramPointInvocation, ProgramPointTargetV1, - ProgramVisiblePointBindingV1, ProgramVisiblePointPassEvidence, - ProgramVisiblePointViolationEvidence, Wcag22Srgb8V1, assess_program_point_hard, + Evaluator, ExactSrgb8IdentityV1, HardDecision, ProgramConstraintContentV1, + ProgramPointAssessmentErrorV1, ProgramPointEvaluatorContentV1, ProgramPointEvaluatorV1, + ProgramPointInvocation, ProgramPointTargetV1, ProgramVisiblePointBindingV1, + ProgramVisiblePointPassEvidence, ProgramVisiblePointViolationEvidence, Wcag22Srgb8V1, + assess_program_point_hard, }; use crate::joint::{ AdmittedFiniteJointOrderV1, FiniteDomainOrdinalV1, FiniteJointOrderErrorV1, @@ -45,6 +46,10 @@ use crate::session::{ }; use crate::wcag22::Wcag22CriterionV1; +#[path = "program_identity.rs"] +mod identity; +pub(crate) use identity::ProgramContentIdentityV1; + /// Opaque identity of one immutable authored colour source. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct SourceId(u32); @@ -483,6 +488,8 @@ pub(crate) trait ProgramConstraintEvaluatorSetV1: Sized { fn pass_binding(evidence: &Self::PassEvidence) -> ProgramVisiblePointBindingV1; fn violation_binding(evidence: &Self::ViolationEvidence) -> ProgramVisiblePointBindingV1; + + fn constraint_content(&self, invocation: Self::Invocation) -> ProgramConstraintContentV1; } impl ProgramConstraintEvaluatorSetV1 for Evaluation @@ -511,6 +518,10 @@ where fn violation_binding(evidence: &Self::ViolationEvidence) -> ProgramVisiblePointBindingV1 { *evidence.binding() } + + fn constraint_content(&self, invocation: Self::Invocation) -> ProgramConstraintContentV1 { + self.program_constraint_content_v1(invocation) + } } /// Generates the code-owned heterogeneous evaluator set as parallel closed @@ -602,6 +613,18 @@ macro_rules! define_core_program_evaluators_v1 { $(CoreProgramViolationEvidenceV1::$variant(evidence) => *evidence.binding()),+ } } + + fn constraint_content(&self, invocation: Self::Invocation) -> ProgramConstraintContentV1 { + match invocation { + $(CoreProgramConstraintInvocationV1::$variant(invocation) => { + let evaluator: $evaluator = $evaluator_value; + <$evaluator as ProgramPointEvaluatorContentV1>::program_constraint_content_v1( + &evaluator, + invocation, + ) + }),+ + } + } } }; } @@ -1022,6 +1045,7 @@ where Evaluation: ProgramConstraintEvaluatorSetV1, ProgramConstraintInvocationOf: Copy, { + content_identity: ProgramContentIdentityV1, evaluator: Evaluation, graph: CompiledAppearanceGraph, binding_template: AdmittedAppearanceBindings, @@ -1061,6 +1085,15 @@ where self.owner_generation.observation_group.id } + /// Контентный адрес Program в границах схемы V1. + /// + /// Opaque ID и порядок неупорядоченных объявлений исключены; явный joint + /// order входит в адрес. Адрес не подтверждает поколение владельца и не + /// заменяет revision-bound evidence. + pub fn content_identity(&self) -> ProgramContentIdentityV1 { + self.owner_generation.content_identity + } + pub fn surface_input_ports(&self) -> &[SurfaceInputPortId] { self.owner_generation.observation_group.schema.as_slice() } @@ -1204,11 +1237,14 @@ where } } -/// Complete revision-bound assessment in case-major, constraint-ID order. +/// Полная оценка, привязанная к revision. Для selected/fixed результата ячейки +/// идут сначала по physical case, затем по constraint ID. Exhaustive conflict +/// дополнительно упорядочен сначала по joint state. pub struct ProgramReportV1 where Evaluation: ProgramConstraintEvaluatorSetV1, { + content_identity: ProgramContentIdentityV1, observation: RevisionBoundObservationV1, cells: Vec>, } @@ -1217,6 +1253,12 @@ impl ProgramReportV1 where Evaluation: ProgramConstraintEvaluatorSetV1, { + /// Адрес содержимого Program, по которому построен report; это не + /// идентификатор поколения и не runtime-authority. + pub const fn content_identity(&self) -> ProgramContentIdentityV1 { + self.content_identity + } + pub const fn observation(&self) -> &RevisionBoundObservationV1 { &self.observation } @@ -1673,6 +1715,7 @@ where Ok(SessionDecision::Violation(ProgramConflictV1 { report: ProgramReportV1 { + content_identity: epoch.content_identity, observation, cells: buffers.conflict_cells, }, @@ -1740,7 +1783,11 @@ where if cells.len() != expected_cell_count { return Err(ProgramSessionEvaluationError::InternalInvariant); } - let report = ProgramReportV1 { observation, cells }; + let report = ProgramReportV1 { + content_identity: epoch.content_identity, + observation, + cells, + }; if has_hard_violation { Ok(SessionDecision::Violation(ProgramConflictV1 { report, @@ -2036,15 +2083,20 @@ where .map_err(map_observation_schema_compile_error)?; validate_terminal_dependency_cone(&program)?; - let (finite_targets, joint_selection) = - compile_targets(&graph, program.targets, program.joint_selection)?; + let (finite_targets, joint_selection) = compile_targets( + &graph, + &mut program.targets, + program.joint_selection.as_mut(), + )?; let all_occurrence_contexts = compile_occurrence_contexts(&graph, &program.occurrences)?; let mut constraints = - compile_constraints::(&graph, &all_occurrence_contexts, program.constraints)?; + compile_constraints::(&graph, &all_occurrence_contexts, &program.constraints)?; let occurrence_contexts = compact_constraint_contexts(&all_occurrence_contexts, &mut constraints)?; - let outputs = compile_outputs(&graph, program.outputs)?; + let outputs = compile_outputs(&graph, &mut program.outputs)?; + let content_identity = identity::compile_program_content_identity_v1(&program)?; Ok(ProgramEpochV1 { + content_identity, evaluator: program.evaluator, graph, binding_template, @@ -2489,8 +2541,8 @@ struct LoweredConstraint { fn compile_targets( graph: &CompiledAppearanceGraph, - authored_targets: Vec, - authored_selection: Option, + authored_targets: &mut [Target], + authored_selection: Option<&mut DeclaredJointSelectionV1>, ) -> Result< ( Box<[CompiledFiniteTargetV1]>, @@ -2498,10 +2550,10 @@ fn compile_targets( ), ProgramCompileError, > { - struct CanonicalFiniteTargetV1 { + struct CanonicalFiniteTargetV1<'a> { id: TargetId, binding: CompiledColorInputSlotV1, - candidates: Vec, + candidates: &'a [TargetCandidateV1], } let mut compiled = Vec::new(); @@ -2509,7 +2561,7 @@ fn compile_targets( .try_reserve_exact(authored_targets.len()) .map_err(|_| ProgramCompileError::ResourceExhausted)?; for target in authored_targets { - let TargetDomainV1::Finite(mut candidates) = target.domain else { + let TargetDomainV1::Finite(candidates) = &mut target.domain else { continue; }; if candidates.is_empty() { @@ -2568,9 +2620,11 @@ fn compile_targets( authored_tuples .try_reserve_exact(authored_selection.states.len()) .map_err(|_| ProgramCompileError::ResourceExhausted)?; - for (state_index, mut state) in authored_selection.states.into_iter().enumerate() { - state.choices.sort_unstable_by_key(|choice| choice.target); - if let Some(target) = state + for (state_index, authored_state) in authored_selection.states.iter_mut().enumerate() { + authored_state + .choices + .sort_unstable_by_key(|choice| choice.target); + if let Some(target) = authored_state .choices .windows(2) .find(|pair| pair[0].target == pair[1].target) @@ -2581,7 +2635,7 @@ fn compile_targets( target, }); } - if let Some(choice) = state.choices.iter().find(|choice| { + if let Some(choice) = authored_state.choices.iter().find(|choice| { compiled .binary_search_by_key(&choice.target, |target| target.id) .is_err() @@ -2597,14 +2651,14 @@ fn compile_targets( .try_reserve_exact(compiled.len()) .map_err(|_| ProgramCompileError::ResourceExhausted)?; for target in &compiled { - let choice_index = state + let choice_index = authored_state .choices .binary_search_by_key(&target.id, |choice| choice.target) .map_err(|_| ProgramCompileError::JointStateMissingTarget { state: state_index, target: target.id, })?; - let choice = state.choices[choice_index]; + let choice = authored_state.choices[choice_index]; let candidate_index = target .candidates .binary_search_by_key(&choice.candidate, |candidate| candidate.id) @@ -2634,7 +2688,7 @@ fn compile_targets( candidates .try_reserve_exact(target.candidates.len()) .map_err(|_| ProgramCompileError::ResourceExhausted)?; - candidates.extend(target.candidates.into_iter().map(TargetCandidateV1::signal)); + candidates.extend(target.candidates.iter().map(|candidate| candidate.signal())); runtime_targets.push(CompiledFiniteTargetV1 { binding: target.binding, candidates: candidates.into_boxed_slice(), @@ -2687,7 +2741,7 @@ fn compile_occurrence_contexts( fn compile_constraints( graph: &CompiledAppearanceGraph, occurrence_contexts: &[CompiledOccurrenceContextV1], - authored: ConstraintSet>, + authored: &ConstraintSet>, ) -> Result< Box<[CompiledPointConstraint>]>, ProgramCompileError, @@ -2705,21 +2759,16 @@ where lowered .try_reserve_exact(total) .map_err(|_| ProgramCompileError::ResourceExhausted)?; - lowered.extend( - authored - .hard - .into_iter() - .map(|constraint| LoweredConstraint { - id: constraint.id, - target: constraint.target, - mode: CompiledConstraintModeV1::Hard, - invocation: constraint.invocation, - }), - ); + lowered.extend(authored.hard.iter().map(|constraint| LoweredConstraint { + id: constraint.id, + target: constraint.target, + mode: CompiledConstraintModeV1::Hard, + invocation: constraint.invocation, + })); lowered.extend( authored .report_only - .into_iter() + .iter() .map(|constraint| LoweredConstraint { id: constraint.id, target: constraint.target, @@ -2809,10 +2858,9 @@ fn compact_constraint_contexts( fn compile_outputs( graph: &CompiledAppearanceGraph, - authored: Vec, + authored: &mut [OutputBinding], ) -> Result, ProgramCompileError> { let len = authored.len(); - let mut authored = authored; authored.sort_unstable_by_key(|output| output.output); if let Some(duplicate) = authored .windows(2) @@ -2821,7 +2869,7 @@ fn compile_outputs( { return Err(ProgramCompileError::DuplicateOutputSlot { output: duplicate }); } - for output in &authored { + for output in authored.iter() { if graph.bind_paint(output.paint).is_none() { return Err(ProgramCompileError::MissingOutputPaint { output: output.output, @@ -2834,7 +2882,7 @@ fn compile_outputs( compiled .try_reserve_exact(len) .map_err(|_| ProgramCompileError::ResourceExhausted)?; - for output in authored { + for output in authored.iter().copied() { let paint = graph .bind_paint(output.paint) .ok_or(ProgramCompileError::InternalInvariant)?; diff --git a/crates/labcolors-core/src/sha256.rs b/crates/labcolors-core/src/sha256.rs index 45b538a9..537760b9 100644 --- a/crates/labcolors-core/src/sha256.rs +++ b/crates/labcolors-core/src/sha256.rs @@ -1,21 +1,20 @@ -//! Dependency-free SHA-256 used only for canonical content identities. +//! SHA-256 без зависимостей только для канонических контентных адресов. //! -//! Constants and operations are the SHA-256 algorithm specified by NIST -//! FIPS 180-4, section 6.2. This module is intentionally private: callers use -//! domain-specific digest types instead of treating a hash as mathematical -//! proof or a semantic identifier. +//! Константы и операции следуют алгоритму NIST FIPS 180-4, раздел 6.2. Модуль +//! намеренно закрыт: вызывающий код использует предметные типы адресов и не +//! выдаёт хеш за математическое доказательство либо семантический ID. -/// Exact 256-bit SHA-256 output. +/// Точный 256-битный результат SHA-256. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub(crate) struct Digest([u8; 32]); impl Digest { - /// Borrow the exact digest bytes. + /// Заимствует точные байты digest-а. pub(crate) const fn as_bytes(&self) -> &[u8; 32] { &self.0 } - /// Canonical lowercase hexadecimal encoding. + /// Каноническая hexadecimal-запись в нижнем регистре. #[cfg(test)] #[allow(dead_code)] pub(crate) fn to_hex(self) -> String { @@ -107,11 +106,10 @@ const ROUND_CONSTANTS: [u32; 64] = [ 0xc671_78f2, ]; -/// Incremental SHA-256 state with one fixed-size pending block. +/// Инкрементальное состояние SHA-256 с одним pending-блоком фиксированного размера. /// -/// The byte count wraps modulo 2^64, matching the encoded message-length field -/// defined by FIPS 180-4. No input bytes are retained after their block has -/// been compressed. +/// Счётчик байтов оборачивается по модулю 2^64, как поле длины сообщения в +/// FIPS 180-4. После сжатия блока входные байты не сохраняются. pub(crate) struct Hasher { state: [u32; 8], pending: [u8; 64], @@ -120,7 +118,7 @@ pub(crate) struct Hasher { } impl Hasher { - /// Start a new SHA-256 computation. + /// Начинает новое вычисление SHA-256. pub(crate) const fn new() -> Self { Self { state: INITIAL_STATE, @@ -130,8 +128,7 @@ impl Hasher { } } - /// Add bytes to this computation without allocating or retaining the - /// caller's slice. + /// Добавляет байты без аллокации и сохранения среза вызывающей стороны. pub(crate) fn update(&mut self, mut bytes: &[u8]) { self.byte_len = self.byte_len.wrapping_add(bytes.len() as u64); @@ -163,7 +160,7 @@ impl Hasher { self.pending_len = remainder.len(); } - /// Finish the computation and return its exact 256-bit output. + /// Завершает вычисление и возвращает точный 256-битный результат. pub(crate) fn finalize(mut self) -> Digest { let mut final_block = self.pending; final_block[self.pending_len] = 0x80; @@ -188,7 +185,7 @@ impl Hasher { } } -/// Hash one byte slice without heap-allocating a padded copy. +/// Хеширует один байтовый срез без padded-копии в heap. pub(crate) fn digest(bytes: &[u8]) -> Digest { let mut hasher = Hasher::new(); hasher.update(bytes); From 39ee0a7e8c3744666f85e6e4c839ef1d3d20ef87 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Fri, 24 Jul 2026 21:09:21 +0300 Subject: [PATCH 2/4] core: name versioned identity discriminants --- crates/labcolors-core/src/program_identity.rs | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/crates/labcolors-core/src/program_identity.rs b/crates/labcolors-core/src/program_identity.rs index 53a1b7f3..0c29d56c 100644 --- a/crates/labcolors-core/src/program_identity.rs +++ b/crates/labcolors-core/src/program_identity.rs @@ -32,6 +32,9 @@ mod release_tag { pub(super) const MUTATION_SENTINEL_FRAME_V1: u8 = 2; pub(super) const CIECAM16_VIEWING_INPUTS_V1: u8 = 1; pub(super) const ENCODED_SRGB8_SOURCE_OVER_V1: u8 = 1; + pub(super) const SURROUND_AVERAGE_V1: u8 = 1; + pub(super) const SURROUND_DIM_V1: u8 = 2; + pub(super) const SURROUND_DARK_V1: u8 = 3; pub(super) const EXACT_SRGB8_FAMILY_V1: u8 = 1; pub(super) const EXACT_SRGB8_IDENTITY_V1: u8 = 1; @@ -47,6 +50,10 @@ mod release_tag { pub(super) const WCAG22_SRGB8_IDENTITY_V1: u8 = 1; pub(super) const WCAG22_SRGB8_PROFILE_V1: u8 = 1; pub(super) const WCAG22_SRGB8_CAPABILITY_V1: u8 = 1; + pub(super) const WCAG22_SC_1_4_3_TEXT_DEFAULT: u8 = 1; + pub(super) const WCAG22_SC_1_4_3_TEXT_LARGE_SCALE: u8 = 2; + pub(super) const WCAG22_SC_1_4_11_UI_COMPONENT_OR_STATE: u8 = 3; + pub(super) const WCAG22_SC_1_4_11_GRAPHICAL_OBJECT: u8 = 4; } /// Устойчивый к коллизиям адрес канонизированного содержимого Program V1. @@ -394,9 +401,9 @@ fn write_context( color.push_u64(context.adapting_luminance_cd_m2().to_bits())?; color.push_u64(context.background_luminance_ratio().to_bits())?; color.push_u8(match context.surround_profile() { - crate::lcs_occurrence::SurroundProfileId::AverageV1 => 1, - crate::lcs_occurrence::SurroundProfileId::DimV1 => 2, - crate::lcs_occurrence::SurroundProfileId::DarkV1 => 3, + crate::lcs_occurrence::SurroundProfileId::AverageV1 => release_tag::SURROUND_AVERAGE_V1, + crate::lcs_occurrence::SurroundProfileId::DimV1 => release_tag::SURROUND_DIM_V1, + crate::lcs_occurrence::SurroundProfileId::DarkV1 => release_tag::SURROUND_DARK_V1, })?; Ok(()) } @@ -412,10 +419,12 @@ fn occurrence_color(occurrence: Occurrence) -> Result u8 { match criterion { - Wcag22CriterionV1::Sc143TextDefault => 1, - Wcag22CriterionV1::Sc143TextLargeScale => 2, - Wcag22CriterionV1::Sc1411UiComponentOrState => 3, - Wcag22CriterionV1::Sc1411GraphicalObject => 4, + Wcag22CriterionV1::Sc143TextDefault => release_tag::WCAG22_SC_1_4_3_TEXT_DEFAULT, + Wcag22CriterionV1::Sc143TextLargeScale => release_tag::WCAG22_SC_1_4_3_TEXT_LARGE_SCALE, + Wcag22CriterionV1::Sc1411UiComponentOrState => { + release_tag::WCAG22_SC_1_4_11_UI_COMPONENT_OR_STATE + } + Wcag22CriterionV1::Sc1411GraphicalObject => release_tag::WCAG22_SC_1_4_11_GRAPHICAL_OBJECT, } } From a09bf73932bc8fb26b058e0226f323874eb74208 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Sat, 25 Jul 2026 20:00:06 +0300 Subject: [PATCH 3/4] fix(ci): re-pin the runtime WASM budget to this slice's measured artifact The exact-length gate still pinned 376830B from `canonical-authored-program-lowerer` (#456, run 29971399220). This slice grew the runtime artifact to 376985B and did not carry the re-pin that every earlier stacked slice performed (#450, #452, #454, #456), so the gate has rejected this head and every head above it. The +155B is attributable to this slice alone: runs 30115821523 (#457), 30124467410 (#458), 30125634830 (#459), 30129537515 (#460) and 30136346868 (#461) all measure exactly 376985B, so #458-#461 contribute zero bytes to the artifact and were failing only on the inherited pin. The new measurement is the CI run for this exact head (39ee0a7e8c3744666f85e6e4c839ef1d3d20ef87), not a local build: the canonical platform is linux-x64 and a local arm64 build only produces a DIAGNOSTIC result. The budget file's own SHA-256 is re-pinned in the checker so the drift gate keeps rejecting unattributed edits. Co-Authored-By: Claude --- packages/colors/bench/wasm.json | 8 ++++---- scripts/check-wasm-size-budget.mjs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/colors/bench/wasm.json b/packages/colors/bench/wasm.json index f88f6209..43d7b1ea 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-29971399220", + "source": "github-actions-run-30115821523", "platform": "linux-x64", - "rawBytes": 376830 + "rawBytes": 376985 }, "policy": { - "maxRawBytes": 376830, - "basis": "canonical-authored-program-lowerer", + "maxRawBytes": 376985, + "basis": "program-content-identity", "gzip": "diagnostic-only" } } diff --git a/scripts/check-wasm-size-budget.mjs b/scripts/check-wasm-size-budget.mjs index a5776086..b41b24f6 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 = - "84a8d2e1f0517f871256fdc64bdf9323135c497e3a374b751d8c0bb710f60084"; + "73722a93248ba005c6b8cf1846e52c1344dcb9f85a3a2133a25c51dacbefdcfc"; const SCHEMA_VERSION = 1; const CANONICAL_ARTIFACT = "packages/colors/pkg/labcolors_bg.wasm"; From 30fe845db50aac774eb7bd66813bcbf4d5bb573c Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Sun, 26 Jul 2026 03:14:30 +0300 Subject: [PATCH 4/4] fix(ci): re-bind the point-support source capsule to this slice's cone This slice moves files inside the point-support semantic cone, so the capsule digest and the committed surplus proof move with it. Both are now regenerated in the same commit that causes the drift, matching the convention the rest of the stack follows; previously the re-bind was batched at #460, which left #457-#459 fail-closed on their own heads and made the stack unmergeable in order. Numerical review: every proof field is unchanged. Only the source-binding identities move -- the file hashes of the cone files this slice edits, the resulting closure digest, the verifier hash and the rolled-up payload hash. The surplus mathematics is byte-identical. Co-Authored-By: Claude --- .../point-support-reference-surplus-q55-bps-proof-v1.json | 2 +- scripts/verify_point_support_surplus.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/labcolors-core/contracts/point-support-reference-surplus-q55-bps-proof-v1.json b/crates/labcolors-core/contracts/point-support-reference-surplus-q55-bps-proof-v1.json index 9445d79b..6cd37887 100644 --- a/crates/labcolors-core/contracts/point-support-reference-surplus-q55-bps-proof-v1.json +++ b/crates/labcolors-core/contracts/point-support-reference-surplus-q55-bps-proof-v1.json @@ -1 +1 @@ -{"artifact_id":"wcag22-srgb8-luminance-q55-v1","basis_point_proof":{"checks":30,"drop_all_semantics":"zero required surplus; current must still meet the anchor","drop_domain_inclusive":[0,10000],"nonpositive_baseline_semantics":"zero required surplus; current must meet the anchor"},"bound_id":"point-support-reference-surplus-q55-bps-v1","certified_claim":"for every successfully evaluated enabled stability cell, decision is Retained iff current_lower_surplus >= (10000-drop_bps)/10000 * max(baseline_lower_surplus,0); the declared anchor remains a separate hard floor","comparator_proof":{"algorithm":"euclidean-continued-fraction-ordering-v1","dense_denominator_inclusive":[1,31],"dense_numerator_inclusive":[0,31],"dense_small_cases":984064,"invariant":"equal integer parts; reciprocal proper fractions reverse order","largest_fibonacci_index":186,"oracle":"unbounded-integer-cross-product","random_cases":250000,"random_corpus_sha256":"97c4af7b452b31a4ab92645f70c17acb38bf57ca55484e32ad9d7d79d97a333d","random_seed":210583930,"termination":"each nonterminal denominator becomes a strictly smaller remainder","u128_adversarial_cases":190},"declared_operation_law":"q55-lower-reference-distance-explicit-anchor-bps-retention-v1","excluded_claim":"does not certify retention against the unknown exact baseline surplus, renderer equivalence outside encoded-sRGB8 source-over, or a successful result when evaluation fails","integer_replay_envelope":{"assumption":"every Q55 luminance upper <= scale + 3","i128_max":170141183460469231731687303715884105727,"offset_cleared_denominator_max":756604737398243388,"positive_baseline_numerator_max":1188950301625811064,"rational_denominator_max":1513209474796486776,"required_denominator_max":15132094747964867760000,"required_numerator_max":11889503016258110640000,"signed_anchor_abs_coarse_max":5296233161787703716,"u128_max":340282366920938463463374607431768211455,"u64_max":18446744073709551615},"profile_id":"srgb8-q55-retained-reference-surplus-bps-v1","proof_id":"point-support-reference-surplus-integer-v1","proof_payload_sha256":"2ab90713f2008fab2ba343d4525164ba9177d5bdda1a890bd3192a5ffd1891d8","q55_dependency":{"artifact_id":"wcag22-srgb8-luminance-q55-v1","artifact_sha256":"7ff239d9052b346f3c50da01ca65ca2330892ed1a3ff30e190797fcef6f03604","maximum_luminance_upper":36028797018963971,"outward_interval_width_bound":3,"proof_id":"wcag22-srgb8-full-domain-q55-v1","proof_payload_sha256":"3c639a7c875046c46b56b51ecdd67d5ecaf14a1134490c88a222e7037b63c0f2","proof_sha256":"ac59cf89503170c789223b91d775213a19d4e571ef930f2ea609fcd51b14defd","q55_scale":36028797018963968},"reference_and_anchor_proof":{"anchor_identity_checks":75,"orientation_law":"distance-magnitude-symmetric-orientation-reported-separately","overlap_lower_distance":"0/1","separated_endpoint_checks":504},"schema_version":2,"site_id":"point-support-retained-reference-surplus-v1","source_binding_exclusions":["whole-crate compilation or compiler/toolchain attestation","binary, package, FFI, renderer, or browser transport attestation","unrelated Lab Colors modules outside the declared point-support semantic cone"],"source_binding_law":"point-support-rust-whole-file-semantic-cone-v2","source_binding_schema_version":2,"source_binding_scope":"exact bytes of the private point-support Rust semantic cone and its two WCAG include_str inputs; comments and cfg(test) text are intentionally significant","source_closure_sha256":"4310a239e732f0710fd6201c2517fd98ef4afd0e3cd8e27c248dee69d6c54cb3","source_files":[{"kind":"compile-time-input","path":"crates/labcolors-core/contracts/wcag22-srgb8-q55-proof-v1.json","sha256":"ac59cf89503170c789223b91d775213a19d4e571ef930f2ea609fcd51b14defd"},{"kind":"compile-time-input","path":"crates/labcolors-core/contracts/wcag22-srgb8-v1.json","sha256":"b4bb7e5f17a99f2c911fdbe3da23a48b049277b796291094950f14680cc3cc7b"},{"kind":"rust-source","path":"crates/labcolors-core/src/appearance.rs","sha256":"455f94bdc0064765214e21ec38e49939e9ebbf765d18bb709512f7210c986953"},{"kind":"rust-source","path":"crates/labcolors-core/src/composition.rs","sha256":"195a67327a3bd86d7816b634481389930bf68577bb1202fad14c2ea152df8625"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/exact.rs","sha256":"33b959f11366415143b5b03fcfe370d1fb7e61e46ed05349ed17560d10663ff7"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/mod.rs","sha256":"37cff33755c5a700853ccbc08bd539d2235325b9eb208be696444799d4cf819e"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/wcag22.rs","sha256":"7e10638e8da68dc1279f078e0a0daa5caf10af403eb00c1cbbd5506190e74d9d"},{"kind":"rust-source","path":"crates/labcolors-core/src/hash.rs","sha256":"f97a0fd7d6ad3162f0f1dfb326fccfb7ed40da9a8fa67a5b8a239a1ae2ae49c3"},{"kind":"rust-source","path":"crates/labcolors-core/src/lcs_occurrence.rs","sha256":"9ad998d3b7ac01a03afa398a6750ab71dc1278da991202933cf9006c3cedf5f5"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"cb52f917d260ee580cd5cb78666ed5d3f1fe9351e9874b51250d1e8571a48c73"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"f1d6c7a66885326caea2f7f469061c723b826ff99b294324e5a478724f8981f6"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"0755210e3e591d7049f293a0f0b7647681631f32feee5ad7d3b3309cffca8f9d"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"6cd73600267b148c3e9ecc8d2d623f7f8576aed0e1c4c7c50071e297810b8d4b"},{"kind":"rust-source","path":"crates/labcolors-core/src/srgb8.rs","sha256":"6c95324eb05476f35f75375a9af0b2b4a41b8b2978c46e67d2ce1aea5adde342"},{"kind":"rust-source","path":"crates/labcolors-core/src/wcag22.rs","sha256":"7ba7864eb7e73789bad6c63c64a4dc2dcc08c2da6921375fb9564fca230c2780"},{"kind":"rust-source","path":"crates/labcolors-core/src/wcag22/kernel.rs","sha256":"c97980c1ca2c7ea9cabff9c8d2fb7282773cca180ae15948391c29c9d6196040"},{"kind":"rust-source","path":"crates/labcolors-core/src/wcag22/q55_data.rs","sha256":"af4d23d6b70c45ce6efa839e7dda4bb0a61f6aae43cb805af6fa9b29e6c3bae2"},{"kind":"rust-source","path":"crates/labcolors-core/src/wcag22_evidence.rs","sha256":"3c5a75b07254c6071a64700af208a64987d0f0ea9698eadc54a9e74585ce1f72"}],"source_negative_controls":43,"universal_algebraic_certificate":{"basis_point_scale_instantiation":10000,"domain":"integers; Q55 scale Q>0; anchor L>=D>=0; lighter monotonicity L2>=L1>D>=0; darker monotonicity L>D2>=D1>=0; current/baseline denominators b,q>0; basis-point scale B>0 instantiated as 10000; p>0; a>=0; 0<=drop_bps<=B","identities":["three explicit anchor-surplus formulas after denominator clearing","reference distance is monotone increasing in lighter L","reference distance is monotone decreasing in darker D","positive-baseline retained threshold is p*(B-drop)/(q*B)","a/b >= p*(B-drop)/(q*B) iff a*q*B >= p*(B-drop)*b"],"method":"exact-sparse-integer-polynomial-identities-plus-positive-denominator-order-lemma-v1","nonpositive_baseline_case":"max(baseline,0)=0; retained threshold is exactly zero","symbolic_mutation_controls":{"anchor_coefficients_and_denominator":6,"retained_cross_product":5},"wolfram_language_cross_check":{"query":"FullSimplify[{20 g/d - 0 == 20 g/d, 20 g/d - 2 == (20 g - 2 d)/d, 20 g/d - 7/2 == (40 g - 7 d)/(2 d), Equivalent[a/b >= p (s-x)/(q s), a q s >= p (s-x) b], Max[p/q, 0] (s-x)/s == Piecewise[{{0, p <= 0}}, p (s-x)/(q s)]}, Assumptions -> Element[{a,b,p,q,s,x,g,d}, Integers] && a >= 0 && b > 0 && q > 0 && s > 0 && 0 <= x <= s && d > 0 && g >= 0]","query_sha256":"8cdbb9964583030c8b92498961896cb2a98613f1cb31eb7c54acdf8e16beff10","result":"{True, True, True, True, True}","result_sha256":"13a8f2ee8d0fde335a638e46d7cc8a8427b9a1437c77d22cfcf925bb87fa6303"}},"verifier_sha256":"742503a19220d71dc5d4c4ff22c9e5e7cb407d2506e450312b87e7ba342761f0"} +{"artifact_id":"wcag22-srgb8-luminance-q55-v1","basis_point_proof":{"checks":30,"drop_all_semantics":"zero required surplus; current must still meet the anchor","drop_domain_inclusive":[0,10000],"nonpositive_baseline_semantics":"zero required surplus; current must meet the anchor"},"bound_id":"point-support-reference-surplus-q55-bps-v1","certified_claim":"for every successfully evaluated enabled stability cell, decision is Retained iff current_lower_surplus >= (10000-drop_bps)/10000 * max(baseline_lower_surplus,0); the declared anchor remains a separate hard floor","comparator_proof":{"algorithm":"euclidean-continued-fraction-ordering-v1","dense_denominator_inclusive":[1,31],"dense_numerator_inclusive":[0,31],"dense_small_cases":984064,"invariant":"equal integer parts; reciprocal proper fractions reverse order","largest_fibonacci_index":186,"oracle":"unbounded-integer-cross-product","random_cases":250000,"random_corpus_sha256":"97c4af7b452b31a4ab92645f70c17acb38bf57ca55484e32ad9d7d79d97a333d","random_seed":210583930,"termination":"each nonterminal denominator becomes a strictly smaller remainder","u128_adversarial_cases":190},"declared_operation_law":"q55-lower-reference-distance-explicit-anchor-bps-retention-v1","excluded_claim":"does not certify retention against the unknown exact baseline surplus, renderer equivalence outside encoded-sRGB8 source-over, or a successful result when evaluation fails","integer_replay_envelope":{"assumption":"every Q55 luminance upper <= scale + 3","i128_max":170141183460469231731687303715884105727,"offset_cleared_denominator_max":756604737398243388,"positive_baseline_numerator_max":1188950301625811064,"rational_denominator_max":1513209474796486776,"required_denominator_max":15132094747964867760000,"required_numerator_max":11889503016258110640000,"signed_anchor_abs_coarse_max":5296233161787703716,"u128_max":340282366920938463463374607431768211455,"u64_max":18446744073709551615},"profile_id":"srgb8-q55-retained-reference-surplus-bps-v1","proof_id":"point-support-reference-surplus-integer-v1","proof_payload_sha256":"78669986ea1a7a75f40e76c19beba4ff9c72abcbf1a240d4437846fc8d28519c","q55_dependency":{"artifact_id":"wcag22-srgb8-luminance-q55-v1","artifact_sha256":"7ff239d9052b346f3c50da01ca65ca2330892ed1a3ff30e190797fcef6f03604","maximum_luminance_upper":36028797018963971,"outward_interval_width_bound":3,"proof_id":"wcag22-srgb8-full-domain-q55-v1","proof_payload_sha256":"3c639a7c875046c46b56b51ecdd67d5ecaf14a1134490c88a222e7037b63c0f2","proof_sha256":"ac59cf89503170c789223b91d775213a19d4e571ef930f2ea609fcd51b14defd","q55_scale":36028797018963968},"reference_and_anchor_proof":{"anchor_identity_checks":75,"orientation_law":"distance-magnitude-symmetric-orientation-reported-separately","overlap_lower_distance":"0/1","separated_endpoint_checks":504},"schema_version":2,"site_id":"point-support-retained-reference-surplus-v1","source_binding_exclusions":["whole-crate compilation or compiler/toolchain attestation","binary, package, FFI, renderer, or browser transport attestation","unrelated Lab Colors modules outside the declared point-support semantic cone"],"source_binding_law":"point-support-rust-whole-file-semantic-cone-v2","source_binding_schema_version":2,"source_binding_scope":"exact bytes of the private point-support Rust semantic cone and its two WCAG include_str inputs; comments and cfg(test) text are intentionally significant","source_closure_sha256":"246c77dbf4923aca4cdaedb12be910ceb2885c94e4e42f652cdcba9b9417a427","source_files":[{"kind":"compile-time-input","path":"crates/labcolors-core/contracts/wcag22-srgb8-q55-proof-v1.json","sha256":"ac59cf89503170c789223b91d775213a19d4e571ef930f2ea609fcd51b14defd"},{"kind":"compile-time-input","path":"crates/labcolors-core/contracts/wcag22-srgb8-v1.json","sha256":"b4bb7e5f17a99f2c911fdbe3da23a48b049277b796291094950f14680cc3cc7b"},{"kind":"rust-source","path":"crates/labcolors-core/src/appearance.rs","sha256":"455f94bdc0064765214e21ec38e49939e9ebbf765d18bb709512f7210c986953"},{"kind":"rust-source","path":"crates/labcolors-core/src/composition.rs","sha256":"195a67327a3bd86d7816b634481389930bf68577bb1202fad14c2ea152df8625"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/exact.rs","sha256":"892576a8621185352583e63dc0a1aacac32e32a8063b6fe24ae16d4ff9dce7cb"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/mod.rs","sha256":"3a4c2911781f91c91c12fc23929843865a8e41d5630f4df41f77130434b5f228"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/wcag22.rs","sha256":"40f288b222fbd102970916437d3f99c33ec2dfb77041b1c8361d27f08ff018ca"},{"kind":"rust-source","path":"crates/labcolors-core/src/hash.rs","sha256":"f97a0fd7d6ad3162f0f1dfb326fccfb7ed40da9a8fa67a5b8a239a1ae2ae49c3"},{"kind":"rust-source","path":"crates/labcolors-core/src/lcs_occurrence.rs","sha256":"9ad998d3b7ac01a03afa398a6750ab71dc1278da991202933cf9006c3cedf5f5"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"b3edb3764119c0b4fd50f52b62cbc07fa81c4bfdf1246b91f20eb3d8d4eebd31"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"f1d6c7a66885326caea2f7f469061c723b826ff99b294324e5a478724f8981f6"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"0755210e3e591d7049f293a0f0b7647681631f32feee5ad7d3b3309cffca8f9d"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"6cd73600267b148c3e9ecc8d2d623f7f8576aed0e1c4c7c50071e297810b8d4b"},{"kind":"rust-source","path":"crates/labcolors-core/src/srgb8.rs","sha256":"6c95324eb05476f35f75375a9af0b2b4a41b8b2978c46e67d2ce1aea5adde342"},{"kind":"rust-source","path":"crates/labcolors-core/src/wcag22.rs","sha256":"7ba7864eb7e73789bad6c63c64a4dc2dcc08c2da6921375fb9564fca230c2780"},{"kind":"rust-source","path":"crates/labcolors-core/src/wcag22/kernel.rs","sha256":"c97980c1ca2c7ea9cabff9c8d2fb7282773cca180ae15948391c29c9d6196040"},{"kind":"rust-source","path":"crates/labcolors-core/src/wcag22/q55_data.rs","sha256":"af4d23d6b70c45ce6efa839e7dda4bb0a61f6aae43cb805af6fa9b29e6c3bae2"},{"kind":"rust-source","path":"crates/labcolors-core/src/wcag22_evidence.rs","sha256":"3c5a75b07254c6071a64700af208a64987d0f0ea9698eadc54a9e74585ce1f72"}],"source_negative_controls":43,"universal_algebraic_certificate":{"basis_point_scale_instantiation":10000,"domain":"integers; Q55 scale Q>0; anchor L>=D>=0; lighter monotonicity L2>=L1>D>=0; darker monotonicity L>D2>=D1>=0; current/baseline denominators b,q>0; basis-point scale B>0 instantiated as 10000; p>0; a>=0; 0<=drop_bps<=B","identities":["three explicit anchor-surplus formulas after denominator clearing","reference distance is monotone increasing in lighter L","reference distance is monotone decreasing in darker D","positive-baseline retained threshold is p*(B-drop)/(q*B)","a/b >= p*(B-drop)/(q*B) iff a*q*B >= p*(B-drop)*b"],"method":"exact-sparse-integer-polynomial-identities-plus-positive-denominator-order-lemma-v1","nonpositive_baseline_case":"max(baseline,0)=0; retained threshold is exactly zero","symbolic_mutation_controls":{"anchor_coefficients_and_denominator":6,"retained_cross_product":5},"wolfram_language_cross_check":{"query":"FullSimplify[{20 g/d - 0 == 20 g/d, 20 g/d - 2 == (20 g - 2 d)/d, 20 g/d - 7/2 == (40 g - 7 d)/(2 d), Equivalent[a/b >= p (s-x)/(q s), a q s >= p (s-x) b], Max[p/q, 0] (s-x)/s == Piecewise[{{0, p <= 0}}, p (s-x)/(q s)]}, Assumptions -> Element[{a,b,p,q,s,x,g,d}, Integers] && a >= 0 && b > 0 && q > 0 && s > 0 && 0 <= x <= s && d > 0 && g >= 0]","query_sha256":"8cdbb9964583030c8b92498961896cb2a98613f1cb31eb7c54acdf8e16beff10","result":"{True, True, True, True, True}","result_sha256":"13a8f2ee8d0fde335a638e46d7cc8a8427b9a1437c77d22cfcf925bb87fa6303"}},"verifier_sha256":"3c067cb4ce2af0d9ff514ba0937650ebd62514bb8d6bafb7fc1bc319e0e1816a"} diff --git a/scripts/verify_point_support_surplus.py b/scripts/verify_point_support_surplus.py index e39328fa..608c6cdb 100755 --- a/scripts/verify_point_support_surplus.py +++ b/scripts/verify_point_support_surplus.py @@ -58,7 +58,7 @@ SOURCE_BINDING_LAW = "point-support-rust-whole-file-semantic-cone-v2" SOURCE_BINDING_DOMAIN = b"labcolors.point-support.rust-whole-file-semantic-cone.v2" EXPECTED_SOURCE_CAPSULE_SHA256 = ( - "4310a239e732f0710fd6201c2517fd98ef4afd0e3cd8e27c248dee69d6c54cb3" + "246c77dbf4923aca4cdaedb12be910ceb2885c94e4e42f652cdcba9b9417a427" ) EXPECTED_Q55_PROOF_SHA256 = ( "ac59cf89503170c789223b91d775213a19d4e571ef930f2ea609fcd51b14defd"