From e51645985ad16ef82ed0256a7ade16f7457b0564 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Tue, 28 Jul 2026 07:31:04 +0300 Subject: [PATCH 1/2] =?UTF-8?q?refactor(core):=20=D0=B7=D0=B0=D0=BC=D0=BA?= =?UTF-8?q?=D0=BD=D1=83=D1=82=D1=8C=20=D1=84=D0=B8=D0=B7=D0=B8=D1=87=D0=B5?= =?UTF-8?q?=D1=81=D0=BA=D0=B8=D0=B9=20intent=20=D1=86=D0=B5=D0=BB=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clean-set-srgb8-v1/receipt-v1.json | 20 +-- .../clean-set-srgb8-v1/receipt-v1.sha256 | 2 +- .../src/generic_boundary_tests.rs | 55 ++++++- crates/labcolors-core/src/program.rs | 115 +++++++------- .../src/program/attachment/tests.rs | 20 +-- .../labcolors-core/src/program_api_tests.rs | 31 ++++ .../src/program_boundary_tests.rs | 50 +++--- .../src/program_clean_set_tests.rs | 18 ++- crates/labcolors-core/src/program_identity.rs | 50 +++--- .../src/program_identity_tests.rs | 112 +++++++++++--- .../src/program_joint_integration_tests.rs | 45 +++--- .../src/program_mixed_evaluator_tests.rs | 15 +- .../src/program_point_causality_tests.rs | 22 +-- crates/labcolors-core/src/program_session.rs | 146 +++++++++++------- .../src/program_session_tests.rs | 4 +- docs/whitepaper.md | 16 +- 16 files changed, 472 insertions(+), 249 deletions(-) diff --git a/crates/labcolors-core/contracts/clean-set-srgb8-v1/receipt-v1.json b/crates/labcolors-core/contracts/clean-set-srgb8-v1/receipt-v1.json index f3163067..b8f1928b 100644 --- a/crates/labcolors-core/contracts/clean-set-srgb8-v1/receipt-v1.json +++ b/crates/labcolors-core/contracts/clean-set-srgb8-v1/receipt-v1.json @@ -83,39 +83,39 @@ "sha256": "0c8a7bb59464e1ddcace07b01ba24a64f8c9d3aeff547651dc38c276a0fc3322" }, { - "bytes": 69967, + "bytes": 70121, "license": "MIT", "path": "crates/labcolors-core/src/program/attachment/tests.rs", "role": "point_attachment_tests", - "sha256": "f1cbd583163a287ac9ea806d748cc789e4fb6f29880432cc97cf77705ad41075" + "sha256": "2c0876c61196f6492d0f250789ded9644bad1de9ff92c9dea2a14fc55e87f18e" }, { - "bytes": 164718, + "bytes": 165362, "license": "MIT", "path": "crates/labcolors-core/src/program.rs", "role": "program_facade_source", - "sha256": "0ebca95215dc6c7fe462cd87b933cf7b67a67ae0b69863601ab5579bcda481d6" + "sha256": "7067a2108a7fe224302ca7e2b6effdff97966d63328ce5e0b93ca0bcd3a9fda3" }, { - "bytes": 71739, + "bytes": 71867, "license": "MIT", "path": "crates/labcolors-core/src/program_identity.rs", "role": "program_identity_source", - "sha256": "300b6a313232a625f98ac14d6ceb66cc866f3605354b5566d02d86ea3ecadcab" + "sha256": "b59f1f5fe0c71637471498635240de6a381c6b8f11308424e81c0f3e0c6b12d0" }, { - "bytes": 168664, + "bytes": 170258, "license": "MIT", "path": "crates/labcolors-core/src/program_session.rs", "role": "program_source", - "sha256": "c5f30b92165d4836f29fa9195885143aadc9a75ee4ca7540674377b4f5ad9a19" + "sha256": "86a035d09bfee36cac9d5761254b21803a41f6f75b2072c1a5df11967ea006de" }, { - "bytes": 21705, + "bytes": 21872, "license": "MIT", "path": "crates/labcolors-core/src/program_clean_set_tests.rs", "role": "program_tests", - "sha256": "bbb4265f0d19f01c5aba71d70c57fbe1846b649bcbd6e944a818c46695dc6373" + "sha256": "16fb30ac575eb3ae1caba9b482673c3ad83e87211a7b50401bc2e8f27880e519" }, { "bytes": 11370, diff --git a/crates/labcolors-core/contracts/clean-set-srgb8-v1/receipt-v1.sha256 b/crates/labcolors-core/contracts/clean-set-srgb8-v1/receipt-v1.sha256 index fecce693..18a3f2a8 100644 --- a/crates/labcolors-core/contracts/clean-set-srgb8-v1/receipt-v1.sha256 +++ b/crates/labcolors-core/contracts/clean-set-srgb8-v1/receipt-v1.sha256 @@ -1 +1 @@ -be3f324c6931d7aa967c5941c3b7c841711e36cef75468dea636662a9e7b0a9b receipt-v1.json +3b51f8ecf38181d68b619b30ebb1fdd28a17606f5ac2699bf8070497a0515b80 receipt-v1.json diff --git a/crates/labcolors-core/src/generic_boundary_tests.rs b/crates/labcolors-core/src/generic_boundary_tests.rs index 2f8cf317..5a24ea5c 100644 --- a/crates/labcolors-core/src/generic_boundary_tests.rs +++ b/crates/labcolors-core/src/generic_boundary_tests.rs @@ -383,6 +383,44 @@ fn staged_program_draft_wraps_the_single_canonical_core_graph() { } } +#[test] +fn finite_target_intent_has_no_dead_source_axis() { + assert_eq!( + normalized_source_scope( + PROGRAM_SESSION_SOURCE, + "pub enum TargetIntentV1 {", + "/// A Paint-addressable target", + ), + "pub enum TargetIntentV1 { FixedSource(SourceId), Finite(FinitePaintDomainV1), }", + "Target intent must remain a closed sum instead of a source × domain product", + ); + assert_eq!( + normalized_source_scope( + PROGRAM_SESSION_SOURCE, + "pub struct Target {", + "impl Target {", + ), + "pub struct Target { id: TargetId, intent: TargetIntentV1, }", + "a finite Target must have no physically dead Source field", + ); + let target_impl = source_scope( + PROGRAM_SESSION_SOURCE, + "impl Target {", + "/// One typed target/candidate assignment", + ); + assert!( + target_impl + .contains("pub const fn finite(id: TargetId, domain: FinitePaintDomainV1) -> Self",), + "finite construction must consume an already admitted domain without SourceId", + ); + for retired in ["TargetDomainV1", "EmptyTargetDomain", "MissingTargetSource"] { + assert!( + !contains_rust_identifier(PROGRAM_SESSION_SOURCE, retired), + "retired product-state symbol `{retired}` must not return", + ); + } +} + #[test] fn staged_session_is_evidence_only_and_retired_operation_authority_cannot_return() { assert_eq!( @@ -1414,24 +1452,30 @@ fn program_identity_binds_lcs_releases_only_through_lcs_constraint_content() { "DOMAIN_V3", "PROGRAM_SCHEMA_V3", "compile_program_content_identity_v3", + "ProgramContentIdentityV4", + "ContentIdentityV4", + "DOMAIN_V4", + "PROGRAM_SCHEMA_V4", + "compile_program_content_identity_v4", ] { for (path, source) in identity_sources { assert!( !contains_rust_identifier(source, retired), - "the V4 content-address cut must not retain legacy identity symbol `{retired}` in {path}", + "the V5 content-address cut must not retain legacy identity symbol `{retired}` in {path}", ); } } assert!(!PROGRAM_IDENTITY_SOURCE.contains("labcolors.program-content-identity.v1")); assert!(!PROGRAM_IDENTITY_SOURCE.contains("labcolors.program-content-identity.v2")); assert!(!PROGRAM_IDENTITY_SOURCE.contains("labcolors.program-content-identity.v3")); + assert!(!PROGRAM_IDENTITY_SOURCE.contains("labcolors.program-content-identity.v4")); for required in [ - "const DOMAIN_V4: &[u8] = b\"labcolors.program-content-identity.v4\\0\";", - "pub(super) const PROGRAM_SCHEMA_V4: u8 = 4;", + "const DOMAIN_V5: &[u8] = b\"labcolors.program-content-identity.v5\\0\";", + "pub(super) const PROGRAM_SCHEMA_V5: u8 = 5;", ] { assert!( PROGRAM_IDENTITY_SOURCE.contains(required), - "the V4 content-address type must bind its exact domain and schema tag; missing `{required}`", + "the V5 content-address type must bind its exact domain and schema tag; missing `{required}`", ); } @@ -1558,7 +1602,8 @@ fn cold_program_normalization_reuses_owned_unordered_buffers() { for required in [ "authored_targets: &mut [Target]", "authored_selection: Option<&mut DeclaredJointSelectionV1>", - "let TargetDomainV1::Finite(candidates) = &mut target.domain", + "let TargetIntentV1::Finite(domain) = &mut target.intent", + "let candidates = domain.candidates_mut()", "authored_state .choices .sort_unstable_by_key", "authored: &mut [OutputBinding]", ] { diff --git a/crates/labcolors-core/src/program.rs b/crates/labcolors-core/src/program.rs index e11b9610..663dbf56 100644 --- a/crates/labcolors-core/src/program.rs +++ b/crates/labcolors-core/src/program.rs @@ -24,7 +24,7 @@ //! [`CertificateV1::Verified`] хранит выбранное состояние, все клетки //! доказательства и сертифицированные Paint outputs. [`CertificateV1::Conflict`] //! хранит исчерпывающий конфликт по всем рассмотренным состояниям. -//! [`ContentIdentityV4`] идентифицирует каноническое содержание, но не даёт +//! [`ContentIdentityV5`] идентифицирует каноническое содержание, но не даёт //! полномочий живого [`OwnerV1`]. #![forbid(unreachable_pub)] @@ -62,11 +62,12 @@ use crate::program_session::{ CoreProgramViolationEvidenceV1, DeclaredJointSelectionV1, DeclaredSrgb8CleanSetPassV1 as CoreDeclaredSrgb8CleanSetPassV1, DeclaredSrgb8CleanSetViolationV1 as CoreDeclaredSrgb8CleanSetViolationV1, + FinitePaintDomainAdmissionErrorV1, FinitePaintDomainV1 as CoreFinitePaintDomainV1, JointCandidateStateV1, Occurrence, OpacityInput, OutputBinding, OutputSlotId, Paint, PointPresentationRootV1, PointPresentationTargetV1, PresentationRootId, ProgramCompileError, ProgramConflictV1, ProgramConstraintCellV1, ProgramConstraintPassEvidenceV1, ProgramConstraintResultV1, ProgramConstraintSubjectV1, ProgramConstraintViolationEvidenceV1, - ProgramContentIdentityV4, ProgramPaintOutputV1, ProgramSessionEvaluationError, + ProgramContentIdentityV5, ProgramPaintOutputV1, ProgramSessionEvaluationError, ProgramSessionInstantiateError, ProgramSessionPlan, ProgramVerifiedV1, Source, SourceId, Surface, Target, TargetCandidateChoiceV1, TargetCandidateId, TargetCandidateV1 as CoreTargetCandidateV1, TargetId, @@ -221,6 +222,36 @@ impl TargetCandidateV1 { } } +/// Admission-ошибка конечного физического домена. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum FinitePaintDomainErrorV1 { + /// Конечный домен обязан содержать хотя бы один атомарный Paint-кандидат. + Empty, +} + +/// Непустой конечный набор атомарных Paint-кандидатов одной цели. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct FinitePaintDomainV1(CoreFinitePaintDomainV1); + +impl FinitePaintDomainV1 { + /// Допускает домен до помещения в Draft, поэтому пустое состояние не может + /// дожить до компиляции графа. + pub(crate) fn try_new( + candidates: Vec, + ) -> Result { + CoreFinitePaintDomainV1::try_new( + candidates + .into_iter() + .map(|candidate| candidate.0) + .collect(), + ) + .map(Self) + .map_err(|error| match error { + FinitePaintDomainAdmissionErrorV1::Empty => FinitePaintDomainErrorV1::Empty, + }) + } +} + /// Admission-ошибка атомарного значения конечного Paint-кандидата. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum PaintValueErrorV1 { @@ -496,8 +527,8 @@ pub(crate) enum CompileErrorKindV1 { DuplicateConstraint, /// Повторно объявлен выходной слот. DuplicateOutputSlot, - /// Цель ссылается на отсутствующий исходный сигнал. - MissingTargetSource, + /// Фиксированная цель ссылается на отсутствующий исходный сигнал. + MissingFixedSource, /// Paint ссылается на отсутствующую цель. MissingPaintTarget, /// Paint ссылается на отсутствующий Paint. @@ -524,10 +555,8 @@ pub(crate) enum CompileErrorKindV1 { RenderCycle, /// Значение прозрачности находится вне `[0, 1]` или не конечно. OpacityOutOfDomain, - /// Конечная цель не содержит кандидатов. - EmptyTargetDomain, /// Конечная цель не участвует ни в одном ограничении. - UnconstrainedTarget, + UnconstrainedFiniteTarget, /// Конечные цели образуют несвязанные компоненты. DisconnectedFiniteTargets, /// Выходной Paint не покрыт ни одним ограничением. @@ -707,9 +736,9 @@ pub(crate) enum CompileErrorV1 { /// Повторный ID. target: TargetIdV1, }, - /// Цель ссылается на отсутствующий исходный сигнал. - MissingTargetSource { - /// Ошибочная цель. + /// Фиксированная цель ссылается на отсутствующий исходный сигнал. + MissingFixedSource { + /// Ошибочная фиксированная цель. target: TargetIdV1, /// Отсутствующий исходный сигнал. source: SourceIdV1, @@ -863,11 +892,6 @@ pub(crate) enum CompileErrorV1 { /// Ошибочный вход. input: OpacityInputIdV1, }, - /// Конечная цель не содержит кандидатов. - EmptyTargetDomain { - /// Пустая цель. - target: TargetIdV1, - }, /// В одной цели повторно объявлен ID кандидата. DuplicateTargetCandidate { /// Цель кандидата. @@ -887,7 +911,7 @@ pub(crate) enum CompileErrorV1 { value: PaintValueV1, }, /// Конечная цель не участвует ни в одном ограничении. - UnconstrainedTarget { + UnconstrainedFiniteTarget { /// Неограниченная цель. target: TargetIdV1, }, @@ -991,7 +1015,7 @@ impl CompileErrorV1 { match self { Self::DuplicateSource { .. } => Kind::DuplicateSource, Self::DuplicateTarget { .. } => Kind::DuplicateTarget, - Self::MissingTargetSource { .. } => Kind::MissingTargetSource, + Self::MissingFixedSource { .. } => Kind::MissingFixedSource, Self::DuplicateOpacityInput { .. } => Kind::DuplicateOpacityInput, Self::DuplicateSurfaceInputPort { .. } => Kind::DuplicateSurfaceInputPort, Self::UnusedSurfaceInputPort { .. } => Kind::UnusedSurfaceInputPort, @@ -1025,10 +1049,9 @@ impl CompileErrorV1 { Self::PaintCycle(_) => Kind::PaintCycle, Self::RenderCycle(_) => Kind::RenderCycle, Self::OpacityOutOfDomain { .. } => Kind::OpacityOutOfDomain, - Self::EmptyTargetDomain { .. } => Kind::EmptyTargetDomain, Self::DuplicateTargetCandidate { .. } => Kind::DuplicateTargetCandidate, Self::DuplicateTargetCandidateValue { .. } => Kind::DuplicateTargetCandidateValue, - Self::UnconstrainedTarget { .. } => Kind::UnconstrainedTarget, + Self::UnconstrainedFiniteTarget { .. } => Kind::UnconstrainedFiniteTarget, Self::DisconnectedFiniteTargets => Kind::DisconnectedFiniteTargets, Self::UnassessedOutput { .. } => Kind::UnassessedOutput, Self::MissingJointSelection => Kind::MissingJointSelection, @@ -1061,13 +1084,12 @@ impl CompileErrorV1 { match self { Self::DuplicateSource { source } => Some(Handle::Source(*source)), Self::DuplicateTarget { target } - | Self::EmptyTargetDomain { target } - | Self::UnconstrainedTarget { target } + | Self::UnconstrainedFiniteTarget { target } | Self::JointStateDuplicateTarget { target, .. } | Self::JointStateMissingTarget { target, .. } | Self::JointStateUnknownTarget { target, .. } | Self::JointStateUnknownCandidate { target, .. } - | Self::MissingTargetSource { target, .. } + | Self::MissingFixedSource { target, .. } | Self::DuplicateTargetCandidate { target, .. } | Self::DuplicateTargetCandidateValue { target, .. } => Some(Handle::Target(*target)), Self::DuplicateOpacityInput { input } | Self::OpacityOutOfDomain { input } => { @@ -1128,7 +1150,7 @@ impl CompileErrorV1 { use CompileErrorHandleV1 as Handle; match self { - Self::MissingTargetSource { source, .. } => Some(Handle::Source(*source)), + Self::MissingFixedSource { source, .. } => Some(Handle::Source(*source)), Self::DuplicateSurfaceInputBinding { duplicate, .. } => { Some(Handle::Surface(*duplicate)) } @@ -1172,8 +1194,7 @@ impl CompileErrorV1 { | Self::PaintCycle(_) | Self::RenderCycle(_) | Self::OpacityOutOfDomain { .. } - | Self::EmptyTargetDomain { .. } - | Self::UnconstrainedTarget { .. } + | Self::UnconstrainedFiniteTarget { .. } | Self::DisconnectedFiniteTargets | Self::MissingJointSelection | Self::JointSelectionWithoutTargets @@ -1232,17 +1253,10 @@ impl DraftV1 { pub(crate) fn push_finite_target( &mut self, id: TargetIdV1, - source: SourceIdV1, - candidates: Vec, + domain: FinitePaintDomainV1, ) -> &mut Self { - self.inner.push_target(Target::finite( - id.into_core(), - source.into_core(), - candidates - .into_iter() - .map(|candidate| candidate.0) - .collect(), - )); + self.inner + .push_target(Target::finite(id.into_core(), domain.0)); self } @@ -1551,8 +1565,8 @@ impl OwnerV1 { /// /// Identity доступна до первого update, но не заменяет полномочия этой /// конкретной owner-эпохи. - pub(crate) fn content_identity(&self) -> ContentIdentityV4 { - ContentIdentityV4::from_core(self.compiled.content_identity()) + pub(crate) fn content_identity(&self) -> ContentIdentityV5 { + ContentIdentityV5::from_core(self.compiled.content_identity()) } /// Вычисляет верхние границы клеток для prospective Observed-update. @@ -1872,10 +1886,10 @@ impl<'session> PreparedSessionTransitionV1<'session> { /// Identity не идентифицирует owner-эпоху и не даёт runtime-полномочий. #[repr(transparent)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub(crate) struct ContentIdentityV4([u8; 32]); +pub(crate) struct ContentIdentityV5([u8; 32]); -impl ContentIdentityV4 { - const fn from_core(value: ProgramContentIdentityV4) -> Self { +impl ContentIdentityV5 { + const fn from_core(value: ProgramContentIdentityV5) -> Self { Self(*value.as_bytes()) } @@ -1893,8 +1907,8 @@ pub(crate) struct VerifiedCertificateV1<'a> { impl<'a> VerifiedCertificateV1<'a> { /// Возвращает identity скомпилированного содержания. - pub(crate) const fn content_identity(self) -> ContentIdentityV4 { - ContentIdentityV4::from_core(self.inner.report().content_identity()) + pub(crate) const fn content_identity(self) -> ContentIdentityV5 { + ContentIdentityV5::from_core(self.inner.report().content_identity()) } /// Возвращает точное наблюдение, на котором выдан сертификат. @@ -1939,8 +1953,8 @@ pub(crate) struct ConflictCertificateV1<'a> { impl<'a> ConflictCertificateV1<'a> { /// Возвращает identity скомпилированного содержания. - pub(crate) const fn content_identity(self) -> ContentIdentityV4 { - ContentIdentityV4::from_core(self.inner.report().content_identity()) + pub(crate) const fn content_identity(self) -> ContentIdentityV5 { + ContentIdentityV5::from_core(self.inner.report().content_identity()) } /// Возвращает точное наблюдение, вызвавшее конфликт. @@ -1989,7 +2003,7 @@ impl<'a> CertificateV1<'a> { } /// Возвращает identity скомпилированного содержания. - pub(crate) const fn content_identity(self) -> ContentIdentityV4 { + pub(crate) const fn content_identity(self) -> ContentIdentityV5 { match self { Self::Verified(value) => value.content_identity(), Self::Conflict(value) => value.content_identity(), @@ -3008,8 +3022,8 @@ fn map_program_compile_error(error: ProgramCompileError) -> CompileErrorV1 { ProgramCompileError::DuplicateTarget { target } => CompileErrorV1::DuplicateTarget { target: TargetIdV1::from_core(target), }, - ProgramCompileError::MissingTargetSource { target, source } => { - CompileErrorV1::MissingTargetSource { + ProgramCompileError::MissingFixedSource { target, source } => { + CompileErrorV1::MissingFixedSource { target: TargetIdV1::from_core(target), source: SourceIdV1::from_core(source), } @@ -3154,9 +3168,6 @@ fn map_program_compile_error(error: ProgramCompileError) -> CompileErrorV1 { ProgramCompileError::OpacityOutOfDomain { input } => CompileErrorV1::OpacityOutOfDomain { input: OpacityInputIdV1::from_core(input), }, - ProgramCompileError::EmptyTargetDomain { target } => CompileErrorV1::EmptyTargetDomain { - target: TargetIdV1::from_core(target), - }, ProgramCompileError::DuplicateTargetCandidate { target, candidate } => { CompileErrorV1::DuplicateTargetCandidate { target: TargetIdV1::from_core(target), @@ -3174,8 +3185,8 @@ fn map_program_compile_error(error: ProgramCompileError) -> CompileErrorV1 { duplicate: TargetCandidateIdV1::from_core(duplicate), value: PaintValueV1(value), }, - ProgramCompileError::UnconstrainedTarget { target } => { - CompileErrorV1::UnconstrainedTarget { + ProgramCompileError::UnconstrainedFiniteTarget { target } => { + CompileErrorV1::UnconstrainedFiniteTarget { target: TargetIdV1::from_core(target), } } diff --git a/crates/labcolors-core/src/program/attachment/tests.rs b/crates/labcolors-core/src/program/attachment/tests.rs index 68d65d58..d3eace2e 100644 --- a/crates/labcolors-core/src/program/attachment/tests.rs +++ b/crates/labcolors-core/src/program/attachment/tests.rs @@ -5,9 +5,9 @@ use super::support::{ use super::*; use crate::Srgb8; use crate::program::{ - AppearanceContextV1, ConstraintIdV1, DraftV1, JointChoiceV1, JointStateV1, PaintIdV1, - PaintValueV1, ScenarioV1, SourceIdV1, StateKindV1, SurfaceIdV1, SurfaceInputPortIdV1, - SurroundV1, TargetCandidateIdV1, TargetCandidateV1, TargetIdV1, + AppearanceContextV1, ConstraintIdV1, DraftV1, FinitePaintDomainV1, JointChoiceV1, JointStateV1, + PaintIdV1, PaintValueV1, ScenarioV1, SourceIdV1, StateKindV1, SurfaceIdV1, + SurfaceInputPortIdV1, SurroundV1, TargetCandidateIdV1, TargetCandidateV1, TargetIdV1, }; use crate::wcag22::Wcag22CriterionV1; use proptest::prelude::*; @@ -24,6 +24,10 @@ const ROOT: PresentationRootIdV1 = PresentationRootIdV1::new(51); const OUTPUT_A: OutputSlotIdV1 = OutputSlotIdV1::new(12); const OUTPUT_B: OutputSlotIdV1 = OutputSlotIdV1::new(13); +fn finite_domain(candidates: Vec) -> FinitePaintDomainV1 { + FinitePaintDomainV1::try_new(candidates).unwrap() +} + #[test] fn terminal_stamp_is_a_fixed_two_word_copy_value() { const fn assert_copy() {} @@ -1002,11 +1006,10 @@ fn allocator_owner() -> OwnerV1 { draft.push_source(SOURCE, Srgb8::new([0; 3])); draft.push_finite_target( TARGET, - SOURCE, - vec![ + finite_domain(vec![ TargetCandidateV1::new(BLACK, PaintValueV1::opaque(Srgb8::new([0; 3]))), TargetCandidateV1::new(GRAY, PaintValueV1::opaque(Srgb8::new([0x80; 3]))), - ], + ]), ); draft .set_joint_selection(vec![ @@ -1333,11 +1336,10 @@ fn selected_nonopaque_finite_paint_reaches_sink_and_render_authority_atomically( draft.push_source(SOURCE, Srgb8::new([0; 3])); draft.push_finite_target( TARGET, - SOURCE, - vec![TargetCandidateV1::new( + finite_domain(vec![TargetCandidateV1::new( HALF_WHITE, PaintValueV1::try_new(Srgb8::new([0xFF; 3]), 0.5).unwrap(), - )], + )]), ); draft .set_joint_selection(vec![JointStateV1::new(vec![JointChoiceV1::new( diff --git a/crates/labcolors-core/src/program_api_tests.rs b/crates/labcolors-core/src/program_api_tests.rs index 27b13fbb..4c97f457 100644 --- a/crates/labcolors-core/src/program_api_tests.rs +++ b/crates/labcolors-core/src/program_api_tests.rs @@ -94,7 +94,38 @@ fn finite_paint_value_admission_is_typed_and_canonical() { assert_eq!(canonical.opacity().to_bits(), 0.0_f64.to_bits()); } +#[test] +fn finite_paint_domain_is_nonempty_before_it_can_enter_a_draft() { + assert_eq!( + program::FinitePaintDomainV1::try_new(Vec::new()), + Err(program::FinitePaintDomainErrorV1::Empty), + ); + + let candidate = program::TargetCandidateV1::new( + program::TargetCandidateIdV1::new(1), + program::PaintValueV1::opaque(Srgb8::new([1, 2, 3])), + ); + assert!(program::FinitePaintDomainV1::try_new(vec![candidate]).is_ok()); +} + proptest! { + #[test] + fn every_nonempty_finite_paint_domain_is_admitted( + source in any::<[u8; 3]>(), + candidate_count in 1_usize..64, + ) { + let candidates = (0..candidate_count) + .map(|index| { + program::TargetCandidateV1::new( + program::TargetCandidateIdV1::new(index as u32), + program::PaintValueV1::opaque(Srgb8::new(source)), + ) + }) + .collect(); + + prop_assert!(program::FinitePaintDomainV1::try_new(candidates).is_ok()); + } + #[test] fn admitted_finite_paint_value_round_trips_source_and_unit_opacity( source in any::<[u8; 3]>(), diff --git a/crates/labcolors-core/src/program_boundary_tests.rs b/crates/labcolors-core/src/program_boundary_tests.rs index 8019604e..44bf9ea7 100644 --- a/crates/labcolors-core/src/program_boundary_tests.rs +++ b/crates/labcolors-core/src/program_boundary_tests.rs @@ -9,13 +9,13 @@ use crate::Srgb8; use crate::program::{ AppearanceContextErrorKindV1, AppearanceContextFieldV1, AppearanceContextV1, AssessmentV1, CertificateV1, CompileErrorHandleV1, CompileErrorKindV1, CompileErrorV1, ConstraintIdV1, - ConstraintSubjectV1, ContentIdentityV4, DraftErrorV1, DraftV1, EvidenceBoundsErrorV1, - EvidenceViewV1, InstantiateErrorV1, JointChoiceV1, JointOrderErrorV1, JointStateV1, - NumericDomainErrorV1, ObservationHeadV1, OccurrenceIdV1, OpacityInputIdV1, OutputSlotIdV1, - OwnerV1, PaintIdV1, PaintValueV1, PhysicalPointV1, PresentationRootIdV1, ScenarioV1, SessionV1, - SignalV1, SourceIdV1, StateKindV1, SurfaceIdV1, SurfaceInputPortIdV1, SurroundV1, - TargetCandidateIdV1, TargetCandidateV1, TargetIdV1, UpdateErrorKindV1, UpdateErrorV1, UpdateV1, - VerdictV1, + ConstraintSubjectV1, ContentIdentityV5, DraftErrorV1, DraftV1, EvidenceBoundsErrorV1, + EvidenceViewV1, FinitePaintDomainV1, InstantiateErrorV1, JointChoiceV1, JointOrderErrorV1, + JointStateV1, NumericDomainErrorV1, ObservationHeadV1, OccurrenceIdV1, OpacityInputIdV1, + OutputSlotIdV1, OwnerV1, PaintIdV1, PaintValueV1, PhysicalPointV1, PresentationRootIdV1, + ScenarioV1, SessionV1, SignalV1, SourceIdV1, StateKindV1, SurfaceIdV1, SurfaceInputPortIdV1, + SurroundV1, TargetCandidateIdV1, TargetCandidateV1, TargetIdV1, UpdateErrorKindV1, + UpdateErrorV1, UpdateV1, VerdictV1, }; use crate::wcag22::Wcag22CriterionV1; @@ -44,6 +44,10 @@ fn exact_size(iterator: I) -> I { iterator } +fn finite_domain(candidates: Vec) -> FinitePaintDomainV1 { + FinitePaintDomainV1::try_new(candidates).unwrap() +} + fn compile_error(draft: DraftV1) -> CompileErrorV1 { match draft.compile() { Ok(_) => panic!("the invalid authored program must not compile"), @@ -422,7 +426,6 @@ fn attach_target_assessment(draft: &mut DraftV1, target: TargetIdV1) { } fn joint_draft(hard: bool) -> DraftV1 { - let source = SourceIdV1::new(1); let target = TargetIdV1::new(2); let black = TargetCandidateIdV1::new(3); let white = TargetCandidateIdV1::new(4); @@ -435,14 +438,12 @@ fn joint_draft(hard: bool) -> DraftV1 { let context = AppearanceContextV1::try_new(64.0, 0.2, SurroundV1::Average).unwrap(); let mut draft = DraftV1::new(); - draft.push_source(source, Srgb8::new([0; 3])); draft.push_finite_target( target, - source, - vec![ + finite_domain(vec![ TargetCandidateV1::new(black, PaintValueV1::opaque(Srgb8::new([0; 3]))), TargetCandidateV1::new(white, PaintValueV1::opaque(Srgb8::new([255; 3]))), - ], + ]), ); draft .set_joint_selection(vec![ @@ -465,7 +466,6 @@ fn joint_draft(hard: bool) -> DraftV1 { #[test] fn finite_paint_candidates_are_not_a_cartesian_source_opacity_domain() { - let source = SourceIdV1::new(1); let target = TargetIdV1::new(2); let translucent_white = TargetCandidateIdV1::new(3); let opaque_gray = TargetCandidateIdV1::new(4); @@ -478,11 +478,9 @@ fn finite_paint_candidates_are_not_a_cartesian_source_opacity_domain() { let context = AppearanceContextV1::try_new(64.0, 0.2, SurroundV1::Average).unwrap(); let mut draft = DraftV1::new(); - draft.push_source(source, Srgb8::new([0; 3])); draft.push_finite_target( target, - source, - vec![ + finite_domain(vec![ TargetCandidateV1::new( translucent_white, PaintValueV1::try_new(Srgb8::new([0xFF; 3]), 0.25).unwrap(), @@ -491,7 +489,7 @@ fn finite_paint_candidates_are_not_a_cartesian_source_opacity_domain() { opaque_gray, PaintValueV1::try_new(Srgb8::new([0x40; 3]), 1.0).unwrap(), ), - ], + ]), ); draft .set_joint_selection(vec![ @@ -735,11 +733,10 @@ fn staged_authoring_lowers_the_actual_closed_program_and_returns_canonical_input draft.push_source(source, Srgb8::new([0x80; 3])); draft.push_finite_target( target, - source, - vec![ + finite_domain(vec![ TargetCandidateV1::new(gray, PaintValueV1::opaque(Srgb8::new([0x80; 3]))), TargetCandidateV1::new(black, PaintValueV1::opaque(Srgb8::new([0; 3]))), - ], + ]), ); draft .set_joint_selection(vec![ @@ -994,7 +991,7 @@ fn owner_and_update_errors_preserve_content_and_input_identity() { let owner = fixed_nested_draft(1.0, SourceIdV1::new(1), input, input) .compile() .unwrap(); - let owner_identity: ContentIdentityV4 = owner.content_identity(); + let owner_identity: ContentIdentityV5 = owner.content_identity(); let mut session = owner.instantiate(13).unwrap(); let no_scenarios = []; @@ -1214,7 +1211,7 @@ fn relational_compile_errors_keep_both_typed_handles() { Ok(_) => panic!("a target cannot reference an undeclared source"), Err(error) => error, }; - assert_eq!(error.kind(), CompileErrorKindV1::MissingTargetSource); + assert_eq!(error.kind(), CompileErrorKindV1::MissingFixedSource); assert_eq!( error.primary_handle(), Some(CompileErrorHandleV1::Target(TargetIdV1::new(2))) @@ -1299,11 +1296,10 @@ fn duplicate_candidate_value_preserves_both_candidates_and_exact_stimulus() { let mut draft = fixed_nested_draft(1.0, SourceIdV1::new(1), input, input); draft.push_finite_target( target, - SourceIdV1::new(1), - vec![ + finite_domain(vec![ TargetCandidateV1::new(first, PaintValueV1::opaque(encoded_srgb8)), TargetCandidateV1::new(duplicate, PaintValueV1::opaque(encoded_srgb8)), - ], + ]), ); attach_target_assessment(&mut draft, target); @@ -1330,7 +1326,7 @@ fn joint_diagnostics_preserve_state_and_total_order_details() { ]; let mut duplicate_target = fixed_nested_draft(1.0, SourceIdV1::new(1), input, input); - duplicate_target.push_finite_target(target, SourceIdV1::new(1), candidates.clone()); + duplicate_target.push_finite_target(target, finite_domain(candidates.clone())); attach_target_assessment(&mut duplicate_target, target); duplicate_target .set_joint_selection(vec![JointStateV1::new(vec![ @@ -1344,7 +1340,7 @@ fn joint_diagnostics_preserve_state_and_total_order_details() { ); let mut incomplete = fixed_nested_draft(1.0, SourceIdV1::new(1), input, input); - incomplete.push_finite_target(target, SourceIdV1::new(1), candidates); + incomplete.push_finite_target(target, finite_domain(candidates)); attach_target_assessment(&mut incomplete, target); incomplete .set_joint_selection(vec![JointStateV1::new(vec![JointChoiceV1::new( diff --git a/crates/labcolors-core/src/program_clean_set_tests.rs b/crates/labcolors-core/src/program_clean_set_tests.rs index cfa78420..758507e6 100644 --- a/crates/labcolors-core/src/program_clean_set_tests.rs +++ b/crates/labcolors-core/src/program_clean_set_tests.rs @@ -13,6 +13,10 @@ const CONSTRAINT: program::ConstraintIdV1 = program::ConstraintIdV1::new(7); const OUTPUT: program::OutputSlotIdV1 = program::OutputSlotIdV1::new(8); const ROOT: program::PresentationRootIdV1 = program::PresentationRootIdV1::new(9); +fn finite_domain(candidates: Vec) -> program::FinitePaintDomainV1 { + program::FinitePaintDomainV1::try_new(candidates).unwrap() +} + fn context() -> program::AppearanceContextV1 { program::AppearanceContextV1::try_new(64.0, 0.2, program::SurroundV1::Average).unwrap() } @@ -148,11 +152,10 @@ fn finite_search_skips_dirty_and_freshly_rechecks_the_first_clean_state() { draft.push_source(SOURCE, dirty); draft.push_finite_target( TARGET, - SOURCE, - vec![ + finite_domain(vec![ program::TargetCandidateV1::new(dirty_id, program::PaintValueV1::opaque(dirty)), program::TargetCandidateV1::new(clean_id, program::PaintValueV1::opaque(clean)), - ], + ]), ); draft .set_joint_selection(vec![ @@ -383,11 +386,10 @@ fn clean_family_fresh_recheck_failure_retains_the_presentation_subject() { draft.push_source(SOURCE, clean); draft.push_finite_target( TARGET, - SOURCE, - vec![program::TargetCandidateV1::new( + finite_domain(vec![program::TargetCandidateV1::new( candidate, program::PaintValueV1::opaque(clean), - )], + )]), ); draft .set_joint_selection(vec![program::JointStateV1::new(vec![ @@ -442,7 +444,7 @@ fn clean_family_fresh_recheck_failure_retains_the_presentation_subject() { ); } -fn opaque_named_clean_identity(name: u32) -> program::ContentIdentityV4 { +fn opaque_named_clean_identity(name: u32) -> program::ContentIdentityV5 { let source = program::SourceIdV1::new(name); let target = program::TargetIdV1::new(name); let port = program::SurfaceInputPortIdV1::new(name); @@ -520,7 +522,7 @@ fn finite_clean_owner(colors: &[Srgb8]) -> program::OwnerV1 { .collect::>(); let mut draft = program::DraftV1::new(); draft.push_source(SOURCE, colors[0]); - draft.push_finite_target(TARGET, SOURCE, candidates); + draft.push_finite_target(TARGET, finite_domain(candidates)); draft.set_joint_selection(states).unwrap(); draft.push_surface_input_port(PORT); draft.push_solid_paint(PAINT, TARGET); diff --git a/crates/labcolors-core/src/program_identity.rs b/crates/labcolors-core/src/program_identity.rs index 8da2c024..efef885e 100644 --- a/crates/labcolors-core/src/program_identity.rs +++ b/crates/labcolors-core/src/program_identity.rs @@ -7,15 +7,15 @@ use super::*; -const DOMAIN_V4: &[u8] = b"labcolors.program-content-identity.v4\0"; -// Максимальный V4-цвет принадлежит ограничению clean-set: тег вершины, +const DOMAIN_V5: &[u8] = b"labcolors.program-content-identity.v5\0"; +// Максимальный V5-цвет принадлежит ограничению clean-set: тег вершины, // семейство и полный дайджест выпуска. Явная граница устраняет аллокацию на // каждую вершину и требует пересмотра при расширении схемы вместо скрытого // лимита времени исполнения. const COLOR_CAPACITY: usize = 1 + 1 + 32; mod release_tag { - pub(super) const PROGRAM_SCHEMA_V4: u8 = 4; + pub(super) const PROGRAM_SCHEMA_V5: u8 = 5; 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; @@ -66,14 +66,14 @@ mod release_tag { pub(super) const MODELED_LCS_PROBE_FAMILY_V1: u8 = 4; } -/// Устойчивый к коллизиям адрес канонизированного содержимого Program V4. +/// Устойчивый к коллизиям адрес канонизированного содержимого Program V5. /// /// SHA-256 не делает адрес инъективным. Адрес не связывает пространства opaque /// ID и не подтверждает владельца, поколение либо revision. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub(crate) struct ProgramContentIdentityV4([u8; 32]); +pub(crate) struct ProgramContentIdentityV5([u8; 32]); -impl ProgramContentIdentityV4 { +impl ProgramContentIdentityV5 { pub(crate) const fn as_bytes(&self) -> &[u8; 32] { &self.0 } @@ -338,7 +338,7 @@ fn program_root_color() -> Result { // производных возможностей связываются только с теми ограничениями, // которые их исполняют. for release in [ - release_tag::PROGRAM_SCHEMA_V4, + release_tag::PROGRAM_SCHEMA_V5, release_tag::DECLARED_TOTAL_ORDER_V1, release_tag::FRESH_FULL_RECHECK_V1, release_tag::ATOMIC_OBSERVATION_GROUP_V1, @@ -653,14 +653,14 @@ where 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_color = match target.intent() { + TargetIntentV1::FixedSource(_) => VertexColorV1::new(vertex_tag::TARGET_FIXED), + TargetIntentV1::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 { + if let TargetIntentV1::Finite(domain) = target.intent() { + for candidate in domain.candidates() { let vertex = graph.add_member(candidate_color(*candidate)?)?; candidates.insert((target.id(), candidate.id()), vertex)?; } @@ -726,13 +726,15 @@ where 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 { + if let TargetIntentV1::FixedSource(source) = target.intent() { + graph.add_edge( + target_vertex, + sources.get(*source)?, + EdgeRoleV1::TargetSource, + )?; + } + if let TargetIntentV1::Finite(domain) = target.intent() { + for candidate in domain.candidates() { graph.add_edge( target_vertex, candidates.get((target.id(), candidate.id()))?, @@ -1135,7 +1137,7 @@ fn serialize_leaf( .and_then(|value| value.checked_add(color.as_slice().len())) .ok_or(ProgramCompileError::ResourceExhausted) })?; - let capacity = DOMAIN_V4 + let capacity = DOMAIN_V5 .len() .checked_add(16) .and_then(|value| value.checked_add(color_bytes)) @@ -1145,7 +1147,7 @@ fn serialize_leaf( output .try_reserve_exact(capacity) .map_err(|_| ProgramCompileError::ResourceExhausted)?; - output.extend_from_slice(DOMAIN_V4); + output.extend_from_slice(DOMAIN_V5); push_u64_bytes(&mut output, usize_as_u64(graph.colors.len())?); push_u64_bytes(&mut output, usize_as_u64(graph.edge_count)?); @@ -1479,9 +1481,9 @@ fn canonical_preimage(graph: &CanonicalGraphV1) -> Result, ProgramCompil canonical_search(graph).map(|(preimage, _)| preimage) } -pub(super) fn compile_program_content_identity_v4( +pub(super) fn compile_program_content_identity_v5( program: &Program, -) -> Result +) -> Result where Evaluation: ProgramConstraintEvaluatorSetV1, ProgramConstraintInvocationOf: Copy, @@ -1489,7 +1491,7 @@ where let graph = build_graph(program)?; let preimage = canonical_preimage(&graph)?; let digest = crate::sha256::digest(&preimage); - Ok(ProgramContentIdentityV4(*digest.as_bytes())) + Ok(ProgramContentIdentityV5(*digest.as_bytes())) } #[cfg(test)] diff --git a/crates/labcolors-core/src/program_identity_tests.rs b/crates/labcolors-core/src/program_identity_tests.rs index cf76006c..8f007c92 100644 --- a/crates/labcolors-core/src/program_identity_tests.rs +++ b/crates/labcolors-core/src/program_identity_tests.rs @@ -11,10 +11,11 @@ use crate::observation::ObservationGroupId; use crate::program_session::{ CompositionProfile, ConstraintId, ConstraintInvocation, ConstraintSet, CoreProgramConstraintInvocationV1, CoreProgramEvaluatorsV1, CoreProgramV1, - DeclaredJointSelectionV1, JointCandidateStateV1, ObservationGroup, Occurrence, OpacityInput, - OutputBinding, OutputSlotId, Paint, PointPresentationRootV1, PointPresentationTargetV1, - PresentationRootId, Program, ProgramContentIdentityV4, Source, SourceId, Surface, Target, - TargetCandidateChoiceV1, TargetCandidateId, TargetCandidateV1, TargetId, + DeclaredJointSelectionV1, FinitePaintDomainV1, JointCandidateStateV1, ObservationGroup, + Occurrence, OpacityInput, OutputBinding, OutputSlotId, Paint, PointPresentationRootV1, + PointPresentationTargetV1, PresentationRootId, Program, ProgramContentIdentityV5, Source, + SourceId, Surface, Target, TargetCandidateChoiceV1, TargetCandidateId, TargetCandidateV1, + TargetId, }; use crate::wcag22::Wcag22CriterionV1; @@ -33,6 +34,10 @@ fn paint_value(signal: ColorSignal, opacity: f64) -> EncodedPointPaintValueV1 { ) } +fn finite_domain(candidates: Vec) -> FinitePaintDomainV1 { + FinitePaintDomainV1::try_new(candidates).unwrap() +} + fn context(surround: SurroundProfileId) -> AppearanceContextId { AppearanceContextId::from_inputs( AppearanceContextSchemaReleaseId::Ciecam16ViewingInputsV1, @@ -268,7 +273,7 @@ fn presentation_topology_ignores_root_names_and_declaration_order() { } #[test] -fn presentation_root_terminal_relation_changes_v4_identity() { +fn presentation_root_terminal_relation_changes_v5_identity() { let ids = canonical_full_ids(); let root = PresentationRootId::new(1); let first = full_program(ids, false, FullMutation::None) @@ -289,7 +294,7 @@ fn presentation_root_terminal_relation_changes_v4_identity() { } #[test] -fn presentation_target_relation_and_multiplicity_change_v4_identity() { +fn presentation_target_relation_and_multiplicity_change_v5_identity() { let ids = canonical_full_ids(); let root = PresentationRootId::new(1); let nested_target = full_program(ids, false, FullMutation::None) @@ -331,7 +336,7 @@ fn presentation_target_relation_and_multiplicity_change_v4_identity() { } #[test] -fn canonical_v4_digest_is_cross_platform_golden() { +fn canonical_v5_digest_is_cross_platform_golden() { let ids = FixedIds { sources: [SourceId::new(10), SourceId::new(20)], targets: [TargetId::new(30), TargetId::new(40)], @@ -352,12 +357,12 @@ fn canonical_v4_digest_is_cross_platform_golden() { .compile() .unwrap(); - let identity: ProgramContentIdentityV4 = compiled.content_identity(); + let identity: ProgramContentIdentityV5 = compiled.content_identity(); assert_eq!( identity.as_bytes(), &[ - 132, 20, 224, 27, 199, 182, 194, 5, 234, 7, 128, 37, 116, 110, 26, 26, 17, 145, 90, - 151, 250, 35, 207, 226, 129, 68, 73, 248, 148, 242, 11, 63, + 238, 31, 74, 246, 76, 225, 22, 249, 250, 182, 134, 250, 248, 170, 128, 78, 215, 239, + 212, 220, 176, 186, 208, 196, 83, 238, 38, 240, 187, 77, 186, 122, ] ); } @@ -469,7 +474,7 @@ fn full_program(ids: FullIds, reverse_unordered: bool, mutation: FullMutation) - if reverse_unordered { candidates.reverse(); } - Target::finite(ids.targets[target], ids.sources[target], candidates) + Target::finite(ids.targets[target], finite_domain(candidates)) }) .collect::>(); let mut paints = vec![ @@ -714,8 +719,8 @@ fn canonical_full_ids() -> FullIds { } #[test] -fn complete_program_schema_v4_digest_is_cross_platform_golden() { - // Вместе с fixed golden этот Program содержит каждый V4 vertex/edge tag, +fn complete_program_schema_v5_digest_is_cross_platform_golden() { + // Вместе с fixed golden этот Program содержит каждый V5 vertex/edge tag, // обе constraint families и оба режима. Случайная смена кодировки требует // явной смены версии, а не тихого перевыпуска прежнего content address. let compiled = full_program( @@ -729,8 +734,8 @@ fn complete_program_schema_v4_digest_is_cross_platform_golden() { assert_eq!( compiled.content_identity().as_bytes(), &[ - 172, 144, 124, 212, 139, 155, 130, 237, 103, 57, 71, 71, 86, 246, 26, 129, 122, 66, - 180, 212, 177, 227, 117, 231, 82, 243, 135, 176, 3, 129, 250, 87, + 96, 211, 110, 208, 129, 129, 17, 133, 60, 202, 30, 74, 62, 176, 156, 216, 200, 37, 22, + 103, 116, 138, 153, 152, 50, 46, 193, 238, 135, 21, 22, 38, ] ); } @@ -1147,11 +1152,10 @@ fn finite_program_with_opacity(reverse_order: bool, first_opacity: f64) -> CoreP vec![Source::new(source, signal([0; 3]))], vec![Target::finite( target, - source, - vec![ + finite_domain(vec![ TargetCandidateV1::new(first, paint_value(signal([0; 3]), first_opacity)), TargetCandidateV1::new(second, opaque_value(signal([0xFF; 3]))), - ], + ]), )], ObservationGroup::new(ObservationGroupId::new(9), vec![port]), vec![], @@ -1185,6 +1189,76 @@ fn finite_program(reverse_order: bool) -> CoreProgramV1 { finite_program_with_opacity(reverse_order, 1.0) } +fn finite_program_with_swapped_unbound_sources(swap_source_signals: bool) -> CoreProgramV1 { + let sources = [SourceId::new(1), SourceId::new(2)]; + let target = TargetId::new(3); + let first = TargetCandidateId::new(4); + let second = TargetCandidateId::new(5); + let paint = PaintId::new(6); + let port = SurfaceInputPortId::new(7); + let surface = SurfaceId::new(8); + let occurrence = OccurrenceId::new(9); + + let mut signals = [signal([0x12, 0x34, 0x56]), signal([0xAB, 0xCD, 0xEF])]; + if swap_source_signals { + signals.reverse(); + } + + Program::new( + vec![ + Source::new(sources[0], signals[0]), + Source::new(sources[1], signals[1]), + ], + vec![Target::finite( + target, + finite_domain(vec![ + TargetCandidateV1::new(first, opaque_value(signal([0; 3]))), + TargetCandidateV1::new(second, opaque_value(signal([0xFF; 3]))), + ]), + )], + ObservationGroup::new(ObservationGroupId::new(10), 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(11), + occurrence, + CoreProgramConstraintInvocationV1::ExactSrgb8(Srgb8::new([0; 3])), + )], + vec![], + ), + vec![OutputBinding::new(OutputSlotId::new(12), paint)], + CoreProgramEvaluatorsV1, + ) + .with_joint_selection(DeclaredJointSelectionV1::new(vec![ + JointCandidateStateV1::new(vec![TargetCandidateChoiceV1::new(target, first)]), + JointCandidateStateV1::new(vec![TargetCandidateChoiceV1::new(target, second)]), + ])) +} + +#[test] +fn finite_target_dead_source_cannot_change_physical_content_identity() { + let first = finite_program_with_swapped_unbound_sources(false) + .compile() + .unwrap(); + let second = finite_program_with_swapped_unbound_sources(true) + .compile() + .unwrap(); + + assert_eq!(first.content_identity(), second.content_identity()); +} + fn fixed_single_target_program() -> CoreProgramV1 { let source = SourceId::new(1); let target = TargetId::new(2); @@ -1240,7 +1314,7 @@ fn content_identity_retains_the_explicit_joint_state_order() { } #[test] -fn finite_candidate_opacity_is_part_of_v4_content_identity() { +fn finite_candidate_opacity_is_part_of_v5_content_identity() { let quarter = finite_program_with_opacity(false, 0.25).compile().unwrap(); let half = finite_program_with_opacity(false, 0.5).compile().unwrap(); diff --git a/crates/labcolors-core/src/program_joint_integration_tests.rs b/crates/labcolors-core/src/program_joint_integration_tests.rs index a28b088d..2ea38074 100644 --- a/crates/labcolors-core/src/program_joint_integration_tests.rs +++ b/crates/labcolors-core/src/program_joint_integration_tests.rs @@ -22,12 +22,13 @@ use crate::observation::{ }; use crate::program_session::{ CompositionProfile, ConstraintId, ConstraintInvocation, ConstraintSet, - DeclaredJointSelectionV1, HardModeV1, JointCandidateStateV1, ObservationGroup, Occurrence, - OpacityInput, OutputBinding, OutputSlotId, Paint, Program, ProgramCompileError, - ProgramConstraintEvaluatorSetV1, ProgramConstraintSubjectV1, ProgramSessionEvaluationError, - ReportModeV1, Source, SourceId, Surface, Target, TargetCandidateChoiceV1, TargetCandidateId, - TargetCandidateV1, TargetDomainV1, TargetId, checked_program_evaluation_cell_counts_for_test, - fail_program_preflight_reservation_for_test, program_preflight_failure_remaining_for_test, + DeclaredJointSelectionV1, FinitePaintDomainV1, HardModeV1, JointCandidateStateV1, + ObservationGroup, Occurrence, OpacityInput, OutputBinding, OutputSlotId, Paint, Program, + ProgramCompileError, ProgramConstraintEvaluatorSetV1, ProgramConstraintSubjectV1, + ProgramSessionEvaluationError, ReportModeV1, Source, SourceId, Surface, Target, + TargetCandidateChoiceV1, TargetCandidateId, TargetCandidateV1, TargetId, TargetIntentV1, + checked_program_evaluation_cell_counts_for_test, fail_program_preflight_reservation_for_test, + program_preflight_failure_remaining_for_test, }; use crate::session::{SessionState, SessionUpdateError}; use crate::session_tests::CommitSessionUpdateForTest as _; @@ -389,8 +390,12 @@ fn state(candidate: TargetCandidateId) -> JointCandidateStateV1 { JointCandidateStateV1::new(vec![TargetCandidateChoiceV1::new(TARGET, candidate)]) } +fn finite_domain(candidates: Vec) -> FinitePaintDomainV1 { + FinitePaintDomainV1::try_new(candidates).unwrap() +} + fn target(candidates: Vec) -> Target { - Target::finite(TARGET, SOURCE, candidates) + Target::finite(TARGET, finite_domain(candidates)) } fn point_program( @@ -499,13 +504,14 @@ fn nested_two_target_program( ) -> Program { let lower = Target::finite( TARGET, - SOURCE, - vec![candidate(FIRST, 0x00), candidate(SECOND, 0xFF)], + finite_domain(vec![candidate(FIRST, 0x00), candidate(SECOND, 0xFF)]), ); let upper = Target::finite( UPPER_TARGET, - UPPER_SOURCE, - vec![candidate(UPPER_FIRST, 0x55), candidate(UPPER_SECOND, 0xFF)], + finite_domain(vec![ + candidate(UPPER_FIRST, 0x55), + candidate(UPPER_SECOND, 0xFF), + ]), ); let mut targets = vec![lower, upper]; if reverse_targets { @@ -612,8 +618,8 @@ fn alpha_renamed_nested_program( upper_candidates.reverse(); } let mut targets = vec![ - Target::finite(ids.lower_target, ids.lower_source, lower_candidates), - Target::finite(ids.upper_target, ids.upper_source, upper_candidates), + Target::finite(ids.lower_target, finite_domain(lower_candidates)), + Target::finite(ids.upper_target, finite_domain(upper_candidates)), ]; if permute_declarations { targets.reverse(); @@ -703,11 +709,10 @@ fn authored_finite_target_values_keep_only_opaque_identity_and_explicit_policy() let target = target(vec![first]); assert_eq!(target.id(), TARGET); - assert_eq!(target.source(), SOURCE); - let TargetDomainV1::Finite(candidates) = target.domain() else { + let TargetIntentV1::Finite(domain) = target.intent() else { panic!("target must retain its explicit finite domain"); }; - assert_eq!(candidates, &[first]); + assert_eq!(domain.candidates(), &[first]); let choice = TargetCandidateChoiceV1::new(TARGET, FIRST); assert_eq!(choice.target(), TARGET); @@ -903,7 +908,7 @@ fn terminal_safety_rejects_an_unconstrained_finite_target() { }; assert_eq!( error, - ProgramCompileError::UnconstrainedTarget { target: TARGET } + ProgramCompileError::UnconstrainedFiniteTarget { target: TARGET } ); } @@ -918,8 +923,10 @@ fn independent_finite_target_components_are_rejected_before_global_product_searc target(vec![candidate(FIRST, 0), candidate(SECOND, 0xFF)]), Target::finite( UPPER_TARGET, - UPPER_SOURCE, - vec![candidate(UPPER_FIRST, 0x55), candidate(UPPER_SECOND, 0xFF)], + finite_domain(vec![ + candidate(UPPER_FIRST, 0x55), + candidate(UPPER_SECOND, 0xFF), + ]), ), ], ObservationGroup::new(GROUP, vec![SURFACE_PORT]), diff --git a/crates/labcolors-core/src/program_mixed_evaluator_tests.rs b/crates/labcolors-core/src/program_mixed_evaluator_tests.rs index ce8f68f9..b971e318 100644 --- a/crates/labcolors-core/src/program_mixed_evaluator_tests.rs +++ b/crates/labcolors-core/src/program_mixed_evaluator_tests.rs @@ -33,9 +33,9 @@ use crate::program_session::{ CORE_PROGRAM_ASSESSMENT_CALLS, CompiledCoreProgramV1, CompositionProfile, ConstraintId, ConstraintInvocation, ConstraintSet, CoreProgramConstraintInvocationV1, CoreProgramEvaluatorsV1, CoreProgramPassEvidenceV1, CoreProgramV1, - CoreProgramViolationEvidenceV1, DeclaredJointSelectionV1, JointCandidateStateV1, - ObservationGroup, Occurrence, OpacityInput, OutputBinding, OutputSlotId, Paint, - PointPresentationRootV1, PointPresentationTargetV1, PresentationRootId, Program, + CoreProgramViolationEvidenceV1, DeclaredJointSelectionV1, FinitePaintDomainV1, + JointCandidateStateV1, ObservationGroup, Occurrence, OpacityInput, OutputBinding, OutputSlotId, + Paint, PointPresentationRootV1, PointPresentationTargetV1, PresentationRootId, Program, ProgramConstraintCellV1, ProgramConstraintPassEvidenceV1, ProgramConstraintResultV1, ProgramConstraintSubjectV1, ProgramConstraintViolationEvidenceV1, Source, SourceId, Surface, Target, TargetCandidateChoiceV1, TargetCandidateId, TargetCandidateV1, TargetId, @@ -63,6 +63,10 @@ fn signal(bytes: [u8; 3]) -> ColorSignal { ColorSignal::from_srgb8(Srgb8::new(bytes)) } +fn finite_domain(candidates: Vec) -> FinitePaintDomainV1 { + FinitePaintDomainV1::try_new(candidates).unwrap() +} + fn context() -> AppearanceContextId { AppearanceContextId::from_inputs( AppearanceContextSchemaReleaseId::Ciecam16ViewingInputsV1, @@ -117,8 +121,7 @@ fn finite_program_with_outputs( vec![Source::new(SOURCE, signal(candidate_signals[0]))], vec![Target::finite( TARGET, - SOURCE, - vec![ + finite_domain(vec![ TargetCandidateV1::new( FIRST, EncodedPointPaintValueV1::opaque(Srgb8::new(candidate_signals[0])), @@ -127,7 +130,7 @@ fn finite_program_with_outputs( SECOND, EncodedPointPaintValueV1::opaque(Srgb8::new(candidate_signals[1])), ), - ], + ]), )], ObservationGroup::new(GROUP, vec![SURFACE_PORT]), vec![], diff --git a/crates/labcolors-core/src/program_point_causality_tests.rs b/crates/labcolors-core/src/program_point_causality_tests.rs index 7109047d..4ffb606b 100644 --- a/crates/labcolors-core/src/program_point_causality_tests.rs +++ b/crates/labcolors-core/src/program_point_causality_tests.rs @@ -15,9 +15,9 @@ use crate::observation::{ }; use crate::program_session::{ CompositionProfile, ConstraintId, ConstraintInvocation, ConstraintSet, - DeclaredJointSelectionV1, JointCandidateStateV1, ObservationGroup, Occurrence, OpacityInput, - OutputBinding, OutputSlotId, Paint, PointPresentationRootV1, PointPresentationTargetV1, - PresentationRootId, Program, ProgramPointCausalConsideredStateV1, + DeclaredJointSelectionV1, FinitePaintDomainV1, JointCandidateStateV1, ObservationGroup, + Occurrence, OpacityInput, OutputBinding, OutputSlotId, Paint, PointPresentationRootV1, + PointPresentationTargetV1, PresentationRootId, Program, ProgramPointCausalConsideredStateV1, ProgramPointCausalSelectedStateV1, ProgramSessionEvaluationError, Source, SourceId, Surface, Target, TargetCandidateChoiceV1, TargetCandidateId, TargetCandidateV1, TargetId, checked_program_point_causal_cardinality_for_test, fail_program_preflight_reservation_for_test, @@ -53,6 +53,10 @@ fn context() -> AppearanceContextId { ) } +fn finite_domain(candidates: Vec) -> FinitePaintDomainV1 { + FinitePaintDomainV1::try_new(candidates).unwrap() +} + fn preflight_program( evaluator: CountingProgramWcag22Srgb8V1, ) -> crate::program_session::CompiledProgram { @@ -134,8 +138,7 @@ fn joint_preflight_program( vec![Source::new(SOURCE, signal([0xEE; 3]))], vec![Target::finite( TARGET, - SOURCE, - vec![ + finite_domain(vec![ TargetCandidateV1::new( lower, EncodedPointPaintValueV1::opaque(Srgb8::new([0xEE; 3])), @@ -144,7 +147,7 @@ fn joint_preflight_program( higher, EncodedPointPaintValueV1::opaque(Srgb8::new([0xFF; 3])), ), - ], + ]), )], ObservationGroup::new(GROUP, vec![PORT]), vec![], @@ -359,14 +362,13 @@ fn finite_fanout_program() -> crate::program_session::CompiledProgram), +pub struct FinitePaintDomainV1(Vec); + +impl FinitePaintDomainV1 { + pub fn try_new( + candidates: Vec, + ) -> Result { + if candidates.is_empty() { + return Err(FinitePaintDomainAdmissionErrorV1::Empty); + } + Ok(Self(candidates)) + } + + pub fn candidates(&self) -> &[TargetCandidateV1] { + &self.0 + } + + fn candidates_mut(&mut self) -> &mut [TargetCandidateV1] { + &mut self.0 + } +} + +/// Closed authored intent of one Target. Fixed targets reference immutable +/// source data; finite targets own all of their physical freedom directly. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TargetIntentV1 { + FixedSource(SourceId), + Finite(FinitePaintDomainV1), } /// A Paint-addressable target distinct from both source data and appearance @@ -167,37 +199,28 @@ pub enum TargetDomainV1 { #[derive(Debug, Clone, PartialEq, Eq)] pub struct Target { id: TargetId, - source: SourceId, - domain: TargetDomainV1, + intent: TargetIntentV1, } impl Target { - pub const fn new(id: TargetId, source: SourceId, domain: TargetDomainV1) -> Self { - Self { id, source, domain } + pub const fn new(id: TargetId, intent: TargetIntentV1) -> Self { + Self { id, intent } } pub const fn fixed(id: TargetId, source: SourceId) -> Self { - Self::new(id, source, TargetDomainV1::Fixed) + Self::new(id, TargetIntentV1::FixedSource(source)) } - pub const fn finite( - id: TargetId, - source: SourceId, - candidates: Vec, - ) -> Self { - Self::new(id, source, TargetDomainV1::Finite(candidates)) + pub const fn finite(id: TargetId, domain: FinitePaintDomainV1) -> Self { + Self::new(id, TargetIntentV1::Finite(domain)) } pub const fn id(&self) -> TargetId { self.id } - pub const fn source(&self) -> SourceId { - self.source - } - - pub const fn domain(&self) -> &TargetDomainV1 { - &self.domain + pub const fn intent(&self) -> &TargetIntentV1 { + &self.intent } } @@ -1061,7 +1084,7 @@ pub enum ProgramCompileError { DuplicateTarget { target: TargetId, }, - MissingTargetSource { + MissingFixedSource { target: TargetId, source: SourceId, }, @@ -1156,9 +1179,6 @@ pub enum ProgramCompileError { OpacityOutOfDomain { input: OpacityInputId, }, - EmptyTargetDomain { - target: TargetId, - }, DuplicateTargetCandidate { target: TargetId, candidate: TargetCandidateId, @@ -1169,7 +1189,7 @@ pub enum ProgramCompileError { duplicate: TargetCandidateId, value: EncodedPointPaintValueV1, }, - UnconstrainedTarget { + UnconstrainedFiniteTarget { target: TargetId, }, DisconnectedFiniteTargets, @@ -1477,7 +1497,7 @@ where Evaluation: ProgramConstraintEvaluatorSetV1, ProgramConstraintInvocationOf: Copy, { - content_identity: ProgramContentIdentityV4, + content_identity: ProgramContentIdentityV5, evaluator: Evaluation, graph: CompiledAppearanceGraph, binding_template: AdmittedAppearanceBindings, @@ -1525,7 +1545,7 @@ where /// Opaque ID и порядок неупорядоченных объявлений исключены; явный joint /// order входит в адрес. Адрес не подтверждает поколение владельца и не /// заменяет revision-bound evidence. - pub fn content_identity(&self) -> ProgramContentIdentityV4 { + pub fn content_identity(&self) -> ProgramContentIdentityV5 { self.owner_generation.content_identity } @@ -2001,7 +2021,7 @@ pub(crate) enum ProgramPointCausalConsideredStateV1 { /// одного моделируемого terminal root. Оно ничего не утверждает о пикселе /// браузера, восприятии или качестве цвета. pub(crate) struct ProgramPointCausalEvidenceV1<'report, State> { - content_identity: ProgramContentIdentityV4, + content_identity: ProgramContentIdentityV5, observation: &'report RevisionBoundObservationV1, record: &'report ProgramPointCausalRecordV1, steps: &'report [PointOccurrenceAbsenceStepV1], @@ -2022,7 +2042,7 @@ where .unwrap_or_else(|| unreachable!("тип replay span запрещает пустой пересчёт")) } - pub(crate) const fn content_identity(&self) -> ProgramContentIdentityV4 { + pub(crate) const fn content_identity(&self) -> ProgramContentIdentityV5 { self.content_identity } @@ -2080,7 +2100,7 @@ pub struct ProgramReportV1 where Evaluation: ProgramConstraintEvaluatorSetV1, { - content_identity: ProgramContentIdentityV4, + content_identity: ProgramContentIdentityV5, observation: RevisionBoundObservationV1, arena: ProgramEvaluationArenaLeaseV1, } @@ -2091,7 +2111,7 @@ where { /// Адрес содержимого Program, по которому построен report; это не /// идентификатор поколения и не runtime-authority. - pub const fn content_identity(&self) -> ProgramContentIdentityV4 { + pub const fn content_identity(&self) -> ProgramContentIdentityV5 { self.content_identity } @@ -3721,7 +3741,7 @@ where let occurrence_contexts = compact_constraint_contexts(&all_occurrence_contexts, &mut constraints)?; let outputs = compile_outputs(&graph, &mut program.outputs)?; - let content_identity = identity::compile_program_content_identity_v4(&program)?; + let content_identity = identity::compile_program_content_identity_v5(&program)?; Ok(ProgramEpochV1 { content_identity, evaluator: program.evaluator, @@ -3768,14 +3788,17 @@ where return Err(ProgramCompileError::DuplicateTarget { target }); } for target in &program.targets { + let TargetIntentV1::FixedSource(source) = target.intent else { + continue; + }; if program .sources - .binary_search_by_key(&target.source, |source| source.id) + .binary_search_by_key(&source, |candidate| candidate.id) .is_err() { - return Err(ProgramCompileError::MissingTargetSource { + return Err(ProgramCompileError::MissingFixedSource { target: target.id, - source: target.source, + source, }); } } @@ -4074,8 +4097,8 @@ where constraints.iter().map(compiled_constraint_dependency_root), )?; for (target_index, target) in program.targets.iter().enumerate() { - if matches!(&target.domain, TargetDomainV1::Finite(_)) && !scratch.targets[target_index] { - return Err(ProgramCompileError::UnconstrainedTarget { target: target.id }); + if matches!(&target.intent, TargetIntentV1::Finite(_)) && !scratch.targets[target_index] { + return Err(ProgramCompileError::UnconstrainedFiniteTarget { target: target.id }); } } for output in &program.outputs { @@ -4093,14 +4116,14 @@ where let finite_count = program .targets .iter() - .filter(|target| matches!(&target.domain, TargetDomainV1::Finite(_))) + .filter(|target| matches!(&target.intent, TargetIntentV1::Finite(_))) .count(); if finite_count > 1 { let mut has_common_assessment = false; for target in constraints.iter().map(compiled_constraint_dependency_root) { scratch.scan(&index, [target])?; if program.targets.iter().enumerate().all(|(index, target)| { - !matches!(&target.domain, TargetDomainV1::Finite(_)) || scratch.targets[index] + !matches!(&target.intent, TargetIntentV1::Finite(_)) || scratch.targets[index] }) { has_common_assessment = true; break; @@ -4157,12 +4180,10 @@ fn compile_targets( .try_reserve_exact(authored_targets.len()) .map_err(|_| ProgramCompileError::ResourceExhausted)?; for target in authored_targets { - let TargetDomainV1::Finite(candidates) = &mut target.domain else { + let TargetIntentV1::Finite(domain) = &mut target.intent else { continue; }; - if candidates.is_empty() { - return Err(ProgramCompileError::EmptyTargetDomain { target: target.id }); - } + let candidates = domain.candidates_mut(); let binding = graph .bind_paint_input(target_paint_input_id(target.id)) .ok_or(ProgramCompileError::InternalInvariant)?; @@ -4807,14 +4828,31 @@ where .try_reserve_exact(program.targets.len()) .map_err(|_| ProgramCompileError::ResourceExhausted)?; for target in &program.targets { - let source_index = program - .sources - .binary_search_by_key(&target.source, |source| source.id) - .map_err(|_| ProgramCompileError::InternalInvariant)?; - paint_inputs.push(( - target_paint_input_id(target.id), - EncodedPointPaintValueV1::opaque(program.sources[source_index].signal.srgb8()), - )); + let value = match &target.intent { + TargetIntentV1::FixedSource(source) => { + let source_index = program + .sources + .binary_search_by_key(source, |candidate| candidate.id) + .map_err(|_| ProgramCompileError::InternalInvariant)?; + EncodedPointPaintValueV1::opaque(program.sources[source_index].signal.srgb8()) + } + TargetIntentV1::Finite(domain) => domain + .candidates() + .iter() + // A canonical admitted value exists only to satisfy the cold + // graph binding. Every search pass overwrites it atomically; + // physical ordering avoids client-ID and declaration-order + // influence even on this non-authoritative seed. + .min_by_key(|candidate| { + ( + candidate.value.source().bytes(), + candidate.value.opacity_bits(), + ) + }) + .map(|candidate| candidate.value) + .ok_or(ProgramCompileError::InternalInvariant)?, + }; + paint_inputs.push((target_paint_input_id(target.id), value)); } let surfaces = try_collect_program( program.observation_group.surface_input_ports.len(), diff --git a/crates/labcolors-core/src/program_session_tests.rs b/crates/labcolors-core/src/program_session_tests.rs index 4030cedb..e6bc8e3d 100644 --- a/crates/labcolors-core/src/program_session_tests.rs +++ b/crates/labcolors-core/src/program_session_tests.rs @@ -15,7 +15,7 @@ use crate::program_session::{ Occurrence, OpacityInput, OutputBinding, OutputSlotId, Paint, PointOutputPresentationBindErrorV1, PointPresentationRootV1, PointPresentationTargetV1, PresentationRootId, Program, ProgramCompileError, ProgramConstraintBodyV1, - ProgramConstraintSubjectV1, Source, SourceId, Surface, Target, TargetId, + ProgramConstraintSubjectV1, Source, SourceId, Surface, Target, TargetId, TargetIntentV1, canonical_surface_input_port_sequence_matches, check_render_node_count, }; use crate::session::{SessionPlanV1, SessionState, SessionUpdateError}; @@ -231,7 +231,7 @@ fn authored_modes_are_marker_typed_and_values_preserve_exact_ids() { ); let target = Target::fixed(TARGET, SOURCE); assert_eq!(target.id(), TARGET); - assert_eq!(target.source(), SOURCE); + assert_eq!(target.intent(), &TargetIntentV1::FixedSource(SOURCE)); let opacity = OpacityInput::new(OPACITY, 0.375); assert_eq!(opacity.id(), OPACITY); assert_eq!(opacity.value(), 0.375); diff --git a/docs/whitepaper.md b/docs/whitepaper.md index 328614ec..b37412e7 100644 --- a/docs/whitepaper.md +++ b/docs/whitepaper.md @@ -96,9 +96,14 @@ token как угодно; Core не выводит физику из имени Реализованный точечный домен приватного графа внешнего вида содержит `Paint::Input`, необязательную цепь `Paint::Opacity` и `Occurrence(fill over input surface) → Surface::FromOccurrence`. Каждый -конечный кандидат Target объявляет одну неделимую пару encoded-sRGB8-источника и -базовой непреумноженной альфы; Core не образует их декартовы комбинации. Явные -узлы `Paint::Opacity` остаются общими последовательными множителями этой альфы. +`Target` имеет ровно одно намерение: `FixedSource(source)` либо непустой +`FinitePaintDomain`. Конечная цель не содержит стартового `Source`: её +физическая свобода полностью и без второго владельца задана атомарными +кандидатами. Желаемый видимый результат задаётся ограничением над `Occurrence`, +где известны `Paint`, альфа, фон, профиль и набор сценариев, а не полем `Target`. +Каждый конечный кандидат объявляет одну неделимую пару encoded-sRGB8-источника и +исходной прямой альфы; Core не образует их декартовы комбинации. Явные узлы +`Paint::Opacity` остаются общими последовательными множителями этой альфы. Occurrence моделирует композицию в объявленном encoded-sRGB8 профиле; это не наблюдение фактических пикселей среды исполнения. Видимый результат становится производной поверхностью без повторной композиции. @@ -111,6 +116,11 @@ Occurrence моделирует композицию в объявленном e тестом. Компилятор принадлежит доказательной поверхности и отсутствует в рабочем артефакте. +Канонический адрес Program использует схему V5. Ребро `Target → Source` +существует только для `FixedSource`; конечный домен связывается непосредственно +с кандидатами. Поэтому физически мёртвый seed не может менять identity или +диагностику конечной цели. + Связь foreground с производной Surface задаёт client-owned Program. Core лоуверит её в общие occurrence и joint-constraint примитивы, строит полный hard-report и выполняет fresh recheck. Закрытых UI-рецептов для конкретного From 258a74065a99df2012a2e15df8ec42f3db065d46 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Tue, 28 Jul 2026 08:58:37 +0300 Subject: [PATCH 2/2] =?UTF-8?q?test(core):=20=D1=83=D1=82=D0=BE=D1=87?= =?UTF-8?q?=D0=BD=D0=B8=D1=82=D1=8C=20=D0=BE=D1=82=D1=81=D1=83=D1=82=D1=81?= =?UTF-8?q?=D1=82=D0=B2=D0=B8=D0=B5=20finite=20Source=20edge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/labcolors-core/src/program_identity_tests.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/labcolors-core/src/program_identity_tests.rs b/crates/labcolors-core/src/program_identity_tests.rs index 8f007c92..948e97b2 100644 --- a/crates/labcolors-core/src/program_identity_tests.rs +++ b/crates/labcolors-core/src/program_identity_tests.rs @@ -1189,7 +1189,7 @@ fn finite_program(reverse_order: bool) -> CoreProgramV1 { finite_program_with_opacity(reverse_order, 1.0) } -fn finite_program_with_swapped_unbound_sources(swap_source_signals: bool) -> CoreProgramV1 { +fn finite_program_with_permuted_source_colors(swap_source_signals: bool) -> CoreProgramV1 { let sources = [SourceId::new(1), SourceId::new(2)]; let target = TargetId::new(3); let first = TargetCandidateId::new(4); @@ -1248,11 +1248,15 @@ fn finite_program_with_swapped_unbound_sources(swap_source_signals: bool) -> Cor } #[test] -fn finite_target_dead_source_cannot_change_physical_content_identity() { - let first = finite_program_with_swapped_unbound_sources(false) +fn finite_target_has_no_source_incidence_in_content_identity() { + // Оба Source намеренно остаются членами Program: перестановка их цветов + // сохраняет мультимножество вершин, но меняет цвет вершины, которую выделило + // бы запрещённое finite Target→Source ребро. Равенство тем самым доказывает + // отсутствие ребра, не заявляя, что прочие декларации исчезают из identity. + let first = finite_program_with_permuted_source_colors(false) .compile() .unwrap(); - let second = finite_program_with_swapped_unbound_sources(true) + let second = finite_program_with_permuted_source_colors(true) .compile() .unwrap();