From bb0ae2c0c292735957424109f022fe4c6ece861a Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Wed, 22 Jul 2026 05:27:42 +0300 Subject: [PATCH 01/58] feat(core): compile generic point-render sessions --- ...rt-reference-surplus-q55-bps-proof-v1.json | 2 +- crates/labcolors-core/src/appearance.rs | 464 ++++++++++++++--- .../src/appearance_graph_tests.rs | 296 ++++++++++- .../src/generic_boundary_tests.rs | 65 +++ crates/labcolors-core/src/lcs_occurrence.rs | 309 +++++++++++ .../src/lcs_occurrence_tests.rs | 118 +++++ crates/labcolors-core/src/lib.rs | 25 + crates/labcolors-core/src/program_session.rs | 493 ++++++++++++++++++ .../src/program_session_tests.rs | 299 +++++++++++ scripts/verify_point_support_surplus.py | 2 +- 10 files changed, 1987 insertions(+), 86 deletions(-) create mode 100644 crates/labcolors-core/src/generic_boundary_tests.rs create mode 100644 crates/labcolors-core/src/lcs_occurrence.rs create mode 100644 crates/labcolors-core/src/lcs_occurrence_tests.rs create mode 100644 crates/labcolors-core/src/program_session.rs create mode 100644 crates/labcolors-core/src/program_session_tests.rs 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 8462366a..066ea0d0 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":"76cc2f9916efb337fdc7cb20c444f619c289dc3a3d9a877fdc4087fefe33a12a","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":"c2825216354b796924560d98e01ae5cebedf324c47e7b26332119c61aded783e","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":"5df64a220c86378c66d25f0e0abe2507b636b0ec6cd8217a6235e17809f00f48"},{"kind":"rust-source","path":"crates/labcolors-core/src/composition.rs","sha256":"5218a845b85a27571a710c7c967b2937b94cf4622a3073487a808936ac85468a"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/exact.rs","sha256":"45bd09cd8f6f74860a0a3ffeb410ee2c3cf91b4868877abbc949e724b61e1ccd"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/mod.rs","sha256":"ad721c625b33a7432de44e5f63459a62c1cbc8ec2492855f783b5440f04978f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/wcag22.rs","sha256":"985e21abccd1fcb4a3f9624d5947ff3d1af7a1bf26b6b170ea8bbef77d6f1798"},{"kind":"rust-source","path":"crates/labcolors-core/src/hash.rs","sha256":"f97a0fd7d6ad3162f0f1dfb326fccfb7ed40da9a8fa67a5b8a239a1ae2ae49c3"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"95113d8a25fd1577823b8c6342c06272b12ea8711ee5e5a62d034948177c13db"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"208066b255bc406303b18dc2eb6f1720c8203ea6eb37d225f9d8110c2449edc4"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"56c06b49f4687b3def69c247978da1b0cb45b42c09515a001d196cb1457c93a6"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"d4b06651e07cf4f93eccf9866f6e63c35ea72fa7125bdb41e222b19b70dd2f32"},{"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":33,"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":"bc9ceb056a9bd4e93f5c5c5fd575779384027f00985b0075356169a8a11aabf4"} +{"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":"109ae673288f47ef1f5da949cd7c19883e4e33b62f332931f96de912239b70cc","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":"2647fad1457ec9743766503b4056bd9a002f7757d0ca60777c358e1085a02cbb","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":"553e904fcecff4ccc936e1f3cffd642eed6b221ad2e7a28238da55d2050b7e37"},{"kind":"rust-source","path":"crates/labcolors-core/src/composition.rs","sha256":"5218a845b85a27571a710c7c967b2937b94cf4622a3073487a808936ac85468a"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/exact.rs","sha256":"45bd09cd8f6f74860a0a3ffeb410ee2c3cf91b4868877abbc949e724b61e1ccd"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/mod.rs","sha256":"ad721c625b33a7432de44e5f63459a62c1cbc8ec2492855f783b5440f04978f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/wcag22.rs","sha256":"985e21abccd1fcb4a3f9624d5947ff3d1af7a1bf26b6b170ea8bbef77d6f1798"},{"kind":"rust-source","path":"crates/labcolors-core/src/hash.rs","sha256":"f97a0fd7d6ad3162f0f1dfb326fccfb7ed40da9a8fa67a5b8a239a1ae2ae49c3"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"d42d4f0bdfd64efc5d2956334dea9d07996849b8185ce448ec61a8bf38b0b684"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"208066b255bc406303b18dc2eb6f1720c8203ea6eb37d225f9d8110c2449edc4"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"56c06b49f4687b3def69c247978da1b0cb45b42c09515a001d196cb1457c93a6"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"d4b06651e07cf4f93eccf9866f6e63c35ea72fa7125bdb41e222b19b70dd2f32"},{"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":33,"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":"7c8ada7078061e6b67eae380bbb326e32de6d533647fb03ff686a233f3975e34"} diff --git a/crates/labcolors-core/src/appearance.rs b/crates/labcolors-core/src/appearance.rs index 32c2c0b7..37bb6e9e 100644 --- a/crates/labcolors-core/src/appearance.rs +++ b/crates/labcolors-core/src/appearance.rs @@ -13,10 +13,18 @@ //! //! Code-owned adapter представлен sealed borrowed IR и исполняется тем же //! evaluator-ом, что результат декларативной компиляции. Структурное равенство -//! статического IR результату compiler-а закреплено proof-тестом; production- -//! артефакт не содержит admission/topology compiler. +//! статического IR результату compiler-а закреплено proof-тестом. Compiler входит +//! в production Core: любой внутренний lowerer собирает тот же нейтральный +//! Paint/Surface/Occurrence DAG, не добавляя в физику словарь клиента. + +#![cfg_attr( + not(test), + expect( + dead_code, + reason = "production physical-graph compiler lands before its Core lowerer consumer" + ) +)] -#[cfg(test)] use std::collections::BTreeSet; use crate::Srgb8; @@ -133,7 +141,6 @@ impl ProgramOccurrenceBindingV1 { } /// Paint-конструкторы point-домена. Ни один вариант не знает Surface. -#[cfg(test)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum PaintSpec { /// Непрозрачный encoded-sRGB8 Paint из цветового входа. @@ -151,7 +158,6 @@ pub(crate) enum PaintSpec { }, } -#[cfg(test)] impl PaintSpec { fn id(&self) -> PaintId { match self { @@ -162,7 +168,6 @@ impl PaintSpec { /// Surface либо приходит извне как point-вход, либо является видимым /// результатом объявленного occurrence. -#[cfg(test)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum SurfaceSpec { Input { @@ -175,7 +180,6 @@ pub(crate) enum SurfaceSpec { }, } -#[cfg(test)] impl SurfaceSpec { fn id(&self) -> SurfaceId { match self { @@ -185,7 +189,6 @@ impl SurfaceSpec { } /// Единственная canonical application Paint к backdrop Surface. -#[cfg(test)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) struct OccurrenceSpec { pub(crate) id: OccurrenceId, @@ -194,9 +197,7 @@ pub(crate) struct OccurrenceSpec { pub(crate) profile: CompositionProfileV1, } -/// Ошибки AOT-компиляции декларации. Compiler принадлежит proof-поверхности и -/// не входит в production-артефакт. -#[cfg(test)] +/// Ошибки атомарной AOT-компиляции физической декларации. #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) enum CompileError { DuplicateColorInput { @@ -258,7 +259,6 @@ pub(crate) enum CompileError { /// Ошибки admission runtime bindings. Исполнение начинается только после /// полной проверки, поэтому частичного результата нет. -#[cfg(test)] #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) enum BindingError { DuplicateColorBinding { @@ -290,8 +290,16 @@ pub(crate) enum BindingError { }, OpacityOutOfDomain { input: OpacityInputId, - message: String, + reason: crate::composition::OpacityAdmissionErrorV1, }, + /// Bindings were admitted against a different exact typed input schema. + IncompatibleAdmittedBindings, + /// Scratch belongs to a different physical graph shape. Reusing storage is + /// allowed only when every typed output domain has the same cardinality. + IncompatibleWorkspace, + /// A fallible allocation needed to prepare bindings, scratch or an owned + /// result could not be satisfied. No numeric policy limit is implied. + ResourceExhausted, } /// Единственный отказ sealed point-adapter-а: невалидная authored alpha. @@ -309,7 +317,6 @@ impl PointOpacityError { } /// Плоские декларации до атомарной компиляции. Порядок списков смысла не несёт. -#[cfg(test)] #[derive(Debug, Clone, PartialEq)] pub(crate) struct AppearanceGraphSpec { color_inputs: Vec, @@ -320,7 +327,6 @@ pub(crate) struct AppearanceGraphSpec { occurrences: Vec, } -#[cfg(test)] impl AppearanceGraphSpec { pub(crate) fn new( color_inputs: Vec, @@ -598,7 +604,6 @@ impl AppearanceGraphSpec { } } -#[cfg(test)] fn adjacent_duplicate(sorted: &[T]) -> Option { sorted .windows(2) @@ -609,7 +614,6 @@ fn adjacent_duplicate(sorted: &[T]) -> Option { /// Topo для functional dependency graph: каждый узел имеет не более одной /// зависимости. При цикле возвращает только его реальные узлы, а не весь /// заблокированный Kahn-остаток. -#[cfg(test)] fn canonical_functional_topology( keys: &[K], dependencies: &[Option], @@ -649,7 +653,6 @@ fn canonical_functional_topology( /// Итеративный functional-cycle detector: O(V), без риска переполнить стек на /// большом входе и без ложного включения деревьев, ведущих в цикл. -#[cfg(test)] fn functional_cycle_members(dependencies: &[Option]) -> Vec { const UNSEEN: u8 = 0; const ACTIVE: u8 = 1; @@ -694,7 +697,6 @@ fn functional_cycle_members(dependencies: &[Option]) -> Vec { } #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -#[cfg(test)] enum RenderKey { Surface(SurfaceId), Occurrence(OccurrenceId), @@ -719,6 +721,14 @@ enum CompiledPaintSpec { }, } +impl CompiledPaintSpec { + const fn id(&self) -> PaintId { + match self { + Self::Solid { id, .. } | Self::Opacity { id, .. } => *id, + } + } +} + #[derive(Debug, Clone, PartialEq, Eq)] enum CompiledSurfaceSpec { Input { @@ -731,7 +741,6 @@ enum CompiledSurfaceSpec { }, } -#[cfg(test)] impl CompiledSurfaceSpec { fn id(&self) -> SurfaceId { match self { @@ -752,7 +761,6 @@ struct CompiledOccurrenceSpec { /// Канонический compiled IR с индексными ссылками: после проверки bindings /// исполнение самих Paint/Surface/Occurrence узлов линейно по их числу. -#[cfg(test)] #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct CompiledAppearanceGraph { color_inputs: Vec, @@ -766,8 +774,8 @@ pub(crate) struct CompiledAppearanceGraph { } /// Borrowed runtime-представление уже проверенного compiled IR. Оно отделяет -/// исполнение от compiler-а и позволяет статическим внутренним adapter-ам не -/// тащить admission/topology machinery в конечный binary. +/// исполнение от compiler-а, а статическим внутренним adapter-ам — исполнять +/// заранее доказанную топологию без повторной компиляции. #[derive(Debug, Clone, Copy, PartialEq, Eq)] struct CompiledInputSchema<'a> { color_inputs: &'a [ColorInputId], @@ -993,7 +1001,6 @@ pub(crate) fn point_program_matches(compiled: &CompiledAppearanceGraph) -> bool } /// Runtime bindings одного атомарного evaluate. -#[cfg(test)] #[derive(Debug, Clone, PartialEq)] pub(crate) struct AppearanceBindings { colors: Vec<(ColorInputId, Srgb8)>, @@ -1001,7 +1008,6 @@ pub(crate) struct AppearanceBindings { opacities: Vec<(OpacityInputId, f64)>, } -#[cfg(test)] impl AppearanceBindings { pub(crate) fn new( mut colors: Vec<(ColorInputId, Srgb8)>, @@ -1019,6 +1025,87 @@ impl AppearanceBindings { } } +/// Один раз полностью проверенные runtime bindings в typed physical domain. +/// +/// IDs остаются рядом со значениями: это позволяет fail-closed отвергнуть +/// случайное применение bindings к другому compiled input schema. Значения +/// alpha уже представлены [`crate::composition::AdmittedOpacityV1`], поэтому +/// steady-state evaluate не повторяет numeric admission. +#[derive(Debug, PartialEq, Eq)] +pub(crate) struct AdmittedAppearanceBindings { + colors: Vec<(ColorInputId, Srgb8)>, + surfaces: Vec<(SurfaceInputPortId, Srgb8)>, + opacities: Vec<(OpacityInputId, crate::composition::AdmittedOpacityV1)>, +} + +impl AdmittedAppearanceBindings { + /// Fallibly duplicate one fully admitted value for an independent Session. + /// + /// An ordinary [`Clone`] can abort the process on allocation failure. The + /// runtime attachment boundary uses this method so resource exhaustion is + /// returned before a partially prepared Session can escape. + pub(crate) fn try_clone_v1(&self) -> Result { + fn copy_vec(source: &[T]) -> Result, BindingError> { + let mut copied = Vec::new(); + copied + .try_reserve_exact(source.len()) + .map_err(|_| BindingError::ResourceExhausted)?; + copied.extend_from_slice(source); + Ok(copied) + } + + Ok(Self { + colors: copy_vec(&self.colors)?, + surfaces: copy_vec(&self.surfaces)?, + opacities: copy_vec(&self.opacities)?, + }) + } + + /// Обновить один уже объявленный physical Surface input без пересборки + /// остальных authored bindings и без allocation. + pub(crate) fn set_surface_input( + &mut self, + input: SurfaceInputPortId, + value: Srgb8, + ) -> Result<(), BindingError> { + let index = self + .surfaces + .binary_search_by_key(&input, |(bound, _)| *bound) + .map_err(|_| BindingError::UnexpectedSurfaceInputBinding { input })?; + self.surfaces[index].1 = value; + Ok(()) + } + + #[cfg(test)] + pub(crate) fn opacity_bits(&self, input: OpacityInputId) -> Option { + self.opacities + .binary_search_by_key(&input, |(bound, _)| *bound) + .ok() + .map(|index| self.opacities[index].1.bits()) + } + + fn matches_schema(&self, schema: CompiledInputSchema<'_>) -> bool { + schema.color_inputs.len() == self.colors.len() + && schema.surface_input_ports.len() == self.surfaces.len() + && schema.opacity_inputs.len() == self.opacities.len() + && schema + .color_inputs + .iter() + .zip(&self.colors) + .all(|(declared, (bound, _))| declared == bound) + && schema + .surface_input_ports + .iter() + .zip(&self.surfaces) + .all(|(declared, (bound, _))| declared == bound) + && schema + .opacity_inputs + .iter() + .zip(&self.opacities) + .all(|(declared, (bound, _))| declared == bound) + } +} + /// Материализованный encoded point Paint вне зависимости от стадии владения. /// /// Graph materialization и downstream recheck разделяют это одно физическое @@ -1210,7 +1297,6 @@ impl VisiblePointBindingV1 { } /// Полный атомарный результат evaluate в каноническом typed-ID порядке. -#[cfg(test)] #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct AppearanceEvaluation { paints: Vec, @@ -1218,7 +1304,6 @@ pub(crate) struct AppearanceEvaluation { occurrences: Vec, } -#[cfg(test)] impl AppearanceEvaluation { pub(crate) fn paint(&self, id: PaintId) -> Option<&EncodedPointPaintV1> { self.paints @@ -1242,7 +1327,173 @@ impl AppearanceEvaluation { } } -#[cfg(test)] +/// Reusable scratch для одного compiled physical graph. +/// +/// После первого fallible sizing повторные evaluate того же shape только +/// очищают slots; capacity и backing allocations остаются неизменными. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct AppearanceWorkspaceShape { + paints: usize, + surfaces: usize, + occurrences: usize, +} + +impl AppearanceWorkspaceShape { + const fn of(program: CompiledAppearanceProgram<'_>) -> Self { + Self { + paints: program.paints.len(), + surfaces: program.surfaces.len(), + occurrences: program.occurrences.len(), + } + } +} + +#[derive(Debug)] +pub(crate) struct AppearanceWorkspace { + shape: AppearanceWorkspaceShape, + paints: Vec>, + surfaces: Vec>, + occurrences: Vec>, +} + +impl AppearanceWorkspace { + fn for_program(program: CompiledAppearanceProgram<'_>) -> Result { + let shape = AppearanceWorkspaceShape::of(program); + let mut workspace = Self { + shape, + paints: Vec::new(), + surfaces: Vec::new(), + occurrences: Vec::new(), + }; + initialise_workspace_slots(&mut workspace.paints, shape.paints)?; + initialise_workspace_slots(&mut workspace.surfaces, shape.surfaces)?; + initialise_workspace_slots(&mut workspace.occurrences, shape.occurrences)?; + Ok(workspace) + } + + fn prepare(&mut self, program: CompiledAppearanceProgram<'_>) -> Result<(), BindingError> { + if self.shape != AppearanceWorkspaceShape::of(program) { + return Err(BindingError::IncompatibleWorkspace); + } + self.paints.fill(None); + self.surfaces.fill(None); + self.occurrences.fill(None); + Ok(()) + } + + #[cfg(test)] + pub(crate) fn storage_signature(&self) -> [(usize, usize); 3] { + [ + (self.paints.as_ptr() as usize, self.paints.capacity()), + (self.surfaces.as_ptr() as usize, self.surfaces.capacity()), + ( + self.occurrences.as_ptr() as usize, + self.occurrences.capacity(), + ), + ] + } +} + +fn initialise_workspace_slots( + slots: &mut Vec>, + required_len: usize, +) -> Result<(), BindingError> { + slots + .try_reserve_exact(required_len) + .map_err(|_| BindingError::ResourceExhausted)?; + slots.resize(required_len, None); + Ok(()) +} + +/// Borrowed allocation-free result of one workspace evaluation. +/// +/// The mutable workspace borrow prevents another evaluate from invalidating +/// these values while a consumer is still reading them. +#[derive(Debug)] +pub(crate) struct AppearanceEvaluationView<'program, 'workspace> { + program: CompiledAppearanceProgram<'program>, + workspace: &'workspace AppearanceWorkspace, +} + +impl AppearanceEvaluationView<'_, '_> { + pub(crate) fn paint(&self, id: PaintId) -> Option<&EncodedPointPaintV1> { + let index = self + .program + .paints + .binary_search_by_key(&id, CompiledPaintSpec::id) + .ok()?; + self.workspace.paints[index].as_ref() + } + + pub(crate) fn surface_rgb(&self, id: SurfaceId) -> Option<[u8; 3]> { + let index = self + .program + .surfaces + .binary_search_by_key(&id, CompiledSurfaceSpec::id) + .ok()?; + self.workspace.surfaces[index].map(Srgb8::bytes) + } + + pub(crate) fn occurrence(&self, id: OccurrenceId) -> Option<&ResolvedOccurrence> { + let index = self + .program + .occurrences + .binary_search_by_key(&id, |occurrence| occurrence.id) + .ok()?; + self.workspace.occurrences[index].as_ref() + } + + pub(crate) fn occurrences(&self) -> impl ExactSizeIterator + '_ { + self.workspace.occurrences.iter().map(|occurrence| { + occurrence + .as_ref() + .unwrap_or_else(|| unreachable!("render topo covers every Occurrence")) + }) + } + + fn try_to_owned(&self) -> Result { + let mut paints = Vec::new(); + paints + .try_reserve_exact(self.workspace.paints.len()) + .map_err(|_| BindingError::ResourceExhausted)?; + for paint in &self.workspace.paints { + paints.push(paint.unwrap_or_else(|| unreachable!("Paint topo covers every node"))); + } + + let mut surfaces = Vec::new(); + surfaces + .try_reserve_exact(self.workspace.surfaces.len()) + .map_err(|_| BindingError::ResourceExhausted)?; + for (spec, value) in self + .program + .surfaces + .iter() + .zip(&self.workspace.surfaces) + { + surfaces.push(( + spec.id(), + value.unwrap_or_else(|| unreachable!("render topo covers every Surface")), + )); + } + + let mut occurrences = Vec::new(); + occurrences + .try_reserve_exact(self.workspace.occurrences.len()) + .map_err(|_| BindingError::ResourceExhausted)?; + for occurrence in &self.workspace.occurrences { + occurrences.push( + occurrence.unwrap_or_else(|| unreachable!("render topo covers every Occurrence")), + ); + } + + Ok(AppearanceEvaluation { + paints, + surfaces, + occurrences, + }) + } +} + impl CompiledAppearanceGraph { fn program(&self) -> CompiledAppearanceProgram<'_> { CompiledAppearanceProgram::from_validated_parts( @@ -1259,26 +1510,74 @@ impl CompiledAppearanceGraph { ) } + #[cfg(test)] fn matches_program(&self, program: CompiledAppearanceProgram<'_>) -> bool { self.program() == program } + /// Canonical client-owned occurrence identities emitted by this program. + pub(crate) fn occurrence_ids(&self) -> impl ExactSizeIterator + '_ { + self.occurrences.iter().map(|occurrence| occurrence.id) + } + + /// Canonical physical Surface-input schema accepted by this program. + pub(crate) fn surface_input_ports( + &self, + ) -> impl ExactSizeIterator + '_ { + self.surface_input_ports.iter().copied() + } + + /// Проверить полный typed schema и один раз понизить authored alpha в + /// admitted physical values. Результат можно клонировать для независимых + /// runtime callers без повторной numeric admission. + pub(crate) fn admit_bindings( + &self, + bindings: &AppearanceBindings, + ) -> Result { + self.program().admit_bindings(bindings) + } + + /// Fallible one-time allocation of scratch for this exact physical shape. + pub(crate) fn new_workspace(&self) -> Result { + AppearanceWorkspace::for_program(self.program()) + } + + /// Allocation-free steady-state execution over already admitted bindings. + pub(crate) fn evaluate_admitted_into<'workspace>( + &self, + bindings: &AdmittedAppearanceBindings, + workspace: &'workspace mut AppearanceWorkspace, + ) -> Result, BindingError> { + self.program() + .evaluate_admitted_into(bindings, workspace) + } + + /// Cold convenience внутри Core для callers, которым нужен owned result. + /// Hot Session обязан хранить admitted bindings и workspace между вызовами. pub(crate) fn evaluate( &self, bindings: &AppearanceBindings, ) -> Result { - self.program().evaluate(bindings) + let admitted = self.admit_bindings(bindings)?; + let mut workspace = self.new_workspace()?; + self.evaluate_admitted_into(&admitted, &mut workspace)? + .try_to_owned() } } -impl CompiledAppearanceProgram<'_> { - /// Проверить bindings, материализовать Paint DAG один раз и исполнить - /// Surface/Occurrence DAG один раз. Частичный результат не возвращается. - #[cfg(test)] - pub(crate) fn evaluate( +impl<'program> CompiledAppearanceProgram<'program> { + const fn input_schema(self) -> CompiledInputSchema<'program> { + CompiledInputSchema::new( + self.color_inputs, + self.surface_input_ports, + self.opacity_inputs, + ) + } + + fn admit_bindings( &self, bindings: &AppearanceBindings, - ) -> Result { + ) -> Result { let colors = &bindings.colors; if let Some(window) = colors.windows(2).find(|window| window[0].0 == window[1].0) { return Err(BindingError::DuplicateColorBinding { input: window[0].0 }); @@ -1337,14 +1636,53 @@ impl CompiledAppearanceProgram<'_> { return Err(BindingError::UnexpectedSurfaceInputBinding { input: *bound }); } } + + let mut admitted_colors = Vec::new(); + admitted_colors + .try_reserve_exact(colors.len()) + .map_err(|_| BindingError::ResourceExhausted)?; + admitted_colors.extend(colors.iter().copied()); + + let mut admitted_surfaces = Vec::new(); + admitted_surfaces + .try_reserve_exact(surfaces.len()) + .map_err(|_| BindingError::ResourceExhausted)?; + admitted_surfaces.extend(surfaces.iter().copied()); + + let mut admitted_opacities = Vec::new(); + admitted_opacities + .try_reserve_exact(opacities.len()) + .map_err(|_| BindingError::ResourceExhausted)?; for (input, alpha) in opacities { - if let Err(message) = crate::composition::validate_alpha(*alpha) { - return Err(BindingError::OpacityOutOfDomain { + let value = crate::composition::AdmittedOpacityV1::new(*alpha).map_err(|reason| { + BindingError::OpacityOutOfDomain { input: *input, - message, - }); - } + reason, + } + })?; + admitted_opacities.push((*input, value)); + } + + Ok(AdmittedAppearanceBindings { + colors: admitted_colors, + surfaces: admitted_surfaces, + opacities: admitted_opacities, + }) + } + + fn evaluate_admitted_into<'workspace>( + self, + bindings: &AdmittedAppearanceBindings, + workspace: &'workspace mut AppearanceWorkspace, + ) -> Result, BindingError> { + if !bindings.matches_schema(self.input_schema()) { + return Err(BindingError::IncompatibleAdmittedBindings); } + workspace.prepare(self)?; + + let colors = &bindings.colors; + let surfaces = &bindings.surfaces; + let opacities = &bindings.opacities; let color_value = |id: ColorInputId| -> Srgb8 { let index = colors @@ -1362,55 +1700,27 @@ impl CompiledAppearanceProgram<'_> { let index = opacities .binary_search_by_key(&id, |(bound, _)| *bound) .unwrap_or_else(|_| unreachable!("bindings were matched before evaluation")); - crate::composition::AdmittedOpacityV1::new(opacities[index].1) - .unwrap_or_else(|_| unreachable!("opacity bindings were admitted before execution")) + opacities[index].1 }; - let mut resolved_paints: Vec> = vec![None; self.paints.len()]; - let mut resolved_surfaces: Vec> = vec![None; self.surfaces.len()]; - let mut resolved_occurrences: Vec> = - vec![None; self.occurrences.len()]; self.execute_into( color_value, surface_value, opacity_value, - &mut resolved_paints, - &mut resolved_surfaces, - &mut resolved_occurrences, + &mut workspace.paints, + &mut workspace.surfaces, + &mut workspace.occurrences, ); - let paints = resolved_paints - .into_iter() - .map(|paint| paint.unwrap_or_else(|| unreachable!("Paint topo covers every node"))) - .collect(); - let surfaces = self - .surfaces - .iter() - .zip(resolved_surfaces) - .map(|(surface, value)| { - ( - surface.id(), - value.unwrap_or_else(|| unreachable!("render topo covers every Surface")), - ) - }) - .collect(); - let occurrences = resolved_occurrences - .into_iter() - .map(|occurrence| { - occurrence.unwrap_or_else(|| unreachable!("render topo covers every Occurrence")) - }) - .collect(); - - Ok(AppearanceEvaluation { - paints, - surfaces, - occurrences, + Ok(AppearanceEvaluationView { + program: self, + workspace, }) } /// Единственное исполнение compiled IR. Scratch принадлежит caller-у: - /// static adapter использует stack arrays, test-only generic admission — - /// динамические buffers. Алгоритм и сертификат при этом общие. + /// static adapter использует stack arrays, generic admission — + /// владеющие buffers. Алгоритм и сертификат при этом общие. fn execute_into( &self, color_value: C, diff --git a/crates/labcolors-core/src/appearance_graph_tests.rs b/crates/labcolors-core/src/appearance_graph_tests.rs index 9e25144f..0bff3090 100644 --- a/crates/labcolors-core/src/appearance_graph_tests.rs +++ b/crates/labcolors-core/src/appearance_graph_tests.rs @@ -3,7 +3,7 @@ //! Граф владеет только физической топологией: Paint материализуется независимо //! от подложки, Occurrence является его единственным применением к Surface, а //! `surfaceFrom` лишь даёт видимому результату повторно используемую identity. -//! Словарь Pair/role и perception-утверждения сюда не входят. +//! Клиентский словарь и perception-утверждения сюда не входят. use proptest::prelude::*; @@ -239,6 +239,96 @@ fn complete_typed_id_renaming_does_not_change_physics() { ); } +#[test] +fn equal_transport_numbers_remain_opaque_in_a_nested_occurrence_graph() { + let first_color = ColorInputId::new(41); + let second_color = ColorInputId::new(7); + let surface_port = SurfaceInputPortId::new(41); + let first_opacity = OpacityInputId::new(41); + let second_opacity = OpacityInputId::new(7); + let first_solid = PaintId::new(41); + let first_modulated = PaintId::new(42); + let second_solid = PaintId::new(7); + let second_modulated = PaintId::new(8); + let backdrop = SurfaceId::new(41); + let derived = SurfaceId::new(7); + let first_occurrence = OccurrenceId::new(41); + let second_occurrence = OccurrenceId::new(7); + + let graph = AppearanceGraphSpec::new( + vec![first_color, second_color], + vec![surface_port], + vec![first_opacity, second_opacity], + vec![ + PaintSpec::Opacity { + id: first_modulated, + source: first_solid, + opacity: first_opacity, + }, + PaintSpec::Solid { + id: second_solid, + color: second_color, + }, + PaintSpec::Opacity { + id: second_modulated, + source: second_solid, + opacity: second_opacity, + }, + PaintSpec::Solid { + id: first_solid, + color: first_color, + }, + ], + vec![ + SurfaceSpec::FromOccurrence { + id: derived, + occurrence: first_occurrence, + }, + SurfaceSpec::Input { + id: backdrop, + port: surface_port, + }, + ], + vec![ + OccurrenceSpec { + id: second_occurrence, + subject: second_modulated, + against: derived, + profile: CompositionProfileV1::EncodedSrgb8SourceOverV1, + }, + OccurrenceSpec { + id: first_occurrence, + subject: first_modulated, + against: backdrop, + profile: CompositionProfileV1::EncodedSrgb8SourceOverV1, + }, + ], + ) + .compile() + .unwrap(); + let evaluated = graph + .evaluate(&AppearanceBindings::new( + vec![ + (first_color, Srgb8::new([200, 80, 40])), + (second_color, Srgb8::new([30, 160, 230])), + ], + vec![(surface_port, Srgb8::new([20, 40, 60]))], + vec![(first_opacity, 0.5), (second_opacity, 0.25)], + )) + .unwrap(); + + let first = evaluated.occurrence(first_occurrence).unwrap(); + let second = evaluated.occurrence(second_occurrence).unwrap(); + assert_eq!(first.subject(), first_modulated); + assert_eq!(first.against(), backdrop); + assert_eq!(first.visible(), [110, 60, 50]); + assert_eq!(evaluated.surface_rgb(derived), Some([110, 60, 50])); + assert_eq!(second.subject(), second_modulated); + assert_eq!(second.against(), derived); + assert_eq!(second.backdrop(), first.visible()); + assert_eq!(second.visible(), [90, 85, 95]); +} + #[test] fn occurrence_uses_the_declared_paint_not_an_unrelated_color_input() { let graph = AppearanceGraphSpec::new( @@ -852,7 +942,8 @@ proptest! { } else { (invalid, 0.5, OPACITY) }; - let expected_message = crate::composition::validate_alpha(invalid).unwrap_err(); + let expected_reason = + crate::composition::AdmittedOpacityV1::new(invalid).unwrap_err(); prop_assert_eq!( graph.evaluate(&AppearanceBindings::new( vec![(SOURCE, Srgb8::new([1, 2, 3]))], @@ -861,7 +952,7 @@ proptest! { )), Err(BindingError::OpacityOutOfDomain { input: expected_input, - message: expected_message, + reason: expected_reason, }) ); } @@ -1333,16 +1424,53 @@ fn evaluate_rejects_duplicate_missing_and_unexpected_bindings() { } #[test] -fn evaluate_rejects_invalid_alpha_with_the_ssot_domain_text() { +fn binding_admission_is_atomic_before_any_occurrence_is_evaluated() { + let graph = point_component(false, false).compile().unwrap(); + + crate::composition::reset_source_over_evaluation_count(); + let duplicate_surface = graph.evaluate(&AppearanceBindings::new( + vec![(SOURCE, Srgb8::new([1, 2, 3]))], + vec![ + (CONTEXT, Srgb8::new([4, 5, 6])), + (CONTEXT, Srgb8::new([7, 8, 9])), + ], + vec![(OPACITY, 0.5)], + )); + assert_eq!( + duplicate_surface, + Err(BindingError::DuplicateSurfaceInputBinding { input: CONTEXT }) + ); + assert_eq!(crate::composition::source_over_evaluation_count(), 0); + + crate::composition::reset_source_over_evaluation_count(); + let invalid_opacity = graph.evaluate(&bindings([1, 2, 3], f64::NAN, [4, 5, 6])); + assert_eq!( + invalid_opacity, + Err(BindingError::OpacityOutOfDomain { + input: OPACITY, + reason: crate::composition::OpacityAdmissionErrorV1::NonFinite, + }) + ); + assert_eq!(crate::composition::source_over_evaluation_count(), 0); + + let evaluated = graph + .evaluate(&bindings([1, 2, 3], 0.5, [4, 5, 6])) + .unwrap(); + assert_eq!(crate::composition::source_over_evaluation_count(), 1); + assert!(evaluated.occurrence(FILL_OCCURRENCE).is_some()); +} + +#[test] +fn evaluate_rejects_invalid_alpha_with_the_typed_admission_reason() { let graph = point_component(false, false).compile().unwrap(); for bad_alpha in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, -0.1, 1.5] { - let expected = crate::alpha::composite_over_srgb8([1, 2, 3], bad_alpha, [4, 5, 6]) - .expect_err("композитор обязан отвергать тот же домен alpha"); + let expected = crate::composition::AdmittedOpacityV1::new(bad_alpha) + .expect_err("общий admission обязан отвергать alpha"); assert_eq!( graph.evaluate(&bindings([1, 2, 3], bad_alpha, [4, 5, 6])), Err(BindingError::OpacityOutOfDomain { input: OPACITY, - message: expected, + reason: expected, }), "alpha={bad_alpha}" ); @@ -1364,3 +1492,157 @@ fn signed_zero_opacity_has_one_canonical_state() { 0.0f64.to_bits() ); } + +#[test] +fn admitted_bindings_and_workspace_are_reused_without_storage_churn() { + let graph = point_component(false, false).compile().unwrap(); + let mut admitted = graph + .admit_bindings(&bindings([200, 80, 40], 0.5, [20, 40, 60])) + .unwrap(); + let mut independent = admitted.try_clone_v1().unwrap(); + assert_eq!(independent, admitted); + independent + .set_surface_input(CONTEXT, Srgb8::new([1, 2, 3])) + .unwrap(); + assert_ne!(independent, admitted); + assert_eq!(admitted.opacity_bits(OPACITY), Some(0.5f64.to_bits())); + assert_eq!( + graph.occurrence_ids().collect::>(), + vec![FILL_OCCURRENCE] + ); + assert_eq!( + graph.surface_input_ports().collect::>(), + vec![CONTEXT] + ); + + let mut workspace = graph.new_workspace().unwrap(); + let storage = workspace.storage_signature(); + for (backdrop, expected) in [ + ([20, 40, 60], [110, 60, 50]), + ([100, 100, 100], [150, 90, 70]), + ] { + admitted + .set_surface_input(CONTEXT, Srgb8::new(backdrop)) + .unwrap(); + { + let evaluated = graph + .evaluate_admitted_into(&admitted, &mut workspace) + .unwrap(); + assert_eq!( + evaluated.paint(FILL_PAINT).unwrap().opacity_bits(), + 0.5f64.to_bits() + ); + assert_eq!(evaluated.surface_rgb(CONTEXT_SURFACE), Some(backdrop)); + assert_eq!( + evaluated.occurrence(FILL_OCCURRENCE).unwrap().visible(), + expected + ); + assert_eq!( + evaluated + .occurrences() + .map(|occurrence| occurrence.id()) + .collect::>(), + vec![FILL_OCCURRENCE] + ); + } + assert_eq!(workspace.storage_signature(), storage); + } +} + +#[test] +fn admitted_schema_and_workspace_shape_mismatches_fail_before_composition() { + let graph = point_component(false, false).compile().unwrap(); + let admitted = graph + .admit_bindings(&bindings([1, 2, 3], 0.5, [4, 5, 6])) + .unwrap(); + let empty = AppearanceGraphSpec::new(vec![], vec![], vec![], vec![], vec![], vec![]) + .compile() + .unwrap(); + let mut wrong_workspace = empty.new_workspace().unwrap(); + let wrong_storage = wrong_workspace.storage_signature(); + + crate::composition::reset_source_over_evaluation_count(); + assert_eq!( + graph + .evaluate_admitted_into(&admitted, &mut wrong_workspace) + .unwrap_err(), + BindingError::IncompatibleWorkspace + ); + assert_eq!(crate::composition::source_over_evaluation_count(), 0); + assert_eq!(wrong_workspace.storage_signature(), wrong_storage); + + let other_source = ColorInputId::new(100); + let other_context = SurfaceInputPortId::new(101); + let other_opacity = OpacityInputId::new(102); + let other_solid = PaintId::new(103); + let other_paint = PaintId::new(104); + let other_surface = SurfaceId::new(105); + let other_derived = SurfaceId::new(106); + let other_occurrence = OccurrenceId::new(107); + let other = AppearanceGraphSpec::new( + vec![other_source], + vec![other_context], + vec![other_opacity], + vec![ + PaintSpec::Solid { + id: other_solid, + color: other_source, + }, + PaintSpec::Opacity { + id: other_paint, + source: other_solid, + opacity: other_opacity, + }, + ], + vec![ + SurfaceSpec::Input { + id: other_surface, + port: other_context, + }, + SurfaceSpec::FromOccurrence { + id: other_derived, + occurrence: other_occurrence, + }, + ], + vec![OccurrenceSpec { + id: other_occurrence, + subject: other_paint, + against: other_surface, + profile: CompositionProfileV1::EncodedSrgb8SourceOverV1, + }], + ) + .compile() + .unwrap(); + let other_admitted = other + .admit_bindings(&AppearanceBindings::new( + vec![(other_source, Srgb8::new([1, 2, 3]))], + vec![(other_context, Srgb8::new([4, 5, 6]))], + vec![(other_opacity, 0.5)], + )) + .unwrap(); + let mut workspace = graph.new_workspace().unwrap(); + let storage = workspace.storage_signature(); + + assert_eq!( + graph + .evaluate_admitted_into(&other_admitted, &mut workspace) + .unwrap_err(), + BindingError::IncompatibleAdmittedBindings + ); + assert_eq!(crate::composition::source_over_evaluation_count(), 0); + assert_eq!(workspace.storage_signature(), storage); +} + +#[test] +fn invalid_opacity_is_rejected_during_admission_before_any_composition() { + let graph = point_component(false, false).compile().unwrap(); + crate::composition::reset_source_over_evaluation_count(); + assert_eq!( + graph.admit_bindings(&bindings([1, 2, 3], f64::NAN, [4, 5, 6])), + Err(BindingError::OpacityOutOfDomain { + input: OPACITY, + reason: crate::composition::OpacityAdmissionErrorV1::NonFinite, + }) + ); + assert_eq!(crate::composition::source_over_evaluation_count(), 0); +} diff --git a/crates/labcolors-core/src/generic_boundary_tests.rs b/crates/labcolors-core/src/generic_boundary_tests.rs new file mode 100644 index 00000000..50ffff1e --- /dev/null +++ b/crates/labcolors-core/src/generic_boundary_tests.rs @@ -0,0 +1,65 @@ +const APPEARANCE_SOURCE: &str = include_str!("appearance.rs"); +const LCS_OCCURRENCE_SOURCE: &str = include_str!("lcs_occurrence.rs"); +const PROGRAM_SESSION_SOURCE: &str = include_str!("program_session.rs"); + +const GENERIC_SOURCES: [(&str, &str); 3] = [ + ("appearance.rs", APPEARANCE_SOURCE), + ("lcs_occurrence.rs", LCS_OCCURRENCE_SOURCE), + ("program_session.rs", PROGRAM_SESSION_SOURCE), +]; + +const CLIENT_OR_LEGACY_VOCABULARY: [&str; 14] = [ + "ThemeConfig", + "RoleRecipe", + "RoleSpec", + "NamedRoleTable", + "PairFill", + "PairLabel", + "Glow", + "Material", + "Ladder", + "AlphaAnalog", + "themeHandle", + "resolveTheme", + "Primary", + "Danger", +]; + +#[test] +fn generic_physical_and_transport_modules_contain_no_client_or_legacy_vocabulary() { + for (path, source) in GENERIC_SOURCES { + for forbidden in CLIENT_OR_LEGACY_VOCABULARY { + assert!( + !source.contains(forbidden), + "{path} must remain client-semantic agnostic; found `{forbidden}`" + ); + } + } +} + +#[test] +fn encoded_point_transport_does_not_claim_lcs_observation_types() { + for forbidden in ["TristimulusSample", "LcsOccurrence", "AppearanceState"] { + assert!( + !PROGRAM_SESSION_SOURCE.contains(forbidden), + "program_session.rs is encoded point transport, not `{forbidden}` evidence" + ); + } +} + +#[test] +fn program_session_module_docs_disclaim_transport_only_scope() { + let module_docs = PROGRAM_SESSION_SOURCE + .lines() + .take_while(|line| line.starts_with("//!")) + .collect::>() + .join("\n") + .to_ascii_lowercase(); + + for required in ["transport-only", "encoded", "not", "lcs", "evidence"] { + assert!( + module_docs.contains(required), + "program_session.rs module docs must explicitly disclaim transport-only scope; missing `{required}`" + ); + } +} diff --git a/crates/labcolors-core/src/lcs_occurrence.rs b/crates/labcolors-core/src/lcs_occurrence.rs new file mode 100644 index 00000000..3e7e9af6 --- /dev/null +++ b/crates/labcolors-core/src/lcs_occurrence.rs @@ -0,0 +1,309 @@ +//! Type foundation for a context-bound Labpics Colors Space occurrence. +//! +//! This module intentionally contains no colour transforms. It makes the +//! physical identity split representable before the existing kernels are moved: +//! encoded output, framed tristimulus evidence, appearance context and derived +//! hue state are different values. In particular, an occurrence has no inverse +//! operation accepting an arbitrary second context. + +/// A registered encoded-output domain. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum OutputProfileId { + Iec61966Srgb8D65V1, +} + +/// A registered render operation. This is deliberately not an output profile. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum RenderProfileId { + EncodedSrgb8PointV1, +} + +/// Exact encoded channels plus the profile which gives those channels meaning. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ColorSignal { + channels: [u8; 3], + output_profile: OutputProfileId, +} + +impl ColorSignal { + pub(crate) const fn new(channels: [u8; 3], output_profile: OutputProfileId) -> Self { + Self { + channels, + output_profile, + } + } + + pub(crate) const fn channels(self) -> [u8; 3] { + self.channels + } + + pub(crate) const fn output_profile(self) -> OutputProfileId { + self.output_profile + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ObserverProfileId { + Cie1931TwoDegreeV1, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ReferenceWhiteId { + Iec61966D65ChromaticityV1, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum TristimulusScale { + RelativeY1, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ColorimetricFrameReleaseId { + XyzV1, + #[cfg(test)] + MutationSentinelV1, +} + +/// Everything required to interpret one XYZ triple. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ColorimetricFrameId { + observer: ObserverProfileId, + reference_white: ReferenceWhiteId, + scale: TristimulusScale, + release: ColorimetricFrameReleaseId, +} + +impl ColorimetricFrameId { + pub(crate) const fn new( + observer: ObserverProfileId, + reference_white: ReferenceWhiteId, + scale: TristimulusScale, + release: ColorimetricFrameReleaseId, + ) -> Self { + Self { + observer, + reference_white, + scale, + release, + } + } + + pub(crate) const fn observer(self) -> ObserverProfileId { + self.observer + } + + pub(crate) const fn reference_white(self) -> ReferenceWhiteId { + self.reference_white + } + + pub(crate) const fn scale(self) -> TristimulusScale { + self.scale + } + + pub(crate) const fn release(self) -> ColorimetricFrameReleaseId { + self.release + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NumericDomainError { + NonFinite, + Negative, + NotPositive, + AboveOne, + HueOutOfRange, +} + +/// Finite, non-negative binary64 value with canonical positive zero. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct FiniteNonNegative(u64); + +impl FiniteNonNegative { + pub(crate) fn new(value: f64) -> Result { + if !value.is_finite() { + return Err(NumericDomainError::NonFinite); + } + if value < 0.0 { + return Err(NumericDomainError::Negative); + } + Ok(Self(if value == 0.0 { + 0.0_f64.to_bits() + } else { + value.to_bits() + })) + } + + pub(crate) fn get(self) -> f64 { + f64::from_bits(self.0) + } +} + +/// One finite XYZ point plus the identity of its colorimetric frame. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct TristimulusSample { + xyz: [FiniteNonNegative; 3], + frame: ColorimetricFrameId, +} + +impl TristimulusSample { + pub(crate) fn new( + xyz: [f64; 3], + frame: ColorimetricFrameId, + ) -> Result { + Ok(Self { + xyz: [ + FiniteNonNegative::new(xyz[0])?, + FiniteNonNegative::new(xyz[1])?, + FiniteNonNegative::new(xyz[2])?, + ], + frame, + }) + } + + pub(crate) fn xyz(self) -> [f64; 3] { + self.xyz.map(FiniteNonNegative::get) + } + + pub(crate) const fn frame(self) -> ColorimetricFrameId { + self.frame + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum AppearanceContextReleaseId { + Cam16V1, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum SurroundProfileId { + AverageV1, + DimV1, + DarkV1, +} + +/// Content identity of immutable semantic viewing inputs. +/// +/// Derived CAM constants are intentionally absent and must remain a private +/// cache of the observer implementation. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct AppearanceContextId { + release: AppearanceContextReleaseId, + frame: ColorimetricFrameId, + adapting_luminance: FiniteNonNegative, + background_relative_luminance: FiniteNonNegative, + surround: SurroundProfileId, +} + +impl AppearanceContextId { + pub(crate) fn new( + release: AppearanceContextReleaseId, + frame: ColorimetricFrameId, + adapting_luminance: f64, + background_relative_luminance: f64, + surround: SurroundProfileId, + ) -> Result { + let adapting_luminance = FiniteNonNegative::new(adapting_luminance)?; + if adapting_luminance.get() == 0.0 { + return Err(NumericDomainError::NotPositive); + } + let background_relative_luminance = + FiniteNonNegative::new(background_relative_luminance)?; + if background_relative_luminance.get() == 0.0 { + return Err(NumericDomainError::NotPositive); + } + if background_relative_luminance.get() > 1.0 { + return Err(NumericDomainError::AboveOne); + } + Ok(Self { + release, + frame, + adapting_luminance, + background_relative_luminance, + surround, + }) + } + + pub(crate) const fn frame(self) -> ColorimetricFrameId { + self.frame + } +} + +/// A finite angle; absence of hue is represented by [`HueState`], never `0°`. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct HueAngle(u64); + +impl HueAngle { + pub(crate) fn new(degrees: f64) -> Result { + if !degrees.is_finite() { + return Err(NumericDomainError::NonFinite); + } + if !(0.0..360.0).contains(°rees) { + return Err(NumericDomainError::HueOutOfRange); + } + Ok(Self(if degrees == 0.0 { + 0.0_f64.to_bits() + } else { + degrees.to_bits() + })) + } + + pub(crate) fn degrees(self) -> f64 { + f64::from_bits(self.0) + } +} + +/// Opaque identity of an admitted powerless-hue rule. +/// +/// It has no constructor until a concrete named-view release is registered. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct HuePowerlessProfileId(u32); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum HueState { + Defined(HueAngle), + UndefinedExact, + PowerlessBy(HuePowerlessProfileId), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ObserveError { + FrameMismatch { + stimulus: ColorimetricFrameId, + context: ColorimetricFrameId, + }, +} + +/// LCS identity: one physical sample observed in one immutable context. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct LcsOccurrence { + sample: TristimulusSample, + context: AppearanceContextId, +} + +impl LcsOccurrence { + /// Observe one sample in one context with an exactly matching frame. + /// + /// Named appearance views are derived later from this pair. No view + /// coordinate is accepted here, so contradictory cached views cannot become + /// part of occurrence identity. + pub(crate) fn observe( + sample: TristimulusSample, + context: AppearanceContextId, + ) -> Result { + if sample.frame() != context.frame() { + return Err(ObserveError::FrameMismatch { + stimulus: sample.frame(), + context: context.frame(), + }); + } + Ok(Self { sample, context }) + } + + pub(crate) const fn sample(self) -> TristimulusSample { + self.sample + } + + pub(crate) const fn context(self) -> AppearanceContextId { + self.context + } +} diff --git a/crates/labcolors-core/src/lcs_occurrence_tests.rs b/crates/labcolors-core/src/lcs_occurrence_tests.rs new file mode 100644 index 00000000..0e0727d0 --- /dev/null +++ b/crates/labcolors-core/src/lcs_occurrence_tests.rs @@ -0,0 +1,118 @@ +use crate::lcs_occurrence::{ + AppearanceContextId, AppearanceContextReleaseId, ColorimetricFrameId, + ColorimetricFrameReleaseId, HueAngle, HueState, LcsOccurrence, NumericDomainError, + ObserveError, ObserverProfileId, ReferenceWhiteId, SurroundProfileId, TristimulusSample, + TristimulusScale, +}; + +fn frame(scale: TristimulusScale) -> ColorimetricFrameId { + ColorimetricFrameId::new( + ObserverProfileId::Cie1931TwoDegreeV1, + ReferenceWhiteId::Iec61966D65ChromaticityV1, + scale, + ColorimetricFrameReleaseId::XyzV1, + ) +} + +fn context(frame: ColorimetricFrameId, la: f64) -> AppearanceContextId { + AppearanceContextId::new( + AppearanceContextReleaseId::Cam16V1, + frame, + la, + 0.2, + SurroundProfileId::AverageV1, + ) + .unwrap() +} + +#[test] +fn xyz_and_context_numeric_admission_is_fail_closed() { + let relative = frame(TristimulusScale::RelativeY1); + for xyz in [ + [f64::NAN, 0.0, 0.0], + [0.0, f64::INFINITY, 0.0], + [0.0, 0.0, -f64::MIN_POSITIVE], + ] { + assert!(TristimulusSample::new(xyz, relative).is_err()); + } + assert_eq!( + AppearanceContextId::new( + AppearanceContextReleaseId::Cam16V1, + relative, + 0.0, + 0.2, + SurroundProfileId::AverageV1, + ), + Err(NumericDomainError::NotPositive) + ); + assert_eq!( + AppearanceContextId::new( + AppearanceContextReleaseId::Cam16V1, + relative, + 64.0, + 0.0, + SurroundProfileId::AverageV1, + ), + Err(NumericDomainError::NotPositive) + ); + assert_eq!( + AppearanceContextId::new( + AppearanceContextReleaseId::Cam16V1, + relative, + 64.0, + 1.01, + SurroundProfileId::AverageV1, + ), + Err(NumericDomainError::AboveOne) + ); +} + +#[test] +fn hue_algebra_cannot_encode_exact_absence_as_zero_degrees() { + assert_ne!( + HueState::UndefinedExact, + HueState::Defined(HueAngle::new(0.0).unwrap()) + ); +} + +#[test] +fn frame_mismatch_cannot_form_an_occurrence() { + let relative = frame(TristimulusScale::RelativeY1); + let mismatching = ColorimetricFrameId::new( + ObserverProfileId::Cie1931TwoDegreeV1, + ReferenceWhiteId::Iec61966D65ChromaticityV1, + TristimulusScale::RelativeY1, + ColorimetricFrameReleaseId::MutationSentinelV1, + ); + let sample = TristimulusSample::new([0.1, 0.2, 0.3], relative).unwrap(); + assert_eq!( + LcsOccurrence::observe(sample, context(mismatching, 64.0)), + Err(ObserveError::FrameMismatch { + stimulus: relative, + context: mismatching, + }) + ); +} + +#[test] +fn occurrence_identity_contains_only_sample_and_context() { + let relative = frame(TristimulusScale::RelativeY1); + let sample = TristimulusSample::new([0.1, 0.2, 0.3], relative).unwrap(); + let observed = LcsOccurrence::observe(sample, context(relative, 64.0)).unwrap(); + assert_eq!(observed.sample(), sample); + assert_eq!(observed.context(), context(relative, 64.0)); +} + +#[test] +fn context_identity_changes_with_semantic_input_bits() { + let relative = frame(TristimulusScale::RelativeY1); + assert_ne!(context(relative, 64.0), context(relative, 32.0)); +} + +#[test] +fn hue_numeric_constructor_rejects_nonfinite_and_out_of_domain() { + for invalid in [f64::NAN, f64::INFINITY, -0.01, 360.0] { + assert!(HueAngle::new(invalid).is_err()); + } + assert_eq!(HueAngle::new(-0.0).unwrap(), HueAngle::new(0.0).unwrap()); +} diff --git a/crates/labcolors-core/src/lib.rs b/crates/labcolors-core/src/lib.rs index bd470c2f..35d7a8f3 100644 --- a/crates/labcolors-core/src/lib.rs +++ b/crates/labcolors-core/src/lib.rs @@ -16,6 +16,22 @@ pub(crate) mod accent_balance; pub mod alpha; pub(crate) mod analog; pub(crate) mod appearance; +#[cfg_attr( + not(test), + expect( + dead_code, + reason = "private F0 colour-identity foundation precedes the terminal public hard cut" + ) +)] +pub(crate) mod lcs_occurrence; +#[cfg_attr( + not(test), + expect( + dead_code, + reason = "private encoded point-render Session precedes its package-private WASM bridge" + ) +)] +pub(crate) mod program_session; pub mod config; pub(crate) mod constraints; pub mod glow; @@ -57,6 +73,15 @@ mod agnostic_gates; #[cfg(test)] mod appearance_graph_tests; +#[cfg(test)] +mod lcs_occurrence_tests; + +#[cfg(test)] +mod program_session_tests; + +#[cfg(test)] +mod generic_boundary_tests; + #[cfg_attr( not(test), expect( diff --git a/crates/labcolors-core/src/program_session.rs b/crates/labcolors-core/src/program_session.rs new file mode 100644 index 00000000..ebf158c8 --- /dev/null +++ b/crates/labcolors-core/src/program_session.rs @@ -0,0 +1,493 @@ +//! Generation-bound execution owner for one compiled point-render graph. +//! +//! This is deliberately below the future public `Program` boundary. It owns +//! no client vocabulary and accepts no recipe-shaped input. The sole strong +//! epoch lives in [`PointRenderOwnerV1`]; attached sessions retain only +//! a [`Weak`] reference, so successful replacement or disposal makes the old +//! graph physically unreachable from every old session. +//! +//! The first executable transport is intentionally narrow: one correlated set +//! of encoded Surface input signals per revision. It is transport-only state, +//! not an observed stimulus, physical evidence or certificate. F0 +//! observer/output/render identities remain a terminal prerequisite before any +//! such claim can be minted. Expanding the private transport to a ScenarioSet +//! does not require exposing the legacy multi-background metric matrix. +//! In particular, the wire magic is not an `lcs` or physical identity. + +use std::mem; +use std::rc::{Rc, Weak}; + +use crate::Srgb8; +use crate::appearance::{ + AdmittedAppearanceBindings, AppearanceBindings, AppearanceWorkspace, BindingError, + AppearanceGraphSpec, CompileError, CompiledAppearanceGraph, OccurrenceId, SurfaceInputPortId, +}; + +/// ASCII `LCR1`: code-owned Lab Colors Render transport version 1. +/// +/// This is a wire discriminator, not an LCS, context or physical identity. +pub(crate) const PACKED_ENCODED_SURFACE_UPDATE_MAGIC_V1: u32 = 0x4c43_5231; +pub(crate) const PACKED_ENCODED_SURFACE_UNAVAILABLE_TAG_V1: u32 = 0; +pub(crate) const PACKED_ENCODED_SURFACE_PRESENT_TAG_V1: u32 = 1; +const PACKED_ENCODED_SURFACE_HEADER_WORDS_V1: usize = 4; +const PACKED_SURFACE_UNAVAILABLE_WORDS_V1: usize = 5; + +#[derive(Debug)] +struct ProgramEpochV1 { + graph: CompiledAppearanceGraph, + binding_template: AdmittedAppearanceBindings, + surface_ports: Box<[SurfaceInputPortId]>, + occurrence_ids: Box<[OccurrenceId]>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum PointRenderEpochBuildErrorV1 { + Compile(CompileError), + Bindings(BindingError), + EmptySurfaceSchema, + EmptyOccurrenceSet, + ResourceExhausted, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum PointRenderAttachErrorV1 { + Disposed, + Bindings(BindingError), + Workspace(BindingError), +} + +/// The only strong owner of the current non-reusable program epoch. +/// +/// Replacement is prepare-then-swap: a failed build leaves the current `Rc` +/// untouched and therefore leaves all of its sessions live. No epoch number, +/// fingerprint or wrapping generation participates in this ownership proof. +#[derive(Debug, Default)] +pub(crate) struct PointRenderOwnerV1 { + current: Option>, +} + +impl PointRenderOwnerV1 { + pub(crate) fn new( + spec: AppearanceGraphSpec, + bindings: AppearanceBindings, + ) -> Result { + Ok(Self { + current: Some(Rc::new(prepare_epoch(spec, bindings)?)), + }) + } + + /// Compile/admit the complete replacement before revoking the old epoch. + pub(crate) fn replace( + &mut self, + spec: AppearanceGraphSpec, + bindings: AppearanceBindings, + ) -> Result<(), PointRenderEpochBuildErrorV1> { + let replacement = Rc::new(prepare_epoch(spec, bindings)?); + self.current = Some(replacement); + Ok(()) + } + + /// Revoke the current epoch. Existing sessions fail on their next call. + pub(crate) fn dispose(&mut self) { + self.current = None; + } + + pub(crate) fn attach(&self) -> Result { + let epoch = self + .current + .as_ref() + .ok_or(PointRenderAttachErrorV1::Disposed)?; + let workspace = epoch + .graph + .new_workspace() + .map_err(PointRenderAttachErrorV1::Workspace)?; + let bindings = epoch + .binding_template + .try_clone_v1() + .map_err(PointRenderAttachErrorV1::Bindings)?; + Ok(PointRenderSessionV1 { + epoch: Rc::downgrade(epoch), + bindings, + workspace, + state: PointRenderSessionStateV1::Waiting { + current_unavailable: None, + }, + }) + } +} + +fn prepare_epoch( + spec: AppearanceGraphSpec, + bindings: AppearanceBindings, +) -> Result { + let graph = spec + .compile() + .map_err(PointRenderEpochBuildErrorV1::Compile)?; + let surface_ports: Box<[_]> = { + let inputs = graph.surface_input_ports(); + let mut values = Vec::new(); + values + .try_reserve_exact(inputs.len()) + .map_err(|_| PointRenderEpochBuildErrorV1::ResourceExhausted)?; + values.extend(inputs); + values.into_boxed_slice() + }; + if surface_ports.is_empty() { + return Err(PointRenderEpochBuildErrorV1::EmptySurfaceSchema); + } + let occurrence_ids: Box<[_]> = { + let occurrences = graph.occurrence_ids(); + let mut values = Vec::new(); + values + .try_reserve_exact(occurrences.len()) + .map_err(|_| PointRenderEpochBuildErrorV1::ResourceExhausted)?; + values.extend(occurrences); + values.into_boxed_slice() + }; + if occurrence_ids.is_empty() { + return Err(PointRenderEpochBuildErrorV1::EmptyOccurrenceSet); + } + let binding_template = graph + .admit_bindings(&bindings) + .map_err(PointRenderEpochBuildErrorV1::Bindings)?; + Ok(ProgramEpochV1 { + graph, + binding_template, + surface_ports, + occurrence_ids, + }) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct RevisionBoundSurfaceUnavailableV1 { + revision: u64, + reason: u32, +} + +impl RevisionBoundSurfaceUnavailableV1 { + pub(crate) const fn revision(self) -> u64 { + self.revision + } + + pub(crate) const fn reason(self) -> u32 { + self.reason + } +} + +/// Compact committed value: one word per compiled occurrence, in the graph's +/// canonical occurrence order. No metric, threshold or JS-derived verdict is +/// present on this boundary. +#[derive(Debug, PartialEq, Eq)] +pub(crate) struct CompositedSignalSnapshotV1 { + revision: u64, + input_surface_signals_rgb24: Vec, + composited_occurrence_signals_rgb24: Vec, +} + +impl CompositedSignalSnapshotV1 { + pub(crate) const fn revision(&self) -> u64 { + self.revision + } + + pub(crate) fn input_surface_signals_rgb24(&self) -> &[u32] { + &self.input_surface_signals_rgb24 + } + + pub(crate) fn composited_occurrence_signals_rgb24(&self) -> &[u32] { + &self.composited_occurrence_signals_rgb24 + } +} + +#[derive(Debug, PartialEq, Eq)] +pub(crate) enum PointRenderSessionStateV1 { + Waiting { + current_unavailable: Option, + }, + Ready { + current: CompositedSignalSnapshotV1, + }, + Stale { + previous: CompositedSignalSnapshotV1, + current_unavailable: RevisionBoundSurfaceUnavailableV1, + }, +} + +impl PointRenderSessionStateV1 { + const fn head_revision(&self) -> Option { + match self { + Self::Waiting { + current_unavailable: None, + } => None, + Self::Waiting { + current_unavailable: Some(unavailable), + } + | Self::Stale { + current_unavailable: unavailable, + .. + } => Some(unavailable.revision), + Self::Ready { current } => Some(current.revision), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum PackedEncodedSurfaceUpdateErrorV1 { + HeaderTooShort, + MagicMismatch { actual: u32 }, + UnsupportedTag { actual: u32 }, + LengthMismatch { expected: usize, actual: usize }, + ReservedSignalByteNonZero { surface_index: usize, value: u32 }, + RevisionOutOfOrder { current: u64, incoming: u64 }, + RevisionConflict { revision: u64 }, + ResourceExhausted, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum PointRenderSessionUpdateErrorV1 { + ProgramExpired, + EncodedSurfaceUpdate(PackedEncodedSurfaceUpdateErrorV1), + Evaluation(BindingError), + CompiledOccurrenceMissing(OccurrenceId), +} + +enum PreparedEncodedSurfaceUpdateV1<'input> { + Unavailable(RevisionBoundSurfaceUnavailableV1), + Present { + revision: u64, + surfaces_rgb24: &'input [u32], + }, +} + +/// Generation-bound mutable runtime. It owns reusable values/scratch, never a +/// strong reference or a copy of the compiled graph. +#[derive(Debug)] +pub(crate) struct PointRenderSessionV1 { + epoch: Weak, + bindings: AdmittedAppearanceBindings, + workspace: AppearanceWorkspace, + state: PointRenderSessionStateV1, +} + +impl PointRenderSessionV1 { + pub(crate) const fn state(&self) -> &PointRenderSessionStateV1 { + &self.state + } + + /// Admit, evaluate and commit one encoded Surface-input update transaction. + pub(crate) fn update_packed( + &mut self, + words: &[u32], + ) -> Result<&PointRenderSessionStateV1, PointRenderSessionUpdateErrorV1> { + let epoch = self + .epoch + .upgrade() + .ok_or(PointRenderSessionUpdateErrorV1::ProgramExpired)?; + let prepared = decode_encoded_surface_update(words, epoch.surface_ports.len()) + .map_err(PointRenderSessionUpdateErrorV1::EncodedSurfaceUpdate)?; + let incoming_revision = match &prepared { + PreparedEncodedSurfaceUpdateV1::Unavailable(unavailable) => unavailable.revision, + PreparedEncodedSurfaceUpdateV1::Present { revision, .. } => *revision, + }; + + if let Some(current) = self.state.head_revision() { + if incoming_revision < current { + return Err(PointRenderSessionUpdateErrorV1::EncodedSurfaceUpdate( + PackedEncodedSurfaceUpdateErrorV1::RevisionOutOfOrder { + current, + incoming: incoming_revision, + }, + )); + } + if incoming_revision == current { + return self.admit_same_revision(prepared); + } + } + + match prepared { + PreparedEncodedSurfaceUpdateV1::Unavailable(unavailable) => { + let previous = take_last_ready(&mut self.state); + self.state = match previous { + Some(previous) => PointRenderSessionStateV1::Stale { + previous, + current_unavailable: unavailable, + }, + None => PointRenderSessionStateV1::Waiting { + current_unavailable: Some(unavailable), + }, + }; + } + PreparedEncodedSurfaceUpdateV1::Present { + revision, + surfaces_rgb24, + } => { + let mut committed_surfaces = Vec::new(); + committed_surfaces + .try_reserve_exact(surfaces_rgb24.len()) + .map_err(|_| { + PointRenderSessionUpdateErrorV1::EncodedSurfaceUpdate( + PackedEncodedSurfaceUpdateErrorV1::ResourceExhausted, + ) + })?; + committed_surfaces.extend_from_slice(surfaces_rgb24); + let mut output = Vec::new(); + output + .try_reserve_exact(epoch.occurrence_ids.len()) + .map_err(|_| { + PointRenderSessionUpdateErrorV1::EncodedSurfaceUpdate( + PackedEncodedSurfaceUpdateErrorV1::ResourceExhausted, + ) + })?; + + // Decode admitted the exact epoch-owned cardinality and every + // word before this loop. Each typed port therefore exists in + // the cloned admitted schema; setters cannot partially reject + // a later element. These mutable values are scratch only and + // are not published until the final state replacement below. + for (&port, &rgb24) in epoch.surface_ports.iter().zip(surfaces_rgb24.iter()) { + self.bindings + .set_surface_input(port, Srgb8::new(unpack_rgb24(rgb24))) + .map_err(PointRenderSessionUpdateErrorV1::Evaluation)?; + } + let evaluation = epoch + .graph + .evaluate_admitted_into(&self.bindings, &mut self.workspace) + .map_err(PointRenderSessionUpdateErrorV1::Evaluation)?; + for &occurrence in epoch.occurrence_ids.iter() { + let resolved = evaluation.occurrence(occurrence).ok_or( + PointRenderSessionUpdateErrorV1::CompiledOccurrenceMissing(occurrence), + )?; + output.push(pack_rgb24(resolved.visible())); + } + + self.state = PointRenderSessionStateV1::Ready { + current: CompositedSignalSnapshotV1 { + revision, + input_surface_signals_rgb24: committed_surfaces, + composited_occurrence_signals_rgb24: output, + }, + }; + } + } + Ok(&self.state) + } + + fn admit_same_revision( + &self, + prepared: PreparedEncodedSurfaceUpdateV1<'_>, + ) -> Result<&PointRenderSessionStateV1, PointRenderSessionUpdateErrorV1> { + let exact = match (prepared, &self.state) { + ( + PreparedEncodedSurfaceUpdateV1::Unavailable(incoming), + PointRenderSessionStateV1::Waiting { + current_unavailable: Some(current), + } + | PointRenderSessionStateV1::Stale { + current_unavailable: current, + .. + }, + ) => incoming == *current, + ( + PreparedEncodedSurfaceUpdateV1::Present { + revision, + surfaces_rgb24, + }, + PointRenderSessionStateV1::Ready { current }, + ) => { + revision == current.revision + && surfaces_rgb24 == current.input_surface_signals_rgb24.as_slice() + } + _ => false, + }; + if exact { + Ok(&self.state) + } else { + Err(PointRenderSessionUpdateErrorV1::EncodedSurfaceUpdate( + PackedEncodedSurfaceUpdateErrorV1::RevisionConflict { + revision: self + .state + .head_revision() + .unwrap_or_else(|| unreachable!("same-revision branch has a head")), + }, + )) + } + } +} + +fn decode_encoded_surface_update( + words: &[u32], + surface_count: usize, +) -> Result, PackedEncodedSurfaceUpdateErrorV1> { + if words.len() < PACKED_ENCODED_SURFACE_HEADER_WORDS_V1 { + return Err(PackedEncodedSurfaceUpdateErrorV1::HeaderTooShort); + } + if words[0] != PACKED_ENCODED_SURFACE_UPDATE_MAGIC_V1 { + return Err(PackedEncodedSurfaceUpdateErrorV1::MagicMismatch { actual: words[0] }); + } + let revision = u64::from(words[2]) | (u64::from(words[3]) << 32); + match words[1] { + PACKED_ENCODED_SURFACE_UNAVAILABLE_TAG_V1 => { + if words.len() != PACKED_SURFACE_UNAVAILABLE_WORDS_V1 { + return Err(PackedEncodedSurfaceUpdateErrorV1::LengthMismatch { + expected: PACKED_SURFACE_UNAVAILABLE_WORDS_V1, + actual: words.len(), + }); + } + Ok(PreparedEncodedSurfaceUpdateV1::Unavailable( + RevisionBoundSurfaceUnavailableV1 { + revision, + reason: words[4], + }, + )) + } + PACKED_ENCODED_SURFACE_PRESENT_TAG_V1 => { + let expected = PACKED_ENCODED_SURFACE_HEADER_WORDS_V1 + .checked_add(surface_count) + .ok_or(PackedEncodedSurfaceUpdateErrorV1::ResourceExhausted)?; + if words.len() != expected { + return Err(PackedEncodedSurfaceUpdateErrorV1::LengthMismatch { + expected, + actual: words.len(), + }); + } + let surfaces = &words[PACKED_ENCODED_SURFACE_HEADER_WORDS_V1..]; + for (surface_index, &value) in surfaces.iter().enumerate() { + if value & 0xff00_0000 != 0 { + return Err(PackedEncodedSurfaceUpdateErrorV1::ReservedSignalByteNonZero { + surface_index, + value, + }); + } + } + Ok(PreparedEncodedSurfaceUpdateV1::Present { + revision, + surfaces_rgb24: surfaces, + }) + } + actual => Err(PackedEncodedSurfaceUpdateErrorV1::UnsupportedTag { actual }), + } +} + +fn take_last_ready(state: &mut PointRenderSessionStateV1) -> Option { + match mem::replace( + state, + PointRenderSessionStateV1::Waiting { + current_unavailable: None, + }, + ) { + PointRenderSessionStateV1::Waiting { .. } => None, + PointRenderSessionStateV1::Ready { current } => Some(current), + PointRenderSessionStateV1::Stale { previous, .. } => Some(previous), + } +} + +const fn unpack_rgb24(word: u32) -> [u8; 3] { + [ + ((word >> 16) & 0xff) as u8, + ((word >> 8) & 0xff) as u8, + (word & 0xff) as u8, + ] +} + +const fn pack_rgb24(bytes: [u8; 3]) -> u32 { + ((bytes[0] as u32) << 16) | ((bytes[1] as u32) << 8) | bytes[2] as u32 +} diff --git a/crates/labcolors-core/src/program_session_tests.rs b/crates/labcolors-core/src/program_session_tests.rs new file mode 100644 index 00000000..3e42db30 --- /dev/null +++ b/crates/labcolors-core/src/program_session_tests.rs @@ -0,0 +1,299 @@ +use crate::Srgb8; +use crate::appearance::{ + AppearanceBindings, AppearanceGraphSpec, BindingError, ColorInputId, CompileError, OccurrenceId, + OccurrenceSpec, OpacityInputId, PaintId, PaintSpec, SurfaceId, SurfaceInputPortId, + SurfaceSpec, +}; +use crate::composition::CompositionProfileV1; +use crate::program_session::{ + PACKED_ENCODED_SURFACE_PRESENT_TAG_V1, PACKED_ENCODED_SURFACE_UNAVAILABLE_TAG_V1, + PACKED_ENCODED_SURFACE_UPDATE_MAGIC_V1, PackedEncodedSurfaceUpdateErrorV1, + PointRenderEpochBuildErrorV1, PointRenderOwnerV1, PointRenderSessionStateV1, + PointRenderSessionUpdateErrorV1, +}; + +const COLOR: ColorInputId = ColorInputId::new(1); +const SURFACE_PORT: SurfaceInputPortId = SurfaceInputPortId::new(2); +const OPACITY: OpacityInputId = OpacityInputId::new(3); +const SOLID: PaintId = PaintId::new(10); +const TRANSLUCENT: PaintId = PaintId::new(11); +const BACKDROP: SurfaceId = SurfaceId::new(20); +const VISIBLE: SurfaceId = SurfaceId::new(21); +const OCCURRENCE: OccurrenceId = OccurrenceId::new(30); + +fn graph_spec() -> AppearanceGraphSpec { + graph_spec_against(BACKDROP) +} + +fn graph_spec_against(against: SurfaceId) -> AppearanceGraphSpec { + AppearanceGraphSpec::new( + vec![COLOR], + vec![SURFACE_PORT], + vec![OPACITY], + vec![ + PaintSpec::Solid { + id: SOLID, + color: COLOR, + }, + PaintSpec::Opacity { + id: TRANSLUCENT, + source: SOLID, + opacity: OPACITY, + }, + ], + vec![ + SurfaceSpec::Input { + id: BACKDROP, + port: SURFACE_PORT, + }, + SurfaceSpec::FromOccurrence { + id: VISIBLE, + occurrence: OCCURRENCE, + }, + ], + vec![OccurrenceSpec { + id: OCCURRENCE, + subject: TRANSLUCENT, + against, + profile: CompositionProfileV1::EncodedSrgb8SourceOverV1, + }], + ) +} + +fn cyclic_graph_spec() -> AppearanceGraphSpec { + AppearanceGraphSpec::new( + vec![COLOR], + vec![SURFACE_PORT], + vec![OPACITY], + vec![ + PaintSpec::Solid { + id: SOLID, + color: COLOR, + }, + PaintSpec::Opacity { + id: TRANSLUCENT, + source: SOLID, + opacity: OPACITY, + }, + ], + vec![SurfaceSpec::FromOccurrence { + id: VISIBLE, + occurrence: OCCURRENCE, + }], + vec![OccurrenceSpec { + id: OCCURRENCE, + subject: TRANSLUCENT, + against: VISIBLE, + profile: CompositionProfileV1::EncodedSrgb8SourceOverV1, + }], + ) +} + +fn bindings(opacity: f64) -> AppearanceBindings { + AppearanceBindings::new( + vec![(COLOR, Srgb8::new([0; 3]))], + vec![(SURFACE_PORT, Srgb8::new([0; 3]))], + vec![(OPACITY, opacity)], + ) +} + +fn point(revision: u64, rgb24: u32) -> Vec { + vec![ + PACKED_ENCODED_SURFACE_UPDATE_MAGIC_V1, + PACKED_ENCODED_SURFACE_PRESENT_TAG_V1, + revision as u32, + (revision >> 32) as u32, + rgb24, + ] +} + +fn unavailable(revision: u64, reason: u32) -> Vec { + vec![ + PACKED_ENCODED_SURFACE_UPDATE_MAGIC_V1, + PACKED_ENCODED_SURFACE_UNAVAILABLE_TAG_V1, + revision as u32, + (revision >> 32) as u32, + reason, + ] +} + +#[test] +fn point_update_executes_the_compiled_graph_and_commits_compact_occurrences() { + let owner = PointRenderOwnerV1::new(graph_spec(), bindings(0.5)).unwrap(); + let mut session = owner.attach().unwrap(); + + let PointRenderSessionStateV1::Ready { current } = + session.update_packed(&point(1, 0xff_ff_ff)).unwrap() + else { + panic!("present encoded Surface signals must produce Ready"); + }; + assert_eq!(current.revision(), 1); + assert_eq!(current.input_surface_signals_rgb24(), &[0xff_ff_ff]); + assert_eq!(current.composited_occurrence_signals_rgb24(), &[0x80_80_80]); +} + +#[test] +fn successful_replace_revokes_old_sessions_without_a_numeric_generation() { + let mut owner = PointRenderOwnerV1::new(graph_spec(), bindings(0.5)).unwrap(); + let mut old = owner.attach().unwrap(); + + owner.replace(graph_spec(), bindings(0.25)).unwrap(); + assert_eq!( + old.update_packed(&point(1, 0xff_ff_ff)), + Err(PointRenderSessionUpdateErrorV1::ProgramExpired) + ); + + let mut current = owner.attach().unwrap(); + assert!(matches!( + current.update_packed(&point(1, 0xff_ff_ff)).unwrap(), + PointRenderSessionStateV1::Ready { .. } + )); +} + +#[test] +fn invalid_opacity_failed_replace_is_atomic_and_keeps_old_epoch_live() { + let mut owner = PointRenderOwnerV1::new(graph_spec(), bindings(0.5)).unwrap(); + let mut session = owner.attach().unwrap(); + + assert!(matches!( + owner.replace(graph_spec(), bindings(1.25)), + Err(PointRenderEpochBuildErrorV1::Bindings( + BindingError::OpacityOutOfDomain { input: OPACITY, .. } + )) + )); + let PointRenderSessionStateV1::Ready { current } = + session.update_packed(&point(1, 0xff_ff_ff)).unwrap() + else { + panic!("failed replacement must not revoke the old epoch"); + }; + assert_eq!(current.composited_occurrence_signals_rgb24(), &[0x80_80_80]); +} + +#[test] +fn dangling_and_cyclic_failed_compiles_do_not_revoke_the_current_epoch() { + let mut owner = PointRenderOwnerV1::new(graph_spec(), bindings(0.5)).unwrap(); + let mut session = owner.attach().unwrap(); + + let missing = SurfaceId::new(999); + assert!(matches!( + owner.replace(graph_spec_against(missing), bindings(0.5)), + Err(PointRenderEpochBuildErrorV1::Compile( + CompileError::MissingOccurrenceBackdrop { + occurrence: OCCURRENCE, + surface + } + )) if surface == missing + )); + assert!(matches!( + owner.replace(cyclic_graph_spec(), bindings(0.5)), + Err(PointRenderEpochBuildErrorV1::Compile( + CompileError::RenderCycle { .. } + )) + )); + + let PointRenderSessionStateV1::Ready { current } = + session.update_packed(&point(1, 0xff_ff_ff)).unwrap() + else { + panic!("compile failures must leave the old strong epoch untouched"); + }; + assert_eq!(current.composited_occurrence_signals_rgb24(), &[0x80_80_80]); +} + +#[test] +fn dispose_revokes_sessions_and_prevents_new_attachment() { + let mut owner = PointRenderOwnerV1::new(graph_spec(), bindings(0.5)).unwrap(); + let mut session = owner.attach().unwrap(); + owner.dispose(); + + assert_eq!( + session.update_packed(&unavailable(1, 7)), + Err(PointRenderSessionUpdateErrorV1::ProgramExpired) + ); + assert!(owner.attach().is_err()); +} + +#[test] +fn unavailable_after_ready_is_stale_and_retains_exactly_one_previous_snapshot() { + let owner = PointRenderOwnerV1::new(graph_spec(), bindings(0.5)).unwrap(); + let mut session = owner.attach().unwrap(); + session.update_packed(&point(1, 0xff_ff_ff)).unwrap(); + + let PointRenderSessionStateV1::Stale { + previous, + current_unavailable, + } = session.update_packed(&unavailable(2, 91)).unwrap() + else { + panic!("unavailable Surface input after Ready must become Stale"); + }; + assert_eq!(previous.revision(), 1); + assert_eq!(previous.composited_occurrence_signals_rgb24(), &[0x80_80_80]); + assert_eq!(current_unavailable.revision(), 2); + assert_eq!(current_unavailable.reason(), 91); + + let PointRenderSessionStateV1::Stale { previous, .. } = + session.update_packed(&unavailable(3, 92)).unwrap() + else { + panic!("a later unavailable update must remain Stale"); + }; + assert_eq!(previous.revision(), 1); +} + +#[test] +fn malformed_lower_and_conflicting_updates_are_atomic_and_do_not_evaluate() { + let owner = PointRenderOwnerV1::new(graph_spec(), bindings(0.5)).unwrap(); + let mut session = owner.attach().unwrap(); + session.update_packed(&point(5, 0xff_ff_ff)).unwrap(); + crate::composition::reset_source_over_evaluation_count(); + + let malformed = point(6, 0x01_ff_ff_ff); + assert_eq!( + session.update_packed(&malformed), + Err(PointRenderSessionUpdateErrorV1::EncodedSurfaceUpdate( + PackedEncodedSurfaceUpdateErrorV1::ReservedSignalByteNonZero { + surface_index: 0, + value: 0x01_ff_ff_ff, + } + )) + ); + assert_eq!(crate::composition::source_over_evaluation_count(), 0); + + assert!(matches!( + session.update_packed(&point(4, 0)), + Err(PointRenderSessionUpdateErrorV1::EncodedSurfaceUpdate( + PackedEncodedSurfaceUpdateErrorV1::RevisionOutOfOrder { + current: 5, + incoming: 4 + } + )) + )); + assert_eq!(crate::composition::source_over_evaluation_count(), 0); + + assert!(matches!( + session.update_packed(&point(5, 0)), + Err(PointRenderSessionUpdateErrorV1::EncodedSurfaceUpdate( + PackedEncodedSurfaceUpdateErrorV1::RevisionConflict { revision: 5 } + )) + )); + assert_eq!(crate::composition::source_over_evaluation_count(), 0); + + let PointRenderSessionStateV1::Ready { current } = session.state() else { + panic!("every rejected update must leave the committed state untouched"); + }; + assert_eq!(current.revision(), 5); + assert_eq!(current.composited_occurrence_signals_rgb24(), &[0x80_80_80]); +} + +#[test] +fn exact_replay_is_idempotent_but_a_new_revision_evaluates_again() { + let owner = PointRenderOwnerV1::new(graph_spec(), bindings(0.5)).unwrap(); + let mut session = owner.attach().unwrap(); + let payload = point(1, 0xff_ff_ff); + crate::composition::reset_source_over_evaluation_count(); + + session.update_packed(&payload).unwrap(); + assert_eq!(crate::composition::source_over_evaluation_count(), 1); + session.update_packed(&payload).unwrap(); + assert_eq!(crate::composition::source_over_evaluation_count(), 1); + session.update_packed(&point(2, 0xff_ff_ff)).unwrap(); + assert_eq!(crate::composition::source_over_evaluation_count(), 2); +} diff --git a/scripts/verify_point_support_surplus.py b/scripts/verify_point_support_surplus.py index 682cc3da..c5c0a707 100644 --- a/scripts/verify_point_support_surplus.py +++ b/scripts/verify_point_support_surplus.py @@ -57,7 +57,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 = ( - "c2825216354b796924560d98e01ae5cebedf324c47e7b26332119c61aded783e" + "2647fad1457ec9743766503b4056bd9a002f7757d0ca60777c358e1085a02cbb" ) EXPECTED_Q55_PROOF_SHA256 = ( "ac59cf89503170c789223b91d775213a19d4e571ef930f2ea609fcd51b14defd" From 103135ca53b3d4724359f9d2c946e09693b631a1 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Wed, 22 Jul 2026 05:38:30 +0300 Subject: [PATCH 02/58] fix(core): satisfy format and all-target lint gates --- ...rt-reference-surplus-q55-bps-proof-v1.json | 2 +- crates/labcolors-core/src/appearance.rs | 10 ++----- crates/labcolors-core/src/composition.rs | 7 ----- crates/labcolors-core/src/lcs_occurrence.rs | 3 +- crates/labcolors-core/src/lib.rs | 29 +++++++++---------- crates/labcolors-core/src/program_session.rs | 14 +++++---- .../src/program_session_tests.rs | 11 ++++--- scripts/verify_point_support_surplus.py | 2 +- 8 files changed, 33 insertions(+), 45 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 066ea0d0..63413c9e 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":"109ae673288f47ef1f5da949cd7c19883e4e33b62f332931f96de912239b70cc","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":"2647fad1457ec9743766503b4056bd9a002f7757d0ca60777c358e1085a02cbb","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":"553e904fcecff4ccc936e1f3cffd642eed6b221ad2e7a28238da55d2050b7e37"},{"kind":"rust-source","path":"crates/labcolors-core/src/composition.rs","sha256":"5218a845b85a27571a710c7c967b2937b94cf4622a3073487a808936ac85468a"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/exact.rs","sha256":"45bd09cd8f6f74860a0a3ffeb410ee2c3cf91b4868877abbc949e724b61e1ccd"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/mod.rs","sha256":"ad721c625b33a7432de44e5f63459a62c1cbc8ec2492855f783b5440f04978f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/wcag22.rs","sha256":"985e21abccd1fcb4a3f9624d5947ff3d1af7a1bf26b6b170ea8bbef77d6f1798"},{"kind":"rust-source","path":"crates/labcolors-core/src/hash.rs","sha256":"f97a0fd7d6ad3162f0f1dfb326fccfb7ed40da9a8fa67a5b8a239a1ae2ae49c3"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"d42d4f0bdfd64efc5d2956334dea9d07996849b8185ce448ec61a8bf38b0b684"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"208066b255bc406303b18dc2eb6f1720c8203ea6eb37d225f9d8110c2449edc4"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"56c06b49f4687b3def69c247978da1b0cb45b42c09515a001d196cb1457c93a6"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"d4b06651e07cf4f93eccf9866f6e63c35ea72fa7125bdb41e222b19b70dd2f32"},{"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":33,"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":"7c8ada7078061e6b67eae380bbb326e32de6d533647fb03ff686a233f3975e34"} +{"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":"0571feddc1f67729f547d5b17bedb335c42a00873d46f6784850c6c43d7270ed","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":"3d37fc38e4003c16c8ac68f957576153b9e734bfd7909c138d87a5e77685adf4","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":"d6d7e7230a39e4e86324a40972423cdac8309ed4a8590f2223cd219c0860d4e0"},{"kind":"rust-source","path":"crates/labcolors-core/src/composition.rs","sha256":"195a67327a3bd86d7816b634481389930bf68577bb1202fad14c2ea152df8625"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/exact.rs","sha256":"45bd09cd8f6f74860a0a3ffeb410ee2c3cf91b4868877abbc949e724b61e1ccd"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/mod.rs","sha256":"ad721c625b33a7432de44e5f63459a62c1cbc8ec2492855f783b5440f04978f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/wcag22.rs","sha256":"985e21abccd1fcb4a3f9624d5947ff3d1af7a1bf26b6b170ea8bbef77d6f1798"},{"kind":"rust-source","path":"crates/labcolors-core/src/hash.rs","sha256":"f97a0fd7d6ad3162f0f1dfb326fccfb7ed40da9a8fa67a5b8a239a1ae2ae49c3"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"1c095f5c98a8699e723c41ee68604754b752543b5d5085463e666b229a85e24f"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"208066b255bc406303b18dc2eb6f1720c8203ea6eb37d225f9d8110c2449edc4"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"56c06b49f4687b3def69c247978da1b0cb45b42c09515a001d196cb1457c93a6"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"d4b06651e07cf4f93eccf9866f6e63c35ea72fa7125bdb41e222b19b70dd2f32"},{"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":33,"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":"9e59298d8101b01e893dd8e26f1818dad075f4ef3f069db349237c7376f49e41"} diff --git a/crates/labcolors-core/src/appearance.rs b/crates/labcolors-core/src/appearance.rs index 37bb6e9e..04ff10fe 100644 --- a/crates/labcolors-core/src/appearance.rs +++ b/crates/labcolors-core/src/appearance.rs @@ -1464,12 +1464,7 @@ impl AppearanceEvaluationView<'_, '_> { surfaces .try_reserve_exact(self.workspace.surfaces.len()) .map_err(|_| BindingError::ResourceExhausted)?; - for (spec, value) in self - .program - .surfaces - .iter() - .zip(&self.workspace.surfaces) - { + for (spec, value) in self.program.surfaces.iter().zip(&self.workspace.surfaces) { surfaces.push(( spec.id(), value.unwrap_or_else(|| unreachable!("render topo covers every Surface")), @@ -1548,8 +1543,7 @@ impl CompiledAppearanceGraph { bindings: &AdmittedAppearanceBindings, workspace: &'workspace mut AppearanceWorkspace, ) -> Result, BindingError> { - self.program() - .evaluate_admitted_into(bindings, workspace) + self.program().evaluate_admitted_into(bindings, workspace) } /// Cold convenience внутри Core для callers, которым нужен owned result. diff --git a/crates/labcolors-core/src/composition.rs b/crates/labcolors-core/src/composition.rs index ffeb7dd0..3f5469dc 100644 --- a/crates/labcolors-core/src/composition.rs +++ b/crates/labcolors-core/src/composition.rs @@ -103,13 +103,6 @@ pub(crate) fn source_over_channel_srgb8(tint: u8, alpha: f64, backdrop: u8) -> u source_over_channel_value(tint, alpha, backdrop).round() as u8 } -#[cfg(test)] -pub(crate) fn validate_alpha(alpha: f64) -> Result<(), String> { - AdmittedOpacityV1::new(alpha) - .map(|_| ()) - .map_err(|_| format!("alpha вне конечного [0,1]: {alpha}")) -} - pub(crate) fn source_over_srgb8( tint: [u8; 3], alpha: f64, diff --git a/crates/labcolors-core/src/lcs_occurrence.rs b/crates/labcolors-core/src/lcs_occurrence.rs index 3e7e9af6..a847d1b2 100644 --- a/crates/labcolors-core/src/lcs_occurrence.rs +++ b/crates/labcolors-core/src/lcs_occurrence.rs @@ -206,8 +206,7 @@ impl AppearanceContextId { if adapting_luminance.get() == 0.0 { return Err(NumericDomainError::NotPositive); } - let background_relative_luminance = - FiniteNonNegative::new(background_relative_luminance)?; + let background_relative_luminance = FiniteNonNegative::new(background_relative_luminance)?; if background_relative_luminance.get() == 0.0 { return Err(NumericDomainError::NotPositive); } diff --git a/crates/labcolors-core/src/lib.rs b/crates/labcolors-core/src/lib.rs index 35d7a8f3..08d29382 100644 --- a/crates/labcolors-core/src/lib.rs +++ b/crates/labcolors-core/src/lib.rs @@ -16,28 +16,17 @@ pub(crate) mod accent_balance; pub mod alpha; pub(crate) mod analog; pub(crate) mod appearance; -#[cfg_attr( - not(test), - expect( - dead_code, - reason = "private F0 colour-identity foundation precedes the terminal public hard cut" - ) -)] -pub(crate) mod lcs_occurrence; -#[cfg_attr( - not(test), - expect( - dead_code, - reason = "private encoded point-render Session precedes its package-private WASM bridge" - ) -)] -pub(crate) mod program_session; pub mod config; pub(crate) mod constraints; pub mod glow; pub mod hash; pub mod ladder; pub mod lcs; +#[expect( + dead_code, + reason = "private F0 colour-identity foundation precedes the terminal public hard cut" +)] +pub(crate) mod lcs_occurrence; pub(crate) mod lpc; pub mod material; pub mod neutral; @@ -51,6 +40,14 @@ pub(crate) mod pair; ) )] pub(crate) mod point_support; +#[cfg_attr( + not(test), + expect( + dead_code, + reason = "private encoded point-render Session precedes its package-private WASM bridge" + ) +)] +pub(crate) mod program_session; pub mod scale; pub mod semantic; pub mod solve; diff --git a/crates/labcolors-core/src/program_session.rs b/crates/labcolors-core/src/program_session.rs index ebf158c8..75569c99 100644 --- a/crates/labcolors-core/src/program_session.rs +++ b/crates/labcolors-core/src/program_session.rs @@ -19,8 +19,8 @@ use std::rc::{Rc, Weak}; use crate::Srgb8; use crate::appearance::{ - AdmittedAppearanceBindings, AppearanceBindings, AppearanceWorkspace, BindingError, - AppearanceGraphSpec, CompileError, CompiledAppearanceGraph, OccurrenceId, SurfaceInputPortId, + AdmittedAppearanceBindings, AppearanceBindings, AppearanceGraphSpec, AppearanceWorkspace, + BindingError, CompileError, CompiledAppearanceGraph, OccurrenceId, SurfaceInputPortId, }; /// ASCII `LCR1`: code-owned Lab Colors Render transport version 1. @@ -452,10 +452,12 @@ fn decode_encoded_surface_update( let surfaces = &words[PACKED_ENCODED_SURFACE_HEADER_WORDS_V1..]; for (surface_index, &value) in surfaces.iter().enumerate() { if value & 0xff00_0000 != 0 { - return Err(PackedEncodedSurfaceUpdateErrorV1::ReservedSignalByteNonZero { - surface_index, - value, - }); + return Err( + PackedEncodedSurfaceUpdateErrorV1::ReservedSignalByteNonZero { + surface_index, + value, + }, + ); } } Ok(PreparedEncodedSurfaceUpdateV1::Present { diff --git a/crates/labcolors-core/src/program_session_tests.rs b/crates/labcolors-core/src/program_session_tests.rs index 3e42db30..3357c522 100644 --- a/crates/labcolors-core/src/program_session_tests.rs +++ b/crates/labcolors-core/src/program_session_tests.rs @@ -1,8 +1,8 @@ use crate::Srgb8; use crate::appearance::{ - AppearanceBindings, AppearanceGraphSpec, BindingError, ColorInputId, CompileError, OccurrenceId, - OccurrenceSpec, OpacityInputId, PaintId, PaintSpec, SurfaceId, SurfaceInputPortId, - SurfaceSpec, + AppearanceBindings, AppearanceGraphSpec, BindingError, ColorInputId, CompileError, + OccurrenceId, OccurrenceSpec, OpacityInputId, PaintId, PaintSpec, SurfaceId, + SurfaceInputPortId, SurfaceSpec, }; use crate::composition::CompositionProfileV1; use crate::program_session::{ @@ -226,7 +226,10 @@ fn unavailable_after_ready_is_stale_and_retains_exactly_one_previous_snapshot() panic!("unavailable Surface input after Ready must become Stale"); }; assert_eq!(previous.revision(), 1); - assert_eq!(previous.composited_occurrence_signals_rgb24(), &[0x80_80_80]); + assert_eq!( + previous.composited_occurrence_signals_rgb24(), + &[0x80_80_80] + ); assert_eq!(current_unavailable.revision(), 2); assert_eq!(current_unavailable.reason(), 91); diff --git a/scripts/verify_point_support_surplus.py b/scripts/verify_point_support_surplus.py index c5c0a707..111dcc47 100644 --- a/scripts/verify_point_support_surplus.py +++ b/scripts/verify_point_support_surplus.py @@ -57,7 +57,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 = ( - "2647fad1457ec9743766503b4056bd9a002f7757d0ca60777c358e1085a02cbb" + "3d37fc38e4003c16c8ac68f957576153b9e734bfd7909c138d87a5e77685adf4" ) EXPECTED_Q55_PROOF_SHA256 = ( "ac59cf89503170c789223b91d775213a19d4e571ef930f2ea609fcd51b14defd" From dea02c0c3291fc9364c265a2ce90c6a2d26b7bd4 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Wed, 22 Jul 2026 05:56:03 +0300 Subject: [PATCH 03/58] perf(core): recycle point-render session buffers --- crates/labcolors-core/src/program_session.rs | 153 +++++++++++++----- .../src/program_session_tests.rs | 142 +++++++++++++++- 2 files changed, 247 insertions(+), 48 deletions(-) diff --git a/crates/labcolors-core/src/program_session.rs b/crates/labcolors-core/src/program_session.rs index 75569c99..4aee3f7b 100644 --- a/crates/labcolors-core/src/program_session.rs +++ b/crates/labcolors-core/src/program_session.rs @@ -20,7 +20,7 @@ use std::rc::{Rc, Weak}; use crate::Srgb8; use crate::appearance::{ AdmittedAppearanceBindings, AppearanceBindings, AppearanceGraphSpec, AppearanceWorkspace, - BindingError, CompileError, CompiledAppearanceGraph, OccurrenceId, SurfaceInputPortId, + BindingError, CompileError, CompiledAppearanceGraph, SurfaceInputPortId, }; /// ASCII `LCR1`: code-owned Lab Colors Render transport version 1. @@ -37,7 +37,7 @@ struct ProgramEpochV1 { graph: CompiledAppearanceGraph, binding_template: AdmittedAppearanceBindings, surface_ports: Box<[SurfaceInputPortId]>, - occurrence_ids: Box<[OccurrenceId]>, + occurrence_count: usize, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -54,6 +54,7 @@ pub(crate) enum PointRenderAttachErrorV1 { Disposed, Bindings(BindingError), Workspace(BindingError), + ResourceExhausted, } /// The only strong owner of the current non-reusable program epoch. @@ -105,10 +106,15 @@ impl PointRenderOwnerV1 { .binding_template .try_clone_v1() .map_err(PointRenderAttachErrorV1::Bindings)?; + let initial_signal_buffers = CompositedSignalBuffersV1::try_new( + epoch.surface_ports.len(), + epoch.occurrence_count, + )?; Ok(PointRenderSessionV1 { epoch: Rc::downgrade(epoch), bindings, workspace, + initial_signal_buffers: Some(initial_signal_buffers), state: PointRenderSessionStateV1::Waiting { current_unavailable: None, }, @@ -116,6 +122,15 @@ impl PointRenderOwnerV1 { } } +fn try_zeroed_signal_words(len: usize) -> Result, PointRenderAttachErrorV1> { + let mut words = Vec::new(); + words + .try_reserve_exact(len) + .map_err(|_| PointRenderAttachErrorV1::ResourceExhausted)?; + words.resize(len, 0); + Ok(words) +} + fn prepare_epoch( spec: AppearanceGraphSpec, bindings: AppearanceBindings, @@ -135,16 +150,8 @@ fn prepare_epoch( if surface_ports.is_empty() { return Err(PointRenderEpochBuildErrorV1::EmptySurfaceSchema); } - let occurrence_ids: Box<[_]> = { - let occurrences = graph.occurrence_ids(); - let mut values = Vec::new(); - values - .try_reserve_exact(occurrences.len()) - .map_err(|_| PointRenderEpochBuildErrorV1::ResourceExhausted)?; - values.extend(occurrences); - values.into_boxed_slice() - }; - if occurrence_ids.is_empty() { + let occurrence_count = graph.occurrence_ids().len(); + if occurrence_count == 0 { return Err(PointRenderEpochBuildErrorV1::EmptyOccurrenceSet); } let binding_template = graph @@ -154,7 +161,7 @@ fn prepare_epoch( graph, binding_template, surface_ports, - occurrence_ids, + occurrence_count, }) } @@ -178,23 +185,40 @@ impl RevisionBoundSurfaceUnavailableV1 { /// canonical occurrence order. No metric, threshold or JS-derived verdict is /// present on this boundary. #[derive(Debug, PartialEq, Eq)] -pub(crate) struct CompositedSignalSnapshotV1 { - revision: u64, +struct CompositedSignalBuffersV1 { input_surface_signals_rgb24: Vec, composited_occurrence_signals_rgb24: Vec, } +impl CompositedSignalBuffersV1 { + fn try_new( + surface_count: usize, + occurrence_count: usize, + ) -> Result { + Ok(Self { + input_surface_signals_rgb24: try_zeroed_signal_words(surface_count)?, + composited_occurrence_signals_rgb24: try_zeroed_signal_words(occurrence_count)?, + }) + } +} + +#[derive(Debug, PartialEq, Eq)] +pub(crate) struct CompositedSignalSnapshotV1 { + revision: u64, + buffers: CompositedSignalBuffersV1, +} + impl CompositedSignalSnapshotV1 { pub(crate) const fn revision(&self) -> u64 { self.revision } pub(crate) fn input_surface_signals_rgb24(&self) -> &[u32] { - &self.input_surface_signals_rgb24 + &self.buffers.input_surface_signals_rgb24 } pub(crate) fn composited_occurrence_signals_rgb24(&self) -> &[u32] { - &self.composited_occurrence_signals_rgb24 + &self.buffers.composited_occurrence_signals_rgb24 } } @@ -247,7 +271,6 @@ pub(crate) enum PointRenderSessionUpdateErrorV1 { ProgramExpired, EncodedSurfaceUpdate(PackedEncodedSurfaceUpdateErrorV1), Evaluation(BindingError), - CompiledOccurrenceMissing(OccurrenceId), } enum PreparedEncodedSurfaceUpdateV1<'input> { @@ -259,12 +282,15 @@ enum PreparedEncodedSurfaceUpdateV1<'input> { } /// Generation-bound mutable runtime. It owns reusable values/scratch, never a -/// strong reference or a copy of the compiled graph. +/// strong reference or a copy of the compiled graph. All fixed-cardinality +/// signal buffers are allocated fallibly by `attach`; `update_packed` only +/// moves and overwrites their ownership after evaluation succeeds. #[derive(Debug)] pub(crate) struct PointRenderSessionV1 { epoch: Weak, bindings: AdmittedAppearanceBindings, workspace: AppearanceWorkspace, + initial_signal_buffers: Option, state: PointRenderSessionStateV1, } @@ -320,23 +346,34 @@ impl PointRenderSessionV1 { revision, surfaces_rgb24, } => { - let mut committed_surfaces = Vec::new(); - committed_surfaces - .try_reserve_exact(surfaces_rgb24.len()) - .map_err(|_| { - PointRenderSessionUpdateErrorV1::EncodedSurfaceUpdate( - PackedEncodedSurfaceUpdateErrorV1::ResourceExhausted, - ) - })?; - committed_surfaces.extend_from_slice(surfaces_rgb24); - let mut output = Vec::new(); - output - .try_reserve_exact(epoch.occurrence_ids.len()) - .map_err(|_| { - PointRenderSessionUpdateErrorV1::EncodedSurfaceUpdate( - PackedEncodedSurfaceUpdateErrorV1::ResourceExhausted, - ) - })?; + let retained_shape_matches = match &self.state { + PointRenderSessionStateV1::Waiting { .. } => self + .initial_signal_buffers + .as_ref() + .is_some_and(|buffers| { + buffers.input_surface_signals_rgb24.len() + == epoch.surface_ports.len() + && buffers.composited_occurrence_signals_rgb24.len() + == epoch.occurrence_count + }), + PointRenderSessionStateV1::Ready { current } => { + current.buffers.input_surface_signals_rgb24.len() + == epoch.surface_ports.len() + && current.buffers.composited_occurrence_signals_rgb24.len() + == epoch.occurrence_count + } + PointRenderSessionStateV1::Stale { previous, .. } => { + previous.buffers.input_surface_signals_rgb24.len() + == epoch.surface_ports.len() + && previous.buffers.composited_occurrence_signals_rgb24.len() + == epoch.occurrence_count + } + }; + if !retained_shape_matches { + return Err(PointRenderSessionUpdateErrorV1::Evaluation( + BindingError::IncompatibleWorkspace, + )); + } // Decode admitted the exact epoch-owned cardinality and every // word before this loop. Each typed port therefore exists in @@ -352,18 +389,45 @@ impl PointRenderSessionV1 { .graph .evaluate_admitted_into(&self.bindings, &mut self.workspace) .map_err(PointRenderSessionUpdateErrorV1::Evaluation)?; - for &occurrence in epoch.occurrence_ids.iter() { - let resolved = evaluation.occurrence(occurrence).ok_or( - PointRenderSessionUpdateErrorV1::CompiledOccurrenceMissing(occurrence), - )?; - output.push(pack_rgb24(resolved.visible())); + if evaluation.occurrences().len() != epoch.occurrence_count { + return Err(PointRenderSessionUpdateErrorV1::Evaluation( + BindingError::IncompatibleWorkspace, + )); } + // No fallible work follows. Preserve the committed snapshot + // through decode, binding mutation and evaluation; only now + // reclaim the one fixed buffer pair and overwrite it. + let mut buffers = match take_last_ready(&mut self.state) { + Some(previous) => previous.buffers, + None => self.initial_signal_buffers.take().unwrap_or_else(|| { + unreachable!("a Session without prior Ready must retain initial buffers") + }), + }; + debug_assert_eq!( + buffers.input_surface_signals_rgb24.len(), + surfaces_rgb24.len() + ); + debug_assert_eq!( + buffers.composited_occurrence_signals_rgb24.len(), + epoch.occurrence_count + ); + buffers + .input_surface_signals_rgb24 + .copy_from_slice(surfaces_rgb24); + for (resolved, output) in evaluation + .occurrences() + .zip(buffers.composited_occurrence_signals_rgb24.iter_mut()) + { + // Packing an already resolved encoded point is infallible. + // Any future fallible verifier must finish before buffer + // reclamation above (or introduce its own staging value). + *output = pack_rgb24(resolved.visible()); + } self.state = PointRenderSessionStateV1::Ready { current: CompositedSignalSnapshotV1 { revision, - input_surface_signals_rgb24: committed_surfaces, - composited_occurrence_signals_rgb24: output, + buffers, }, }; } @@ -394,7 +458,8 @@ impl PointRenderSessionV1 { PointRenderSessionStateV1::Ready { current }, ) => { revision == current.revision - && surfaces_rgb24 == current.input_surface_signals_rgb24.as_slice() + && surfaces_rgb24 + == current.buffers.input_surface_signals_rgb24.as_slice() } _ => false, }; diff --git a/crates/labcolors-core/src/program_session_tests.rs b/crates/labcolors-core/src/program_session_tests.rs index 3357c522..c1495f49 100644 --- a/crates/labcolors-core/src/program_session_tests.rs +++ b/crates/labcolors-core/src/program_session_tests.rs @@ -97,8 +97,8 @@ fn bindings(opacity: f64) -> AppearanceBindings { ) } -fn point(revision: u64, rgb24: u32) -> Vec { - vec![ +fn point(revision: u64, rgb24: u32) -> [u32; 5] { + [ PACKED_ENCODED_SURFACE_UPDATE_MAGIC_V1, PACKED_ENCODED_SURFACE_PRESENT_TAG_V1, revision as u32, @@ -107,8 +107,8 @@ fn point(revision: u64, rgb24: u32) -> Vec { ] } -fn unavailable(revision: u64, reason: u32) -> Vec { - vec![ +fn unavailable(revision: u64, reason: u32) -> [u32; 5] { + [ PACKED_ENCODED_SURFACE_UPDATE_MAGIC_V1, PACKED_ENCODED_SURFACE_UNAVAILABLE_TAG_V1, revision as u32, @@ -117,6 +117,22 @@ fn unavailable(revision: u64, reason: u32) -> Vec { ] } +fn retained_signal_storage_pointers( + state: &PointRenderSessionStateV1, +) -> (*const u32, *const u32) { + let snapshot = match state { + PointRenderSessionStateV1::Ready { current } => current, + PointRenderSessionStateV1::Stale { previous, .. } => previous, + PointRenderSessionStateV1::Waiting { .. } => { + panic!("the allocation test requires a retained successful snapshot") + } + }; + ( + snapshot.input_surface_signals_rgb24().as_ptr(), + snapshot.composited_occurrence_signals_rgb24().as_ptr(), + ) +} + #[test] fn point_update_executes_the_compiled_graph_and_commits_compact_occurrences() { let owner = PointRenderOwnerV1::new(graph_spec(), bindings(0.5)).unwrap(); @@ -300,3 +316,121 @@ fn exact_replay_is_idempotent_but_a_new_revision_evaluates_again() { session.update_packed(&point(2, 0xff_ff_ff)).unwrap(); assert_eq!(crate::composition::source_over_evaluation_count(), 2); } + +#[test] +fn attached_session_reuses_buffers_for_every_update_state() { + let owner = PointRenderOwnerV1::new(graph_spec(), bindings(0.5)).unwrap(); + let mut session = owner.attach().unwrap(); + let first = point(1, 0xff_ff_ff); + let second = point(2, 0x20_40_60); + let missing = unavailable(3, 91); + let still_missing = unavailable(4, 92); + let recovered = point(5, 0x20_40_60); + + let mut retained_storage = None; + crate::composition::reset_source_over_evaluation_count(); + for (update, expected_evaluations) in [ + (first.as_slice(), 1), + (first.as_slice(), 1), + (second.as_slice(), 2), + (missing.as_slice(), 2), + (still_missing.as_slice(), 2), + (recovered.as_slice(), 3), + ] { + let (result, allocations) = crate::test_support::measured_allocations(|| { + session.update_packed(update).map(|_| ()) + }); + assert!(result.is_ok()); + assert_eq!( + allocations, 0, + "attach must preallocate every fixed-cardinality Session buffer" + ); + let current_storage = retained_signal_storage_pointers(session.state()); + if let Some(initial_storage) = retained_storage { + assert_eq!(current_storage, initial_storage); + } else { + retained_storage = Some(current_storage); + } + assert_eq!( + crate::composition::source_over_evaluation_count(), + expected_evaluations + ); + } + + let PointRenderSessionStateV1::Ready { current } = session.state() else { + panic!("a successful observation after Stale must recover Ready"); + }; + assert_eq!(current.revision(), 5); + assert_eq!(current.input_surface_signals_rgb24(), &[0x20_40_60]); + assert_eq!(current.composited_occurrence_signals_rgb24(), &[0x10_20_30]); +} + +#[test] +fn rejected_update_preserves_cold_buffers_for_allocation_free_retry() { + let owner = PointRenderOwnerV1::new(graph_spec(), bindings(0.5)).unwrap(); + let mut session = owner.attach().unwrap(); + let malformed = point(1, 0x01_ff_ff_ff); + let valid = point(1, 0xff_ff_ff); + crate::composition::reset_source_over_evaluation_count(); + + let (rejected, rejected_allocations) = crate::test_support::measured_allocations(|| { + session.update_packed(&malformed).map(|_| ()) + }); + assert_eq!( + rejected, + Err(PointRenderSessionUpdateErrorV1::EncodedSurfaceUpdate( + PackedEncodedSurfaceUpdateErrorV1::ReservedSignalByteNonZero { + surface_index: 0, + value: 0x01_ff_ff_ff, + } + )) + ); + assert_eq!(rejected_allocations, 0); + assert!(matches!( + session.state(), + PointRenderSessionStateV1::Waiting { + current_unavailable: None + } + )); + assert_eq!(crate::composition::source_over_evaluation_count(), 0); + + let (accepted, accepted_allocations) = crate::test_support::measured_allocations(|| { + session.update_packed(&valid).map(|_| ()) + }); + assert!(accepted.is_ok()); + assert_eq!(accepted_allocations, 0); + assert_eq!(crate::composition::source_over_evaluation_count(), 1); +} + +#[test] +fn waiting_unknown_chain_preserves_preallocated_buffers() { + let owner = PointRenderOwnerV1::new(graph_spec(), bindings(0.5)).unwrap(); + let mut session = owner.attach().unwrap(); + let first_missing = unavailable(1, 91); + let later_missing = unavailable(2, 92); + let ready = point(3, 0xff_ff_ff); + crate::composition::reset_source_over_evaluation_count(); + + for (update, expected_evaluations) in [ + (first_missing.as_slice(), 0), + (first_missing.as_slice(), 0), + (later_missing.as_slice(), 0), + (ready.as_slice(), 1), + ] { + let (result, allocations) = crate::test_support::measured_allocations(|| { + session.update_packed(update).map(|_| ()) + }); + assert!(result.is_ok()); + assert_eq!(allocations, 0); + assert_eq!( + crate::composition::source_over_evaluation_count(), + expected_evaluations + ); + } + + let PointRenderSessionStateV1::Ready { current } = session.state() else { + panic!("the first admitted point after Waiting must commit Ready"); + }; + assert_eq!(current.revision(), 3); + assert_eq!(current.composited_occurrence_signals_rgb24(), &[0x80_80_80]); +} From 00a897d3dacc74357cc5a6ba0378c3632196be8d Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Wed, 22 Jul 2026 06:02:04 +0300 Subject: [PATCH 04/58] style(core): format reusable session buffers --- crates/labcolors-core/src/program_session.rs | 26 +++++++------------ .../src/program_session_tests.rs | 24 +++++++---------- 2 files changed, 18 insertions(+), 32 deletions(-) diff --git a/crates/labcolors-core/src/program_session.rs b/crates/labcolors-core/src/program_session.rs index 4aee3f7b..e4528632 100644 --- a/crates/labcolors-core/src/program_session.rs +++ b/crates/labcolors-core/src/program_session.rs @@ -106,10 +106,8 @@ impl PointRenderOwnerV1 { .binding_template .try_clone_v1() .map_err(PointRenderAttachErrorV1::Bindings)?; - let initial_signal_buffers = CompositedSignalBuffersV1::try_new( - epoch.surface_ports.len(), - epoch.occurrence_count, - )?; + let initial_signal_buffers = + CompositedSignalBuffersV1::try_new(epoch.surface_ports.len(), epoch.occurrence_count)?; Ok(PointRenderSessionV1 { epoch: Rc::downgrade(epoch), bindings, @@ -347,15 +345,13 @@ impl PointRenderSessionV1 { surfaces_rgb24, } => { let retained_shape_matches = match &self.state { - PointRenderSessionStateV1::Waiting { .. } => self - .initial_signal_buffers - .as_ref() - .is_some_and(|buffers| { - buffers.input_surface_signals_rgb24.len() - == epoch.surface_ports.len() + PointRenderSessionStateV1::Waiting { .. } => { + self.initial_signal_buffers.as_ref().is_some_and(|buffers| { + buffers.input_surface_signals_rgb24.len() == epoch.surface_ports.len() && buffers.composited_occurrence_signals_rgb24.len() == epoch.occurrence_count - }), + }) + } PointRenderSessionStateV1::Ready { current } => { current.buffers.input_surface_signals_rgb24.len() == epoch.surface_ports.len() @@ -425,10 +421,7 @@ impl PointRenderSessionV1 { *output = pack_rgb24(resolved.visible()); } self.state = PointRenderSessionStateV1::Ready { - current: CompositedSignalSnapshotV1 { - revision, - buffers, - }, + current: CompositedSignalSnapshotV1 { revision, buffers }, }; } } @@ -458,8 +451,7 @@ impl PointRenderSessionV1 { PointRenderSessionStateV1::Ready { current }, ) => { revision == current.revision - && surfaces_rgb24 - == current.buffers.input_surface_signals_rgb24.as_slice() + && surfaces_rgb24 == current.buffers.input_surface_signals_rgb24.as_slice() } _ => false, }; diff --git a/crates/labcolors-core/src/program_session_tests.rs b/crates/labcolors-core/src/program_session_tests.rs index c1495f49..663c1517 100644 --- a/crates/labcolors-core/src/program_session_tests.rs +++ b/crates/labcolors-core/src/program_session_tests.rs @@ -117,9 +117,7 @@ fn unavailable(revision: u64, reason: u32) -> [u32; 5] { ] } -fn retained_signal_storage_pointers( - state: &PointRenderSessionStateV1, -) -> (*const u32, *const u32) { +fn retained_signal_storage_pointers(state: &PointRenderSessionStateV1) -> (*const u32, *const u32) { let snapshot = match state { PointRenderSessionStateV1::Ready { current } => current, PointRenderSessionStateV1::Stale { previous, .. } => previous, @@ -337,9 +335,8 @@ fn attached_session_reuses_buffers_for_every_update_state() { (still_missing.as_slice(), 2), (recovered.as_slice(), 3), ] { - let (result, allocations) = crate::test_support::measured_allocations(|| { - session.update_packed(update).map(|_| ()) - }); + let (result, allocations) = + crate::test_support::measured_allocations(|| session.update_packed(update).map(|_| ())); assert!(result.is_ok()); assert_eq!( allocations, 0, @@ -373,9 +370,8 @@ fn rejected_update_preserves_cold_buffers_for_allocation_free_retry() { let valid = point(1, 0xff_ff_ff); crate::composition::reset_source_over_evaluation_count(); - let (rejected, rejected_allocations) = crate::test_support::measured_allocations(|| { - session.update_packed(&malformed).map(|_| ()) - }); + let (rejected, rejected_allocations) = + crate::test_support::measured_allocations(|| session.update_packed(&malformed).map(|_| ())); assert_eq!( rejected, Err(PointRenderSessionUpdateErrorV1::EncodedSurfaceUpdate( @@ -394,9 +390,8 @@ fn rejected_update_preserves_cold_buffers_for_allocation_free_retry() { )); assert_eq!(crate::composition::source_over_evaluation_count(), 0); - let (accepted, accepted_allocations) = crate::test_support::measured_allocations(|| { - session.update_packed(&valid).map(|_| ()) - }); + let (accepted, accepted_allocations) = + crate::test_support::measured_allocations(|| session.update_packed(&valid).map(|_| ())); assert!(accepted.is_ok()); assert_eq!(accepted_allocations, 0); assert_eq!(crate::composition::source_over_evaluation_count(), 1); @@ -417,9 +412,8 @@ fn waiting_unknown_chain_preserves_preallocated_buffers() { (later_missing.as_slice(), 0), (ready.as_slice(), 1), ] { - let (result, allocations) = crate::test_support::measured_allocations(|| { - session.update_packed(update).map(|_| ()) - }); + let (result, allocations) = + crate::test_support::measured_allocations(|| session.update_packed(update).map(|_| ())); assert!(result.is_ok()); assert_eq!(allocations, 0); assert_eq!( From 6d3d900711a8aeb376d0718cc1b2a99d087e0383 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Wed, 22 Jul 2026 06:34:00 +0300 Subject: [PATCH 05/58] feat(lcs): execute sealed sRGB8 tristimulus derivation --- .../src/generic_boundary_tests.rs | 86 ++++ crates/labcolors-core/src/lcs_occurrence.rs | 388 +++++++++++++++--- .../src/lcs_occurrence_tests.rs | 349 +++++++++++++--- crates/labcolors-core/src/spaces/srgb.rs | 11 + 4 files changed, 712 insertions(+), 122 deletions(-) diff --git a/crates/labcolors-core/src/generic_boundary_tests.rs b/crates/labcolors-core/src/generic_boundary_tests.rs index 50ffff1e..cbc8e6a4 100644 --- a/crates/labcolors-core/src/generic_boundary_tests.rs +++ b/crates/labcolors-core/src/generic_boundary_tests.rs @@ -63,3 +63,89 @@ fn program_session_module_docs_disclaim_transport_only_scope() { ); } } + +#[test] +fn f0_signal_transform_has_no_renderer_alias_or_raw_xyz_production_constructor() { + for forbidden in [ + "RenderProfileId", + "AppearanceContextReleaseId", + "enum ObserveError", + "fn observe(", + "pub(crate) fn new(\n xyz", + ] { + assert!( + !LCS_OCCURRENCE_SOURCE.contains(forbidden), + "lcs_occurrence.rs must keep the F0 identity boundary sealed; found `{forbidden}`", + ); + } + + for required in [ + "ColorimetricTransformReleaseId", + "AppearanceContextSchemaReleaseId", + "fn admitted_binding(", + "match output_profile", + "fn derive_sample_with_binding(", + "binding.transform_release()", + "xyz_d65_from_srgb8_v1", + "ModeledTristimulusProvenanceV1", + "ModeledTristimulusDerivationV1", + ] { + assert!( + LCS_OCCURRENCE_SOURCE.contains(required), + "lcs_occurrence.rs must retain the sealed F0 route; missing `{required}`", + ); + } + + let raw_xyz_declaration = LCS_OCCURRENCE_SOURCE + .lines() + .find(|line| line.contains("fn try_from_registered_xyz(")) + .expect("registered raw-XYZ admission must remain visible to this source gate"); + assert_eq!( + raw_xyz_declaration.trim(), + "fn try_from_registered_xyz(", + "registered raw-XYZ admission must remain module-private", + ); + + let raw_frame_declaration = LCS_OCCURRENCE_SOURCE + .lines() + .find(|line| line.contains("const fn registered(")) + .expect("registered frame constructor must remain visible to this source gate"); + assert_eq!( + raw_frame_declaration.trim(), + "const fn registered(", + "registered frame construction must remain module-private", + ); + + let dispatch_start = LCS_OCCURRENCE_SOURCE + .find("fn admitted_binding(") + .expect("binding dispatch must remain present"); + let dispatch_end = LCS_OCCURRENCE_SOURCE[dispatch_start..] + .find("/// Derive one modeled tristimulus") + .map(|offset| dispatch_start + offset) + .expect("modeled derivation docs must delimit the dispatch source gate"); + let dispatch_source = &LCS_OCCURRENCE_SOURCE[dispatch_start..dispatch_end]; + assert!( + !dispatch_source.contains("_ =>"), + "closed F0 profile/transform dispatch must not silently fall back", + ); +} + +#[test] +fn f0_modeled_derivation_mints_no_observation_or_bounded_evidence() { + for forbidden in [ + "BoundEvidence", + "NumericalDecisionEvidenceV1", + "CanonicalFiniteBoundedEvidenceV1", + "SourceOverCertificateV1", + "GlowCompositeCertificateV1", + "RendererCapability", + "RenderObservation", + "crate::constraints", + "crate::wcag22_evidence", + ] { + assert!( + !LCS_OCCURRENCE_SOURCE.contains(forbidden), + "deterministic signal lowering cannot mint `{forbidden}`", + ); + } +} diff --git a/crates/labcolors-core/src/lcs_occurrence.rs b/crates/labcolors-core/src/lcs_occurrence.rs index a847d1b2..f626b646 100644 --- a/crates/labcolors-core/src/lcs_occurrence.rs +++ b/crates/labcolors-core/src/lcs_occurrence.rs @@ -1,10 +1,15 @@ //! Type foundation for a context-bound Labpics Colors Space occurrence. //! -//! This module intentionally contains no colour transforms. It makes the -//! physical identity split representable before the existing kernels are moved: -//! encoded output, framed tristimulus evidence, appearance context and derived -//! hue state are different values. In particular, an occurrence has no inverse -//! operation accepting an arbitrary second context. +//! Encoded output, a framed tristimulus, immutable appearance context and +//! derived hue state are different values. The one executable transform here is +//! a sealed, versioned lowering of one encoded sRGB8 point through the existing +//! IEC transfer table and XYZ(D65) matrix. It is a deterministic +//! model derivation, not evidence that a host rendered or a person observed the +//! result. In particular, an occurrence has no inverse operation accepting an +//! arbitrary second context. + +use crate::Srgb8; +use crate::spaces::srgb::xyz_d65_from_srgb8_v1; /// A registered encoded-output domain. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -12,29 +17,25 @@ pub enum OutputProfileId { Iec61966Srgb8D65V1, } -/// A registered render operation. This is deliberately not an output profile. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum RenderProfileId { - EncodedSrgb8PointV1, -} - -/// Exact encoded channels plus the profile which gives those channels meaning. +/// Exact encoded channels plus the output profile which gives them meaning. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct ColorSignal { - channels: [u8; 3], + srgb8: Srgb8, output_profile: OutputProfileId, } impl ColorSignal { - pub(crate) const fn new(channels: [u8; 3], output_profile: OutputProfileId) -> Self { + /// Form the only admitted encoded signal without accepting a free-form + /// channel/profile pairing. + pub(crate) const fn from_srgb8(srgb8: Srgb8) -> Self { Self { - channels, - output_profile, + srgb8, + output_profile: OutputProfileId::Iec61966Srgb8D65V1, } } - pub(crate) const fn channels(self) -> [u8; 3] { - self.channels + pub(crate) const fn srgb8(self) -> Srgb8 { + self.srgb8 } pub(crate) const fn output_profile(self) -> OutputProfileId { @@ -42,6 +43,14 @@ impl ColorSignal { } } +/// Exact code release for one colorimetric signal-to-tristimulus transform. +/// +/// This is not a composition profile, renderer capability or observation. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum ColorimetricTransformReleaseId { + Iec61966Srgb8ToCie1931TwoDegreeXyzD65RelativeY1V1, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum ObserverProfileId { Cie1931TwoDegreeV1, @@ -74,7 +83,7 @@ pub struct ColorimetricFrameId { } impl ColorimetricFrameId { - pub(crate) const fn new( + const fn registered( observer: ObserverProfileId, reference_white: ReferenceWhiteId, scale: TristimulusScale, @@ -105,6 +114,61 @@ impl ColorimetricFrameId { } } +/// Canonical result frame of the registered encoded-sRGB8 transform. +pub(crate) const IEC_SRGB_D65_XYZ_FRAME_V1: ColorimetricFrameId = ColorimetricFrameId::registered( + ObserverProfileId::Cie1931TwoDegreeV1, + ReferenceWhiteId::Iec61966D65ChromaticityV1, + TristimulusScale::RelativeY1, + ColorimetricFrameReleaseId::XyzV1, +); + +#[cfg(test)] +pub(crate) const MUTATION_SENTINEL_XYZ_FRAME_V1: ColorimetricFrameId = + ColorimetricFrameId::registered( + ObserverProfileId::Cie1931TwoDegreeV1, + ReferenceWhiteId::Iec61966D65ChromaticityV1, + TristimulusScale::RelativeY1, + ColorimetricFrameReleaseId::MutationSentinelV1, + ); + +/// The one closed, code-owned binding admitted by the current F0 slice. +/// +/// A variant is the tuple: independent profile, transform and frame fields +/// cannot be authored or mixed. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum AdmittedSrgb8TristimulusBindingV1 { + Iec61966Srgb8ToCie1931TwoDegreeXyzD65RelativeY1V1, +} + +impl AdmittedSrgb8TristimulusBindingV1 { + pub(crate) const fn signal_output_profile(self) -> OutputProfileId { + match self { + Self::Iec61966Srgb8ToCie1931TwoDegreeXyzD65RelativeY1V1 => { + OutputProfileId::Iec61966Srgb8D65V1 + } + } + } + + pub(crate) const fn transform_release(self) -> ColorimetricTransformReleaseId { + match self { + Self::Iec61966Srgb8ToCie1931TwoDegreeXyzD65RelativeY1V1 => { + ColorimetricTransformReleaseId::Iec61966Srgb8ToCie1931TwoDegreeXyzD65RelativeY1V1 + } + } + } + + pub(crate) const fn result_frame(self) -> ColorimetricFrameId { + match self { + Self::Iec61966Srgb8ToCie1931TwoDegreeXyzD65RelativeY1V1 => { + IEC_SRGB_D65_XYZ_FRAME_V1 + } + } + } +} + +pub(crate) const ADMITTED_SRGB8_TRISTIMULUS_BINDING_V1: AdmittedSrgb8TristimulusBindingV1 = + AdmittedSrgb8TristimulusBindingV1::Iec61966Srgb8ToCie1931TwoDegreeXyzD65RelativeY1V1; + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum NumericDomainError { NonFinite, @@ -116,10 +180,10 @@ pub enum NumericDomainError { /// Finite, non-negative binary64 value with canonical positive zero. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct FiniteNonNegative(u64); +struct FiniteNonNegative(u64); impl FiniteNonNegative { - pub(crate) fn new(value: f64) -> Result { + fn new(value: f64) -> Result { if !value.is_finite() { return Err(NumericDomainError::NonFinite); } @@ -133,7 +197,7 @@ impl FiniteNonNegative { })) } - pub(crate) fn get(self) -> f64 { + fn get(self) -> f64 { f64::from_bits(self.0) } } @@ -145,21 +209,56 @@ pub struct TristimulusSample { frame: ColorimetricFrameId, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum TristimulusComponentV1 { + X, + Y, + Z, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct TristimulusDomainErrorV1 { + component: TristimulusComponentV1, + reason: NumericDomainError, +} + +impl TristimulusDomainErrorV1 { + pub(crate) const fn component(self) -> TristimulusComponentV1 { + self.component + } + + pub(crate) const fn reason(self) -> NumericDomainError { + self.reason + } +} + impl TristimulusSample { - pub(crate) fn new( + fn try_from_registered_xyz( xyz: [f64; 3], frame: ColorimetricFrameId, - ) -> Result { + ) -> Result { + let admit = |value, component| { + FiniteNonNegative::new(value) + .map_err(|reason| TristimulusDomainErrorV1 { component, reason }) + }; Ok(Self { xyz: [ - FiniteNonNegative::new(xyz[0])?, - FiniteNonNegative::new(xyz[1])?, - FiniteNonNegative::new(xyz[2])?, + admit(xyz[0], TristimulusComponentV1::X)?, + admit(xyz[1], TristimulusComponentV1::Y)?, + admit(xyz[2], TristimulusComponentV1::Z)?, ], frame, }) } + #[cfg(test)] + pub(crate) fn try_from_xyz_for_test( + xyz: [f64; 3], + frame: ColorimetricFrameId, + ) -> Result { + Self::try_from_registered_xyz(xyz, frame) + } + pub(crate) fn xyz(self) -> [f64; 3] { self.xyz.map(FiniteNonNegative::get) } @@ -169,9 +268,96 @@ impl TristimulusSample { } } +/// Content-bound provenance of one deterministic modeled transform. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ModeledTristimulusProvenanceV1 { + source_signal: ColorSignal, + binding: AdmittedSrgb8TristimulusBindingV1, +} + +impl ModeledTristimulusProvenanceV1 { + pub(crate) const fn source_signal(self) -> ColorSignal { + self.source_signal + } + + pub(crate) const fn binding(self) -> AdmittedSrgb8TristimulusBindingV1 { + self.binding + } +} + +/// Replayable ideal colorimetric derivation under one admitted binding. +/// +/// This is not a renderer capability, render observation, human observation or +/// certified field bound. It cannot by itself satisfy such predicates. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct ModeledTristimulusDerivationV1 { + sample: TristimulusSample, + provenance: ModeledTristimulusProvenanceV1, +} + +impl ModeledTristimulusDerivationV1 { + pub(crate) const fn sample(self) -> TristimulusSample { + self.sample + } + + pub(crate) const fn provenance(self) -> ModeledTristimulusProvenanceV1 { + self.provenance + } + + pub(crate) fn replay(self) -> Result { + derive_sample_with_binding( + self.provenance.source_signal, + self.provenance.binding, + ) + } +} + +fn admitted_binding(output_profile: OutputProfileId) -> AdmittedSrgb8TristimulusBindingV1 { + match output_profile { + OutputProfileId::Iec61966Srgb8D65V1 => ADMITTED_SRGB8_TRISTIMULUS_BINDING_V1, + } +} + +fn derive_sample_with_binding( + signal: ColorSignal, + binding: AdmittedSrgb8TristimulusBindingV1, +) -> Result { + let xyz = match ( + signal.output_profile(), + binding.signal_output_profile(), + binding.transform_release(), + ) { + ( + OutputProfileId::Iec61966Srgb8D65V1, + OutputProfileId::Iec61966Srgb8D65V1, + ColorimetricTransformReleaseId::Iec61966Srgb8ToCie1931TwoDegreeXyzD65RelativeY1V1, + ) => xyz_d65_from_srgb8_v1(signal.srgb8()), + }; + TristimulusSample::try_from_registered_xyz(xyz, binding.result_frame()) +} + +/// Derive one modeled tristimulus from an encoded sRGB8 point. +/// +/// Composition provenance remains the responsibility of the upstream +/// render/composition layer that produced the signal; renderer capability and +/// actual observations are deliberately outside this deterministic transform. +pub(crate) fn derive_modeled_tristimulus_v1( + signal: ColorSignal, +) -> Result { + let binding = admitted_binding(signal.output_profile()); + let sample = derive_sample_with_binding(signal, binding)?; + Ok(ModeledTristimulusDerivationV1 { + sample, + provenance: ModeledTristimulusProvenanceV1 { + source_signal: signal, + binding, + }, + }) +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum AppearanceContextReleaseId { - Cam16V1, +pub enum AppearanceContextSchemaReleaseId { + Ciecam16ViewingInputsV1, } #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -181,50 +367,129 @@ pub enum SurroundProfileId { DarkV1, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum AppearanceContextFieldV1 { + AdaptingLuminanceCdM2, + BackgroundLuminanceRatio, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct AppearanceContextDomainErrorV1 { + field: AppearanceContextFieldV1, + reason: NumericDomainError, +} + +impl AppearanceContextDomainErrorV1 { + pub(crate) const fn field(self) -> AppearanceContextFieldV1 { + self.field + } + + pub(crate) const fn reason(self) -> NumericDomainError { + self.reason + } +} + +/// Finite, strictly positive CIECAM16 adapting luminance in cd/m². +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct AdaptingLuminanceCdM2(FiniteNonNegative); + +impl AdaptingLuminanceCdM2 { + pub(crate) fn try_new(value: f64) -> Result { + let field = AppearanceContextFieldV1::AdaptingLuminanceCdM2; + let value = FiniteNonNegative::new(value) + .map_err(|reason| AppearanceContextDomainErrorV1 { field, reason })?; + if value.get() == 0.0 { + return Err(AppearanceContextDomainErrorV1 { + field, + reason: NumericDomainError::NotPositive, + }); + } + Ok(Self(value)) + } + + pub(crate) fn get(self) -> f64 { + self.0.get() + } +} + +/// Finite CIECAM16 background ratio `Y_b / Y_w` in `(0, 1]`. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct BackgroundLuminanceRatio(FiniteNonNegative); + +impl BackgroundLuminanceRatio { + pub(crate) fn try_new(value: f64) -> Result { + let field = AppearanceContextFieldV1::BackgroundLuminanceRatio; + let value = FiniteNonNegative::new(value) + .map_err(|reason| AppearanceContextDomainErrorV1 { field, reason })?; + if value.get() == 0.0 { + return Err(AppearanceContextDomainErrorV1 { + field, + reason: NumericDomainError::NotPositive, + }); + } + if value.get() > 1.0 { + return Err(AppearanceContextDomainErrorV1 { + field, + reason: NumericDomainError::AboveOne, + }); + } + Ok(Self(value)) + } + + pub(crate) fn get(self) -> f64 { + self.0.get() + } +} + /// Content identity of immutable semantic viewing inputs. /// /// Derived CAM constants are intentionally absent and must remain a private -/// cache of the observer implementation. +/// cache of the appearance-view implementation. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct AppearanceContextId { - release: AppearanceContextReleaseId, + schema_release: AppearanceContextSchemaReleaseId, frame: ColorimetricFrameId, - adapting_luminance: FiniteNonNegative, - background_relative_luminance: FiniteNonNegative, + adapting_luminance_cd_m2: AdaptingLuminanceCdM2, + background_luminance_ratio: BackgroundLuminanceRatio, surround: SurroundProfileId, } impl AppearanceContextId { - pub(crate) fn new( - release: AppearanceContextReleaseId, + pub(crate) const fn from_inputs( + schema_release: AppearanceContextSchemaReleaseId, frame: ColorimetricFrameId, - adapting_luminance: f64, - background_relative_luminance: f64, + adapting_luminance_cd_m2: AdaptingLuminanceCdM2, + background_luminance_ratio: BackgroundLuminanceRatio, surround: SurroundProfileId, - ) -> Result { - let adapting_luminance = FiniteNonNegative::new(adapting_luminance)?; - if adapting_luminance.get() == 0.0 { - return Err(NumericDomainError::NotPositive); - } - let background_relative_luminance = FiniteNonNegative::new(background_relative_luminance)?; - if background_relative_luminance.get() == 0.0 { - return Err(NumericDomainError::NotPositive); - } - if background_relative_luminance.get() > 1.0 { - return Err(NumericDomainError::AboveOne); - } - Ok(Self { - release, + ) -> Self { + Self { + schema_release, frame, - adapting_luminance, - background_relative_luminance, + adapting_luminance_cd_m2, + background_luminance_ratio, surround, - }) + } + } + + pub(crate) const fn schema_release(self) -> AppearanceContextSchemaReleaseId { + self.schema_release } pub(crate) const fn frame(self) -> ColorimetricFrameId { self.frame } + + pub(crate) fn adapting_luminance_cd_m2(self) -> f64 { + self.adapting_luminance_cd_m2.get() + } + + pub(crate) fn background_luminance_ratio(self) -> f64 { + self.background_luminance_ratio.get() + } + + pub(crate) const fn surround_profile(self) -> SurroundProfileId { + self.surround + } } /// A finite angle; absence of hue is represented by [`HueState`], never `0°`. @@ -265,14 +530,17 @@ pub enum HueState { } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ObserveError { +pub enum OccurrenceFormationError { FrameMismatch { stimulus: ColorimetricFrameId, context: ColorimetricFrameId, }, } -/// LCS identity: one physical sample observed in one immutable context. +/// LCS identity: one tristimulus sample bound to one immutable context. +/// +/// Whether the sample is modeled, renderer-observed or measured belongs to its +/// external provenance; forming this identity does not upgrade that claim. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct LcsOccurrence { sample: TristimulusSample, @@ -280,17 +548,17 @@ pub struct LcsOccurrence { } impl LcsOccurrence { - /// Observe one sample in one context with an exactly matching frame. + /// Bind one sample to one context with an exactly matching frame. /// /// Named appearance views are derived later from this pair. No view /// coordinate is accepted here, so contradictory cached views cannot become /// part of occurrence identity. - pub(crate) fn observe( + pub(crate) fn in_context( sample: TristimulusSample, context: AppearanceContextId, - ) -> Result { + ) -> Result { if sample.frame() != context.frame() { - return Err(ObserveError::FrameMismatch { + return Err(OccurrenceFormationError::FrameMismatch { stimulus: sample.frame(), context: context.frame(), }); diff --git a/crates/labcolors-core/src/lcs_occurrence_tests.rs b/crates/labcolors-core/src/lcs_occurrence_tests.rs index 0e0727d0..a1e216ec 100644 --- a/crates/labcolors-core/src/lcs_occurrence_tests.rs +++ b/crates/labcolors-core/src/lcs_occurrence_tests.rs @@ -1,70 +1,71 @@ +use proptest::prelude::*; + +use crate::Srgb8; use crate::lcs_occurrence::{ - AppearanceContextId, AppearanceContextReleaseId, ColorimetricFrameId, - ColorimetricFrameReleaseId, HueAngle, HueState, LcsOccurrence, NumericDomainError, - ObserveError, ObserverProfileId, ReferenceWhiteId, SurroundProfileId, TristimulusSample, - TristimulusScale, + ADMITTED_SRGB8_TRISTIMULUS_BINDING_V1, AdaptingLuminanceCdM2, AppearanceContextFieldV1, + AppearanceContextId, AppearanceContextSchemaReleaseId, BackgroundLuminanceRatio, ColorSignal, + ColorimetricFrameId, ColorimetricFrameReleaseId, ColorimetricTransformReleaseId, HueAngle, + HueState, IEC_SRGB_D65_XYZ_FRAME_V1, LcsOccurrence, MUTATION_SENTINEL_XYZ_FRAME_V1, + ModeledTristimulusDerivationV1, NumericDomainError, ObserverProfileId, + OccurrenceFormationError, ReferenceWhiteId, SurroundProfileId, TristimulusComponentV1, + TristimulusDomainErrorV1, TristimulusSample, TristimulusScale, + derive_modeled_tristimulus_v1, }; - -fn frame(scale: TristimulusScale) -> ColorimetricFrameId { - ColorimetricFrameId::new( - ObserverProfileId::Cie1931TwoDegreeV1, - ReferenceWhiteId::Iec61966D65ChromaticityV1, - scale, - ColorimetricFrameReleaseId::XyzV1, - ) -} +use crate::spaces::srgb::{D65_WHITE, srgb_linear_from_srgb8, srgb_to_xyz}; fn context(frame: ColorimetricFrameId, la: f64) -> AppearanceContextId { - AppearanceContextId::new( - AppearanceContextReleaseId::Cam16V1, + AppearanceContextId::from_inputs( + AppearanceContextSchemaReleaseId::Ciecam16ViewingInputsV1, frame, - la, - 0.2, + AdaptingLuminanceCdM2::try_new(la).unwrap(), + BackgroundLuminanceRatio::try_new(0.2).unwrap(), SurroundProfileId::AverageV1, ) - .unwrap() } #[test] fn xyz_and_context_numeric_admission_is_fail_closed() { - let relative = frame(TristimulusScale::RelativeY1); + let relative = IEC_SRGB_D65_XYZ_FRAME_V1; for xyz in [ [f64::NAN, 0.0, 0.0], [0.0, f64::INFINITY, 0.0], [0.0, 0.0, -f64::MIN_POSITIVE], ] { - assert!(TristimulusSample::new(xyz, relative).is_err()); + assert!(TristimulusSample::try_from_xyz_for_test(xyz, relative).is_err()); } + + let zero_la = AdaptingLuminanceCdM2::try_new(0.0).unwrap_err(); + assert_eq!(zero_la.field(), AppearanceContextFieldV1::AdaptingLuminanceCdM2); + assert_eq!(zero_la.reason(), NumericDomainError::NotPositive); + + let zero_background = BackgroundLuminanceRatio::try_new(0.0).unwrap_err(); assert_eq!( - AppearanceContextId::new( - AppearanceContextReleaseId::Cam16V1, - relative, - 0.0, - 0.2, - SurroundProfileId::AverageV1, - ), - Err(NumericDomainError::NotPositive) - ); - assert_eq!( - AppearanceContextId::new( - AppearanceContextReleaseId::Cam16V1, - relative, - 64.0, - 0.0, - SurroundProfileId::AverageV1, - ), - Err(NumericDomainError::NotPositive) + zero_background.field(), + AppearanceContextFieldV1::BackgroundLuminanceRatio, ); + assert_eq!(zero_background.reason(), NumericDomainError::NotPositive); + + let high_background = BackgroundLuminanceRatio::try_new(1.01).unwrap_err(); assert_eq!( - AppearanceContextId::new( - AppearanceContextReleaseId::Cam16V1, - relative, - 64.0, - 1.01, - SurroundProfileId::AverageV1, - ), - Err(NumericDomainError::AboveOne) + high_background.field(), + AppearanceContextFieldV1::BackgroundLuminanceRatio, ); + assert_eq!(high_background.reason(), NumericDomainError::AboveOne); + + for invalid in [f64::NAN, f64::INFINITY, -f64::MIN_POSITIVE, -0.0] { + assert_eq!( + AdaptingLuminanceCdM2::try_new(invalid) + .unwrap_err() + .field(), + AppearanceContextFieldV1::AdaptingLuminanceCdM2, + ); + assert_eq!( + BackgroundLuminanceRatio::try_new(invalid) + .unwrap_err() + .field(), + AppearanceContextFieldV1::BackgroundLuminanceRatio, + ); + } } #[test] @@ -77,17 +78,17 @@ fn hue_algebra_cannot_encode_exact_absence_as_zero_degrees() { #[test] fn frame_mismatch_cannot_form_an_occurrence() { - let relative = frame(TristimulusScale::RelativeY1); - let mismatching = ColorimetricFrameId::new( - ObserverProfileId::Cie1931TwoDegreeV1, - ReferenceWhiteId::Iec61966D65ChromaticityV1, - TristimulusScale::RelativeY1, - ColorimetricFrameReleaseId::MutationSentinelV1, - ); - let sample = TristimulusSample::new([0.1, 0.2, 0.3], relative).unwrap(); + let relative = IEC_SRGB_D65_XYZ_FRAME_V1; + let mismatching = MUTATION_SENTINEL_XYZ_FRAME_V1; + let sample = derive_modeled_tristimulus_v1(ColorSignal::from_srgb8(Srgb8::new([ + 0x44, 0x88, 0xCC, + ]))) + .unwrap() + .sample(); + assert_eq!(sample.frame(), relative); assert_eq!( - LcsOccurrence::observe(sample, context(mismatching, 64.0)), - Err(ObserveError::FrameMismatch { + LcsOccurrence::in_context(sample, context(mismatching, 64.0)), + Err(OccurrenceFormationError::FrameMismatch { stimulus: relative, context: mismatching, }) @@ -96,17 +97,35 @@ fn frame_mismatch_cannot_form_an_occurrence() { #[test] fn occurrence_identity_contains_only_sample_and_context() { - let relative = frame(TristimulusScale::RelativeY1); - let sample = TristimulusSample::new([0.1, 0.2, 0.3], relative).unwrap(); - let observed = LcsOccurrence::observe(sample, context(relative, 64.0)).unwrap(); - assert_eq!(observed.sample(), sample); - assert_eq!(observed.context(), context(relative, 64.0)); + let relative = IEC_SRGB_D65_XYZ_FRAME_V1; + let sample = + TristimulusSample::try_from_xyz_for_test([0.1, 0.2, 0.3], relative).unwrap(); + let occurrence = LcsOccurrence::in_context(sample, context(relative, 64.0)).unwrap(); + assert_eq!(occurrence.sample(), sample); + assert_eq!(occurrence.context(), context(relative, 64.0)); } #[test] fn context_identity_changes_with_semantic_input_bits() { - let relative = frame(TristimulusScale::RelativeY1); + let relative = IEC_SRGB_D65_XYZ_FRAME_V1; assert_ne!(context(relative, 64.0), context(relative, 32.0)); + + let changed_background = AppearanceContextId::from_inputs( + AppearanceContextSchemaReleaseId::Ciecam16ViewingInputsV1, + relative, + AdaptingLuminanceCdM2::try_new(64.0).unwrap(), + BackgroundLuminanceRatio::try_new(0.3).unwrap(), + SurroundProfileId::AverageV1, + ); + let changed_surround = AppearanceContextId::from_inputs( + AppearanceContextSchemaReleaseId::Ciecam16ViewingInputsV1, + relative, + AdaptingLuminanceCdM2::try_new(64.0).unwrap(), + BackgroundLuminanceRatio::try_new(0.2).unwrap(), + SurroundProfileId::DimV1, + ); + assert_ne!(context(relative, 64.0), changed_background); + assert_ne!(context(relative, 64.0), changed_surround); } #[test] @@ -116,3 +135,209 @@ fn hue_numeric_constructor_rejects_nonfinite_and_out_of_domain() { } assert_eq!(HueAngle::new(-0.0).unwrap(), HueAngle::new(0.0).unwrap()); } + +proptest! { + #![proptest_config(ProptestConfig::with_cases(256))] + + #[test] + fn admitted_srgb8_lowering_preserves_existing_decode_and_matrix_bits( + bytes in any::<[u8; 3]>(), + ) { + let signal = ColorSignal::from_srgb8(Srgb8::new(bytes)); + let derived = derive_modeled_tristimulus_v1(signal).unwrap(); + let expected = srgb_to_xyz(srgb_linear_from_srgb8(Srgb8::new(bytes))); + + prop_assert_eq!( + derived.sample().xyz().map(f64::to_bits), + expected.map(f64::to_bits), + ); + prop_assert_eq!(derived.sample().frame(), IEC_SRGB_D65_XYZ_FRAME_V1); + } +} + +#[test] +fn f0_lowering_signature_accepts_only_one_profiled_signal() { + let lower: fn( + ColorSignal, + ) -> Result = + derive_modeled_tristimulus_v1; + assert!(lower(ColorSignal::from_srgb8(Srgb8::new([0; 3]))).is_ok()); +} + +#[test] +fn fixed_corpus_including_eotf_boundary_matches_existing_kernel_bits() { + for bytes in [ + [0x00, 0x00, 0x00], + [0xFF, 0xFF, 0xFF], + [0xFF, 0x00, 0x00], + [0x00, 0xFF, 0x00], + [0x00, 0x00, 0xFF], + [0x0A, 0x0B, 0x80], + [0x44, 0x88, 0xCC], + ] { + let actual = derive_modeled_tristimulus_v1(ColorSignal::from_srgb8(Srgb8::new(bytes))) + .unwrap() + .sample() + .xyz(); + let expected = srgb_to_xyz(srgb_linear_from_srgb8(Srgb8::new(bytes))); + assert_eq!(actual.map(f64::to_bits), expected.map(f64::to_bits)); + } +} + +#[test] +fn transform_release_v1_pins_a_pre_f0_binary64_vector() { + // Recorded once from the pre-F0 decode-table + matrix operation order. This + // is a release anti-drift vector, not a claim of measured or bounded + // colorimetric evidence. + let xyz = derive_modeled_tristimulus_v1(ColorSignal::from_srgb8(Srgb8::new([ + 0x0A, 0x0B, 0x80, + ]))) + .unwrap() + .sample() + .xyz(); + assert_eq!( + xyz.map(f64::to_bits), + [ + 0x3FA5_334E_5BB6_D38E, + 0x3F93_11B4_45B1_935D, + 0x3FCA_5268_973F_D7F0, + ], + ); +} + +#[test] +fn every_srgb8_channel_code_has_finite_nonnegative_basis_contribution() { + // The registered EOTF acts independently on each byte and the XYZ matrix is + // a sum of three non-negative basis contributions. Exhausting 3 × 256 basis + // inputs therefore covers the finite/non-negative invariant for every mixed + // triplet without adding a 256³ debug-test loop; the property test above + // separately exercises mixed-sum routing and exact operation order. + for byte in u8::MIN..=u8::MAX { + for channel in 0..3 { + let mut bytes = [0_u8; 3]; + bytes[channel] = byte; + let xyz = derive_modeled_tristimulus_v1(ColorSignal::from_srgb8(Srgb8::new(bytes))) + .unwrap() + .sample() + .xyz(); + assert!(xyz.into_iter().all(|component| component.is_finite() && component >= 0.0)); + } + } +} + +#[test] +fn admitted_binding_is_one_closed_profile_transform_frame_tuple() { + let binding = ADMITTED_SRGB8_TRISTIMULUS_BINDING_V1; + assert_eq!( + binding.signal_output_profile(), + crate::lcs_occurrence::OutputProfileId::Iec61966Srgb8D65V1, + ); + assert_eq!( + binding.transform_release(), + ColorimetricTransformReleaseId::Iec61966Srgb8ToCie1931TwoDegreeXyzD65RelativeY1V1, + ); + assert_eq!(binding.result_frame(), IEC_SRGB_D65_XYZ_FRAME_V1); + assert_eq!( + binding.result_frame().observer(), + ObserverProfileId::Cie1931TwoDegreeV1, + ); + assert_eq!( + binding.result_frame().reference_white(), + ReferenceWhiteId::Iec61966D65ChromaticityV1, + ); + assert_eq!( + binding.result_frame().scale(), + TristimulusScale::RelativeY1, + ); + assert_eq!( + binding.result_frame().release(), + ColorimetricFrameReleaseId::XyzV1, + ); +} + +#[test] +fn black_is_positive_zero_and_white_tracks_the_existing_matrix() { + let black = derive_modeled_tristimulus_v1(ColorSignal::from_srgb8(Srgb8::new([0; 3]))) + .unwrap() + .sample() + .xyz(); + assert_eq!(black.map(f64::to_bits), [0_u64; 3]); + + let white = derive_modeled_tristimulus_v1(ColorSignal::from_srgb8(Srgb8::new([255; 3]))) + .unwrap() + .sample() + .xyz(); + let matrix_white = srgb_to_xyz([1.0; 3]); + assert_eq!(white.map(f64::to_bits), matrix_white.map(f64::to_bits)); + for (actual, reference) in white.into_iter().zip(D65_WHITE) { + assert!((actual - reference).abs() <= f64::EPSILON); + } +} + +#[test] +fn modeled_derivation_is_content_bound_replayable_and_allocation_free() { + let signal = ColorSignal::from_srgb8(Srgb8::new([0x0A, 0x0B, 0x80])); + let (derived, lowering_allocations) = + crate::test_support::measured_allocations(|| derive_modeled_tristimulus_v1(signal)); + let derived = derived.unwrap(); + let (replayed, replay_allocations) = + crate::test_support::measured_allocations(|| derived.replay()); + + assert_eq!(lowering_allocations, 0); + assert_eq!(replay_allocations, 0); + assert_eq!(replayed.unwrap(), derived.sample()); + assert_eq!(derived.provenance().source_signal(), signal); + assert_eq!( + derived.provenance().binding(), + ADMITTED_SRGB8_TRISTIMULUS_BINDING_V1, + ); + + let changed_signal = ColorSignal::from_srgb8(Srgb8::new([0x0B, 0x0B, 0x80])); + let changed = derive_modeled_tristimulus_v1(changed_signal).unwrap(); + assert_ne!(derived.provenance(), changed.provenance()); + assert_ne!(derived.sample(), changed.sample()); + assert_eq!(derived.provenance().source_signal(), signal); +} + +#[test] +fn raw_xyz_admission_is_test_only_component_qualified_and_fail_closed() { + let frame = IEC_SRGB_D65_XYZ_FRAME_V1; + for (xyz, component, reason) in [ + ( + [f64::NAN, 0.0, 0.0], + TristimulusComponentV1::X, + NumericDomainError::NonFinite, + ), + ( + [0.0, f64::INFINITY, 0.0], + TristimulusComponentV1::Y, + NumericDomainError::NonFinite, + ), + ( + [0.0, 0.0, -f64::MIN_POSITIVE], + TristimulusComponentV1::Z, + NumericDomainError::Negative, + ), + ] { + let error = TristimulusSample::try_from_xyz_for_test(xyz, frame).unwrap_err(); + assert_eq!(error.component(), component); + assert_eq!(error.reason(), reason); + } + + let admitted = TristimulusSample::try_from_xyz_for_test([-0.0, 0.5, 1.25], frame).unwrap(); + assert_eq!(admitted.xyz()[0].to_bits(), 0.0_f64.to_bits()); + assert_eq!(admitted.xyz()[2], 1.25); +} + +#[test] +fn appearance_context_identity_exposes_only_semantic_inputs() { + let context = context(IEC_SRGB_D65_XYZ_FRAME_V1, 64.0); + assert_eq!( + context.schema_release(), + AppearanceContextSchemaReleaseId::Ciecam16ViewingInputsV1, + ); + assert_eq!(context.frame(), IEC_SRGB_D65_XYZ_FRAME_V1); + assert_eq!(context.adapting_luminance_cd_m2(), 64.0); + assert_eq!(context.background_luminance_ratio(), 0.2); + assert_eq!(context.surround_profile(), SurroundProfileId::AverageV1); +} diff --git a/crates/labcolors-core/src/spaces/srgb.rs b/crates/labcolors-core/src/spaces/srgb.rs index 79ad447f..c5392669 100644 --- a/crates/labcolors-core/src/spaces/srgb.rs +++ b/crates/labcolors-core/src/spaces/srgb.rs @@ -147,6 +147,17 @@ pub(crate) fn srgb_linear_from_srgb8(rgb: Srgb8) -> [f64; 3] { [decode_8bit(r), decode_8bit(g), decode_8bit(b)] } +/// Derive one CIE 1931 XYZ(D65, relative Y = 1) point from exact encoded sRGB8. +/// +/// This is the single operation-order owner for the registered finite-input +/// colourimetric transform. It is a deterministic model of a declared encoded +/// point signal; it does not claim that a renderer emitted or an +/// observer measured the resulting tristimulus. +#[inline] +pub(crate) fn xyz_d65_from_srgb8_v1(rgb: Srgb8) -> [f64; 3] { + srgb_to_xyz(srgb_linear_from_srgb8(rgb)) +} + /// Parse `#RRGGBB` → `(linear, display)` in one pass over the bytes: `linear` /// is the exact 8-bit decode (as [`srgb_from_hex`]), `display` is the /// **gamma-encoded** value WCAG measures — `[r/255, g/255, b/255]` — obtained From c5b934903f03af9f8c6e4201261eee9691721e04 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Wed, 22 Jul 2026 06:38:19 +0300 Subject: [PATCH 06/58] style(lcs): format sealed tristimulus slice --- crates/labcolors-core/src/lcs_occurrence.rs | 9 +--- .../src/lcs_occurrence_tests.rs | 49 +++++++++---------- 2 files changed, 24 insertions(+), 34 deletions(-) diff --git a/crates/labcolors-core/src/lcs_occurrence.rs b/crates/labcolors-core/src/lcs_occurrence.rs index f626b646..24569da3 100644 --- a/crates/labcolors-core/src/lcs_occurrence.rs +++ b/crates/labcolors-core/src/lcs_occurrence.rs @@ -159,9 +159,7 @@ impl AdmittedSrgb8TristimulusBindingV1 { pub(crate) const fn result_frame(self) -> ColorimetricFrameId { match self { - Self::Iec61966Srgb8ToCie1931TwoDegreeXyzD65RelativeY1V1 => { - IEC_SRGB_D65_XYZ_FRAME_V1 - } + Self::Iec61966Srgb8ToCie1931TwoDegreeXyzD65RelativeY1V1 => IEC_SRGB_D65_XYZ_FRAME_V1, } } } @@ -305,10 +303,7 @@ impl ModeledTristimulusDerivationV1 { } pub(crate) fn replay(self) -> Result { - derive_sample_with_binding( - self.provenance.source_signal, - self.provenance.binding, - ) + derive_sample_with_binding(self.provenance.source_signal, self.provenance.binding) } } diff --git a/crates/labcolors-core/src/lcs_occurrence_tests.rs b/crates/labcolors-core/src/lcs_occurrence_tests.rs index a1e216ec..b9c49ee6 100644 --- a/crates/labcolors-core/src/lcs_occurrence_tests.rs +++ b/crates/labcolors-core/src/lcs_occurrence_tests.rs @@ -8,8 +8,7 @@ use crate::lcs_occurrence::{ HueState, IEC_SRGB_D65_XYZ_FRAME_V1, LcsOccurrence, MUTATION_SENTINEL_XYZ_FRAME_V1, ModeledTristimulusDerivationV1, NumericDomainError, ObserverProfileId, OccurrenceFormationError, ReferenceWhiteId, SurroundProfileId, TristimulusComponentV1, - TristimulusDomainErrorV1, TristimulusSample, TristimulusScale, - derive_modeled_tristimulus_v1, + TristimulusDomainErrorV1, TristimulusSample, TristimulusScale, derive_modeled_tristimulus_v1, }; use crate::spaces::srgb::{D65_WHITE, srgb_linear_from_srgb8, srgb_to_xyz}; @@ -35,7 +34,10 @@ fn xyz_and_context_numeric_admission_is_fail_closed() { } let zero_la = AdaptingLuminanceCdM2::try_new(0.0).unwrap_err(); - assert_eq!(zero_la.field(), AppearanceContextFieldV1::AdaptingLuminanceCdM2); + assert_eq!( + zero_la.field(), + AppearanceContextFieldV1::AdaptingLuminanceCdM2 + ); assert_eq!(zero_la.reason(), NumericDomainError::NotPositive); let zero_background = BackgroundLuminanceRatio::try_new(0.0).unwrap_err(); @@ -54,9 +56,7 @@ fn xyz_and_context_numeric_admission_is_fail_closed() { for invalid in [f64::NAN, f64::INFINITY, -f64::MIN_POSITIVE, -0.0] { assert_eq!( - AdaptingLuminanceCdM2::try_new(invalid) - .unwrap_err() - .field(), + AdaptingLuminanceCdM2::try_new(invalid).unwrap_err().field(), AppearanceContextFieldV1::AdaptingLuminanceCdM2, ); assert_eq!( @@ -80,11 +80,10 @@ fn hue_algebra_cannot_encode_exact_absence_as_zero_degrees() { fn frame_mismatch_cannot_form_an_occurrence() { let relative = IEC_SRGB_D65_XYZ_FRAME_V1; let mismatching = MUTATION_SENTINEL_XYZ_FRAME_V1; - let sample = derive_modeled_tristimulus_v1(ColorSignal::from_srgb8(Srgb8::new([ - 0x44, 0x88, 0xCC, - ]))) - .unwrap() - .sample(); + let sample = + derive_modeled_tristimulus_v1(ColorSignal::from_srgb8(Srgb8::new([0x44, 0x88, 0xCC]))) + .unwrap() + .sample(); assert_eq!(sample.frame(), relative); assert_eq!( LcsOccurrence::in_context(sample, context(mismatching, 64.0)), @@ -98,8 +97,7 @@ fn frame_mismatch_cannot_form_an_occurrence() { #[test] fn occurrence_identity_contains_only_sample_and_context() { let relative = IEC_SRGB_D65_XYZ_FRAME_V1; - let sample = - TristimulusSample::try_from_xyz_for_test([0.1, 0.2, 0.3], relative).unwrap(); + let sample = TristimulusSample::try_from_xyz_for_test([0.1, 0.2, 0.3], relative).unwrap(); let occurrence = LcsOccurrence::in_context(sample, context(relative, 64.0)).unwrap(); assert_eq!(occurrence.sample(), sample); assert_eq!(occurrence.context(), context(relative, 64.0)); @@ -157,9 +155,7 @@ proptest! { #[test] fn f0_lowering_signature_accepts_only_one_profiled_signal() { - let lower: fn( - ColorSignal, - ) -> Result = + let lower: fn(ColorSignal) -> Result = derive_modeled_tristimulus_v1; assert!(lower(ColorSignal::from_srgb8(Srgb8::new([0; 3]))).is_ok()); } @@ -189,12 +185,11 @@ fn transform_release_v1_pins_a_pre_f0_binary64_vector() { // Recorded once from the pre-F0 decode-table + matrix operation order. This // is a release anti-drift vector, not a claim of measured or bounded // colorimetric evidence. - let xyz = derive_modeled_tristimulus_v1(ColorSignal::from_srgb8(Srgb8::new([ - 0x0A, 0x0B, 0x80, - ]))) - .unwrap() - .sample() - .xyz(); + let xyz = + derive_modeled_tristimulus_v1(ColorSignal::from_srgb8(Srgb8::new([0x0A, 0x0B, 0x80]))) + .unwrap() + .sample() + .xyz(); assert_eq!( xyz.map(f64::to_bits), [ @@ -220,7 +215,10 @@ fn every_srgb8_channel_code_has_finite_nonnegative_basis_contribution() { .unwrap() .sample() .xyz(); - assert!(xyz.into_iter().all(|component| component.is_finite() && component >= 0.0)); + assert!( + xyz.into_iter() + .all(|component| component.is_finite() && component >= 0.0) + ); } } } @@ -245,10 +243,7 @@ fn admitted_binding_is_one_closed_profile_transform_frame_tuple() { binding.result_frame().reference_white(), ReferenceWhiteId::Iec61966D65ChromaticityV1, ); - assert_eq!( - binding.result_frame().scale(), - TristimulusScale::RelativeY1, - ); + assert_eq!(binding.result_frame().scale(), TristimulusScale::RelativeY1,); assert_eq!( binding.result_frame().release(), ColorimetricFrameReleaseId::XyzV1, From 705fb68ccbf95ca1a353dec8c047af3671b35dd0 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Wed, 22 Jul 2026 13:19:10 +0300 Subject: [PATCH 07/58] feat(lcs): derive versioned appearance views --- crates/labcolors-core/src/lcs_occurrence.rs | 272 +++++++++++++- .../src/lcs_occurrence_tests.rs | 339 +++++++++++++++++- crates/labcolors-core/src/spaces/cam16.rs | 50 ++- crates/labcolors-core/src/spaces/oklab.rs | 26 +- crates/labcolors-core/src/spaces/vc.rs | 36 ++ 5 files changed, 710 insertions(+), 13 deletions(-) diff --git a/crates/labcolors-core/src/lcs_occurrence.rs b/crates/labcolors-core/src/lcs_occurrence.rs index 24569da3..bc051d57 100644 --- a/crates/labcolors-core/src/lcs_occurrence.rs +++ b/crates/labcolors-core/src/lcs_occurrence.rs @@ -1,15 +1,19 @@ //! Type foundation for a context-bound Labpics Colors Space occurrence. //! //! Encoded output, a framed tristimulus, immutable appearance context and -//! derived hue state are different values. The one executable transform here is -//! a sealed, versioned lowering of one encoded sRGB8 point through the existing -//! IEC transfer table and XYZ(D65) matrix. It is a deterministic -//! model derivation, not evidence that a host rendered or a person observed the +//! separately named derived views are different values. Executable transforms +//! are sealed and versioned: encoded sRGB8 lowers through the existing IEC +//! transfer table and XYZ(D65) matrix, then an occurrence can derive independent +//! rectangular Oklab and contextual CAM16 views. These are deterministic model +//! derivations, not evidence that a host rendered or a person observed the //! result. In particular, an occurrence has no inverse operation accepting an //! arbitrary second context. use crate::Srgb8; +use crate::spaces::cam16::forward_correlates_v1; +use crate::spaces::oklab::xyz_d65_to_oklab_v1; use crate::spaces::srgb::xyz_d65_from_srgb8_v1; +use crate::spaces::vc::{Cam16SurroundV1, ViewingConditions}; /// A registered encoded-output domain. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -569,3 +573,263 @@ impl LcsOccurrence { self.context } } + +/// Formula and operation-order release of the rectangular Oklab view. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum OklabViewReleaseId { + Ottosson20210125XyzD65V1, +} + +/// Formula and operation-order release of the context-dependent CAM16 view. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Cam16ViewReleaseId { + LiEtAl2017Cie248ForwardV1, +} + +/// Typed release discriminator used only to qualify derivation errors. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum AppearanceViewReleaseIdV1 { + Oklab(OklabViewReleaseId), + Cam16(Cam16ViewReleaseId), +} + +pub(crate) const OKLAB_VIEW_RELEASE_V1: OklabViewReleaseId = + OklabViewReleaseId::Ottosson20210125XyzD65V1; +pub(crate) const CAM16_VIEW_RELEASE_V1: Cam16ViewReleaseId = + Cam16ViewReleaseId::LiEtAl2017Cie248ForwardV1; + +/// Finite binary64 coordinate with canonical positive zero. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +struct FiniteCoordinate(u64); + +impl FiniteCoordinate { + fn new(value: f64) -> Result { + if !value.is_finite() { + return Err(NumericDomainError::NonFinite); + } + Ok(Self(if value == 0.0 { + 0.0_f64.to_bits() + } else { + value.to_bits() + })) + } + + fn get(self) -> f64 { + f64::from_bits(self.0) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum AppearanceViewFieldV1 { + OklabL, + OklabA, + OklabB, + Cam16J, + Cam16Q, + Cam16C, + Cam16M, + Cam16S, + Cam16Hue, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AppearanceStateDerivationErrorV1 { + UnsupportedFrame { + frame: ColorimetricFrameId, + }, + NumericDomain { + release: AppearanceViewReleaseIdV1, + field: AppearanceViewFieldV1, + reason: NumericDomainError, + }, +} + +/// Rectangular Oklab geometry of one admitted XYZ(D65) stimulus. +/// +/// It deliberately has no hue, context-dependent correlate, inverse or setter. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct OklabViewV1 { + release: OklabViewReleaseId, + l: FiniteCoordinate, + a: FiniteCoordinate, + b: FiniteCoordinate, +} + +impl OklabViewV1 { + pub(crate) const fn release(self) -> OklabViewReleaseId { + self.release + } + + pub(crate) fn l(self) -> f64 { + self.l.get() + } + + pub(crate) fn a(self) -> f64 { + self.a.get() + } + + pub(crate) fn b(self) -> f64 { + self.b.get() + } +} + +/// CAM16 appearance correlates of one occurrence under its own context. +/// +/// This view is not CAM16-UCS, a difference calibration or a rendering claim. +/// Its hue is the CAM16 angular correlate only; no Oklab direction enters it. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Cam16ViewV1 { + release: Cam16ViewReleaseId, + j: FiniteNonNegative, + q: FiniteNonNegative, + c: FiniteNonNegative, + m: FiniteNonNegative, + s: FiniteNonNegative, + hue: HueState, +} + +impl Cam16ViewV1 { + pub(crate) const fn release(self) -> Cam16ViewReleaseId { + self.release + } + + pub(crate) fn j(self) -> f64 { + self.j.get() + } + + pub(crate) fn q(self) -> f64 { + self.q.get() + } + + pub(crate) fn c(self) -> f64 { + self.c.get() + } + + pub(crate) fn m(self) -> f64 { + self.m.get() + } + + pub(crate) fn s(self) -> f64 { + self.s.get() + } + + pub(crate) const fn hue(self) -> HueState { + self.hue + } +} + +/// One-way, derived appearance snapshot of exactly one occurrence. +/// +/// Canonical LCS identity remains [`LcsOccurrence`] (`sample × context`). This +/// type is only a deterministic cache of separately named views and cannot be +/// constructed from, edited through or inverted from view coordinates. +#[derive(Debug, Clone, Copy)] +pub struct AppearanceState { + occurrence: LcsOccurrence, + oklab: OklabViewV1, + cam16: Cam16ViewV1, +} + +impl AppearanceState { + pub(crate) fn derive_v1( + occurrence: LcsOccurrence, + ) -> Result { + if occurrence.sample().frame() != IEC_SRGB_D65_XYZ_FRAME_V1 { + return Err(AppearanceStateDerivationErrorV1::UnsupportedFrame { + frame: occurrence.sample().frame(), + }); + } + + let oklab = derive_oklab_view_v1(occurrence.sample().xyz())?; + let cam16 = derive_cam16_view_v1(occurrence)?; + Ok(Self { + occurrence, + oklab, + cam16, + }) + } + + pub(crate) const fn occurrence(self) -> LcsOccurrence { + self.occurrence + } + + pub(crate) const fn oklab(self) -> OklabViewV1 { + self.oklab + } + + pub(crate) const fn cam16(self) -> Cam16ViewV1 { + self.cam16 + } +} + +fn view_numeric_error( + release: AppearanceViewReleaseIdV1, + field: AppearanceViewFieldV1, + reason: NumericDomainError, +) -> AppearanceStateDerivationErrorV1 { + AppearanceStateDerivationErrorV1::NumericDomain { + release, + field, + reason, + } +} + +fn derive_oklab_view_v1(xyz: [f64; 3]) -> Result { + let [l, a, b] = xyz_d65_to_oklab_v1(xyz); + let release = AppearanceViewReleaseIdV1::Oklab(OKLAB_VIEW_RELEASE_V1); + Ok(OklabViewV1 { + release: OKLAB_VIEW_RELEASE_V1, + l: FiniteCoordinate::new(l) + .map_err(|reason| view_numeric_error(release, AppearanceViewFieldV1::OklabL, reason))?, + a: FiniteCoordinate::new(a) + .map_err(|reason| view_numeric_error(release, AppearanceViewFieldV1::OklabA, reason))?, + b: FiniteCoordinate::new(b) + .map_err(|reason| view_numeric_error(release, AppearanceViewFieldV1::OklabB, reason))?, + }) +} + +fn derive_cam16_view_v1( + occurrence: LcsOccurrence, +) -> Result { + let context = occurrence.context(); + let surround = match context.surround_profile() { + SurroundProfileId::AverageV1 => Cam16SurroundV1::Average, + SurroundProfileId::DimV1 => Cam16SurroundV1::Dim, + SurroundProfileId::DarkV1 => Cam16SurroundV1::Dark, + }; + let vc = match context.schema_release() { + AppearanceContextSchemaReleaseId::Ciecam16ViewingInputsV1 => { + ViewingConditions::from_semantic_inputs_v1( + context.adapting_luminance_cd_m2(), + context.background_luminance_ratio(), + surround, + ) + } + }; + let coordinates = forward_correlates_v1(occurrence.sample().xyz(), &vc); + let release = AppearanceViewReleaseIdV1::Cam16(CAM16_VIEW_RELEASE_V1); + let admit = |value, field| { + FiniteNonNegative::new(value).map_err(|reason| view_numeric_error(release, field, reason)) + }; + let j = admit(coordinates.j, AppearanceViewFieldV1::Cam16J)?; + let q = admit(coordinates.q, AppearanceViewFieldV1::Cam16Q)?; + let c = admit(coordinates.c, AppearanceViewFieldV1::Cam16C)?; + let m = admit(coordinates.m, AppearanceViewFieldV1::Cam16M)?; + let s = admit(coordinates.s, AppearanceViewFieldV1::Cam16S)?; + let hue = if m.get() == 0.0 { + HueState::UndefinedExact + } else { + HueState::Defined(HueAngle::new(coordinates.h).map_err(|reason| { + view_numeric_error(release, AppearanceViewFieldV1::Cam16Hue, reason) + })?) + }; + Ok(Cam16ViewV1 { + release: CAM16_VIEW_RELEASE_V1, + j, + q, + c, + m, + s, + hue, + }) +} diff --git a/crates/labcolors-core/src/lcs_occurrence_tests.rs b/crates/labcolors-core/src/lcs_occurrence_tests.rs index b9c49ee6..7f9659d9 100644 --- a/crates/labcolors-core/src/lcs_occurrence_tests.rs +++ b/crates/labcolors-core/src/lcs_occurrence_tests.rs @@ -3,25 +3,45 @@ use proptest::prelude::*; use crate::Srgb8; use crate::lcs_occurrence::{ ADMITTED_SRGB8_TRISTIMULUS_BINDING_V1, AdaptingLuminanceCdM2, AppearanceContextFieldV1, - AppearanceContextId, AppearanceContextSchemaReleaseId, BackgroundLuminanceRatio, ColorSignal, - ColorimetricFrameId, ColorimetricFrameReleaseId, ColorimetricTransformReleaseId, HueAngle, - HueState, IEC_SRGB_D65_XYZ_FRAME_V1, LcsOccurrence, MUTATION_SENTINEL_XYZ_FRAME_V1, - ModeledTristimulusDerivationV1, NumericDomainError, ObserverProfileId, + AppearanceContextId, AppearanceContextSchemaReleaseId, AppearanceState, + AppearanceStateDerivationErrorV1, AppearanceViewFieldV1, AppearanceViewReleaseIdV1, + BackgroundLuminanceRatio, CAM16_VIEW_RELEASE_V1, ColorSignal, ColorimetricFrameId, + ColorimetricFrameReleaseId, ColorimetricTransformReleaseId, HueAngle, HueState, + IEC_SRGB_D65_XYZ_FRAME_V1, LcsOccurrence, MUTATION_SENTINEL_XYZ_FRAME_V1, + ModeledTristimulusDerivationV1, NumericDomainError, OKLAB_VIEW_RELEASE_V1, ObserverProfileId, OccurrenceFormationError, ReferenceWhiteId, SurroundProfileId, TristimulusComponentV1, TristimulusDomainErrorV1, TristimulusSample, TristimulusScale, derive_modeled_tristimulus_v1, }; +use crate::spaces::cam16::{ForwardCacheGuard, forward, forward_correlates_v1}; +use crate::spaces::oklab::{srgb_linear_to_oklab, xyz_d65_to_oklab_v1}; use crate::spaces::srgb::{D65_WHITE, srgb_linear_from_srgb8, srgb_to_xyz}; +use crate::spaces::vc::ViewingConditions; fn context(frame: ColorimetricFrameId, la: f64) -> AppearanceContextId { + context_with_surround(frame, la, SurroundProfileId::AverageV1) +} + +fn context_with_surround( + frame: ColorimetricFrameId, + la: f64, + surround: SurroundProfileId, +) -> AppearanceContextId { AppearanceContextId::from_inputs( AppearanceContextSchemaReleaseId::Ciecam16ViewingInputsV1, frame, AdaptingLuminanceCdM2::try_new(la).unwrap(), BackgroundLuminanceRatio::try_new(0.2).unwrap(), - SurroundProfileId::AverageV1, + surround, ) } +fn occurrence_from_srgb8(bytes: [u8; 3], context: AppearanceContextId) -> LcsOccurrence { + let sample = derive_modeled_tristimulus_v1(ColorSignal::from_srgb8(Srgb8::new(bytes))) + .unwrap() + .sample(); + LcsOccurrence::in_context(sample, context).unwrap() +} + #[test] fn xyz_and_context_numeric_admission_is_fail_closed() { let relative = IEC_SRGB_D65_XYZ_FRAME_V1; @@ -151,6 +171,25 @@ proptest! { ); prop_assert_eq!(derived.sample().frame(), IEC_SRGB_D65_XYZ_FRAME_V1); } + + #[test] + fn direct_xyz_oklab_projection_tracks_the_existing_srgb_kernel_without_routing_through_it( + bytes in any::<[u8; 3]>(), + ) { + let linear = srgb_linear_from_srgb8(Srgb8::new(bytes)); + let legacy_srgb_projection = srgb_linear_to_oklab(linear); + let direct_xyz_projection = xyz_d65_to_oklab_v1(srgb_to_xyz(linear)); + + for component in 0..3 { + prop_assert!( + (direct_xyz_projection[component] - legacy_srgb_projection[component]).abs() + <= 2.0e-8, + "component {component}: direct={} existing={}", + direct_xyz_projection[component], + legacy_srgb_projection[component], + ); + } + } } #[test] @@ -336,3 +375,293 @@ fn appearance_context_identity_exposes_only_semantic_inputs() { assert_eq!(context.background_luminance_ratio(), 0.2); assert_eq!(context.surround_profile(), SurroundProfileId::AverageV1); } + +#[test] +fn appearance_state_is_one_way_views_of_the_same_occurrence_with_separate_releases() { + let occurrence = + occurrence_from_srgb8([0x00, 0x00, 0xFF], context(IEC_SRGB_D65_XYZ_FRAME_V1, 64.0)); + let state = AppearanceState::derive_v1(occurrence).unwrap(); + + assert_eq!(state.occurrence(), occurrence); + assert_eq!(state.oklab().release(), OKLAB_VIEW_RELEASE_V1); + assert_eq!(state.cam16().release(), CAM16_VIEW_RELEASE_V1); + + let direct = xyz_d65_to_oklab_v1(occurrence.sample().xyz()); + assert_eq!( + [state.oklab().l(), state.oklab().a(), state.oklab().b()].map(f64::to_bits), + direct.map(f64::to_bits), + ); + + let expected_cam = forward_correlates_v1(occurrence.sample().xyz(), &ViewingConditions::srgb()); + let cam = state.cam16(); + assert_eq!(cam.j().to_bits(), expected_cam.j.to_bits()); + assert_eq!(cam.q().to_bits(), expected_cam.q.to_bits()); + assert_eq!(cam.c().to_bits(), expected_cam.c.to_bits()); + assert_eq!(cam.m().to_bits(), expected_cam.m.to_bits()); + assert_eq!(cam.s().to_bits(), expected_cam.s.to_bits()); + let HueState::Defined(cam_hue) = cam.hue() else { + panic!("chromatic blue must have a CAM16 hue"); + }; + assert_eq!(cam_hue.degrees().to_bits(), expected_cam.h.to_bits()); + + let oklab_hue = state.oklab().b().atan2(state.oklab().a()).to_degrees(); + let oklab_hue = if oklab_hue < 0.0 { + oklab_hue + 360.0 + } else { + oklab_hue + }; + assert!( + (cam_hue.degrees() - oklab_hue).abs() > 10.0, + "CAM16 hue must not be copied from Oklab: CAM16={} Oklab={oklab_hue}", + cam_hue.degrees(), + ); +} + +#[test] +fn context_changes_only_the_contextual_cam16_view() { + let sample = + derive_modeled_tristimulus_v1(ColorSignal::from_srgb8(Srgb8::new([0x44, 0x88, 0xCC]))) + .unwrap() + .sample(); + let average = AppearanceState::derive_v1( + LcsOccurrence::in_context( + sample, + context_with_surround( + IEC_SRGB_D65_XYZ_FRAME_V1, + 64.0, + SurroundProfileId::AverageV1, + ), + ) + .unwrap(), + ) + .unwrap(); + let dim = AppearanceState::derive_v1( + LcsOccurrence::in_context( + sample, + context_with_surround(IEC_SRGB_D65_XYZ_FRAME_V1, 64.0, SurroundProfileId::DimV1), + ) + .unwrap(), + ) + .unwrap(); + + assert_eq!(average.oklab(), dim.oklab()); + assert_ne!(average.cam16().j().to_bits(), dim.cam16().j().to_bits()); + assert_ne!(average.cam16().m().to_bits(), dim.cam16().m().to_bits()); + assert_ne!(average.occurrence(), dim.occurrence()); +} + +#[test] +fn every_registered_surround_maps_to_its_exact_cam16_kernel_tuple() { + for (surround, vc) in [ + (SurroundProfileId::AverageV1, ViewingConditions::srgb()), + (SurroundProfileId::DimV1, ViewingConditions::dim_surround()), + ( + SurroundProfileId::DarkV1, + ViewingConditions::dark_surround(), + ), + ] { + let occurrence = occurrence_from_srgb8( + [0x44, 0x88, 0xCC], + context_with_surround(IEC_SRGB_D65_XYZ_FRAME_V1, 64.0, surround), + ); + let actual = AppearanceState::derive_v1(occurrence).unwrap().cam16(); + let expected = forward_correlates_v1(occurrence.sample().xyz(), &vc); + assert_eq!( + [actual.j(), actual.q(), actual.c(), actual.m(), actual.s(),].map(f64::to_bits), + [expected.j, expected.q, expected.c, expected.m, expected.s,].map(f64::to_bits), + "surround {surround:?} must not mix registered tuple fields", + ); + let HueState::Defined(actual_hue) = actual.hue() else { + panic!("chromatic fixture must have hue under {surround:?}"); + }; + assert_eq!(actual_hue.degrees().to_bits(), expected.h.to_bits()); + } +} + +#[test] +fn exact_zero_coordinate_has_no_invented_hue() { + let state = AppearanceState::derive_v1(occurrence_from_srgb8( + [0; 3], + context(IEC_SRGB_D65_XYZ_FRAME_V1, 64.0), + )) + .unwrap(); + + assert_eq!( + [state.oklab().l(), state.oklab().a(), state.oklab().b()].map(f64::to_bits), + [0; 3], + ); + assert_eq!(state.cam16().j().to_bits(), 0); + assert_eq!(state.cam16().q().to_bits(), 0); + assert_eq!(state.cam16().c().to_bits(), 0); + assert_eq!(state.cam16().m().to_bits(), 0); + assert_eq!(state.cam16().s().to_bits(), 0); + assert_eq!(state.cam16().hue(), HueState::UndefinedExact); +} + +#[test] +fn state_derivation_rejects_an_unregistered_frame_before_any_view_math() { + let frame = MUTATION_SENTINEL_XYZ_FRAME_V1; + let sample = TristimulusSample::try_from_xyz_for_test([0.1, 0.2, 0.3], frame).unwrap(); + let occurrence = LcsOccurrence::in_context(sample, context(frame, 64.0)).unwrap(); + + assert_eq!( + AppearanceState::derive_v1(occurrence).unwrap_err(), + AppearanceStateDerivationErrorV1::UnsupportedFrame { frame }, + ); +} + +#[test] +fn state_derivation_rejects_nonfinite_derived_coordinates_with_release_and_field() { + let frame = IEC_SRGB_D65_XYZ_FRAME_V1; + let sample = TristimulusSample::try_from_xyz_for_test([f64::MAX; 3], frame).unwrap(); + let occurrence = LcsOccurrence::in_context(sample, context(frame, 64.0)).unwrap(); + + assert_eq!( + AppearanceState::derive_v1(occurrence).unwrap_err(), + AppearanceStateDerivationErrorV1::NumericDomain { + release: AppearanceViewReleaseIdV1::Oklab(OKLAB_VIEW_RELEASE_V1), + field: AppearanceViewFieldV1::OklabL, + reason: NumericDomainError::NonFinite, + }, + ); +} + +#[test] +fn direct_oklab_release_pins_an_external_xyz_projection_vector() { + // Fixed vector from the CSS Color 4 direct XYZ(D65) -> Oklab matrices for + // the already-pinned `[0A, 0B, 80]` F0 tristimulus. Tolerance covers only + // cross-libm cbrt ULPs; it is far below the direct-vs-legacy matrix delta. + let state = AppearanceState::derive_v1(occurrence_from_srgb8( + [0x0A, 0x0B, 0x80], + context(IEC_SRGB_D65_XYZ_FRAME_V1, 64.0), + )) + .unwrap(); + let actual = [state.oklab().l(), state.oklab().a(), state.oklab().b()]; + let expected = [ + 0.284_226_036_666_170_47, + -0.009_591_562_466_562_426, + -0.178_614_436_252_897_94, + ]; + for component in 0..3 { + assert!( + (actual[component] - expected[component]).abs() <= 1.0e-14, + "Oklab component {component} drifted: actual={} expected={}", + actual[component], + expected[component], + ); + } +} + +#[test] +fn cam16_release_pins_a_full_external_correlate_vector() { + // colour-science CIECAM16, D65, L_A=64 cd/m², Y_b/Y_w=0.2, average + // surround. Unlike the older J/M/h table this pins the newly exposed + // Q/C/s correlates as well. The tolerances cover only the documented CSS + // XYZ constant delta from colour-science's matrix derivation. + let state = AppearanceState::derive_v1(occurrence_from_srgb8( + [0x00, 0x00, 0xFF], + context(IEC_SRGB_D65_XYZ_FRAME_V1, 64.0), + )) + .unwrap(); + let cam = state.cam16(); + let HueState::Defined(hue) = cam.hue() else { + panic!("reference blue must retain a CAM16 hue"); + }; + + for (name, actual, expected, tolerance) in [ + ("J", cam.j(), 25.271_208_691_856_113, 0.01), + ("Q", cam.q(), 109.108_232_192_767_33, 0.1), + ("C", cam.c(), 86.580_098_936_732_16, 0.1), + ("M", cam.m(), 78.737_310_637_269_06, 0.05), + ("s", cam.s(), 84.949_637_273_879, 0.1), + ("h", hue.degrees(), 282.871_080_928_130_14, 0.15), + ] { + assert!( + (actual - expected).abs() < tolerance, + "CAM16 {name} drifted: actual={actual} expected={expected}", + ); + } +} + +#[test] +fn full_appearance_state_derivation_is_allocation_free() { + let occurrence = + occurrence_from_srgb8([0x44, 0x88, 0xCC], context(IEC_SRGB_D65_XYZ_FRAME_V1, 64.0)); + let (derived, allocations) = + crate::test_support::measured_allocations(|| AppearanceState::derive_v1(occurrence)); + + assert_eq!(allocations, 0); + assert!(derived.is_ok()); +} + +#[test] +fn appearance_state_bypasses_active_xyz_only_cache_for_each_context_without_allocating() { + let frame = IEC_SRGB_D65_XYZ_FRAME_V1; + let sample = + derive_modeled_tristimulus_v1(ColorSignal::from_srgb8(Srgb8::new([0x44, 0x88, 0xCC]))) + .unwrap() + .sample(); + let xyz = sample.xyz(); + + // Freeze the per-context cache-free answers before activating the legacy + // XYZ-only cache. These are independent of its ambient guard state. + let expected_dim = forward_correlates_v1(xyz, &ViewingConditions::dim_surround()); + let expected_dark = forward_correlates_v1(xyz, &ViewingConditions::dark_surround()); + + let dim_occurrence = LcsOccurrence::in_context( + sample, + context_with_surround(frame, 64.0, SurroundProfileId::DimV1), + ) + .unwrap(); + let dark_occurrence = LcsOccurrence::in_context( + sample, + context_with_surround(frame, 64.0, SurroundProfileId::DarkV1), + ) + .unwrap(); + + let _guard = ForwardCacheGuard::activate(); + let cached_average = forward(xyz, &ViewingConditions::srgb()); + assert_ne!(cached_average.0.to_bits(), expected_dim.j.to_bits()); + assert_ne!(cached_average.0.to_bits(), expected_dark.j.to_bits()); + + let (dim, dim_allocations) = + crate::test_support::measured_allocations(|| AppearanceState::derive_v1(dim_occurrence)); + let (dark, dark_allocations) = + crate::test_support::measured_allocations(|| AppearanceState::derive_v1(dark_occurrence)); + let dim = dim.unwrap().cam16(); + let dark = dark.unwrap().cam16(); + + assert_eq!(dim_allocations, 0); + assert_eq!(dark_allocations, 0); + assert_eq!( + [dim.j(), dim.q(), dim.c(), dim.m(), dim.s()].map(f64::to_bits), + [ + expected_dim.j, + expected_dim.q, + expected_dim.c, + expected_dim.m, + expected_dim.s, + ] + .map(f64::to_bits), + ); + assert_eq!( + [dark.j(), dark.q(), dark.c(), dark.m(), dark.s()].map(f64::to_bits), + [ + expected_dark.j, + expected_dark.q, + expected_dark.c, + expected_dark.m, + expected_dark.s, + ] + .map(f64::to_bits), + ); + + let HueState::Defined(dim_hue) = dim.hue() else { + panic!("chromatic dim fixture must have CAM16 hue"); + }; + let HueState::Defined(dark_hue) = dark.hue() else { + panic!("chromatic dark fixture must have CAM16 hue"); + }; + assert_eq!(dim_hue.degrees().to_bits(), expected_dim.h.to_bits()); + assert_eq!(dark_hue.degrees().to_bits(), expected_dark.h.to_bits()); +} diff --git a/crates/labcolors-core/src/spaces/cam16.rs b/crates/labcolors-core/src/spaces/cam16.rs index fbfcc99c..54d5aace 100644 --- a/crates/labcolors-core/src/spaces/cam16.rs +++ b/crates/labcolors-core/src/spaces/cam16.rs @@ -285,9 +285,7 @@ pub(crate) fn forward(xyz: [f64; 3], vc: &ViewingConditions) -> (f64, f64, f64) }) { return hit; } - #[cfg(test)] - FORWARD_CALLS.with(|c| c.set(c.get() + 1)); - let result = forward_compute(xyz, vc); + let result = forward_cache_free_v1(xyz, vc); FORWARD_CACHE.with(|c| { let mut c = c.borrow_mut(); if c.active { @@ -297,6 +295,52 @@ pub(crate) fn forward(xyz: [f64; 3], vc: &ViewingConditions) -> (f64, f64, f64) result } +/// Execute one CAM16 forward pass without consulting the legacy per-set cache. +/// +/// The legacy cache is deliberately keyed only by XYZ because its +/// `resolve_set` owner holds one viewing condition for the entire guard scope. +/// An F0 appearance state instead carries its own immutable context, so it must +/// never inherit that ambient single-context assumption. Both paths still use +/// the same numeric owner below; this boundary changes caching only, not math or +/// operation order. +#[inline] +fn forward_cache_free_v1(xyz: [f64; 3], vc: &ViewingConditions) -> (f64, f64, f64) { + #[cfg(test)] + FORWARD_CALLS.with(|c| c.set(c.get() + 1)); + forward_compute(xyz, vc) +} + +/// Complete correlates of the registered CAM16 forward view. +/// +/// This is an internal numeric carrier, not an editable colour value or a +/// difference metric. Hue absence is classified by the occurrence layer after +/// all coordinates have passed its finite-domain admission. +#[derive(Debug, Clone, Copy, PartialEq)] +pub(crate) struct Cam16CorrelatesV1 { + pub(crate) j: f64, + pub(crate) q: f64, + pub(crate) c: f64, + pub(crate) m: f64, + pub(crate) s: f64, + pub(crate) h: f64, +} + +/// Derive the full CAM16 correlate set from the same cache-free `J/M/h` +/// operation-order owner used on a cache miss by [`forward`]. +/// +/// `C`, `Q` and `s` are the published CAM16 correlates. The explicit `J = 0` +/// branch gives mathematical black `(C, M, Q, s) = 0` without evaluating the +/// otherwise indeterminate `C / sqrt(J / 100)` ratio. +pub(crate) fn forward_correlates_v1(xyz: [f64; 3], vc: &ViewingConditions) -> Cam16CorrelatesV1 { + let (j, m, h) = forward_cache_free_v1(xyz, vc); + let root_j = (j / 100.0).sqrt(); + let c = m / vc.fl_pow_025; + let q = (4.0 / vc.c) * root_j * (vc.aw + 4.0) * vc.fl_pow_025; + let alpha = if root_j == 0.0 { 0.0 } else { c / root_j }; + let s = 50.0 * (vc.c * alpha / (vc.aw + 4.0)).sqrt(); + Cam16CorrelatesV1 { j, q, c, m, s, h } +} + /// The CIECAM16 forward math itself (cache-free); see [`forward`]. fn forward_compute(xyz: [f64; 3], vc: &ViewingConditions) -> (f64, f64, f64) { let xyz = [xyz[0] * 100.0, xyz[1] * 100.0, xyz[2] * 100.0]; diff --git a/crates/labcolors-core/src/spaces/oklab.rs b/crates/labcolors-core/src/spaces/oklab.rs index 2d02a326..d461906f 100644 --- a/crates/labcolors-core/src/spaces/oklab.rs +++ b/crates/labcolors-core/src/spaces/oklab.rs @@ -1,4 +1,4 @@ -//! Oklab perceptual colour space (sRGB path). +//! Oklab perceptual colour-space kernels for direct XYZ(D65) and sRGB paths. //! //! Source: Björn Ottosson, "A perceptual color space for image processing" //! (2020), . The matrices below are @@ -25,6 +25,18 @@ const LMS_TO_OKLAB: [[f64; 3]; 3] = [ [ 0.0259040371, 0.7827717662, -0.8086757660], ]; +// Direct XYZ(D65) -> LMS projection used by the registered F0 Oklab view. +// These are the CSS Color 4 / 2021 Oklab coefficients. Keeping this projection +// direct is important: an XYZ stimulus is not an encoded or linear-sRGB value, +// and routing it through the inverse sRGB matrix would make an output space an +// accidental part of stimulus geometry. +#[rustfmt::skip] +const XYZ_D65_TO_LMS_OKLAB_20210125: [[f64; 3]; 3] = [ + [0.8190224379967030, 0.3619062600528904, -0.1288737815209879], + [0.0329836539323885, 0.9292868615863434, 0.0361446663506424], + [0.0481771893596242, 0.2642395317527308, 0.6335478284694309], +]; + /// Canonical degree domain of a hue angle. pub(crate) const HUE_DEG_MIN_INCLUSIVE: f64 = 0.0; pub(crate) const HUE_DEG_MAX_EXCLUSIVE: f64 = 360.0; @@ -57,6 +69,18 @@ pub(crate) fn srgb_linear_to_oklab(rgb: [f64; 3]) -> [f64; 3] { mat_vec_mul(LMS_TO_OKLAB, lms_) } +/// Direct 2021 Oklab projection of one relative XYZ(D65) stimulus. +/// +/// The caller owns frame admission. This kernel does not perform chromatic +/// adaptation, infer an output profile, clamp coordinates or provide an +/// inverse/editing operation. +#[inline] +pub(crate) fn xyz_d65_to_oklab_v1(xyz: [f64; 3]) -> [f64; 3] { + let lms = mat_vec_mul(XYZ_D65_TO_LMS_OKLAB_20210125, xyz); + let lms_root = [lms[0].cbrt(), lms[1].cbrt(), lms[2].cbrt()]; + mat_vec_mul(LMS_TO_OKLAB, lms_root) +} + pub(crate) fn oklab_to_srgb_linear(lab: [f64; 3]) -> [f64; 3] { let lms_ = mat_vec_mul(OKLAB_TO_LMS, lab); let lms = [lms_[0].powi(3), lms_[1].powi(3), lms_[2].powi(3)]; diff --git a/crates/labcolors-core/src/spaces/vc.rs b/crates/labcolors-core/src/spaces/vc.rs index 6079c6b5..5bf159ed 100644 --- a/crates/labcolors-core/src/spaces/vc.rs +++ b/crates/labcolors-core/src/spaces/vc.rs @@ -17,6 +17,17 @@ use std::sync::OnceLock; use super::{cam16::adapt, cat16::xyz_to_cone}; +/// Closed CIECAM16 surround tuple admitted by the F0 occurrence context. +/// +/// Keeping the triplets behind variants prevents callers from independently +/// combining `F`, `c` and `N_c` into a context the release never registered. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum Cam16SurroundV1 { + Average, + Dim, + Dark, +} + /// Условия просмотра для модели цветового восприятия CIECAM16. /// /// Значения по умолчанию соответствуют sRGB: D65, серый фон 20 %, среднее @@ -75,6 +86,31 @@ impl Default for ViewingConditions { } impl ViewingConditions { + /// Build CAM16 derived constants from immutable semantic context inputs. + /// + /// `background_luminance_ratio` is `Y_b / Y_w`, not a percentage. The + /// existing builder consumes percent, so the conversion remains here at + /// the legacy-kernel boundary rather than leaking into the occurrence + /// domain. Admission of the numeric inputs is owned by `AppearanceContext`. + pub(crate) fn from_semantic_inputs_v1( + adapting_luminance_cd_m2: f64, + background_luminance_ratio: f64, + surround: Cam16SurroundV1, + ) -> Self { + let (f, c, nc) = match surround { + Cam16SurroundV1::Average => (1.0, 0.69, 1.0), + Cam16SurroundV1::Dim => (0.9, 0.59, 0.9), + Cam16SurroundV1::Dark => (0.8, 0.525, 0.8), + }; + Self::build( + adapting_luminance_cd_m2, + background_luminance_ratio * 100.0, + f, + c, + nc, + ) + } + /// Коэффициент фоновой яркости CAM16: `n = Y_b / Y_w`. pub fn n(&self) -> f64 { self.n From 68f6ac4b76dcca4491d93763aa4cd69c82713d45 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Wed, 22 Jul 2026 13:21:23 +0300 Subject: [PATCH 08/58] style(lcs): canonicalize Oklab coefficient --- crates/labcolors-core/src/spaces/oklab.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/labcolors-core/src/spaces/oklab.rs b/crates/labcolors-core/src/spaces/oklab.rs index d461906f..6f23b42d 100644 --- a/crates/labcolors-core/src/spaces/oklab.rs +++ b/crates/labcolors-core/src/spaces/oklab.rs @@ -32,7 +32,7 @@ const LMS_TO_OKLAB: [[f64; 3]; 3] = [ // accidental part of stimulus geometry. #[rustfmt::skip] const XYZ_D65_TO_LMS_OKLAB_20210125: [[f64; 3]; 3] = [ - [0.8190224379967030, 0.3619062600528904, -0.1288737815209879], + [0.819_022_437_996_703, 0.3619062600528904, -0.1288737815209879], [0.0329836539323885, 0.9292868615863434, 0.0361446663506424], [0.0481771893596242, 0.2642395317527308, 0.6335478284694309], ]; From 8bfcdd2ca5bea8164f0f378134f486f5779b6b40 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Wed, 22 Jul 2026 13:33:36 +0300 Subject: [PATCH 09/58] feat(core): expose typed terminal program path --- crates/labcolors-core/src/program_session.rs | 1191 +++++++++++++++-- .../src/program_session_tests.rs | 313 +++-- 2 files changed, 1275 insertions(+), 229 deletions(-) diff --git a/crates/labcolors-core/src/program_session.rs b/crates/labcolors-core/src/program_session.rs index e4528632..67d4340d 100644 --- a/crates/labcolors-core/src/program_session.rs +++ b/crates/labcolors-core/src/program_session.rs @@ -1,10 +1,13 @@ -//! Generation-bound execution owner for one compiled point-render graph. +//! Terminal generation-bound path prepared for the atomic public cut. //! -//! This is deliberately below the future public `Program` boundary. It owns -//! no client vocabulary and accepts no recipe-shaped input. The sole strong -//! epoch lives in [`PointRenderOwnerV1`]; attached sessions retain only -//! a [`Weak`] reference, so successful replacement or disposal makes the old +//! [`Program`] is the authored, typed Paint/Surface/Occurrence declaration. +//! [`Program::compile`] validates and canonicalises the complete graph before a +//! [`CompiledProgram`] can exist. [`PointRenderOwner`] then becomes the sole +//! strong owner of one compiled epoch. Attached [`Session`] values retain only +//! a [`Weak`] reference, so replacement, disposal or owner drop makes the old //! graph physically unreachable from every old session. +//! The crate root keeps this path private until the atomic public-surface cut; +//! it must not create a second simultaneously supported authoring schema. //! //! The first executable transport is intentionally narrow: one correlated set //! of encoded Surface input signals per revision. It is transport-only state, @@ -20,8 +23,262 @@ use std::rc::{Rc, Weak}; use crate::Srgb8; use crate::appearance::{ AdmittedAppearanceBindings, AppearanceBindings, AppearanceGraphSpec, AppearanceWorkspace, - BindingError, CompileError, CompiledAppearanceGraph, SurfaceInputPortId, + BindingError, ColorInputId as AppearanceColorInputId, CompileError, + CompiledAppearanceGraph, OccurrenceId as AppearanceOccurrenceId, + OccurrenceSpec as AppearanceOccurrenceSpec, OpacityInputId as AppearanceOpacityInputId, + PaintId as AppearancePaintId, PaintSpec as AppearancePaintSpec, + SurfaceId as AppearanceSurfaceId, SurfaceInputPortId as AppearanceSurfaceInputId, + SurfaceSpec as AppearanceSurfaceSpec, }; +use crate::composition::CompositionProfileV1; + +macro_rules! opaque_program_id { + ($name:ident, $description:literal) => { + #[doc = $description] + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct $name(u32); + + impl $name { + /// Construct one client-owned opaque identity. + pub const fn new(raw: u32) -> Self { + Self(raw) + } + + /// Return the exact transport identity. + pub const fn value(self) -> u32 { + self.0 + } + } + }; +} + +opaque_program_id!(ColorInputId, "Identity of one immutable encoded colour input."); +opaque_program_id!( + SurfaceInputId, + "Identity of one runtime encoded Surface input." +); +opaque_program_id!(OpacityInputId, "Identity of one immutable opacity input."); +opaque_program_id!(PaintId, "Identity of one Paint node."); +opaque_program_id!(SurfaceId, "Identity of one Surface node."); +opaque_program_id!(OccurrenceId, "Identity of one Paint-on-Surface occurrence."); + +/// One immutable encoded colour binding owned by a [`Program`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ColorInput { + id: ColorInputId, + value: Srgb8, +} + +impl ColorInput { + /// Bind one opaque input identity to exact encoded-sRGB8 bytes. + pub const fn new(id: ColorInputId, value: Srgb8) -> Self { + Self { id, value } + } + + /// Return the opaque input identity. + pub const fn id(self) -> ColorInputId { + self.id + } + + /// Return the exact immutable value. + pub const fn value(self) -> Srgb8 { + self.value + } +} + +/// One immutable straight-alpha binding owned by a [`Program`]. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct OpacityInput { + id: OpacityInputId, + value: f64, +} + +impl OpacityInput { + /// Bind one opaque input identity to a finite value in `[0, 1]`. + /// + /// Numeric admission happens atomically in [`Program::compile`]. + pub const fn new(id: OpacityInputId, value: f64) -> Self { + Self { id, value } + } + + /// Return the opaque input identity. + pub const fn id(self) -> OpacityInputId { + self.id + } + + /// Return the authored binary64 value. + pub const fn value(self) -> f64 { + self.value + } +} + +/// Generic Paint constructor algebra supported by the point renderer. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Paint { + /// Create an opaque Paint from exact encoded bytes. + Solid { id: PaintId, color: ColorInputId }, + /// Multiply a Paint's straight alpha by one admitted scalar. + Opacity { + id: PaintId, + source: PaintId, + opacity: OpacityInputId, + }, +} + +/// Generic Surface constructor algebra supported by the point renderer. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Surface { + /// Read one revision-bound runtime point input. + Input { + id: SurfaceId, + input: SurfaceInputId, + }, + /// Give a visible occurrence result a Surface identity for nesting. + FromOccurrence { + id: SurfaceId, + occurrence: OccurrenceId, + }, +} + +/// Closed mathematical composition profile set for this point-program version. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CompositionProfile { + /// Exact encoded-sRGB8 source-over with its declared byte rounding order. + EncodedSrgb8SourceOverV1, +} + +/// The only canonical application of one Paint to one Surface. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Occurrence { + id: OccurrenceId, + subject: PaintId, + against: SurfaceId, + composition: CompositionProfile, +} + +impl Occurrence { + /// Declare one Paint-on-Surface application. + pub const fn new( + id: OccurrenceId, + subject: PaintId, + against: SurfaceId, + composition: CompositionProfile, + ) -> Self { + Self { + id, + subject, + against, + composition, + } + } + + /// Return the occurrence identity. + pub const fn id(self) -> OccurrenceId { + self.id + } + + /// Return the subject Paint identity. + pub const fn subject(self) -> PaintId { + self.subject + } + + /// Return the backdrop Surface identity. + pub const fn against(self) -> SurfaceId { + self.against + } + + /// Return the exact mathematical composition profile. + pub const fn composition(self) -> CompositionProfile { + self.composition + } +} + +/// Immutable generic point-render declaration. +/// +/// List order carries no semantics. Compilation canonicalises every typed ID +/// domain and rejects dangling edges, duplicates, cycles and invalid numeric +/// inputs before any runtime owner can be constructed. +#[derive(Debug, Clone, PartialEq)] +pub struct Program { + colors: Vec, + surface_inputs: Vec, + opacities: Vec, + paints: Vec, + surfaces: Vec, + occurrences: Vec, +} + +impl Program { + /// Assemble one declaration. This constructor performs no partial compile. + pub fn new( + colors: Vec, + surface_inputs: Vec, + opacities: Vec, + paints: Vec, + surfaces: Vec, + occurrences: Vec, + ) -> Self { + Self { + colors, + surface_inputs, + opacities, + paints, + surfaces, + occurrences, + } + } + + /// Atomically validate, bind and canonicalise this complete declaration. + pub fn compile(self) -> Result { + prepare_program(self).map(|epoch| CompiledProgram { epoch }) + } +} + +/// Public compile failure; every variant leaves no executable partial graph. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ProgramCompileError { + DuplicateColorInput { input: ColorInputId }, + DuplicateOpacityInput { input: OpacityInputId }, + DuplicateSurfaceInput { input: SurfaceInputId }, + DuplicatePaint { paint: PaintId }, + DuplicateSurface { surface: SurfaceId }, + DuplicateOccurrence { occurrence: OccurrenceId }, + MissingPaintColorInput { + paint: PaintId, + input: ColorInputId, + }, + MissingPaintSource { paint: PaintId, source: PaintId }, + MissingPaintOpacityInput { + paint: PaintId, + input: OpacityInputId, + }, + MissingSurfaceInput { + surface: SurfaceId, + input: SurfaceInputId, + }, + MissingSurfaceOccurrence { + surface: SurfaceId, + occurrence: OccurrenceId, + }, + MissingOccurrencePaint { + occurrence: OccurrenceId, + paint: PaintId, + }, + MissingOccurrenceBackdrop { + occurrence: OccurrenceId, + surface: SurfaceId, + }, + PaintCycle { paints: Vec }, + RenderCycle { + surfaces: Vec, + occurrences: Vec, + }, + OpacityOutOfDomain { input: OpacityInputId }, + EmptySurfaceSchema, + EmptyOccurrenceSet, + ResourceExhausted, + InternalInvariant, +} /// ASCII `LCR1`: code-owned Lab Colors Render transport version 1. /// @@ -36,181 +293,713 @@ const PACKED_SURFACE_UNAVAILABLE_WORDS_V1: usize = 5; struct ProgramEpochV1 { graph: CompiledAppearanceGraph, binding_template: AdmittedAppearanceBindings, - surface_ports: Box<[SurfaceInputPortId]>, - occurrence_count: usize, + surface_inputs: Box<[SurfaceInputId]>, + occurrence_ids: Box<[OccurrenceId]>, } -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) enum PointRenderEpochBuildErrorV1 { - Compile(CompileError), - Bindings(BindingError), - EmptySurfaceSchema, - EmptyOccurrenceSet, - ResourceExhausted, +/// Fully validated immutable point-render program, not yet attached to runtime. +/// +/// This value is deliberately not `Clone`: moving it into an owner establishes +/// one unambiguous strong-ownership root for its compiled epoch. +#[derive(Debug)] +pub struct CompiledProgram { + epoch: ProgramEpochV1, } -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) enum PointRenderAttachErrorV1 { +impl CompiledProgram { + /// Canonical Surface-input order required by [`SurfaceUpdate::Present`]. + pub fn surface_inputs(&self) -> &[SurfaceInputId] { + &self.epoch.surface_inputs + } + + /// Canonical occurrence order emitted by every [`Snapshot`]. + pub fn occurrences(&self) -> &[OccurrenceId] { + &self.epoch.occurrence_ids + } + + /// Transfer this compiled epoch to its sole runtime owner. + pub fn into_owner(self) -> PointRenderOwner { + PointRenderOwner::new(self) + } +} + +/// Failure while preparing an independent allocation-owning [`Session`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PointRenderAttachError { Disposed, - Bindings(BindingError), - Workspace(BindingError), ResourceExhausted, + InternalInvariant, } /// The only strong owner of the current non-reusable program epoch. /// -/// Replacement is prepare-then-swap: a failed build leaves the current `Rc` -/// untouched and therefore leaves all of its sessions live. No epoch number, -/// fingerprint or wrapping generation participates in this ownership proof. -#[derive(Debug, Default)] -pub(crate) struct PointRenderOwnerV1 { +/// Replacement accepts only an already complete [`CompiledProgram`]. Compile +/// failure therefore happens before the swap and cannot revoke the live epoch. +/// No numeric generation participates in this ownership proof. +#[derive(Debug)] +pub struct PointRenderOwner { current: Option>, } -impl PointRenderOwnerV1 { - pub(crate) fn new( - spec: AppearanceGraphSpec, - bindings: AppearanceBindings, - ) -> Result { - Ok(Self { - current: Some(Rc::new(prepare_epoch(spec, bindings)?)), - }) +impl PointRenderOwner { + /// Establish the sole strong owner of one compiled epoch. + pub fn new(compiled: CompiledProgram) -> Self { + Self { + current: Some(Rc::new(compiled.epoch)), + } } - /// Compile/admit the complete replacement before revoking the old epoch. - pub(crate) fn replace( - &mut self, - spec: AppearanceGraphSpec, - bindings: AppearanceBindings, - ) -> Result<(), PointRenderEpochBuildErrorV1> { - let replacement = Rc::new(prepare_epoch(spec, bindings)?); - self.current = Some(replacement); - Ok(()) + /// Atomically replace the current epoch and revoke all attached old sessions. + pub fn replace(&mut self, compiled: CompiledProgram) { + self.current = Some(Rc::new(compiled.epoch)); } /// Revoke the current epoch. Existing sessions fail on their next call. - pub(crate) fn dispose(&mut self) { + pub fn dispose(&mut self) { self.current = None; } - pub(crate) fn attach(&self) -> Result { + /// Return the current canonical Surface-input order, or `None` if disposed. + pub fn surface_inputs(&self) -> Option<&[SurfaceInputId]> { + self.current + .as_deref() + .map(|epoch| epoch.surface_inputs.as_ref()) + } + + /// Return the current canonical occurrence order, or `None` if disposed. + pub fn occurrences(&self) -> Option<&[OccurrenceId]> { + self.current + .as_deref() + .map(|epoch| epoch.occurrence_ids.as_ref()) + } + + /// Allocate all independent mutable storage before a Session escapes. + pub fn attach(&self) -> Result { let epoch = self .current .as_ref() - .ok_or(PointRenderAttachErrorV1::Disposed)?; + .ok_or(PointRenderAttachError::Disposed)?; let workspace = epoch .graph .new_workspace() - .map_err(PointRenderAttachErrorV1::Workspace)?; + .map_err(map_attach_binding_error)?; let bindings = epoch .binding_template .try_clone_v1() - .map_err(PointRenderAttachErrorV1::Bindings)?; - let initial_signal_buffers = - CompositedSignalBuffersV1::try_new(epoch.surface_ports.len(), epoch.occurrence_count)?; - Ok(PointRenderSessionV1 { + .map_err(map_attach_binding_error)?; + let initial_signal_buffers = CompositedSignalBuffersV1::try_new( + &epoch.surface_inputs, + &epoch.occurrence_ids, + )?; + Ok(Session { epoch: Rc::downgrade(epoch), bindings, workspace, initial_signal_buffers: Some(initial_signal_buffers), - state: PointRenderSessionStateV1::Waiting { + state: SessionState::Waiting { current_unavailable: None, }, }) } } -fn try_zeroed_signal_words(len: usize) -> Result, PointRenderAttachErrorV1> { +fn map_attach_binding_error(error: BindingError) -> PointRenderAttachError { + match error { + BindingError::ResourceExhausted => PointRenderAttachError::ResourceExhausted, + _ => PointRenderAttachError::InternalInvariant, + } +} + +fn try_zeroed_signal_words(len: usize) -> Result, PointRenderAttachError> { let mut words = Vec::new(); words .try_reserve_exact(len) - .map_err(|_| PointRenderAttachErrorV1::ResourceExhausted)?; + .map_err(|_| PointRenderAttachError::ResourceExhausted)?; words.resize(len, 0); Ok(words) } -fn prepare_epoch( - spec: AppearanceGraphSpec, - bindings: AppearanceBindings, -) -> Result { - let graph = spec +fn prepare_program(program: Program) -> Result { + let graph = lower_graph(&program) .compile() - .map_err(PointRenderEpochBuildErrorV1::Compile)?; - let surface_ports: Box<[_]> = { - let inputs = graph.surface_input_ports(); - let mut values = Vec::new(); - values - .try_reserve_exact(inputs.len()) - .map_err(|_| PointRenderEpochBuildErrorV1::ResourceExhausted)?; - values.extend(inputs); - values.into_boxed_slice() - }; - if surface_ports.is_empty() { - return Err(PointRenderEpochBuildErrorV1::EmptySurfaceSchema); + .map_err(|error| map_compile_error(&program, error))?; + + if program.surface_inputs.is_empty() { + return Err(ProgramCompileError::EmptySurfaceSchema); } - let occurrence_count = graph.occurrence_ids().len(); - if occurrence_count == 0 { - return Err(PointRenderEpochBuildErrorV1::EmptyOccurrenceSet); + if program.occurrences.is_empty() { + return Err(ProgramCompileError::EmptyOccurrenceSet); } - let binding_template = graph - .admit_bindings(&bindings) - .map_err(PointRenderEpochBuildErrorV1::Bindings)?; + + let mut surface_inputs = Vec::new(); + surface_inputs + .try_reserve_exact(program.surface_inputs.len()) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + surface_inputs.extend_from_slice(&program.surface_inputs); + surface_inputs.sort_unstable(); + + let mut occurrence_ids = Vec::new(); + occurrence_ids + .try_reserve_exact(program.occurrences.len()) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + occurrence_ids.extend(program.occurrences.iter().map(|occurrence| occurrence.id)); + occurrence_ids.sort_unstable(); + + debug_assert_eq!(graph.surface_input_ports().len(), surface_inputs.len()); + debug_assert_eq!(graph.occurrence_ids().len(), occurrence_ids.len()); + + let bindings = lower_bindings(&program); + let binding_template = graph.admit_bindings(&bindings).map_err(|error| { + map_binding_compile_error(&program, error) + })?; Ok(ProgramEpochV1 { graph, binding_template, - surface_ports, - occurrence_count, + surface_inputs: surface_inputs.into_boxed_slice(), + occurrence_ids: occurrence_ids.into_boxed_slice(), }) } +fn lower_graph(program: &Program) -> AppearanceGraphSpec { + AppearanceGraphSpec::new( + program + .colors + .iter() + .map(|input| AppearanceColorInputId::new(input.id.value())) + .collect(), + program + .surface_inputs + .iter() + .map(|input| AppearanceSurfaceInputId::new(input.value())) + .collect(), + program + .opacities + .iter() + .map(|input| AppearanceOpacityInputId::new(input.id.value())) + .collect(), + program + .paints + .iter() + .map(|paint| match *paint { + Paint::Solid { id, color } => AppearancePaintSpec::Solid { + id: AppearancePaintId::new(id.value()), + color: AppearanceColorInputId::new(color.value()), + }, + Paint::Opacity { + id, + source, + opacity, + } => AppearancePaintSpec::Opacity { + id: AppearancePaintId::new(id.value()), + source: AppearancePaintId::new(source.value()), + opacity: AppearanceOpacityInputId::new(opacity.value()), + }, + }) + .collect(), + program + .surfaces + .iter() + .map(|surface| match *surface { + Surface::Input { id, input } => AppearanceSurfaceSpec::Input { + id: AppearanceSurfaceId::new(id.value()), + port: AppearanceSurfaceInputId::new(input.value()), + }, + Surface::FromOccurrence { id, occurrence } => { + AppearanceSurfaceSpec::FromOccurrence { + id: AppearanceSurfaceId::new(id.value()), + occurrence: AppearanceOccurrenceId::new(occurrence.value()), + } + } + }) + .collect(), + program + .occurrences + .iter() + .map(|occurrence| AppearanceOccurrenceSpec { + id: AppearanceOccurrenceId::new(occurrence.id.value()), + subject: AppearancePaintId::new(occurrence.subject.value()), + against: AppearanceSurfaceId::new(occurrence.against.value()), + profile: match occurrence.composition { + CompositionProfile::EncodedSrgb8SourceOverV1 => { + CompositionProfileV1::EncodedSrgb8SourceOverV1 + } + }, + }) + .collect(), + ) +} + +fn lower_bindings(program: &Program) -> AppearanceBindings { + AppearanceBindings::new( + program + .colors + .iter() + .map(|input| { + ( + AppearanceColorInputId::new(input.id.value()), + input.value, + ) + }) + .collect(), + program + .surface_inputs + .iter() + .map(|input| { + ( + AppearanceSurfaceInputId::new(input.value()), + Srgb8::new([0; 3]), + ) + }) + .collect(), + program + .opacities + .iter() + .map(|input| { + ( + AppearanceOpacityInputId::new(input.id.value()), + input.value, + ) + }) + .collect(), + ) +} + +fn public_color_id(program: &Program, value: AppearanceColorInputId) -> Option { + for input in &program.colors { + if AppearanceColorInputId::new(input.id.value()) == value { + return Some(input.id); + } + } + for paint in &program.paints { + if let Paint::Solid { color, .. } = *paint { + if AppearanceColorInputId::new(color.value()) == value { + return Some(color); + } + } + } + None +} + +fn public_opacity_id( + program: &Program, + value: AppearanceOpacityInputId, +) -> Option { + for input in &program.opacities { + if AppearanceOpacityInputId::new(input.id.value()) == value { + return Some(input.id); + } + } + for paint in &program.paints { + if let Paint::Opacity { opacity, .. } = *paint { + if AppearanceOpacityInputId::new(opacity.value()) == value { + return Some(opacity); + } + } + } + None +} + +fn public_surface_input_id( + program: &Program, + value: AppearanceSurfaceInputId, +) -> Option { + for input in &program.surface_inputs { + if AppearanceSurfaceInputId::new(input.value()) == value { + return Some(*input); + } + } + for surface in &program.surfaces { + if let Surface::Input { input, .. } = *surface { + if AppearanceSurfaceInputId::new(input.value()) == value { + return Some(input); + } + } + } + None +} + +fn public_paint_id(program: &Program, value: AppearancePaintId) -> Option { + for paint in &program.paints { + match *paint { + Paint::Solid { id, .. } => { + if AppearancePaintId::new(id.value()) == value { + return Some(id); + } + } + Paint::Opacity { id, source, .. } => { + for candidate in [id, source] { + if AppearancePaintId::new(candidate.value()) == value { + return Some(candidate); + } + } + } + } + } + for occurrence in &program.occurrences { + if AppearancePaintId::new(occurrence.subject.value()) == value { + return Some(occurrence.subject); + } + } + None +} + +fn public_surface_id(program: &Program, value: AppearanceSurfaceId) -> Option { + for surface in &program.surfaces { + let id = match *surface { + Surface::Input { id, .. } | Surface::FromOccurrence { id, .. } => id, + }; + if AppearanceSurfaceId::new(id.value()) == value { + return Some(id); + } + } + for occurrence in &program.occurrences { + if AppearanceSurfaceId::new(occurrence.against.value()) == value { + return Some(occurrence.against); + } + } + None +} + +fn public_occurrence_id( + program: &Program, + value: AppearanceOccurrenceId, +) -> Option { + for occurrence in &program.occurrences { + if AppearanceOccurrenceId::new(occurrence.id.value()) == value { + return Some(occurrence.id); + } + } + for surface in &program.surfaces { + if let Surface::FromOccurrence { occurrence, .. } = *surface { + if AppearanceOccurrenceId::new(occurrence.value()) == value { + return Some(occurrence); + } + } + } + None +} + +fn map_compile_error(program: &Program, error: CompileError) -> ProgramCompileError { + match error { + CompileError::DuplicateColorInput { input } => public_color_id(program, input) + .map_or(ProgramCompileError::InternalInvariant, |input| { + ProgramCompileError::DuplicateColorInput { input } + }), + CompileError::DuplicateOpacityInput { input } => public_opacity_id(program, input) + .map_or(ProgramCompileError::InternalInvariant, |input| { + ProgramCompileError::DuplicateOpacityInput { input } + }), + CompileError::DuplicateSurfaceInputPort { input } => { + public_surface_input_id(program, input).map_or( + ProgramCompileError::InternalInvariant, + |input| ProgramCompileError::DuplicateSurfaceInput { input }, + ) + } + CompileError::DuplicatePaint { paint } => public_paint_id(program, paint) + .map_or(ProgramCompileError::InternalInvariant, |paint| { + ProgramCompileError::DuplicatePaint { paint } + }), + CompileError::DuplicateSurface { surface } => public_surface_id(program, surface) + .map_or(ProgramCompileError::InternalInvariant, |surface| { + ProgramCompileError::DuplicateSurface { surface } + }), + CompileError::DuplicateOccurrence { occurrence } => { + public_occurrence_id(program, occurrence).map_or( + ProgramCompileError::InternalInvariant, + |occurrence| ProgramCompileError::DuplicateOccurrence { occurrence }, + ) + } + CompileError::MissingPaintColorInput { paint, input } => { + match ( + public_paint_id(program, paint), + public_color_id(program, input), + ) { + (Some(paint), Some(input)) => { + ProgramCompileError::MissingPaintColorInput { paint, input } + } + _ => ProgramCompileError::InternalInvariant, + } + } + CompileError::MissingPaintSource { paint, source } => { + match ( + public_paint_id(program, paint), + public_paint_id(program, source), + ) { + (Some(paint), Some(source)) => { + ProgramCompileError::MissingPaintSource { paint, source } + } + _ => ProgramCompileError::InternalInvariant, + } + } + CompileError::MissingPaintOpacityInput { paint, input } => { + match ( + public_paint_id(program, paint), + public_opacity_id(program, input), + ) { + (Some(paint), Some(input)) => { + ProgramCompileError::MissingPaintOpacityInput { paint, input } + } + _ => ProgramCompileError::InternalInvariant, + } + } + CompileError::MissingSurfaceInputPort { surface, input } => { + match ( + public_surface_id(program, surface), + public_surface_input_id(program, input), + ) { + (Some(surface), Some(input)) => { + ProgramCompileError::MissingSurfaceInput { surface, input } + } + _ => ProgramCompileError::InternalInvariant, + } + } + CompileError::MissingSurfaceOccurrence { + surface, + occurrence, + } => match ( + public_surface_id(program, surface), + public_occurrence_id(program, occurrence), + ) { + (Some(surface), Some(occurrence)) => { + ProgramCompileError::MissingSurfaceOccurrence { + surface, + occurrence, + } + } + _ => ProgramCompileError::InternalInvariant, + }, + CompileError::MissingOccurrencePaint { occurrence, paint } => { + match ( + public_occurrence_id(program, occurrence), + public_paint_id(program, paint), + ) { + (Some(occurrence), Some(paint)) => { + ProgramCompileError::MissingOccurrencePaint { occurrence, paint } + } + _ => ProgramCompileError::InternalInvariant, + } + } + CompileError::MissingOccurrenceBackdrop { + occurrence, + surface, + } => match ( + public_occurrence_id(program, occurrence), + public_surface_id(program, surface), + ) { + (Some(occurrence), Some(surface)) => { + ProgramCompileError::MissingOccurrenceBackdrop { + occurrence, + surface, + } + } + _ => ProgramCompileError::InternalInvariant, + }, + CompileError::PaintCycle { paints } => paints + .into_iter() + .map(|paint| public_paint_id(program, paint)) + .collect::>>() + .map_or(ProgramCompileError::InternalInvariant, |paints| { + ProgramCompileError::PaintCycle { paints } + }), + CompileError::RenderCycle { + surfaces, + occurrences, + } => { + let surfaces = surfaces + .into_iter() + .map(|surface| public_surface_id(program, surface)) + .collect::>>(); + let occurrences = occurrences + .into_iter() + .map(|occurrence| public_occurrence_id(program, occurrence)) + .collect::>>(); + match (surfaces, occurrences) { + (Some(surfaces), Some(occurrences)) => ProgramCompileError::RenderCycle { + surfaces, + occurrences, + }, + _ => ProgramCompileError::InternalInvariant, + } + } + } +} + +fn map_binding_compile_error(program: &Program, error: BindingError) -> ProgramCompileError { + match error { + BindingError::OpacityOutOfDomain { input, .. } => public_opacity_id(program, input) + .map_or(ProgramCompileError::InternalInvariant, |input| { + ProgramCompileError::OpacityOutOfDomain { input } + }), + BindingError::ResourceExhausted => ProgramCompileError::ResourceExhausted, + _ => ProgramCompileError::InternalInvariant, + } +} + +/// One revision-bound absence of the correlated Surface-input set. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) struct RevisionBoundSurfaceUnavailableV1 { +pub struct SurfaceUnavailable { revision: u64, reason: u32, } -impl RevisionBoundSurfaceUnavailableV1 { - pub(crate) const fn revision(self) -> u64 { +impl SurfaceUnavailable { + /// Return the stream revision carrying this absence. + pub const fn revision(self) -> u64 { self.revision } - pub(crate) const fn reason(self) -> u32 { + /// Return the client-owned opaque absence reason. + pub const fn reason(self) -> u32 { self.reason } } +/// One exact runtime Surface-input value. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SurfaceSignal { + input: SurfaceInputId, + value: Srgb8, +} + +impl SurfaceSignal { + /// Bind one runtime input identity to exact encoded bytes. + pub const fn new(input: SurfaceInputId, value: Srgb8) -> Self { + Self { input, value } + } + + /// Return the runtime input identity. + pub const fn input(self) -> SurfaceInputId { + self.input + } + + /// Return the exact encoded value. + pub const fn value(self) -> Srgb8 { + self.value + } +} + +/// One exact visible value emitted for a compiled occurrence. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct OccurrenceSignal { + occurrence: OccurrenceId, + value: Srgb8, +} + +impl OccurrenceSignal { + /// Return the compiled occurrence identity. + pub const fn occurrence(self) -> OccurrenceId { + self.occurrence + } + + /// Return the exact encoded visible value. + pub const fn value(self) -> Srgb8 { + self.value + } +} + +/// Borrowed, correlated runtime update for one attached Session. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SurfaceUpdate<'input> { + /// The entire Surface-input set is unavailable at this revision. + Unavailable { revision: u64, reason: u32 }, + /// The complete input set in [`CompiledProgram::surface_inputs`] order. + Present { + revision: u64, + surfaces: &'input [SurfaceSignal], + }, +} + /// Compact committed value: one word per compiled occurrence, in the graph's /// canonical occurrence order. No metric, threshold or JS-derived verdict is /// present on this boundary. #[derive(Debug, PartialEq, Eq)] struct CompositedSignalBuffersV1 { + surface_inputs: Box<[SurfaceInputId]>, input_surface_signals_rgb24: Vec, + occurrence_ids: Box<[OccurrenceId]>, composited_occurrence_signals_rgb24: Vec, } impl CompositedSignalBuffersV1 { fn try_new( - surface_count: usize, - occurrence_count: usize, - ) -> Result { + surface_inputs: &[SurfaceInputId], + occurrence_ids: &[OccurrenceId], + ) -> Result { Ok(Self { - input_surface_signals_rgb24: try_zeroed_signal_words(surface_count)?, - composited_occurrence_signals_rgb24: try_zeroed_signal_words(occurrence_count)?, + surface_inputs: try_copy_ids(surface_inputs)?, + input_surface_signals_rgb24: try_zeroed_signal_words(surface_inputs.len())?, + occurrence_ids: try_copy_ids(occurrence_ids)?, + composited_occurrence_signals_rgb24: try_zeroed_signal_words(occurrence_ids.len())?, }) } } +fn try_copy_ids(values: &[T]) -> Result, PointRenderAttachError> { + let mut copied = Vec::new(); + copied + .try_reserve_exact(values.len()) + .map_err(|_| PointRenderAttachError::ResourceExhausted)?; + copied.extend_from_slice(values); + Ok(copied.into_boxed_slice()) +} + +/// One committed, revision-bound point-render result. #[derive(Debug, PartialEq, Eq)] -pub(crate) struct CompositedSignalSnapshotV1 { +pub struct Snapshot { revision: u64, buffers: CompositedSignalBuffersV1, } -impl CompositedSignalSnapshotV1 { - pub(crate) const fn revision(&self) -> u64 { +impl Snapshot { + /// Return the exact revision used for every input and output in this value. + pub const fn revision(&self) -> u64 { self.revision } + /// Iterate admitted inputs in canonical compiled order without allocation. + pub fn surfaces(&self) -> impl ExactSizeIterator + '_ { + self.buffers + .surface_inputs + .iter() + .copied() + .zip(self.buffers.input_surface_signals_rgb24.iter().copied()) + .map(|(input, value)| SurfaceSignal { + input, + value: Srgb8::new(unpack_rgb24(value)), + }) + } + + /// Iterate visible occurrence values in canonical compiled order. + pub fn occurrences(&self) -> impl ExactSizeIterator + '_ { + self.buffers + .occurrence_ids + .iter() + .copied() + .zip( + self.buffers + .composited_occurrence_signals_rgb24 + .iter() + .copied(), + ) + .map(|(occurrence, value)| OccurrenceSignal { + occurrence, + value: Srgb8::new(unpack_rgb24(value)), + }) + } + + /// Look up one canonical occurrence output without allocation. + pub fn occurrence(&self, occurrence: OccurrenceId) -> Option { + self.buffers + .occurrence_ids + .binary_search(&occurrence) + .ok() + .map(|index| { + Srgb8::new(unpack_rgb24( + self.buffers.composited_occurrence_signals_rgb24[index], + )) + }) + } + pub(crate) fn input_surface_signals_rgb24(&self) -> &[u32] { &self.buffers.input_surface_signals_rgb24 } @@ -220,21 +1009,22 @@ impl CompositedSignalSnapshotV1 { } } +/// Current state of one generation-bound Session. #[derive(Debug, PartialEq, Eq)] -pub(crate) enum PointRenderSessionStateV1 { +pub enum SessionState { Waiting { - current_unavailable: Option, + current_unavailable: Option, }, Ready { - current: CompositedSignalSnapshotV1, + current: Snapshot, }, Stale { - previous: CompositedSignalSnapshotV1, - current_unavailable: RevisionBoundSurfaceUnavailableV1, + previous: Snapshot, + current_unavailable: SurfaceUnavailable, }, } -impl PointRenderSessionStateV1 { +impl SessionState { const fn head_revision(&self) -> Option { match self { Self::Waiting { @@ -271,43 +1061,143 @@ pub(crate) enum PointRenderSessionUpdateErrorV1 { Evaluation(BindingError), } +/// Failure to admit or execute one typed Session update. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SessionUpdateError { + ProgramExpired, + SurfaceInputLengthMismatch { expected: usize, actual: usize }, + SurfaceInputMismatch { + index: usize, + expected: SurfaceInputId, + actual: SurfaceInputId, + }, + RevisionOutOfOrder { current: u64, incoming: u64 }, + RevisionConflict { revision: u64 }, + InternalInvariant, +} + +enum PreparedSurfaceValuesV1<'input> { + Typed(&'input [SurfaceSignal]), + PackedRgb24(&'input [u32]), +} + +impl PreparedSurfaceValuesV1<'_> { + fn len(&self) -> usize { + match self { + Self::Typed(values) => values.len(), + Self::PackedRgb24(values) => values.len(), + } + } + + fn value(&self, index: usize) -> Srgb8 { + match self { + Self::Typed(values) => values[index].value, + Self::PackedRgb24(values) => Srgb8::new(unpack_rgb24(values[index])), + } + } + + fn matches_rgb24(&self, expected: &[u32]) -> bool { + self.len() == expected.len() + && expected + .iter() + .enumerate() + .all(|(index, &word)| pack_rgb24(self.value(index).bytes()) == word) + } +} + enum PreparedEncodedSurfaceUpdateV1<'input> { - Unavailable(RevisionBoundSurfaceUnavailableV1), + Unavailable(SurfaceUnavailable), Present { revision: u64, - surfaces_rgb24: &'input [u32], + surfaces: PreparedSurfaceValuesV1<'input>, }, } /// Generation-bound mutable runtime. It owns reusable values/scratch, never a /// strong reference or a copy of the compiled graph. All fixed-cardinality -/// signal buffers are allocated fallibly by `attach`; `update_packed` only -/// moves and overwrites their ownership after evaluation succeeds. +/// signal buffers are allocated fallibly by `attach`; updates only move and +/// overwrite their ownership after evaluation succeeds. #[derive(Debug)] -pub(crate) struct PointRenderSessionV1 { +pub struct Session { epoch: Weak, bindings: AdmittedAppearanceBindings, workspace: AppearanceWorkspace, initial_signal_buffers: Option, - state: PointRenderSessionStateV1, + state: SessionState, } -impl PointRenderSessionV1 { - pub(crate) const fn state(&self) -> &PointRenderSessionStateV1 { +impl Session { + /// Borrow the current committed state. + pub const fn state(&self) -> &SessionState { &self.state } - /// Admit, evaluate and commit one encoded Surface-input update transaction. + /// Admit, evaluate and atomically commit one typed Surface-input update. + pub fn update( + &mut self, + update: SurfaceUpdate<'_>, + ) -> Result<&SessionState, SessionUpdateError> { + let epoch = self + .epoch + .upgrade() + .ok_or(SessionUpdateError::ProgramExpired)?; + let prepared = match update { + SurfaceUpdate::Unavailable { revision, reason } => { + PreparedEncodedSurfaceUpdateV1::Unavailable(SurfaceUnavailable { + revision, + reason, + }) + } + SurfaceUpdate::Present { revision, surfaces } => { + if surfaces.len() != epoch.surface_inputs.len() { + return Err(SessionUpdateError::SurfaceInputLengthMismatch { + expected: epoch.surface_inputs.len(), + actual: surfaces.len(), + }); + } + for (index, (&expected, actual)) in epoch + .surface_inputs + .iter() + .zip(surfaces.iter()) + .enumerate() + { + if actual.input != expected { + return Err(SessionUpdateError::SurfaceInputMismatch { + index, + expected, + actual: actual.input, + }); + } + } + PreparedEncodedSurfaceUpdateV1::Present { + revision, + surfaces: PreparedSurfaceValuesV1::Typed(surfaces), + } + } + }; + self.apply_prepared(&epoch, prepared) + .map_err(map_session_update_error) + } + + /// Private allocation-free packed bridge for the WASM boundary. pub(crate) fn update_packed( &mut self, words: &[u32], - ) -> Result<&PointRenderSessionStateV1, PointRenderSessionUpdateErrorV1> { + ) -> Result<&SessionState, PointRenderSessionUpdateErrorV1> { let epoch = self .epoch .upgrade() .ok_or(PointRenderSessionUpdateErrorV1::ProgramExpired)?; - let prepared = decode_encoded_surface_update(words, epoch.surface_ports.len()) + let prepared = decode_encoded_surface_update(words, epoch.surface_inputs.len()) .map_err(PointRenderSessionUpdateErrorV1::EncodedSurfaceUpdate)?; + self.apply_prepared(&epoch, prepared) + } + + fn apply_prepared<'input>( + &mut self, + epoch: &ProgramEpochV1, + prepared: PreparedEncodedSurfaceUpdateV1<'input>, + ) -> Result<&SessionState, PointRenderSessionUpdateErrorV1> { let incoming_revision = match &prepared { PreparedEncodedSurfaceUpdateV1::Unavailable(unavailable) => unavailable.revision, PreparedEncodedSurfaceUpdateV1::Present { revision, .. } => *revision, @@ -331,38 +1221,38 @@ impl PointRenderSessionV1 { PreparedEncodedSurfaceUpdateV1::Unavailable(unavailable) => { let previous = take_last_ready(&mut self.state); self.state = match previous { - Some(previous) => PointRenderSessionStateV1::Stale { + Some(previous) => SessionState::Stale { previous, current_unavailable: unavailable, }, - None => PointRenderSessionStateV1::Waiting { + None => SessionState::Waiting { current_unavailable: Some(unavailable), }, }; } PreparedEncodedSurfaceUpdateV1::Present { revision, - surfaces_rgb24, + surfaces, } => { let retained_shape_matches = match &self.state { - PointRenderSessionStateV1::Waiting { .. } => { + SessionState::Waiting { .. } => { self.initial_signal_buffers.as_ref().is_some_and(|buffers| { - buffers.input_surface_signals_rgb24.len() == epoch.surface_ports.len() + buffers.input_surface_signals_rgb24.len() == epoch.surface_inputs.len() && buffers.composited_occurrence_signals_rgb24.len() - == epoch.occurrence_count + == epoch.occurrence_ids.len() }) } - PointRenderSessionStateV1::Ready { current } => { + SessionState::Ready { current } => { current.buffers.input_surface_signals_rgb24.len() - == epoch.surface_ports.len() + == epoch.surface_inputs.len() && current.buffers.composited_occurrence_signals_rgb24.len() - == epoch.occurrence_count + == epoch.occurrence_ids.len() } - PointRenderSessionStateV1::Stale { previous, .. } => { + SessionState::Stale { previous, .. } => { previous.buffers.input_surface_signals_rgb24.len() - == epoch.surface_ports.len() + == epoch.surface_inputs.len() && previous.buffers.composited_occurrence_signals_rgb24.len() - == epoch.occurrence_count + == epoch.occurrence_ids.len() } }; if !retained_shape_matches { @@ -376,16 +1266,19 @@ impl PointRenderSessionV1 { // the cloned admitted schema; setters cannot partially reject // a later element. These mutable values are scratch only and // are not published until the final state replacement below. - for (&port, &rgb24) in epoch.surface_ports.iter().zip(surfaces_rgb24.iter()) { + for (index, &port) in epoch.surface_inputs.iter().enumerate() { self.bindings - .set_surface_input(port, Srgb8::new(unpack_rgb24(rgb24))) + .set_surface_input( + AppearanceSurfaceInputId::new(port.value()), + surfaces.value(index), + ) .map_err(PointRenderSessionUpdateErrorV1::Evaluation)?; } let evaluation = epoch .graph .evaluate_admitted_into(&self.bindings, &mut self.workspace) .map_err(PointRenderSessionUpdateErrorV1::Evaluation)?; - if evaluation.occurrences().len() != epoch.occurrence_count { + if evaluation.occurrences().len() != epoch.occurrence_ids.len() { return Err(PointRenderSessionUpdateErrorV1::Evaluation( BindingError::IncompatibleWorkspace, )); @@ -402,15 +1295,19 @@ impl PointRenderSessionV1 { }; debug_assert_eq!( buffers.input_surface_signals_rgb24.len(), - surfaces_rgb24.len() + surfaces.len() ); debug_assert_eq!( buffers.composited_occurrence_signals_rgb24.len(), - epoch.occurrence_count + epoch.occurrence_ids.len() ); - buffers + for (index, output) in buffers .input_surface_signals_rgb24 - .copy_from_slice(surfaces_rgb24); + .iter_mut() + .enumerate() + { + *output = pack_rgb24(surfaces.value(index).bytes()); + } for (resolved, output) in evaluation .occurrences() .zip(buffers.composited_occurrence_signals_rgb24.iter_mut()) @@ -420,8 +1317,8 @@ impl PointRenderSessionV1 { // reclamation above (or introduce its own staging value). *output = pack_rgb24(resolved.visible()); } - self.state = PointRenderSessionStateV1::Ready { - current: CompositedSignalSnapshotV1 { revision, buffers }, + self.state = SessionState::Ready { + current: Snapshot { revision, buffers }, }; } } @@ -431,14 +1328,14 @@ impl PointRenderSessionV1 { fn admit_same_revision( &self, prepared: PreparedEncodedSurfaceUpdateV1<'_>, - ) -> Result<&PointRenderSessionStateV1, PointRenderSessionUpdateErrorV1> { + ) -> Result<&SessionState, PointRenderSessionUpdateErrorV1> { let exact = match (prepared, &self.state) { ( PreparedEncodedSurfaceUpdateV1::Unavailable(incoming), - PointRenderSessionStateV1::Waiting { + SessionState::Waiting { current_unavailable: Some(current), } - | PointRenderSessionStateV1::Stale { + | SessionState::Stale { current_unavailable: current, .. }, @@ -446,12 +1343,12 @@ impl PointRenderSessionV1 { ( PreparedEncodedSurfaceUpdateV1::Present { revision, - surfaces_rgb24, + surfaces, }, - PointRenderSessionStateV1::Ready { current }, + SessionState::Ready { current }, ) => { revision == current.revision - && surfaces_rgb24 == current.buffers.input_surface_signals_rgb24.as_slice() + && surfaces.matches_rgb24(¤t.buffers.input_surface_signals_rgb24) } _ => false, }; @@ -470,6 +1367,20 @@ impl PointRenderSessionV1 { } } +fn map_session_update_error(error: PointRenderSessionUpdateErrorV1) -> SessionUpdateError { + match error { + PointRenderSessionUpdateErrorV1::ProgramExpired => SessionUpdateError::ProgramExpired, + PointRenderSessionUpdateErrorV1::EncodedSurfaceUpdate( + PackedEncodedSurfaceUpdateErrorV1::RevisionOutOfOrder { current, incoming }, + ) => SessionUpdateError::RevisionOutOfOrder { current, incoming }, + PointRenderSessionUpdateErrorV1::EncodedSurfaceUpdate( + PackedEncodedSurfaceUpdateErrorV1::RevisionConflict { revision }, + ) => SessionUpdateError::RevisionConflict { revision }, + PointRenderSessionUpdateErrorV1::EncodedSurfaceUpdate(_) + | PointRenderSessionUpdateErrorV1::Evaluation(_) => SessionUpdateError::InternalInvariant, + } +} + fn decode_encoded_surface_update( words: &[u32], surface_count: usize, @@ -490,7 +1401,7 @@ fn decode_encoded_surface_update( }); } Ok(PreparedEncodedSurfaceUpdateV1::Unavailable( - RevisionBoundSurfaceUnavailableV1 { + SurfaceUnavailable { revision, reason: words[4], }, @@ -519,23 +1430,23 @@ fn decode_encoded_surface_update( } Ok(PreparedEncodedSurfaceUpdateV1::Present { revision, - surfaces_rgb24: surfaces, + surfaces: PreparedSurfaceValuesV1::PackedRgb24(surfaces), }) } actual => Err(PackedEncodedSurfaceUpdateErrorV1::UnsupportedTag { actual }), } } -fn take_last_ready(state: &mut PointRenderSessionStateV1) -> Option { +fn take_last_ready(state: &mut SessionState) -> Option { match mem::replace( state, - PointRenderSessionStateV1::Waiting { + SessionState::Waiting { current_unavailable: None, }, ) { - PointRenderSessionStateV1::Waiting { .. } => None, - PointRenderSessionStateV1::Ready { current } => Some(current), - PointRenderSessionStateV1::Stale { previous, .. } => Some(previous), + SessionState::Waiting { .. } => None, + SessionState::Ready { current } => Some(current), + SessionState::Stale { previous, .. } => Some(previous), } } diff --git a/crates/labcolors-core/src/program_session_tests.rs b/crates/labcolors-core/src/program_session_tests.rs index 663c1517..77a778b6 100644 --- a/crates/labcolors-core/src/program_session_tests.rs +++ b/crates/labcolors-core/src/program_session_tests.rs @@ -1,19 +1,15 @@ use crate::Srgb8; -use crate::appearance::{ - AppearanceBindings, AppearanceGraphSpec, BindingError, ColorInputId, CompileError, - OccurrenceId, OccurrenceSpec, OpacityInputId, PaintId, PaintSpec, SurfaceId, - SurfaceInputPortId, SurfaceSpec, -}; -use crate::composition::CompositionProfileV1; use crate::program_session::{ + ColorInput, ColorInputId, CompiledProgram, CompositionProfile, Occurrence, OccurrenceId, + OpacityInput, OpacityInputId, Paint, PaintId, PointRenderOwner, Program, ProgramCompileError, PACKED_ENCODED_SURFACE_PRESENT_TAG_V1, PACKED_ENCODED_SURFACE_UNAVAILABLE_TAG_V1, PACKED_ENCODED_SURFACE_UPDATE_MAGIC_V1, PackedEncodedSurfaceUpdateErrorV1, - PointRenderEpochBuildErrorV1, PointRenderOwnerV1, PointRenderSessionStateV1, - PointRenderSessionUpdateErrorV1, + PointRenderSessionUpdateErrorV1, SessionState, SessionUpdateError, Surface, SurfaceId, + SurfaceInputId, SurfaceSignal, SurfaceUpdate, }; const COLOR: ColorInputId = ColorInputId::new(1); -const SURFACE_PORT: SurfaceInputPortId = SurfaceInputPortId::new(2); +const SURFACE_PORT: SurfaceInputId = SurfaceInputId::new(2); const OPACITY: OpacityInputId = OpacityInputId::new(3); const SOLID: PaintId = PaintId::new(10); const TRANSLUCENT: PaintId = PaintId::new(11); @@ -21,80 +17,76 @@ const BACKDROP: SurfaceId = SurfaceId::new(20); const VISIBLE: SurfaceId = SurfaceId::new(21); const OCCURRENCE: OccurrenceId = OccurrenceId::new(30); -fn graph_spec() -> AppearanceGraphSpec { - graph_spec_against(BACKDROP) +fn program(opacity: f64) -> Program { + program_against(BACKDROP, opacity) } -fn graph_spec_against(against: SurfaceId) -> AppearanceGraphSpec { - AppearanceGraphSpec::new( - vec![COLOR], +fn program_against(against: SurfaceId, opacity: f64) -> Program { + Program::new( + vec![ColorInput::new(COLOR, Srgb8::new([0; 3]))], vec![SURFACE_PORT], - vec![OPACITY], + vec![OpacityInput::new(OPACITY, opacity)], vec![ - PaintSpec::Solid { + Paint::Solid { id: SOLID, color: COLOR, }, - PaintSpec::Opacity { + Paint::Opacity { id: TRANSLUCENT, source: SOLID, opacity: OPACITY, }, ], vec![ - SurfaceSpec::Input { + Surface::Input { id: BACKDROP, - port: SURFACE_PORT, + input: SURFACE_PORT, }, - SurfaceSpec::FromOccurrence { + Surface::FromOccurrence { id: VISIBLE, occurrence: OCCURRENCE, }, ], - vec![OccurrenceSpec { - id: OCCURRENCE, - subject: TRANSLUCENT, + vec![Occurrence::new( + OCCURRENCE, + TRANSLUCENT, against, - profile: CompositionProfileV1::EncodedSrgb8SourceOverV1, - }], + CompositionProfile::EncodedSrgb8SourceOverV1, + )], ) } -fn cyclic_graph_spec() -> AppearanceGraphSpec { - AppearanceGraphSpec::new( - vec![COLOR], +fn cyclic_program() -> Program { + Program::new( + vec![ColorInput::new(COLOR, Srgb8::new([0; 3]))], vec![SURFACE_PORT], - vec![OPACITY], + vec![OpacityInput::new(OPACITY, 0.5)], vec![ - PaintSpec::Solid { + Paint::Solid { id: SOLID, color: COLOR, }, - PaintSpec::Opacity { + Paint::Opacity { id: TRANSLUCENT, source: SOLID, opacity: OPACITY, }, ], - vec![SurfaceSpec::FromOccurrence { + vec![Surface::FromOccurrence { id: VISIBLE, occurrence: OCCURRENCE, }], - vec![OccurrenceSpec { - id: OCCURRENCE, - subject: TRANSLUCENT, - against: VISIBLE, - profile: CompositionProfileV1::EncodedSrgb8SourceOverV1, - }], + vec![Occurrence::new( + OCCURRENCE, + TRANSLUCENT, + VISIBLE, + CompositionProfile::EncodedSrgb8SourceOverV1, + )], ) } -fn bindings(opacity: f64) -> AppearanceBindings { - AppearanceBindings::new( - vec![(COLOR, Srgb8::new([0; 3]))], - vec![(SURFACE_PORT, Srgb8::new([0; 3]))], - vec![(OPACITY, opacity)], - ) +fn compiled(opacity: f64) -> CompiledProgram { + program(opacity).compile().unwrap() } fn point(revision: u64, rgb24: u32) -> [u32; 5] { @@ -117,11 +109,11 @@ fn unavailable(revision: u64, reason: u32) -> [u32; 5] { ] } -fn retained_signal_storage_pointers(state: &PointRenderSessionStateV1) -> (*const u32, *const u32) { +fn retained_signal_storage_pointers(state: &SessionState) -> (*const u32, *const u32) { let snapshot = match state { - PointRenderSessionStateV1::Ready { current } => current, - PointRenderSessionStateV1::Stale { previous, .. } => previous, - PointRenderSessionStateV1::Waiting { .. } => { + SessionState::Ready { current } => current, + SessionState::Stale { previous, .. } => previous, + SessionState::Waiting { .. } => { panic!("the allocation test requires a retained successful snapshot") } }; @@ -133,10 +125,10 @@ fn retained_signal_storage_pointers(state: &PointRenderSessionStateV1) -> (*cons #[test] fn point_update_executes_the_compiled_graph_and_commits_compact_occurrences() { - let owner = PointRenderOwnerV1::new(graph_spec(), bindings(0.5)).unwrap(); + let owner = compiled(0.5).into_owner(); let mut session = owner.attach().unwrap(); - let PointRenderSessionStateV1::Ready { current } = + let SessionState::Ready { current } = session.update_packed(&point(1, 0xff_ff_ff)).unwrap() else { panic!("present encoded Surface signals must produce Ready"); @@ -148,10 +140,10 @@ fn point_update_executes_the_compiled_graph_and_commits_compact_occurrences() { #[test] fn successful_replace_revokes_old_sessions_without_a_numeric_generation() { - let mut owner = PointRenderOwnerV1::new(graph_spec(), bindings(0.5)).unwrap(); + let mut owner = compiled(0.5).into_owner(); let mut old = owner.attach().unwrap(); - owner.replace(graph_spec(), bindings(0.25)).unwrap(); + owner.replace(compiled(0.25)); assert_eq!( old.update_packed(&point(1, 0xff_ff_ff)), Err(PointRenderSessionUpdateErrorV1::ProgramExpired) @@ -160,22 +152,20 @@ fn successful_replace_revokes_old_sessions_without_a_numeric_generation() { let mut current = owner.attach().unwrap(); assert!(matches!( current.update_packed(&point(1, 0xff_ff_ff)).unwrap(), - PointRenderSessionStateV1::Ready { .. } + SessionState::Ready { .. } )); } #[test] fn invalid_opacity_failed_replace_is_atomic_and_keeps_old_epoch_live() { - let mut owner = PointRenderOwnerV1::new(graph_spec(), bindings(0.5)).unwrap(); + let owner = compiled(0.5).into_owner(); let mut session = owner.attach().unwrap(); - assert!(matches!( - owner.replace(graph_spec(), bindings(1.25)), - Err(PointRenderEpochBuildErrorV1::Bindings( - BindingError::OpacityOutOfDomain { input: OPACITY, .. } - )) - )); - let PointRenderSessionStateV1::Ready { current } = + assert_eq!( + program(1.25).compile().unwrap_err(), + ProgramCompileError::OpacityOutOfDomain { input: OPACITY } + ); + let SessionState::Ready { current } = session.update_packed(&point(1, 0xff_ff_ff)).unwrap() else { panic!("failed replacement must not revoke the old epoch"); @@ -185,27 +175,23 @@ fn invalid_opacity_failed_replace_is_atomic_and_keeps_old_epoch_live() { #[test] fn dangling_and_cyclic_failed_compiles_do_not_revoke_the_current_epoch() { - let mut owner = PointRenderOwnerV1::new(graph_spec(), bindings(0.5)).unwrap(); + let owner = compiled(0.5).into_owner(); let mut session = owner.attach().unwrap(); let missing = SurfaceId::new(999); + assert_eq!( + program_against(missing, 0.5).compile().unwrap_err(), + ProgramCompileError::MissingOccurrenceBackdrop { + occurrence: OCCURRENCE, + surface: missing, + } + ); assert!(matches!( - owner.replace(graph_spec_against(missing), bindings(0.5)), - Err(PointRenderEpochBuildErrorV1::Compile( - CompileError::MissingOccurrenceBackdrop { - occurrence: OCCURRENCE, - surface - } - )) if surface == missing - )); - assert!(matches!( - owner.replace(cyclic_graph_spec(), bindings(0.5)), - Err(PointRenderEpochBuildErrorV1::Compile( - CompileError::RenderCycle { .. } - )) + cyclic_program().compile(), + Err(ProgramCompileError::RenderCycle { .. }) )); - let PointRenderSessionStateV1::Ready { current } = + let SessionState::Ready { current } = session.update_packed(&point(1, 0xff_ff_ff)).unwrap() else { panic!("compile failures must leave the old strong epoch untouched"); @@ -215,7 +201,7 @@ fn dangling_and_cyclic_failed_compiles_do_not_revoke_the_current_epoch() { #[test] fn dispose_revokes_sessions_and_prevents_new_attachment() { - let mut owner = PointRenderOwnerV1::new(graph_spec(), bindings(0.5)).unwrap(); + let mut owner = compiled(0.5).into_owner(); let mut session = owner.attach().unwrap(); owner.dispose(); @@ -228,11 +214,11 @@ fn dispose_revokes_sessions_and_prevents_new_attachment() { #[test] fn unavailable_after_ready_is_stale_and_retains_exactly_one_previous_snapshot() { - let owner = PointRenderOwnerV1::new(graph_spec(), bindings(0.5)).unwrap(); + let owner = compiled(0.5).into_owner(); let mut session = owner.attach().unwrap(); session.update_packed(&point(1, 0xff_ff_ff)).unwrap(); - let PointRenderSessionStateV1::Stale { + let SessionState::Stale { previous, current_unavailable, } = session.update_packed(&unavailable(2, 91)).unwrap() @@ -247,7 +233,7 @@ fn unavailable_after_ready_is_stale_and_retains_exactly_one_previous_snapshot() assert_eq!(current_unavailable.revision(), 2); assert_eq!(current_unavailable.reason(), 91); - let PointRenderSessionStateV1::Stale { previous, .. } = + let SessionState::Stale { previous, .. } = session.update_packed(&unavailable(3, 92)).unwrap() else { panic!("a later unavailable update must remain Stale"); @@ -257,7 +243,7 @@ fn unavailable_after_ready_is_stale_and_retains_exactly_one_previous_snapshot() #[test] fn malformed_lower_and_conflicting_updates_are_atomic_and_do_not_evaluate() { - let owner = PointRenderOwnerV1::new(graph_spec(), bindings(0.5)).unwrap(); + let owner = compiled(0.5).into_owner(); let mut session = owner.attach().unwrap(); session.update_packed(&point(5, 0xff_ff_ff)).unwrap(); crate::composition::reset_source_over_evaluation_count(); @@ -293,7 +279,7 @@ fn malformed_lower_and_conflicting_updates_are_atomic_and_do_not_evaluate() { )); assert_eq!(crate::composition::source_over_evaluation_count(), 0); - let PointRenderSessionStateV1::Ready { current } = session.state() else { + let SessionState::Ready { current } = session.state() else { panic!("every rejected update must leave the committed state untouched"); }; assert_eq!(current.revision(), 5); @@ -302,7 +288,7 @@ fn malformed_lower_and_conflicting_updates_are_atomic_and_do_not_evaluate() { #[test] fn exact_replay_is_idempotent_but_a_new_revision_evaluates_again() { - let owner = PointRenderOwnerV1::new(graph_spec(), bindings(0.5)).unwrap(); + let owner = compiled(0.5).into_owner(); let mut session = owner.attach().unwrap(); let payload = point(1, 0xff_ff_ff); crate::composition::reset_source_over_evaluation_count(); @@ -317,7 +303,7 @@ fn exact_replay_is_idempotent_but_a_new_revision_evaluates_again() { #[test] fn attached_session_reuses_buffers_for_every_update_state() { - let owner = PointRenderOwnerV1::new(graph_spec(), bindings(0.5)).unwrap(); + let owner = compiled(0.5).into_owner(); let mut session = owner.attach().unwrap(); let first = point(1, 0xff_ff_ff); let second = point(2, 0x20_40_60); @@ -354,7 +340,7 @@ fn attached_session_reuses_buffers_for_every_update_state() { ); } - let PointRenderSessionStateV1::Ready { current } = session.state() else { + let SessionState::Ready { current } = session.state() else { panic!("a successful observation after Stale must recover Ready"); }; assert_eq!(current.revision(), 5); @@ -364,7 +350,7 @@ fn attached_session_reuses_buffers_for_every_update_state() { #[test] fn rejected_update_preserves_cold_buffers_for_allocation_free_retry() { - let owner = PointRenderOwnerV1::new(graph_spec(), bindings(0.5)).unwrap(); + let owner = compiled(0.5).into_owner(); let mut session = owner.attach().unwrap(); let malformed = point(1, 0x01_ff_ff_ff); let valid = point(1, 0xff_ff_ff); @@ -384,7 +370,7 @@ fn rejected_update_preserves_cold_buffers_for_allocation_free_retry() { assert_eq!(rejected_allocations, 0); assert!(matches!( session.state(), - PointRenderSessionStateV1::Waiting { + SessionState::Waiting { current_unavailable: None } )); @@ -399,7 +385,7 @@ fn rejected_update_preserves_cold_buffers_for_allocation_free_retry() { #[test] fn waiting_unknown_chain_preserves_preallocated_buffers() { - let owner = PointRenderOwnerV1::new(graph_spec(), bindings(0.5)).unwrap(); + let owner = compiled(0.5).into_owner(); let mut session = owner.attach().unwrap(); let first_missing = unavailable(1, 91); let later_missing = unavailable(2, 92); @@ -422,9 +408,158 @@ fn waiting_unknown_chain_preserves_preallocated_buffers() { ); } - let PointRenderSessionStateV1::Ready { current } = session.state() else { + let SessionState::Ready { current } = session.state() else { panic!("the first admitted point after Waiting must commit Ready"); }; assert_eq!(current.revision(), 3); assert_eq!(current.composited_occurrence_signals_rgb24(), &[0x80_80_80]); } + +#[test] +fn public_program_compile_owner_session_path_emits_typed_occurrence_values() { + let compiled = compiled(0.5); + assert_eq!(compiled.surface_inputs(), &[SURFACE_PORT]); + assert_eq!(compiled.occurrences(), &[OCCURRENCE]); + + let owner = PointRenderOwner::new(compiled); + let mut session = owner.attach().unwrap(); + let surfaces = [SurfaceSignal::new( + SURFACE_PORT, + Srgb8::new([0xff; 3]), + )]; + let SessionState::Ready { current } = session + .update(SurfaceUpdate::Present { + revision: 11, + surfaces: &surfaces, + }) + .unwrap() + else { + panic!("typed present update must produce Ready"); + }; + + assert_eq!(current.revision(), 11); + assert_eq!(current.surfaces().collect::>(), surfaces.to_vec()); + assert_eq!( + current + .occurrences() + .map(|signal| (signal.occurrence(), signal.value())) + .collect::>(), + vec![(OCCURRENCE, Srgb8::new([0x80; 3]))] + ); + assert_eq!(current.occurrence(OCCURRENCE), Some(Srgb8::new([0x80; 3]))); + assert_eq!(current.occurrence(OccurrenceId::new(999)), None); +} + +#[test] +fn typed_update_schema_rejection_is_atomic_and_allocation_free() { + let owner = compiled(0.5).into_owner(); + let mut session = owner.attach().unwrap(); + let wrong = [SurfaceSignal::new( + SurfaceInputId::new(999), + Srgb8::new([0xff; 3]), + )]; + crate::composition::reset_source_over_evaluation_count(); + + let (result, allocations) = crate::test_support::measured_allocations(|| { + session + .update(SurfaceUpdate::Present { + revision: 1, + surfaces: &wrong, + }) + .map(|_| ()) + }); + assert_eq!( + result, + Err(SessionUpdateError::SurfaceInputMismatch { + index: 0, + expected: SURFACE_PORT, + actual: SurfaceInputId::new(999), + }) + ); + assert_eq!(allocations, 0); + assert_eq!(crate::composition::source_over_evaluation_count(), 0); + assert!(matches!( + session.state(), + SessionState::Waiting { + current_unavailable: None + } + )); +} + +#[test] +fn typed_update_reuses_attach_storage_for_ready_stale_and_recovery() { + let owner = compiled(0.5).into_owner(); + let mut session = owner.attach().unwrap(); + let white = [SurfaceSignal::new(SURFACE_PORT, Srgb8::new([0xff; 3]))]; + let black = [SurfaceSignal::new(SURFACE_PORT, Srgb8::new([0; 3]))]; + + for update in [ + SurfaceUpdate::Present { + revision: 1, + surfaces: &white, + }, + SurfaceUpdate::Unavailable { + revision: 2, + reason: 7, + }, + SurfaceUpdate::Present { + revision: 3, + surfaces: &black, + }, + ] { + let (result, allocations) = + crate::test_support::measured_allocations(|| session.update(update).map(|_| ())); + assert!(result.is_ok()); + assert_eq!(allocations, 0); + } +} + +#[test] +fn dropping_the_only_owner_physically_expires_attached_sessions() { + let mut session = { + let owner = compiled(0.5).into_owner(); + owner.attach().unwrap() + }; + let surfaces = [SurfaceSignal::new(SURFACE_PORT, Srgb8::new([0; 3]))]; + assert_eq!( + session.update(SurfaceUpdate::Present { + revision: 1, + surfaces: &surfaces, + }), + Err(SessionUpdateError::ProgramExpired) + ); +} + +#[test] +fn generic_program_module_has_no_recipe_or_ui_compatibility_surface() { + let source = include_str!("program_session.rs"); + for forbidden in [ + "ThemeConfig", + "RoleRecipe", + "NamedRoleTable", + "PairFill", + "PairLabel", + "AlphaAnalog", + "resolve_named_set", + "resolveTheme", + "themeHandle", + ] { + assert!( + !source.contains(forbidden), + "generic Program module must not contain `{forbidden}`" + ); + } + for required in [ + "pub struct Program", + "pub struct CompiledProgram", + "pub struct PointRenderOwner", + "pub struct Session", + "pub fn compile(self)", + "pub fn update(", + ] { + assert!( + source.contains(required), + "generic Program module must retain `{required}`" + ); + } +} From abfa74122dbec59ed67b0f81645971f234f900cc Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Wed, 22 Jul 2026 13:47:54 +0300 Subject: [PATCH 10/58] fix(core): satisfy terminal Program quality gates --- crates/labcolors-core/src/program_session.rs | 172 ++++++++---------- .../src/program_session_tests.rs | 60 ++++-- 2 files changed, 123 insertions(+), 109 deletions(-) diff --git a/crates/labcolors-core/src/program_session.rs b/crates/labcolors-core/src/program_session.rs index 67d4340d..d0ce6e8e 100644 --- a/crates/labcolors-core/src/program_session.rs +++ b/crates/labcolors-core/src/program_session.rs @@ -23,12 +23,11 @@ use std::rc::{Rc, Weak}; use crate::Srgb8; use crate::appearance::{ AdmittedAppearanceBindings, AppearanceBindings, AppearanceGraphSpec, AppearanceWorkspace, - BindingError, ColorInputId as AppearanceColorInputId, CompileError, - CompiledAppearanceGraph, OccurrenceId as AppearanceOccurrenceId, - OccurrenceSpec as AppearanceOccurrenceSpec, OpacityInputId as AppearanceOpacityInputId, - PaintId as AppearancePaintId, PaintSpec as AppearancePaintSpec, - SurfaceId as AppearanceSurfaceId, SurfaceInputPortId as AppearanceSurfaceInputId, - SurfaceSpec as AppearanceSurfaceSpec, + BindingError, ColorInputId as AppearanceColorInputId, CompileError, CompiledAppearanceGraph, + OccurrenceId as AppearanceOccurrenceId, OccurrenceSpec as AppearanceOccurrenceSpec, + OpacityInputId as AppearanceOpacityInputId, PaintId as AppearancePaintId, + PaintSpec as AppearancePaintSpec, SurfaceId as AppearanceSurfaceId, + SurfaceInputPortId as AppearanceSurfaceInputId, SurfaceSpec as AppearanceSurfaceSpec, }; use crate::composition::CompositionProfileV1; @@ -52,7 +51,10 @@ macro_rules! opaque_program_id { }; } -opaque_program_id!(ColorInputId, "Identity of one immutable encoded colour input."); +opaque_program_id!( + ColorInputId, + "Identity of one immutable encoded colour input." +); opaque_program_id!( SurfaceInputId, "Identity of one runtime encoded Surface input." @@ -237,17 +239,32 @@ impl Program { /// Public compile failure; every variant leaves no executable partial graph. #[derive(Debug, Clone, PartialEq, Eq)] pub enum ProgramCompileError { - DuplicateColorInput { input: ColorInputId }, - DuplicateOpacityInput { input: OpacityInputId }, - DuplicateSurfaceInput { input: SurfaceInputId }, - DuplicatePaint { paint: PaintId }, - DuplicateSurface { surface: SurfaceId }, - DuplicateOccurrence { occurrence: OccurrenceId }, + DuplicateColorInput { + input: ColorInputId, + }, + DuplicateOpacityInput { + input: OpacityInputId, + }, + DuplicateSurfaceInput { + input: SurfaceInputId, + }, + DuplicatePaint { + paint: PaintId, + }, + DuplicateSurface { + surface: SurfaceId, + }, + DuplicateOccurrence { + occurrence: OccurrenceId, + }, MissingPaintColorInput { paint: PaintId, input: ColorInputId, }, - MissingPaintSource { paint: PaintId, source: PaintId }, + MissingPaintSource { + paint: PaintId, + source: PaintId, + }, MissingPaintOpacityInput { paint: PaintId, input: OpacityInputId, @@ -268,12 +285,16 @@ pub enum ProgramCompileError { occurrence: OccurrenceId, surface: SurfaceId, }, - PaintCycle { paints: Vec }, + PaintCycle { + paints: Vec, + }, RenderCycle { surfaces: Vec, occurrences: Vec, }, - OpacityOutOfDomain { input: OpacityInputId }, + OpacityOutOfDomain { + input: OpacityInputId, + }, EmptySurfaceSchema, EmptyOccurrenceSet, ResourceExhausted, @@ -387,10 +408,8 @@ impl PointRenderOwner { .binding_template .try_clone_v1() .map_err(map_attach_binding_error)?; - let initial_signal_buffers = CompositedSignalBuffersV1::try_new( - &epoch.surface_inputs, - &epoch.occurrence_ids, - )?; + let initial_signal_buffers = + CompositedSignalBuffersV1::try_new(&epoch.surface_inputs, &epoch.occurrence_ids)?; Ok(Session { epoch: Rc::downgrade(epoch), bindings, @@ -449,9 +468,9 @@ fn prepare_program(program: Program) -> Result AppearanceBindings { program .colors .iter() - .map(|input| { - ( - AppearanceColorInputId::new(input.id.value()), - input.value, - ) - }) + .map(|input| (AppearanceColorInputId::new(input.id.value()), input.value)) .collect(), program .surface_inputs @@ -554,12 +568,7 @@ fn lower_bindings(program: &Program) -> AppearanceBindings { program .opacities .iter() - .map(|input| { - ( - AppearanceOpacityInputId::new(input.id.value()), - input.value, - ) - }) + .map(|input| (AppearanceOpacityInputId::new(input.id.value()), input.value)) .collect(), ) } @@ -580,10 +589,7 @@ fn public_color_id(program: &Program, value: AppearanceColorInputId) -> Option Option { +fn public_opacity_id(program: &Program, value: AppearanceOpacityInputId) -> Option { for input in &program.opacities { if AppearanceOpacityInputId::new(input.id.value()) == value { return Some(input.id); @@ -660,10 +666,7 @@ fn public_surface_id(program: &Program, value: AppearanceSurfaceId) -> Option Option { +fn public_occurrence_id(program: &Program, value: AppearanceOccurrenceId) -> Option { for occurrence in &program.occurrences { if AppearanceOccurrenceId::new(occurrence.id.value()) == value { return Some(occurrence.id); @@ -690,10 +693,10 @@ fn map_compile_error(program: &Program, error: CompileError) -> ProgramCompileEr ProgramCompileError::DuplicateOpacityInput { input } }), CompileError::DuplicateSurfaceInputPort { input } => { - public_surface_input_id(program, input).map_or( - ProgramCompileError::InternalInvariant, - |input| ProgramCompileError::DuplicateSurfaceInput { input }, - ) + public_surface_input_id(program, input) + .map_or(ProgramCompileError::InternalInvariant, |input| { + ProgramCompileError::DuplicateSurfaceInput { input } + }) } CompileError::DuplicatePaint { paint } => public_paint_id(program, paint) .map_or(ProgramCompileError::InternalInvariant, |paint| { @@ -704,10 +707,10 @@ fn map_compile_error(program: &Program, error: CompileError) -> ProgramCompileEr ProgramCompileError::DuplicateSurface { surface } }), CompileError::DuplicateOccurrence { occurrence } => { - public_occurrence_id(program, occurrence).map_or( - ProgramCompileError::InternalInvariant, - |occurrence| ProgramCompileError::DuplicateOccurrence { occurrence }, - ) + public_occurrence_id(program, occurrence) + .map_or(ProgramCompileError::InternalInvariant, |occurrence| { + ProgramCompileError::DuplicateOccurrence { occurrence } + }) } CompileError::MissingPaintColorInput { paint, input } => { match ( @@ -760,12 +763,10 @@ fn map_compile_error(program: &Program, error: CompileError) -> ProgramCompileEr public_surface_id(program, surface), public_occurrence_id(program, occurrence), ) { - (Some(surface), Some(occurrence)) => { - ProgramCompileError::MissingSurfaceOccurrence { - surface, - occurrence, - } - } + (Some(surface), Some(occurrence)) => ProgramCompileError::MissingSurfaceOccurrence { + surface, + occurrence, + }, _ => ProgramCompileError::InternalInvariant, }, CompileError::MissingOccurrencePaint { occurrence, paint } => { @@ -786,12 +787,10 @@ fn map_compile_error(program: &Program, error: CompileError) -> ProgramCompileEr public_occurrence_id(program, occurrence), public_surface_id(program, surface), ) { - (Some(occurrence), Some(surface)) => { - ProgramCompileError::MissingOccurrenceBackdrop { - occurrence, - surface, - } - } + (Some(occurrence), Some(surface)) => ProgramCompileError::MissingOccurrenceBackdrop { + occurrence, + surface, + }, _ => ProgramCompileError::InternalInvariant, }, CompileError::PaintCycle { paints } => paints @@ -1065,14 +1064,22 @@ pub(crate) enum PointRenderSessionUpdateErrorV1 { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SessionUpdateError { ProgramExpired, - SurfaceInputLengthMismatch { expected: usize, actual: usize }, + SurfaceInputLengthMismatch { + expected: usize, + actual: usize, + }, SurfaceInputMismatch { index: usize, expected: SurfaceInputId, actual: SurfaceInputId, }, - RevisionOutOfOrder { current: u64, incoming: u64 }, - RevisionConflict { revision: u64 }, + RevisionOutOfOrder { + current: u64, + incoming: u64, + }, + RevisionConflict { + revision: u64, + }, InternalInvariant, } @@ -1143,10 +1150,7 @@ impl Session { .ok_or(SessionUpdateError::ProgramExpired)?; let prepared = match update { SurfaceUpdate::Unavailable { revision, reason } => { - PreparedEncodedSurfaceUpdateV1::Unavailable(SurfaceUnavailable { - revision, - reason, - }) + PreparedEncodedSurfaceUpdateV1::Unavailable(SurfaceUnavailable { revision, reason }) } SurfaceUpdate::Present { revision, surfaces } => { if surfaces.len() != epoch.surface_inputs.len() { @@ -1155,11 +1159,8 @@ impl Session { actual: surfaces.len(), }); } - for (index, (&expected, actual)) in epoch - .surface_inputs - .iter() - .zip(surfaces.iter()) - .enumerate() + for (index, (&expected, actual)) in + epoch.surface_inputs.iter().zip(surfaces.iter()).enumerate() { if actual.input != expected { return Err(SessionUpdateError::SurfaceInputMismatch { @@ -1230,10 +1231,7 @@ impl Session { }, }; } - PreparedEncodedSurfaceUpdateV1::Present { - revision, - surfaces, - } => { + PreparedEncodedSurfaceUpdateV1::Present { revision, surfaces } => { let retained_shape_matches = match &self.state { SessionState::Waiting { .. } => { self.initial_signal_buffers.as_ref().is_some_and(|buffers| { @@ -1293,19 +1291,12 @@ impl Session { unreachable!("a Session without prior Ready must retain initial buffers") }), }; - debug_assert_eq!( - buffers.input_surface_signals_rgb24.len(), - surfaces.len() - ); + debug_assert_eq!(buffers.input_surface_signals_rgb24.len(), surfaces.len()); debug_assert_eq!( buffers.composited_occurrence_signals_rgb24.len(), epoch.occurrence_ids.len() ); - for (index, output) in buffers - .input_surface_signals_rgb24 - .iter_mut() - .enumerate() - { + for (index, output) in buffers.input_surface_signals_rgb24.iter_mut().enumerate() { *output = pack_rgb24(surfaces.value(index).bytes()); } for (resolved, output) in evaluation @@ -1341,10 +1332,7 @@ impl Session { }, ) => incoming == *current, ( - PreparedEncodedSurfaceUpdateV1::Present { - revision, - surfaces, - }, + PreparedEncodedSurfaceUpdateV1::Present { revision, surfaces }, SessionState::Ready { current }, ) => { revision == current.revision diff --git a/crates/labcolors-core/src/program_session_tests.rs b/crates/labcolors-core/src/program_session_tests.rs index 77a778b6..9bca9406 100644 --- a/crates/labcolors-core/src/program_session_tests.rs +++ b/crates/labcolors-core/src/program_session_tests.rs @@ -1,11 +1,11 @@ use crate::Srgb8; use crate::program_session::{ ColorInput, ColorInputId, CompiledProgram, CompositionProfile, Occurrence, OccurrenceId, - OpacityInput, OpacityInputId, Paint, PaintId, PointRenderOwner, Program, ProgramCompileError, - PACKED_ENCODED_SURFACE_PRESENT_TAG_V1, PACKED_ENCODED_SURFACE_UNAVAILABLE_TAG_V1, - PACKED_ENCODED_SURFACE_UPDATE_MAGIC_V1, PackedEncodedSurfaceUpdateErrorV1, - PointRenderSessionUpdateErrorV1, SessionState, SessionUpdateError, Surface, SurfaceId, - SurfaceInputId, SurfaceSignal, SurfaceUpdate, + OpacityInput, OpacityInputId, PACKED_ENCODED_SURFACE_PRESENT_TAG_V1, + PACKED_ENCODED_SURFACE_UNAVAILABLE_TAG_V1, PACKED_ENCODED_SURFACE_UPDATE_MAGIC_V1, + PackedEncodedSurfaceUpdateErrorV1, Paint, PaintId, PointRenderOwner, + PointRenderSessionUpdateErrorV1, Program, ProgramCompileError, SessionState, + SessionUpdateError, Surface, SurfaceId, SurfaceInputId, SurfaceSignal, SurfaceUpdate, }; const COLOR: ColorInputId = ColorInputId::new(1); @@ -123,13 +123,43 @@ fn retained_signal_storage_pointers(state: &SessionState) -> (*const u32, *const ) } +#[test] +fn authored_and_runtime_values_preserve_exact_typed_bindings() { + let color_value = Srgb8::new([0x12, 0x34, 0x56]); + let color = ColorInput::new(COLOR, color_value); + assert_eq!(color.id(), COLOR); + assert_eq!(color.value(), color_value); + + let opacity = OpacityInput::new(OPACITY, 0.375); + assert_eq!(opacity.id(), OPACITY); + assert_eq!(opacity.value(), 0.375); + + let occurrence = Occurrence::new( + OCCURRENCE, + TRANSLUCENT, + BACKDROP, + CompositionProfile::EncodedSrgb8SourceOverV1, + ); + assert_eq!(occurrence.id(), OCCURRENCE); + assert_eq!(occurrence.subject(), TRANSLUCENT); + assert_eq!(occurrence.against(), BACKDROP); + assert_eq!( + occurrence.composition(), + CompositionProfile::EncodedSrgb8SourceOverV1 + ); + + let surface_value = Srgb8::new([0xab, 0xcd, 0xef]); + let signal = SurfaceSignal::new(SURFACE_PORT, surface_value); + assert_eq!(signal.input(), SURFACE_PORT); + assert_eq!(signal.value(), surface_value); +} + #[test] fn point_update_executes_the_compiled_graph_and_commits_compact_occurrences() { let owner = compiled(0.5).into_owner(); let mut session = owner.attach().unwrap(); - let SessionState::Ready { current } = - session.update_packed(&point(1, 0xff_ff_ff)).unwrap() + let SessionState::Ready { current } = session.update_packed(&point(1, 0xff_ff_ff)).unwrap() else { panic!("present encoded Surface signals must produce Ready"); }; @@ -165,8 +195,7 @@ fn invalid_opacity_failed_replace_is_atomic_and_keeps_old_epoch_live() { program(1.25).compile().unwrap_err(), ProgramCompileError::OpacityOutOfDomain { input: OPACITY } ); - let SessionState::Ready { current } = - session.update_packed(&point(1, 0xff_ff_ff)).unwrap() + let SessionState::Ready { current } = session.update_packed(&point(1, 0xff_ff_ff)).unwrap() else { panic!("failed replacement must not revoke the old epoch"); }; @@ -191,8 +220,7 @@ fn dangling_and_cyclic_failed_compiles_do_not_revoke_the_current_epoch() { Err(ProgramCompileError::RenderCycle { .. }) )); - let SessionState::Ready { current } = - session.update_packed(&point(1, 0xff_ff_ff)).unwrap() + let SessionState::Ready { current } = session.update_packed(&point(1, 0xff_ff_ff)).unwrap() else { panic!("compile failures must leave the old strong epoch untouched"); }; @@ -233,8 +261,7 @@ fn unavailable_after_ready_is_stale_and_retains_exactly_one_previous_snapshot() assert_eq!(current_unavailable.revision(), 2); assert_eq!(current_unavailable.reason(), 91); - let SessionState::Stale { previous, .. } = - session.update_packed(&unavailable(3, 92)).unwrap() + let SessionState::Stale { previous, .. } = session.update_packed(&unavailable(3, 92)).unwrap() else { panic!("a later unavailable update must remain Stale"); }; @@ -422,11 +449,10 @@ fn public_program_compile_owner_session_path_emits_typed_occurrence_values() { assert_eq!(compiled.occurrences(), &[OCCURRENCE]); let owner = PointRenderOwner::new(compiled); + assert_eq!(owner.surface_inputs(), Some(&[SURFACE_PORT][..])); + assert_eq!(owner.occurrences(), Some(&[OCCURRENCE][..])); let mut session = owner.attach().unwrap(); - let surfaces = [SurfaceSignal::new( - SURFACE_PORT, - Srgb8::new([0xff; 3]), - )]; + let surfaces = [SurfaceSignal::new(SURFACE_PORT, Srgb8::new([0xff; 3]))]; let SessionState::Ready { current } = session .update(SurfaceUpdate::Present { revision: 11, From 76b9fb5b30e2567342d34c51a796b9138ef6b1ea Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Wed, 22 Jul 2026 13:56:37 +0300 Subject: [PATCH 11/58] feat(lcs): bind private output projection registry --- ...rt-reference-surplus-q55-bps-proof-v1.json | 2 +- crates/labcolors-core/src/lib.rs | 16 + .../labcolors-core/src/output_projection.rs | 543 ++++++++++++++++++ .../src/output_projection_tests.rs | 230 ++++++++ crates/labcolors-core/src/release_registry.rs | 214 +++++++ .../src/release_registry_tests.rs | 142 +++++ scripts/verify_point_support_surplus.py | 2 +- 7 files changed, 1147 insertions(+), 2 deletions(-) create mode 100644 crates/labcolors-core/src/output_projection.rs create mode 100644 crates/labcolors-core/src/output_projection_tests.rs create mode 100644 crates/labcolors-core/src/release_registry.rs create mode 100644 crates/labcolors-core/src/release_registry_tests.rs 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 63413c9e..435a6f51 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":"0571feddc1f67729f547d5b17bedb335c42a00873d46f6784850c6c43d7270ed","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":"3d37fc38e4003c16c8ac68f957576153b9e734bfd7909c138d87a5e77685adf4","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":"d6d7e7230a39e4e86324a40972423cdac8309ed4a8590f2223cd219c0860d4e0"},{"kind":"rust-source","path":"crates/labcolors-core/src/composition.rs","sha256":"195a67327a3bd86d7816b634481389930bf68577bb1202fad14c2ea152df8625"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/exact.rs","sha256":"45bd09cd8f6f74860a0a3ffeb410ee2c3cf91b4868877abbc949e724b61e1ccd"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/mod.rs","sha256":"ad721c625b33a7432de44e5f63459a62c1cbc8ec2492855f783b5440f04978f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/wcag22.rs","sha256":"985e21abccd1fcb4a3f9624d5947ff3d1af7a1bf26b6b170ea8bbef77d6f1798"},{"kind":"rust-source","path":"crates/labcolors-core/src/hash.rs","sha256":"f97a0fd7d6ad3162f0f1dfb326fccfb7ed40da9a8fa67a5b8a239a1ae2ae49c3"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"1c095f5c98a8699e723c41ee68604754b752543b5d5085463e666b229a85e24f"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"208066b255bc406303b18dc2eb6f1720c8203ea6eb37d225f9d8110c2449edc4"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"56c06b49f4687b3def69c247978da1b0cb45b42c09515a001d196cb1457c93a6"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"d4b06651e07cf4f93eccf9866f6e63c35ea72fa7125bdb41e222b19b70dd2f32"},{"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":33,"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":"9e59298d8101b01e893dd8e26f1818dad075f4ef3f069db349237c7376f49e41"} +{"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":"7085814495e8b244b1aaed38867e30f18be6676700411a3500b2698fd4465db3","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":"c5bead5ae70e46bbc9bb0105a65d37f46a224f2a142b6814223ee8b1a109cb76","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":"d6d7e7230a39e4e86324a40972423cdac8309ed4a8590f2223cd219c0860d4e0"},{"kind":"rust-source","path":"crates/labcolors-core/src/composition.rs","sha256":"195a67327a3bd86d7816b634481389930bf68577bb1202fad14c2ea152df8625"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/exact.rs","sha256":"45bd09cd8f6f74860a0a3ffeb410ee2c3cf91b4868877abbc949e724b61e1ccd"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/mod.rs","sha256":"ad721c625b33a7432de44e5f63459a62c1cbc8ec2492855f783b5440f04978f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/wcag22.rs","sha256":"985e21abccd1fcb4a3f9624d5947ff3d1af7a1bf26b6b170ea8bbef77d6f1798"},{"kind":"rust-source","path":"crates/labcolors-core/src/hash.rs","sha256":"f97a0fd7d6ad3162f0f1dfb326fccfb7ed40da9a8fa67a5b8a239a1ae2ae49c3"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"902eb891959234968ea72dba914226bca31f8d62a1b0df1a1d42e978f15b70f1"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"208066b255bc406303b18dc2eb6f1720c8203ea6eb37d225f9d8110c2449edc4"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"56c06b49f4687b3def69c247978da1b0cb45b42c09515a001d196cb1457c93a6"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"d4b06651e07cf4f93eccf9866f6e63c35ea72fa7125bdb41e222b19b70dd2f32"},{"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":33,"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":"89b362d126d0ab3443e9558db501ba1b71b683a79d33412f3fefcc7db814033e"} diff --git a/crates/labcolors-core/src/lib.rs b/crates/labcolors-core/src/lib.rs index 08d29382..2b58c5bf 100644 --- a/crates/labcolors-core/src/lib.rs +++ b/crates/labcolors-core/src/lib.rs @@ -31,6 +31,11 @@ pub(crate) mod lpc; pub mod material; pub mod neutral; pub mod numerical_plan; +#[expect( + dead_code, + reason = "private F0 output-projection release firewall precedes the atomic public hard cut" +)] +pub(crate) mod output_projection; pub(crate) mod pair; #[cfg_attr( not(test), @@ -40,6 +45,11 @@ pub(crate) mod pair; ) )] pub(crate) mod point_support; +#[expect( + dead_code, + reason = "private F0 release registry precedes the atomic public hard cut" +)] +pub(crate) mod release_registry; #[cfg_attr( not(test), expect( @@ -73,9 +83,15 @@ mod appearance_graph_tests; #[cfg(test)] mod lcs_occurrence_tests; +#[cfg(test)] +mod output_projection_tests; + #[cfg(test)] mod program_session_tests; +#[cfg(test)] +mod release_registry_tests; + #[cfg(test)] mod generic_boundary_tests; diff --git a/crates/labcolors-core/src/output_projection.rs b/crates/labcolors-core/src/output_projection.rs new file mode 100644 index 00000000..58e6c5a4 --- /dev/null +++ b/crates/labcolors-core/src/output_projection.rs @@ -0,0 +1,543 @@ +//! Private, release-bound output projection from one modeled LCS occurrence. +//! +//! This module deliberately admits one narrow edge only: +//! +//! ```text +//! modeled IEC sRGB8 signal +//! -> replayed tristimulus +//! -> the same context-bound LCS occurrence +//! -> solid CSS Color 4 `oklch(...)` + replayable certificate +//! ``` +//! +//! An output projection is neither an appearance view nor a pairwise +//! difference calibration. The nominal release identifiers below therefore +//! cannot be substituted for one another. No alpha/composition, inverse, +//! output-gamut transform, P3 path or perceptual metric is admitted by this +//! slice. + +use crate::lcs_occurrence::{ + ColorSignal, HueAngle, HueState, LcsOccurrence, ModeledTristimulusDerivationV1, + ModeledTristimulusProvenanceV1, NumericDomainError, OKLAB_VIEW_RELEASE_V1, OklabViewReleaseId, + TristimulusDomainErrorV1, TristimulusSample, +}; +use crate::spaces::oklab::xyz_d65_to_oklab_v1; + +/// Formula, policy and operation-order identity of an output projection. +/// +/// The source qualifier is intentional: this release can re-express only an +/// occurrence whose exact modeled IEC sRGB8 provenance is supplied and +/// replayed. It does not claim an inverse rendering solution for arbitrary +/// XYZ occurrences. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) enum OutputProjectionReleaseIdV1 { + CssColor4OklchD65FromModeledIec61966Srgb8SolidV1, +} + +impl OutputProjectionReleaseIdV1 { + pub(crate) const fn key(self) -> &'static str { + match self { + Self::CssColor4OklchD65FromModeledIec61966Srgb8SolidV1 => { + "css-color-4-oklch-d65-from-modeled-iec61966-srgb8-solid-v1" + } + } + } +} + +pub(crate) const CSS_COLOR_4_OKLCH_D65_FROM_MODELED_SRGB8_SOLID_V1: OutputProjectionReleaseIdV1 = + OutputProjectionReleaseIdV1::CssColor4OklchD65FromModeledIec61966Srgb8SolidV1; + +/// No pairwise difference calibration is admitted by #441-A. +/// +/// Keeping this as a nominal, uninhabited type makes absence executable: code +/// cannot mint a distance result, alias an unrelated selector or pass +/// an output/appearance release where a calibration release is required. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) enum DifferenceCalibrationReleaseIdV1 {} + +impl DifferenceCalibrationReleaseIdV1 { + #[allow(dead_code)] + pub(crate) const fn key(self) -> &'static str { + match self {} + } +} + +/// Polar view derived from the registered rectangular Oklab appearance view. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) enum OklchViewReleaseId { + PolarFromOttosson20210125OklabV1, +} + +impl OklchViewReleaseId { + pub(crate) const fn key(self) -> &'static str { + match self { + Self::PolarFromOttosson20210125OklabV1 => "polar-from-ottosson-2021-01-25-oklab-v1", + } + } +} + +pub(crate) const OKLCH_VIEW_RELEASE_V1: OklchViewReleaseId = + OklchViewReleaseId::PolarFromOttosson20210125OklabV1; + +/// Finite binary64 appearance coordinate with canonical positive zero. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +struct FiniteOklchCoordinateV1(u64); + +impl FiniteOklchCoordinateV1 { + fn new(value: f64) -> Result { + if !value.is_finite() { + return Err(NumericDomainError::NonFinite); + } + Ok(Self(if value == 0.0 { + 0.0_f64.to_bits() + } else { + value.to_bits() + })) + } + + fn get(self) -> f64 { + f64::from_bits(self.0) + } +} + +/// Immutable polar Oklch view of the output projection's admitted occurrence. +/// +/// `UndefinedExact` remains a distinct appearance state. Mapping it to a CSS +/// numeric token happens later under +/// [`CssOklchHueSerializationReleaseIdV1`], never inside this view. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) struct OklchViewV1 { + release: OklchViewReleaseId, + l: FiniteOklchCoordinateV1, + c: FiniteOklchCoordinateV1, + hue: HueState, +} + +impl OklchViewV1 { + pub(crate) const fn release(self) -> OklchViewReleaseId { + self.release + } + + pub(crate) fn l(self) -> f64 { + self.l.get() + } + + pub(crate) fn c(self) -> f64 { + self.c.get() + } + + pub(crate) const fn hue(self) -> HueState { + self.hue + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) enum OklchViewFieldV1 { + OklabL, + OklabA, + OklabB, + OklchChroma, + OklchHue, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct OklchViewDerivationErrorV1 { + release: OklchViewReleaseId, + field: OklchViewFieldV1, + reason: NumericDomainError, +} + +impl OklchViewDerivationErrorV1 { + pub(crate) const fn release(self) -> OklchViewReleaseId { + self.release + } + + pub(crate) const fn field(self) -> OklchViewFieldV1 { + self.field + } + + pub(crate) const fn reason(self) -> NumericDomainError { + self.reason + } +} + +fn oklch_view_error( + field: OklchViewFieldV1, + reason: NumericDomainError, +) -> OklchViewDerivationErrorV1 { + OklchViewDerivationErrorV1 { + release: OKLCH_VIEW_RELEASE_V1, + field, + reason, + } +} + +/// Derive the named polar view before any CSS serialization policy runs. +/// +/// Hue is derived solely from the rectangular coordinates. Encoded-source +/// facts belong to serialization policy and cannot change this view. +fn derive_oklch_view_v1(oklab: [f64; 3]) -> Result { + let [l, a, b] = oklab; + let l = FiniteOklchCoordinateV1::new(l) + .map_err(|reason| oklch_view_error(OklchViewFieldV1::OklabL, reason))?; + FiniteOklchCoordinateV1::new(a) + .map_err(|reason| oklch_view_error(OklchViewFieldV1::OklabA, reason))?; + FiniteOklchCoordinateV1::new(b) + .map_err(|reason| oklch_view_error(OklchViewFieldV1::OklabB, reason))?; + let c = FiniteOklchCoordinateV1::new(a.hypot(b)) + .map_err(|reason| oklch_view_error(OklchViewFieldV1::OklchChroma, reason))?; + let hue = if a == 0.0 && b == 0.0 { + HueState::UndefinedExact + } else { + let degrees = b.atan2(a).to_degrees(); + let canonical = if degrees < 0.0 { + degrees + 360.0 + } else { + degrees + }; + HueState::Defined( + HueAngle::new(canonical) + .map_err(|reason| oklch_view_error(OklchViewFieldV1::OklchHue, reason))?, + ) + }; + Ok(OklchViewV1 { + release: OKLCH_VIEW_RELEASE_V1, + l, + c, + hue, + }) +} + +/// Exact decimal serialization policy carried by the projection certificate. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) enum CssOklchNumberEncodingReleaseIdV1 { + LPercent5C6Hue3V1, +} + +/// Hue serialization is output syntax, not occurrence hue identity. +/// +/// Exact encoded greys and an exact rectangular Oklab origin serialize as the +/// harmless numeric CSS convention `0`. This never changes an appearance +/// view's [`crate::lcs_occurrence::HueState`] to `Defined(0°)` and introduces no +/// tolerance or perceptual-achromaticity threshold. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) enum CssOklchHueSerializationReleaseIdV1 { + ExactSourceGreyOrRectangularOriginToZeroV1, +} + +/// This projection release performs no explicit output-gamut mapping step. +/// +/// The name deliberately makes no identity or round-trip claim about a later +/// inverse conversion, quantizer or host renderer. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) enum OutputGamutTreatmentV1 { + NoExplicitProjectionGamutMapV1, +} + +/// A modeled derivation and the occurrence which claims that exact sample. +/// +/// Construction is sealed so a caller cannot attach convenient source bytes to +/// an unrelated XYZ occurrence. Context is retained inside the occurrence and +/// therefore remains part of certificate identity even though this output edge +/// uses only the occurrence's stimulus coordinates. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct ProjectionSourceV1 { + occurrence: LcsOccurrence, + modeled: ModeledTristimulusDerivationV1, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ProjectionSourceFormationErrorV1 { + ProvenanceReplayFailed(TristimulusDomainErrorV1), + RecordedSampleDoesNotReplay { + recorded: TristimulusSample, + replayed: TristimulusSample, + }, + OccurrenceSampleMismatch { + occurrence: TristimulusSample, + modeled: TristimulusSample, + }, +} + +impl ProjectionSourceV1 { + pub(crate) fn bind( + occurrence: LcsOccurrence, + modeled: ModeledTristimulusDerivationV1, + ) -> Result { + let source = Self { + occurrence, + modeled, + }; + source.verify()?; + Ok(source) + } + + fn verify(self) -> Result<(), ProjectionSourceFormationErrorV1> { + let replayed = self + .modeled + .replay() + .map_err(ProjectionSourceFormationErrorV1::ProvenanceReplayFailed)?; + let recorded = self.modeled.sample(); + if replayed != recorded { + return Err( + ProjectionSourceFormationErrorV1::RecordedSampleDoesNotReplay { + recorded, + replayed, + }, + ); + } + let occurrence = self.occurrence.sample(); + if occurrence != recorded { + return Err(ProjectionSourceFormationErrorV1::OccurrenceSampleMismatch { + occurrence, + modeled: recorded, + }); + } + Ok(()) + } + + pub(crate) const fn occurrence(self) -> LcsOccurrence { + self.occurrence + } + + pub(crate) const fn modeled(self) -> ModeledTristimulusDerivationV1 { + self.modeled + } + + pub(crate) const fn provenance(self) -> ModeledTristimulusProvenanceV1 { + self.modeled.provenance() + } + + pub(crate) const fn signal(self) -> ColorSignal { + self.provenance().source_signal() + } +} + +/// The release choice is made before any coordinates or output bytes exist. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct OutputProjectionRequestV1 { + source: ProjectionSourceV1, + release: OutputProjectionReleaseIdV1, +} + +impl OutputProjectionRequestV1 { + pub(crate) const fn new( + source: ProjectionSourceV1, + release: OutputProjectionReleaseIdV1, + ) -> Self { + Self { source, release } + } + + pub(crate) const fn source(self) -> ProjectionSourceV1 { + self.source + } + + pub(crate) const fn release(self) -> OutputProjectionReleaseIdV1 { + self.release + } +} + +/// A solid CSS Color 4 value. There is intentionally no alpha field or +/// constructor accepting opacity. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct CssColor4OklchD65SolidV1(String); + +impl CssColor4OklchD65SolidV1 { + pub(crate) fn as_str(&self) -> &str { + &self.0 + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) enum OutputProjectionFieldV1 { + OklchLightness, + OklchHueState, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum OutputProjectionNumericErrorV1 { + LightnessOutsideSourceDomain, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum OutputProjectionErrorV1 { + Source(ProjectionSourceFormationErrorV1), + OklchView(OklchViewDerivationErrorV1), + Numeric { + release: OutputProjectionReleaseIdV1, + field: OutputProjectionFieldV1, + reason: OutputProjectionNumericErrorV1, + }, + UnsupportedHueState { + release: OutputProjectionReleaseIdV1, + field: OutputProjectionFieldV1, + hue: HueState, + }, +} + +/// All inputs and versioned policies needed to replay one projection. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct OutputProjectionCertificateV1 { + source: ProjectionSourceV1, + release: OutputProjectionReleaseIdV1, + oklab_release: OklabViewReleaseId, + oklch_release: OklchViewReleaseId, + number_encoding: CssOklchNumberEncodingReleaseIdV1, + hue_serialization: CssOklchHueSerializationReleaseIdV1, + gamut_treatment: OutputGamutTreatmentV1, +} + +impl OutputProjectionCertificateV1 { + pub(crate) const fn source(self) -> ProjectionSourceV1 { + self.source + } + + pub(crate) const fn source_occurrence(self) -> LcsOccurrence { + self.source.occurrence() + } + + pub(crate) const fn source_provenance(self) -> ModeledTristimulusProvenanceV1 { + self.source.provenance() + } + + pub(crate) const fn source_signal(self) -> ColorSignal { + self.source.signal() + } + + pub(crate) const fn release(self) -> OutputProjectionReleaseIdV1 { + self.release + } + + pub(crate) const fn oklab_release(self) -> OklabViewReleaseId { + self.oklab_release + } + + pub(crate) const fn oklch_release(self) -> OklchViewReleaseId { + self.oklch_release + } + + pub(crate) const fn number_encoding(self) -> CssOklchNumberEncodingReleaseIdV1 { + self.number_encoding + } + + pub(crate) const fn hue_serialization(self) -> CssOklchHueSerializationReleaseIdV1 { + self.hue_serialization + } + + pub(crate) const fn gamut_treatment(self) -> OutputGamutTreatmentV1 { + self.gamut_treatment + } + + /// Re-run source provenance, view math and serialization under the same + /// release. Equality with the certified value is the byte replay check. + pub(crate) fn replay(self) -> Result { + project_output_v1(OutputProjectionRequestV1::new(self.source, self.release)) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct OutputProjectionV1 { + oklch_view: OklchViewV1, + value: CssColor4OklchD65SolidV1, + certificate: OutputProjectionCertificateV1, +} + +impl OutputProjectionV1 { + pub(crate) const fn oklch_view(&self) -> OklchViewV1 { + self.oklch_view + } + + pub(crate) fn value(&self) -> &CssColor4OklchD65SolidV1 { + &self.value + } + + pub(crate) const fn certificate(&self) -> OutputProjectionCertificateV1 { + self.certificate + } +} + +fn numeric_error( + release: OutputProjectionReleaseIdV1, + field: OutputProjectionFieldV1, + reason: OutputProjectionNumericErrorV1, +) -> OutputProjectionErrorV1 { + OutputProjectionErrorV1::Numeric { + release, + field, + reason, + } +} + +fn project_css_color4_oklch_d65_from_modeled_srgb8_solid_v1( + source: ProjectionSourceV1, + release: OutputProjectionReleaseIdV1, +) -> Result { + source.verify().map_err(OutputProjectionErrorV1::Source)?; + + // The occurrence sample, not the encoded source channels, owns appearance + // geometry. Provenance is used only to prove that this narrow output + // release may serialize the occurrence under its registered policies. + let oklab = xyz_d65_to_oklab_v1(source.occurrence().sample().xyz()); + let source_srgb8 = source.signal().srgb8(); + let oklch_view = derive_oklch_view_v1(oklab).map_err(OutputProjectionErrorV1::OklchView)?; + let l = oklch_view.l(); + if !(0.0..=1.0).contains(&l) { + return Err(numeric_error( + release, + OutputProjectionFieldV1::OklchLightness, + OutputProjectionNumericErrorV1::LightnessOutsideSourceDomain, + )); + } + + let hue_degrees = if source_srgb8.is_achromatic() { + 0.0 + } else { + match oklch_view.hue() { + HueState::Defined(angle) => angle.degrees(), + HueState::UndefinedExact => 0.0, + hue @ HueState::PowerlessBy(_) => { + return Err(OutputProjectionErrorV1::UnsupportedHueState { + release, + field: OutputProjectionFieldV1::OklchHueState, + hue, + }); + } + } + }; + + let value = CssColor4OklchD65SolidV1(format!( + "oklch({:.5}% {:.6} {:.3})", + l * 100.0, + oklch_view.c(), + hue_degrees, + )); + let certificate = OutputProjectionCertificateV1 { + source, + release, + oklab_release: OKLAB_VIEW_RELEASE_V1, + oklch_release: oklch_view.release(), + number_encoding: CssOklchNumberEncodingReleaseIdV1::LPercent5C6Hue3V1, + hue_serialization: + CssOklchHueSerializationReleaseIdV1::ExactSourceGreyOrRectangularOriginToZeroV1, + gamut_treatment: OutputGamutTreatmentV1::NoExplicitProjectionGamutMapV1, + }; + Ok(OutputProjectionV1 { + oklch_view, + value, + certificate, + }) +} + +/// Execute exactly the release selected in the request. There is no +/// result-dependent release selection or fallback. +pub(crate) fn project_output_v1( + request: OutputProjectionRequestV1, +) -> Result { + match request.release() { + OutputProjectionReleaseIdV1::CssColor4OklchD65FromModeledIec61966Srgb8SolidV1 => { + project_css_color4_oklch_d65_from_modeled_srgb8_solid_v1( + request.source(), + request.release(), + ) + } + } +} diff --git a/crates/labcolors-core/src/output_projection_tests.rs b/crates/labcolors-core/src/output_projection_tests.rs new file mode 100644 index 00000000..b3e19d1a --- /dev/null +++ b/crates/labcolors-core/src/output_projection_tests.rs @@ -0,0 +1,230 @@ +use std::any::TypeId; + +use crate::Srgb8; +use crate::lcs_occurrence::{ + AdaptingLuminanceCdM2, AppearanceContextId, AppearanceContextSchemaReleaseId, + BackgroundLuminanceRatio, ColorSignal, IEC_SRGB_D65_XYZ_FRAME_V1, LcsOccurrence, + ModeledTristimulusDerivationV1, OKLAB_VIEW_RELEASE_V1, SurroundProfileId, + derive_modeled_tristimulus_v1, +}; +use crate::output_projection::{ + CSS_COLOR_4_OKLCH_D65_FROM_MODELED_SRGB8_SOLID_V1, CssOklchHueSerializationReleaseIdV1, + CssOklchNumberEncodingReleaseIdV1, DifferenceCalibrationReleaseIdV1, OKLCH_VIEW_RELEASE_V1, + OutputGamutTreatmentV1, OutputProjectionErrorV1, OutputProjectionReleaseIdV1, + OutputProjectionRequestV1, OutputProjectionV1, ProjectionSourceFormationErrorV1, + ProjectionSourceV1, project_output_v1, +}; +use crate::spaces::oklab::oklab_to_srgb_linear; +use crate::spaces::srgb::srgb8_from_linear; + +fn context(adapting_luminance_cd_m2: f64) -> AppearanceContextId { + AppearanceContextId::from_inputs( + AppearanceContextSchemaReleaseId::Ciecam16ViewingInputsV1, + IEC_SRGB_D65_XYZ_FRAME_V1, + AdaptingLuminanceCdM2::try_new(adapting_luminance_cd_m2).unwrap(), + BackgroundLuminanceRatio::try_new(0.2).unwrap(), + SurroundProfileId::AverageV1, + ) +} + +fn modeled(bytes: [u8; 3]) -> ModeledTristimulusDerivationV1 { + derive_modeled_tristimulus_v1(ColorSignal::from_srgb8(Srgb8::new(bytes))).unwrap() +} + +fn source(bytes: [u8; 3], adapting_luminance_cd_m2: f64) -> ProjectionSourceV1 { + let modeled = modeled(bytes); + let occurrence = + LcsOccurrence::in_context(modeled.sample(), context(adapting_luminance_cd_m2)).unwrap(); + ProjectionSourceV1::bind(occurrence, modeled).unwrap() +} + +fn project(bytes: [u8; 3], adapting_luminance_cd_m2: f64) -> OutputProjectionV1 { + project_output_v1(OutputProjectionRequestV1::new( + source(bytes, adapting_luminance_cd_m2), + CSS_COLOR_4_OKLCH_D65_FROM_MODELED_SRGB8_SOLID_V1, + )) + .unwrap() +} + +fn difference_release_is_uninhabited(value: DifferenceCalibrationReleaseIdV1) -> ! { + match value {} +} + +#[test] +fn release_kinds_are_nominal_and_difference_registry_is_empty() { + assert_ne!( + TypeId::of::(), + TypeId::of::(), + ); + let _: fn(DifferenceCalibrationReleaseIdV1) -> ! = difference_release_is_uninhabited; + assert_eq!( + CSS_COLOR_4_OKLCH_D65_FROM_MODELED_SRGB8_SOLID_V1.key(), + "css-color-4-oklch-d65-from-modeled-iec61966-srgb8-solid-v1", + ); +} + +#[test] +fn projector_signature_has_no_alpha_metric_or_fallback_input() { + let projector: fn( + OutputProjectionRequestV1, + ) -> Result = project_output_v1; + assert!( + projector(OutputProjectionRequestV1::new( + source([0x44, 0x88, 0xCC], 64.0), + CSS_COLOR_4_OKLCH_D65_FROM_MODELED_SRGB8_SOLID_V1, + )) + .is_ok() + ); +} + +#[test] +fn source_binding_rejects_bytes_from_an_unrelated_modeled_derivation() { + let occurrence_model = modeled([0x44, 0x88, 0xCC]); + let unrelated_model = modeled([0xCC, 0x88, 0x44]); + let occurrence = LcsOccurrence::in_context(occurrence_model.sample(), context(64.0)).unwrap(); + + assert_eq!( + ProjectionSourceV1::bind(occurrence, unrelated_model), + Err(ProjectionSourceFormationErrorV1::OccurrenceSampleMismatch { + occurrence: occurrence_model.sample(), + modeled: unrelated_model.sample(), + }), + ); +} + +#[test] +fn certificate_replays_source_view_formula_and_exact_css_bytes() { + let projected = project([0x44, 0x88, 0xCC], 64.0); + let certificate = projected.certificate(); + + assert_eq!(certificate.replay().unwrap(), projected); + assert_eq!( + certificate.release(), + CSS_COLOR_4_OKLCH_D65_FROM_MODELED_SRGB8_SOLID_V1, + ); + assert_eq!(certificate.oklab_release(), OKLAB_VIEW_RELEASE_V1); + assert_eq!(certificate.oklch_release(), OKLCH_VIEW_RELEASE_V1); + assert_eq!( + certificate.oklch_release().key(), + "polar-from-ottosson-2021-01-25-oklab-v1", + ); + assert_eq!( + certificate.number_encoding(), + CssOklchNumberEncodingReleaseIdV1::LPercent5C6Hue3V1, + ); + assert_eq!( + certificate.hue_serialization(), + CssOklchHueSerializationReleaseIdV1::ExactSourceGreyOrRectangularOriginToZeroV1, + ); + assert_eq!( + certificate.gamut_treatment(), + OutputGamutTreatmentV1::NoExplicitProjectionGamutMapV1, + ); + assert_eq!( + certificate.source_signal().srgb8().bytes(), + [0x44, 0x88, 0xCC] + ); + assert_eq!( + certificate.source_provenance(), + certificate.source().modeled().provenance(), + ); +} + +#[test] +fn occurrence_context_remains_in_certificate_identity_not_css_geometry() { + let first = project([0x44, 0x88, 0xCC], 32.0); + let second = project([0x44, 0x88, 0xCC], 64.0); + + assert_eq!(first.value(), second.value()); + assert_ne!(first.certificate(), second.certificate()); + assert_ne!( + first.certificate().source_occurrence(), + second.certificate().source_occurrence(), + ); +} + +#[test] +fn solid_css_has_no_alpha_and_exact_encoded_greys_use_the_release_zero_convention() { + for byte in u8::MIN..=u8::MAX { + let projected = project([byte; 3], 64.0); + let css = projected.value().as_str(); + let view = projected.oklch_view(); + match view.hue() { + crate::lcs_occurrence::HueState::UndefinedExact => assert_eq!(view.c(), 0.0), + crate::lcs_occurrence::HueState::Defined(_) => assert!(view.c() > 0.0), + crate::lcs_occurrence::HueState::PowerlessBy(_) => { + panic!("pure polar view introduced a powerless policy state") + } + } + assert!(css.starts_with("oklch(") && css.ends_with(')'), "{css}"); + assert!(!css.contains('/'), "solid release emitted alpha: {css}"); + assert!(css.ends_with(" 0.000)"), "grey hue policy drifted: {css}"); + assert!(!css.contains("-0."), "signed zero escaped: {css}"); + } + + assert!(matches!( + project([u8::MAX; 3], 64.0).oklch_view().hue(), + crate::lcs_occurrence::HueState::Defined(_), + )); +} + +fn parse_solid_css(css: &str) -> [f64; 3] { + let inner = css + .strip_prefix("oklch(") + .and_then(|value| value.strip_suffix(')')) + .expect("registered solid CSS shape"); + assert!(!inner.contains('/')); + let mut fields = inner.split_whitespace(); + let l = fields + .next() + .and_then(|value| value.strip_suffix('%')) + .expect("percentage lightness") + .parse::() + .unwrap() + / 100.0; + let c = fields.next().unwrap().parse::().unwrap(); + let h = fields.next().unwrap().parse::().unwrap(); + assert!(fields.next().is_none()); + [l, c, h] +} + +fn replay_css_through_existing_inverse_to_srgb8(css: &str) -> Srgb8 { + let [l, c, h] = parse_solid_css(css); + let (sin, cos) = h.to_radians().sin_cos(); + srgb8_from_linear(oklab_to_srgb_linear([l, c * cos, c * sin])) +} + +#[test] +fn registered_precision_replays_a_non_aligned_lattice_through_existing_inverse_clamp_and_round() { + // 16^3 points including both ends. This is a deterministic regression + // corpus, not a claim about every CSS implementation or all 16.7M inputs. + let steps: Vec = (0_u16..=255).step_by(17).map(|value| value as u8).collect(); + assert_eq!(steps.first(), Some(&0)); + assert_eq!(steps.last(), Some(&255)); + + for &red in &steps { + for &green in &steps { + for &blue in &steps { + let expected = Srgb8::new([red, green, blue]); + let projected = project(expected.bytes(), 64.0); + assert_eq!( + replay_css_through_existing_inverse_to_srgb8(projected.value().as_str()), + expected, + "CSS byte replay drifted: {}", + projected.value().as_str(), + ); + } + } + } +} + +#[test] +fn fixed_css_values_pin_formula_order_and_solid_serialization() { + for (bytes, expected) in [ + ([0x00, 0x00, 0x00], "oklch(0.00000% 0.000000 0.000)"), + ([0xFF, 0xFF, 0xFF], "oklch(100.00000% 0.000000 0.000)"), + ([0xFF, 0x00, 0x00], "oklch(62.79554% 0.257683 29.234)"), + ] { + assert_eq!(project(bytes, 64.0).value().as_str(), expected); + } +} diff --git a/crates/labcolors-core/src/release_registry.rs b/crates/labcolors-core/src/release_registry.rs new file mode 100644 index 00000000..6a3d47fc --- /dev/null +++ b/crates/labcolors-core/src/release_registry.rs @@ -0,0 +1,214 @@ +//! Minimal machine-readable registry for the F0 color-model releases. +//! +//! Registry rows name only code releases that already exist. The absence of a +//! difference calibration is an explicit row, not a placeholder calibration. +//! No applicability, empirical validation, uncertainty or observer-study data +//! is inferred here. + +use crate::lcs_occurrence::{ + CAM16_VIEW_RELEASE_V1, Cam16ViewReleaseId, OKLAB_VIEW_RELEASE_V1, OklabViewReleaseId, +}; +use crate::output_projection::{ + CSS_COLOR_4_OKLCH_D65_FROM_MODELED_SRGB8_SOLID_V1, DifferenceCalibrationReleaseIdV1, + OKLCH_VIEW_RELEASE_V1, OklchViewReleaseId, OutputProjectionReleaseIdV1, +}; + +pub(crate) const RELEASE_REGISTRY_SCHEMA_VERSION_V1: u16 = 1; + +const RELEASE_REGISTRY_CANONICAL_BYTES_V1: &[u8] = concat!( + "labcolors.release-registry.canonical-binary.v1\0", + "\0\x01\0\x05", + "\x01\x01\0\x26cam16-li-et-al-2017-cie-248-forward-v1", + "\x01\x01\0\x24oklab-ottosson-2021-01-25-xyz-d65-v1", + "\x01\x01\0\x27polar-from-ottosson-2021-01-25-oklab-v1", + "\x02\0\0\0", + "\x03\x01\0\x3acss-color-4-oklch-d65-from-modeled-iec61966-srgb8-solid-v1", +) +.as_bytes(); + +const RELEASE_REGISTRY_FNV1A32_V1: u32 = 3_103_457_152; + +/// The disjoint kinds whose availability is reported by this registry. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) enum ReleaseRegistryClassV1 { + AppearanceView, + DifferenceCalibration, + OutputProjection, +} + +impl ReleaseRegistryClassV1 { + pub(crate) const fn canonical_tag(self) -> u8 { + match self { + Self::AppearanceView => 1, + Self::DifferenceCalibration => 2, + Self::OutputProjection => 3, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) enum ReleaseRegistryAvailabilityV1 { + Unavailable, + Registered, +} + +impl ReleaseRegistryAvailabilityV1 { + pub(crate) const fn canonical_tag(self) -> u8 { + match self { + Self::Unavailable => 0, + Self::Registered => 1, + } + } +} + +/// A nominal link to one release implemented by the current F0 code. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) enum RegisteredColorReleaseIdV1 { + Cam16View(Cam16ViewReleaseId), + OklabView(OklabViewReleaseId), + OklchView(OklchViewReleaseId), + CssColor4OklchD65FromModeledSrgb8Solid(OutputProjectionReleaseIdV1), +} + +impl RegisteredColorReleaseIdV1 { + pub(crate) const fn class(self) -> ReleaseRegistryClassV1 { + match self { + Self::Cam16View(_) | Self::OklabView(_) | Self::OklchView(_) => { + ReleaseRegistryClassV1::AppearanceView + } + Self::CssColor4OklchD65FromModeledSrgb8Solid(_) => { + ReleaseRegistryClassV1::OutputProjection + } + } + } + + /// Stable ASCII key for the exact formula/operation-order release. + pub(crate) const fn key(self) -> &'static str { + match self { + Self::Cam16View(Cam16ViewReleaseId::LiEtAl2017Cie248ForwardV1) => { + "cam16-li-et-al-2017-cie-248-forward-v1" + } + Self::OklabView(OklabViewReleaseId::Ottosson20210125XyzD65V1) => { + "oklab-ottosson-2021-01-25-xyz-d65-v1" + } + Self::OklchView(OklchViewReleaseId::PolarFromOttosson20210125OklabV1) => { + "polar-from-ottosson-2021-01-25-oklab-v1" + } + Self::CssColor4OklchD65FromModeledSrgb8Solid(release) => release.key(), + } + } +} + +/// One closed registry row. +/// +/// The unavailable variant carries no release identifier, so it cannot be +/// mistaken for an admitted difference formula. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) enum ReleaseRegistryRecordV1 { + Registered(RegisteredColorReleaseIdV1), + DifferenceCalibrationUnavailable, +} + +impl ReleaseRegistryRecordV1 { + pub(crate) const fn class(self) -> ReleaseRegistryClassV1 { + match self { + Self::Registered(release) => release.class(), + Self::DifferenceCalibrationUnavailable => ReleaseRegistryClassV1::DifferenceCalibration, + } + } + + pub(crate) const fn availability(self) -> ReleaseRegistryAvailabilityV1 { + match self { + Self::Registered(_) => ReleaseRegistryAvailabilityV1::Registered, + Self::DifferenceCalibrationUnavailable => ReleaseRegistryAvailabilityV1::Unavailable, + } + } + + pub(crate) const fn release(self) -> Option { + match self { + Self::Registered(release) => Some(release), + Self::DifferenceCalibrationUnavailable => None, + } + } +} + +// Canonical order is class tag, then release-key bytes. An unavailable class +// has exactly one keyless row. +const RELEASE_REGISTRY_RECORDS_V1: [ReleaseRegistryRecordV1; 5] = [ + ReleaseRegistryRecordV1::Registered(RegisteredColorReleaseIdV1::Cam16View( + CAM16_VIEW_RELEASE_V1, + )), + ReleaseRegistryRecordV1::Registered(RegisteredColorReleaseIdV1::OklabView( + OKLAB_VIEW_RELEASE_V1, + )), + ReleaseRegistryRecordV1::Registered(RegisteredColorReleaseIdV1::OklchView( + OKLCH_VIEW_RELEASE_V1, + )), + ReleaseRegistryRecordV1::DifferenceCalibrationUnavailable, + ReleaseRegistryRecordV1::Registered( + RegisteredColorReleaseIdV1::CssColor4OklchD65FromModeledSrgb8Solid( + CSS_COLOR_4_OKLCH_D65_FROM_MODELED_SRGB8_SOLID_V1, + ), + ), +]; + +pub(crate) const fn release_registry_records_v1() -> &'static [ReleaseRegistryRecordV1] { + &RELEASE_REGISTRY_RECORDS_V1 +} + +/// The existing difference-release type is uninhabited in this registry. +pub(crate) const fn impossible_difference_calibration_release_v1( + release: DifferenceCalibrationReleaseIdV1, +) -> ! { + match release {} +} + +/// Canonical binary encoding of the complete registry. +/// +/// Layout is `magic || schema:u16be || rows:u16be`, followed by rows in the +/// declared canonical order. Every row is +/// `class:u8 || availability:u8 || key_length:u16be || key:utf8`; unavailable +/// difference calibration has a zero key length. +pub(crate) const fn release_registry_canonical_bytes_v1() -> &'static [u8] { + RELEASE_REGISTRY_CANONICAL_BYTES_V1 +} + +/// Explicit algorithm identity for the registry's non-cryptographic drift +/// sentinel. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) enum ReleaseRegistryDigestAlgorithmV1 { + Fnv1a32V1, +} + +impl ReleaseRegistryDigestAlgorithmV1 { + pub(crate) const fn key(self) -> &'static str { + match self { + Self::Fnv1a32V1 => "fnv1a-32-v1", + } + } +} + +/// A deterministic drift sentinel, not cryptographic evidence or an +/// authenticity/content-identity claim. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct ReleaseRegistryDigestV1 { + algorithm: ReleaseRegistryDigestAlgorithmV1, + value: u32, +} + +impl ReleaseRegistryDigestV1 { + pub(crate) const fn algorithm(self) -> ReleaseRegistryDigestAlgorithmV1 { + self.algorithm + } + + pub(crate) const fn value(self) -> u32 { + self.value + } +} + +pub(crate) fn release_registry_digest_v1() -> ReleaseRegistryDigestV1 { + ReleaseRegistryDigestV1 { + algorithm: ReleaseRegistryDigestAlgorithmV1::Fnv1a32V1, + value: RELEASE_REGISTRY_FNV1A32_V1, + } +} diff --git a/crates/labcolors-core/src/release_registry_tests.rs b/crates/labcolors-core/src/release_registry_tests.rs new file mode 100644 index 00000000..006f18e5 --- /dev/null +++ b/crates/labcolors-core/src/release_registry_tests.rs @@ -0,0 +1,142 @@ +use crate::lcs_occurrence::{CAM16_VIEW_RELEASE_V1, OKLAB_VIEW_RELEASE_V1}; +use crate::output_projection::{ + CSS_COLOR_4_OKLCH_D65_FROM_MODELED_SRGB8_SOLID_V1, DifferenceCalibrationReleaseIdV1, + OKLCH_VIEW_RELEASE_V1, +}; +use crate::release_registry::{ + RELEASE_REGISTRY_SCHEMA_VERSION_V1, RegisteredColorReleaseIdV1, ReleaseRegistryAvailabilityV1, + ReleaseRegistryClassV1, ReleaseRegistryDigestAlgorithmV1, ReleaseRegistryRecordV1, + impossible_difference_calibration_release_v1, release_registry_canonical_bytes_v1, + release_registry_digest_v1, release_registry_records_v1, +}; + +#[test] +fn registry_contains_only_the_four_implemented_releases() { + assert_eq!( + release_registry_records_v1(), + &[ + ReleaseRegistryRecordV1::Registered(RegisteredColorReleaseIdV1::Cam16View( + CAM16_VIEW_RELEASE_V1, + )), + ReleaseRegistryRecordV1::Registered(RegisteredColorReleaseIdV1::OklabView( + OKLAB_VIEW_RELEASE_V1, + )), + ReleaseRegistryRecordV1::Registered(RegisteredColorReleaseIdV1::OklchView( + OKLCH_VIEW_RELEASE_V1, + )), + ReleaseRegistryRecordV1::DifferenceCalibrationUnavailable, + ReleaseRegistryRecordV1::Registered( + RegisteredColorReleaseIdV1::CssColor4OklchD65FromModeledSrgb8Solid( + CSS_COLOR_4_OKLCH_D65_FROM_MODELED_SRGB8_SOLID_V1, + ), + ), + ], + ); +} + +#[test] +fn difference_registry_is_explicitly_unavailable_and_release_type_is_uninhabited() { + let row = release_registry_records_v1() + .iter() + .copied() + .find(|row| row.class() == ReleaseRegistryClassV1::DifferenceCalibration) + .expect("difference availability row"); + assert_eq!( + row, + ReleaseRegistryRecordV1::DifferenceCalibrationUnavailable + ); + assert_eq!( + row.availability(), + ReleaseRegistryAvailabilityV1::Unavailable + ); + assert_eq!(row.release(), None); + + let _: fn(DifferenceCalibrationReleaseIdV1) -> ! = impossible_difference_calibration_release_v1; +} + +#[test] +fn registered_rows_have_stable_exact_release_keys() { + let keys: Vec<_> = release_registry_records_v1() + .iter() + .filter_map(|row| row.release().map(RegisteredColorReleaseIdV1::key)) + .collect(); + assert_eq!( + keys, + [ + "cam16-li-et-al-2017-cie-248-forward-v1", + "oklab-ottosson-2021-01-25-xyz-d65-v1", + "polar-from-ottosson-2021-01-25-oklab-v1", + "css-color-4-oklch-d65-from-modeled-iec61966-srgb8-solid-v1", + ], + ); +} + +#[test] +fn canonical_bytes_pin_schema_order_availability_and_keys() { + assert_eq!(RELEASE_REGISTRY_SCHEMA_VERSION_V1, 1); + assert_eq!( + release_registry_canonical_bytes_v1(), + concat!( + "labcolors.release-registry.canonical-binary.v1\0", + "\0\x01\0\x05", + "\x01\x01\0\x26cam16-li-et-al-2017-cie-248-forward-v1", + "\x01\x01\0\x24oklab-ottosson-2021-01-25-xyz-d65-v1", + "\x01\x01\0\x27polar-from-ottosson-2021-01-25-oklab-v1", + "\x02\0\0\0", + "\x03\x01\0\x3acss-color-4-oklch-d65-from-modeled-iec61966-srgb8-solid-v1", + ) + .as_bytes(), + ); +} + +#[test] +fn canonical_bytes_encode_every_typed_registry_row_in_order() { + const MAGIC: &[u8] = b"labcolors.release-registry.canonical-binary.v1\0"; + + let bytes = release_registry_canonical_bytes_v1(); + assert_eq!(&bytes[..MAGIC.len()], MAGIC); + let mut cursor = MAGIC.len(); + let take_u16 = |cursor: &mut usize| { + let value = u16::from_be_bytes([bytes[*cursor], bytes[*cursor + 1]]); + *cursor += 2; + value + }; + + assert_eq!(take_u16(&mut cursor), RELEASE_REGISTRY_SCHEMA_VERSION_V1); + let records = release_registry_records_v1(); + assert_eq!(usize::from(take_u16(&mut cursor)), records.len()); + + for record in records { + assert_eq!(bytes[cursor], record.class().canonical_tag()); + cursor += 1; + assert_eq!(bytes[cursor], record.availability().canonical_tag()); + cursor += 1; + + let key_length = usize::from(take_u16(&mut cursor)); + let expected_key = record + .release() + .map(RegisteredColorReleaseIdV1::key) + .unwrap_or("") + .as_bytes(); + assert_eq!(key_length, expected_key.len()); + assert_eq!(&bytes[cursor..cursor + key_length], expected_key); + cursor += key_length; + } + + assert_eq!(cursor, bytes.len()); +} + +#[test] +fn digest_names_its_non_cryptographic_algorithm_and_binds_canonical_bytes() { + let digest = release_registry_digest_v1(); + assert_eq!( + digest.algorithm(), + ReleaseRegistryDigestAlgorithmV1::Fnv1a32V1, + ); + assert_eq!(digest.algorithm().key(), "fnv1a-32-v1"); + assert_eq!( + digest.value(), + crate::fnv1a_32(release_registry_canonical_bytes_v1()), + ); + assert_eq!(digest.value(), 3_103_457_152); +} diff --git a/scripts/verify_point_support_surplus.py b/scripts/verify_point_support_surplus.py index 111dcc47..eb5b0acc 100644 --- a/scripts/verify_point_support_surplus.py +++ b/scripts/verify_point_support_surplus.py @@ -57,7 +57,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 = ( - "3d37fc38e4003c16c8ac68f957576153b9e734bfd7909c138d87a5e77685adf4" + "c5bead5ae70e46bbc9bb0105a65d37f46a224f2a142b6814223ee8b1a109cb76" ) EXPECTED_Q55_PROOF_SHA256 = ( "ac59cf89503170c789223b91d775213a19d4e571ef930f2ea609fcd51b14defd" From d80cb785b80e00e11fbc521f9b405fd74b715bb4 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Wed, 22 Jul 2026 13:59:40 +0300 Subject: [PATCH 12/58] style(core): canonicalize release module order --- ...int-support-reference-surplus-q55-bps-proof-v1.json | 2 +- crates/labcolors-core/src/lib.rs | 10 +++++----- scripts/verify_point_support_surplus.py | 2 +- 3 files changed, 7 insertions(+), 7 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 435a6f51..63f62511 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":"7085814495e8b244b1aaed38867e30f18be6676700411a3500b2698fd4465db3","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":"c5bead5ae70e46bbc9bb0105a65d37f46a224f2a142b6814223ee8b1a109cb76","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":"d6d7e7230a39e4e86324a40972423cdac8309ed4a8590f2223cd219c0860d4e0"},{"kind":"rust-source","path":"crates/labcolors-core/src/composition.rs","sha256":"195a67327a3bd86d7816b634481389930bf68577bb1202fad14c2ea152df8625"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/exact.rs","sha256":"45bd09cd8f6f74860a0a3ffeb410ee2c3cf91b4868877abbc949e724b61e1ccd"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/mod.rs","sha256":"ad721c625b33a7432de44e5f63459a62c1cbc8ec2492855f783b5440f04978f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/wcag22.rs","sha256":"985e21abccd1fcb4a3f9624d5947ff3d1af7a1bf26b6b170ea8bbef77d6f1798"},{"kind":"rust-source","path":"crates/labcolors-core/src/hash.rs","sha256":"f97a0fd7d6ad3162f0f1dfb326fccfb7ed40da9a8fa67a5b8a239a1ae2ae49c3"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"902eb891959234968ea72dba914226bca31f8d62a1b0df1a1d42e978f15b70f1"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"208066b255bc406303b18dc2eb6f1720c8203ea6eb37d225f9d8110c2449edc4"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"56c06b49f4687b3def69c247978da1b0cb45b42c09515a001d196cb1457c93a6"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"d4b06651e07cf4f93eccf9866f6e63c35ea72fa7125bdb41e222b19b70dd2f32"},{"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":33,"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":"89b362d126d0ab3443e9558db501ba1b71b683a79d33412f3fefcc7db814033e"} +{"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":"f307eb96bab42262d1305f76a6e0890597d3c24b622828f8cbc21573e2f7da39","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":"a34371d8885a1d532e3be07ebf47d6383a7b584dd749f70d097c02cbcc7b8cc6","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":"d6d7e7230a39e4e86324a40972423cdac8309ed4a8590f2223cd219c0860d4e0"},{"kind":"rust-source","path":"crates/labcolors-core/src/composition.rs","sha256":"195a67327a3bd86d7816b634481389930bf68577bb1202fad14c2ea152df8625"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/exact.rs","sha256":"45bd09cd8f6f74860a0a3ffeb410ee2c3cf91b4868877abbc949e724b61e1ccd"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/mod.rs","sha256":"ad721c625b33a7432de44e5f63459a62c1cbc8ec2492855f783b5440f04978f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/wcag22.rs","sha256":"985e21abccd1fcb4a3f9624d5947ff3d1af7a1bf26b6b170ea8bbef77d6f1798"},{"kind":"rust-source","path":"crates/labcolors-core/src/hash.rs","sha256":"f97a0fd7d6ad3162f0f1dfb326fccfb7ed40da9a8fa67a5b8a239a1ae2ae49c3"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"57e4997f0b1cdcda3613e85cdb47ac656e39744f5d6c6e3c49f36e78b3701484"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"208066b255bc406303b18dc2eb6f1720c8203ea6eb37d225f9d8110c2449edc4"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"56c06b49f4687b3def69c247978da1b0cb45b42c09515a001d196cb1457c93a6"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"d4b06651e07cf4f93eccf9866f6e63c35ea72fa7125bdb41e222b19b70dd2f32"},{"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":33,"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":"d0c569a5ee322d48062ad185b40a7762a2b8881db8f77eaecdba71c768a8dcb4"} diff --git a/crates/labcolors-core/src/lib.rs b/crates/labcolors-core/src/lib.rs index 2b58c5bf..9c6ef997 100644 --- a/crates/labcolors-core/src/lib.rs +++ b/crates/labcolors-core/src/lib.rs @@ -45,11 +45,6 @@ pub(crate) mod pair; ) )] pub(crate) mod point_support; -#[expect( - dead_code, - reason = "private F0 release registry precedes the atomic public hard cut" -)] -pub(crate) mod release_registry; #[cfg_attr( not(test), expect( @@ -58,6 +53,11 @@ pub(crate) mod release_registry; ) )] pub(crate) mod program_session; +#[expect( + dead_code, + reason = "private F0 release registry precedes the atomic public hard cut" +)] +pub(crate) mod release_registry; pub mod scale; pub mod semantic; pub mod solve; diff --git a/scripts/verify_point_support_surplus.py b/scripts/verify_point_support_surplus.py index eb5b0acc..fe9e3659 100644 --- a/scripts/verify_point_support_surplus.py +++ b/scripts/verify_point_support_surplus.py @@ -57,7 +57,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 = ( - "c5bead5ae70e46bbc9bb0105a65d37f46a224f2a142b6814223ee8b1a109cb76" + "a34371d8885a1d532e3be07ebf47d6383a7b584dd749f70d097c02cbcc7b8cc6" ) EXPECTED_Q55_PROOF_SHA256 = ( "ac59cf89503170c789223b91d775213a19d4e571ef930f2ea609fcd51b14defd" From 1a9f978c5446485bee48fd0928332922c3943008 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Wed, 22 Jul 2026 14:03:07 +0300 Subject: [PATCH 13/58] fix(core): scope private registry lint firewall --- ...point-support-reference-surplus-q55-bps-proof-v1.json | 2 +- crates/labcolors-core/src/lib.rs | 9 ++++++--- scripts/verify_point_support_surplus.py | 2 +- 3 files changed, 8 insertions(+), 5 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 63f62511..9772fe84 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":"f307eb96bab42262d1305f76a6e0890597d3c24b622828f8cbc21573e2f7da39","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":"a34371d8885a1d532e3be07ebf47d6383a7b584dd749f70d097c02cbcc7b8cc6","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":"d6d7e7230a39e4e86324a40972423cdac8309ed4a8590f2223cd219c0860d4e0"},{"kind":"rust-source","path":"crates/labcolors-core/src/composition.rs","sha256":"195a67327a3bd86d7816b634481389930bf68577bb1202fad14c2ea152df8625"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/exact.rs","sha256":"45bd09cd8f6f74860a0a3ffeb410ee2c3cf91b4868877abbc949e724b61e1ccd"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/mod.rs","sha256":"ad721c625b33a7432de44e5f63459a62c1cbc8ec2492855f783b5440f04978f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/wcag22.rs","sha256":"985e21abccd1fcb4a3f9624d5947ff3d1af7a1bf26b6b170ea8bbef77d6f1798"},{"kind":"rust-source","path":"crates/labcolors-core/src/hash.rs","sha256":"f97a0fd7d6ad3162f0f1dfb326fccfb7ed40da9a8fa67a5b8a239a1ae2ae49c3"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"57e4997f0b1cdcda3613e85cdb47ac656e39744f5d6c6e3c49f36e78b3701484"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"208066b255bc406303b18dc2eb6f1720c8203ea6eb37d225f9d8110c2449edc4"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"56c06b49f4687b3def69c247978da1b0cb45b42c09515a001d196cb1457c93a6"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"d4b06651e07cf4f93eccf9866f6e63c35ea72fa7125bdb41e222b19b70dd2f32"},{"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":33,"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":"d0c569a5ee322d48062ad185b40a7762a2b8881db8f77eaecdba71c768a8dcb4"} +{"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":"f4ee9b40639afe1dce44d913020f8ab8362bead7fb4c9767bbb43892fbbaa758","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":"953c895007e94cb82da759734d3eec44df813dad1726aee792c8c18cf0d3f9aa","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":"d6d7e7230a39e4e86324a40972423cdac8309ed4a8590f2223cd219c0860d4e0"},{"kind":"rust-source","path":"crates/labcolors-core/src/composition.rs","sha256":"195a67327a3bd86d7816b634481389930bf68577bb1202fad14c2ea152df8625"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/exact.rs","sha256":"45bd09cd8f6f74860a0a3ffeb410ee2c3cf91b4868877abbc949e724b61e1ccd"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/mod.rs","sha256":"ad721c625b33a7432de44e5f63459a62c1cbc8ec2492855f783b5440f04978f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/wcag22.rs","sha256":"985e21abccd1fcb4a3f9624d5947ff3d1af7a1bf26b6b170ea8bbef77d6f1798"},{"kind":"rust-source","path":"crates/labcolors-core/src/hash.rs","sha256":"f97a0fd7d6ad3162f0f1dfb326fccfb7ed40da9a8fa67a5b8a239a1ae2ae49c3"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"d87d4959e28556de54f815b7f833d5b9310cf4f4209916257e2d1ed8ad160810"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"208066b255bc406303b18dc2eb6f1720c8203ea6eb37d225f9d8110c2449edc4"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"56c06b49f4687b3def69c247978da1b0cb45b42c09515a001d196cb1457c93a6"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"d4b06651e07cf4f93eccf9866f6e63c35ea72fa7125bdb41e222b19b70dd2f32"},{"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":33,"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":"858cc8714aaacaa674f705ffaf5583f1018fad3d6442018be343d3c1af3a1a13"} diff --git a/crates/labcolors-core/src/lib.rs b/crates/labcolors-core/src/lib.rs index 9c6ef997..a6900d9b 100644 --- a/crates/labcolors-core/src/lib.rs +++ b/crates/labcolors-core/src/lib.rs @@ -53,9 +53,12 @@ pub(crate) mod point_support; ) )] pub(crate) mod program_session; -#[expect( - dead_code, - reason = "private F0 release registry precedes the atomic public hard cut" +#[cfg_attr( + not(test), + expect( + dead_code, + reason = "private F0 release registry precedes the atomic public hard cut" + ) )] pub(crate) mod release_registry; pub mod scale; diff --git a/scripts/verify_point_support_surplus.py b/scripts/verify_point_support_surplus.py index fe9e3659..64501972 100644 --- a/scripts/verify_point_support_surplus.py +++ b/scripts/verify_point_support_surplus.py @@ -57,7 +57,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 = ( - "a34371d8885a1d532e3be07ebf47d6383a7b584dd749f70d097c02cbcc7b8cc6" + "953c895007e94cb82da759734d3eec44df813dad1726aee792c8c18cf0d3f9aa" ) EXPECTED_Q55_PROOF_SHA256 = ( "ac59cf89503170c789223b91d775213a19d4e571ef930f2ea609fcd51b14defd" From f034c402e097784479e79a8bfb2d1954bc71cd1e Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Wed, 22 Jul 2026 14:09:22 +0300 Subject: [PATCH 14/58] fix(core): harden terminal Program admission --- crates/labcolors-core/src/program_session.rs | 50 ++++++-- .../src/program_session_tests.rs | 119 +++++++++++++++++- 2 files changed, 154 insertions(+), 15 deletions(-) diff --git a/crates/labcolors-core/src/program_session.rs b/crates/labcolors-core/src/program_session.rs index d0ce6e8e..3024e040 100644 --- a/crates/labcolors-core/src/program_session.rs +++ b/crates/labcolors-core/src/program_session.rs @@ -20,7 +20,6 @@ use std::mem; use std::rc::{Rc, Weak}; -use crate::Srgb8; use crate::appearance::{ AdmittedAppearanceBindings, AppearanceBindings, AppearanceGraphSpec, AppearanceWorkspace, BindingError, ColorInputId as AppearanceColorInputId, CompileError, CompiledAppearanceGraph, @@ -30,6 +29,7 @@ use crate::appearance::{ SurfaceInputPortId as AppearanceSurfaceInputId, SurfaceSpec as AppearanceSurfaceSpec, }; use crate::composition::CompositionProfileV1; +use crate::Srgb8; macro_rules! opaque_program_id { ($name:ident, $description:literal) => { @@ -439,16 +439,17 @@ fn try_zeroed_signal_words(len: usize) -> Result, PointRenderAttachErro } fn prepare_program(program: Program) -> Result { - let graph = lower_graph(&program) - .compile() - .map_err(|error| map_compile_error(&program, error))?; - if program.surface_inputs.is_empty() { return Err(ProgramCompileError::EmptySurfaceSchema); } if program.occurrences.is_empty() { return Err(ProgramCompileError::EmptyOccurrenceSet); } + check_render_node_count(program.surfaces.len(), program.occurrences.len())?; + + let graph = lower_graph(&program) + .compile() + .map_err(|error| map_compile_error(&program, error))?; let mut surface_inputs = Vec::new(); surface_inputs @@ -464,8 +465,11 @@ fn prepare_program(program: Program) -> Result Result Result<(), ProgramCompileError> { + surface_count + .checked_add(occurrence_count) + .ok_or(ProgramCompileError::ResourceExhausted) + .map(|_| ()) +} + +pub(crate) fn canonical_surface_input_sequence_matches( + actual: impl IntoIterator, + expected: &[SurfaceInputId], +) -> bool { + actual.into_iter().eq(expected + .iter() + .map(|input| AppearanceSurfaceInputId::new(input.value()))) +} + +pub(crate) fn canonical_occurrence_sequence_matches( + actual: impl IntoIterator, + expected: &[OccurrenceId], +) -> bool { + actual.into_iter().eq(expected + .iter() + .map(|occurrence| AppearanceOccurrenceId::new(occurrence.value()))) +} + fn lower_graph(program: &Program) -> AppearanceGraphSpec { AppearanceGraphSpec::new( program @@ -1194,10 +1226,10 @@ impl Session { self.apply_prepared(&epoch, prepared) } - fn apply_prepared<'input>( + fn apply_prepared( &mut self, epoch: &ProgramEpochV1, - prepared: PreparedEncodedSurfaceUpdateV1<'input>, + prepared: PreparedEncodedSurfaceUpdateV1<'_>, ) -> Result<&SessionState, PointRenderSessionUpdateErrorV1> { let incoming_revision = match &prepared { PreparedEncodedSurfaceUpdateV1::Unavailable(unavailable) => unavailable.revision, diff --git a/crates/labcolors-core/src/program_session_tests.rs b/crates/labcolors-core/src/program_session_tests.rs index 9bca9406..ffcf6315 100644 --- a/crates/labcolors-core/src/program_session_tests.rs +++ b/crates/labcolors-core/src/program_session_tests.rs @@ -1,12 +1,16 @@ -use crate::Srgb8; +use crate::appearance::{ + OccurrenceId as AppearanceOccurrenceId, SurfaceInputPortId as AppearanceSurfaceInputId, +}; use crate::program_session::{ - ColorInput, ColorInputId, CompiledProgram, CompositionProfile, Occurrence, OccurrenceId, - OpacityInput, OpacityInputId, PACKED_ENCODED_SURFACE_PRESENT_TAG_V1, + canonical_occurrence_sequence_matches, canonical_surface_input_sequence_matches, + check_render_node_count, ColorInput, ColorInputId, CompiledProgram, CompositionProfile, + Occurrence, OccurrenceId, OpacityInput, OpacityInputId, PackedEncodedSurfaceUpdateErrorV1, + Paint, PaintId, PointRenderOwner, PointRenderSessionUpdateErrorV1, Program, + ProgramCompileError, SessionState, SessionUpdateError, Surface, SurfaceId, SurfaceInputId, + SurfaceSignal, SurfaceUpdate, PACKED_ENCODED_SURFACE_PRESENT_TAG_V1, PACKED_ENCODED_SURFACE_UNAVAILABLE_TAG_V1, PACKED_ENCODED_SURFACE_UPDATE_MAGIC_V1, - PackedEncodedSurfaceUpdateErrorV1, Paint, PaintId, PointRenderOwner, - PointRenderSessionUpdateErrorV1, Program, ProgramCompileError, SessionState, - SessionUpdateError, Surface, SurfaceId, SurfaceInputId, SurfaceSignal, SurfaceUpdate, }; +use crate::Srgb8; const COLOR: ColorInputId = ColorInputId::new(1); const SURFACE_PORT: SurfaceInputId = SurfaceInputId::new(2); @@ -154,6 +158,109 @@ fn authored_and_runtime_values_preserve_exact_typed_bindings() { assert_eq!(signal.value(), surface_value); } +#[test] +fn empty_surface_schema_precedes_dangling_surface_input_analysis() { + let declaration = Program::new( + vec![ColorInput::new(COLOR, Srgb8::new([0; 3]))], + vec![], + vec![OpacityInput::new(OPACITY, 0.5)], + vec![Paint::Solid { + id: SOLID, + color: COLOR, + }], + vec![Surface::Input { + id: BACKDROP, + input: SURFACE_PORT, + }], + vec![Occurrence::new( + OCCURRENCE, + SOLID, + BACKDROP, + CompositionProfile::EncodedSrgb8SourceOverV1, + )], + ); + + assert_eq!( + declaration.compile().unwrap_err(), + ProgramCompileError::EmptySurfaceSchema + ); +} + +#[test] +fn empty_occurrence_set_precedes_dangling_occurrence_analysis() { + let declaration = Program::new( + vec![ColorInput::new(COLOR, Srgb8::new([0; 3]))], + vec![SURFACE_PORT], + vec![], + vec![Paint::Solid { + id: SOLID, + color: COLOR, + }], + vec![Surface::FromOccurrence { + id: VISIBLE, + occurrence: OCCURRENCE, + }], + vec![], + ); + + assert_eq!( + declaration.compile().unwrap_err(), + ProgramCompileError::EmptyOccurrenceSet + ); +} + +#[test] +fn combined_render_cardinality_overflow_is_resource_exhaustion() { + assert_eq!(check_render_node_count(usize::MAX - 1, 1), Ok(())); + assert_eq!( + check_render_node_count(usize::MAX, 1), + Err(ProgramCompileError::ResourceExhausted) + ); +} + +#[test] +fn canonical_sequence_firewall_rejects_reordering_relabeling_and_truncation() { + let surface_inputs = [SurfaceInputId::new(1), SurfaceInputId::new(2)]; + assert!(canonical_surface_input_sequence_matches( + [ + AppearanceSurfaceInputId::new(1), + AppearanceSurfaceInputId::new(2), + ], + &surface_inputs, + )); + assert!(!canonical_surface_input_sequence_matches( + [ + AppearanceSurfaceInputId::new(2), + AppearanceSurfaceInputId::new(1), + ], + &surface_inputs, + )); + assert!(!canonical_surface_input_sequence_matches( + [AppearanceSurfaceInputId::new(1)], + &surface_inputs, + )); + + let occurrences = [OccurrenceId::new(10), OccurrenceId::new(20)]; + assert!(canonical_occurrence_sequence_matches( + [ + AppearanceOccurrenceId::new(10), + AppearanceOccurrenceId::new(20), + ], + &occurrences, + )); + assert!(!canonical_occurrence_sequence_matches( + [ + AppearanceOccurrenceId::new(10), + AppearanceOccurrenceId::new(21), + ], + &occurrences, + )); + assert!(!canonical_occurrence_sequence_matches( + [AppearanceOccurrenceId::new(10)], + &occurrences, + )); +} + #[test] fn point_update_executes_the_compiled_graph_and_commits_compact_occurrences() { let owner = compiled(0.5).into_owner(); From 31acc78081e0b73d9cd391049e66c2447437e090 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Wed, 22 Jul 2026 14:16:56 +0300 Subject: [PATCH 15/58] style(core): match pinned Rust formatter --- crates/labcolors-core/src/program_session.rs | 2 +- .../labcolors-core/src/program_session_tests.rs | 15 ++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/crates/labcolors-core/src/program_session.rs b/crates/labcolors-core/src/program_session.rs index 3024e040..fc0d152a 100644 --- a/crates/labcolors-core/src/program_session.rs +++ b/crates/labcolors-core/src/program_session.rs @@ -20,6 +20,7 @@ use std::mem; use std::rc::{Rc, Weak}; +use crate::Srgb8; use crate::appearance::{ AdmittedAppearanceBindings, AppearanceBindings, AppearanceGraphSpec, AppearanceWorkspace, BindingError, ColorInputId as AppearanceColorInputId, CompileError, CompiledAppearanceGraph, @@ -29,7 +30,6 @@ use crate::appearance::{ SurfaceInputPortId as AppearanceSurfaceInputId, SurfaceSpec as AppearanceSurfaceSpec, }; use crate::composition::CompositionProfileV1; -use crate::Srgb8; macro_rules! opaque_program_id { ($name:ident, $description:literal) => { diff --git a/crates/labcolors-core/src/program_session_tests.rs b/crates/labcolors-core/src/program_session_tests.rs index ffcf6315..68263466 100644 --- a/crates/labcolors-core/src/program_session_tests.rs +++ b/crates/labcolors-core/src/program_session_tests.rs @@ -1,16 +1,17 @@ +use crate::Srgb8; use crate::appearance::{ OccurrenceId as AppearanceOccurrenceId, SurfaceInputPortId as AppearanceSurfaceInputId, }; use crate::program_session::{ - canonical_occurrence_sequence_matches, canonical_surface_input_sequence_matches, - check_render_node_count, ColorInput, ColorInputId, CompiledProgram, CompositionProfile, - Occurrence, OccurrenceId, OpacityInput, OpacityInputId, PackedEncodedSurfaceUpdateErrorV1, - Paint, PaintId, PointRenderOwner, PointRenderSessionUpdateErrorV1, Program, - ProgramCompileError, SessionState, SessionUpdateError, Surface, SurfaceId, SurfaceInputId, - SurfaceSignal, SurfaceUpdate, PACKED_ENCODED_SURFACE_PRESENT_TAG_V1, + ColorInput, ColorInputId, CompiledProgram, CompositionProfile, Occurrence, OccurrenceId, + OpacityInput, OpacityInputId, PACKED_ENCODED_SURFACE_PRESENT_TAG_V1, PACKED_ENCODED_SURFACE_UNAVAILABLE_TAG_V1, PACKED_ENCODED_SURFACE_UPDATE_MAGIC_V1, + PackedEncodedSurfaceUpdateErrorV1, Paint, PaintId, PointRenderOwner, + PointRenderSessionUpdateErrorV1, Program, ProgramCompileError, SessionState, + SessionUpdateError, Surface, SurfaceId, SurfaceInputId, SurfaceSignal, SurfaceUpdate, + canonical_occurrence_sequence_matches, canonical_surface_input_sequence_matches, + check_render_node_count, }; -use crate::Srgb8; const COLOR: ColorInputId = ColorInputId::new(1); const SURFACE_PORT: SurfaceInputId = SurfaceInputId::new(2); From 02f91a901f8692e637e871417ab691a1d16bd2dd Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Wed, 22 Jul 2026 14:21:57 +0300 Subject: [PATCH 16/58] feat(core): bind F0 release descriptors --- crates/labcolors-core/src/release_registry.rs | 427 ++++++++++++++++-- .../src/release_registry_tests.rs | 215 +++++++-- 2 files changed, 583 insertions(+), 59 deletions(-) diff --git a/crates/labcolors-core/src/release_registry.rs b/crates/labcolors-core/src/release_registry.rs index 6a3d47fc..e101b737 100644 --- a/crates/labcolors-core/src/release_registry.rs +++ b/crates/labcolors-core/src/release_registry.rs @@ -1,32 +1,43 @@ //! Minimal machine-readable registry for the F0 color-model releases. //! -//! Registry rows name only code releases that already exist. The absence of a -//! difference calibration is an explicit row, not a placeholder calibration. -//! No applicability, empirical validation, uncertainty or observer-study data -//! is inferred here. +//! Registry descriptors contain only facts fixed by the implemented code: +//! context consumption, admitted frame/domain, coordinate units, achromatic +//! law, formula/reference identity and typed release dependencies. The absence +//! of a difference calibration is an explicit keyless row, not a placeholder +//! calibration. No empirical applicability, validation, uncertainty or +//! observer-study data is inferred here. use crate::lcs_occurrence::{ - CAM16_VIEW_RELEASE_V1, Cam16ViewReleaseId, OKLAB_VIEW_RELEASE_V1, OklabViewReleaseId, + ADMITTED_SRGB8_TRISTIMULUS_BINDING_V1, AdmittedSrgb8TristimulusBindingV1, + AppearanceContextSchemaReleaseId, CAM16_VIEW_RELEASE_V1, Cam16ViewReleaseId, + ColorimetricFrameId, IEC_SRGB_D65_XYZ_FRAME_V1, OKLAB_VIEW_RELEASE_V1, OklabViewReleaseId, }; use crate::output_projection::{ - CSS_COLOR_4_OKLCH_D65_FROM_MODELED_SRGB8_SOLID_V1, DifferenceCalibrationReleaseIdV1, - OKLCH_VIEW_RELEASE_V1, OklchViewReleaseId, OutputProjectionReleaseIdV1, + CSS_COLOR_4_OKLCH_D65_FROM_MODELED_SRGB8_SOLID_V1, CssOklchHueSerializationReleaseIdV1, + CssOklchNumberEncodingReleaseIdV1, DifferenceCalibrationReleaseIdV1, OKLCH_VIEW_RELEASE_V1, + OklchViewReleaseId, OutputGamutTreatmentV1, OutputProjectionReleaseIdV1, }; pub(crate) const RELEASE_REGISTRY_SCHEMA_VERSION_V1: u16 = 1; +// Registered rows append a fixed-width 15-byte descriptor after the release +// key. The byte order is documented by `RegisteredReleaseDescriptorV1` below. const RELEASE_REGISTRY_CANONICAL_BYTES_V1: &[u8] = concat!( "labcolors.release-registry.canonical-binary.v1\0", "\0\x01\0\x05", "\x01\x01\0\x26cam16-li-et-al-2017-cie-248-forward-v1", + "\x01\x02\x01\x01\x01\x02\x02\x02\0\0\0\0\0\0\0", "\x01\x01\0\x24oklab-ottosson-2021-01-25-xyz-d65-v1", + "\x01\x01\0\x01\x01\x01\x01\x01\0\0\0\0\0\0\0", "\x01\x01\0\x27polar-from-ottosson-2021-01-25-oklab-v1", + "\x01\x01\0\x01\x02\x03\x03\x03\x01\0\x01\0\0\0\0", "\x02\0\0\0", "\x03\x01\0\x3acss-color-4-oklch-d65-from-modeled-iec61966-srgb8-solid-v1", + "\x01\x03\0\x01\x03\x04\x04\x04\x02\x01\x01\x01\x01\x01\x01", ) .as_bytes(); -const RELEASE_REGISTRY_FNV1A32_V1: u32 = 3_103_457_152; +const RELEASE_REGISTRY_FNV1A32_V1: u32 = 1_293_630_307; /// The disjoint kinds whose availability is reported by this registry. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -99,20 +110,371 @@ impl RegisteredColorReleaseIdV1 { } } +/// Whether and how a release consumes the occurrence's appearance context. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) enum ReleaseContextRequirementV1 { + NoAppearanceContextConsumptionV1, + ConsumesAppearanceContextV1(AppearanceContextSchemaReleaseId), + RetainsOccurrenceContextWithoutGeometryConsumptionV1, +} + +impl ReleaseContextRequirementV1 { + pub(crate) const fn schema_release(self) -> Option { + match self { + Self::ConsumesAppearanceContextV1(schema) => Some(schema), + Self::NoAppearanceContextConsumptionV1 + | Self::RetainsOccurrenceContextWithoutGeometryConsumptionV1 => None, + } + } + + const fn canonical_tag(self) -> u8 { + match self { + Self::NoAppearanceContextConsumptionV1 => 1, + Self::ConsumesAppearanceContextV1( + AppearanceContextSchemaReleaseId::Ciecam16ViewingInputsV1, + ) => 2, + Self::RetainsOccurrenceContextWithoutGeometryConsumptionV1 => 3, + } + } + + const fn schema_tag(self) -> u8 { + match self.schema_release() { + None => 0, + Some(AppearanceContextSchemaReleaseId::Ciecam16ViewingInputsV1) => 1, + } + } +} + +/// Exact colorimetric frame admitted by every current registered release. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) enum RegistryAdmittedFrameV1 { + Cie1931TwoDegreeXyzIecD65RelativeY1V1, +} + +impl RegistryAdmittedFrameV1 { + pub(crate) const fn frame(self) -> ColorimetricFrameId { + match self { + Self::Cie1931TwoDegreeXyzIecD65RelativeY1V1 => IEC_SRGB_D65_XYZ_FRAME_V1, + } + } + + const fn canonical_tag(self) -> u8 { + match self { + Self::Cie1931TwoDegreeXyzIecD65RelativeY1V1 => 1, + } + } +} + +/// Code-admitted input domain; this carries no empirical applicability claim. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) enum RegistryAdmittedDomainV1 { + FiniteNonNegativeXyzStimulusV1, + FiniteOklabRectangularViewV1, + ModeledIec61966Srgb8OccurrenceV1, +} + +impl RegistryAdmittedDomainV1 { + const fn canonical_tag(self) -> u8 { + match self { + Self::FiniteNonNegativeXyzStimulusV1 => 1, + Self::FiniteOklabRectangularViewV1 => 2, + Self::ModeledIec61966Srgb8OccurrenceV1 => 3, + } + } +} + +/// Coordinate/output units fixed by the registered code release. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) enum RegistryCoordinateUnitsV1 { + OklabCoordinatesUnitlessV1, + Cam16CorrelatesUnitlessHueDegreesV1, + OklchCoordinatesUnitlessHueDegreesV1, + CssColor4OklchPercentLightnessNumericChromaHueDegreesV1, +} + +impl RegistryCoordinateUnitsV1 { + const fn canonical_tag(self) -> u8 { + match self { + Self::OklabCoordinatesUnitlessV1 => 1, + Self::Cam16CorrelatesUnitlessHueDegreesV1 => 2, + Self::OklchCoordinatesUnitlessHueDegreesV1 => 3, + Self::CssColor4OklchPercentLightnessNumericChromaHueDegreesV1 => 4, + } + } +} + +/// Exact hue/achromatic behavior implemented by a release. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) enum RegistryAchromaticLawV1 { + NoHueCoordinateV1, + HueUndefinedExactlyWhenCam16MIsZeroV1, + HueUndefinedExactlyWhenOklabAAndBAreZeroV1, + ExactSourceGreyOrRectangularOriginSerializesHueZeroV1, +} + +impl RegistryAchromaticLawV1 { + const fn canonical_tag(self) -> u8 { + match self { + Self::NoHueCoordinateV1 => 1, + Self::HueUndefinedExactlyWhenCam16MIsZeroV1 => 2, + Self::HueUndefinedExactlyWhenOklabAAndBAreZeroV1 => 3, + Self::ExactSourceGreyOrRectangularOriginSerializesHueZeroV1 => 4, + } + } +} + +/// Formula/specification identity only, never empirical validation metadata. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) enum RegistryReferenceIdentityV1 { + Ottosson20210125OklabXyzD65V1, + LiEtAl2017Cie248Cam16ForwardV1, + Ottosson20210125OklabPolarV1, + CssColor4OklchD65V1, +} + +impl RegistryReferenceIdentityV1 { + const fn canonical_tag(self) -> u8 { + match self { + Self::Ottosson20210125OklabXyzD65V1 => 1, + Self::LiEtAl2017Cie248Cam16ForwardV1 => 2, + Self::Ottosson20210125OklabPolarV1 => 3, + Self::CssColor4OklchD65V1 => 4, + } + } +} + +/// Every typed edge executed by the current CSS output projection. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct OutputProjectionDependencyGraphV1 { + modeled_source_binding: AdmittedSrgb8TristimulusBindingV1, + oklab_view: OklabViewReleaseId, + oklch_view: OklchViewReleaseId, + number_encoding: CssOklchNumberEncodingReleaseIdV1, + hue_serialization: CssOklchHueSerializationReleaseIdV1, + gamut_treatment: OutputGamutTreatmentV1, +} + +impl OutputProjectionDependencyGraphV1 { + const fn registered_v1() -> Self { + Self { + modeled_source_binding: ADMITTED_SRGB8_TRISTIMULUS_BINDING_V1, + oklab_view: OKLAB_VIEW_RELEASE_V1, + oklch_view: OKLCH_VIEW_RELEASE_V1, + number_encoding: CssOklchNumberEncodingReleaseIdV1::LPercent5C6Hue3V1, + hue_serialization: + CssOklchHueSerializationReleaseIdV1::ExactSourceGreyOrRectangularOriginToZeroV1, + gamut_treatment: OutputGamutTreatmentV1::NoExplicitProjectionGamutMapV1, + } + } + + pub(crate) const fn modeled_source_binding(self) -> AdmittedSrgb8TristimulusBindingV1 { + self.modeled_source_binding + } + + pub(crate) const fn oklab_view(self) -> OklabViewReleaseId { + self.oklab_view + } + + pub(crate) const fn oklch_view(self) -> OklchViewReleaseId { + self.oklch_view + } + + pub(crate) const fn number_encoding(self) -> CssOklchNumberEncodingReleaseIdV1 { + self.number_encoding + } + + pub(crate) const fn hue_serialization(self) -> CssOklchHueSerializationReleaseIdV1 { + self.hue_serialization + } + + pub(crate) const fn gamut_treatment(self) -> OutputGamutTreatmentV1 { + self.gamut_treatment + } +} + +/// Typed release-to-release prerequisites; no free-form dependency keys exist. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) enum ReleaseDependencyGraphV1 { + DirectV1, + OklchPolarFromOklabV1(OklabViewReleaseId), + CssColor4OklchD65V1(OutputProjectionDependencyGraphV1), +} + +impl ReleaseDependencyGraphV1 { + /// Fixed canonical fields: + /// `[graph, source-binding, Oklab, Oklch, number, hue-policy, gamut-policy]`. + const fn canonical_fields(self) -> [u8; 7] { + match self { + Self::DirectV1 => [0; 7], + Self::OklchPolarFromOklabV1(OklabViewReleaseId::Ottosson20210125XyzD65V1) => { + [1, 0, 1, 0, 0, 0, 0] + } + Self::CssColor4OklchD65V1(graph) => [ + 2, + match graph.modeled_source_binding { + AdmittedSrgb8TristimulusBindingV1::Iec61966Srgb8ToCie1931TwoDegreeXyzD65RelativeY1V1 => 1, + }, + match graph.oklab_view { + OklabViewReleaseId::Ottosson20210125XyzD65V1 => 1, + }, + match graph.oklch_view { + OklchViewReleaseId::PolarFromOttosson20210125OklabV1 => 1, + }, + match graph.number_encoding { + CssOklchNumberEncodingReleaseIdV1::LPercent5C6Hue3V1 => 1, + }, + match graph.hue_serialization { + CssOklchHueSerializationReleaseIdV1::ExactSourceGreyOrRectangularOriginToZeroV1 => 1, + }, + match graph.gamut_treatment { + OutputGamutTreatmentV1::NoExplicitProjectionGamutMapV1 => 1, + }, + ], + } + } +} + +/// Complete code-truth descriptor for one registered release. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct RegisteredReleaseDescriptorV1 { + release: RegisteredColorReleaseIdV1, + context_requirement: ReleaseContextRequirementV1, + admitted_frame: RegistryAdmittedFrameV1, + admitted_domain: RegistryAdmittedDomainV1, + coordinate_units: RegistryCoordinateUnitsV1, + achromatic_law: RegistryAchromaticLawV1, + reference_identity: RegistryReferenceIdentityV1, + dependencies: ReleaseDependencyGraphV1, +} + +impl RegisteredReleaseDescriptorV1 { + pub(crate) const fn release(self) -> RegisteredColorReleaseIdV1 { + self.release + } + + pub(crate) const fn context_requirement(self) -> ReleaseContextRequirementV1 { + self.context_requirement + } + + pub(crate) const fn admitted_frame(self) -> RegistryAdmittedFrameV1 { + self.admitted_frame + } + + pub(crate) const fn admitted_domain(self) -> RegistryAdmittedDomainV1 { + self.admitted_domain + } + + pub(crate) const fn coordinate_units(self) -> RegistryCoordinateUnitsV1 { + self.coordinate_units + } + + pub(crate) const fn achromatic_law(self) -> RegistryAchromaticLawV1 { + self.achromatic_law + } + + pub(crate) const fn reference_identity(self) -> RegistryReferenceIdentityV1 { + self.reference_identity + } + + pub(crate) const fn dependencies(self) -> ReleaseDependencyGraphV1 { + self.dependencies + } + + /// Fixed-width descriptor bytes: + /// + /// `schema, context, context-schema, frame, domain, units, achromatic, + /// reference, dependency-graph, source-binding, Oklab, Oklch, number, + /// hue-policy, gamut-policy`. + pub(crate) const fn canonical_fields(self) -> [u8; 15] { + let dependencies = self.dependencies.canonical_fields(); + [ + 1, + self.context_requirement.canonical_tag(), + self.context_requirement.schema_tag(), + self.admitted_frame.canonical_tag(), + self.admitted_domain.canonical_tag(), + self.coordinate_units.canonical_tag(), + self.achromatic_law.canonical_tag(), + self.reference_identity.canonical_tag(), + dependencies[0], + dependencies[1], + dependencies[2], + dependencies[3], + dependencies[4], + dependencies[5], + dependencies[6], + ] + } +} + +const CAM16_VIEW_DESCRIPTOR_V1: RegisteredReleaseDescriptorV1 = RegisteredReleaseDescriptorV1 { + release: RegisteredColorReleaseIdV1::Cam16View(CAM16_VIEW_RELEASE_V1), + context_requirement: ReleaseContextRequirementV1::ConsumesAppearanceContextV1( + AppearanceContextSchemaReleaseId::Ciecam16ViewingInputsV1, + ), + admitted_frame: RegistryAdmittedFrameV1::Cie1931TwoDegreeXyzIecD65RelativeY1V1, + admitted_domain: RegistryAdmittedDomainV1::FiniteNonNegativeXyzStimulusV1, + coordinate_units: RegistryCoordinateUnitsV1::Cam16CorrelatesUnitlessHueDegreesV1, + achromatic_law: RegistryAchromaticLawV1::HueUndefinedExactlyWhenCam16MIsZeroV1, + reference_identity: RegistryReferenceIdentityV1::LiEtAl2017Cie248Cam16ForwardV1, + dependencies: ReleaseDependencyGraphV1::DirectV1, +}; + +const OKLAB_VIEW_DESCRIPTOR_V1: RegisteredReleaseDescriptorV1 = RegisteredReleaseDescriptorV1 { + release: RegisteredColorReleaseIdV1::OklabView(OKLAB_VIEW_RELEASE_V1), + context_requirement: ReleaseContextRequirementV1::NoAppearanceContextConsumptionV1, + admitted_frame: RegistryAdmittedFrameV1::Cie1931TwoDegreeXyzIecD65RelativeY1V1, + admitted_domain: RegistryAdmittedDomainV1::FiniteNonNegativeXyzStimulusV1, + coordinate_units: RegistryCoordinateUnitsV1::OklabCoordinatesUnitlessV1, + achromatic_law: RegistryAchromaticLawV1::NoHueCoordinateV1, + reference_identity: RegistryReferenceIdentityV1::Ottosson20210125OklabXyzD65V1, + dependencies: ReleaseDependencyGraphV1::DirectV1, +}; + +const OKLCH_VIEW_DESCRIPTOR_V1: RegisteredReleaseDescriptorV1 = RegisteredReleaseDescriptorV1 { + release: RegisteredColorReleaseIdV1::OklchView(OKLCH_VIEW_RELEASE_V1), + context_requirement: ReleaseContextRequirementV1::NoAppearanceContextConsumptionV1, + admitted_frame: RegistryAdmittedFrameV1::Cie1931TwoDegreeXyzIecD65RelativeY1V1, + admitted_domain: RegistryAdmittedDomainV1::FiniteOklabRectangularViewV1, + coordinate_units: RegistryCoordinateUnitsV1::OklchCoordinatesUnitlessHueDegreesV1, + achromatic_law: RegistryAchromaticLawV1::HueUndefinedExactlyWhenOklabAAndBAreZeroV1, + reference_identity: RegistryReferenceIdentityV1::Ottosson20210125OklabPolarV1, + dependencies: ReleaseDependencyGraphV1::OklchPolarFromOklabV1(OKLAB_VIEW_RELEASE_V1), +}; + +const OUTPUT_PROJECTION_DESCRIPTOR_V1: RegisteredReleaseDescriptorV1 = + RegisteredReleaseDescriptorV1 { + release: RegisteredColorReleaseIdV1::CssColor4OklchD65FromModeledSrgb8Solid( + CSS_COLOR_4_OKLCH_D65_FROM_MODELED_SRGB8_SOLID_V1, + ), + context_requirement: + ReleaseContextRequirementV1::RetainsOccurrenceContextWithoutGeometryConsumptionV1, + admitted_frame: RegistryAdmittedFrameV1::Cie1931TwoDegreeXyzIecD65RelativeY1V1, + admitted_domain: RegistryAdmittedDomainV1::ModeledIec61966Srgb8OccurrenceV1, + coordinate_units: + RegistryCoordinateUnitsV1::CssColor4OklchPercentLightnessNumericChromaHueDegreesV1, + achromatic_law: + RegistryAchromaticLawV1::ExactSourceGreyOrRectangularOriginSerializesHueZeroV1, + reference_identity: RegistryReferenceIdentityV1::CssColor4OklchD65V1, + dependencies: ReleaseDependencyGraphV1::CssColor4OklchD65V1( + OutputProjectionDependencyGraphV1::registered_v1(), + ), + }; + /// One closed registry row. /// -/// The unavailable variant carries no release identifier, so it cannot be -/// mistaken for an admitted difference formula. +/// The unavailable variant carries no descriptor or release identifier, so it +/// cannot be mistaken for an admitted difference formula. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub(crate) enum ReleaseRegistryRecordV1 { - Registered(RegisteredColorReleaseIdV1), + Registered(RegisteredReleaseDescriptorV1), DifferenceCalibrationUnavailable, } impl ReleaseRegistryRecordV1 { pub(crate) const fn class(self) -> ReleaseRegistryClassV1 { match self { - Self::Registered(release) => release.class(), + Self::Registered(descriptor) => descriptor.release().class(), Self::DifferenceCalibrationUnavailable => ReleaseRegistryClassV1::DifferenceCalibration, } } @@ -126,30 +488,27 @@ impl ReleaseRegistryRecordV1 { pub(crate) const fn release(self) -> Option { match self { - Self::Registered(release) => Some(release), + Self::Registered(descriptor) => Some(descriptor.release()), + Self::DifferenceCalibrationUnavailable => None, + } + } + + pub(crate) const fn descriptor(self) -> Option { + match self { + Self::Registered(descriptor) => Some(descriptor), Self::DifferenceCalibrationUnavailable => None, } } } -// Canonical order is class tag, then release-key bytes. An unavailable class -// has exactly one keyless row. +// Canonical order is class tag, then release-key bytes. An unavailable class +// has exactly one keyless and descriptor-less row. const RELEASE_REGISTRY_RECORDS_V1: [ReleaseRegistryRecordV1; 5] = [ - ReleaseRegistryRecordV1::Registered(RegisteredColorReleaseIdV1::Cam16View( - CAM16_VIEW_RELEASE_V1, - )), - ReleaseRegistryRecordV1::Registered(RegisteredColorReleaseIdV1::OklabView( - OKLAB_VIEW_RELEASE_V1, - )), - ReleaseRegistryRecordV1::Registered(RegisteredColorReleaseIdV1::OklchView( - OKLCH_VIEW_RELEASE_V1, - )), + ReleaseRegistryRecordV1::Registered(CAM16_VIEW_DESCRIPTOR_V1), + ReleaseRegistryRecordV1::Registered(OKLAB_VIEW_DESCRIPTOR_V1), + ReleaseRegistryRecordV1::Registered(OKLCH_VIEW_DESCRIPTOR_V1), ReleaseRegistryRecordV1::DifferenceCalibrationUnavailable, - ReleaseRegistryRecordV1::Registered( - RegisteredColorReleaseIdV1::CssColor4OklchD65FromModeledSrgb8Solid( - CSS_COLOR_4_OKLCH_D65_FROM_MODELED_SRGB8_SOLID_V1, - ), - ), + ReleaseRegistryRecordV1::Registered(OUTPUT_PROJECTION_DESCRIPTOR_V1), ]; pub(crate) const fn release_registry_records_v1() -> &'static [ReleaseRegistryRecordV1] { @@ -166,9 +525,11 @@ pub(crate) const fn impossible_difference_calibration_release_v1( /// Canonical binary encoding of the complete registry. /// /// Layout is `magic || schema:u16be || rows:u16be`, followed by rows in the -/// declared canonical order. Every row is -/// `class:u8 || availability:u8 || key_length:u16be || key:utf8`; unavailable -/// difference calibration has a zero key length. +/// declared canonical order. Every row starts with +/// `class:u8 || availability:u8 || key_length:u16be || key:utf8`. A registered +/// row then carries the 15 descriptor bytes documented by +/// [`RegisteredReleaseDescriptorV1::canonical_fields`]. The unavailable +/// difference row has a zero key length and no descriptor bytes. pub(crate) const fn release_registry_canonical_bytes_v1() -> &'static [u8] { RELEASE_REGISTRY_CANONICAL_BYTES_V1 } @@ -206,7 +567,7 @@ impl ReleaseRegistryDigestV1 { } } -pub(crate) fn release_registry_digest_v1() -> ReleaseRegistryDigestV1 { +pub(crate) const fn release_registry_digest_v1() -> ReleaseRegistryDigestV1 { ReleaseRegistryDigestV1 { algorithm: ReleaseRegistryDigestAlgorithmV1::Fnv1a32V1, value: RELEASE_REGISTRY_FNV1A32_V1, diff --git a/crates/labcolors-core/src/release_registry_tests.rs b/crates/labcolors-core/src/release_registry_tests.rs index 006f18e5..1f1c40d0 100644 --- a/crates/labcolors-core/src/release_registry_tests.rs +++ b/crates/labcolors-core/src/release_registry_tests.rs @@ -1,41 +1,51 @@ -use crate::lcs_occurrence::{CAM16_VIEW_RELEASE_V1, OKLAB_VIEW_RELEASE_V1}; +use crate::lcs_occurrence::{ + ADMITTED_SRGB8_TRISTIMULUS_BINDING_V1, AppearanceContextSchemaReleaseId, CAM16_VIEW_RELEASE_V1, + IEC_SRGB_D65_XYZ_FRAME_V1, OKLAB_VIEW_RELEASE_V1, +}; use crate::output_projection::{ - CSS_COLOR_4_OKLCH_D65_FROM_MODELED_SRGB8_SOLID_V1, DifferenceCalibrationReleaseIdV1, - OKLCH_VIEW_RELEASE_V1, + CSS_COLOR_4_OKLCH_D65_FROM_MODELED_SRGB8_SOLID_V1, CssOklchHueSerializationReleaseIdV1, + CssOklchNumberEncodingReleaseIdV1, DifferenceCalibrationReleaseIdV1, OKLCH_VIEW_RELEASE_V1, + OutputGamutTreatmentV1, }; use crate::release_registry::{ - RELEASE_REGISTRY_SCHEMA_VERSION_V1, RegisteredColorReleaseIdV1, ReleaseRegistryAvailabilityV1, - ReleaseRegistryClassV1, ReleaseRegistryDigestAlgorithmV1, ReleaseRegistryRecordV1, + RELEASE_REGISTRY_SCHEMA_VERSION_V1, RegisteredColorReleaseIdV1, RegisteredReleaseDescriptorV1, + RegistryAchromaticLawV1, RegistryAdmittedDomainV1, RegistryAdmittedFrameV1, + RegistryCoordinateUnitsV1, RegistryReferenceIdentityV1, ReleaseContextRequirementV1, + ReleaseDependencyGraphV1, ReleaseRegistryAvailabilityV1, ReleaseRegistryClassV1, + ReleaseRegistryDigestAlgorithmV1, ReleaseRegistryRecordV1, impossible_difference_calibration_release_v1, release_registry_canonical_bytes_v1, release_registry_digest_v1, release_registry_records_v1, }; +fn descriptor(release: RegisteredColorReleaseIdV1) -> RegisteredReleaseDescriptorV1 { + release_registry_records_v1() + .iter() + .filter_map(|record| record.descriptor()) + .find(|descriptor| descriptor.release() == release) + .expect("registered release descriptor") +} + #[test] fn registry_contains_only_the_four_implemented_releases() { + let releases: Vec<_> = release_registry_records_v1() + .iter() + .filter_map(|record| record.release()) + .collect(); assert_eq!( - release_registry_records_v1(), - &[ - ReleaseRegistryRecordV1::Registered(RegisteredColorReleaseIdV1::Cam16View( - CAM16_VIEW_RELEASE_V1, - )), - ReleaseRegistryRecordV1::Registered(RegisteredColorReleaseIdV1::OklabView( - OKLAB_VIEW_RELEASE_V1, - )), - ReleaseRegistryRecordV1::Registered(RegisteredColorReleaseIdV1::OklchView( - OKLCH_VIEW_RELEASE_V1, - )), - ReleaseRegistryRecordV1::DifferenceCalibrationUnavailable, - ReleaseRegistryRecordV1::Registered( - RegisteredColorReleaseIdV1::CssColor4OklchD65FromModeledSrgb8Solid( - CSS_COLOR_4_OKLCH_D65_FROM_MODELED_SRGB8_SOLID_V1, - ), + releases, + [ + RegisteredColorReleaseIdV1::Cam16View(CAM16_VIEW_RELEASE_V1), + RegisteredColorReleaseIdV1::OklabView(OKLAB_VIEW_RELEASE_V1), + RegisteredColorReleaseIdV1::OklchView(OKLCH_VIEW_RELEASE_V1), + RegisteredColorReleaseIdV1::CssColor4OklchD65FromModeledSrgb8Solid( + CSS_COLOR_4_OKLCH_D65_FROM_MODELED_SRGB8_SOLID_V1, ), ], ); } #[test] -fn difference_registry_is_explicitly_unavailable_and_release_type_is_uninhabited() { +fn difference_registry_is_explicitly_unavailable_and_has_no_fake_descriptor() { let row = release_registry_records_v1() .iter() .copied() @@ -50,10 +60,153 @@ fn difference_registry_is_explicitly_unavailable_and_release_type_is_uninhabited ReleaseRegistryAvailabilityV1::Unavailable ); assert_eq!(row.release(), None); + assert_eq!(row.descriptor(), None); let _: fn(DifferenceCalibrationReleaseIdV1) -> ! = impossible_difference_calibration_release_v1; } +#[test] +fn appearance_descriptors_pin_only_code_owned_domain_units_hue_and_reference_facts() { + let cam16 = descriptor(RegisteredColorReleaseIdV1::Cam16View(CAM16_VIEW_RELEASE_V1)); + assert_eq!( + cam16.context_requirement(), + ReleaseContextRequirementV1::ConsumesAppearanceContextV1( + AppearanceContextSchemaReleaseId::Ciecam16ViewingInputsV1, + ), + ); + assert_eq!( + cam16.context_requirement().schema_release(), + Some(AppearanceContextSchemaReleaseId::Ciecam16ViewingInputsV1), + ); + assert_eq!( + cam16.admitted_frame(), + RegistryAdmittedFrameV1::Cie1931TwoDegreeXyzIecD65RelativeY1V1, + ); + assert_eq!(cam16.admitted_frame().frame(), IEC_SRGB_D65_XYZ_FRAME_V1); + assert_eq!( + cam16.admitted_domain(), + RegistryAdmittedDomainV1::FiniteNonNegativeXyzStimulusV1, + ); + assert_eq!( + cam16.coordinate_units(), + RegistryCoordinateUnitsV1::Cam16CorrelatesUnitlessHueDegreesV1, + ); + assert_eq!( + cam16.achromatic_law(), + RegistryAchromaticLawV1::HueUndefinedExactlyWhenCam16MIsZeroV1, + ); + assert_eq!( + cam16.reference_identity(), + RegistryReferenceIdentityV1::LiEtAl2017Cie248Cam16ForwardV1, + ); + assert_eq!(cam16.dependencies(), ReleaseDependencyGraphV1::DirectV1); + + let oklab = descriptor(RegisteredColorReleaseIdV1::OklabView(OKLAB_VIEW_RELEASE_V1)); + assert_eq!( + oklab.context_requirement(), + ReleaseContextRequirementV1::NoAppearanceContextConsumptionV1, + ); + assert_eq!(oklab.context_requirement().schema_release(), None); + assert_eq!(oklab.admitted_frame().frame(), IEC_SRGB_D65_XYZ_FRAME_V1); + assert_eq!( + oklab.admitted_domain(), + RegistryAdmittedDomainV1::FiniteNonNegativeXyzStimulusV1, + ); + assert_eq!( + oklab.coordinate_units(), + RegistryCoordinateUnitsV1::OklabCoordinatesUnitlessV1, + ); + assert_eq!( + oklab.achromatic_law(), + RegistryAchromaticLawV1::NoHueCoordinateV1, + ); + assert_eq!( + oklab.reference_identity(), + RegistryReferenceIdentityV1::Ottosson20210125OklabXyzD65V1, + ); + assert_eq!(oklab.dependencies(), ReleaseDependencyGraphV1::DirectV1); + + let oklch = descriptor(RegisteredColorReleaseIdV1::OklchView(OKLCH_VIEW_RELEASE_V1)); + assert_eq!( + oklch.context_requirement(), + ReleaseContextRequirementV1::NoAppearanceContextConsumptionV1, + ); + assert_eq!(oklch.admitted_frame().frame(), IEC_SRGB_D65_XYZ_FRAME_V1); + assert_eq!( + oklch.admitted_domain(), + RegistryAdmittedDomainV1::FiniteOklabRectangularViewV1, + ); + assert_eq!( + oklch.coordinate_units(), + RegistryCoordinateUnitsV1::OklchCoordinatesUnitlessHueDegreesV1, + ); + assert_eq!( + oklch.achromatic_law(), + RegistryAchromaticLawV1::HueUndefinedExactlyWhenOklabAAndBAreZeroV1, + ); + assert_eq!( + oklch.reference_identity(), + RegistryReferenceIdentityV1::Ottosson20210125OklabPolarV1, + ); + assert_eq!( + oklch.dependencies(), + ReleaseDependencyGraphV1::OklchPolarFromOklabV1(OKLAB_VIEW_RELEASE_V1), + ); +} + +#[test] +fn output_descriptor_binds_the_complete_typed_projection_dependency_chain() { + let output = descriptor( + RegisteredColorReleaseIdV1::CssColor4OklchD65FromModeledSrgb8Solid( + CSS_COLOR_4_OKLCH_D65_FROM_MODELED_SRGB8_SOLID_V1, + ), + ); + assert_eq!( + output.context_requirement(), + ReleaseContextRequirementV1::RetainsOccurrenceContextWithoutGeometryConsumptionV1, + ); + assert_eq!(output.context_requirement().schema_release(), None); + assert_eq!(output.admitted_frame().frame(), IEC_SRGB_D65_XYZ_FRAME_V1); + assert_eq!( + output.admitted_domain(), + RegistryAdmittedDomainV1::ModeledIec61966Srgb8OccurrenceV1, + ); + assert_eq!( + output.coordinate_units(), + RegistryCoordinateUnitsV1::CssColor4OklchPercentLightnessNumericChromaHueDegreesV1, + ); + assert_eq!( + output.achromatic_law(), + RegistryAchromaticLawV1::ExactSourceGreyOrRectangularOriginSerializesHueZeroV1, + ); + assert_eq!( + output.reference_identity(), + RegistryReferenceIdentityV1::CssColor4OklchD65V1, + ); + + let ReleaseDependencyGraphV1::CssColor4OklchD65V1(dependencies) = output.dependencies() else { + panic!("output descriptor must carry its typed dependency graph"); + }; + assert_eq!( + dependencies.modeled_source_binding(), + ADMITTED_SRGB8_TRISTIMULUS_BINDING_V1, + ); + assert_eq!(dependencies.oklab_view(), OKLAB_VIEW_RELEASE_V1); + assert_eq!(dependencies.oklch_view(), OKLCH_VIEW_RELEASE_V1); + assert_eq!( + dependencies.number_encoding(), + CssOklchNumberEncodingReleaseIdV1::LPercent5C6Hue3V1, + ); + assert_eq!( + dependencies.hue_serialization(), + CssOklchHueSerializationReleaseIdV1::ExactSourceGreyOrRectangularOriginToZeroV1, + ); + assert_eq!( + dependencies.gamut_treatment(), + OutputGamutTreatmentV1::NoExplicitProjectionGamutMapV1, + ); +} + #[test] fn registered_rows_have_stable_exact_release_keys() { let keys: Vec<_> = release_registry_records_v1() @@ -72,7 +225,7 @@ fn registered_rows_have_stable_exact_release_keys() { } #[test] -fn canonical_bytes_pin_schema_order_availability_and_keys() { +fn canonical_bytes_pin_schema_rows_descriptors_dependencies_and_order() { assert_eq!(RELEASE_REGISTRY_SCHEMA_VERSION_V1, 1); assert_eq!( release_registry_canonical_bytes_v1(), @@ -80,17 +233,21 @@ fn canonical_bytes_pin_schema_order_availability_and_keys() { "labcolors.release-registry.canonical-binary.v1\0", "\0\x01\0\x05", "\x01\x01\0\x26cam16-li-et-al-2017-cie-248-forward-v1", + "\x01\x02\x01\x01\x01\x02\x02\x02\0\0\0\0\0\0\0", "\x01\x01\0\x24oklab-ottosson-2021-01-25-xyz-d65-v1", + "\x01\x01\0\x01\x01\x01\x01\x01\0\0\0\0\0\0\0", "\x01\x01\0\x27polar-from-ottosson-2021-01-25-oklab-v1", + "\x01\x01\0\x01\x02\x03\x03\x03\x01\0\x01\0\0\0\0", "\x02\0\0\0", "\x03\x01\0\x3acss-color-4-oklch-d65-from-modeled-iec61966-srgb8-solid-v1", + "\x01\x03\0\x01\x03\x04\x04\x04\x02\x01\x01\x01\x01\x01\x01", ) .as_bytes(), ); } #[test] -fn canonical_bytes_encode_every_typed_registry_row_in_order() { +fn canonical_bytes_encode_every_typed_registry_row_and_descriptor_in_order() { const MAGIC: &[u8] = b"labcolors.release-registry.canonical-binary.v1\0"; let bytes = release_registry_canonical_bytes_v1(); @@ -121,13 +278,19 @@ fn canonical_bytes_encode_every_typed_registry_row_in_order() { assert_eq!(key_length, expected_key.len()); assert_eq!(&bytes[cursor..cursor + key_length], expected_key); cursor += key_length; + + if let Some(descriptor) = record.descriptor() { + let fields = descriptor.canonical_fields(); + assert_eq!(&bytes[cursor..cursor + fields.len()], fields); + cursor += fields.len(); + } } assert_eq!(cursor, bytes.len()); } #[test] -fn digest_names_its_non_cryptographic_algorithm_and_binds_canonical_bytes() { +fn digest_names_its_non_cryptographic_algorithm_and_covers_descriptor_bytes() { let digest = release_registry_digest_v1(); assert_eq!( digest.algorithm(), @@ -138,5 +301,5 @@ fn digest_names_its_non_cryptographic_algorithm_and_binds_canonical_bytes() { digest.value(), crate::fnv1a_32(release_registry_canonical_bytes_v1()), ); - assert_eq!(digest.value(), 3_103_457_152); + assert_eq!(digest.value(), 1_293_630_307); } From e6e2b290bae7266dbf699d634ec741a390b5b06c Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Wed, 22 Jul 2026 14:28:24 +0300 Subject: [PATCH 17/58] perf(core): add linear canonical surface ingestion --- ...rt-reference-surplus-q55-bps-proof-v1.json | 2 +- crates/labcolors-core/src/appearance.rs | 32 ++++++ .../src/appearance_graph_tests.rs | 105 +++++++++++++++++- scripts/verify_point_support_surplus.py | 2 +- 4 files changed, 136 insertions(+), 5 deletions(-) mode change 100644 => 100755 scripts/verify_point_support_surplus.py 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 9772fe84..ffdcd34d 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":"f4ee9b40639afe1dce44d913020f8ab8362bead7fb4c9767bbb43892fbbaa758","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":"953c895007e94cb82da759734d3eec44df813dad1726aee792c8c18cf0d3f9aa","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":"d6d7e7230a39e4e86324a40972423cdac8309ed4a8590f2223cd219c0860d4e0"},{"kind":"rust-source","path":"crates/labcolors-core/src/composition.rs","sha256":"195a67327a3bd86d7816b634481389930bf68577bb1202fad14c2ea152df8625"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/exact.rs","sha256":"45bd09cd8f6f74860a0a3ffeb410ee2c3cf91b4868877abbc949e724b61e1ccd"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/mod.rs","sha256":"ad721c625b33a7432de44e5f63459a62c1cbc8ec2492855f783b5440f04978f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/wcag22.rs","sha256":"985e21abccd1fcb4a3f9624d5947ff3d1af7a1bf26b6b170ea8bbef77d6f1798"},{"kind":"rust-source","path":"crates/labcolors-core/src/hash.rs","sha256":"f97a0fd7d6ad3162f0f1dfb326fccfb7ed40da9a8fa67a5b8a239a1ae2ae49c3"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"d87d4959e28556de54f815b7f833d5b9310cf4f4209916257e2d1ed8ad160810"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"208066b255bc406303b18dc2eb6f1720c8203ea6eb37d225f9d8110c2449edc4"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"56c06b49f4687b3def69c247978da1b0cb45b42c09515a001d196cb1457c93a6"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"d4b06651e07cf4f93eccf9866f6e63c35ea72fa7125bdb41e222b19b70dd2f32"},{"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":33,"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":"858cc8714aaacaa674f705ffaf5583f1018fad3d6442018be343d3c1af3a1a13"} +{"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":"5a949a5b7bfe37fdbaf113989d2d50f9556a14f94867458ef2ee0a87e6a8e941","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":"e417c266e0889c839540cc1e20b947e4d3bf7e4a422900b381f74ca006144c51","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":"1142a30888565a629e8597436d3f68e43e90223e1aa91265db839b2deaee49eb"},{"kind":"rust-source","path":"crates/labcolors-core/src/composition.rs","sha256":"195a67327a3bd86d7816b634481389930bf68577bb1202fad14c2ea152df8625"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/exact.rs","sha256":"45bd09cd8f6f74860a0a3ffeb410ee2c3cf91b4868877abbc949e724b61e1ccd"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/mod.rs","sha256":"ad721c625b33a7432de44e5f63459a62c1cbc8ec2492855f783b5440f04978f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/wcag22.rs","sha256":"985e21abccd1fcb4a3f9624d5947ff3d1af7a1bf26b6b170ea8bbef77d6f1798"},{"kind":"rust-source","path":"crates/labcolors-core/src/hash.rs","sha256":"f97a0fd7d6ad3162f0f1dfb326fccfb7ed40da9a8fa67a5b8a239a1ae2ae49c3"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"d87d4959e28556de54f815b7f833d5b9310cf4f4209916257e2d1ed8ad160810"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"208066b255bc406303b18dc2eb6f1720c8203ea6eb37d225f9d8110c2449edc4"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"56c06b49f4687b3def69c247978da1b0cb45b42c09515a001d196cb1457c93a6"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"d4b06651e07cf4f93eccf9866f6e63c35ea72fa7125bdb41e222b19b70dd2f32"},{"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":33,"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":"48d3115e201d46c2b4564860d817e6a1cb45182b9de05da5f984c13f6251c47b"} diff --git a/crates/labcolors-core/src/appearance.rs b/crates/labcolors-core/src/appearance.rs index 04ff10fe..0873183e 100644 --- a/crates/labcolors-core/src/appearance.rs +++ b/crates/labcolors-core/src/appearance.rs @@ -1076,6 +1076,38 @@ impl AdmittedAppearanceBindings { Ok(()) } + /// Overwrite the complete canonical Surface-input slice from one borrowed + /// value source. + /// + /// The exact typed schema is checked in full before `value_at` can run or + /// any admitted value can change. After that O(N) preflight, `value_at` is + /// called exactly once per canonical input and every destination value is + /// overwritten exactly once. Neither pass needs lookup or allocation. + pub(crate) fn overwrite_surface_inputs_canonical( + &mut self, + expected_inputs: impl IntoIterator, + mut value_at: impl FnMut(usize) -> Srgb8, + ) -> Result<(), BindingError> { + if !expected_inputs + .into_iter() + .eq(self.surfaces.iter().map(|(input, _)| *input)) + { + return Err(BindingError::IncompatibleAdmittedBindings); + } + + for (index, (_, value)) in self.surfaces.iter_mut().enumerate() { + *value = value_at(index); + } + Ok(()) + } + + /// Borrow the admitted Surface-input slice in its exact canonical order. + pub(crate) fn surface_inputs_canonical( + &self, + ) -> impl ExactSizeIterator + '_ { + self.surfaces.iter().copied() + } + #[cfg(test)] pub(crate) fn opacity_bits(&self, input: OpacityInputId) -> Option { self.opacities diff --git a/crates/labcolors-core/src/appearance_graph_tests.rs b/crates/labcolors-core/src/appearance_graph_tests.rs index 0bff3090..11e4355d 100644 --- a/crates/labcolors-core/src/appearance_graph_tests.rs +++ b/crates/labcolors-core/src/appearance_graph_tests.rs @@ -5,13 +5,15 @@ //! `surfaceFrom` лишь даёт видимому результату повторно используемую identity. //! Клиентский словарь и perception-утверждения сюда не входят. +use std::cell::Cell; + use proptest::prelude::*; use crate::Srgb8; use crate::appearance::{ - AppearanceBindings, AppearanceGraphSpec, BindingError, ColorInputId, CompileError, - CompositionProfileV1, EncodedPointPaintV1, OccurrenceId, OccurrenceSpec, OpacityInputId, - PaintId, PaintSpec, SurfaceId, SurfaceInputPortId, SurfaceSpec, + AdmittedAppearanceBindings, AppearanceBindings, AppearanceGraphSpec, BindingError, + ColorInputId, CompileError, CompositionProfileV1, EncodedPointPaintV1, OccurrenceId, + OccurrenceSpec, OpacityInputId, PaintId, PaintSpec, SurfaceId, SurfaceInputPortId, SurfaceSpec, }; use crate::constraints::Evaluator; @@ -80,6 +82,28 @@ fn bindings(source: [u8; 3], opacity: f64, context: [u8; 3]) -> AppearanceBindin ) } +fn admitted_surface_triplet() -> AdmittedAppearanceBindings { + let inputs = [ + SurfaceInputPortId::new(30), + SurfaceInputPortId::new(10), + SurfaceInputPortId::new(20), + ]; + let graph = AppearanceGraphSpec::new(vec![], inputs.to_vec(), vec![], vec![], vec![], vec![]) + .compile() + .unwrap(); + graph + .admit_bindings(&AppearanceBindings::new( + vec![], + vec![ + (inputs[0], Srgb8::new([30; 3])), + (inputs[1], Srgb8::new([10; 3])), + (inputs[2], Srgb8::new([20; 3])), + ], + vec![], + )) + .unwrap() +} + #[test] fn static_exact_program_is_declarative_topology_plus_typed_constraint() { let compiled = crate::appearance::point_opacity_over_surface_declarative_spec() @@ -1493,6 +1517,81 @@ fn signed_zero_opacity_has_one_canonical_state() { ); } +#[test] +fn canonical_surface_overwrite_rejects_every_schema_drift_before_read_or_mutation() { + let mut admitted = admitted_surface_triplet(); + let first = SurfaceInputPortId::new(10); + let second = SurfaceInputPortId::new(20); + let third = SurfaceInputPortId::new(30); + let original = [ + (first, Srgb8::new([10; 3])), + (second, Srgb8::new([20; 3])), + (third, Srgb8::new([30; 3])), + ]; + let reordered = [second, first, third]; + let relabelled = [first, SurfaceInputPortId::new(21), third]; + let truncated = [first, second]; + let extended = [first, second, third, SurfaceInputPortId::new(40)]; + + for expected in [ + reordered.as_slice(), + relabelled.as_slice(), + truncated.as_slice(), + extended.as_slice(), + ] { + let reads = Cell::new(0); + let (result, allocations) = crate::test_support::measured_allocations(|| { + admitted.overwrite_surface_inputs_canonical(expected.iter().copied(), |_| { + reads.set(reads.get() + 1); + Srgb8::new([0; 3]) + }) + }); + + assert_eq!(result, Err(BindingError::IncompatibleAdmittedBindings)); + assert_eq!(reads.get(), 0); + assert_eq!(allocations, 0); + assert!(admitted.surface_inputs_canonical().eq(original)); + } +} + +#[test] +fn canonical_surface_overwrite_reads_once_and_exposes_all_values_without_allocation() { + let mut admitted = admitted_surface_triplet(); + let expected_inputs = [ + SurfaceInputPortId::new(10), + SurfaceInputPortId::new(20), + SurfaceInputPortId::new(30), + ]; + + admitted + .overwrite_surface_inputs_canonical(expected_inputs, |index| Srgb8::new([index as u8; 3])) + .unwrap(); + + let values = [ + Srgb8::new([101, 102, 103]), + Srgb8::new([111, 112, 113]), + Srgb8::new([121, 122, 123]), + ]; + let reads = Cell::new([0_usize; 3]); + let (result, allocations) = crate::test_support::measured_allocations(|| { + admitted.overwrite_surface_inputs_canonical(expected_inputs, |index| { + let mut counts = reads.get(); + counts[index] += 1; + reads.set(counts); + values[index] + })?; + Ok::<_, BindingError>( + admitted + .surface_inputs_canonical() + .eq(expected_inputs.into_iter().zip(values)), + ) + }); + + assert_eq!(result, Ok(true)); + assert_eq!(reads.get(), [1, 1, 1]); + assert_eq!(allocations, 0); +} + #[test] fn admitted_bindings_and_workspace_are_reused_without_storage_churn() { let graph = point_component(false, false).compile().unwrap(); diff --git a/scripts/verify_point_support_surplus.py b/scripts/verify_point_support_surplus.py old mode 100644 new mode 100755 index 64501972..2f08093f --- a/scripts/verify_point_support_surplus.py +++ b/scripts/verify_point_support_surplus.py @@ -57,7 +57,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 = ( - "953c895007e94cb82da759734d3eec44df813dad1726aee792c8c18cf0d3f9aa" + "e417c266e0889c839540cc1e20b947e4d3bf7e4a422900b381f74ca006144c51" ) EXPECTED_Q55_PROOF_SHA256 = ( "ac59cf89503170c789223b91d775213a19d4e571ef930f2ea609fcd51b14defd" From 01ed02c7edc64ac73773b88e6caedce45e6fc776 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Wed, 22 Jul 2026 14:44:45 +0300 Subject: [PATCH 18/58] refactor(core): unify terminal physical identities --- crates/labcolors-core/src/program_session.rs | 486 ++++-------------- .../src/program_session_tests.rs | 67 +-- 2 files changed, 141 insertions(+), 412 deletions(-) diff --git a/crates/labcolors-core/src/program_session.rs b/crates/labcolors-core/src/program_session.rs index fc0d152a..6f01e321 100644 --- a/crates/labcolors-core/src/program_session.rs +++ b/crates/labcolors-core/src/program_session.rs @@ -23,47 +23,11 @@ use std::rc::{Rc, Weak}; use crate::Srgb8; use crate::appearance::{ AdmittedAppearanceBindings, AppearanceBindings, AppearanceGraphSpec, AppearanceWorkspace, - BindingError, ColorInputId as AppearanceColorInputId, CompileError, CompiledAppearanceGraph, - OccurrenceId as AppearanceOccurrenceId, OccurrenceSpec as AppearanceOccurrenceSpec, - OpacityInputId as AppearanceOpacityInputId, PaintId as AppearancePaintId, - PaintSpec as AppearancePaintSpec, SurfaceId as AppearanceSurfaceId, - SurfaceInputPortId as AppearanceSurfaceInputId, SurfaceSpec as AppearanceSurfaceSpec, + BindingError, ColorInputId, CompileError, CompiledAppearanceGraph, OccurrenceId, + OccurrenceSpec, OpacityInputId, PaintId, PaintSpec, SurfaceId, SurfaceInputPortId, SurfaceSpec, }; use crate::composition::CompositionProfileV1; -macro_rules! opaque_program_id { - ($name:ident, $description:literal) => { - #[doc = $description] - #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] - pub struct $name(u32); - - impl $name { - /// Construct one client-owned opaque identity. - pub const fn new(raw: u32) -> Self { - Self(raw) - } - - /// Return the exact transport identity. - pub const fn value(self) -> u32 { - self.0 - } - } - }; -} - -opaque_program_id!( - ColorInputId, - "Identity of one immutable encoded colour input." -); -opaque_program_id!( - SurfaceInputId, - "Identity of one runtime encoded Surface input." -); -opaque_program_id!(OpacityInputId, "Identity of one immutable opacity input."); -opaque_program_id!(PaintId, "Identity of one Paint node."); -opaque_program_id!(SurfaceId, "Identity of one Surface node."); -opaque_program_id!(OccurrenceId, "Identity of one Paint-on-Surface occurrence."); - /// One immutable encoded colour binding owned by a [`Program`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ColorInput { @@ -133,7 +97,7 @@ pub enum Surface { /// Read one revision-bound runtime point input. Input { id: SurfaceId, - input: SurfaceInputId, + input: SurfaceInputPortId, }, /// Give a visible occurrence result a Surface identity for nesting. FromOccurrence { @@ -203,7 +167,7 @@ impl Occurrence { #[derive(Debug, Clone, PartialEq)] pub struct Program { colors: Vec, - surface_inputs: Vec, + surface_input_ports: Vec, opacities: Vec, paints: Vec, surfaces: Vec, @@ -214,7 +178,7 @@ impl Program { /// Assemble one declaration. This constructor performs no partial compile. pub fn new( colors: Vec, - surface_inputs: Vec, + surface_input_ports: Vec, opacities: Vec, paints: Vec, surfaces: Vec, @@ -222,7 +186,7 @@ impl Program { ) -> Self { Self { colors, - surface_inputs, + surface_input_ports, opacities, paints, surfaces, @@ -245,8 +209,8 @@ pub enum ProgramCompileError { DuplicateOpacityInput { input: OpacityInputId, }, - DuplicateSurfaceInput { - input: SurfaceInputId, + DuplicateSurfaceInputPort { + input: SurfaceInputPortId, }, DuplicatePaint { paint: PaintId, @@ -269,9 +233,9 @@ pub enum ProgramCompileError { paint: PaintId, input: OpacityInputId, }, - MissingSurfaceInput { + MissingSurfaceInputPort { surface: SurfaceId, - input: SurfaceInputId, + input: SurfaceInputPortId, }, MissingSurfaceOccurrence { surface: SurfaceId, @@ -314,7 +278,7 @@ const PACKED_SURFACE_UNAVAILABLE_WORDS_V1: usize = 5; struct ProgramEpochV1 { graph: CompiledAppearanceGraph, binding_template: AdmittedAppearanceBindings, - surface_inputs: Box<[SurfaceInputId]>, + surface_input_ports: Box<[SurfaceInputPortId]>, occurrence_ids: Box<[OccurrenceId]>, } @@ -329,8 +293,8 @@ pub struct CompiledProgram { impl CompiledProgram { /// Canonical Surface-input order required by [`SurfaceUpdate::Present`]. - pub fn surface_inputs(&self) -> &[SurfaceInputId] { - &self.epoch.surface_inputs + pub fn surface_input_ports(&self) -> &[SurfaceInputPortId] { + &self.epoch.surface_input_ports } /// Canonical occurrence order emitted by every [`Snapshot`]. @@ -381,10 +345,10 @@ impl PointRenderOwner { } /// Return the current canonical Surface-input order, or `None` if disposed. - pub fn surface_inputs(&self) -> Option<&[SurfaceInputId]> { + pub fn surface_input_ports(&self) -> Option<&[SurfaceInputPortId]> { self.current .as_deref() - .map(|epoch| epoch.surface_inputs.as_ref()) + .map(|epoch| epoch.surface_input_ports.as_ref()) } /// Return the current canonical occurrence order, or `None` if disposed. @@ -409,7 +373,7 @@ impl PointRenderOwner { .try_clone_v1() .map_err(map_attach_binding_error)?; let initial_signal_buffers = - CompositedSignalBuffersV1::try_new(&epoch.surface_inputs, &epoch.occurrence_ids)?; + CompositedSignalBuffersV1::try_new(&epoch.surface_input_ports, &epoch.occurrence_ids)?; Ok(Session { epoch: Rc::downgrade(epoch), bindings, @@ -439,7 +403,7 @@ fn try_zeroed_signal_words(len: usize) -> Result, PointRenderAttachErro } fn prepare_program(program: Program) -> Result { - if program.surface_inputs.is_empty() { + if program.surface_input_ports.is_empty() { return Err(ProgramCompileError::EmptySurfaceSchema); } if program.occurrences.is_empty() { @@ -447,16 +411,14 @@ fn prepare_program(program: Program) -> Result Result Result, - expected: &[SurfaceInputId], +pub(crate) fn canonical_surface_input_port_sequence_matches( + actual: impl IntoIterator, + expected: &[SurfaceInputPortId], ) -> bool { - actual.into_iter().eq(expected - .iter() - .map(|input| AppearanceSurfaceInputId::new(input.value()))) + actual.into_iter().eq(expected.iter().copied()) } pub(crate) fn canonical_occurrence_sequence_matches( - actual: impl IntoIterator, + actual: impl IntoIterator, expected: &[OccurrenceId], ) -> bool { - actual.into_iter().eq(expected - .iter() - .map(|occurrence| AppearanceOccurrenceId::new(occurrence.value()))) + actual.into_iter().eq(expected.iter().copied()) } fn lower_graph(program: &Program) -> AppearanceGraphSpec { AppearanceGraphSpec::new( - program - .colors - .iter() - .map(|input| AppearanceColorInputId::new(input.id.value())) - .collect(), - program - .surface_inputs - .iter() - .map(|input| AppearanceSurfaceInputId::new(input.value())) - .collect(), - program - .opacities - .iter() - .map(|input| AppearanceOpacityInputId::new(input.id.value())) - .collect(), + program.colors.iter().map(|input| input.id).collect(), + program.surface_input_ports.clone(), + program.opacities.iter().map(|input| input.id).collect(), program .paints .iter() .map(|paint| match *paint { - Paint::Solid { id, color } => AppearancePaintSpec::Solid { - id: AppearancePaintId::new(id.value()), - color: AppearanceColorInputId::new(color.value()), - }, + Paint::Solid { id, color } => PaintSpec::Solid { id, color }, Paint::Opacity { id, source, opacity, - } => AppearancePaintSpec::Opacity { - id: AppearancePaintId::new(id.value()), - source: AppearancePaintId::new(source.value()), - opacity: AppearanceOpacityInputId::new(opacity.value()), + } => PaintSpec::Opacity { + id, + source, + opacity, }, }) .collect(), @@ -551,25 +496,19 @@ fn lower_graph(program: &Program) -> AppearanceGraphSpec { .surfaces .iter() .map(|surface| match *surface { - Surface::Input { id, input } => AppearanceSurfaceSpec::Input { - id: AppearanceSurfaceId::new(id.value()), - port: AppearanceSurfaceInputId::new(input.value()), - }, + Surface::Input { id, input } => SurfaceSpec::Input { id, port: input }, Surface::FromOccurrence { id, occurrence } => { - AppearanceSurfaceSpec::FromOccurrence { - id: AppearanceSurfaceId::new(id.value()), - occurrence: AppearanceOccurrenceId::new(occurrence.value()), - } + SurfaceSpec::FromOccurrence { id, occurrence } } }) .collect(), program .occurrences .iter() - .map(|occurrence| AppearanceOccurrenceSpec { - id: AppearanceOccurrenceId::new(occurrence.id.value()), - subject: AppearancePaintId::new(occurrence.subject.value()), - against: AppearanceSurfaceId::new(occurrence.against.value()), + .map(|occurrence| OccurrenceSpec { + id: occurrence.id, + subject: occurrence.subject, + against: occurrence.against, profile: match occurrence.composition { CompositionProfile::EncodedSrgb8SourceOverV1 => { CompositionProfileV1::EncodedSrgb8SourceOverV1 @@ -585,282 +524,84 @@ fn lower_bindings(program: &Program) -> AppearanceBindings { program .colors .iter() - .map(|input| (AppearanceColorInputId::new(input.id.value()), input.value)) + .map(|input| (input.id, input.value)) .collect(), program - .surface_inputs + .surface_input_ports .iter() - .map(|input| { - ( - AppearanceSurfaceInputId::new(input.value()), - Srgb8::new([0; 3]), - ) - }) + .map(|input| (*input, Srgb8::new([0; 3]))) .collect(), program .opacities .iter() - .map(|input| (AppearanceOpacityInputId::new(input.id.value()), input.value)) + .map(|input| (input.id, input.value)) .collect(), ) } -fn public_color_id(program: &Program, value: AppearanceColorInputId) -> Option { - for input in &program.colors { - if AppearanceColorInputId::new(input.id.value()) == value { - return Some(input.id); - } - } - for paint in &program.paints { - if let Paint::Solid { color, .. } = *paint { - if AppearanceColorInputId::new(color.value()) == value { - return Some(color); - } - } - } - None -} - -fn public_opacity_id(program: &Program, value: AppearanceOpacityInputId) -> Option { - for input in &program.opacities { - if AppearanceOpacityInputId::new(input.id.value()) == value { - return Some(input.id); - } - } - for paint in &program.paints { - if let Paint::Opacity { opacity, .. } = *paint { - if AppearanceOpacityInputId::new(opacity.value()) == value { - return Some(opacity); - } - } - } - None -} - -fn public_surface_input_id( - program: &Program, - value: AppearanceSurfaceInputId, -) -> Option { - for input in &program.surface_inputs { - if AppearanceSurfaceInputId::new(input.value()) == value { - return Some(*input); - } - } - for surface in &program.surfaces { - if let Surface::Input { input, .. } = *surface { - if AppearanceSurfaceInputId::new(input.value()) == value { - return Some(input); - } - } - } - None -} - -fn public_paint_id(program: &Program, value: AppearancePaintId) -> Option { - for paint in &program.paints { - match *paint { - Paint::Solid { id, .. } => { - if AppearancePaintId::new(id.value()) == value { - return Some(id); - } - } - Paint::Opacity { id, source, .. } => { - for candidate in [id, source] { - if AppearancePaintId::new(candidate.value()) == value { - return Some(candidate); - } - } - } - } - } - for occurrence in &program.occurrences { - if AppearancePaintId::new(occurrence.subject.value()) == value { - return Some(occurrence.subject); - } - } - None -} - -fn public_surface_id(program: &Program, value: AppearanceSurfaceId) -> Option { - for surface in &program.surfaces { - let id = match *surface { - Surface::Input { id, .. } | Surface::FromOccurrence { id, .. } => id, - }; - if AppearanceSurfaceId::new(id.value()) == value { - return Some(id); - } - } - for occurrence in &program.occurrences { - if AppearanceSurfaceId::new(occurrence.against.value()) == value { - return Some(occurrence.against); - } - } - None -} - -fn public_occurrence_id(program: &Program, value: AppearanceOccurrenceId) -> Option { - for occurrence in &program.occurrences { - if AppearanceOccurrenceId::new(occurrence.id.value()) == value { - return Some(occurrence.id); +fn map_compile_error(error: CompileError) -> ProgramCompileError { + match error { + CompileError::DuplicateColorInput { input } => { + ProgramCompileError::DuplicateColorInput { input } } - } - for surface in &program.surfaces { - if let Surface::FromOccurrence { occurrence, .. } = *surface { - if AppearanceOccurrenceId::new(occurrence.value()) == value { - return Some(occurrence); - } + CompileError::DuplicateOpacityInput { input } => { + ProgramCompileError::DuplicateOpacityInput { input } } - } - None -} - -fn map_compile_error(program: &Program, error: CompileError) -> ProgramCompileError { - match error { - CompileError::DuplicateColorInput { input } => public_color_id(program, input) - .map_or(ProgramCompileError::InternalInvariant, |input| { - ProgramCompileError::DuplicateColorInput { input } - }), - CompileError::DuplicateOpacityInput { input } => public_opacity_id(program, input) - .map_or(ProgramCompileError::InternalInvariant, |input| { - ProgramCompileError::DuplicateOpacityInput { input } - }), CompileError::DuplicateSurfaceInputPort { input } => { - public_surface_input_id(program, input) - .map_or(ProgramCompileError::InternalInvariant, |input| { - ProgramCompileError::DuplicateSurfaceInput { input } - }) + ProgramCompileError::DuplicateSurfaceInputPort { input } + } + CompileError::DuplicatePaint { paint } => ProgramCompileError::DuplicatePaint { paint }, + CompileError::DuplicateSurface { surface } => { + ProgramCompileError::DuplicateSurface { surface } } - CompileError::DuplicatePaint { paint } => public_paint_id(program, paint) - .map_or(ProgramCompileError::InternalInvariant, |paint| { - ProgramCompileError::DuplicatePaint { paint } - }), - CompileError::DuplicateSurface { surface } => public_surface_id(program, surface) - .map_or(ProgramCompileError::InternalInvariant, |surface| { - ProgramCompileError::DuplicateSurface { surface } - }), CompileError::DuplicateOccurrence { occurrence } => { - public_occurrence_id(program, occurrence) - .map_or(ProgramCompileError::InternalInvariant, |occurrence| { - ProgramCompileError::DuplicateOccurrence { occurrence } - }) + ProgramCompileError::DuplicateOccurrence { occurrence } } CompileError::MissingPaintColorInput { paint, input } => { - match ( - public_paint_id(program, paint), - public_color_id(program, input), - ) { - (Some(paint), Some(input)) => { - ProgramCompileError::MissingPaintColorInput { paint, input } - } - _ => ProgramCompileError::InternalInvariant, - } + ProgramCompileError::MissingPaintColorInput { paint, input } } CompileError::MissingPaintSource { paint, source } => { - match ( - public_paint_id(program, paint), - public_paint_id(program, source), - ) { - (Some(paint), Some(source)) => { - ProgramCompileError::MissingPaintSource { paint, source } - } - _ => ProgramCompileError::InternalInvariant, - } + ProgramCompileError::MissingPaintSource { paint, source } } CompileError::MissingPaintOpacityInput { paint, input } => { - match ( - public_paint_id(program, paint), - public_opacity_id(program, input), - ) { - (Some(paint), Some(input)) => { - ProgramCompileError::MissingPaintOpacityInput { paint, input } - } - _ => ProgramCompileError::InternalInvariant, - } + ProgramCompileError::MissingPaintOpacityInput { paint, input } } CompileError::MissingSurfaceInputPort { surface, input } => { - match ( - public_surface_id(program, surface), - public_surface_input_id(program, input), - ) { - (Some(surface), Some(input)) => { - ProgramCompileError::MissingSurfaceInput { surface, input } - } - _ => ProgramCompileError::InternalInvariant, - } + ProgramCompileError::MissingSurfaceInputPort { surface, input } } CompileError::MissingSurfaceOccurrence { surface, occurrence, - } => match ( - public_surface_id(program, surface), - public_occurrence_id(program, occurrence), - ) { - (Some(surface), Some(occurrence)) => ProgramCompileError::MissingSurfaceOccurrence { - surface, - occurrence, - }, - _ => ProgramCompileError::InternalInvariant, + } => ProgramCompileError::MissingSurfaceOccurrence { + surface, + occurrence, }, CompileError::MissingOccurrencePaint { occurrence, paint } => { - match ( - public_occurrence_id(program, occurrence), - public_paint_id(program, paint), - ) { - (Some(occurrence), Some(paint)) => { - ProgramCompileError::MissingOccurrencePaint { occurrence, paint } - } - _ => ProgramCompileError::InternalInvariant, - } + ProgramCompileError::MissingOccurrencePaint { occurrence, paint } } CompileError::MissingOccurrenceBackdrop { occurrence, surface, - } => match ( - public_occurrence_id(program, occurrence), - public_surface_id(program, surface), - ) { - (Some(occurrence), Some(surface)) => ProgramCompileError::MissingOccurrenceBackdrop { - occurrence, - surface, - }, - _ => ProgramCompileError::InternalInvariant, + } => ProgramCompileError::MissingOccurrenceBackdrop { + occurrence, + surface, }, - CompileError::PaintCycle { paints } => paints - .into_iter() - .map(|paint| public_paint_id(program, paint)) - .collect::>>() - .map_or(ProgramCompileError::InternalInvariant, |paints| { - ProgramCompileError::PaintCycle { paints } - }), + CompileError::PaintCycle { paints } => ProgramCompileError::PaintCycle { paints }, CompileError::RenderCycle { surfaces, occurrences, - } => { - let surfaces = surfaces - .into_iter() - .map(|surface| public_surface_id(program, surface)) - .collect::>>(); - let occurrences = occurrences - .into_iter() - .map(|occurrence| public_occurrence_id(program, occurrence)) - .collect::>>(); - match (surfaces, occurrences) { - (Some(surfaces), Some(occurrences)) => ProgramCompileError::RenderCycle { - surfaces, - occurrences, - }, - _ => ProgramCompileError::InternalInvariant, - } - } + } => ProgramCompileError::RenderCycle { + surfaces, + occurrences, + }, } } -fn map_binding_compile_error(program: &Program, error: BindingError) -> ProgramCompileError { +fn map_binding_compile_error(error: BindingError) -> ProgramCompileError { match error { - BindingError::OpacityOutOfDomain { input, .. } => public_opacity_id(program, input) - .map_or(ProgramCompileError::InternalInvariant, |input| { - ProgramCompileError::OpacityOutOfDomain { input } - }), + BindingError::OpacityOutOfDomain { input, .. } => { + ProgramCompileError::OpacityOutOfDomain { input } + } BindingError::ResourceExhausted => ProgramCompileError::ResourceExhausted, _ => ProgramCompileError::InternalInvariant, } @@ -888,18 +629,18 @@ impl SurfaceUnavailable { /// One exact runtime Surface-input value. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct SurfaceSignal { - input: SurfaceInputId, + input: SurfaceInputPortId, value: Srgb8, } impl SurfaceSignal { /// Bind one runtime input identity to exact encoded bytes. - pub const fn new(input: SurfaceInputId, value: Srgb8) -> Self { + pub const fn new(input: SurfaceInputPortId, value: Srgb8) -> Self { Self { input, value } } /// Return the runtime input identity. - pub const fn input(self) -> SurfaceInputId { + pub const fn input(self) -> SurfaceInputPortId { self.input } @@ -933,7 +674,7 @@ impl OccurrenceSignal { pub enum SurfaceUpdate<'input> { /// The entire Surface-input set is unavailable at this revision. Unavailable { revision: u64, reason: u32 }, - /// The complete input set in [`CompiledProgram::surface_inputs`] order. + /// The complete input set in [`CompiledProgram::surface_input_ports`] order. Present { revision: u64, surfaces: &'input [SurfaceSignal], @@ -945,7 +686,7 @@ pub enum SurfaceUpdate<'input> { /// present on this boundary. #[derive(Debug, PartialEq, Eq)] struct CompositedSignalBuffersV1 { - surface_inputs: Box<[SurfaceInputId]>, + surface_input_ports: Box<[SurfaceInputPortId]>, input_surface_signals_rgb24: Vec, occurrence_ids: Box<[OccurrenceId]>, composited_occurrence_signals_rgb24: Vec, @@ -953,12 +694,12 @@ struct CompositedSignalBuffersV1 { impl CompositedSignalBuffersV1 { fn try_new( - surface_inputs: &[SurfaceInputId], + surface_input_ports: &[SurfaceInputPortId], occurrence_ids: &[OccurrenceId], ) -> Result { Ok(Self { - surface_inputs: try_copy_ids(surface_inputs)?, - input_surface_signals_rgb24: try_zeroed_signal_words(surface_inputs.len())?, + surface_input_ports: try_copy_ids(surface_input_ports)?, + input_surface_signals_rgb24: try_zeroed_signal_words(surface_input_ports.len())?, occurrence_ids: try_copy_ids(occurrence_ids)?, composited_occurrence_signals_rgb24: try_zeroed_signal_words(occurrence_ids.len())?, }) @@ -990,7 +731,7 @@ impl Snapshot { /// Iterate admitted inputs in canonical compiled order without allocation. pub fn surfaces(&self) -> impl ExactSizeIterator + '_ { self.buffers - .surface_inputs + .surface_input_ports .iter() .copied() .zip(self.buffers.input_surface_signals_rgb24.iter().copied()) @@ -1096,14 +837,14 @@ pub(crate) enum PointRenderSessionUpdateErrorV1 { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SessionUpdateError { ProgramExpired, - SurfaceInputLengthMismatch { + SurfaceInputPortLengthMismatch { expected: usize, actual: usize, }, - SurfaceInputMismatch { + SurfaceInputPortMismatch { index: usize, - expected: SurfaceInputId, - actual: SurfaceInputId, + expected: SurfaceInputPortId, + actual: SurfaceInputPortId, }, RevisionOutOfOrder { current: u64, @@ -1185,17 +926,20 @@ impl Session { PreparedEncodedSurfaceUpdateV1::Unavailable(SurfaceUnavailable { revision, reason }) } SurfaceUpdate::Present { revision, surfaces } => { - if surfaces.len() != epoch.surface_inputs.len() { - return Err(SessionUpdateError::SurfaceInputLengthMismatch { - expected: epoch.surface_inputs.len(), + if surfaces.len() != epoch.surface_input_ports.len() { + return Err(SessionUpdateError::SurfaceInputPortLengthMismatch { + expected: epoch.surface_input_ports.len(), actual: surfaces.len(), }); } - for (index, (&expected, actual)) in - epoch.surface_inputs.iter().zip(surfaces.iter()).enumerate() + for (index, (&expected, actual)) in epoch + .surface_input_ports + .iter() + .zip(surfaces.iter()) + .enumerate() { if actual.input != expected { - return Err(SessionUpdateError::SurfaceInputMismatch { + return Err(SessionUpdateError::SurfaceInputPortMismatch { index, expected, actual: actual.input, @@ -1221,7 +965,7 @@ impl Session { .epoch .upgrade() .ok_or(PointRenderSessionUpdateErrorV1::ProgramExpired)?; - let prepared = decode_encoded_surface_update(words, epoch.surface_inputs.len()) + let prepared = decode_encoded_surface_update(words, epoch.surface_input_ports.len()) .map_err(PointRenderSessionUpdateErrorV1::EncodedSurfaceUpdate)?; self.apply_prepared(&epoch, prepared) } @@ -1267,20 +1011,21 @@ impl Session { let retained_shape_matches = match &self.state { SessionState::Waiting { .. } => { self.initial_signal_buffers.as_ref().is_some_and(|buffers| { - buffers.input_surface_signals_rgb24.len() == epoch.surface_inputs.len() + buffers.input_surface_signals_rgb24.len() + == epoch.surface_input_ports.len() && buffers.composited_occurrence_signals_rgb24.len() == epoch.occurrence_ids.len() }) } SessionState::Ready { current } => { current.buffers.input_surface_signals_rgb24.len() - == epoch.surface_inputs.len() + == epoch.surface_input_ports.len() && current.buffers.composited_occurrence_signals_rgb24.len() == epoch.occurrence_ids.len() } SessionState::Stale { previous, .. } => { previous.buffers.input_surface_signals_rgb24.len() - == epoch.surface_inputs.len() + == epoch.surface_input_ports.len() && previous.buffers.composited_occurrence_signals_rgb24.len() == epoch.occurrence_ids.len() } @@ -1296,12 +1041,9 @@ impl Session { // the cloned admitted schema; setters cannot partially reject // a later element. These mutable values are scratch only and // are not published until the final state replacement below. - for (index, &port) in epoch.surface_inputs.iter().enumerate() { + for (index, &port) in epoch.surface_input_ports.iter().enumerate() { self.bindings - .set_surface_input( - AppearanceSurfaceInputId::new(port.value()), - surfaces.value(index), - ) + .set_surface_input(port, surfaces.value(index)) .map_err(PointRenderSessionUpdateErrorV1::Evaluation)?; } let evaluation = epoch diff --git a/crates/labcolors-core/src/program_session_tests.rs b/crates/labcolors-core/src/program_session_tests.rs index 68263466..12627e73 100644 --- a/crates/labcolors-core/src/program_session_tests.rs +++ b/crates/labcolors-core/src/program_session_tests.rs @@ -1,20 +1,19 @@ use crate::Srgb8; use crate::appearance::{ - OccurrenceId as AppearanceOccurrenceId, SurfaceInputPortId as AppearanceSurfaceInputId, + ColorInputId, OccurrenceId, OpacityInputId, PaintId, SurfaceId, SurfaceInputPortId, }; use crate::program_session::{ - ColorInput, ColorInputId, CompiledProgram, CompositionProfile, Occurrence, OccurrenceId, - OpacityInput, OpacityInputId, PACKED_ENCODED_SURFACE_PRESENT_TAG_V1, - PACKED_ENCODED_SURFACE_UNAVAILABLE_TAG_V1, PACKED_ENCODED_SURFACE_UPDATE_MAGIC_V1, - PackedEncodedSurfaceUpdateErrorV1, Paint, PaintId, PointRenderOwner, - PointRenderSessionUpdateErrorV1, Program, ProgramCompileError, SessionState, - SessionUpdateError, Surface, SurfaceId, SurfaceInputId, SurfaceSignal, SurfaceUpdate, - canonical_occurrence_sequence_matches, canonical_surface_input_sequence_matches, + ColorInput, CompiledProgram, CompositionProfile, Occurrence, OpacityInput, + PACKED_ENCODED_SURFACE_PRESENT_TAG_V1, PACKED_ENCODED_SURFACE_UNAVAILABLE_TAG_V1, + PACKED_ENCODED_SURFACE_UPDATE_MAGIC_V1, PackedEncodedSurfaceUpdateErrorV1, Paint, + PointRenderOwner, PointRenderSessionUpdateErrorV1, Program, ProgramCompileError, SessionState, + SessionUpdateError, Surface, SurfaceSignal, SurfaceUpdate, + canonical_occurrence_sequence_matches, canonical_surface_input_port_sequence_matches, check_render_node_count, }; const COLOR: ColorInputId = ColorInputId::new(1); -const SURFACE_PORT: SurfaceInputId = SurfaceInputId::new(2); +const SURFACE_PORT: SurfaceInputPortId = SurfaceInputPortId::new(2); const OPACITY: OpacityInputId = OpacityInputId::new(3); const SOLID: PaintId = PaintId::new(10); const TRANSLUCENT: PaintId = PaintId::new(11); @@ -160,7 +159,7 @@ fn authored_and_runtime_values_preserve_exact_typed_bindings() { } #[test] -fn empty_surface_schema_precedes_dangling_surface_input_analysis() { +fn empty_surface_schema_precedes_dangling_surface_input_port_analysis() { let declaration = Program::new( vec![ColorInput::new(COLOR, Srgb8::new([0; 3]))], vec![], @@ -221,43 +220,31 @@ fn combined_render_cardinality_overflow_is_resource_exhaustion() { #[test] fn canonical_sequence_firewall_rejects_reordering_relabeling_and_truncation() { - let surface_inputs = [SurfaceInputId::new(1), SurfaceInputId::new(2)]; - assert!(canonical_surface_input_sequence_matches( - [ - AppearanceSurfaceInputId::new(1), - AppearanceSurfaceInputId::new(2), - ], - &surface_inputs, + let surface_input_ports = [SurfaceInputPortId::new(1), SurfaceInputPortId::new(2)]; + assert!(canonical_surface_input_port_sequence_matches( + [SurfaceInputPortId::new(1), SurfaceInputPortId::new(2),], + &surface_input_ports, )); - assert!(!canonical_surface_input_sequence_matches( - [ - AppearanceSurfaceInputId::new(2), - AppearanceSurfaceInputId::new(1), - ], - &surface_inputs, + assert!(!canonical_surface_input_port_sequence_matches( + [SurfaceInputPortId::new(2), SurfaceInputPortId::new(1),], + &surface_input_ports, )); - assert!(!canonical_surface_input_sequence_matches( - [AppearanceSurfaceInputId::new(1)], - &surface_inputs, + assert!(!canonical_surface_input_port_sequence_matches( + [SurfaceInputPortId::new(1)], + &surface_input_ports, )); let occurrences = [OccurrenceId::new(10), OccurrenceId::new(20)]; assert!(canonical_occurrence_sequence_matches( - [ - AppearanceOccurrenceId::new(10), - AppearanceOccurrenceId::new(20), - ], + [OccurrenceId::new(10), OccurrenceId::new(20),], &occurrences, )); assert!(!canonical_occurrence_sequence_matches( - [ - AppearanceOccurrenceId::new(10), - AppearanceOccurrenceId::new(21), - ], + [OccurrenceId::new(10), OccurrenceId::new(21),], &occurrences, )); assert!(!canonical_occurrence_sequence_matches( - [AppearanceOccurrenceId::new(10)], + [OccurrenceId::new(10)], &occurrences, )); } @@ -553,11 +540,11 @@ fn waiting_unknown_chain_preserves_preallocated_buffers() { #[test] fn public_program_compile_owner_session_path_emits_typed_occurrence_values() { let compiled = compiled(0.5); - assert_eq!(compiled.surface_inputs(), &[SURFACE_PORT]); + assert_eq!(compiled.surface_input_ports(), &[SURFACE_PORT]); assert_eq!(compiled.occurrences(), &[OCCURRENCE]); let owner = PointRenderOwner::new(compiled); - assert_eq!(owner.surface_inputs(), Some(&[SURFACE_PORT][..])); + assert_eq!(owner.surface_input_ports(), Some(&[SURFACE_PORT][..])); assert_eq!(owner.occurrences(), Some(&[OCCURRENCE][..])); let mut session = owner.attach().unwrap(); let surfaces = [SurfaceSignal::new(SURFACE_PORT, Srgb8::new([0xff; 3]))]; @@ -589,7 +576,7 @@ fn typed_update_schema_rejection_is_atomic_and_allocation_free() { let owner = compiled(0.5).into_owner(); let mut session = owner.attach().unwrap(); let wrong = [SurfaceSignal::new( - SurfaceInputId::new(999), + SurfaceInputPortId::new(999), Srgb8::new([0xff; 3]), )]; crate::composition::reset_source_over_evaluation_count(); @@ -604,10 +591,10 @@ fn typed_update_schema_rejection_is_atomic_and_allocation_free() { }); assert_eq!( result, - Err(SessionUpdateError::SurfaceInputMismatch { + Err(SessionUpdateError::SurfaceInputPortMismatch { index: 0, expected: SURFACE_PORT, - actual: SurfaceInputId::new(999), + actual: SurfaceInputPortId::new(999), }) ); assert_eq!(allocations, 0); From 2b98cb9d76025e0ae2ac97cae95a92639f273b76 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Wed, 22 Jul 2026 14:56:36 +0300 Subject: [PATCH 19/58] perf(core): prebind terminal evaluation slots --- ...rt-reference-surplus-q55-bps-proof-v1.json | 2 +- crates/labcolors-core/src/appearance.rs | 68 ++++++++ .../src/appearance_graph_tests.rs | 165 ++++++++++++++++++ scripts/verify_point_support_surplus.py | 2 +- 4 files changed, 235 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 ffdcd34d..9e39093a 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":"5a949a5b7bfe37fdbaf113989d2d50f9556a14f94867458ef2ee0a87e6a8e941","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":"e417c266e0889c839540cc1e20b947e4d3bf7e4a422900b381f74ca006144c51","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":"1142a30888565a629e8597436d3f68e43e90223e1aa91265db839b2deaee49eb"},{"kind":"rust-source","path":"crates/labcolors-core/src/composition.rs","sha256":"195a67327a3bd86d7816b634481389930bf68577bb1202fad14c2ea152df8625"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/exact.rs","sha256":"45bd09cd8f6f74860a0a3ffeb410ee2c3cf91b4868877abbc949e724b61e1ccd"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/mod.rs","sha256":"ad721c625b33a7432de44e5f63459a62c1cbc8ec2492855f783b5440f04978f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/wcag22.rs","sha256":"985e21abccd1fcb4a3f9624d5947ff3d1af7a1bf26b6b170ea8bbef77d6f1798"},{"kind":"rust-source","path":"crates/labcolors-core/src/hash.rs","sha256":"f97a0fd7d6ad3162f0f1dfb326fccfb7ed40da9a8fa67a5b8a239a1ae2ae49c3"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"d87d4959e28556de54f815b7f833d5b9310cf4f4209916257e2d1ed8ad160810"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"208066b255bc406303b18dc2eb6f1720c8203ea6eb37d225f9d8110c2449edc4"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"56c06b49f4687b3def69c247978da1b0cb45b42c09515a001d196cb1457c93a6"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"d4b06651e07cf4f93eccf9866f6e63c35ea72fa7125bdb41e222b19b70dd2f32"},{"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":33,"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":"48d3115e201d46c2b4564860d817e6a1cb45182b9de05da5f984c13f6251c47b"} +{"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":"443ec5442182361a65276fc679071020eebd45bfa57360835385a9ab34abaea0","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":"ea369b574933231de42ec62a699300d392048d50e4d0a83b337fe4d43a13eea3","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":"04dc6abd879e6f63ee060b10bc00460d2b297cb6b40d5f0806fab91feab86f8a"},{"kind":"rust-source","path":"crates/labcolors-core/src/composition.rs","sha256":"195a67327a3bd86d7816b634481389930bf68577bb1202fad14c2ea152df8625"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/exact.rs","sha256":"45bd09cd8f6f74860a0a3ffeb410ee2c3cf91b4868877abbc949e724b61e1ccd"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/mod.rs","sha256":"ad721c625b33a7432de44e5f63459a62c1cbc8ec2492855f783b5440f04978f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/wcag22.rs","sha256":"985e21abccd1fcb4a3f9624d5947ff3d1af7a1bf26b6b170ea8bbef77d6f1798"},{"kind":"rust-source","path":"crates/labcolors-core/src/hash.rs","sha256":"f97a0fd7d6ad3162f0f1dfb326fccfb7ed40da9a8fa67a5b8a239a1ae2ae49c3"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"d87d4959e28556de54f815b7f833d5b9310cf4f4209916257e2d1ed8ad160810"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"208066b255bc406303b18dc2eb6f1720c8203ea6eb37d225f9d8110c2449edc4"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"56c06b49f4687b3def69c247978da1b0cb45b42c09515a001d196cb1457c93a6"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"d4b06651e07cf4f93eccf9866f6e63c35ea72fa7125bdb41e222b19b70dd2f32"},{"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":33,"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":"1f9d9afc7cc30de72ee17767ab704c435d1eefeca0d2f96311584361df9020c2"} diff --git a/crates/labcolors-core/src/appearance.rs b/crates/labcolors-core/src/appearance.rs index 0873183e..2fa47adf 100644 --- a/crates/labcolors-core/src/appearance.rs +++ b/crates/labcolors-core/src/appearance.rs @@ -759,6 +759,26 @@ struct CompiledOccurrenceSpec { profile: CompositionProfileV1, } +/// Cold-bound canonical Paint position for allocation-free repeated lookup. +/// +/// Both fields are private to this module: callers can obtain a slot only from +/// a compiled graph and cannot forge a raw ordinal. The retained nominal ID is +/// checked again by every evaluation view before the ordinal is dereferenced. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct CompiledPaintSlotV1 { + index: usize, + id: PaintId, +} + +/// Cold-bound canonical Occurrence position for allocation-free repeated +/// lookup. As with [`CompiledPaintSlotV1`], construction remains sealed inside +/// the compiled appearance graph and every use revalidates the exact ID. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct CompiledOccurrenceSlotV1 { + index: usize, + id: OccurrenceId, +} + /// Канонический compiled IR с индексными ссылками: после проверки bindings /// исполнение самих Paint/Surface/Occurrence узлов линейно по их числу. #[derive(Debug, Clone, PartialEq, Eq)] @@ -1457,6 +1477,19 @@ impl AppearanceEvaluationView<'_, '_> { self.workspace.paints[index].as_ref() } + /// Resolve a compiler-minted Paint slot in constant time. + /// + /// A slot from a graph whose canonical ordinal names another Paint is + /// rejected before returning workspace data. No fallback ID lookup occurs. + pub(crate) fn paint_at(&self, slot: CompiledPaintSlotV1) -> Option<&EncodedPointPaintV1> { + let spec = self.program.paints.get(slot.index)?; + if spec.id() != slot.id { + return None; + } + let paint = self.workspace.paints.get(slot.index)?.as_ref()?; + (paint.id == slot.id).then_some(paint) + } + pub(crate) fn surface_rgb(&self, id: SurfaceId) -> Option<[u8; 3]> { let index = self .program @@ -1475,6 +1508,20 @@ impl AppearanceEvaluationView<'_, '_> { self.workspace.occurrences[index].as_ref() } + /// Resolve a compiler-minted Occurrence slot in constant time, retaining + /// exact nominal identity as the fail-closed cross-graph check. + pub(crate) fn occurrence_at( + &self, + slot: CompiledOccurrenceSlotV1, + ) -> Option<&ResolvedOccurrence> { + let spec = self.program.occurrences.get(slot.index)?; + if spec.id != slot.id { + return None; + } + let occurrence = self.workspace.occurrences.get(slot.index)?.as_ref()?; + (occurrence.id == slot.id).then_some(occurrence) + } + pub(crate) fn occurrences(&self) -> impl ExactSizeIterator + '_ { self.workspace.occurrences.iter().map(|occurrence| { occurrence @@ -1542,6 +1589,27 @@ impl CompiledAppearanceGraph { self.program() == program } + /// Bind one Paint identity to its canonical compiled ordinal. + /// + /// This is the only cold lookup. Repeated evaluations consume the sealed + /// slot through [`AppearanceEvaluationView::paint_at`] without searching. + pub(crate) fn bind_paint(&self, id: PaintId) -> Option { + let index = self + .paints + .binary_search_by_key(&id, CompiledPaintSpec::id) + .ok()?; + Some(CompiledPaintSlotV1 { index, id }) + } + + /// Bind one Occurrence identity to its canonical compiled ordinal. + pub(crate) fn bind_occurrence(&self, id: OccurrenceId) -> Option { + let index = self + .occurrences + .binary_search_by_key(&id, |occurrence| occurrence.id) + .ok()?; + Some(CompiledOccurrenceSlotV1 { index, id }) + } + /// Canonical client-owned occurrence identities emitted by this program. pub(crate) fn occurrence_ids(&self) -> impl ExactSizeIterator + '_ { self.occurrences.iter().map(|occurrence| occurrence.id) diff --git a/crates/labcolors-core/src/appearance_graph_tests.rs b/crates/labcolors-core/src/appearance_graph_tests.rs index 11e4355d..991fc5a8 100644 --- a/crates/labcolors-core/src/appearance_graph_tests.rs +++ b/crates/labcolors-core/src/appearance_graph_tests.rs @@ -29,6 +29,7 @@ const OTHER_PAINT: PaintId = PaintId::new(41); const CONTEXT_SURFACE: SurfaceId = SurfaceId::new(90); const DERIVED_SURFACE: SurfaceId = SurfaceId::new(2); const FILL_OCCURRENCE: OccurrenceId = OccurrenceId::new(800); +const OTHER_OCCURRENCE: OccurrenceId = OccurrenceId::new(400); fn point_component(reverse_paints: bool, reverse_surfaces: bool) -> AppearanceGraphSpec { let mut paints = vec![ @@ -82,6 +83,49 @@ fn bindings(source: [u8; 3], opacity: f64, context: [u8; 3]) -> AppearanceBindin ) } +fn slot_component(reverse_declarations: bool) -> AppearanceGraphSpec { + let mut paints = vec![ + PaintSpec::Solid { + id: SOLID_PAINT, + color: SOURCE, + }, + PaintSpec::Solid { + id: FILL_PAINT, + color: OTHER_SOURCE, + }, + ]; + let mut occurrences = vec![ + OccurrenceSpec { + id: FILL_OCCURRENCE, + subject: FILL_PAINT, + against: CONTEXT_SURFACE, + profile: CompositionProfileV1::EncodedSrgb8SourceOverV1, + }, + OccurrenceSpec { + id: OTHER_OCCURRENCE, + subject: SOLID_PAINT, + against: CONTEXT_SURFACE, + profile: CompositionProfileV1::EncodedSrgb8SourceOverV1, + }, + ]; + if reverse_declarations { + paints.reverse(); + occurrences.reverse(); + } + + AppearanceGraphSpec::new( + vec![SOURCE, OTHER_SOURCE], + vec![CONTEXT], + vec![], + paints, + vec![SurfaceSpec::Input { + id: CONTEXT_SURFACE, + port: CONTEXT, + }], + occurrences, + ) +} + fn admitted_surface_triplet() -> AdmittedAppearanceBindings { let inputs = [ SurfaceInputPortId::new(30), @@ -172,6 +216,127 @@ fn compile_and_evaluate_ignore_declaration_order() { assert_eq!(expected, both_reversed.evaluate(&values).unwrap()); } +#[test] +fn compiled_slots_bind_found_ids_and_reject_missing_ids() { + let graph = point_component(false, false).compile().unwrap(); + + assert!(graph.bind_paint(SOLID_PAINT).is_some()); + assert!(graph.bind_paint(FILL_PAINT).is_some()); + assert!(graph.bind_paint(PaintId::new(999)).is_none()); + assert!(graph.bind_occurrence(FILL_OCCURRENCE).is_some()); + assert!(graph.bind_occurrence(OccurrenceId::new(999)).is_none()); +} + +#[test] +fn compiled_slots_have_canonical_ordinals_across_declaration_permutations() { + let canonical = slot_component(false).compile().unwrap(); + let reversed = slot_component(true).compile().unwrap(); + + for paint in [FILL_PAINT, SOLID_PAINT] { + assert_eq!(canonical.bind_paint(paint), reversed.bind_paint(paint)); + } + for occurrence in [OTHER_OCCURRENCE, FILL_OCCURRENCE] { + assert_eq!( + canonical.bind_occurrence(occurrence), + reversed.bind_occurrence(occurrence) + ); + } +} + +#[test] +fn evaluation_view_rejects_same_ordinal_slots_with_different_nominal_ids() { + let compile_single = |paint, occurrence| { + AppearanceGraphSpec::new( + vec![SOURCE], + vec![CONTEXT], + vec![], + vec![PaintSpec::Solid { + id: paint, + color: SOURCE, + }], + vec![SurfaceSpec::Input { + id: CONTEXT_SURFACE, + port: CONTEXT, + }], + vec![OccurrenceSpec { + id: occurrence, + subject: paint, + against: CONTEXT_SURFACE, + profile: CompositionProfileV1::EncodedSrgb8SourceOverV1, + }], + ) + .compile() + .unwrap() + }; + let graph = compile_single(FILL_PAINT, FILL_OCCURRENCE); + let incompatible = compile_single(OTHER_PAINT, OTHER_OCCURRENCE); + let incompatible_paint = incompatible.bind_paint(OTHER_PAINT).unwrap(); + let incompatible_occurrence = incompatible.bind_occurrence(OTHER_OCCURRENCE).unwrap(); + let admitted = graph + .admit_bindings(&AppearanceBindings::new( + vec![(SOURCE, Srgb8::new([10, 20, 30]))], + vec![(CONTEXT, Srgb8::new([40, 50, 60]))], + vec![], + )) + .unwrap(); + let mut workspace = graph.new_workspace().unwrap(); + let evaluated = graph + .evaluate_admitted_into(&admitted, &mut workspace) + .unwrap(); + + assert!(evaluated.paint_at(incompatible_paint).is_none()); + assert!(evaluated.occurrence_at(incompatible_occurrence).is_none()); +} + +#[test] +fn prebound_view_lookup_returns_exact_values_and_allocates_nothing() { + let graph = point_component(false, false).compile().unwrap(); + let admitted = graph + .admit_bindings(&bindings([0xFF, 0xA1, 0x00], 0.122, [0xFF; 3])) + .unwrap(); + let mut workspace = graph.new_workspace().unwrap(); + let evaluated = graph + .evaluate_admitted_into(&admitted, &mut workspace) + .unwrap(); + let paint_slot = graph.bind_paint(FILL_PAINT).unwrap(); + let occurrence_slot = graph.bind_occurrence(FILL_OCCURRENCE).unwrap(); + + let (resolved, allocations) = crate::test_support::measured_allocations(|| { + let paint = evaluated.paint_at(paint_slot); + let occurrence = evaluated.occurrence_at(occurrence_slot); + ( + paint.map(|paint| (paint.id(), paint.source(), paint.opacity_bits())), + occurrence.map(|occurrence| { + ( + occurrence.id(), + occurrence.subject(), + occurrence.against(), + occurrence.backdrop(), + occurrence.visible(), + *occurrence.certificate(), + ) + }), + ) + }); + + assert_eq!(allocations, 0); + assert_eq!( + resolved.0, + Some(( + FILL_PAINT, + Srgb8::new([0xFF, 0xA1, 0x00]), + 0.122f64.to_bits(), + )) + ); + let occurrence = resolved.1.unwrap(); + assert_eq!(occurrence.0, FILL_OCCURRENCE); + assert_eq!(occurrence.1, FILL_PAINT); + assert_eq!(occurrence.2, CONTEXT_SURFACE); + assert_eq!(occurrence.3, [0xFF; 3]); + assert_eq!(occurrence.4, [0xFF, 0xF4, 0xE0]); + assert_eq!(occurrence.5.replay(), [0xFF, 0xF4, 0xE0]); +} + #[test] fn complete_typed_id_renaming_does_not_change_physics() { let source = ColorInputId::new(700); diff --git a/scripts/verify_point_support_surplus.py b/scripts/verify_point_support_surplus.py index 2f08093f..d707626a 100755 --- a/scripts/verify_point_support_surplus.py +++ b/scripts/verify_point_support_surplus.py @@ -57,7 +57,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 = ( - "e417c266e0889c839540cc1e20b947e4d3bf7e4a422900b381f74ca006144c51" + "ea369b574933231de42ec62a699300d392048d50e4d0a83b337fe4d43a13eea3" ) EXPECTED_Q55_PROOF_SHA256 = ( "ac59cf89503170c789223b91d775213a19d4e571ef930f2ea609fcd51b14defd" From c01c7f0a27212bbb735d2a097bf21e680ca1e9ac Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Wed, 22 Jul 2026 15:08:38 +0300 Subject: [PATCH 20/58] perf(core): make present ingestion lazy and borrowed --- crates/labcolors-core/src/program_session.rs | 474 +++++++----------- .../src/program_session_tests.rs | 472 +++++++++++------ 2 files changed, 485 insertions(+), 461 deletions(-) diff --git a/crates/labcolors-core/src/program_session.rs b/crates/labcolors-core/src/program_session.rs index 6f01e321..da1254c6 100644 --- a/crates/labcolors-core/src/program_session.rs +++ b/crates/labcolors-core/src/program_session.rs @@ -9,13 +9,13 @@ //! The crate root keeps this path private until the atomic public-surface cut; //! it must not create a second simultaneously supported authoring schema. //! -//! The first executable transport is intentionally narrow: one correlated set -//! of encoded Surface input signals per revision. It is transport-only state, -//! not an observed stimulus, physical evidence or certificate. F0 -//! observer/output/render identities remain a terminal prerequisite before any -//! such claim can be minted. Expanding the private transport to a ScenarioSet -//! does not require exposing the legacy multi-background metric matrix. -//! In particular, the wire magic is not an `lcs` or physical identity. +//! The first executable boundary is intentionally narrow: one correlated set +//! of encoded Surface input signals per revision. It is transport-only runtime +//! state, not an observed stimulus, physical evidence or certificate, and not +//! an `lcs` identity. F0 observer/output/render identities remain a terminal +//! prerequisite before any such claim can be minted. Expanding the private +//! boundary to a ScenarioSet does not require exposing the legacy +//! multi-background metric matrix. use std::mem; use std::rc::{Rc, Weak}; @@ -265,15 +265,6 @@ pub enum ProgramCompileError { InternalInvariant, } -/// ASCII `LCR1`: code-owned Lab Colors Render transport version 1. -/// -/// This is a wire discriminator, not an LCS, context or physical identity. -pub(crate) const PACKED_ENCODED_SURFACE_UPDATE_MAGIC_V1: u32 = 0x4c43_5231; -pub(crate) const PACKED_ENCODED_SURFACE_UNAVAILABLE_TAG_V1: u32 = 0; -pub(crate) const PACKED_ENCODED_SURFACE_PRESENT_TAG_V1: u32 = 1; -const PACKED_ENCODED_SURFACE_HEADER_WORDS_V1: usize = 4; -const PACKED_SURFACE_UNAVAILABLE_WORDS_V1: usize = 5; - #[derive(Debug)] struct ProgramEpochV1 { graph: CompiledAppearanceGraph, @@ -426,7 +417,6 @@ fn prepare_program(program: Program) -> Result { - Typed(&'input [SurfaceSignal]), - PackedRgb24(&'input [u32]), -} - -impl PreparedSurfaceValuesV1<'_> { - fn len(&self) -> usize { - match self { - Self::Typed(values) => values.len(), - Self::PackedRgb24(values) => values.len(), - } - } - - fn value(&self, index: usize) -> Srgb8 { - match self { - Self::Typed(values) => values[index].value, - Self::PackedRgb24(values) => Srgb8::new(unpack_rgb24(values[index])), - } - } - - fn matches_rgb24(&self, expected: &[u32]) -> bool { - self.len() == expected.len() - && expected - .iter() - .enumerate() - .all(|(index, &word)| pack_rgb24(self.value(index).bytes()) == word) - } -} - -enum PreparedEncodedSurfaceUpdateV1<'input> { - Unavailable(SurfaceUnavailable), - Present { - revision: u64, - surfaces: PreparedSurfaceValuesV1<'input>, - }, -} - /// Generation-bound mutable runtime. It owns reusable values/scratch, never a /// strong reference or a copy of the compiled graph. All fixed-cardinality /// signal buffers are allocated fallibly by `attach`; updates only move and @@ -912,6 +846,13 @@ impl Session { &self.state } + #[cfg(test)] + pub(crate) fn bound_surface_inputs_for_test( + &self, + ) -> impl ExactSizeIterator + '_ { + self.bindings.surface_inputs_canonical() + } + /// Admit, evaluate and atomically commit one typed Surface-input update. pub fn update( &mut self, @@ -921,9 +862,9 @@ impl Session { .epoch .upgrade() .ok_or(SessionUpdateError::ProgramExpired)?; - let prepared = match update { + match update { SurfaceUpdate::Unavailable { revision, reason } => { - PreparedEncodedSurfaceUpdateV1::Unavailable(SurfaceUnavailable { revision, reason }) + self.apply_unavailable(SurfaceUnavailable { revision, reason }) } SurfaceUpdate::Present { revision, surfaces } => { if surfaces.len() != epoch.surface_input_ports.len() { @@ -946,256 +887,201 @@ impl Session { }); } } - PreparedEncodedSurfaceUpdateV1::Present { - revision, - surfaces: PreparedSurfaceValuesV1::Typed(surfaces), - } + self.apply_canonical_present(&epoch, revision, surfaces.len(), |index| { + surfaces[index].value + }) } - }; - self.apply_prepared(&epoch, prepared) - .map_err(map_session_update_error) + } } - /// Private allocation-free packed bridge for the WASM boundary. - pub(crate) fn update_packed( + /// Read one complete canonical Surface-input set lazily and commit it as one + /// revision. The callback is not invoked until lifetime, cardinality and + /// revision admission have all succeeded. + pub(crate) fn update_canonical_present( &mut self, - words: &[u32], - ) -> Result<&SessionState, PointRenderSessionUpdateErrorV1> { + revision: u64, + surface_input_port_count: usize, + value_at: impl FnMut(usize) -> Srgb8, + ) -> Result<&SessionState, SessionUpdateError> { let epoch = self .epoch .upgrade() - .ok_or(PointRenderSessionUpdateErrorV1::ProgramExpired)?; - let prepared = decode_encoded_surface_update(words, epoch.surface_input_ports.len()) - .map_err(PointRenderSessionUpdateErrorV1::EncodedSurfaceUpdate)?; - self.apply_prepared(&epoch, prepared) + .ok_or(SessionUpdateError::ProgramExpired)?; + self.apply_canonical_present(&epoch, revision, surface_input_port_count, value_at) } - fn apply_prepared( + fn apply_unavailable( &mut self, - epoch: &ProgramEpochV1, - prepared: PreparedEncodedSurfaceUpdateV1<'_>, - ) -> Result<&SessionState, PointRenderSessionUpdateErrorV1> { - let incoming_revision = match &prepared { - PreparedEncodedSurfaceUpdateV1::Unavailable(unavailable) => unavailable.revision, - PreparedEncodedSurfaceUpdateV1::Present { revision, .. } => *revision, - }; - + unavailable: SurfaceUnavailable, + ) -> Result<&SessionState, SessionUpdateError> { + let incoming_revision = unavailable.revision; if let Some(current) = self.state.head_revision() { if incoming_revision < current { - return Err(PointRenderSessionUpdateErrorV1::EncodedSurfaceUpdate( - PackedEncodedSurfaceUpdateErrorV1::RevisionOutOfOrder { - current, - incoming: incoming_revision, - }, - )); + return Err(SessionUpdateError::RevisionOutOfOrder { + current, + incoming: incoming_revision, + }); } if incoming_revision == current { - return self.admit_same_revision(prepared); - } - } - - match prepared { - PreparedEncodedSurfaceUpdateV1::Unavailable(unavailable) => { - let previous = take_last_ready(&mut self.state); - self.state = match previous { - Some(previous) => SessionState::Stale { - previous, - current_unavailable: unavailable, - }, - None => SessionState::Waiting { - current_unavailable: Some(unavailable), - }, - }; - } - PreparedEncodedSurfaceUpdateV1::Present { revision, surfaces } => { - let retained_shape_matches = match &self.state { - SessionState::Waiting { .. } => { - self.initial_signal_buffers.as_ref().is_some_and(|buffers| { - buffers.input_surface_signals_rgb24.len() - == epoch.surface_input_ports.len() - && buffers.composited_occurrence_signals_rgb24.len() - == epoch.occurrence_ids.len() - }) - } - SessionState::Ready { current } => { - current.buffers.input_surface_signals_rgb24.len() - == epoch.surface_input_ports.len() - && current.buffers.composited_occurrence_signals_rgb24.len() - == epoch.occurrence_ids.len() + let exact = match &self.state { + SessionState::Waiting { + current_unavailable: Some(current), } - SessionState::Stale { previous, .. } => { - previous.buffers.input_surface_signals_rgb24.len() - == epoch.surface_input_ports.len() - && previous.buffers.composited_occurrence_signals_rgb24.len() - == epoch.occurrence_ids.len() - } - }; - if !retained_shape_matches { - return Err(PointRenderSessionUpdateErrorV1::Evaluation( - BindingError::IncompatibleWorkspace, - )); - } - - // Decode admitted the exact epoch-owned cardinality and every - // word before this loop. Each typed port therefore exists in - // the cloned admitted schema; setters cannot partially reject - // a later element. These mutable values are scratch only and - // are not published until the final state replacement below. - for (index, &port) in epoch.surface_input_ports.iter().enumerate() { - self.bindings - .set_surface_input(port, surfaces.value(index)) - .map_err(PointRenderSessionUpdateErrorV1::Evaluation)?; - } - let evaluation = epoch - .graph - .evaluate_admitted_into(&self.bindings, &mut self.workspace) - .map_err(PointRenderSessionUpdateErrorV1::Evaluation)?; - if evaluation.occurrences().len() != epoch.occurrence_ids.len() { - return Err(PointRenderSessionUpdateErrorV1::Evaluation( - BindingError::IncompatibleWorkspace, - )); - } - - // No fallible work follows. Preserve the committed snapshot - // through decode, binding mutation and evaluation; only now - // reclaim the one fixed buffer pair and overwrite it. - let mut buffers = match take_last_ready(&mut self.state) { - Some(previous) => previous.buffers, - None => self.initial_signal_buffers.take().unwrap_or_else(|| { - unreachable!("a Session without prior Ready must retain initial buffers") - }), + | SessionState::Stale { + current_unavailable: current, + .. + } => unavailable == *current, + _ => false, }; - debug_assert_eq!(buffers.input_surface_signals_rgb24.len(), surfaces.len()); - debug_assert_eq!( - buffers.composited_occurrence_signals_rgb24.len(), - epoch.occurrence_ids.len() - ); - for (index, output) in buffers.input_surface_signals_rgb24.iter_mut().enumerate() { - *output = pack_rgb24(surfaces.value(index).bytes()); - } - for (resolved, output) in evaluation - .occurrences() - .zip(buffers.composited_occurrence_signals_rgb24.iter_mut()) - { - // Packing an already resolved encoded point is infallible. - // Any future fallible verifier must finish before buffer - // reclamation above (or introduce its own staging value). - *output = pack_rgb24(resolved.visible()); - } - self.state = SessionState::Ready { - current: Snapshot { revision, buffers }, + return if exact { + Ok(&self.state) + } else { + Err(SessionUpdateError::RevisionConflict { revision: current }) }; } } + + let previous = take_last_ready(&mut self.state); + self.state = match previous { + Some(previous) => SessionState::Stale { + previous, + current_unavailable: unavailable, + }, + None => SessionState::Waiting { + current_unavailable: Some(unavailable), + }, + }; Ok(&self.state) } - fn admit_same_revision( - &self, - prepared: PreparedEncodedSurfaceUpdateV1<'_>, - ) -> Result<&SessionState, PointRenderSessionUpdateErrorV1> { - let exact = match (prepared, &self.state) { - ( - PreparedEncodedSurfaceUpdateV1::Unavailable(incoming), - SessionState::Waiting { - current_unavailable: Some(current), - } - | SessionState::Stale { - current_unavailable: current, - .. - }, - ) => incoming == *current, - ( - PreparedEncodedSurfaceUpdateV1::Present { revision, surfaces }, - SessionState::Ready { current }, - ) => { - revision == current.revision - && surfaces.matches_rgb24(¤t.buffers.input_surface_signals_rgb24) + fn apply_canonical_present( + &mut self, + epoch: &ProgramEpochV1, + revision: u64, + surface_input_port_count: usize, + mut value_at: impl FnMut(usize) -> Srgb8, + ) -> Result<&SessionState, SessionUpdateError> { + if surface_input_port_count != epoch.surface_input_ports.len() { + return Err(SessionUpdateError::SurfaceInputPortLengthMismatch { + expected: epoch.surface_input_ports.len(), + actual: surface_input_port_count, + }); + } + + if let Some(current) = self.state.head_revision() { + if revision < current { + return Err(SessionUpdateError::RevisionOutOfOrder { + current, + incoming: revision, + }); + } + if revision == current { + return self.admit_same_revision_present(revision, &mut value_at); + } + } + + let retained_shape_matches = match &self.state { + SessionState::Waiting { .. } => { + self.initial_signal_buffers.as_ref().is_some_and(|buffers| { + buffers.input_surface_signals_rgb24.len() == epoch.surface_input_ports.len() + && buffers.composited_occurrence_signals_rgb24.len() + == epoch.occurrence_ids.len() + }) + } + SessionState::Ready { current } => { + current.buffers.input_surface_signals_rgb24.len() == epoch.surface_input_ports.len() + && current.buffers.composited_occurrence_signals_rgb24.len() + == epoch.occurrence_ids.len() + } + SessionState::Stale { previous, .. } => { + previous.buffers.input_surface_signals_rgb24.len() + == epoch.surface_input_ports.len() + && previous.buffers.composited_occurrence_signals_rgb24.len() + == epoch.occurrence_ids.len() } - _ => false, }; - if exact { - Ok(&self.state) - } else { - Err(PointRenderSessionUpdateErrorV1::EncodedSurfaceUpdate( - PackedEncodedSurfaceUpdateErrorV1::RevisionConflict { - revision: self - .state - .head_revision() - .unwrap_or_else(|| unreachable!("same-revision branch has a head")), - }, - )) + if !retained_shape_matches { + return Err(SessionUpdateError::InternalInvariant); } - } -} -fn map_session_update_error(error: PointRenderSessionUpdateErrorV1) -> SessionUpdateError { - match error { - PointRenderSessionUpdateErrorV1::ProgramExpired => SessionUpdateError::ProgramExpired, - PointRenderSessionUpdateErrorV1::EncodedSurfaceUpdate( - PackedEncodedSurfaceUpdateErrorV1::RevisionOutOfOrder { current, incoming }, - ) => SessionUpdateError::RevisionOutOfOrder { current, incoming }, - PointRenderSessionUpdateErrorV1::EncodedSurfaceUpdate( - PackedEncodedSurfaceUpdateErrorV1::RevisionConflict { revision }, - ) => SessionUpdateError::RevisionConflict { revision }, - PointRenderSessionUpdateErrorV1::EncodedSurfaceUpdate(_) - | PointRenderSessionUpdateErrorV1::Evaluation(_) => SessionUpdateError::InternalInvariant, - } -} + self.bindings + .overwrite_surface_inputs_canonical( + epoch.surface_input_ports.iter().copied(), + &mut value_at, + ) + .map_err(|_| SessionUpdateError::InternalInvariant)?; + let evaluation = epoch + .graph + .evaluate_admitted_into(&self.bindings, &mut self.workspace) + .map_err(|_| SessionUpdateError::InternalInvariant)?; + if evaluation.occurrences().len() != epoch.occurrence_ids.len() { + return Err(SessionUpdateError::InternalInvariant); + } -fn decode_encoded_surface_update( - words: &[u32], - surface_count: usize, -) -> Result, PackedEncodedSurfaceUpdateErrorV1> { - if words.len() < PACKED_ENCODED_SURFACE_HEADER_WORDS_V1 { - return Err(PackedEncodedSurfaceUpdateErrorV1::HeaderTooShort); - } - if words[0] != PACKED_ENCODED_SURFACE_UPDATE_MAGIC_V1 { - return Err(PackedEncodedSurfaceUpdateErrorV1::MagicMismatch { actual: words[0] }); + // No fallible work follows. The callback is already gone from the + // commit path: the input snapshot is read back from the admitted + // canonical bindings that evaluation consumed. + let mut buffers = match take_last_ready(&mut self.state) { + Some(previous) => previous.buffers, + None => self.initial_signal_buffers.take().unwrap_or_else(|| { + unreachable!("a Session without prior Ready must retain initial buffers") + }), + }; + debug_assert_eq!( + buffers.input_surface_signals_rgb24.len(), + surface_input_port_count + ); + debug_assert_eq!( + buffers.composited_occurrence_signals_rgb24.len(), + epoch.occurrence_ids.len() + ); + for (((input, value), expected), output) in self + .bindings + .surface_inputs_canonical() + .zip(buffers.surface_input_ports.iter().copied()) + .zip(buffers.input_surface_signals_rgb24.iter_mut()) + { + debug_assert_eq!(input, expected); + *output = pack_rgb24(value.bytes()); + } + for (resolved, output) in evaluation + .occurrences() + .zip(buffers.composited_occurrence_signals_rgb24.iter_mut()) + { + *output = pack_rgb24(resolved.visible()); + } + self.state = SessionState::Ready { + current: Snapshot { revision, buffers }, + }; + Ok(&self.state) } - let revision = u64::from(words[2]) | (u64::from(words[3]) << 32); - match words[1] { - PACKED_ENCODED_SURFACE_UNAVAILABLE_TAG_V1 => { - if words.len() != PACKED_SURFACE_UNAVAILABLE_WORDS_V1 { - return Err(PackedEncodedSurfaceUpdateErrorV1::LengthMismatch { - expected: PACKED_SURFACE_UNAVAILABLE_WORDS_V1, - actual: words.len(), - }); + + fn admit_same_revision_present( + &self, + revision: u64, + mut value_at: impl FnMut(usize) -> Srgb8, + ) -> Result<&SessionState, SessionUpdateError> { + let SessionState::Ready { current } = &self.state else { + return Err(SessionUpdateError::RevisionConflict { revision }); + }; + debug_assert_eq!(current.revision, revision); + + let mut exact = true; + for (index, &expected) in current + .buffers + .input_surface_signals_rgb24 + .iter() + .enumerate() + { + if pack_rgb24(value_at(index).bytes()) != expected { + exact = false; } - Ok(PreparedEncodedSurfaceUpdateV1::Unavailable( - SurfaceUnavailable { - revision, - reason: words[4], - }, - )) } - PACKED_ENCODED_SURFACE_PRESENT_TAG_V1 => { - let expected = PACKED_ENCODED_SURFACE_HEADER_WORDS_V1 - .checked_add(surface_count) - .ok_or(PackedEncodedSurfaceUpdateErrorV1::ResourceExhausted)?; - if words.len() != expected { - return Err(PackedEncodedSurfaceUpdateErrorV1::LengthMismatch { - expected, - actual: words.len(), - }); - } - let surfaces = &words[PACKED_ENCODED_SURFACE_HEADER_WORDS_V1..]; - for (surface_index, &value) in surfaces.iter().enumerate() { - if value & 0xff00_0000 != 0 { - return Err( - PackedEncodedSurfaceUpdateErrorV1::ReservedSignalByteNonZero { - surface_index, - value, - }, - ); - } - } - Ok(PreparedEncodedSurfaceUpdateV1::Present { - revision, - surfaces: PreparedSurfaceValuesV1::PackedRgb24(surfaces), - }) + if exact { + Ok(&self.state) + } else { + Err(SessionUpdateError::RevisionConflict { revision }) } - actual => Err(PackedEncodedSurfaceUpdateErrorV1::UnsupportedTag { actual }), } } diff --git a/crates/labcolors-core/src/program_session_tests.rs b/crates/labcolors-core/src/program_session_tests.rs index 12627e73..2c6a39e2 100644 --- a/crates/labcolors-core/src/program_session_tests.rs +++ b/crates/labcolors-core/src/program_session_tests.rs @@ -1,15 +1,14 @@ +use std::cell::Cell; + use crate::Srgb8; use crate::appearance::{ ColorInputId, OccurrenceId, OpacityInputId, PaintId, SurfaceId, SurfaceInputPortId, }; use crate::program_session::{ - ColorInput, CompiledProgram, CompositionProfile, Occurrence, OpacityInput, - PACKED_ENCODED_SURFACE_PRESENT_TAG_V1, PACKED_ENCODED_SURFACE_UNAVAILABLE_TAG_V1, - PACKED_ENCODED_SURFACE_UPDATE_MAGIC_V1, PackedEncodedSurfaceUpdateErrorV1, Paint, - PointRenderOwner, PointRenderSessionUpdateErrorV1, Program, ProgramCompileError, SessionState, - SessionUpdateError, Surface, SurfaceSignal, SurfaceUpdate, - canonical_occurrence_sequence_matches, canonical_surface_input_port_sequence_matches, - check_render_node_count, + ColorInput, CompiledProgram, CompositionProfile, Occurrence, OpacityInput, Paint, + PointRenderOwner, Program, ProgramCompileError, SessionState, SessionUpdateError, Surface, + SurfaceSignal, SurfaceUpdate, canonical_occurrence_sequence_matches, + canonical_surface_input_port_sequence_matches, check_render_node_count, }; const COLOR: ColorInputId = ColorInputId::new(1); @@ -21,6 +20,14 @@ const BACKDROP: SurfaceId = SurfaceId::new(20); const VISIBLE: SurfaceId = SurfaceId::new(21); const OCCURRENCE: OccurrenceId = OccurrenceId::new(30); +const MULTI_PORT_A: SurfaceInputPortId = SurfaceInputPortId::new(10); +const MULTI_PORT_B: SurfaceInputPortId = SurfaceInputPortId::new(20); +const MULTI_PORT_C: SurfaceInputPortId = SurfaceInputPortId::new(30); +const MULTI_PORTS: [SurfaceInputPortId; 3] = [MULTI_PORT_A, MULTI_PORT_B, MULTI_PORT_C]; +const MULTI_SURFACE_A: SurfaceId = SurfaceId::new(100); +const MULTI_SURFACE_B: SurfaceId = SurfaceId::new(101); +const MULTI_SURFACE_C: SurfaceId = SurfaceId::new(102); + fn program(opacity: f64) -> Program { program_against(BACKDROP, opacity) } @@ -93,24 +100,76 @@ fn compiled(opacity: f64) -> CompiledProgram { program(opacity).compile().unwrap() } -fn point(revision: u64, rgb24: u32) -> [u32; 5] { - [ - PACKED_ENCODED_SURFACE_UPDATE_MAGIC_V1, - PACKED_ENCODED_SURFACE_PRESENT_TAG_V1, - revision as u32, - (revision >> 32) as u32, - rgb24, - ] +fn multi_surface_program() -> Program { + Program::new( + vec![ColorInput::new(COLOR, Srgb8::new([0; 3]))], + vec![MULTI_PORT_C, MULTI_PORT_A, MULTI_PORT_B], + vec![OpacityInput::new(OPACITY, 0.5)], + vec![ + Paint::Solid { + id: SOLID, + color: COLOR, + }, + Paint::Opacity { + id: TRANSLUCENT, + source: SOLID, + opacity: OPACITY, + }, + ], + vec![ + Surface::Input { + id: MULTI_SURFACE_C, + input: MULTI_PORT_C, + }, + Surface::Input { + id: MULTI_SURFACE_A, + input: MULTI_PORT_A, + }, + Surface::Input { + id: MULTI_SURFACE_B, + input: MULTI_PORT_B, + }, + Surface::FromOccurrence { + id: VISIBLE, + occurrence: OCCURRENCE, + }, + ], + vec![Occurrence::new( + OCCURRENCE, + TRANSLUCENT, + MULTI_SURFACE_B, + CompositionProfile::EncodedSrgb8SourceOverV1, + )], + ) +} + +fn multi_compiled() -> CompiledProgram { + multi_surface_program().compile().unwrap() } -fn unavailable(revision: u64, reason: u32) -> [u32; 5] { - [ - PACKED_ENCODED_SURFACE_UPDATE_MAGIC_V1, - PACKED_ENCODED_SURFACE_UNAVAILABLE_TAG_V1, - revision as u32, - (revision >> 32) as u32, - reason, - ] +struct ReadProbe { + values: [Srgb8; N], + reads: Cell<[usize; N]>, +} + +impl ReadProbe { + fn new(values: [Srgb8; N]) -> Self { + Self { + values, + reads: Cell::new([0; N]), + } + } + + fn read(&self, index: usize) -> Srgb8 { + let mut reads = self.reads.get(); + reads[index] += 1; + self.reads.set(reads); + self.values[index] + } + + fn reads(&self) -> [usize; N] { + self.reads.get() + } } fn retained_signal_storage_pointers(state: &SessionState) -> (*const u32, *const u32) { @@ -250,13 +309,15 @@ fn canonical_sequence_firewall_rejects_reordering_relabeling_and_truncation() { } #[test] -fn point_update_executes_the_compiled_graph_and_commits_compact_occurrences() { +fn canonical_present_executes_the_compiled_graph_and_commits_compact_occurrences() { let owner = compiled(0.5).into_owner(); let mut session = owner.attach().unwrap(); - let SessionState::Ready { current } = session.update_packed(&point(1, 0xff_ff_ff)).unwrap() + let SessionState::Ready { current } = session + .update_canonical_present(1, 1, |_| Srgb8::new([0xff; 3])) + .unwrap() else { - panic!("present encoded Surface signals must produce Ready"); + panic!("a complete canonical Surface set must produce Ready"); }; assert_eq!(current.revision(), 1); assert_eq!(current.input_surface_signals_rgb24(), &[0xff_ff_ff]); @@ -270,13 +331,15 @@ fn successful_replace_revokes_old_sessions_without_a_numeric_generation() { owner.replace(compiled(0.25)); assert_eq!( - old.update_packed(&point(1, 0xff_ff_ff)), - Err(PointRenderSessionUpdateErrorV1::ProgramExpired) + old.update_canonical_present(1, 1, |_| Srgb8::new([0xff; 3])), + Err(SessionUpdateError::ProgramExpired) ); let mut current = owner.attach().unwrap(); assert!(matches!( - current.update_packed(&point(1, 0xff_ff_ff)).unwrap(), + current + .update_canonical_present(1, 1, |_| Srgb8::new([0xff; 3])) + .unwrap(), SessionState::Ready { .. } )); } @@ -290,7 +353,9 @@ fn invalid_opacity_failed_replace_is_atomic_and_keeps_old_epoch_live() { program(1.25).compile().unwrap_err(), ProgramCompileError::OpacityOutOfDomain { input: OPACITY } ); - let SessionState::Ready { current } = session.update_packed(&point(1, 0xff_ff_ff)).unwrap() + let SessionState::Ready { current } = session + .update_canonical_present(1, 1, |_| Srgb8::new([0xff; 3])) + .unwrap() else { panic!("failed replacement must not revoke the old epoch"); }; @@ -315,7 +380,9 @@ fn dangling_and_cyclic_failed_compiles_do_not_revoke_the_current_epoch() { Err(ProgramCompileError::RenderCycle { .. }) )); - let SessionState::Ready { current } = session.update_packed(&point(1, 0xff_ff_ff)).unwrap() + let SessionState::Ready { current } = session + .update_canonical_present(1, 1, |_| Srgb8::new([0xff; 3])) + .unwrap() else { panic!("compile failures must leave the old strong epoch untouched"); }; @@ -329,8 +396,11 @@ fn dispose_revokes_sessions_and_prevents_new_attachment() { owner.dispose(); assert_eq!( - session.update_packed(&unavailable(1, 7)), - Err(PointRenderSessionUpdateErrorV1::ProgramExpired) + session.update(SurfaceUpdate::Unavailable { + revision: 1, + reason: 7, + }), + Err(SessionUpdateError::ProgramExpired) ); assert!(owner.attach().is_err()); } @@ -339,12 +409,19 @@ fn dispose_revokes_sessions_and_prevents_new_attachment() { fn unavailable_after_ready_is_stale_and_retains_exactly_one_previous_snapshot() { let owner = compiled(0.5).into_owner(); let mut session = owner.attach().unwrap(); - session.update_packed(&point(1, 0xff_ff_ff)).unwrap(); + session + .update_canonical_present(1, 1, |_| Srgb8::new([0xff; 3])) + .unwrap(); let SessionState::Stale { previous, current_unavailable, - } = session.update_packed(&unavailable(2, 91)).unwrap() + } = session + .update(SurfaceUpdate::Unavailable { + revision: 2, + reason: 91, + }) + .unwrap() else { panic!("unavailable Surface input after Ready must become Stale"); }; @@ -356,7 +433,12 @@ fn unavailable_after_ready_is_stale_and_retains_exactly_one_previous_snapshot() assert_eq!(current_unavailable.revision(), 2); assert_eq!(current_unavailable.reason(), 91); - let SessionState::Stale { previous, .. } = session.update_packed(&unavailable(3, 92)).unwrap() + let SessionState::Stale { previous, .. } = session + .update(SurfaceUpdate::Unavailable { + revision: 3, + reason: 92, + }) + .unwrap() else { panic!("a later unavailable update must remain Stale"); }; @@ -364,177 +446,228 @@ fn unavailable_after_ready_is_stale_and_retains_exactly_one_previous_snapshot() } #[test] -fn malformed_lower_and_conflicting_updates_are_atomic_and_do_not_evaluate() { - let owner = compiled(0.5).into_owner(); - let mut session = owner.attach().unwrap(); - session.update_packed(&point(5, 0xff_ff_ff)).unwrap(); - crate::composition::reset_source_over_evaluation_count(); - - let malformed = point(6, 0x01_ff_ff_ff); - assert_eq!( - session.update_packed(&malformed), - Err(PointRenderSessionUpdateErrorV1::EncodedSurfaceUpdate( - PackedEncodedSurfaceUpdateErrorV1::ReservedSignalByteNonZero { - surface_index: 0, - value: 0x01_ff_ff_ff, - } - )) - ); - assert_eq!(crate::composition::source_over_evaluation_count(), 0); +fn borrowed_present_expired_epoch_reads_zero_and_allocates_zero() { + let mut session = { + let owner = compiled(0.5).into_owner(); + owner.attach().unwrap() + }; + let reads = Cell::new(0); - assert!(matches!( - session.update_packed(&point(4, 0)), - Err(PointRenderSessionUpdateErrorV1::EncodedSurfaceUpdate( - PackedEncodedSurfaceUpdateErrorV1::RevisionOutOfOrder { - current: 5, - incoming: 4 - } - )) - )); - assert_eq!(crate::composition::source_over_evaluation_count(), 0); + let (result, allocations) = crate::test_support::measured_allocations(|| { + session + .update_canonical_present(1, 1, |_| { + reads.set(reads.get() + 1); + Srgb8::new([0xff; 3]) + }) + .map(|_| ()) + }); + assert_eq!(result, Err(SessionUpdateError::ProgramExpired)); + assert_eq!(reads.get(), 0); + assert_eq!(allocations, 0); assert!(matches!( - session.update_packed(&point(5, 0)), - Err(PointRenderSessionUpdateErrorV1::EncodedSurfaceUpdate( - PackedEncodedSurfaceUpdateErrorV1::RevisionConflict { revision: 5 } - )) + session.state(), + SessionState::Waiting { + current_unavailable: None + } )); - assert_eq!(crate::composition::source_over_evaluation_count(), 0); - - let SessionState::Ready { current } = session.state() else { - panic!("every rejected update must leave the committed state untouched"); - }; - assert_eq!(current.revision(), 5); - assert_eq!(current.composited_occurrence_signals_rgb24(), &[0x80_80_80]); } #[test] -fn exact_replay_is_idempotent_but_a_new_revision_evaluates_again() { +fn borrowed_present_schema_mismatch_reads_zero_and_allocates_zero() { let owner = compiled(0.5).into_owner(); let mut session = owner.attach().unwrap(); - let payload = point(1, 0xff_ff_ff); crate::composition::reset_source_over_evaluation_count(); - session.update_packed(&payload).unwrap(); - assert_eq!(crate::composition::source_over_evaluation_count(), 1); - session.update_packed(&payload).unwrap(); - assert_eq!(crate::composition::source_over_evaluation_count(), 1); - session.update_packed(&point(2, 0xff_ff_ff)).unwrap(); - assert_eq!(crate::composition::source_over_evaluation_count(), 2); + for actual in [0, 2] { + let reads = Cell::new(0); + let (result, allocations) = crate::test_support::measured_allocations(|| { + session + .update_canonical_present(1, actual, |_| { + reads.set(reads.get() + 1); + Srgb8::new([0xff; 3]) + }) + .map(|_| ()) + }); + assert_eq!( + result, + Err(SessionUpdateError::SurfaceInputPortLengthMismatch { + expected: 1, + actual, + }) + ); + assert_eq!(reads.get(), 0); + assert_eq!(allocations, 0); + } + + assert_eq!(crate::composition::source_over_evaluation_count(), 0); + assert!(matches!( + session.state(), + SessionState::Waiting { + current_unavailable: None + } + )); } #[test] -fn attached_session_reuses_buffers_for_every_update_state() { +fn borrowed_present_lower_revision_reads_zero_and_preserves_state() { let owner = compiled(0.5).into_owner(); let mut session = owner.attach().unwrap(); - let first = point(1, 0xff_ff_ff); - let second = point(2, 0x20_40_60); - let missing = unavailable(3, 91); - let still_missing = unavailable(4, 92); - let recovered = point(5, 0x20_40_60); - - let mut retained_storage = None; + let white = Srgb8::new([0xff; 3]); + session.update_canonical_present(5, 1, |_| white).unwrap(); + let storage = retained_signal_storage_pointers(session.state()); crate::composition::reset_source_over_evaluation_count(); - for (update, expected_evaluations) in [ - (first.as_slice(), 1), - (first.as_slice(), 1), - (second.as_slice(), 2), - (missing.as_slice(), 2), - (still_missing.as_slice(), 2), - (recovered.as_slice(), 3), - ] { - let (result, allocations) = - crate::test_support::measured_allocations(|| session.update_packed(update).map(|_| ())); - assert!(result.is_ok()); - assert_eq!( - allocations, 0, - "attach must preallocate every fixed-cardinality Session buffer" - ); - let current_storage = retained_signal_storage_pointers(session.state()); - if let Some(initial_storage) = retained_storage { - assert_eq!(current_storage, initial_storage); - } else { - retained_storage = Some(current_storage); - } - assert_eq!( - crate::composition::source_over_evaluation_count(), - expected_evaluations - ); - } + let reads = Cell::new(0); + + let (result, allocations) = crate::test_support::measured_allocations(|| { + session + .update_canonical_present(4, 1, |_| { + reads.set(reads.get() + 1); + Srgb8::new([0; 3]) + }) + .map(|_| ()) + }); + assert_eq!( + result, + Err(SessionUpdateError::RevisionOutOfOrder { + current: 5, + incoming: 4, + }) + ); + assert_eq!(reads.get(), 0); + assert_eq!(allocations, 0); + assert_eq!(crate::composition::source_over_evaluation_count(), 0); + assert_eq!(retained_signal_storage_pointers(session.state()), storage); let SessionState::Ready { current } = session.state() else { - panic!("a successful observation after Stale must recover Ready"); + panic!("a lower revision must leave Ready untouched"); }; assert_eq!(current.revision(), 5); - assert_eq!(current.input_surface_signals_rgb24(), &[0x20_40_60]); - assert_eq!(current.composited_occurrence_signals_rgb24(), &[0x10_20_30]); + assert!( + current + .surfaces() + .eq([SurfaceSignal::new(SURFACE_PORT, white)]) + ); + assert_eq!(current.composited_occurrence_signals_rgb24(), &[0x80_80_80]); + assert!( + session + .bound_surface_inputs_for_test() + .eq([(SURFACE_PORT, white)]) + ); } #[test] -fn rejected_update_preserves_cold_buffers_for_allocation_free_retry() { - let owner = compiled(0.5).into_owner(); +fn same_revision_replay_and_conflict_read_every_value_without_mutation() { + let compiled = multi_compiled(); + assert_eq!(compiled.surface_input_ports(), &MULTI_PORTS); + let owner = compiled.into_owner(); let mut session = owner.attach().unwrap(); - let malformed = point(1, 0x01_ff_ff_ff); - let valid = point(1, 0xff_ff_ff); + let committed_values = [ + Srgb8::new([0x10, 0x20, 0x30]), + Srgb8::new([0xff; 3]), + Srgb8::new([0x70, 0x80, 0x90]), + ]; + session + .update_canonical_present(5, MULTI_PORTS.len(), |index| committed_values[index]) + .unwrap(); + let storage = retained_signal_storage_pointers(session.state()); crate::composition::reset_source_over_evaluation_count(); - let (rejected, rejected_allocations) = - crate::test_support::measured_allocations(|| session.update_packed(&malformed).map(|_| ())); + let replay = ReadProbe::new(committed_values); + let (result, allocations) = crate::test_support::measured_allocations(|| { + session + .update_canonical_present(5, MULTI_PORTS.len(), |index| replay.read(index)) + .map(|_| ()) + }); + assert_eq!(result, Ok(())); + assert_eq!(replay.reads(), [1, 1, 1]); + assert_eq!(allocations, 0); + assert_eq!(crate::composition::source_over_evaluation_count(), 0); + assert_eq!(retained_signal_storage_pointers(session.state()), storage); + + let mut conflicting_values = committed_values; + conflicting_values[0] = Srgb8::new([0; 3]); + let conflict = ReadProbe::new(conflicting_values); + let (result, allocations) = crate::test_support::measured_allocations(|| { + session + .update_canonical_present(5, MULTI_PORTS.len(), |index| conflict.read(index)) + .map(|_| ()) + }); assert_eq!( - rejected, - Err(PointRenderSessionUpdateErrorV1::EncodedSurfaceUpdate( - PackedEncodedSurfaceUpdateErrorV1::ReservedSignalByteNonZero { - surface_index: 0, - value: 0x01_ff_ff_ff, - } - )) + result, + Err(SessionUpdateError::RevisionConflict { revision: 5 }) ); - assert_eq!(rejected_allocations, 0); - assert!(matches!( - session.state(), - SessionState::Waiting { - current_unavailable: None - } - )); + assert_eq!(conflict.reads(), [1, 1, 1]); + assert_eq!(allocations, 0); assert_eq!(crate::composition::source_over_evaluation_count(), 0); + assert_eq!(retained_signal_storage_pointers(session.state()), storage); - let (accepted, accepted_allocations) = - crate::test_support::measured_allocations(|| session.update_packed(&valid).map(|_| ())); - assert!(accepted.is_ok()); - assert_eq!(accepted_allocations, 0); - assert_eq!(crate::composition::source_over_evaluation_count(), 1); + let expected_surfaces = [ + SurfaceSignal::new(MULTI_PORT_A, committed_values[0]), + SurfaceSignal::new(MULTI_PORT_B, committed_values[1]), + SurfaceSignal::new(MULTI_PORT_C, committed_values[2]), + ]; + let SessionState::Ready { current } = session.state() else { + panic!("same-revision admission must retain Ready"); + }; + assert_eq!(current.revision(), 5); + assert!(current.surfaces().eq(expected_surfaces)); + assert_eq!(current.composited_occurrence_signals_rgb24(), &[0x80_80_80]); + assert!(session.bound_surface_inputs_for_test().eq([ + (MULTI_PORT_A, committed_values[0]), + (MULTI_PORT_B, committed_values[1]), + (MULTI_PORT_C, committed_values[2]), + ])); } #[test] -fn waiting_unknown_chain_preserves_preallocated_buffers() { - let owner = compiled(0.5).into_owner(); +fn new_revision_reads_each_value_once_and_snapshots_binding_readback() { + let owner = multi_compiled().into_owner(); let mut session = owner.attach().unwrap(); - let first_missing = unavailable(1, 91); - let later_missing = unavailable(2, 92); - let ready = point(3, 0xff_ff_ff); + let initial_values = [ + Srgb8::new([0x10; 3]), + Srgb8::new([0xff; 3]), + Srgb8::new([0x30; 3]), + ]; + session + .update_canonical_present(1, MULTI_PORTS.len(), |index| initial_values[index]) + .unwrap(); + let storage = retained_signal_storage_pointers(session.state()); + let next_values = [ + Srgb8::new([0xa1, 0xa2, 0xa3]), + Srgb8::new([0; 3]), + Srgb8::new([0xc1, 0xc2, 0xc3]), + ]; + let probe = ReadProbe::new(next_values); crate::composition::reset_source_over_evaluation_count(); - for (update, expected_evaluations) in [ - (first_missing.as_slice(), 0), - (first_missing.as_slice(), 0), - (later_missing.as_slice(), 0), - (ready.as_slice(), 1), - ] { - let (result, allocations) = - crate::test_support::measured_allocations(|| session.update_packed(update).map(|_| ())); - assert!(result.is_ok()); - assert_eq!(allocations, 0); - assert_eq!( - crate::composition::source_over_evaluation_count(), - expected_evaluations - ); - } + let (result, allocations) = crate::test_support::measured_allocations(|| { + session + .update_canonical_present(2, MULTI_PORTS.len(), |index| probe.read(index)) + .map(|_| ()) + }); + assert_eq!(result, Ok(())); + assert_eq!(probe.reads(), [1, 1, 1]); + assert_eq!(allocations, 0); + assert_eq!(crate::composition::source_over_evaluation_count(), 1); + assert_eq!(retained_signal_storage_pointers(session.state()), storage); + let expected_surfaces = [ + SurfaceSignal::new(MULTI_PORT_A, next_values[0]), + SurfaceSignal::new(MULTI_PORT_B, next_values[1]), + SurfaceSignal::new(MULTI_PORT_C, next_values[2]), + ]; let SessionState::Ready { current } = session.state() else { - panic!("the first admitted point after Waiting must commit Ready"); + panic!("a new canonical revision must commit Ready"); }; - assert_eq!(current.revision(), 3); - assert_eq!(current.composited_occurrence_signals_rgb24(), &[0x80_80_80]); + assert_eq!(current.revision(), 2); + assert!(current.surfaces().eq(expected_surfaces)); + assert_eq!(current.composited_occurrence_signals_rgb24(), &[0]); + assert!(session.bound_surface_inputs_for_test().eq([ + (MULTI_PORT_A, next_values[0]), + (MULTI_PORT_B, next_values[1]), + (MULTI_PORT_C, next_values[2]), + ])); } #[test] @@ -664,6 +797,11 @@ fn generic_program_module_has_no_recipe_or_ui_compatibility_surface() { "resolve_named_set", "resolveTheme", "themeHandle", + concat!("PACK", "ED_ENCODED_"), + concat!("Pack", "edEncoded"), + "PointRenderSessionUpdateError", + concat!("update_pa", "cked"), + concat!("decode_encoded_", "surface_update"), ] { assert!( !source.contains(forbidden), From e0f4956f16a6b0e71e60f754cd735b34ceca103e Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Wed, 22 Jul 2026 15:17:37 +0300 Subject: [PATCH 21/58] refactor(core): remove per-port surface mutation --- ...rt-reference-surplus-q55-bps-proof-v1.json | 2 +- crates/labcolors-core/src/appearance.rs | 15 ------------ .../src/appearance_graph_tests.rs | 23 +++++++++++++++++-- scripts/verify_point_support_surplus.py | 2 +- 4 files changed, 23 insertions(+), 19 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 9e39093a..2e5a13ab 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":"443ec5442182361a65276fc679071020eebd45bfa57360835385a9ab34abaea0","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":"ea369b574933231de42ec62a699300d392048d50e4d0a83b337fe4d43a13eea3","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":"04dc6abd879e6f63ee060b10bc00460d2b297cb6b40d5f0806fab91feab86f8a"},{"kind":"rust-source","path":"crates/labcolors-core/src/composition.rs","sha256":"195a67327a3bd86d7816b634481389930bf68577bb1202fad14c2ea152df8625"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/exact.rs","sha256":"45bd09cd8f6f74860a0a3ffeb410ee2c3cf91b4868877abbc949e724b61e1ccd"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/mod.rs","sha256":"ad721c625b33a7432de44e5f63459a62c1cbc8ec2492855f783b5440f04978f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/wcag22.rs","sha256":"985e21abccd1fcb4a3f9624d5947ff3d1af7a1bf26b6b170ea8bbef77d6f1798"},{"kind":"rust-source","path":"crates/labcolors-core/src/hash.rs","sha256":"f97a0fd7d6ad3162f0f1dfb326fccfb7ed40da9a8fa67a5b8a239a1ae2ae49c3"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"d87d4959e28556de54f815b7f833d5b9310cf4f4209916257e2d1ed8ad160810"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"208066b255bc406303b18dc2eb6f1720c8203ea6eb37d225f9d8110c2449edc4"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"56c06b49f4687b3def69c247978da1b0cb45b42c09515a001d196cb1457c93a6"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"d4b06651e07cf4f93eccf9866f6e63c35ea72fa7125bdb41e222b19b70dd2f32"},{"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":33,"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":"1f9d9afc7cc30de72ee17767ab704c435d1eefeca0d2f96311584361df9020c2"} +{"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":"1fd5e554ac22749654bd1d285e94fe853230f9c5025a97ae467650ceac2aa5bd","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":"690cd5ed543f1e30602e825153a9d0857bfea1089350d234e701b181e8819862","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":"57e6feeab15182322c258829d10214c31bb3c93fa58d8d47e37127db204cdc28"},{"kind":"rust-source","path":"crates/labcolors-core/src/composition.rs","sha256":"195a67327a3bd86d7816b634481389930bf68577bb1202fad14c2ea152df8625"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/exact.rs","sha256":"45bd09cd8f6f74860a0a3ffeb410ee2c3cf91b4868877abbc949e724b61e1ccd"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/mod.rs","sha256":"ad721c625b33a7432de44e5f63459a62c1cbc8ec2492855f783b5440f04978f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/wcag22.rs","sha256":"985e21abccd1fcb4a3f9624d5947ff3d1af7a1bf26b6b170ea8bbef77d6f1798"},{"kind":"rust-source","path":"crates/labcolors-core/src/hash.rs","sha256":"f97a0fd7d6ad3162f0f1dfb326fccfb7ed40da9a8fa67a5b8a239a1ae2ae49c3"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"d87d4959e28556de54f815b7f833d5b9310cf4f4209916257e2d1ed8ad160810"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"208066b255bc406303b18dc2eb6f1720c8203ea6eb37d225f9d8110c2449edc4"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"56c06b49f4687b3def69c247978da1b0cb45b42c09515a001d196cb1457c93a6"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"d4b06651e07cf4f93eccf9866f6e63c35ea72fa7125bdb41e222b19b70dd2f32"},{"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":33,"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":"839c75345c05a13492373ad75789668ab6db8184111dac05e0c03c61e94bb781"} diff --git a/crates/labcolors-core/src/appearance.rs b/crates/labcolors-core/src/appearance.rs index 2fa47adf..f6e02fb1 100644 --- a/crates/labcolors-core/src/appearance.rs +++ b/crates/labcolors-core/src/appearance.rs @@ -1081,21 +1081,6 @@ impl AdmittedAppearanceBindings { }) } - /// Обновить один уже объявленный physical Surface input без пересборки - /// остальных authored bindings и без allocation. - pub(crate) fn set_surface_input( - &mut self, - input: SurfaceInputPortId, - value: Srgb8, - ) -> Result<(), BindingError> { - let index = self - .surfaces - .binary_search_by_key(&input, |(bound, _)| *bound) - .map_err(|_| BindingError::UnexpectedSurfaceInputBinding { input })?; - self.surfaces[index].1 = value; - Ok(()) - } - /// Overwrite the complete canonical Surface-input slice from one borrowed /// value source. /// diff --git a/crates/labcolors-core/src/appearance_graph_tests.rs b/crates/labcolors-core/src/appearance_graph_tests.rs index 991fc5a8..14a047af 100644 --- a/crates/labcolors-core/src/appearance_graph_tests.rs +++ b/crates/labcolors-core/src/appearance_graph_tests.rs @@ -1757,6 +1757,25 @@ fn canonical_surface_overwrite_reads_once_and_exposes_all_values_without_allocat assert_eq!(allocations, 0); } +#[test] +fn admitted_surface_runtime_exposes_only_canonical_bulk_seams() { + let source = include_str!("appearance.rs"); + let forbidden = concat!("set_surface_", "input"); + assert!( + !source.contains(forbidden), + "per-port Surface mutation must not return after the canonical bulk cut" + ); + for required in [ + "overwrite_surface_inputs_canonical", + "surface_inputs_canonical", + ] { + assert!( + source.contains(required), + "canonical runtime seam `{required}` must remain" + ); + } +} + #[test] fn admitted_bindings_and_workspace_are_reused_without_storage_churn() { let graph = point_component(false, false).compile().unwrap(); @@ -1766,7 +1785,7 @@ fn admitted_bindings_and_workspace_are_reused_without_storage_churn() { let mut independent = admitted.try_clone_v1().unwrap(); assert_eq!(independent, admitted); independent - .set_surface_input(CONTEXT, Srgb8::new([1, 2, 3])) + .overwrite_surface_inputs_canonical([CONTEXT], |_| Srgb8::new([1, 2, 3])) .unwrap(); assert_ne!(independent, admitted); assert_eq!(admitted.opacity_bits(OPACITY), Some(0.5f64.to_bits())); @@ -1786,7 +1805,7 @@ fn admitted_bindings_and_workspace_are_reused_without_storage_churn() { ([100, 100, 100], [150, 90, 70]), ] { admitted - .set_surface_input(CONTEXT, Srgb8::new(backdrop)) + .overwrite_surface_inputs_canonical([CONTEXT], |_| Srgb8::new(backdrop)) .unwrap(); { let evaluated = graph diff --git a/scripts/verify_point_support_surplus.py b/scripts/verify_point_support_surplus.py index d707626a..71e1010f 100755 --- a/scripts/verify_point_support_surplus.py +++ b/scripts/verify_point_support_surplus.py @@ -57,7 +57,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 = ( - "ea369b574933231de42ec62a699300d392048d50e4d0a83b337fe4d43a13eea3" + "690cd5ed543f1e30602e825153a9d0857bfea1089350d234e701b181e8819862" ) EXPECTED_Q55_PROOF_SHA256 = ( "ac59cf89503170c789223b91d775213a19d4e571ef930f2ea609fcd51b14defd" From bf60c9cca55fa76661095caa080b040c3e675a4c Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Wed, 22 Jul 2026 16:03:17 +0300 Subject: [PATCH 22/58] feat(core): evaluate typed constraints and emit terminal paints --- ...rt-reference-surplus-q55-bps-proof-v1.json | 2 +- crates/labcolors-core/src/config/tests.rs | 2 - crates/labcolors-core/src/constraints/mod.rs | 152 +- .../src/generic_boundary_tests.rs | 3 +- crates/labcolors-core/src/numerical_plan.rs | 12 +- crates/labcolors-core/src/program_session.rs | 1290 ++++++++++---- .../src/program_session_tests.rs | 1553 +++++++++++------ scripts/verify_point_support_surplus.py | 2 +- 8 files changed, 2135 insertions(+), 881 deletions(-) mode change 100755 => 100644 scripts/verify_point_support_surplus.py 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 2e5a13ab..2ff6ca52 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":"1fd5e554ac22749654bd1d285e94fe853230f9c5025a97ae467650ceac2aa5bd","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":"690cd5ed543f1e30602e825153a9d0857bfea1089350d234e701b181e8819862","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":"57e6feeab15182322c258829d10214c31bb3c93fa58d8d47e37127db204cdc28"},{"kind":"rust-source","path":"crates/labcolors-core/src/composition.rs","sha256":"195a67327a3bd86d7816b634481389930bf68577bb1202fad14c2ea152df8625"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/exact.rs","sha256":"45bd09cd8f6f74860a0a3ffeb410ee2c3cf91b4868877abbc949e724b61e1ccd"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/mod.rs","sha256":"ad721c625b33a7432de44e5f63459a62c1cbc8ec2492855f783b5440f04978f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/wcag22.rs","sha256":"985e21abccd1fcb4a3f9624d5947ff3d1af7a1bf26b6b170ea8bbef77d6f1798"},{"kind":"rust-source","path":"crates/labcolors-core/src/hash.rs","sha256":"f97a0fd7d6ad3162f0f1dfb326fccfb7ed40da9a8fa67a5b8a239a1ae2ae49c3"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"d87d4959e28556de54f815b7f833d5b9310cf4f4209916257e2d1ed8ad160810"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"208066b255bc406303b18dc2eb6f1720c8203ea6eb37d225f9d8110c2449edc4"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"56c06b49f4687b3def69c247978da1b0cb45b42c09515a001d196cb1457c93a6"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"d4b06651e07cf4f93eccf9866f6e63c35ea72fa7125bdb41e222b19b70dd2f32"},{"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":33,"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":"839c75345c05a13492373ad75789668ab6db8184111dac05e0c03c61e94bb781"} +{"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":"4f45115564741394c194ff7ab4fe43a277c9cdc442371b18055d6eb25019d262","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":"d95e75277933ecf04f72cef57d287a1c4c4ad373445552af110fae17d730d592","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":"57e6feeab15182322c258829d10214c31bb3c93fa58d8d47e37127db204cdc28"},{"kind":"rust-source","path":"crates/labcolors-core/src/composition.rs","sha256":"195a67327a3bd86d7816b634481389930bf68577bb1202fad14c2ea152df8625"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/exact.rs","sha256":"45bd09cd8f6f74860a0a3ffeb410ee2c3cf91b4868877abbc949e724b61e1ccd"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/mod.rs","sha256":"2f3e44b2db837deed0df3e34063df9cfc6af82a912373da488c9fe2137a119c0"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/wcag22.rs","sha256":"985e21abccd1fcb4a3f9624d5947ff3d1af7a1bf26b6b170ea8bbef77d6f1798"},{"kind":"rust-source","path":"crates/labcolors-core/src/hash.rs","sha256":"f97a0fd7d6ad3162f0f1dfb326fccfb7ed40da9a8fa67a5b8a239a1ae2ae49c3"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"d87d4959e28556de54f815b7f833d5b9310cf4f4209916257e2d1ed8ad160810"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"208066b255bc406303b18dc2eb6f1720c8203ea6eb37d225f9d8110c2449edc4"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"56c06b49f4687b3def69c247978da1b0cb45b42c09515a001d196cb1457c93a6"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"d4b06651e07cf4f93eccf9866f6e63c35ea72fa7125bdb41e222b19b70dd2f32"},{"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":33,"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":"096af1ff7bd7baa94d34407705b3181b7c1d04a40587ac9310235557f5bceb32"} diff --git a/crates/labcolors-core/src/config/tests.rs b/crates/labcolors-core/src/config/tests.rs index 3270bcca..ad1397a9 100644 --- a/crates/labcolors-core/src/config/tests.rs +++ b/crates/labcolors-core/src/config/tests.rs @@ -785,8 +785,6 @@ fn consumed_roles_diff_is_empty_against_labui_contract() { }; assert!(hits("label-on-accent") && hits("bg-material-thick") && hits("tint-static-dark-4")); assert!(!hits("fx-glow-inverted") && !hits("label-danger-primary")); - // COLLAPSED_ROLES не пуст — декларация причин присутствует. - assert!(!COLLAPSED_ROLES.is_empty()); } /// Glob-сопоставление паттернов [`COLLAPSED_ROLES`] (`*` — любая подстрока): diff --git a/crates/labcolors-core/src/constraints/mod.rs b/crates/labcolors-core/src/constraints/mod.rs index fdcaf610..8a956161 100644 --- a/crates/labcolors-core/src/constraints/mod.rs +++ b/crates/labcolors-core/src/constraints/mod.rs @@ -27,6 +27,13 @@ pub(crate) use wcag22::{ Wcag22ViolationV1, }; +#[cfg(test)] +pub(crate) use program_test_evaluator::{ + ProgramTestEvaluationErrorV1, ProgramTestEvaluatorV1, ProgramTestInvocationV1, + arm_program_test_failure_once, program_test_evaluation_count, + reset_program_test_evaluation_count, +}; + /// Seals недоступны внешним crate-ам: новые evaluator/classifier families /// добавляются только вместе с code-owned physical adapter-ом. mod private { @@ -120,8 +127,7 @@ impl ClassifiedMeasurement { Pass(Pass), @@ -154,6 +160,29 @@ pub(crate) type PointViolation = , >>::Violation; +pub(crate) type PointEvaluationError = + >::Error; + +/// One statically dispatched point evaluator/classifier family. +/// +/// The first executable Program slice is deliberately homogeneous: every +/// compiled invocation has this evaluator's one typed invocation and result +/// family. The trait remains sealed through both parent protocols; this slice +/// contains no dynamic registry or open payload enum. +pub(crate) trait PointEvaluatorV1: + Sized + + Evaluator + + HardClassifier, PointMeasurement> +{ +} + +impl PointEvaluatorV1 for Evaluation where + Evaluation: Sized + + Evaluator + + HardClassifier, PointMeasurement> +{ +} + type BoundVisiblePointMeasurement = BoundEvidence< VisiblePointBindingV1, >::Identity, @@ -235,6 +264,125 @@ impl } } +/// Code-owned fallible evaluator used only to prove Program transactionality. +/// Its measurement intentionally does not implement `Clone` or `Copy`. +#[cfg(test)] +mod program_test_evaluator { + use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; + + use super::{Evaluator, HardClassifier, HardDecision, private}; + use crate::Srgb8; + use crate::appearance::ModeledSrgb8PointOccurrence; + + static EVALUATIONS: AtomicUsize = AtomicUsize::new(0); + static FAIL_ONCE_ARMED: AtomicBool = AtomicBool::new(false); + + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub(crate) struct ProgramTestEvaluatorV1; + + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub(crate) struct ProgramTestInvocationV1 { + expected: Srgb8, + fail_once_when_armed: bool, + } + + impl ProgramTestInvocationV1 { + pub(crate) const fn exact(expected: Srgb8) -> Self { + Self { + expected, + fail_once_when_armed: false, + } + } + + pub(crate) const fn fail_once_when_armed(expected: Srgb8) -> Self { + Self { + expected, + fail_once_when_armed: true, + } + } + } + + #[derive(Debug, PartialEq, Eq)] + pub(crate) struct ProgramTestMeasurementV1 { + visible: Srgb8, + backdrop: Srgb8, + } + + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub(crate) enum ProgramTestEvaluationErrorV1 { + Forced, + } + + #[derive(Debug, PartialEq, Eq)] + pub(crate) struct ProgramTestPassV1; + + #[derive(Debug, PartialEq, Eq)] + pub(crate) struct ProgramTestViolationV1; + + impl private::EvaluatorSealed for ProgramTestEvaluatorV1 {} + impl private::HardClassifierSealed for ProgramTestEvaluatorV1 {} + + impl Evaluator for ProgramTestEvaluatorV1 { + type Invocation = ProgramTestInvocationV1; + type Identity = (); + type Release = (); + type Capability = (); + type Measurement = ProgramTestMeasurementV1; + type Error = ProgramTestEvaluationErrorV1; + + fn identity(&self) {} + + fn release(&self) {} + + fn capability(&self) {} + + fn evaluate( + &self, + target: &ModeledSrgb8PointOccurrence, + invocation: &Self::Invocation, + ) -> Result { + EVALUATIONS.fetch_add(1, Ordering::Relaxed); + if invocation.fail_once_when_armed && FAIL_ONCE_ARMED.swap(false, Ordering::Relaxed) { + return Err(ProgramTestEvaluationErrorV1::Forced); + } + Ok(ProgramTestMeasurementV1 { + visible: Srgb8::new(target.visible()), + backdrop: Srgb8::new(target.backdrop()), + }) + } + } + + impl HardClassifier for ProgramTestEvaluatorV1 { + type Pass = ProgramTestPassV1; + type Violation = ProgramTestViolationV1; + + fn classify( + &self, + invocation: &ProgramTestInvocationV1, + measurement: &ProgramTestMeasurementV1, + ) -> HardDecision { + if measurement.visible == invocation.expected { + HardDecision::Pass(ProgramTestPassV1) + } else { + HardDecision::Violation(ProgramTestViolationV1) + } + } + } + + pub(crate) fn reset_program_test_evaluation_count() { + EVALUATIONS.store(0, Ordering::Relaxed); + FAIL_ONCE_ARMED.store(false, Ordering::Relaxed); + } + + pub(crate) fn program_test_evaluation_count() -> usize { + EVALUATIONS.load(Ordering::Relaxed) + } + + pub(crate) fn arm_program_test_failure_once() { + FAIL_ONCE_ARMED.store(true, Ordering::Relaxed); + } +} + #[cfg(test)] mod tests { use super::{ diff --git a/crates/labcolors-core/src/generic_boundary_tests.rs b/crates/labcolors-core/src/generic_boundary_tests.rs index cbc8e6a4..7fb322ed 100644 --- a/crates/labcolors-core/src/generic_boundary_tests.rs +++ b/crates/labcolors-core/src/generic_boundary_tests.rs @@ -8,7 +8,8 @@ const GENERIC_SOURCES: [(&str, &str); 3] = [ ("program_session.rs", PROGRAM_SESSION_SOURCE), ]; -const CLIENT_OR_LEGACY_VOCABULARY: [&str; 14] = [ +const CLIENT_OR_LEGACY_VOCABULARY: [&str; 15] = [ + "Lab UI", "ThemeConfig", "RoleRecipe", "RoleSpec", diff --git a/crates/labcolors-core/src/numerical_plan.rs b/crates/labcolors-core/src/numerical_plan.rs index bd05d4f5..ab674f6d 100644 --- a/crates/labcolors-core/src/numerical_plan.rs +++ b/crates/labcolors-core/src/numerical_plan.rs @@ -271,6 +271,8 @@ pub fn compile_numerical_plan_v1<'a>( #[cfg(test)] mod tests { + use std::fmt::Write as _; + use super::*; const SITE: NumericalSiteIdV1 = NumericalSiteIdV1::GlowTargetOrMaximumV1; @@ -386,11 +388,11 @@ mod tests { (b"z".as_slice(), SITE, compatibility()), ]) .unwrap(); - let hex: String = plan - .canonical_checksum_preimage() - .iter() - .map(|b| format!("{b:02x}")) - .collect(); + let preimage = plan.canonical_checksum_preimage(); + let mut hex = String::with_capacity(preimage.len() * 2); + for byte in preimage { + write!(&mut hex, "{byte:02x}").expect("writing to String cannot fail"); + } let frozen = "1b0000006c6162636f6c6f72732e6e756d65726963616c2d706c616e2e763101000000020000004f000000210000006c6162636f6c6f72732e6e756d65726963616c2d696e766f636174696f6e2e763101000000010000006119000000676c6f772d7461726765742d6f722d6d6178696d756d2d76310000000019000000676c6f772d7461726765742d6f722d6d6178696d756d2d76310b000000737461626c652d6f6e6c79000000004f000000210000006c6162636f6c6f72732e6e756d65726963616c2d696e766f636174696f6e2e763101000000010000007a19000000676c6f772d7461726765742d6f722d6d6178696d756d2d76310000000019000000676c6f772d7461726765742d6f722d6d6178696d756d2d7631160000006578706c696369742d636f6d7061746962696c69747926000000676c6f772d63616d31362d7563732d6a7072696d652d7461726765742d6f722d6d61782d7631"; assert_eq!(hex, frozen, "canonical plan encoding v1 заморожен"); assert_eq!(plan.checksum.hex(), "49e5b6b7"); diff --git a/crates/labcolors-core/src/program_session.rs b/crates/labcolors-core/src/program_session.rs index da1254c6..ce331113 100644 --- a/crates/labcolors-core/src/program_session.rs +++ b/crates/labcolors-core/src/program_session.rs @@ -1,32 +1,30 @@ -//! Terminal generation-bound path prepared for the atomic public cut. +//! Private generic point Program, constraint recheck and terminal Paint path. //! -//! [`Program`] is the authored, typed Paint/Surface/Occurrence declaration. -//! [`Program::compile`] validates and canonicalises the complete graph before a -//! [`CompiledProgram`] can exist. [`PointRenderOwner`] then becomes the sole -//! strong owner of one compiled epoch. Attached [`Session`] values retain only -//! a [`Weak`] reference, so replacement, disposal or owner drop makes the old -//! graph physically unreachable from every old session. -//! The crate root keeps this path private until the atomic public-surface cut; -//! it must not create a second simultaneously supported authoring schema. +//! The authored graph has no client/UI role vocabulary. Paints are physical +//! source-plus-straight-alpha programs, occurrences are modeled applications of +//! Paint to Surface, constraints assess those exact occurrences, and outputs +//! bind opaque slots back to Paints. A visible occurrence is evidence, never a +//! terminal emitted value. //! -//! The first executable boundary is intentionally narrow: one correlated set -//! of encoded Surface input signals per revision. It is transport-only runtime -//! state, not an observed stimulus, physical evidence or certificate, and not -//! an `lcs` identity. F0 observer/output/render identities remain a terminal -//! prerequisite before any such claim can be minted. Expanding the private -//! boundary to a ScenarioSet does not require exposing the legacy -//! multi-background metric matrix. +//! This is the encoded-sRGB8 transport-only executable slice, not the LCS +//! observation/evidence layer. +use std::marker::PhantomData; use std::mem; use std::rc::{Rc, Weak}; use crate::Srgb8; use crate::appearance::{ AdmittedAppearanceBindings, AppearanceBindings, AppearanceGraphSpec, AppearanceWorkspace, - BindingError, ColorInputId, CompileError, CompiledAppearanceGraph, OccurrenceId, - OccurrenceSpec, OpacityInputId, PaintId, PaintSpec, SurfaceId, SurfaceInputPortId, SurfaceSpec, + BindingError, ColorInputId, CompileError, CompiledAppearanceGraph, CompiledOccurrenceSlotV1, + CompiledPaintSlotV1, OccurrenceId, OccurrenceSpec, OpacityInputId, PaintId, PaintSpec, + SurfaceId, SurfaceInputPortId, SurfaceSpec, +}; +use crate::composition::{AdmittedOpacityV1, CompositionProfileV1}; +use crate::constraints::{ + HardDecision, PointEvaluationError, PointEvaluatorV1, PointInvocation, + VisiblePointPassEvidence, VisiblePointViolationEvidence, assess_visible_point_hard, }; -use crate::composition::CompositionProfileV1; /// One immutable encoded colour binding owned by a [`Program`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -36,17 +34,14 @@ pub struct ColorInput { } impl ColorInput { - /// Bind one opaque input identity to exact encoded-sRGB8 bytes. pub const fn new(id: ColorInputId, value: Srgb8) -> Self { Self { id, value } } - /// Return the opaque input identity. pub const fn id(self) -> ColorInputId { self.id } - /// Return the exact immutable value. pub const fn value(self) -> Srgb8 { self.value } @@ -60,30 +55,26 @@ pub struct OpacityInput { } impl OpacityInput { - /// Bind one opaque input identity to a finite value in `[0, 1]`. - /// - /// Numeric admission happens atomically in [`Program::compile`]. pub const fn new(id: OpacityInputId, value: f64) -> Self { Self { id, value } } - /// Return the opaque input identity. pub const fn id(self) -> OpacityInputId { self.id } - /// Return the authored binary64 value. pub const fn value(self) -> f64 { self.value } } -/// Generic Paint constructor algebra supported by the point renderer. +/// Generic point Paint constructor algebra. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Paint { - /// Create an opaque Paint from exact encoded bytes. - Solid { id: PaintId, color: ColorInputId }, - /// Multiply a Paint's straight alpha by one admitted scalar. + Solid { + id: PaintId, + color: ColorInputId, + }, Opacity { id: PaintId, source: PaintId, @@ -91,29 +82,26 @@ pub enum Paint { }, } -/// Generic Surface constructor algebra supported by the point renderer. +/// Generic point Surface constructor algebra. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Surface { - /// Read one revision-bound runtime point input. Input { id: SurfaceId, input: SurfaceInputPortId, }, - /// Give a visible occurrence result a Surface identity for nesting. FromOccurrence { id: SurfaceId, occurrence: OccurrenceId, }, } -/// Closed mathematical composition profile set for this point-program version. +/// Closed mathematical composition profile set for this Program version. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CompositionProfile { - /// Exact encoded-sRGB8 source-over with its declared byte rounding order. EncodedSrgb8SourceOverV1, } -/// The only canonical application of one Paint to one Surface. +/// The canonical application of one Paint to one Surface. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Occurrence { id: OccurrenceId, @@ -123,7 +111,6 @@ pub struct Occurrence { } impl Occurrence { - /// Declare one Paint-on-Surface application. pub const fn new( id: OccurrenceId, subject: PaintId, @@ -138,44 +125,184 @@ impl Occurrence { } } - /// Return the occurrence identity. pub const fn id(self) -> OccurrenceId { self.id } - /// Return the subject Paint identity. pub const fn subject(self) -> PaintId { self.subject } - /// Return the backdrop Surface identity. pub const fn against(self) -> SurfaceId { self.against } - /// Return the exact mathematical composition profile. pub const fn composition(self) -> CompositionProfile { self.composition } } -/// Immutable generic point-render declaration. -/// -/// List order carries no semantics. Compilation canonicalises every typed ID -/// domain and rejects dangling edges, duplicates, cycles and invalid numeric -/// inputs before any runtime owner can be constructed. -#[derive(Debug, Clone, PartialEq)] -pub struct Program { +/// Opaque authored constraint identity. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ConstraintId(u32); + +impl ConstraintId { + pub const fn new(value: u32) -> Self { + Self(value) + } + + pub const fn value(self) -> u32 { + self.0 + } +} + +/// Opaque authored terminal output identity. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct OutputSlotId(u32); + +impl OutputSlotId { + pub const fn new(value: u32) -> Self { + Self(value) + } + + pub const fn value(self) -> u32 { + self.0 + } +} + +/// Type-level marker for a mandatory constraint. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HardModeV1 {} + +/// Type-level marker for a diagnostic-only constraint. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ReportModeV1 {} + +/// One typed evaluator invocation over one exact visible occurrence. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ConstraintInvocation { + id: ConstraintId, + target: OccurrenceId, + invocation: Invocation, + mode: PhantomData Mode>, +} + +impl ConstraintInvocation { + pub const fn hard(id: ConstraintId, target: OccurrenceId, invocation: Invocation) -> Self { + Self { + id, + target, + invocation, + mode: PhantomData, + } + } +} + +impl ConstraintInvocation { + pub const fn report_only( + id: ConstraintId, + target: OccurrenceId, + invocation: Invocation, + ) -> Self { + Self { + id, + target, + invocation, + mode: PhantomData, + } + } +} + +impl ConstraintInvocation { + pub const fn id(&self) -> ConstraintId { + self.id + } + + pub const fn target(&self) -> OccurrenceId { + self.target + } + + pub const fn invocation(&self) -> &Invocation { + &self.invocation + } +} + +/// The two authored modality domains remain type-separated until compilation. +#[derive(Debug, PartialEq, Eq)] +pub struct ConstraintSet { + hard: Vec>, + report_only: Vec>, +} + +impl ConstraintSet { + pub fn new( + hard: Vec>, + report_only: Vec>, + ) -> Self { + Self { hard, report_only } + } + + pub fn hard(&self) -> &[ConstraintInvocation] { + &self.hard + } + + pub fn report_only(&self) -> &[ConstraintInvocation] { + &self.report_only + } + + fn is_empty(&self) -> bool { + self.hard.is_empty() && self.report_only.is_empty() + } + + fn checked_len(&self) -> Option { + self.hard.len().checked_add(self.report_only.len()) + } +} + +/// Compile-time binding from one terminal slot to one Paint. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct OutputBinding { + output: OutputSlotId, + paint: PaintId, +} + +impl OutputBinding { + pub const fn new(output: OutputSlotId, paint: PaintId) -> Self { + Self { output, paint } + } + + pub const fn output(self) -> OutputSlotId { + self.output + } + + pub const fn paint(self) -> PaintId { + self.paint + } +} + +/// Immutable generic point Program. +pub struct Program +where + Evaluation: PointEvaluatorV1, + PointInvocation: Copy, +{ colors: Vec, surface_input_ports: Vec, opacities: Vec, paints: Vec, surfaces: Vec, occurrences: Vec, + constraints: ConstraintSet>, + outputs: Vec, + evaluator: Evaluation, } -impl Program { - /// Assemble one declaration. This constructor performs no partial compile. +impl Program +where + Evaluation: PointEvaluatorV1, + PointInvocation: Copy, +{ + #[allow(clippy::too_many_arguments)] pub fn new( colors: Vec, surface_input_ports: Vec, @@ -183,6 +310,9 @@ impl Program { paints: Vec, surfaces: Vec, occurrences: Vec, + constraints: ConstraintSet>, + outputs: Vec, + evaluator: Evaluation, ) -> Self { Self { colors, @@ -191,17 +321,19 @@ impl Program { paints, surfaces, occurrences, + constraints, + outputs, + evaluator, } } - /// Atomically validate, bind and canonicalise this complete declaration. - pub fn compile(self) -> Result { + pub fn compile(self) -> Result, ProgramCompileError> { prepare_program(self).map(|epoch| CompiledProgram { epoch }) } } -/// Public compile failure; every variant leaves no executable partial graph. -#[derive(Debug, Clone, PartialEq, Eq)] +/// Atomic compile failure. No executable partial graph escapes. +#[derive(Debug, PartialEq, Eq)] pub enum ProgramCompileError { DuplicateColorInput { input: ColorInputId, @@ -261,45 +393,97 @@ pub enum ProgramCompileError { }, EmptySurfaceSchema, EmptyOccurrenceSet, + EmptyConstraintSet, + EmptyOutputSet, + DuplicateConstraint { + constraint: ConstraintId, + }, + MissingConstraintOccurrence { + constraint: ConstraintId, + occurrence: OccurrenceId, + }, + DuplicateOutputSlot { + output: OutputSlotId, + }, + MissingOutputPaint { + output: OutputSlotId, + paint: PaintId, + }, ResourceExhausted, InternalInvariant, } -#[derive(Debug)] -struct ProgramEpochV1 { +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum CompiledConstraintModeV1 { + Hard, + ReportOnly, +} + +struct CompiledPointConstraint { + id: ConstraintId, + target_id: OccurrenceId, + target: CompiledOccurrenceSlotV1, + mode: CompiledConstraintModeV1, + invocation: Invocation, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct CompiledOutputBinding { + output: OutputSlotId, + paint_id: PaintId, + paint: CompiledPaintSlotV1, +} + +struct ProgramEpochV1 +where + Evaluation: PointEvaluatorV1, + PointInvocation: Copy, +{ + evaluator: Evaluation, graph: CompiledAppearanceGraph, binding_template: AdmittedAppearanceBindings, surface_input_ports: Box<[SurfaceInputPortId]>, - occurrence_ids: Box<[OccurrenceId]>, + constraints: Box<[CompiledPointConstraint>]>, + outputs: Box<[CompiledOutputBinding]>, } -/// Fully validated immutable point-render program, not yet attached to runtime. -/// -/// This value is deliberately not `Clone`: moving it into an owner establishes -/// one unambiguous strong-ownership root for its compiled epoch. -#[derive(Debug)] -pub struct CompiledProgram { - epoch: ProgramEpochV1, +/// Fully validated immutable Program, not yet attached to runtime. +pub struct CompiledProgram +where + Evaluation: PointEvaluatorV1, + PointInvocation: Copy, +{ + epoch: ProgramEpochV1, } -impl CompiledProgram { - /// Canonical Surface-input order required by [`SurfaceUpdate::Present`]. +impl CompiledProgram +where + Evaluation: PointEvaluatorV1, + PointInvocation: Copy, +{ pub fn surface_input_ports(&self) -> &[SurfaceInputPortId] { &self.epoch.surface_input_ports } - /// Canonical occurrence order emitted by every [`Snapshot`]. - pub fn occurrences(&self) -> &[OccurrenceId] { - &self.epoch.occurrence_ids + pub fn constraint_ids(&self) -> impl ExactSizeIterator + '_ { + self.epoch + .constraints + .iter() + .map(|constraint| constraint.id) + } + + pub fn outputs(&self) -> impl ExactSizeIterator + '_ { + self.epoch + .outputs + .iter() + .map(|output| (output.output, output.paint_id)) } - /// Transfer this compiled epoch to its sole runtime owner. - pub fn into_owner(self) -> PointRenderOwner { + pub fn into_owner(self) -> PointRenderOwner { PointRenderOwner::new(self) } } -/// Failure while preparing an independent allocation-owning [`Session`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PointRenderAttachError { Disposed, @@ -307,50 +491,57 @@ pub enum PointRenderAttachError { InternalInvariant, } -/// The only strong owner of the current non-reusable program epoch. -/// -/// Replacement accepts only an already complete [`CompiledProgram`]. Compile -/// failure therefore happens before the swap and cannot revoke the live epoch. -/// No numeric generation participates in this ownership proof. -#[derive(Debug)] -pub struct PointRenderOwner { - current: Option>, +/// Sole strong owner of one non-reusable compiled epoch. +pub struct PointRenderOwner +where + Evaluation: PointEvaluatorV1, + PointInvocation: Copy, +{ + current: Option>>, } -impl PointRenderOwner { - /// Establish the sole strong owner of one compiled epoch. - pub fn new(compiled: CompiledProgram) -> Self { +impl PointRenderOwner +where + Evaluation: PointEvaluatorV1, + PointInvocation: Copy, +{ + pub fn new(compiled: CompiledProgram) -> Self { Self { current: Some(Rc::new(compiled.epoch)), } } - /// Atomically replace the current epoch and revoke all attached old sessions. - pub fn replace(&mut self, compiled: CompiledProgram) { + pub fn replace(&mut self, compiled: CompiledProgram) { self.current = Some(Rc::new(compiled.epoch)); } - /// Revoke the current epoch. Existing sessions fail on their next call. pub fn dispose(&mut self) { self.current = None; } - /// Return the current canonical Surface-input order, or `None` if disposed. pub fn surface_input_ports(&self) -> Option<&[SurfaceInputPortId]> { self.current .as_deref() .map(|epoch| epoch.surface_input_ports.as_ref()) } - /// Return the current canonical occurrence order, or `None` if disposed. - pub fn occurrences(&self) -> Option<&[OccurrenceId]> { + pub fn constraint_ids(&self) -> Option + '_> { self.current .as_deref() - .map(|epoch| epoch.occurrence_ids.as_ref()) + .map(|epoch| epoch.constraints.iter().map(|constraint| constraint.id)) + } + + pub fn outputs(&self) -> Option + '_> { + self.current.as_deref().map(|epoch| { + epoch + .outputs + .iter() + .map(|output| (output.output, output.paint_id)) + }) } - /// Allocate all independent mutable storage before a Session escapes. - pub fn attach(&self) -> Result { + /// Fallibly allocate every hot-path frame before the Session escapes. + pub fn attach(&self) -> Result, PointRenderAttachError> { let epoch = self .current .as_ref() @@ -363,13 +554,16 @@ impl PointRenderOwner { .binding_template .try_clone_v1() .map_err(map_attach_binding_error)?; - let initial_signal_buffers = - CompositedSignalBuffersV1::try_new(&epoch.surface_input_ports, &epoch.occurrence_ids)?; + let free_frames = [ + Some(ExecutionFrame::try_new(epoch)?), + Some(ExecutionFrame::try_new(epoch)?), + Some(ExecutionFrame::try_new(epoch)?), + ]; Ok(Session { epoch: Rc::downgrade(epoch), bindings, workspace, - initial_signal_buffers: Some(initial_signal_buffers), + free_frames, state: SessionState::Waiting { current_unavailable: None, }, @@ -384,25 +578,35 @@ fn map_attach_binding_error(error: BindingError) -> PointRenderAttachError { } } -fn try_zeroed_signal_words(len: usize) -> Result, PointRenderAttachError> { - let mut words = Vec::new(); - words - .try_reserve_exact(len) - .map_err(|_| PointRenderAttachError::ResourceExhausted)?; - words.resize(len, 0); - Ok(words) -} - -fn prepare_program(program: Program) -> Result { +fn prepare_program( + program: Program, +) -> Result, ProgramCompileError> +where + Evaluation: PointEvaluatorV1, + PointInvocation: Copy, +{ if program.surface_input_ports.is_empty() { return Err(ProgramCompileError::EmptySurfaceSchema); } if program.occurrences.is_empty() { return Err(ProgramCompileError::EmptyOccurrenceSet); } + if program.constraints.is_empty() { + return Err(ProgramCompileError::EmptyConstraintSet); + } + if program.outputs.is_empty() { + return Err(ProgramCompileError::EmptyOutputSet); + } check_render_node_count(program.surfaces.len(), program.occurrences.len())?; + program + .constraints + .checked_len() + .ok_or(ProgramCompileError::ResourceExhausted)?; let graph = lower_graph(&program).compile().map_err(map_compile_error)?; + let binding_template = graph + .admit_bindings(&lower_bindings(&program)) + .map_err(map_binding_compile_error)?; let mut surface_input_ports = Vec::new(); surface_input_ports @@ -410,33 +614,149 @@ fn prepare_program(program: Program) -> Result(&graph, program.constraints)?; + let outputs = compile_outputs(&graph, program.outputs)?; Ok(ProgramEpochV1 { + evaluator: program.evaluator, graph, binding_template, surface_input_ports: surface_input_ports.into_boxed_slice(), - occurrence_ids: occurrence_ids.into_boxed_slice(), + constraints, + outputs, }) } +struct LoweredConstraint { + id: ConstraintId, + target: OccurrenceId, + mode: CompiledConstraintModeV1, + invocation: Invocation, +} + +fn compile_constraints( + graph: &CompiledAppearanceGraph, + authored: ConstraintSet>, +) -> Result>]>, ProgramCompileError> +where + Evaluation: PointEvaluatorV1, + PointInvocation: Copy, +{ + let total = authored + .hard + .len() + .checked_add(authored.report_only.len()) + .ok_or(ProgramCompileError::ResourceExhausted)?; + let mut lowered = Vec::new(); + 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 + .report_only + .into_iter() + .map(|constraint| LoweredConstraint { + id: constraint.id, + target: constraint.target, + mode: CompiledConstraintModeV1::ReportOnly, + invocation: constraint.invocation, + }), + ); + lowered.sort_unstable_by_key(|constraint| constraint.id); + if let Some(duplicate) = lowered + .windows(2) + .find(|pair| pair[0].id == pair[1].id) + .map(|pair| pair[0].id) + { + return Err(ProgramCompileError::DuplicateConstraint { + constraint: duplicate, + }); + } + for constraint in &lowered { + if graph.bind_occurrence(constraint.target).is_none() { + return Err(ProgramCompileError::MissingConstraintOccurrence { + constraint: constraint.id, + occurrence: constraint.target, + }); + } + } + + let mut compiled = Vec::new(); + compiled + .try_reserve_exact(total) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + for constraint in lowered { + let target = graph + .bind_occurrence(constraint.target) + .ok_or(ProgramCompileError::InternalInvariant)?; + compiled.push(CompiledPointConstraint { + id: constraint.id, + target_id: constraint.target, + target, + mode: constraint.mode, + invocation: constraint.invocation, + }); + } + Ok(compiled.into_boxed_slice()) +} + +fn compile_outputs( + graph: &CompiledAppearanceGraph, + authored: Vec, +) -> 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) + .find(|pair| pair[0].output == pair[1].output) + .map(|pair| pair[0].output) + { + return Err(ProgramCompileError::DuplicateOutputSlot { output: duplicate }); + } + for output in &authored { + if graph.bind_paint(output.paint).is_none() { + return Err(ProgramCompileError::MissingOutputPaint { + output: output.output, + paint: output.paint, + }); + } + } + + let mut compiled = Vec::new(); + compiled + .try_reserve_exact(len) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + for output in authored { + let paint = graph + .bind_paint(output.paint) + .ok_or(ProgramCompileError::InternalInvariant)?; + compiled.push(CompiledOutputBinding { + output: output.output, + paint_id: output.paint, + paint, + }); + } + Ok(compiled.into_boxed_slice()) +} + pub(crate) fn check_render_node_count( surface_count: usize, occurrence_count: usize, @@ -454,14 +774,11 @@ pub(crate) fn canonical_surface_input_port_sequence_matches( actual.into_iter().eq(expected.iter().copied()) } -pub(crate) fn canonical_occurrence_sequence_matches( - actual: impl IntoIterator, - expected: &[OccurrenceId], -) -> bool { - actual.into_iter().eq(expected.iter().copied()) -} - -fn lower_graph(program: &Program) -> AppearanceGraphSpec { +fn lower_graph(program: &Program) -> AppearanceGraphSpec +where + Evaluation: PointEvaluatorV1, + PointInvocation: Copy, +{ AppearanceGraphSpec::new( program.colors.iter().map(|input| input.id).collect(), program.surface_input_ports.clone(), @@ -509,7 +826,11 @@ fn lower_graph(program: &Program) -> AppearanceGraphSpec { ) } -fn lower_bindings(program: &Program) -> AppearanceBindings { +fn lower_bindings(program: &Program) -> AppearanceBindings +where + Evaluation: PointEvaluatorV1, + PointInvocation: Copy, +{ AppearanceBindings::new( program .colors @@ -597,7 +918,7 @@ fn map_binding_compile_error(error: BindingError) -> ProgramCompileError { } } -/// One revision-bound absence of the correlated Surface-input set. +/// Revision-bound unavailable input descriptor. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct SurfaceUnavailable { revision: u64, @@ -605,18 +926,16 @@ pub struct SurfaceUnavailable { } impl SurfaceUnavailable { - /// Return the stream revision carrying this absence. pub const fn revision(self) -> u64 { self.revision } - /// Return the client-owned opaque absence reason. pub const fn reason(self) -> u32 { self.reason } } -/// One exact runtime Surface-input value. +/// One typed runtime Surface input signal. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct SurfaceSignal { input: SurfaceInputPortId, @@ -624,169 +943,350 @@ pub struct SurfaceSignal { } impl SurfaceSignal { - /// Bind one runtime input identity to exact encoded bytes. pub const fn new(input: SurfaceInputPortId, value: Srgb8) -> Self { Self { input, value } } - /// Return the runtime input identity. pub const fn input(self) -> SurfaceInputPortId { self.input } - /// Return the exact encoded value. pub const fn value(self) -> Srgb8 { self.value } } -/// One exact visible value emitted for a compiled occurrence. +/// Borrowed, correlated runtime update for one attached Session. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct OccurrenceSignal { - occurrence: OccurrenceId, - value: Srgb8, +pub enum SurfaceUpdate<'input> { + Unavailable { + revision: u64, + reason: u32, + }, + Present { + revision: u64, + surfaces: &'input [SurfaceSignal], + }, +} + +/// Evaluator classification with the exact bound occurrence evidence. +pub enum ConstraintOutcome +where + Evaluation: PointEvaluatorV1, +{ + Pass(VisiblePointPassEvidence), + Violation(VisiblePointViolationEvidence), } -impl OccurrenceSignal { - /// Return the compiled occurrence identity. - pub const fn occurrence(self) -> OccurrenceId { - self.occurrence +/// One mode-refined report cell. +pub struct ConstraintAssessment +where + Evaluation: PointEvaluatorV1, +{ + constraint: ConstraintId, + target: OccurrenceId, + outcome: ConstraintOutcome, + mode: PhantomData Mode>, +} + +impl ConstraintAssessment +where + Evaluation: PointEvaluatorV1, +{ + fn new( + constraint: ConstraintId, + target: OccurrenceId, + outcome: ConstraintOutcome, + ) -> Self { + Self { + constraint, + target, + outcome, + mode: PhantomData, + } } - /// Return the exact encoded visible value. - pub const fn value(self) -> Srgb8 { - self.value + pub const fn constraint(&self) -> ConstraintId { + self.constraint + } + + pub const fn target(&self) -> OccurrenceId { + self.target + } + + pub const fn outcome(&self) -> &ConstraintOutcome { + &self.outcome } } -/// Borrowed, correlated runtime update for one attached Session. +/// Canonical full-report entry; authored mode remains visible in the type. +pub enum ConstraintReportEntry +where + Evaluation: PointEvaluatorV1, +{ + Hard(ConstraintAssessment), + ReportOnly(ConstraintAssessment), +} + +impl ConstraintReportEntry +where + Evaluation: PointEvaluatorV1, +{ + pub const fn constraint(&self) -> ConstraintId { + match self { + Self::Hard(assessment) => assessment.constraint, + Self::ReportOnly(assessment) => assessment.constraint, + } + } + + pub const fn target(&self) -> OccurrenceId { + match self { + Self::Hard(assessment) => assessment.target, + Self::ReportOnly(assessment) => assessment.target, + } + } +} + +/// Pure terminal Paint value. Routing identities are intentionally outside it. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum SurfaceUpdate<'input> { - /// The entire Surface-input set is unavailable at this revision. - Unavailable { revision: u64, reason: u32 }, - /// The complete input set in [`CompiledProgram::surface_input_ports`] order. - Present { - revision: u64, - surfaces: &'input [SurfaceSignal], - }, +pub struct OutputPaintV1 { + source: Srgb8, + straight_alpha: AdmittedOpacityV1, } -/// Compact committed value: one word per compiled occurrence, in the graph's -/// canonical occurrence order. No metric, threshold or JS-derived verdict is -/// present on this boundary. -#[derive(Debug, PartialEq, Eq)] -struct CompositedSignalBuffersV1 { - surface_input_ports: Box<[SurfaceInputPortId]>, - input_surface_signals_rgb24: Vec, - occurrence_ids: Box<[OccurrenceId]>, - composited_occurrence_signals_rgb24: Vec, +impl OutputPaintV1 { + pub const fn source(self) -> Srgb8 { + self.source + } + + pub const fn straight_alpha(self) -> f64 { + self.straight_alpha.value() + } + + pub const fn straight_alpha_bits(self) -> u64 { + self.straight_alpha.bits() + } +} + +/// One routed terminal cell: opaque client slot, authored Paint identity and +/// the independent physical Paint value produced for it. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct OutputValueV1 { + output: OutputSlotId, + paint: PaintId, + value: OutputPaintV1, +} + +impl OutputValueV1 { + pub const fn output(self) -> OutputSlotId { + self.output + } + + pub const fn paint(self) -> PaintId { + self.paint + } + + pub const fn value(self) -> OutputPaintV1 { + self.value + } } -impl CompositedSignalBuffersV1 { - fn try_new( - surface_input_ports: &[SurfaceInputPortId], - occurrence_ids: &[OccurrenceId], - ) -> Result { +struct ExecutionFrame +where + Evaluation: PointEvaluatorV1, +{ + surfaces: Box<[SurfaceSignal]>, + reports: Vec>>, + outputs: Vec>, +} + +impl ExecutionFrame +where + Evaluation: PointEvaluatorV1, + PointInvocation: Copy, +{ + fn try_new(epoch: &ProgramEpochV1) -> Result { + let mut surfaces = Vec::new(); + surfaces + .try_reserve_exact(epoch.surface_input_ports.len()) + .map_err(|_| PointRenderAttachError::ResourceExhausted)?; + surfaces.extend( + epoch + .surface_input_ports + .iter() + .copied() + .map(|input| SurfaceSignal::new(input, Srgb8::new([0; 3]))), + ); + + let mut reports = Vec::new(); + reports + .try_reserve_exact(epoch.constraints.len()) + .map_err(|_| PointRenderAttachError::ResourceExhausted)?; + reports.resize_with(epoch.constraints.len(), || None); + + let mut outputs = Vec::new(); + outputs + .try_reserve_exact(epoch.outputs.len()) + .map_err(|_| PointRenderAttachError::ResourceExhausted)?; + outputs.resize_with(epoch.outputs.len(), || None); + Ok(Self { - surface_input_ports: try_copy_ids(surface_input_ports)?, - input_surface_signals_rgb24: try_zeroed_signal_words(surface_input_ports.len())?, - occurrence_ids: try_copy_ids(occurrence_ids)?, - composited_occurrence_signals_rgb24: try_zeroed_signal_words(occurrence_ids.len())?, + surfaces: surfaces.into_boxed_slice(), + reports, + outputs, + }) + } + + fn clear_dynamic(&mut self) { + for report in &mut self.reports { + report.take(); + } + for output in &mut self.outputs { + output.take(); + } + } + + fn report(&self) -> impl ExactSizeIterator> + '_ { + self.reports.iter().map(|report| { + report + .as_ref() + .unwrap_or_else(|| unreachable!("committed report is complete")) }) } -} -fn try_copy_ids(values: &[T]) -> Result, PointRenderAttachError> { - let mut copied = Vec::new(); - copied - .try_reserve_exact(values.len()) - .map_err(|_| PointRenderAttachError::ResourceExhausted)?; - copied.extend_from_slice(values); - Ok(copied.into_boxed_slice()) + fn outputs(&self) -> impl ExactSizeIterator + '_ { + self.outputs.iter().map(|output| { + output + .as_ref() + .copied() + .unwrap_or_else(|| unreachable!("verified output set is complete")) + }) + } + + fn present_payload_matches(&self, mut value_at: impl FnMut(usize) -> Srgb8) -> bool { + let mut exact = true; + for (index, signal) in self.surfaces.iter().enumerate() { + if value_at(index) != signal.value { + exact = false; + } + } + exact + } } -/// One committed, revision-bound point-render result. -#[derive(Debug, PartialEq, Eq)] -pub struct Snapshot { +/// One hard-admitted snapshot with full evidence and terminal Paints. +pub struct Snapshot +where + Evaluation: PointEvaluatorV1, +{ revision: u64, - buffers: CompositedSignalBuffersV1, + frame: ExecutionFrame, } -impl Snapshot { - /// Return the exact revision used for every input and output in this value. +impl Snapshot +where + Evaluation: PointEvaluatorV1, + PointInvocation: Copy, +{ pub const fn revision(&self) -> u64 { self.revision } - /// Iterate admitted inputs in canonical compiled order without allocation. pub fn surfaces(&self) -> impl ExactSizeIterator + '_ { - self.buffers - .surface_input_ports - .iter() - .copied() - .zip(self.buffers.input_surface_signals_rgb24.iter().copied()) - .map(|(input, value)| SurfaceSignal { - input, - value: Srgb8::new(unpack_rgb24(value)), - }) + self.frame.surfaces.iter().copied() } - /// Iterate visible occurrence values in canonical compiled order. - pub fn occurrences(&self) -> impl ExactSizeIterator + '_ { - self.buffers - .occurrence_ids - .iter() - .copied() - .zip( - self.buffers - .composited_occurrence_signals_rgb24 - .iter() - .copied(), - ) - .map(|(occurrence, value)| OccurrenceSignal { - occurrence, - value: Srgb8::new(unpack_rgb24(value)), - }) + pub fn report(&self) -> impl ExactSizeIterator> + '_ { + self.frame.report() } - /// Look up one canonical occurrence output without allocation. - pub fn occurrence(&self, occurrence: OccurrenceId) -> Option { - self.buffers - .occurrence_ids - .binary_search(&occurrence) - .ok() - .map(|index| { - Srgb8::new(unpack_rgb24( - self.buffers.composited_occurrence_signals_rgb24[index], - )) + pub fn outputs(&self) -> impl ExactSizeIterator + '_ { + self.frame.outputs() + } + + pub fn output(&self, output: OutputSlotId) -> Option { + let index = self + .frame + .outputs + .binary_search_by_key(&output, |slot| { + slot.as_ref() + .unwrap_or_else(|| unreachable!("verified output set is complete")) + .output }) + .ok()?; + self.frame.outputs[index] + } + + #[cfg(test)] + pub(crate) fn storage_pointers_for_test(&self) -> (*const SurfaceSignal, *const ()) { + ( + self.frame.surfaces.as_ptr(), + self.frame.reports.as_ptr().cast(), + ) + } +} + +/// Complete current report containing at least one hard violation. +pub struct ConstraintConflict +where + Evaluation: PointEvaluatorV1, +{ + revision: u64, + frame: ExecutionFrame, +} + +impl ConstraintConflict +where + Evaluation: PointEvaluatorV1, + PointInvocation: Copy, +{ + pub const fn revision(&self) -> u64 { + self.revision } - pub(crate) fn input_surface_signals_rgb24(&self) -> &[u32] { - &self.buffers.input_surface_signals_rgb24 + pub fn surfaces(&self) -> impl ExactSizeIterator + '_ { + self.frame.surfaces.iter().copied() } - pub(crate) fn composited_occurrence_signals_rgb24(&self) -> &[u32] { - &self.buffers.composited_occurrence_signals_rgb24 + pub fn report(&self) -> impl ExactSizeIterator> + '_ { + self.frame.report() + } + + #[cfg(test)] + pub(crate) fn storage_pointers_for_test(&self) -> (*const SurfaceSignal, *const ()) { + ( + self.frame.surfaces.as_ptr(), + self.frame.reports.as_ptr().cast(), + ) } } -/// Current state of one generation-bound Session. -#[derive(Debug, PartialEq, Eq)] -pub enum SessionState { +/// Current lifecycle state of one generation-bound Session. +pub enum SessionState +where + Evaluation: PointEvaluatorV1, +{ Waiting { current_unavailable: Option, }, Ready { - current: Snapshot, + current: Snapshot, }, Stale { - previous: Snapshot, + previous: Snapshot, current_unavailable: SurfaceUnavailable, }, + Conflict { + current: ConstraintConflict, + previous: Option>, + }, } -impl SessionState { +impl SessionState +where + Evaluation: PointEvaluatorV1, + PointInvocation: Copy, +{ const fn head_revision(&self) -> Option { match self { Self::Waiting { @@ -800,13 +1300,15 @@ impl SessionState { .. } => Some(unavailable.revision), Self::Ready { current } => Some(current.revision), + Self::Conflict { current, .. } => Some(current.revision), } } } -/// Failure to admit or execute one typed Session update. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum SessionUpdateError { +/// Failure to admit or evaluate one Session update. A hard violation is not an +/// error; it commits [`SessionState::Conflict`] with its full report. +#[derive(Debug, PartialEq, Eq)] +pub enum SessionUpdateError { ProgramExpired, SurfaceInputPortLengthMismatch { expected: usize, @@ -824,40 +1326,40 @@ pub enum SessionUpdateError { RevisionConflict { revision: u64, }, + Evaluator { + constraint: ConstraintId, + source: EvaluationError, + }, InternalInvariant, } -/// Generation-bound mutable runtime. It owns reusable values/scratch, never a -/// strong reference or a copy of the compiled graph. All fixed-cardinality -/// signal buffers are allocated fallibly by `attach`; updates only move and -/// overwrite their ownership after evaluation succeeds. -#[derive(Debug)] -pub struct Session { - epoch: Weak, +/// Mutable runtime with three attach-allocated transactional frames. +pub struct Session +where + Evaluation: PointEvaluatorV1, + PointInvocation: Copy, +{ + epoch: Weak>, bindings: AdmittedAppearanceBindings, workspace: AppearanceWorkspace, - initial_signal_buffers: Option, - state: SessionState, + free_frames: [Option>; 3], + state: SessionState, } -impl Session { - /// Borrow the current committed state. - pub const fn state(&self) -> &SessionState { +impl Session +where + Evaluation: PointEvaluatorV1, + PointInvocation: Copy, +{ + pub const fn state(&self) -> &SessionState { &self.state } - #[cfg(test)] - pub(crate) fn bound_surface_inputs_for_test( - &self, - ) -> impl ExactSizeIterator + '_ { - self.bindings.surface_inputs_canonical() - } - - /// Admit, evaluate and atomically commit one typed Surface-input update. pub fn update( &mut self, update: SurfaceUpdate<'_>, - ) -> Result<&SessionState, SessionUpdateError> { + ) -> Result<&SessionState, SessionUpdateError>> + { let epoch = self .epoch .upgrade() @@ -894,15 +1396,13 @@ impl Session { } } - /// Read one complete canonical Surface-input set lazily and commit it as one - /// revision. The callback is not invoked until lifetime, cardinality and - /// revision admission have all succeeded. pub(crate) fn update_canonical_present( &mut self, revision: u64, surface_input_port_count: usize, value_at: impl FnMut(usize) -> Srgb8, - ) -> Result<&SessionState, SessionUpdateError> { + ) -> Result<&SessionState, SessionUpdateError>> + { let epoch = self .epoch .upgrade() @@ -913,7 +1413,8 @@ impl Session { fn apply_unavailable( &mut self, unavailable: SurfaceUnavailable, - ) -> Result<&SessionState, SessionUpdateError> { + ) -> Result<&SessionState, SessionUpdateError>> + { let incoming_revision = unavailable.revision; if let Some(current) = self.state.head_revision() { if incoming_revision < current { @@ -941,7 +1442,7 @@ impl Session { } } - let previous = take_last_ready(&mut self.state); + let previous = self.take_last_verified_and_recycle_current(); self.state = match previous { Some(previous) => SessionState::Stale { previous, @@ -956,18 +1457,18 @@ impl Session { fn apply_canonical_present( &mut self, - epoch: &ProgramEpochV1, + epoch: &ProgramEpochV1, revision: u64, surface_input_port_count: usize, mut value_at: impl FnMut(usize) -> Srgb8, - ) -> Result<&SessionState, SessionUpdateError> { + ) -> Result<&SessionState, SessionUpdateError>> + { if surface_input_port_count != epoch.surface_input_ports.len() { return Err(SessionUpdateError::SurfaceInputPortLengthMismatch { expected: epoch.surface_input_ports.len(), actual: surface_input_port_count, }); } - if let Some(current) = self.state.head_revision() { if revision < current { return Err(SessionUpdateError::RevisionOutOfOrder { @@ -976,136 +1477,195 @@ impl Session { }); } if revision == current { - return self.admit_same_revision_present(revision, &mut value_at); + return self.admit_same_revision_present(revision, value_at); } } - let retained_shape_matches = match &self.state { - SessionState::Waiting { .. } => { - self.initial_signal_buffers.as_ref().is_some_and(|buffers| { - buffers.input_surface_signals_rgb24.len() == epoch.surface_input_ports.len() - && buffers.composited_occurrence_signals_rgb24.len() - == epoch.occurrence_ids.len() - }) - } - SessionState::Ready { current } => { - current.buffers.input_surface_signals_rgb24.len() == epoch.surface_input_ports.len() - && current.buffers.composited_occurrence_signals_rgb24.len() - == epoch.occurrence_ids.len() - } - SessionState::Stale { previous, .. } => { - previous.buffers.input_surface_signals_rgb24.len() - == epoch.surface_input_ports.len() - && previous.buffers.composited_occurrence_signals_rgb24.len() - == epoch.occurrence_ids.len() - } - }; - if !retained_shape_matches { + let mut frame = + take_free_frame(&mut self.free_frames).ok_or(SessionUpdateError::InternalInvariant)?; + frame.clear_dynamic(); + if frame.surfaces.len() != epoch.surface_input_ports.len() + || frame.reports.len() != epoch.constraints.len() + || frame.outputs.len() != epoch.outputs.len() + { + put_free_frame(&mut self.free_frames, frame); return Err(SessionUpdateError::InternalInvariant); } - self.bindings + let surface_slots = &mut frame.surfaces; + if self + .bindings .overwrite_surface_inputs_canonical( epoch.surface_input_ports.iter().copied(), - &mut value_at, + &mut |index| { + let value = value_at(index); + surface_slots[index] = + SurfaceSignal::new(epoch.surface_input_ports[index], value); + value + }, ) - .map_err(|_| SessionUpdateError::InternalInvariant)?; - let evaluation = epoch - .graph - .evaluate_admitted_into(&self.bindings, &mut self.workspace) - .map_err(|_| SessionUpdateError::InternalInvariant)?; - if evaluation.occurrences().len() != epoch.occurrence_ids.len() { + .is_err() + { + put_free_frame(&mut self.free_frames, frame); return Err(SessionUpdateError::InternalInvariant); } - // No fallible work follows. The callback is already gone from the - // commit path: the input snapshot is read back from the admitted - // canonical bindings that evaluation consumed. - let mut buffers = match take_last_ready(&mut self.state) { - Some(previous) => previous.buffers, - None => self.initial_signal_buffers.take().unwrap_or_else(|| { - unreachable!("a Session without prior Ready must retain initial buffers") - }), - }; - debug_assert_eq!( - buffers.input_surface_signals_rgb24.len(), - surface_input_port_count - ); - debug_assert_eq!( - buffers.composited_occurrence_signals_rgb24.len(), - epoch.occurrence_ids.len() - ); - for (((input, value), expected), output) in self - .bindings - .surface_inputs_canonical() - .zip(buffers.surface_input_ports.iter().copied()) - .zip(buffers.input_surface_signals_rgb24.iter_mut()) + let evaluation = match epoch + .graph + .evaluate_admitted_into(&self.bindings, &mut self.workspace) { - debug_assert_eq!(input, expected); - *output = pack_rgb24(value.bytes()); + Ok(evaluation) => evaluation, + Err(_) => { + put_free_frame(&mut self.free_frames, frame); + return Err(SessionUpdateError::InternalInvariant); + } + }; + + let mut has_hard_violation = false; + for (index, constraint) in epoch.constraints.iter().enumerate() { + let Some(source) = evaluation.occurrence_at(constraint.target) else { + put_free_frame(&mut self.free_frames, frame); + return Err(SessionUpdateError::InternalInvariant); + }; + let decision = + match assess_visible_point_hard(source, &epoch.evaluator, constraint.invocation) { + Ok(decision) => decision, + Err(source) => { + put_free_frame(&mut self.free_frames, frame); + return Err(SessionUpdateError::Evaluator { + constraint: constraint.id, + source, + }); + } + }; + let (outcome, violation) = match decision { + HardDecision::Pass(evidence) => (ConstraintOutcome::Pass(evidence), false), + HardDecision::Violation(evidence) => (ConstraintOutcome::Violation(evidence), true), + }; + frame.reports[index] = Some(match constraint.mode { + CompiledConstraintModeV1::Hard => { + has_hard_violation |= violation; + ConstraintReportEntry::Hard(ConstraintAssessment::new( + constraint.id, + constraint.target_id, + outcome, + )) + } + CompiledConstraintModeV1::ReportOnly => ConstraintReportEntry::ReportOnly( + ConstraintAssessment::new(constraint.id, constraint.target_id, outcome), + ), + }); } - for (resolved, output) in evaluation - .occurrences() - .zip(buffers.composited_occurrence_signals_rgb24.iter_mut()) - { - *output = pack_rgb24(resolved.visible()); + + if !has_hard_violation { + for (index, output) in epoch.outputs.iter().enumerate() { + let Some(paint) = evaluation.paint_at(output.paint) else { + put_free_frame(&mut self.free_frames, frame); + return Err(SessionUpdateError::InternalInvariant); + }; + frame.outputs[index] = Some(OutputValueV1 { + output: output.output, + paint: output.paint_id, + value: OutputPaintV1 { + source: paint.source(), + straight_alpha: paint.opacity(), + }, + }); + } + } + if has_hard_violation { + let previous = self.take_last_verified_and_recycle_current(); + self.state = SessionState::Conflict { + current: ConstraintConflict { revision, frame }, + previous, + }; + } else { + self.recycle_entire_state(); + self.state = SessionState::Ready { + current: Snapshot { revision, frame }, + }; } - self.state = SessionState::Ready { - current: Snapshot { revision, buffers }, - }; Ok(&self.state) } fn admit_same_revision_present( &self, revision: u64, - mut value_at: impl FnMut(usize) -> Srgb8, - ) -> Result<&SessionState, SessionUpdateError> { - let SessionState::Ready { current } = &self.state else { - return Err(SessionUpdateError::RevisionConflict { revision }); - }; - debug_assert_eq!(current.revision, revision); - - let mut exact = true; - for (index, &expected) in current - .buffers - .input_surface_signals_rgb24 - .iter() - .enumerate() - { - if pack_rgb24(value_at(index).bytes()) != expected { - exact = false; + value_at: impl FnMut(usize) -> Srgb8, + ) -> Result<&SessionState, SessionUpdateError>> + { + let exact = match &self.state { + SessionState::Ready { current } => current.frame.present_payload_matches(value_at), + SessionState::Conflict { current, .. } => { + current.frame.present_payload_matches(value_at) } - } + _ => return Err(SessionUpdateError::RevisionConflict { revision }), + }; if exact { Ok(&self.state) } else { Err(SessionUpdateError::RevisionConflict { revision }) } } -} -fn take_last_ready(state: &mut SessionState) -> Option { - match mem::replace( - state, - SessionState::Waiting { - current_unavailable: None, - }, - ) { - SessionState::Waiting { .. } => None, - SessionState::Ready { current } => Some(current), - SessionState::Stale { previous, .. } => Some(previous), + fn take_last_verified_and_recycle_current(&mut self) -> Option> { + match mem::replace( + &mut self.state, + SessionState::Waiting { + current_unavailable: None, + }, + ) { + SessionState::Waiting { .. } => None, + SessionState::Ready { current } => Some(current), + SessionState::Stale { previous, .. } => Some(previous), + SessionState::Conflict { current, previous } => { + put_free_frame(&mut self.free_frames, current.frame); + previous + } + } + } + + fn recycle_entire_state(&mut self) { + match mem::replace( + &mut self.state, + SessionState::Waiting { + current_unavailable: None, + }, + ) { + SessionState::Waiting { .. } => {} + SessionState::Ready { current } => { + put_free_frame(&mut self.free_frames, current.frame); + } + SessionState::Stale { previous, .. } => { + put_free_frame(&mut self.free_frames, previous.frame); + } + SessionState::Conflict { current, previous } => { + put_free_frame(&mut self.free_frames, current.frame); + if let Some(previous) = previous { + put_free_frame(&mut self.free_frames, previous.frame); + } + } + } } } -const fn unpack_rgb24(word: u32) -> [u8; 3] { - [ - ((word >> 16) & 0xff) as u8, - ((word >> 8) & 0xff) as u8, - (word & 0xff) as u8, - ] +fn take_free_frame( + pool: &mut [Option>; 3], +) -> Option> +where + Evaluation: PointEvaluatorV1, +{ + pool.iter_mut().find_map(Option::take) } -const fn pack_rgb24(bytes: [u8; 3]) -> u32 { - ((bytes[0] as u32) << 16) | ((bytes[1] as u32) << 8) | bytes[2] as u32 +fn put_free_frame( + pool: &mut [Option>; 3], + frame: ExecutionFrame, +) where + Evaluation: PointEvaluatorV1, +{ + let Some(slot) = pool.iter_mut().find(|slot| slot.is_none()) else { + unreachable!("three-frame ownership invariant exceeded") + }; + *slot = Some(frame); } diff --git a/crates/labcolors-core/src/program_session_tests.rs b/crates/labcolors-core/src/program_session_tests.rs index 2c6a39e2..6dce9845 100644 --- a/crates/labcolors-core/src/program_session_tests.rs +++ b/crates/labcolors-core/src/program_session_tests.rs @@ -1,15 +1,23 @@ use std::cell::Cell; +use std::convert::Infallible; use crate::Srgb8; use crate::appearance::{ ColorInputId, OccurrenceId, OpacityInputId, PaintId, SurfaceId, SurfaceInputPortId, }; +use crate::constraints::{ + ExactSrgb8IdentityV1, PointEvaluatorV1, PointInvocation, ProgramTestEvaluationErrorV1, + ProgramTestEvaluatorV1, ProgramTestInvocationV1, Wcag22Srgb8V1, arm_program_test_failure_once, + program_test_evaluation_count, reset_program_test_evaluation_count, +}; use crate::program_session::{ - ColorInput, CompiledProgram, CompositionProfile, Occurrence, OpacityInput, Paint, - PointRenderOwner, Program, ProgramCompileError, SessionState, SessionUpdateError, Surface, - SurfaceSignal, SurfaceUpdate, canonical_occurrence_sequence_matches, + ColorInput, CompiledProgram, CompositionProfile, ConstraintAssessment, ConstraintId, + ConstraintInvocation, ConstraintOutcome, ConstraintReportEntry, ConstraintSet, HardModeV1, + Occurrence, OpacityInput, OutputBinding, OutputSlotId, Paint, Program, ProgramCompileError, + ReportModeV1, SessionState, SessionUpdateError, Surface, SurfaceSignal, SurfaceUpdate, canonical_surface_input_port_sequence_matches, check_render_node_count, }; +use crate::wcag22::{Wcag22ApplicableDecisionV1, Wcag22CriterionV1}; const COLOR: ColorInputId = ColorInputId::new(1); const SURFACE_PORT: SurfaceInputPortId = SurfaceInputPortId::new(2); @@ -17,22 +25,22 @@ const OPACITY: OpacityInputId = OpacityInputId::new(3); const SOLID: PaintId = PaintId::new(10); const TRANSLUCENT: PaintId = PaintId::new(11); const BACKDROP: SurfaceId = SurfaceId::new(20); -const VISIBLE: SurfaceId = SurfaceId::new(21); +const VISIBLE_SURFACE: SurfaceId = SurfaceId::new(21); const OCCURRENCE: OccurrenceId = OccurrenceId::new(30); - -const MULTI_PORT_A: SurfaceInputPortId = SurfaceInputPortId::new(10); -const MULTI_PORT_B: SurfaceInputPortId = SurfaceInputPortId::new(20); -const MULTI_PORT_C: SurfaceInputPortId = SurfaceInputPortId::new(30); -const MULTI_PORTS: [SurfaceInputPortId; 3] = [MULTI_PORT_A, MULTI_PORT_B, MULTI_PORT_C]; -const MULTI_SURFACE_A: SurfaceId = SurfaceId::new(100); -const MULTI_SURFACE_B: SurfaceId = SurfaceId::new(101); -const MULTI_SURFACE_C: SurfaceId = SurfaceId::new(102); - -fn program(opacity: f64) -> Program { - program_against(BACKDROP, opacity) -} - -fn program_against(against: SurfaceId, opacity: f64) -> Program { +const OUTPUT: OutputSlotId = OutputSlotId::new(40); +const REQUIRED: ConstraintId = ConstraintId::new(50); + +fn base_program( + opacity: f64, + against: SurfaceId, + constraints: ConstraintSet>, + outputs: Vec, + evaluator: Evaluation, +) -> Program +where + Evaluation: PointEvaluatorV1, + PointInvocation: Copy, +{ Program::new( vec![ColorInput::new(COLOR, Srgb8::new([0; 3]))], vec![SURFACE_PORT], @@ -54,7 +62,7 @@ fn program_against(against: SurfaceId, opacity: f64) -> Program { input: SURFACE_PORT, }, Surface::FromOccurrence { - id: VISIBLE, + id: VISIBLE_SURFACE, occurrence: OCCURRENCE, }, ], @@ -64,87 +72,65 @@ fn program_against(against: SurfaceId, opacity: f64) -> Program { against, CompositionProfile::EncodedSrgb8SourceOverV1, )], + constraints, + outputs, + evaluator, ) } -fn cyclic_program() -> Program { - Program::new( - vec![ColorInput::new(COLOR, Srgb8::new([0; 3]))], - vec![SURFACE_PORT], - vec![OpacityInput::new(OPACITY, 0.5)], - vec![ - Paint::Solid { - id: SOLID, - color: COLOR, - }, - Paint::Opacity { - id: TRANSLUCENT, - source: SOLID, - opacity: OPACITY, - }, - ], - vec![Surface::FromOccurrence { - id: VISIBLE, - occurrence: OCCURRENCE, - }], - vec![Occurrence::new( - OCCURRENCE, - TRANSLUCENT, - VISIBLE, - CompositionProfile::EncodedSrgb8SourceOverV1, - )], +fn exact_program( + hard: Vec>, + report_only: Vec>, +) -> Program { + base_program( + 0.5, + BACKDROP, + ConstraintSet::new(hard, report_only), + vec![OutputBinding::new(OUTPUT, TRANSLUCENT)], + ExactSrgb8IdentityV1, ) } -fn compiled(opacity: f64) -> CompiledProgram { - program(opacity).compile().unwrap() +fn exact_required(expected: Srgb8) -> Program { + exact_program( + vec![ConstraintInvocation::hard(REQUIRED, OCCURRENCE, expected)], + vec![], + ) } -fn multi_surface_program() -> Program { - Program::new( - vec![ColorInput::new(COLOR, Srgb8::new([0; 3]))], - vec![MULTI_PORT_C, MULTI_PORT_A, MULTI_PORT_B], - vec![OpacityInput::new(OPACITY, 0.5)], - vec![ - Paint::Solid { - id: SOLID, - color: COLOR, - }, - Paint::Opacity { - id: TRANSLUCENT, - source: SOLID, - opacity: OPACITY, - }, - ], - vec![ - Surface::Input { - id: MULTI_SURFACE_C, - input: MULTI_PORT_C, - }, - Surface::Input { - id: MULTI_SURFACE_A, - input: MULTI_PORT_A, - }, - Surface::Input { - id: MULTI_SURFACE_B, - input: MULTI_PORT_B, - }, - Surface::FromOccurrence { - id: VISIBLE, - occurrence: OCCURRENCE, - }, - ], - vec![Occurrence::new( - OCCURRENCE, - TRANSLUCENT, - MULTI_SURFACE_B, - CompositionProfile::EncodedSrgb8SourceOverV1, - )], +fn compiled_exact(expected: Srgb8) -> CompiledProgram { + exact_required(expected).compile().unwrap() +} + +fn wcag_program( + hard: Vec>, + report_only: Vec>, +) -> Program { + base_program( + 0.5, + BACKDROP, + ConstraintSet::new(hard, report_only), + vec![OutputBinding::new(OUTPUT, TRANSLUCENT)], + Wcag22Srgb8V1, ) } -fn multi_compiled() -> CompiledProgram { - multi_surface_program().compile().unwrap() +fn compile_error(program: Program) -> ProgramCompileError +where + Evaluation: PointEvaluatorV1, + PointInvocation: Copy, +{ + match program.compile() { + Ok(_) => panic!("invalid declaration compiled"), + Err(error) => error, + } +} + +fn update_error(result: Result) -> Error { + match result { + Ok(_) => panic!("invalid update committed"), + Err(error) => error, + } } struct ReadProbe { @@ -172,27 +158,35 @@ impl ReadProbe { } } -fn retained_signal_storage_pointers(state: &SessionState) -> (*const u32, *const u32) { - let snapshot = match state { - SessionState::Ready { current } => current, - SessionState::Stale { previous, .. } => previous, - SessionState::Waiting { .. } => { - panic!("the allocation test requires a retained successful snapshot") - } - }; - ( - snapshot.input_surface_signals_rgb24().as_ptr(), - snapshot.composited_occurrence_signals_rgb24().as_ptr(), - ) +fn exact_outcome( + assessment: &ConstraintAssessment, +) -> (Srgb8, Srgb8) { + match assessment.outcome() { + ConstraintOutcome::Pass(evidence) => (evidence.target(), evidence.actual()), + ConstraintOutcome::Violation(evidence) => (evidence.target(), evidence.actual()), + } } #[test] -fn authored_and_runtime_values_preserve_exact_typed_bindings() { - let color_value = Srgb8::new([0x12, 0x34, 0x56]); - let color = ColorInput::new(COLOR, color_value); +fn authored_modes_are_marker_typed_and_values_preserve_exact_ids() { + let hard = ConstraintInvocation::hard(REQUIRED, OCCURRENCE, Srgb8::new([0x80; 3])); + let report = + ConstraintInvocation::report_only(ConstraintId::new(51), OCCURRENCE, Srgb8::new([0x81; 3])); + let set = ConstraintSet::new(vec![hard], vec![report]); + assert_eq!(set.hard()[0].id(), REQUIRED); + assert_eq!(set.hard()[0].target(), OCCURRENCE); + assert_eq!(*set.hard()[0].invocation(), Srgb8::new([0x80; 3])); + assert_eq!(set.report_only()[0].id(), ConstraintId::new(51)); + + let output = OutputBinding::new(OUTPUT, TRANSLUCENT); + assert_eq!(output.output(), OUTPUT); + assert_eq!(output.paint(), TRANSLUCENT); + assert_eq!(ConstraintId::new(7).value(), 7); + assert_eq!(OutputSlotId::new(8).value(), 8); + + let color = ColorInput::new(COLOR, Srgb8::new([1, 2, 3])); assert_eq!(color.id(), COLOR); - assert_eq!(color.value(), color_value); - + assert_eq!(color.value(), Srgb8::new([1, 2, 3])); let opacity = OpacityInput::new(OPACITY, 0.375); assert_eq!(opacity.id(), OPACITY); assert_eq!(opacity.value(), 0.375); @@ -211,18 +205,17 @@ fn authored_and_runtime_values_preserve_exact_typed_bindings() { CompositionProfile::EncodedSrgb8SourceOverV1 ); - let surface_value = Srgb8::new([0xab, 0xcd, 0xef]); - let signal = SurfaceSignal::new(SURFACE_PORT, surface_value); + let signal = SurfaceSignal::new(SURFACE_PORT, Srgb8::new([4, 5, 6])); assert_eq!(signal.input(), SURFACE_PORT); - assert_eq!(signal.value(), surface_value); + assert_eq!(signal.value(), Srgb8::new([4, 5, 6])); } #[test] -fn empty_surface_schema_precedes_dangling_surface_input_port_analysis() { - let declaration = Program::new( +fn empty_domains_have_stable_precedence() { + let empty_surface = Program::new( vec![ColorInput::new(COLOR, Srgb8::new([0; 3]))], vec![], - vec![OpacityInput::new(OPACITY, 0.5)], + vec![], vec![Paint::Solid { id: SOLID, color: COLOR, @@ -237,17 +230,23 @@ fn empty_surface_schema_precedes_dangling_surface_input_port_analysis() { BACKDROP, CompositionProfile::EncodedSrgb8SourceOverV1, )], + ConstraintSet::new( + vec![ConstraintInvocation::hard( + REQUIRED, + OCCURRENCE, + Srgb8::new([0; 3]), + )], + vec![], + ), + vec![OutputBinding::new(OUTPUT, SOLID)], + ExactSrgb8IdentityV1, ); - assert_eq!( - declaration.compile().unwrap_err(), + compile_error(empty_surface), ProgramCompileError::EmptySurfaceSchema ); -} -#[test] -fn empty_occurrence_set_precedes_dangling_occurrence_analysis() { - let declaration = Program::new( + let empty_occurrence = Program::new( vec![ColorInput::new(COLOR, Srgb8::new([0; 3]))], vec![SURFACE_PORT], vec![], @@ -255,465 +254,804 @@ fn empty_occurrence_set_precedes_dangling_occurrence_analysis() { id: SOLID, color: COLOR, }], - vec![Surface::FromOccurrence { - id: VISIBLE, - occurrence: OCCURRENCE, + vec![Surface::Input { + id: BACKDROP, + input: SURFACE_PORT, }], vec![], + ConstraintSet::new( + vec![ConstraintInvocation::hard( + REQUIRED, + OCCURRENCE, + Srgb8::new([0; 3]), + )], + vec![], + ), + vec![OutputBinding::new(OUTPUT, SOLID)], + ExactSrgb8IdentityV1, ); - assert_eq!( - declaration.compile().unwrap_err(), + compile_error(empty_occurrence), ProgramCompileError::EmptyOccurrenceSet ); + + let empty_constraints = base_program( + 0.5, + BACKDROP, + ConstraintSet::::new(vec![], vec![]), + vec![], + ExactSrgb8IdentityV1, + ); + assert_eq!( + compile_error(empty_constraints), + ProgramCompileError::EmptyConstraintSet + ); + + let empty_outputs = base_program( + 0.5, + BACKDROP, + ConstraintSet::new( + vec![ConstraintInvocation::hard( + REQUIRED, + OCCURRENCE, + Srgb8::new([0x80; 3]), + )], + vec![], + ), + vec![], + ExactSrgb8IdentityV1, + ); + assert_eq!( + compile_error(empty_outputs), + ProgramCompileError::EmptyOutputSet + ); } #[test] -fn combined_render_cardinality_overflow_is_resource_exhaustion() { - assert_eq!(check_render_node_count(usize::MAX - 1, 1), Ok(())); +fn physical_errors_precede_constraint_and_output_errors() { + let missing_surface = SurfaceId::new(999); + let duplicate = ConstraintId::new(77); + let program = base_program( + 0.5, + missing_surface, + ConstraintSet::new( + vec![ConstraintInvocation::hard( + duplicate, + OccurrenceId::new(998), + Srgb8::new([0; 3]), + )], + vec![ConstraintInvocation::report_only( + duplicate, + OccurrenceId::new(997), + Srgb8::new([0; 3]), + )], + ), + vec![ + OutputBinding::new(OUTPUT, PaintId::new(996)), + OutputBinding::new(OUTPUT, PaintId::new(995)), + ], + ExactSrgb8IdentityV1, + ); assert_eq!( - check_render_node_count(usize::MAX, 1), - Err(ProgramCompileError::ResourceExhausted) + compile_error(program), + ProgramCompileError::MissingOccurrenceBackdrop { + occurrence: OCCURRENCE, + surface: missing_surface, + } ); } #[test] -fn canonical_sequence_firewall_rejects_reordering_relabeling_and_truncation() { - let surface_input_ports = [SurfaceInputPortId::new(1), SurfaceInputPortId::new(2)]; - assert!(canonical_surface_input_port_sequence_matches( - [SurfaceInputPortId::new(1), SurfaceInputPortId::new(2),], - &surface_input_ports, - )); - assert!(!canonical_surface_input_port_sequence_matches( - [SurfaceInputPortId::new(2), SurfaceInputPortId::new(1),], - &surface_input_ports, - )); - assert!(!canonical_surface_input_port_sequence_matches( - [SurfaceInputPortId::new(1)], - &surface_input_ports, - )); +fn constraint_and_output_error_precedence_is_canonical() { + let duplicate = ConstraintId::new(77); + let missing_occurrence = OccurrenceId::new(999); + let duplicate_constraints = base_program( + 0.5, + BACKDROP, + ConstraintSet::new( + vec![ConstraintInvocation::hard( + duplicate, + missing_occurrence, + Srgb8::new([0; 3]), + )], + vec![ConstraintInvocation::report_only( + duplicate, + OCCURRENCE, + Srgb8::new([0; 3]), + )], + ), + vec![ + OutputBinding::new(OUTPUT, PaintId::new(998)), + OutputBinding::new(OUTPUT, TRANSLUCENT), + ], + ExactSrgb8IdentityV1, + ); + assert_eq!( + compile_error(duplicate_constraints), + ProgramCompileError::DuplicateConstraint { + constraint: duplicate, + } + ); - let occurrences = [OccurrenceId::new(10), OccurrenceId::new(20)]; - assert!(canonical_occurrence_sequence_matches( - [OccurrenceId::new(10), OccurrenceId::new(20),], - &occurrences, - )); - assert!(!canonical_occurrence_sequence_matches( - [OccurrenceId::new(10), OccurrenceId::new(21),], - &occurrences, - )); - assert!(!canonical_occurrence_sequence_matches( - [OccurrenceId::new(10)], - &occurrences, - )); + let missing_constraint = base_program( + 0.5, + BACKDROP, + ConstraintSet::new( + vec![ConstraintInvocation::hard( + REQUIRED, + missing_occurrence, + Srgb8::new([0; 3]), + )], + vec![], + ), + vec![ + OutputBinding::new(OUTPUT, TRANSLUCENT), + OutputBinding::new(OUTPUT, PaintId::new(998)), + ], + ExactSrgb8IdentityV1, + ); + assert_eq!( + compile_error(missing_constraint), + ProgramCompileError::MissingConstraintOccurrence { + constraint: REQUIRED, + occurrence: missing_occurrence, + } + ); + + let duplicate_output = base_program( + 0.5, + BACKDROP, + ConstraintSet::new( + vec![ConstraintInvocation::hard( + REQUIRED, + OCCURRENCE, + Srgb8::new([0x80; 3]), + )], + vec![], + ), + vec![ + OutputBinding::new(OUTPUT, PaintId::new(998)), + OutputBinding::new(OUTPUT, TRANSLUCENT), + ], + ExactSrgb8IdentityV1, + ); + assert_eq!( + compile_error(duplicate_output), + ProgramCompileError::DuplicateOutputSlot { output: OUTPUT } + ); + + let missing_paint = PaintId::new(998); + let missing_output = base_program( + 0.5, + BACKDROP, + ConstraintSet::new( + vec![ConstraintInvocation::hard( + REQUIRED, + OCCURRENCE, + Srgb8::new([0x80; 3]), + )], + vec![], + ), + vec![OutputBinding::new(OUTPUT, missing_paint)], + ExactSrgb8IdentityV1, + ); + assert_eq!( + compile_error(missing_output), + ProgramCompileError::MissingOutputPaint { + output: OUTPUT, + paint: missing_paint, + } + ); +} + +#[test] +fn compile_canonicalizes_constraints_and_outputs_independent_of_mode_lists() { + let low = ConstraintId::new(1); + let high = ConstraintId::new(9); + let output_low = OutputSlotId::new(2); + let output_high = OutputSlotId::new(8); + let compiled = base_program( + 0.5, + BACKDROP, + ConstraintSet::new( + vec![ConstraintInvocation::hard( + high, + OCCURRENCE, + Srgb8::new([0x80; 3]), + )], + vec![ConstraintInvocation::report_only( + low, + OCCURRENCE, + Srgb8::new([0x80; 3]), + )], + ), + vec![ + OutputBinding::new(output_high, TRANSLUCENT), + OutputBinding::new(output_low, TRANSLUCENT), + ], + ExactSrgb8IdentityV1, + ) + .compile() + .unwrap(); + assert_eq!(compiled.surface_input_ports(), &[SURFACE_PORT]); + assert_eq!( + compiled.constraint_ids().collect::>(), + vec![low, high] + ); + assert_eq!( + compiled.outputs().collect::>(), + vec![(output_low, TRANSLUCENT), (output_high, TRANSLUCENT)] + ); + let owner = compiled.into_owner(); + assert_eq!( + owner.constraint_ids().unwrap().collect::>(), + vec![low, high] + ); + assert_eq!( + owner.outputs().unwrap().collect::>(), + vec![(output_low, TRANSLUCENT), (output_high, TRANSLUCENT)] + ); } #[test] -fn canonical_present_executes_the_compiled_graph_and_commits_compact_occurrences() { - let owner = compiled(0.5).into_owner(); +fn wcag_report_only_uses_visible_808080_but_emits_black_half_alpha_paint() { + let criterion = Wcag22CriterionV1::Sc143TextDefault; + let program = wcag_program( + vec![], + vec![ConstraintInvocation::report_only( + REQUIRED, OCCURRENCE, criterion, + )], + ); + let owner = program.compile().unwrap().into_owner(); let mut session = owner.attach().unwrap(); + let (result, allocations) = crate::test_support::measured_allocations(|| { + session + .update_canonical_present(1, 1, |_| Srgb8::new([0xff; 3])) + .map(|_| ()) + }); + assert!(result.is_ok()); + assert_eq!(allocations, 0); + let SessionState::Ready { current } = session.state() else { + panic!("report-only violation must commit Ready"); + }; + assert!( + current + .surfaces() + .eq([SurfaceSignal::new(SURFACE_PORT, Srgb8::new([0xff; 3]))]) + ); - let SessionState::Ready { current } = session + let output = current.output(OUTPUT).expect("compiled output slot"); + assert_eq!(output.output(), OUTPUT); + assert_eq!(output.paint(), TRANSLUCENT); + assert_eq!(output.value().source(), Srgb8::new([0; 3])); + assert_eq!(output.value().straight_alpha(), 0.5); + assert_eq!(output.value().straight_alpha_bits(), 0.5f64.to_bits()); + + let mut report = current.report(); + let Some(ConstraintReportEntry::ReportOnly(assessment)) = report.next() else { + panic!("WCAG diagnostic must retain report-only mode"); + }; + assert_eq!(assessment.constraint(), REQUIRED); + assert_eq!(assessment.target(), OCCURRENCE); + let ConstraintOutcome::Violation(evidence) = assessment.outcome() else { + panic!("#808080 on white must violate text-default WCAG"); + }; + let applicable = evidence.measurement().value(); + assert_eq!(applicable.criterion(), criterion); + assert_eq!(applicable.decision(), Wcag22ApplicableDecisionV1::Fail); + assert_eq!(applicable.measurement().foreground, [0x80; 3]); + assert_eq!(applicable.measurement().background, [0xff; 3]); + assert!(report.next().is_none()); +} + +#[test] +fn wcag_hard_violation_commits_conflict_with_full_report_and_no_current_output() { + let program = wcag_program( + vec![ConstraintInvocation::hard( + REQUIRED, + OCCURRENCE, + Wcag22CriterionV1::Sc143TextDefault, + )], + vec![], + ); + let owner = program.compile().unwrap().into_owner(); + let mut session = owner.attach().unwrap(); + let SessionState::Conflict { current, previous } = session .update_canonical_present(1, 1, |_| Srgb8::new([0xff; 3])) .unwrap() else { - panic!("a complete canonical Surface set must produce Ready"); + panic!("mandatory WCAG violation must commit Conflict"); }; + assert!(previous.is_none()); assert_eq!(current.revision(), 1); - assert_eq!(current.input_surface_signals_rgb24(), &[0xff_ff_ff]); - assert_eq!(current.composited_occurrence_signals_rgb24(), &[0x80_80_80]); -} - -#[test] -fn successful_replace_revokes_old_sessions_without_a_numeric_generation() { - let mut owner = compiled(0.5).into_owner(); - let mut old = owner.attach().unwrap(); - - owner.replace(compiled(0.25)); - assert_eq!( - old.update_canonical_present(1, 1, |_| Srgb8::new([0xff; 3])), - Err(SessionUpdateError::ProgramExpired) + assert!( + current + .surfaces() + .eq([SurfaceSignal::new(SURFACE_PORT, Srgb8::new([0xff; 3]))]) ); - - let mut current = owner.attach().unwrap(); + let entries = current.report().collect::>(); + assert_eq!(entries.len(), 1); + let ConstraintReportEntry::Hard(assessment) = entries[0] else { + panic!("mandatory result lost its hard type"); + }; assert!(matches!( - current - .update_canonical_present(1, 1, |_| Srgb8::new([0xff; 3])) - .unwrap(), - SessionState::Ready { .. } + assessment.outcome(), + ConstraintOutcome::Violation(_) )); } #[test] -fn invalid_opacity_failed_replace_is_atomic_and_keeps_old_epoch_live() { - let owner = compiled(0.5).into_owner(); - let mut session = owner.attach().unwrap(); - - assert_eq!( - program(1.25).compile().unwrap_err(), - ProgramCompileError::OpacityOutOfDomain { input: OPACITY } +fn all_constraints_run_before_hard_gate_and_report_order_is_canonical() { + let low_hard = ConstraintId::new(1); + let middle_report = ConstraintId::new(2); + let high_hard = ConstraintId::new(3); + let program = exact_program( + vec![ + ConstraintInvocation::hard(high_hard, OCCURRENCE, Srgb8::new([0x80; 3])), + ConstraintInvocation::hard(low_hard, OCCURRENCE, Srgb8::new([0x81; 3])), + ], + vec![ConstraintInvocation::report_only( + middle_report, + OCCURRENCE, + Srgb8::new([0x80; 3]), + )], ); - let SessionState::Ready { current } = session + let owner = program.compile().unwrap().into_owner(); + let mut session = owner.attach().unwrap(); + let SessionState::Conflict { current, .. } = session .update_canonical_present(1, 1, |_| Srgb8::new([0xff; 3])) .unwrap() else { - panic!("failed replacement must not revoke the old epoch"); + panic!("one hard violation must gate the complete result"); + }; + let entries = current.report().collect::>(); + assert_eq!( + entries + .iter() + .map(|entry| entry.constraint()) + .collect::>(), + vec![low_hard, middle_report, high_hard] + ); + assert!(entries.iter().all(|entry| entry.target() == OCCURRENCE)); + assert!(matches!(entries[0], ConstraintReportEntry::Hard(_))); + assert!(matches!(entries[1], ConstraintReportEntry::ReportOnly(_))); + assert!(matches!(entries[2], ConstraintReportEntry::Hard(_))); + let ConstraintReportEntry::Hard(last) = entries[2] else { + unreachable!() }; - assert_eq!(current.composited_occurrence_signals_rgb24(), &[0x80_80_80]); + assert_eq!( + exact_outcome(last), + (Srgb8::new([0x80; 3]), Srgb8::new([0x80; 3])) + ); } #[test] -fn dangling_and_cyclic_failed_compiles_do_not_revoke_the_current_epoch() { - let owner = compiled(0.5).into_owner(); - let mut session = owner.attach().unwrap(); - - let missing = SurfaceId::new(999); - assert_eq!( - program_against(missing, 0.5).compile().unwrap_err(), - ProgramCompileError::MissingOccurrenceBackdrop { - occurrence: OCCURRENCE, - surface: missing, - } +fn report_only_violation_never_gates_terminal_paint() { + let program = exact_program( + vec![ConstraintInvocation::hard( + REQUIRED, + OCCURRENCE, + Srgb8::new([0x80; 3]), + )], + vec![ConstraintInvocation::report_only( + ConstraintId::new(51), + OCCURRENCE, + Srgb8::new([0x81; 3]), + )], ); - assert!(matches!( - cyclic_program().compile(), - Err(ProgramCompileError::RenderCycle { .. }) - )); - + let owner = program.compile().unwrap().into_owner(); + let mut session = owner.attach().unwrap(); let SessionState::Ready { current } = session .update_canonical_present(1, 1, |_| Srgb8::new([0xff; 3])) .unwrap() else { - panic!("compile failures must leave the old strong epoch untouched"); + panic!("report-only violation cannot gate"); }; - assert_eq!(current.composited_occurrence_signals_rgb24(), &[0x80_80_80]); + assert_eq!(current.outputs().count(), 1); + assert!(matches!( + current.report().nth(1), + Some(ConstraintReportEntry::ReportOnly(assessment)) + if matches!(assessment.outcome(), ConstraintOutcome::Violation(_)) + )); } #[test] -fn dispose_revokes_sessions_and_prevents_new_attachment() { - let mut owner = compiled(0.5).into_owner(); - let mut session = owner.attach().unwrap(); - owner.dispose(); +fn output_slot_renaming_changes_routing_not_physical_paint() { + fn resolve(output: OutputSlotId) -> crate::program_session::OutputValueV1 { + let program = base_program( + 0.5, + BACKDROP, + ConstraintSet::new( + vec![ConstraintInvocation::hard( + REQUIRED, + OCCURRENCE, + Srgb8::new([0x80; 3]), + )], + vec![], + ), + vec![OutputBinding::new(output, TRANSLUCENT)], + ExactSrgb8IdentityV1, + ); + let owner = program.compile().unwrap().into_owner(); + let mut session = owner.attach().unwrap(); + let SessionState::Ready { current } = session + .update_canonical_present(1, 1, |_| Srgb8::new([0xff; 3])) + .unwrap() + else { + unreachable!() + }; + current.outputs().next().unwrap() + } - assert_eq!( - session.update(SurfaceUpdate::Unavailable { - revision: 1, - reason: 7, - }), - Err(SessionUpdateError::ProgramExpired) - ); - assert!(owner.attach().is_err()); + let left = resolve(OutputSlotId::new(1)); + let renamed = resolve(OutputSlotId::new(999)); + assert_ne!(left.output(), renamed.output()); + assert_eq!(left.paint(), renamed.paint()); + assert_eq!(left.value(), renamed.value()); + assert_eq!(left.value().source(), Srgb8::new([0; 3])); + assert_eq!(left.value().straight_alpha_bits(), 0.5f64.to_bits()); } #[test] -fn unavailable_after_ready_is_stale_and_retains_exactly_one_previous_snapshot() { - let owner = compiled(0.5).into_owner(); +fn evaluator_error_preserves_prior_snapshot_head_and_all_owned_evidence() { + reset_program_test_evaluation_count(); + let first = ConstraintId::new(1); + let failing = ConstraintId::new(2); + let program = base_program( + 0.5, + BACKDROP, + ConstraintSet::new( + vec![ConstraintInvocation::hard( + first, + OCCURRENCE, + ProgramTestInvocationV1::exact(Srgb8::new([0x80; 3])), + )], + vec![ConstraintInvocation::report_only( + failing, + OCCURRENCE, + ProgramTestInvocationV1::fail_once_when_armed(Srgb8::new([0x80; 3])), + )], + ), + vec![OutputBinding::new(OUTPUT, TRANSLUCENT)], + ProgramTestEvaluatorV1, + ); + let owner = program.compile().unwrap().into_owner(); let mut session = owner.attach().unwrap(); session .update_canonical_present(1, 1, |_| Srgb8::new([0xff; 3])) .unwrap(); - - let SessionState::Stale { - previous, - current_unavailable, - } = session - .update(SurfaceUpdate::Unavailable { - revision: 2, - reason: 91, - }) - .unwrap() - else { - panic!("unavailable Surface input after Ready must become Stale"); + let SessionState::Ready { current } = session.state() else { + panic!("control update must be Ready"); }; - assert_eq!(previous.revision(), 1); + let retained_storage = current.storage_pointers_for_test(); + let retained_output = current.output(OUTPUT); + let retained_report_ids = current + .report() + .map(ConstraintReportEntry::constraint) + .collect::>(); + + arm_program_test_failure_once(); + let error = update_error(session.update_canonical_present(2, 1, |_| Srgb8::new([0xff; 3]))); assert_eq!( - previous.composited_occurrence_signals_rgb24(), - &[0x80_80_80] + error, + SessionUpdateError::Evaluator { + constraint: failing, + source: ProgramTestEvaluationErrorV1::Forced, + } ); - assert_eq!(current_unavailable.revision(), 2); - assert_eq!(current_unavailable.reason(), 91); - - let SessionState::Stale { previous, .. } = session - .update(SurfaceUpdate::Unavailable { - revision: 3, - reason: 92, - }) + let SessionState::Ready { current } = session.state() else { + panic!("evaluator Err must leave the prior state exact"); + }; + assert_eq!(current.revision(), 1); + assert_eq!(current.storage_pointers_for_test(), retained_storage); + assert_eq!(current.output(OUTPUT), retained_output); + assert_eq!( + current + .report() + .map(ConstraintReportEntry::constraint) + .collect::>(), + retained_report_ids + ); + + let SessionState::Ready { current } = session + .update_canonical_present(2, 1, |_| Srgb8::new([0xff; 3])) .unwrap() else { - panic!("a later unavailable update must remain Stale"); + panic!("same incoming revision must be retryable after evaluator Err"); }; - assert_eq!(previous.revision(), 1); -} + assert_eq!(current.revision(), 2); -#[test] -fn borrowed_present_expired_epoch_reads_zero_and_allocates_zero() { - let mut session = { - let owner = compiled(0.5).into_owner(); - owner.attach().unwrap() + session + .update_canonical_present(3, 1, |_| Srgb8::new([0; 3])) + .unwrap(); + let SessionState::Conflict { current, previous } = session.state() else { + panic!("black backdrop must create the retained Conflict control"); }; - let reads = Cell::new(0); - - let (result, allocations) = crate::test_support::measured_allocations(|| { - session - .update_canonical_present(1, 1, |_| { - reads.set(reads.get() + 1); - Srgb8::new([0xff; 3]) - }) - .map(|_| ()) - }); - - assert_eq!(result, Err(SessionUpdateError::ProgramExpired)); - assert_eq!(reads.get(), 0); - assert_eq!(allocations, 0); - assert!(matches!( - session.state(), - SessionState::Waiting { - current_unavailable: None + let retained_conflict = current.storage_pointers_for_test(); + let retained_previous = previous + .as_ref() + .expect("Conflict after Ready must retain full prior evidence") + .storage_pointers_for_test(); + + arm_program_test_failure_once(); + let error = update_error(session.update_canonical_present(4, 1, |_| Srgb8::new([0; 3]))); + assert_eq!( + error, + SessionUpdateError::Evaluator { + constraint: failing, + source: ProgramTestEvaluationErrorV1::Forced, } - )); + ); + let SessionState::Conflict { current, previous } = session.state() else { + panic!("evaluator Err must preserve retained Conflict exactly"); + }; + assert_eq!(current.revision(), 3); + assert_eq!(current.storage_pointers_for_test(), retained_conflict); + assert_eq!( + previous.as_ref().unwrap().storage_pointers_for_test(), + retained_previous + ); + + let SessionState::Conflict { current, previous } = session + .update_canonical_present(4, 1, |_| Srgb8::new([0; 3])) + .unwrap() + else { + panic!("retry after evaluator Err must execute and commit Conflict"); + }; + assert_eq!(current.revision(), 4); + assert_eq!( + previous.as_ref().unwrap().storage_pointers_for_test(), + retained_previous + ); + assert_eq!(program_test_evaluation_count(), 12); } #[test] -fn borrowed_present_schema_mismatch_reads_zero_and_allocates_zero() { - let owner = compiled(0.5).into_owner(); +fn three_frames_preserve_verified_and_replace_complete_conflict_reports_without_clone() { + let owner = compiled_exact(Srgb8::new([0x80; 3])).into_owner(); let mut session = owner.attach().unwrap(); - crate::composition::reset_source_over_evaluation_count(); + session + .update_canonical_present(1, 1, |_| Srgb8::new([0xff; 3])) + .unwrap(); + let SessionState::Ready { current } = session.state() else { + unreachable!() + }; + let verified_storage = current.storage_pointers_for_test(); - for actual in [0, 2] { - let reads = Cell::new(0); - let (result, allocations) = crate::test_support::measured_allocations(|| { - session - .update_canonical_present(1, actual, |_| { - reads.set(reads.get() + 1); - Srgb8::new([0xff; 3]) - }) - .map(|_| ()) - }); - assert_eq!( - result, - Err(SessionUpdateError::SurfaceInputPortLengthMismatch { - expected: 1, - actual, - }) - ); - assert_eq!(reads.get(), 0); - assert_eq!(allocations, 0); - } + session + .update_canonical_present(2, 1, |_| Srgb8::new([0; 3])) + .unwrap(); + let SessionState::Conflict { current, previous } = session.state() else { + panic!("black backdrop must violate exact #808080"); + }; + let conflict_two_storage = current.storage_pointers_for_test(); + assert_ne!(conflict_two_storage, verified_storage); + assert_eq!( + previous + .as_ref() + .expect("Conflict retains prior verified evidence") + .storage_pointers_for_test(), + verified_storage + ); - assert_eq!(crate::composition::source_over_evaluation_count(), 0); - assert!(matches!( - session.state(), - SessionState::Waiting { - current_unavailable: None - } - )); + session + .update_canonical_present(3, 1, |_| Srgb8::new([0x20; 3])) + .unwrap(); + let SessionState::Conflict { current, previous } = session.state() else { + unreachable!() + }; + let conflict_three_storage = current.storage_pointers_for_test(); + assert_ne!(conflict_three_storage, verified_storage); + assert_ne!(conflict_three_storage, conflict_two_storage); + assert_eq!( + previous + .as_ref() + .expect("new Conflict must retain the one verified witness") + .storage_pointers_for_test(), + verified_storage + ); + assert_eq!(current.report().count(), 1); + + session + .update_canonical_present(4, 1, |_| Srgb8::new([0xff; 3])) + .unwrap(); + let SessionState::Ready { current } = session.state() else { + panic!("Conflict must recover directly to a new verified Snapshot"); + }; + let recovered_storage = current.storage_pointers_for_test(); + assert_ne!(recovered_storage, verified_storage); + assert_ne!(recovered_storage, conflict_three_storage); + + session + .update_canonical_present(5, 1, |_| Srgb8::new([0; 3])) + .unwrap(); + let SessionState::Conflict { previous, .. } = session.state() else { + unreachable!() + }; + assert_eq!( + previous.as_ref().unwrap().storage_pointers_for_test(), + recovered_storage + ); + + let SessionState::Stale { + previous, + current_unavailable, + } = session + .update(SurfaceUpdate::Unavailable { + revision: 6, + reason: 9, + }) + .unwrap() + else { + panic!("Unknown after Conflict(previous) must retain that full Snapshot"); + }; + assert_eq!(previous.storage_pointers_for_test(), recovered_storage); + assert_eq!(current_unavailable.revision(), 6); + assert_eq!(current_unavailable.reason(), 9); } #[test] -fn borrowed_present_lower_revision_reads_zero_and_preserves_state() { - let owner = compiled(0.5).into_owner(); +fn same_revision_conflict_replay_is_idempotent_and_changed_payload_conflicts() { + let owner = compiled_exact(Srgb8::new([0x80; 3])).into_owner(); let mut session = owner.attach().unwrap(); - let white = Srgb8::new([0xff; 3]); - session.update_canonical_present(5, 1, |_| white).unwrap(); - let storage = retained_signal_storage_pointers(session.state()); + let black = Srgb8::new([0; 3]); + session.update_canonical_present(1, 1, |_| black).unwrap(); + let SessionState::Conflict { current, .. } = session.state() else { + unreachable!() + }; + let storage = current.storage_pointers_for_test(); crate::composition::reset_source_over_evaluation_count(); - let reads = Cell::new(0); - let (result, allocations) = crate::test_support::measured_allocations(|| { - session - .update_canonical_present(4, 1, |_| { - reads.set(reads.get() + 1); - Srgb8::new([0; 3]) - }) - .map(|_| ()) - }); + assert!(session.update_canonical_present(1, 1, |_| black).is_ok()); + assert_eq!(crate::composition::source_over_evaluation_count(), 0); + let SessionState::Conflict { current, .. } = session.state() else { + unreachable!() + }; + assert_eq!(current.storage_pointers_for_test(), storage); + let error = update_error(session.update_canonical_present(1, 1, |_| Srgb8::new([0xff; 3]))); assert_eq!( - result, - Err(SessionUpdateError::RevisionOutOfOrder { - current: 5, - incoming: 4, - }) + error, + SessionUpdateError::::RevisionConflict { revision: 1 } ); - assert_eq!(reads.get(), 0); - assert_eq!(allocations, 0); assert_eq!(crate::composition::source_over_evaluation_count(), 0); - assert_eq!(retained_signal_storage_pointers(session.state()), storage); - let SessionState::Ready { current } = session.state() else { - panic!("a lower revision must leave Ready untouched"); - }; - assert_eq!(current.revision(), 5); - assert!( - current - .surfaces() - .eq([SurfaceSignal::new(SURFACE_PORT, white)]) - ); - assert_eq!(current.composited_occurrence_signals_rgb24(), &[0x80_80_80]); - assert!( - session - .bound_surface_inputs_for_test() - .eq([(SURFACE_PORT, white)]) - ); } #[test] -fn same_revision_replay_and_conflict_read_every_value_without_mutation() { - let compiled = multi_compiled(); - assert_eq!(compiled.surface_input_ports(), &MULTI_PORTS); +fn three_port_same_revision_conflict_reads_every_value_without_execution_or_allocation() { + const PORT_A: SurfaceInputPortId = SurfaceInputPortId::new(10); + const PORT_B: SurfaceInputPortId = SurfaceInputPortId::new(20); + const PORT_C: SurfaceInputPortId = SurfaceInputPortId::new(30); + const SURFACE_A: SurfaceId = SurfaceId::new(110); + const SURFACE_B: SurfaceId = SurfaceId::new(120); + const SURFACE_C: SurfaceId = SurfaceId::new(130); + let program = Program::new( + vec![ColorInput::new(COLOR, Srgb8::new([0; 3]))], + vec![PORT_C, PORT_A, PORT_B], + vec![OpacityInput::new(OPACITY, 0.5)], + vec![ + Paint::Solid { + id: SOLID, + color: COLOR, + }, + Paint::Opacity { + id: TRANSLUCENT, + source: SOLID, + opacity: OPACITY, + }, + ], + vec![ + Surface::Input { + id: SURFACE_C, + input: PORT_C, + }, + Surface::Input { + id: SURFACE_A, + input: PORT_A, + }, + Surface::Input { + id: SURFACE_B, + input: PORT_B, + }, + ], + vec![Occurrence::new( + OCCURRENCE, + TRANSLUCENT, + SURFACE_B, + CompositionProfile::EncodedSrgb8SourceOverV1, + )], + ConstraintSet::new( + vec![ConstraintInvocation::hard( + REQUIRED, + OCCURRENCE, + Srgb8::new([0x80; 3]), + )], + vec![], + ), + vec![OutputBinding::new(OUTPUT, TRANSLUCENT)], + ExactSrgb8IdentityV1, + ); + let compiled = program.compile().unwrap(); + assert_eq!(compiled.surface_input_ports(), &[PORT_A, PORT_B, PORT_C]); let owner = compiled.into_owner(); let mut session = owner.attach().unwrap(); - let committed_values = [ - Srgb8::new([0x10, 0x20, 0x30]), - Srgb8::new([0xff; 3]), - Srgb8::new([0x70, 0x80, 0x90]), + let committed = [ + Srgb8::new([0x10; 3]), + Srgb8::new([0; 3]), + Srgb8::new([0x30; 3]), ]; session - .update_canonical_present(5, MULTI_PORTS.len(), |index| committed_values[index]) + .update_canonical_present(5, 3, |index| committed[index]) .unwrap(); - let storage = retained_signal_storage_pointers(session.state()); - crate::composition::reset_source_over_evaluation_count(); + let SessionState::Conflict { current, .. } = session.state() else { + unreachable!() + }; + let storage = current.storage_pointers_for_test(); - let replay = ReadProbe::new(committed_values); + crate::composition::reset_source_over_evaluation_count(); + let replay = ReadProbe::new(committed); let (result, allocations) = crate::test_support::measured_allocations(|| { session - .update_canonical_present(5, MULTI_PORTS.len(), |index| replay.read(index)) + .update_canonical_present(5, 3, |index| replay.read(index)) .map(|_| ()) }); - assert_eq!(result, Ok(())); + assert!(result.is_ok()); assert_eq!(replay.reads(), [1, 1, 1]); assert_eq!(allocations, 0); assert_eq!(crate::composition::source_over_evaluation_count(), 0); - assert_eq!(retained_signal_storage_pointers(session.state()), storage); + let SessionState::Conflict { current, .. } = session.state() else { + unreachable!() + }; + assert_eq!(current.storage_pointers_for_test(), storage); - let mut conflicting_values = committed_values; - conflicting_values[0] = Srgb8::new([0; 3]); - let conflict = ReadProbe::new(conflicting_values); + let mut mismatched = committed; + mismatched[0] = Srgb8::new([0xff; 3]); + let mismatch = ReadProbe::new(mismatched); let (result, allocations) = crate::test_support::measured_allocations(|| { session - .update_canonical_present(5, MULTI_PORTS.len(), |index| conflict.read(index)) + .update_canonical_present(5, 3, |index| mismatch.read(index)) .map(|_| ()) }); assert_eq!( result, - Err(SessionUpdateError::RevisionConflict { revision: 5 }) + Err(SessionUpdateError::::RevisionConflict { revision: 5 }) ); - assert_eq!(conflict.reads(), [1, 1, 1]); + assert_eq!(mismatch.reads(), [1, 1, 1]); assert_eq!(allocations, 0); assert_eq!(crate::composition::source_over_evaluation_count(), 0); - assert_eq!(retained_signal_storage_pointers(session.state()), storage); - - let expected_surfaces = [ - SurfaceSignal::new(MULTI_PORT_A, committed_values[0]), - SurfaceSignal::new(MULTI_PORT_B, committed_values[1]), - SurfaceSignal::new(MULTI_PORT_C, committed_values[2]), - ]; - let SessionState::Ready { current } = session.state() else { - panic!("same-revision admission must retain Ready"); + let SessionState::Conflict { current, .. } = session.state() else { + unreachable!() }; - assert_eq!(current.revision(), 5); - assert!(current.surfaces().eq(expected_surfaces)); - assert_eq!(current.composited_occurrence_signals_rgb24(), &[0x80_80_80]); - assert!(session.bound_surface_inputs_for_test().eq([ - (MULTI_PORT_A, committed_values[0]), - (MULTI_PORT_B, committed_values[1]), - (MULTI_PORT_C, committed_values[2]), - ])); -} - -#[test] -fn new_revision_reads_each_value_once_and_snapshots_binding_readback() { - let owner = multi_compiled().into_owner(); - let mut session = owner.attach().unwrap(); - let initial_values = [ - Srgb8::new([0x10; 3]), - Srgb8::new([0xff; 3]), - Srgb8::new([0x30; 3]), - ]; - session - .update_canonical_present(1, MULTI_PORTS.len(), |index| initial_values[index]) - .unwrap(); - let storage = retained_signal_storage_pointers(session.state()); - let next_values = [ - Srgb8::new([0xa1, 0xa2, 0xa3]), - Srgb8::new([0; 3]), - Srgb8::new([0xc1, 0xc2, 0xc3]), - ]; - let probe = ReadProbe::new(next_values); - crate::composition::reset_source_over_evaluation_count(); + assert_eq!(current.storage_pointers_for_test(), storage); let (result, allocations) = crate::test_support::measured_allocations(|| { session - .update_canonical_present(2, MULTI_PORTS.len(), |index| probe.read(index)) + .update(SurfaceUpdate::Unavailable { + revision: 5, + reason: 91, + }) .map(|_| ()) }); - - assert_eq!(result, Ok(())); - assert_eq!(probe.reads(), [1, 1, 1]); - assert_eq!(allocations, 0); - assert_eq!(crate::composition::source_over_evaluation_count(), 1); - assert_eq!(retained_signal_storage_pointers(session.state()), storage); - let expected_surfaces = [ - SurfaceSignal::new(MULTI_PORT_A, next_values[0]), - SurfaceSignal::new(MULTI_PORT_B, next_values[1]), - SurfaceSignal::new(MULTI_PORT_C, next_values[2]), - ]; - let SessionState::Ready { current } = session.state() else { - panic!("a new canonical revision must commit Ready"); - }; - assert_eq!(current.revision(), 2); - assert!(current.surfaces().eq(expected_surfaces)); - assert_eq!(current.composited_occurrence_signals_rgb24(), &[0]); - assert!(session.bound_surface_inputs_for_test().eq([ - (MULTI_PORT_A, next_values[0]), - (MULTI_PORT_B, next_values[1]), - (MULTI_PORT_C, next_values[2]), - ])); -} - -#[test] -fn public_program_compile_owner_session_path_emits_typed_occurrence_values() { - let compiled = compiled(0.5); - assert_eq!(compiled.surface_input_ports(), &[SURFACE_PORT]); - assert_eq!(compiled.occurrences(), &[OCCURRENCE]); - - let owner = PointRenderOwner::new(compiled); - assert_eq!(owner.surface_input_ports(), Some(&[SURFACE_PORT][..])); - assert_eq!(owner.occurrences(), Some(&[OCCURRENCE][..])); - let mut session = owner.attach().unwrap(); - let surfaces = [SurfaceSignal::new(SURFACE_PORT, Srgb8::new([0xff; 3]))]; - let SessionState::Ready { current } = session - .update(SurfaceUpdate::Present { - revision: 11, - surfaces: &surfaces, - }) - .unwrap() - else { - panic!("typed present update must produce Ready"); - }; - - assert_eq!(current.revision(), 11); - assert_eq!(current.surfaces().collect::>(), surfaces.to_vec()); assert_eq!( - current - .occurrences() - .map(|signal| (signal.occurrence(), signal.value())) - .collect::>(), - vec![(OCCURRENCE, Srgb8::new([0x80; 3]))] + result, + Err(SessionUpdateError::::RevisionConflict { revision: 5 }) ); - assert_eq!(current.occurrence(OCCURRENCE), Some(Srgb8::new([0x80; 3]))); - assert_eq!(current.occurrence(OccurrenceId::new(999)), None); + assert_eq!(allocations, 0); + let SessionState::Conflict { current, .. } = session.state() else { + unreachable!() + }; + assert_eq!(current.storage_pointers_for_test(), storage); } #[test] -fn typed_update_schema_rejection_is_atomic_and_allocation_free() { - let owner = compiled(0.5).into_owner(); +fn typed_schema_revision_and_lifetime_failures_are_atomic() { + let mut owner = compiled_exact(Srgb8::new([0x80; 3])).into_owner(); let mut session = owner.attach().unwrap(); let wrong = [SurfaceSignal::new( SurfaceInputPortId::new(999), Srgb8::new([0xff; 3]), )]; crate::composition::reset_source_over_evaluation_count(); - let (result, allocations) = crate::test_support::measured_allocations(|| { session .update(SurfaceUpdate::Present { @@ -724,7 +1062,7 @@ fn typed_update_schema_rejection_is_atomic_and_allocation_free() { }); assert_eq!( result, - Err(SessionUpdateError::SurfaceInputPortMismatch { + Err(SessionUpdateError::::SurfaceInputPortMismatch { index: 0, expected: SURFACE_PORT, actual: SurfaceInputPortId::new(999), @@ -738,11 +1076,92 @@ fn typed_update_schema_rejection_is_atomic_and_allocation_free() { current_unavailable: None } )); + + session + .update_canonical_present(5, 1, |_| Srgb8::new([0xff; 3])) + .unwrap(); + let SessionState::Ready { current } = session.state() else { + unreachable!() + }; + let retained_storage = current.storage_pointers_for_test(); + let reads = Cell::new(0); + crate::composition::reset_source_over_evaluation_count(); + let (result, allocations) = crate::test_support::measured_allocations(|| { + session + .update_canonical_present(4, 1, |_| { + reads.set(reads.get() + 1); + Srgb8::new([0; 3]) + }) + .map(|_| ()) + }); + assert_eq!( + result, + Err(SessionUpdateError::::RevisionOutOfOrder { + current: 5, + incoming: 4, + }) + ); + assert_eq!(reads.get(), 0); + assert_eq!(allocations, 0); + assert_eq!(crate::composition::source_over_evaluation_count(), 0); + let SessionState::Ready { current } = session.state() else { + unreachable!() + }; + assert_eq!(current.revision(), 5); + assert_eq!(current.storage_pointers_for_test(), retained_storage); + + owner.dispose(); + let (result, allocations) = crate::test_support::measured_allocations(|| { + session + .update(SurfaceUpdate::Unavailable { + revision: 6, + reason: 1, + }) + .map(|_| ()) + }); + assert_eq!( + result, + Err(SessionUpdateError::::ProgramExpired) + ); + assert_eq!(allocations, 0); } #[test] -fn typed_update_reuses_attach_storage_for_ready_stale_and_recovery() { - let owner = compiled(0.5).into_owner(); +fn replacement_revokes_old_sessions_and_compile_error_keeps_live_epoch() { + let mut owner = compiled_exact(Srgb8::new([0x80; 3])).into_owner(); + let mut old = owner.attach().unwrap(); + assert_eq!( + compile_error(base_program( + 1.25, + BACKDROP, + ConstraintSet::new( + vec![ConstraintInvocation::hard( + REQUIRED, + OCCURRENCE, + Srgb8::new([0x80; 3]), + )], + vec![], + ), + vec![OutputBinding::new(OUTPUT, TRANSLUCENT)], + ExactSrgb8IdentityV1, + )), + ProgramCompileError::OpacityOutOfDomain { input: OPACITY } + ); + assert!( + old.update_canonical_present(1, 1, |_| Srgb8::new([0xff; 3])) + .is_ok() + ); + + owner.replace(compiled_exact(Srgb8::new([0x80; 3]))); + assert_eq!( + update_error(old.update_canonical_present(2, 1, |_| Srgb8::new([0xff; 3]))), + SessionUpdateError::::ProgramExpired + ); +} + +#[test] +fn present_ready_conflict_stale_and_recovery_allocate_zero_after_attach() { + let owner = compiled_exact(Srgb8::new([0x80; 3])).into_owner(); let mut session = owner.attach().unwrap(); let white = [SurfaceSignal::new(SURFACE_PORT, Srgb8::new([0xff; 3]))]; let black = [SurfaceSignal::new(SURFACE_PORT, Srgb8::new([0; 3]))]; @@ -752,13 +1171,17 @@ fn typed_update_reuses_attach_storage_for_ready_stale_and_recovery() { revision: 1, surfaces: &white, }, - SurfaceUpdate::Unavailable { + SurfaceUpdate::Present { revision: 2, + surfaces: &black, + }, + SurfaceUpdate::Unavailable { + revision: 3, reason: 7, }, SurfaceUpdate::Present { - revision: 3, - surfaces: &black, + revision: 4, + surfaces: &white, }, ] { let (result, allocations) = @@ -769,56 +1192,178 @@ fn typed_update_reuses_attach_storage_for_ready_stale_and_recovery() { } #[test] -fn dropping_the_only_owner_physically_expires_attached_sessions() { - let mut session = { - let owner = compiled(0.5).into_owner(); - owner.attach().unwrap() - }; - let surfaces = [SurfaceSignal::new(SURFACE_PORT, Srgb8::new([0; 3]))]; +fn canonical_helpers_and_checked_cardinality_fail_closed() { + assert_eq!(check_render_node_count(usize::MAX - 1, 1), Ok(())); assert_eq!( - session.update(SurfaceUpdate::Present { - revision: 1, - surfaces: &surfaces, - }), - Err(SessionUpdateError::ProgramExpired) + check_render_node_count(usize::MAX, 1), + Err(ProgramCompileError::ResourceExhausted) ); + let canonical = [SurfaceInputPortId::new(1), SurfaceInputPortId::new(2)]; + assert!(canonical_surface_input_port_sequence_matches( + [SurfaceInputPortId::new(1), SurfaceInputPortId::new(2)], + &canonical, + )); + assert!(!canonical_surface_input_port_sequence_matches( + [SurfaceInputPortId::new(2), SurfaceInputPortId::new(1)], + &canonical, + )); + assert!(!canonical_surface_input_port_sequence_matches( + [SurfaceInputPortId::new(1)], + &canonical, + )); } #[test] -fn generic_program_module_has_no_recipe_or_ui_compatibility_surface() { - let source = include_str!("program_session.rs"); - for forbidden in [ - "ThemeConfig", - "RoleRecipe", - "NamedRoleTable", - "PairFill", - "PairLabel", - "AlphaAnalog", - "resolve_named_set", - "resolveTheme", - "themeHandle", - concat!("PACK", "ED_ENCODED_"), - concat!("Pack", "edEncoded"), - "PointRenderSessionUpdateError", - concat!("update_pa", "cked"), - concat!("decode_encoded_", "surface_update"), - ] { - assert!( - !source.contains(forbidden), - "generic Program module must not contain `{forbidden}`" - ); - } - for required in [ - "pub struct Program", - "pub struct CompiledProgram", - "pub struct PointRenderOwner", - "pub struct Session", - "pub fn compile(self)", - "pub fn update(", - ] { - assert!( - source.contains(required), - "generic Program module must retain `{required}`" +fn callback_is_not_read_before_lifetime_cardinality_or_revision_admission() { + let mut expired = { + let owner = compiled_exact(Srgb8::new([0x80; 3])).into_owner(); + owner.attach().unwrap() + }; + let reads = Cell::new(0); + let (result, allocations) = crate::test_support::measured_allocations(|| { + expired + .update_canonical_present(1, 1, |_| { + reads.set(reads.get() + 1); + Srgb8::new([0xff; 3]) + }) + .map(|_| ()) + }); + assert_eq!( + result, + Err(SessionUpdateError::::ProgramExpired) + ); + assert_eq!(reads.get(), 0); + assert_eq!(allocations, 0); + + let owner = compiled_exact(Srgb8::new([0x80; 3])).into_owner(); + let mut session = owner.attach().unwrap(); + for actual in [0, 2] { + let reads = Cell::new(0); + let (result, allocations) = crate::test_support::measured_allocations(|| { + session + .update_canonical_present(1, actual, |_| { + reads.set(reads.get() + 1); + Srgb8::new([0xff; 3]) + }) + .map(|_| ()) + }); + assert_eq!( + result, + Err( + SessionUpdateError::::SurfaceInputPortLengthMismatch { + expected: 1, + actual, + } + ) ); + assert_eq!(reads.get(), 0); + assert_eq!(allocations, 0); } + + session + .update(SurfaceUpdate::Unavailable { + revision: 7, + reason: 12, + }) + .unwrap(); + let reads = Cell::new(0); + let (result, allocations) = crate::test_support::measured_allocations(|| { + session + .update_canonical_present(7, 1, |_| { + reads.set(reads.get() + 1); + Srgb8::new([0xff; 3]) + }) + .map(|_| ()) + }); + assert_eq!( + result, + Err(SessionUpdateError::::RevisionConflict { revision: 7 }) + ); + assert_eq!(reads.get(), 0); + assert_eq!(allocations, 0); +} + +#[test] +fn same_paint_can_be_assessed_in_multiple_occurrences_but_is_one_terminal_output() { + const OTHER_PORT: SurfaceInputPortId = SurfaceInputPortId::new(4); + const OTHER_SURFACE: SurfaceId = SurfaceId::new(22); + const OTHER_OCCURRENCE: OccurrenceId = OccurrenceId::new(31); + let program = Program::new( + vec![ColorInput::new(COLOR, Srgb8::new([0; 3]))], + vec![OTHER_PORT, SURFACE_PORT], + vec![OpacityInput::new(OPACITY, 0.5)], + vec![ + Paint::Solid { + id: SOLID, + color: COLOR, + }, + Paint::Opacity { + id: TRANSLUCENT, + source: SOLID, + opacity: OPACITY, + }, + ], + vec![ + Surface::Input { + id: BACKDROP, + input: SURFACE_PORT, + }, + Surface::Input { + id: OTHER_SURFACE, + input: OTHER_PORT, + }, + ], + vec![ + Occurrence::new( + OCCURRENCE, + TRANSLUCENT, + BACKDROP, + CompositionProfile::EncodedSrgb8SourceOverV1, + ), + Occurrence::new( + OTHER_OCCURRENCE, + TRANSLUCENT, + OTHER_SURFACE, + CompositionProfile::EncodedSrgb8SourceOverV1, + ), + ], + ConstraintSet::new( + vec![ConstraintInvocation::hard( + REQUIRED, + OCCURRENCE, + Srgb8::new([0x80; 3]), + )], + vec![ConstraintInvocation::report_only( + ConstraintId::new(51), + OTHER_OCCURRENCE, + Srgb8::new([0; 3]), + )], + ), + vec![OutputBinding::new(OUTPUT, TRANSLUCENT)], + ExactSrgb8IdentityV1, + ); + let owner = program.compile().unwrap().into_owner(); + assert_eq!( + owner.surface_input_ports(), + Some(&[SURFACE_PORT, OTHER_PORT][..]) + ); + let mut session = owner.attach().unwrap(); + let signals = [ + SurfaceSignal::new(SURFACE_PORT, Srgb8::new([0xff; 3])), + SurfaceSignal::new(OTHER_PORT, Srgb8::new([0; 3])), + ]; + let SessionState::Ready { current } = session + .update(SurfaceUpdate::Present { + revision: 1, + surfaces: &signals, + }) + .unwrap() + else { + panic!("both exact occurrence contracts should pass"); + }; + assert_eq!(current.report().count(), 2); + let outputs = current.outputs().collect::>(); + assert_eq!(outputs.len(), 1); + assert_eq!(outputs[0].value().source(), Srgb8::new([0; 3])); + assert_eq!(outputs[0].value().straight_alpha_bits(), 0.5f64.to_bits()); } diff --git a/scripts/verify_point_support_surplus.py b/scripts/verify_point_support_surplus.py old mode 100755 new mode 100644 index 71e1010f..84c7a663 --- a/scripts/verify_point_support_surplus.py +++ b/scripts/verify_point_support_surplus.py @@ -57,7 +57,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 = ( - "690cd5ed543f1e30602e825153a9d0857bfea1089350d234e701b181e8819862" + "d95e75277933ecf04f72cef57d287a1c4c4ad373445552af110fae17d730d592" ) EXPECTED_Q55_PROOF_SHA256 = ( "ac59cf89503170c789223b91d775213a19d4e571ef930f2ea609fcd51b14defd" From b9c796526febedb6644a22f501a8bd6d5ad68d38 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Wed, 22 Jul 2026 16:35:51 +0300 Subject: [PATCH 23/58] feat(core): bind observation groups to runtime streams --- ...rt-reference-surplus-q55-bps-proof-v1.json | 2 +- .../src/generic_boundary_tests.rs | 67 +++ crates/labcolors-core/src/observation.rs | 10 + crates/labcolors-core/src/program_session.rs | 218 ++++++-- .../src/program_session_tests.rs | 515 ++++++++++++++---- scripts/verify_point_support_surplus.py | 2 +- 6 files changed, 682 insertions(+), 132 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 2ff6ca52..19170185 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":"4f45115564741394c194ff7ab4fe43a277c9cdc442371b18055d6eb25019d262","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":"d95e75277933ecf04f72cef57d287a1c4c4ad373445552af110fae17d730d592","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":"57e6feeab15182322c258829d10214c31bb3c93fa58d8d47e37127db204cdc28"},{"kind":"rust-source","path":"crates/labcolors-core/src/composition.rs","sha256":"195a67327a3bd86d7816b634481389930bf68577bb1202fad14c2ea152df8625"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/exact.rs","sha256":"45bd09cd8f6f74860a0a3ffeb410ee2c3cf91b4868877abbc949e724b61e1ccd"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/mod.rs","sha256":"2f3e44b2db837deed0df3e34063df9cfc6af82a912373da488c9fe2137a119c0"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/wcag22.rs","sha256":"985e21abccd1fcb4a3f9624d5947ff3d1af7a1bf26b6b170ea8bbef77d6f1798"},{"kind":"rust-source","path":"crates/labcolors-core/src/hash.rs","sha256":"f97a0fd7d6ad3162f0f1dfb326fccfb7ed40da9a8fa67a5b8a239a1ae2ae49c3"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"d87d4959e28556de54f815b7f833d5b9310cf4f4209916257e2d1ed8ad160810"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"208066b255bc406303b18dc2eb6f1720c8203ea6eb37d225f9d8110c2449edc4"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"56c06b49f4687b3def69c247978da1b0cb45b42c09515a001d196cb1457c93a6"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"d4b06651e07cf4f93eccf9866f6e63c35ea72fa7125bdb41e222b19b70dd2f32"},{"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":33,"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":"096af1ff7bd7baa94d34407705b3181b7c1d04a40587ac9310235557f5bceb32"} +{"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":"03f21bc3ced62e400748cae568ea2313c68a7bf5a46d5e800d501e6db61880ba","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":"51b242a0ff319f34357fbeef89037118515172dcdc088a09b9ff28e0cc38e860","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":"57e6feeab15182322c258829d10214c31bb3c93fa58d8d47e37127db204cdc28"},{"kind":"rust-source","path":"crates/labcolors-core/src/composition.rs","sha256":"195a67327a3bd86d7816b634481389930bf68577bb1202fad14c2ea152df8625"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/exact.rs","sha256":"45bd09cd8f6f74860a0a3ffeb410ee2c3cf91b4868877abbc949e724b61e1ccd"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/mod.rs","sha256":"2f3e44b2db837deed0df3e34063df9cfc6af82a912373da488c9fe2137a119c0"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/wcag22.rs","sha256":"985e21abccd1fcb4a3f9624d5947ff3d1af7a1bf26b6b170ea8bbef77d6f1798"},{"kind":"rust-source","path":"crates/labcolors-core/src/hash.rs","sha256":"f97a0fd7d6ad3162f0f1dfb326fccfb7ed40da9a8fa67a5b8a239a1ae2ae49c3"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"d87d4959e28556de54f815b7f833d5b9310cf4f4209916257e2d1ed8ad160810"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"abddb1cbc5b799428da1e1b15ff6bfa9631eea4e19c06825abaed76e74c266ef"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"56c06b49f4687b3def69c247978da1b0cb45b42c09515a001d196cb1457c93a6"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"d4b06651e07cf4f93eccf9866f6e63c35ea72fa7125bdb41e222b19b70dd2f32"},{"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":33,"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":"0e51a601f9ca4a386b9141b764606ddbdeafb53c4c5fcb6170b0cb26fc65863a"} diff --git a/crates/labcolors-core/src/generic_boundary_tests.rs b/crates/labcolors-core/src/generic_boundary_tests.rs index 7fb322ed..77bec8d8 100644 --- a/crates/labcolors-core/src/generic_boundary_tests.rs +++ b/crates/labcolors-core/src/generic_boundary_tests.rs @@ -65,6 +65,73 @@ fn program_session_module_docs_disclaim_transport_only_scope() { } } +#[test] +fn current_og0_program_epoch_has_one_explicit_group_without_scenario_scope_creep() { + fn declaration<'a>(source: &'a str, start: &str, end: &str) -> &'a str { + let start = source + .find(start) + .unwrap_or_else(|| panic!("missing OG0 declaration start `{start}`")); + let end = source[start..] + .find(end) + .map(|offset| start + offset) + .unwrap_or_else(|| panic!("missing OG0 declaration end `{end}`")); + &source[start..end] + } + + // This pins only the current private OG0 Program/epoch shape. It does not + // prescribe how a future explicit join or independent component API works. + let program = declaration( + PROGRAM_SESSION_SOURCE, + "pub struct Program<", + "impl Program", + ); + let epoch = declaration( + PROGRAM_SESSION_SOURCE, + "struct ProgramEpochV1<", + "/// Fully validated immutable Program", + ); + assert_eq!( + program + .matches("observation_group: ObservationGroup,") + .count(), + 1, + "the current OG0 authored Program owns one explicit atomic group" + ); + assert_eq!( + epoch + .matches("observation_group: CompiledObservationGroupV1,") + .count(), + 1, + "the current OG0 epoch must retain that one compiled group" + ); + for (name, scope) in [("Program", program), ("ProgramEpochV1", epoch)] { + for forbidden in ["Vec", "Scenario"] { + assert!( + !scope.contains(forbidden), + "current OG0 {name} declaration must not grow `{forbidden}` scope" + ); + } + } + + for forbidden in [ + "Vec", + "ScenarioId", + "ScenarioSet", + "ObservedScenarioSet", + "GraphTemplate", + "Cartesian", + "wasm_bindgen", + "serde", + "Dto", + "DTO", + ] { + assert!( + !PROGRAM_SESSION_SOURCE.contains(forbidden), + "current OG0 transport module must not acquire `{forbidden}` scope" + ); + } +} + #[test] fn f0_signal_transform_has_no_renderer_alias_or_raw_xyz_production_constructor() { for forbidden in [ diff --git a/crates/labcolors-core/src/observation.rs b/crates/labcolors-core/src/observation.rs index a67d813a..ae09a7e4 100644 --- a/crates/labcolors-core/src/observation.rs +++ b/crates/labcolors-core/src/observation.rs @@ -9,6 +9,16 @@ use core::ops::Range; use crate::Srgb8; use crate::appearance::SurfaceInputPortId; +/// Stable compile-time identity of one atomic observation boundary. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct ObservationGroupId(u32); + +impl ObservationGroupId { + pub(crate) const fn new(raw: u32) -> Self { + Self(raw) + } +} + /// Runtime instance/epoch of one atomic observation stream. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub(crate) struct ObservationStreamId(u32); diff --git a/crates/labcolors-core/src/program_session.rs b/crates/labcolors-core/src/program_session.rs index ce331113..c5eb7290 100644 --- a/crates/labcolors-core/src/program_session.rs +++ b/crates/labcolors-core/src/program_session.rs @@ -25,6 +25,7 @@ use crate::constraints::{ HardDecision, PointEvaluationError, PointEvaluatorV1, PointInvocation, VisiblePointPassEvidence, VisiblePointViolationEvidence, assess_visible_point_hard, }; +use crate::observation::{ObservationGroupId, ObservationStreamId}; /// One immutable encoded colour binding owned by a [`Program`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -280,6 +281,51 @@ impl OutputBinding { } } +/// One compile-time atomic correlation boundary for this Program epoch. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ObservationGroup { + id: ObservationGroupId, + surface_input_ports: Vec, +} + +impl ObservationGroup { + pub const fn new(id: ObservationGroupId, surface_input_ports: Vec) -> Self { + Self { + id, + surface_input_ports, + } + } + + pub const fn id(&self) -> ObservationGroupId { + self.id + } + + pub fn surface_input_ports(&self) -> &[SurfaceInputPortId] { + &self.surface_input_ports + } +} + +/// Attachment-time binding of a compiled group to one runtime stream epoch. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ObservationStreamBinding { + group: ObservationGroupId, + stream: ObservationStreamId, +} + +impl ObservationStreamBinding { + pub const fn new(group: ObservationGroupId, stream: ObservationStreamId) -> Self { + Self { group, stream } + } + + pub const fn group(self) -> ObservationGroupId { + self.group + } + + pub const fn stream(self) -> ObservationStreamId { + self.stream + } +} + /// Immutable generic point Program. pub struct Program where @@ -287,7 +333,7 @@ where PointInvocation: Copy, { colors: Vec, - surface_input_ports: Vec, + observation_group: ObservationGroup, opacities: Vec, paints: Vec, surfaces: Vec, @@ -305,7 +351,7 @@ where #[allow(clippy::too_many_arguments)] pub fn new( colors: Vec, - surface_input_ports: Vec, + observation_group: ObservationGroup, opacities: Vec, paints: Vec, surfaces: Vec, @@ -316,7 +362,7 @@ where ) -> Self { Self { colors, - surface_input_ports, + observation_group, opacities, paints, surfaces, @@ -391,7 +437,9 @@ pub enum ProgramCompileError { OpacityOutOfDomain { input: OpacityInputId, }, - EmptySurfaceSchema, + EmptyObservationGroup { + group: ObservationGroupId, + }, EmptyOccurrenceSet, EmptyConstraintSet, EmptyOutputSet, @@ -434,6 +482,11 @@ struct CompiledOutputBinding { paint: CompiledPaintSlotV1, } +struct CompiledObservationGroupV1 { + id: ObservationGroupId, + surface_input_ports: Box<[SurfaceInputPortId]>, +} + struct ProgramEpochV1 where Evaluation: PointEvaluatorV1, @@ -442,7 +495,7 @@ where evaluator: Evaluation, graph: CompiledAppearanceGraph, binding_template: AdmittedAppearanceBindings, - surface_input_ports: Box<[SurfaceInputPortId]>, + observation_group: CompiledObservationGroupV1, constraints: Box<[CompiledPointConstraint>]>, outputs: Box<[CompiledOutputBinding]>, } @@ -461,8 +514,12 @@ where Evaluation: PointEvaluatorV1, PointInvocation: Copy, { + pub const fn observation_group_id(&self) -> ObservationGroupId { + self.epoch.observation_group.id + } + pub fn surface_input_ports(&self) -> &[SurfaceInputPortId] { - &self.epoch.surface_input_ports + &self.epoch.observation_group.surface_input_ports } pub fn constraint_ids(&self) -> impl ExactSizeIterator + '_ { @@ -487,6 +544,10 @@ where #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PointRenderAttachError { Disposed, + ObservationGroupMismatch { + expected: ObservationGroupId, + actual: ObservationGroupId, + }, ResourceExhausted, InternalInvariant, } @@ -519,10 +580,16 @@ where self.current = None; } + pub fn observation_group_id(&self) -> Option { + self.current + .as_deref() + .map(|epoch| epoch.observation_group.id) + } + pub fn surface_input_ports(&self) -> Option<&[SurfaceInputPortId]> { self.current .as_deref() - .map(|epoch| epoch.surface_input_ports.as_ref()) + .map(|epoch| epoch.observation_group.surface_input_ports.as_ref()) } pub fn constraint_ids(&self) -> Option + '_> { @@ -541,11 +608,20 @@ where } /// Fallibly allocate every hot-path frame before the Session escapes. - pub fn attach(&self) -> Result, PointRenderAttachError> { + pub fn attach( + &self, + binding: ObservationStreamBinding, + ) -> Result, PointRenderAttachError> { let epoch = self .current .as_ref() .ok_or(PointRenderAttachError::Disposed)?; + if binding.group != epoch.observation_group.id { + return Err(PointRenderAttachError::ObservationGroupMismatch { + expected: epoch.observation_group.id, + actual: binding.group, + }); + } let workspace = epoch .graph .new_workspace() @@ -561,6 +637,7 @@ where ]; Ok(Session { epoch: Rc::downgrade(epoch), + stream: binding.stream, bindings, workspace, free_frames, @@ -585,8 +662,10 @@ where Evaluation: PointEvaluatorV1, PointInvocation: Copy, { - if program.surface_input_ports.is_empty() { - return Err(ProgramCompileError::EmptySurfaceSchema); + if program.observation_group.surface_input_ports.is_empty() { + return Err(ProgramCompileError::EmptyObservationGroup { + group: program.observation_group.id, + }); } if program.occurrences.is_empty() { return Err(ProgramCompileError::EmptyOccurrenceSet); @@ -610,9 +689,9 @@ where let mut surface_input_ports = Vec::new(); surface_input_ports - .try_reserve_exact(program.surface_input_ports.len()) + .try_reserve_exact(program.observation_group.surface_input_ports.len()) .map_err(|_| ProgramCompileError::ResourceExhausted)?; - surface_input_ports.extend_from_slice(&program.surface_input_ports); + surface_input_ports.extend_from_slice(&program.observation_group.surface_input_ports); surface_input_ports.sort_unstable(); if !canonical_surface_input_port_sequence_matches( graph.surface_input_ports(), @@ -627,7 +706,10 @@ where evaluator: program.evaluator, graph, binding_template, - surface_input_ports: surface_input_ports.into_boxed_slice(), + observation_group: CompiledObservationGroupV1 { + id: program.observation_group.id, + surface_input_ports: surface_input_ports.into_boxed_slice(), + }, constraints, outputs, }) @@ -781,7 +863,7 @@ where { AppearanceGraphSpec::new( program.colors.iter().map(|input| input.id).collect(), - program.surface_input_ports.clone(), + program.observation_group.surface_input_ports.clone(), program.opacities.iter().map(|input| input.id).collect(), program .paints @@ -838,6 +920,7 @@ where .map(|input| (input.id, input.value)) .collect(), program + .observation_group .surface_input_ports .iter() .map(|input| (*input, Srgb8::new([0; 3]))) @@ -956,17 +1039,53 @@ impl SurfaceSignal { } } +/// Transport-only payload carried by one stream-affine point update. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SurfaceUpdatePayload<'input> { + Unavailable { reason: u32 }, + Present { surfaces: &'input [SurfaceSignal] }, +} + /// Borrowed, correlated runtime update for one attached Session. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum SurfaceUpdate<'input> { - Unavailable { - revision: u64, - reason: u32, - }, - Present { +pub struct SurfaceUpdate<'input> { + stream: ObservationStreamId, + revision: u64, + payload: SurfaceUpdatePayload<'input>, +} + +impl<'input> SurfaceUpdate<'input> { + pub const fn unavailable(stream: ObservationStreamId, revision: u64, reason: u32) -> Self { + Self { + stream, + revision, + payload: SurfaceUpdatePayload::Unavailable { reason }, + } + } + + pub const fn present( + stream: ObservationStreamId, revision: u64, surfaces: &'input [SurfaceSignal], - }, + ) -> Self { + Self { + stream, + revision, + payload: SurfaceUpdatePayload::Present { surfaces }, + } + } + + pub const fn stream(self) -> ObservationStreamId { + self.stream + } + + pub const fn revision(self) -> u64 { + self.revision + } + + pub const fn payload(self) -> SurfaceUpdatePayload<'input> { + self.payload + } } /// Evaluator classification with the exact bound occurrence evidence. @@ -1108,10 +1227,11 @@ where fn try_new(epoch: &ProgramEpochV1) -> Result { let mut surfaces = Vec::new(); surfaces - .try_reserve_exact(epoch.surface_input_ports.len()) + .try_reserve_exact(epoch.observation_group.surface_input_ports.len()) .map_err(|_| PointRenderAttachError::ResourceExhausted)?; surfaces.extend( epoch + .observation_group .surface_input_ports .iter() .copied() @@ -1310,6 +1430,10 @@ where #[derive(Debug, PartialEq, Eq)] pub enum SessionUpdateError { ProgramExpired, + ObservationStreamMismatch { + expected: ObservationStreamId, + actual: ObservationStreamId, + }, SurfaceInputPortLengthMismatch { expected: usize, actual: usize, @@ -1340,6 +1464,7 @@ where PointInvocation: Copy, { epoch: Weak>, + stream: ObservationStreamId, bindings: AdmittedAppearanceBindings, workspace: AppearanceWorkspace, free_frames: [Option>; 3], @@ -1355,6 +1480,10 @@ where &self.state } + pub const fn stream(&self) -> ObservationStreamId { + self.stream + } + pub fn update( &mut self, update: SurfaceUpdate<'_>, @@ -1364,18 +1493,28 @@ where .epoch .upgrade() .ok_or(SessionUpdateError::ProgramExpired)?; - match update { - SurfaceUpdate::Unavailable { revision, reason } => { - self.apply_unavailable(SurfaceUnavailable { revision, reason }) + if update.stream != self.stream { + return Err(SessionUpdateError::ObservationStreamMismatch { + expected: self.stream, + actual: update.stream, + }); + } + match update.payload { + SurfaceUpdatePayload::Unavailable { reason } => { + self.apply_unavailable(SurfaceUnavailable { + revision: update.revision, + reason, + }) } - SurfaceUpdate::Present { revision, surfaces } => { - if surfaces.len() != epoch.surface_input_ports.len() { + SurfaceUpdatePayload::Present { surfaces } => { + if surfaces.len() != epoch.observation_group.surface_input_ports.len() { return Err(SessionUpdateError::SurfaceInputPortLengthMismatch { - expected: epoch.surface_input_ports.len(), + expected: epoch.observation_group.surface_input_ports.len(), actual: surfaces.len(), }); } for (index, (&expected, actual)) in epoch + .observation_group .surface_input_ports .iter() .zip(surfaces.iter()) @@ -1389,7 +1528,7 @@ where }); } } - self.apply_canonical_present(&epoch, revision, surfaces.len(), |index| { + self.apply_canonical_present(&epoch, update.revision, surfaces.len(), |index| { surfaces[index].value }) } @@ -1398,6 +1537,7 @@ where pub(crate) fn update_canonical_present( &mut self, + stream: ObservationStreamId, revision: u64, surface_input_port_count: usize, value_at: impl FnMut(usize) -> Srgb8, @@ -1407,6 +1547,12 @@ where .epoch .upgrade() .ok_or(SessionUpdateError::ProgramExpired)?; + if stream != self.stream { + return Err(SessionUpdateError::ObservationStreamMismatch { + expected: self.stream, + actual: stream, + }); + } self.apply_canonical_present(&epoch, revision, surface_input_port_count, value_at) } @@ -1463,9 +1609,9 @@ where mut value_at: impl FnMut(usize) -> Srgb8, ) -> Result<&SessionState, SessionUpdateError>> { - if surface_input_port_count != epoch.surface_input_ports.len() { + if surface_input_port_count != epoch.observation_group.surface_input_ports.len() { return Err(SessionUpdateError::SurfaceInputPortLengthMismatch { - expected: epoch.surface_input_ports.len(), + expected: epoch.observation_group.surface_input_ports.len(), actual: surface_input_port_count, }); } @@ -1484,7 +1630,7 @@ where let mut frame = take_free_frame(&mut self.free_frames).ok_or(SessionUpdateError::InternalInvariant)?; frame.clear_dynamic(); - if frame.surfaces.len() != epoch.surface_input_ports.len() + if frame.surfaces.len() != epoch.observation_group.surface_input_ports.len() || frame.reports.len() != epoch.constraints.len() || frame.outputs.len() != epoch.outputs.len() { @@ -1496,11 +1642,13 @@ where if self .bindings .overwrite_surface_inputs_canonical( - epoch.surface_input_ports.iter().copied(), + epoch.observation_group.surface_input_ports.iter().copied(), &mut |index| { let value = value_at(index); - surface_slots[index] = - SurfaceSignal::new(epoch.surface_input_ports[index], value); + surface_slots[index] = SurfaceSignal::new( + epoch.observation_group.surface_input_ports[index], + value, + ); value }, ) diff --git a/crates/labcolors-core/src/program_session_tests.rs b/crates/labcolors-core/src/program_session_tests.rs index 6dce9845..b7907486 100644 --- a/crates/labcolors-core/src/program_session_tests.rs +++ b/crates/labcolors-core/src/program_session_tests.rs @@ -10,11 +10,13 @@ use crate::constraints::{ ProgramTestEvaluatorV1, ProgramTestInvocationV1, Wcag22Srgb8V1, arm_program_test_failure_once, program_test_evaluation_count, reset_program_test_evaluation_count, }; +use crate::observation::{ObservationGroupId, ObservationStreamId}; use crate::program_session::{ ColorInput, CompiledProgram, CompositionProfile, ConstraintAssessment, ConstraintId, ConstraintInvocation, ConstraintOutcome, ConstraintReportEntry, ConstraintSet, HardModeV1, - Occurrence, OpacityInput, OutputBinding, OutputSlotId, Paint, Program, ProgramCompileError, - ReportModeV1, SessionState, SessionUpdateError, Surface, SurfaceSignal, SurfaceUpdate, + ObservationGroup, ObservationStreamBinding, Occurrence, OpacityInput, OutputBinding, + OutputSlotId, Paint, PointRenderAttachError, Program, ProgramCompileError, ReportModeV1, + SessionState, SessionUpdateError, Surface, SurfaceSignal, SurfaceUpdate, SurfaceUpdatePayload, canonical_surface_input_port_sequence_matches, check_render_node_count, }; use crate::wcag22::{Wcag22ApplicableDecisionV1, Wcag22CriterionV1}; @@ -29,6 +31,25 @@ const VISIBLE_SURFACE: SurfaceId = SurfaceId::new(21); const OCCURRENCE: OccurrenceId = OccurrenceId::new(30); const OUTPUT: OutputSlotId = OutputSlotId::new(40); const REQUIRED: ConstraintId = ConstraintId::new(50); +const GROUP: ObservationGroupId = ObservationGroupId::new(60); +const OTHER_GROUP: ObservationGroupId = ObservationGroupId::new(61); +const STREAM_A: ObservationStreamId = ObservationStreamId::new(70); +const STREAM_B: ObservationStreamId = ObservationStreamId::new(71); + +fn observation_group(surface_input_ports: Vec) -> ObservationGroup { + ObservationGroup::new(GROUP, surface_input_ports) +} + +const fn stream_binding( + group: ObservationGroupId, + stream: ObservationStreamId, +) -> ObservationStreamBinding { + ObservationStreamBinding::new(group, stream) +} + +const fn default_stream_binding() -> ObservationStreamBinding { + stream_binding(GROUP, STREAM_A) +} fn base_program( opacity: f64, @@ -37,13 +58,28 @@ fn base_program( outputs: Vec, evaluator: Evaluation, ) -> Program +where + Evaluation: PointEvaluatorV1, + PointInvocation: Copy, +{ + base_program_in_group(GROUP, opacity, against, constraints, outputs, evaluator) +} + +fn base_program_in_group( + group: ObservationGroupId, + opacity: f64, + against: SurfaceId, + constraints: ConstraintSet>, + outputs: Vec, + evaluator: Evaluation, +) -> Program where Evaluation: PointEvaluatorV1, PointInvocation: Copy, { Program::new( vec![ColorInput::new(COLOR, Srgb8::new([0; 3]))], - vec![SURFACE_PORT], + ObservationGroup::new(group, vec![SURFACE_PORT]), vec![OpacityInput::new(OPACITY, opacity)], vec![ Paint::Solid { @@ -102,6 +138,25 @@ fn compiled_exact(expected: Srgb8) -> CompiledProgram { exact_required(expected).compile().unwrap() } +fn compiled_exact_in_group( + group: ObservationGroupId, + expected: Srgb8, +) -> CompiledProgram { + base_program_in_group( + group, + 0.5, + BACKDROP, + ConstraintSet::new( + vec![ConstraintInvocation::hard(REQUIRED, OCCURRENCE, expected)], + vec![], + ), + vec![OutputBinding::new(OUTPUT, TRANSLUCENT)], + ExactSrgb8IdentityV1, + ) + .compile() + .unwrap() +} + fn wcag_program( hard: Vec>, report_only: Vec>, @@ -208,13 +263,32 @@ fn authored_modes_are_marker_typed_and_values_preserve_exact_ids() { let signal = SurfaceSignal::new(SURFACE_PORT, Srgb8::new([4, 5, 6])); assert_eq!(signal.input(), SURFACE_PORT); assert_eq!(signal.value(), Srgb8::new([4, 5, 6])); + + let group = observation_group(vec![SURFACE_PORT]); + assert_eq!(group.id(), GROUP); + assert_eq!(group.surface_input_ports(), &[SURFACE_PORT]); + let binding = default_stream_binding(); + assert_eq!(binding.group(), GROUP); + assert_eq!(binding.stream(), STREAM_A); + let signals = [signal]; + let update = SurfaceUpdate::present(STREAM_A, 17, &signals); + assert_eq!(update.stream(), STREAM_A); + assert_eq!(update.revision(), 17); + assert_eq!( + update.payload(), + SurfaceUpdatePayload::Present { surfaces: &signals } + ); + assert_eq!( + SurfaceUpdate::unavailable(STREAM_A, 18, 9).payload(), + SurfaceUpdatePayload::Unavailable { reason: 9 } + ); } #[test] fn empty_domains_have_stable_precedence() { let empty_surface = Program::new( vec![ColorInput::new(COLOR, Srgb8::new([0; 3]))], - vec![], + observation_group(vec![]), vec![], vec![Paint::Solid { id: SOLID, @@ -243,12 +317,12 @@ fn empty_domains_have_stable_precedence() { ); assert_eq!( compile_error(empty_surface), - ProgramCompileError::EmptySurfaceSchema + ProgramCompileError::EmptyObservationGroup { group: GROUP } ); let empty_occurrence = Program::new( vec![ColorInput::new(COLOR, Srgb8::new([0; 3]))], - vec![SURFACE_PORT], + observation_group(vec![SURFACE_PORT]), vec![], vec![Paint::Solid { id: SOLID, @@ -473,6 +547,7 @@ fn compile_canonicalizes_constraints_and_outputs_independent_of_mode_lists() { ) .compile() .unwrap(); + assert_eq!(compiled.observation_group_id(), GROUP); assert_eq!(compiled.surface_input_ports(), &[SURFACE_PORT]); assert_eq!( compiled.constraint_ids().collect::>(), @@ -483,6 +558,7 @@ fn compile_canonicalizes_constraints_and_outputs_independent_of_mode_lists() { vec![(output_low, TRANSLUCENT), (output_high, TRANSLUCENT)] ); let owner = compiled.into_owner(); + assert_eq!(owner.observation_group_id(), Some(GROUP)); assert_eq!( owner.constraint_ids().unwrap().collect::>(), vec![low, high] @@ -493,6 +569,184 @@ fn compile_canonicalizes_constraints_and_outputs_independent_of_mode_lists() { ); } +#[test] +fn attach_rejects_the_wrong_group_without_allocation_and_disposed_wins() { + let program = base_program( + 0.5, + BACKDROP, + ConstraintSet::new( + vec![ConstraintInvocation::hard( + REQUIRED, + OCCURRENCE, + ProgramTestInvocationV1::exact(Srgb8::new([0x80; 3])), + )], + vec![], + ), + vec![OutputBinding::new(OUTPUT, TRANSLUCENT)], + ProgramTestEvaluatorV1, + ); + let mut owner = program.compile().unwrap().into_owner(); + let wrong_binding = stream_binding(OTHER_GROUP, STREAM_A); + + reset_program_test_evaluation_count(); + let (result, allocations) = + crate::test_support::measured_allocations(|| owner.attach(wrong_binding)); + let Err(error) = result else { + panic!("a stream from another observation group must not attach"); + }; + assert_eq!( + error, + PointRenderAttachError::ObservationGroupMismatch { + expected: GROUP, + actual: OTHER_GROUP, + } + ); + assert_eq!(allocations, 0); + assert_eq!(program_test_evaluation_count(), 0); + + let retry = owner + .attach(default_stream_binding()) + .expect("a rejected foreign group must leave attach retryable"); + assert_eq!(retry.stream(), STREAM_A); + assert_eq!(program_test_evaluation_count(), 0); + drop(retry); + + owner.dispose(); + let (result, allocations) = + crate::test_support::measured_allocations(|| owner.attach(wrong_binding)); + let Err(error) = result else { + panic!("a disposed owner must not attach"); + }; + assert_eq!(error, PointRenderAttachError::Disposed); + assert_eq!(allocations, 0); +} + +#[test] +fn wrong_stream_precedes_shape_revision_callback_evaluation_and_state_mutation() { + let program = base_program( + 0.5, + BACKDROP, + ConstraintSet::new( + vec![ConstraintInvocation::hard( + REQUIRED, + OCCURRENCE, + ProgramTestInvocationV1::exact(Srgb8::new([0x80; 3])), + )], + vec![], + ), + vec![OutputBinding::new(OUTPUT, TRANSLUCENT)], + ProgramTestEvaluatorV1, + ); + let owner = program.compile().unwrap().into_owner(); + let mut session = owner.attach(default_stream_binding()).unwrap(); + session + .update_canonical_present(STREAM_A, 10, 1, |_| Srgb8::new([0xff; 3])) + .unwrap(); + let SessionState::Ready { current } = session.state() else { + unreachable!() + }; + let retained_storage = current.storage_pointers_for_test(); + let retained_output = current.output(OUTPUT); + + let wrong_present = SurfaceUpdate::present(STREAM_B, 9, &[]); + let wrong_unavailable = SurfaceUpdate::unavailable(STREAM_B, 9, 77); + for update in [wrong_present, wrong_unavailable] { + reset_program_test_evaluation_count(); + crate::composition::reset_source_over_evaluation_count(); + let (result, allocations) = + crate::test_support::measured_allocations(|| session.update(update).map(|_| ())); + assert_eq!( + result, + Err(SessionUpdateError::ObservationStreamMismatch { + expected: STREAM_A, + actual: STREAM_B, + }) + ); + assert_eq!(allocations, 0); + assert_eq!(program_test_evaluation_count(), 0); + assert_eq!(crate::composition::source_over_evaluation_count(), 0); + let SessionState::Ready { current } = session.state() else { + panic!("wrong-stream public update mutated the session state"); + }; + assert_eq!(current.revision(), 10); + assert_eq!(current.storage_pointers_for_test(), retained_storage); + assert_eq!(current.output(OUTPUT), retained_output); + } + + reset_program_test_evaluation_count(); + crate::composition::reset_source_over_evaluation_count(); + let reads = Cell::new(0); + let (result, allocations) = crate::test_support::measured_allocations(|| { + session + .update_canonical_present(STREAM_B, 9, 0, |_| { + reads.set(reads.get() + 1); + Srgb8::new([0; 3]) + }) + .map(|_| ()) + }); + assert_eq!( + result, + Err(SessionUpdateError::ObservationStreamMismatch { + expected: STREAM_A, + actual: STREAM_B, + }) + ); + assert_eq!(reads.get(), 0); + assert_eq!(allocations, 0); + assert_eq!(program_test_evaluation_count(), 0); + assert_eq!(crate::composition::source_over_evaluation_count(), 0); + let SessionState::Ready { current } = session.state() else { + panic!("wrong-stream canonical update mutated the session state"); + }; + assert_eq!(current.revision(), 10); + assert_eq!(current.storage_pointers_for_test(), retained_storage); + assert_eq!(current.output(OUTPUT), retained_output); +} + +#[test] +fn sessions_bound_to_distinct_streams_keep_independent_revision_watermarks() { + let owner = compiled_exact(Srgb8::new([0x80; 3])).into_owner(); + let mut session_a = owner.attach(stream_binding(GROUP, STREAM_A)).unwrap(); + let mut session_b = owner.attach(stream_binding(GROUP, STREAM_B)).unwrap(); + assert_eq!(session_a.stream(), STREAM_A); + assert_eq!(session_b.stream(), STREAM_B); + + for (session, stream, revision) in [ + (&mut session_a, STREAM_A, 100), + (&mut session_b, STREAM_B, 1), + ] { + let (result, allocations) = crate::test_support::measured_allocations(|| { + session + .update_canonical_present(stream, revision, 1, |_| Srgb8::new([0xff; 3])) + .map(|_| ()) + }); + assert!(result.is_ok()); + assert_eq!(allocations, 0); + } + + assert_eq!( + update_error( + session_a.update_canonical_present(STREAM_A, 99, 1, |_| Srgb8::new([0xff; 3])) + ), + SessionUpdateError::::RevisionOutOfOrder { + current: 100, + incoming: 99, + } + ); + session_b + .update_canonical_present(STREAM_B, 2, 1, |_| Srgb8::new([0xff; 3])) + .unwrap(); + + let SessionState::Ready { current: current_a } = session_a.state() else { + unreachable!() + }; + let SessionState::Ready { current: current_b } = session_b.state() else { + unreachable!() + }; + assert_eq!(current_a.revision(), 100); + assert_eq!(current_b.revision(), 2); +} + #[test] fn wcag_report_only_uses_visible_808080_but_emits_black_half_alpha_paint() { let criterion = Wcag22CriterionV1::Sc143TextDefault; @@ -503,10 +757,10 @@ fn wcag_report_only_uses_visible_808080_but_emits_black_half_alpha_paint() { )], ); let owner = program.compile().unwrap().into_owner(); - let mut session = owner.attach().unwrap(); + let mut session = owner.attach(default_stream_binding()).unwrap(); let (result, allocations) = crate::test_support::measured_allocations(|| { session - .update_canonical_present(1, 1, |_| Srgb8::new([0xff; 3])) + .update_canonical_present(STREAM_A, 1, 1, |_| Srgb8::new([0xff; 3])) .map(|_| ()) }); assert!(result.is_ok()); @@ -555,9 +809,9 @@ fn wcag_hard_violation_commits_conflict_with_full_report_and_no_current_output() vec![], ); let owner = program.compile().unwrap().into_owner(); - let mut session = owner.attach().unwrap(); + let mut session = owner.attach(default_stream_binding()).unwrap(); let SessionState::Conflict { current, previous } = session - .update_canonical_present(1, 1, |_| Srgb8::new([0xff; 3])) + .update_canonical_present(STREAM_A, 1, 1, |_| Srgb8::new([0xff; 3])) .unwrap() else { panic!("mandatory WCAG violation must commit Conflict"); @@ -597,9 +851,9 @@ fn all_constraints_run_before_hard_gate_and_report_order_is_canonical() { )], ); let owner = program.compile().unwrap().into_owner(); - let mut session = owner.attach().unwrap(); + let mut session = owner.attach(default_stream_binding()).unwrap(); let SessionState::Conflict { current, .. } = session - .update_canonical_present(1, 1, |_| Srgb8::new([0xff; 3])) + .update_canonical_present(STREAM_A, 1, 1, |_| Srgb8::new([0xff; 3])) .unwrap() else { panic!("one hard violation must gate the complete result"); @@ -640,9 +894,9 @@ fn report_only_violation_never_gates_terminal_paint() { )], ); let owner = program.compile().unwrap().into_owner(); - let mut session = owner.attach().unwrap(); + let mut session = owner.attach(default_stream_binding()).unwrap(); let SessionState::Ready { current } = session - .update_canonical_present(1, 1, |_| Srgb8::new([0xff; 3])) + .update_canonical_present(STREAM_A, 1, 1, |_| Srgb8::new([0xff; 3])) .unwrap() else { panic!("report-only violation cannot gate"); @@ -673,9 +927,9 @@ fn output_slot_renaming_changes_routing_not_physical_paint() { ExactSrgb8IdentityV1, ); let owner = program.compile().unwrap().into_owner(); - let mut session = owner.attach().unwrap(); + let mut session = owner.attach(default_stream_binding()).unwrap(); let SessionState::Ready { current } = session - .update_canonical_present(1, 1, |_| Srgb8::new([0xff; 3])) + .update_canonical_present(STREAM_A, 1, 1, |_| Srgb8::new([0xff; 3])) .unwrap() else { unreachable!() @@ -692,6 +946,58 @@ fn output_slot_renaming_changes_routing_not_physical_paint() { assert_eq!(left.value().straight_alpha_bits(), 0.5f64.to_bits()); } +#[test] +fn point_transport_values_and_classifier_payload_are_nominal_id_invariant_before_f2_binding() { + type TerminalPaintProjection = (PaintId, Srgb8, u64); + type ClassifierPayloadProjection = (ConstraintId, OccurrenceId, Srgb8, Srgb8); + + fn resolve( + group: ObservationGroupId, + stream: ObservationStreamId, + ) -> (TerminalPaintProjection, ClassifierPayloadProjection) { + let program = base_program_in_group( + group, + 0.5, + BACKDROP, + ConstraintSet::new( + vec![ConstraintInvocation::hard( + REQUIRED, + OCCURRENCE, + Srgb8::new([0x80; 3]), + )], + vec![], + ), + vec![OutputBinding::new(OUTPUT, TRANSLUCENT)], + ExactSrgb8IdentityV1, + ); + let owner = program.compile().unwrap().into_owner(); + let mut session = owner.attach(stream_binding(group, stream)).unwrap(); + let SessionState::Ready { current } = session + .update_canonical_present(stream, 1, 1, |_| Srgb8::new([0xff; 3])) + .unwrap() + else { + unreachable!() + }; + let output = current.output(OUTPUT).unwrap(); + let Some(ConstraintReportEntry::Hard(assessment)) = current.report().next() else { + unreachable!() + }; + let (target, actual) = exact_outcome(assessment); + ( + ( + output.paint(), + output.value().source(), + output.value().straight_alpha_bits(), + ), + (assessment.constraint(), assessment.target(), target, actual), + ) + } + + let original = resolve(GROUP, STREAM_A); + let renamed = resolve(OTHER_GROUP, STREAM_B); + assert_eq!(original, renamed); +} + #[test] fn evaluator_error_preserves_prior_snapshot_head_and_all_owned_evidence() { reset_program_test_evaluation_count(); @@ -716,9 +1022,9 @@ fn evaluator_error_preserves_prior_snapshot_head_and_all_owned_evidence() { ProgramTestEvaluatorV1, ); let owner = program.compile().unwrap().into_owner(); - let mut session = owner.attach().unwrap(); + let mut session = owner.attach(default_stream_binding()).unwrap(); session - .update_canonical_present(1, 1, |_| Srgb8::new([0xff; 3])) + .update_canonical_present(STREAM_A, 1, 1, |_| Srgb8::new([0xff; 3])) .unwrap(); let SessionState::Ready { current } = session.state() else { panic!("control update must be Ready"); @@ -731,7 +1037,8 @@ fn evaluator_error_preserves_prior_snapshot_head_and_all_owned_evidence() { .collect::>(); arm_program_test_failure_once(); - let error = update_error(session.update_canonical_present(2, 1, |_| Srgb8::new([0xff; 3]))); + let error = + update_error(session.update_canonical_present(STREAM_A, 2, 1, |_| Srgb8::new([0xff; 3]))); assert_eq!( error, SessionUpdateError::Evaluator { @@ -754,7 +1061,7 @@ fn evaluator_error_preserves_prior_snapshot_head_and_all_owned_evidence() { ); let SessionState::Ready { current } = session - .update_canonical_present(2, 1, |_| Srgb8::new([0xff; 3])) + .update_canonical_present(STREAM_A, 2, 1, |_| Srgb8::new([0xff; 3])) .unwrap() else { panic!("same incoming revision must be retryable after evaluator Err"); @@ -762,7 +1069,7 @@ fn evaluator_error_preserves_prior_snapshot_head_and_all_owned_evidence() { assert_eq!(current.revision(), 2); session - .update_canonical_present(3, 1, |_| Srgb8::new([0; 3])) + .update_canonical_present(STREAM_A, 3, 1, |_| Srgb8::new([0; 3])) .unwrap(); let SessionState::Conflict { current, previous } = session.state() else { panic!("black backdrop must create the retained Conflict control"); @@ -774,7 +1081,8 @@ fn evaluator_error_preserves_prior_snapshot_head_and_all_owned_evidence() { .storage_pointers_for_test(); arm_program_test_failure_once(); - let error = update_error(session.update_canonical_present(4, 1, |_| Srgb8::new([0; 3]))); + let error = + update_error(session.update_canonical_present(STREAM_A, 4, 1, |_| Srgb8::new([0; 3]))); assert_eq!( error, SessionUpdateError::Evaluator { @@ -793,7 +1101,7 @@ fn evaluator_error_preserves_prior_snapshot_head_and_all_owned_evidence() { ); let SessionState::Conflict { current, previous } = session - .update_canonical_present(4, 1, |_| Srgb8::new([0; 3])) + .update_canonical_present(STREAM_A, 4, 1, |_| Srgb8::new([0; 3])) .unwrap() else { panic!("retry after evaluator Err must execute and commit Conflict"); @@ -809,9 +1117,9 @@ fn evaluator_error_preserves_prior_snapshot_head_and_all_owned_evidence() { #[test] fn three_frames_preserve_verified_and_replace_complete_conflict_reports_without_clone() { let owner = compiled_exact(Srgb8::new([0x80; 3])).into_owner(); - let mut session = owner.attach().unwrap(); + let mut session = owner.attach(default_stream_binding()).unwrap(); session - .update_canonical_present(1, 1, |_| Srgb8::new([0xff; 3])) + .update_canonical_present(STREAM_A, 1, 1, |_| Srgb8::new([0xff; 3])) .unwrap(); let SessionState::Ready { current } = session.state() else { unreachable!() @@ -819,7 +1127,7 @@ fn three_frames_preserve_verified_and_replace_complete_conflict_reports_without_ let verified_storage = current.storage_pointers_for_test(); session - .update_canonical_present(2, 1, |_| Srgb8::new([0; 3])) + .update_canonical_present(STREAM_A, 2, 1, |_| Srgb8::new([0; 3])) .unwrap(); let SessionState::Conflict { current, previous } = session.state() else { panic!("black backdrop must violate exact #808080"); @@ -835,7 +1143,7 @@ fn three_frames_preserve_verified_and_replace_complete_conflict_reports_without_ ); session - .update_canonical_present(3, 1, |_| Srgb8::new([0x20; 3])) + .update_canonical_present(STREAM_A, 3, 1, |_| Srgb8::new([0x20; 3])) .unwrap(); let SessionState::Conflict { current, previous } = session.state() else { unreachable!() @@ -853,7 +1161,7 @@ fn three_frames_preserve_verified_and_replace_complete_conflict_reports_without_ assert_eq!(current.report().count(), 1); session - .update_canonical_present(4, 1, |_| Srgb8::new([0xff; 3])) + .update_canonical_present(STREAM_A, 4, 1, |_| Srgb8::new([0xff; 3])) .unwrap(); let SessionState::Ready { current } = session.state() else { panic!("Conflict must recover directly to a new verified Snapshot"); @@ -863,7 +1171,7 @@ fn three_frames_preserve_verified_and_replace_complete_conflict_reports_without_ assert_ne!(recovered_storage, conflict_three_storage); session - .update_canonical_present(5, 1, |_| Srgb8::new([0; 3])) + .update_canonical_present(STREAM_A, 5, 1, |_| Srgb8::new([0; 3])) .unwrap(); let SessionState::Conflict { previous, .. } = session.state() else { unreachable!() @@ -877,10 +1185,7 @@ fn three_frames_preserve_verified_and_replace_complete_conflict_reports_without_ previous, current_unavailable, } = session - .update(SurfaceUpdate::Unavailable { - revision: 6, - reason: 9, - }) + .update(SurfaceUpdate::unavailable(STREAM_A, 6, 9)) .unwrap() else { panic!("Unknown after Conflict(previous) must retain that full Snapshot"); @@ -893,23 +1198,30 @@ fn three_frames_preserve_verified_and_replace_complete_conflict_reports_without_ #[test] fn same_revision_conflict_replay_is_idempotent_and_changed_payload_conflicts() { let owner = compiled_exact(Srgb8::new([0x80; 3])).into_owner(); - let mut session = owner.attach().unwrap(); + let mut session = owner.attach(default_stream_binding()).unwrap(); let black = Srgb8::new([0; 3]); - session.update_canonical_present(1, 1, |_| black).unwrap(); + session + .update_canonical_present(STREAM_A, 1, 1, |_| black) + .unwrap(); let SessionState::Conflict { current, .. } = session.state() else { unreachable!() }; let storage = current.storage_pointers_for_test(); crate::composition::reset_source_over_evaluation_count(); - assert!(session.update_canonical_present(1, 1, |_| black).is_ok()); + assert!( + session + .update_canonical_present(STREAM_A, 1, 1, |_| black) + .is_ok() + ); assert_eq!(crate::composition::source_over_evaluation_count(), 0); let SessionState::Conflict { current, .. } = session.state() else { unreachable!() }; assert_eq!(current.storage_pointers_for_test(), storage); - let error = update_error(session.update_canonical_present(1, 1, |_| Srgb8::new([0xff; 3]))); + let error = + update_error(session.update_canonical_present(STREAM_A, 1, 1, |_| Srgb8::new([0xff; 3]))); assert_eq!( error, SessionUpdateError::::RevisionConflict { revision: 1 } @@ -927,7 +1239,7 @@ fn three_port_same_revision_conflict_reads_every_value_without_execution_or_allo const SURFACE_C: SurfaceId = SurfaceId::new(130); let program = Program::new( vec![ColorInput::new(COLOR, Srgb8::new([0; 3]))], - vec![PORT_C, PORT_A, PORT_B], + observation_group(vec![PORT_C, PORT_A, PORT_B]), vec![OpacityInput::new(OPACITY, 0.5)], vec![ Paint::Solid { @@ -974,14 +1286,14 @@ fn three_port_same_revision_conflict_reads_every_value_without_execution_or_allo let compiled = program.compile().unwrap(); assert_eq!(compiled.surface_input_ports(), &[PORT_A, PORT_B, PORT_C]); let owner = compiled.into_owner(); - let mut session = owner.attach().unwrap(); + let mut session = owner.attach(default_stream_binding()).unwrap(); let committed = [ Srgb8::new([0x10; 3]), Srgb8::new([0; 3]), Srgb8::new([0x30; 3]), ]; session - .update_canonical_present(5, 3, |index| committed[index]) + .update_canonical_present(STREAM_A, 5, 3, |index| committed[index]) .unwrap(); let SessionState::Conflict { current, .. } = session.state() else { unreachable!() @@ -992,7 +1304,7 @@ fn three_port_same_revision_conflict_reads_every_value_without_execution_or_allo let replay = ReadProbe::new(committed); let (result, allocations) = crate::test_support::measured_allocations(|| { session - .update_canonical_present(5, 3, |index| replay.read(index)) + .update_canonical_present(STREAM_A, 5, 3, |index| replay.read(index)) .map(|_| ()) }); assert!(result.is_ok()); @@ -1009,7 +1321,7 @@ fn three_port_same_revision_conflict_reads_every_value_without_execution_or_allo let mismatch = ReadProbe::new(mismatched); let (result, allocations) = crate::test_support::measured_allocations(|| { session - .update_canonical_present(5, 3, |index| mismatch.read(index)) + .update_canonical_present(STREAM_A, 5, 3, |index| mismatch.read(index)) .map(|_| ()) }); assert_eq!( @@ -1026,10 +1338,7 @@ fn three_port_same_revision_conflict_reads_every_value_without_execution_or_allo let (result, allocations) = crate::test_support::measured_allocations(|| { session - .update(SurfaceUpdate::Unavailable { - revision: 5, - reason: 91, - }) + .update(SurfaceUpdate::unavailable(STREAM_A, 5, 91)) .map(|_| ()) }); assert_eq!( @@ -1046,7 +1355,7 @@ fn three_port_same_revision_conflict_reads_every_value_without_execution_or_allo #[test] fn typed_schema_revision_and_lifetime_failures_are_atomic() { let mut owner = compiled_exact(Srgb8::new([0x80; 3])).into_owner(); - let mut session = owner.attach().unwrap(); + let mut session = owner.attach(default_stream_binding()).unwrap(); let wrong = [SurfaceSignal::new( SurfaceInputPortId::new(999), Srgb8::new([0xff; 3]), @@ -1054,10 +1363,7 @@ fn typed_schema_revision_and_lifetime_failures_are_atomic() { crate::composition::reset_source_over_evaluation_count(); let (result, allocations) = crate::test_support::measured_allocations(|| { session - .update(SurfaceUpdate::Present { - revision: 1, - surfaces: &wrong, - }) + .update(SurfaceUpdate::present(STREAM_A, 1, &wrong)) .map(|_| ()) }); assert_eq!( @@ -1078,7 +1384,7 @@ fn typed_schema_revision_and_lifetime_failures_are_atomic() { )); session - .update_canonical_present(5, 1, |_| Srgb8::new([0xff; 3])) + .update_canonical_present(STREAM_A, 5, 1, |_| Srgb8::new([0xff; 3])) .unwrap(); let SessionState::Ready { current } = session.state() else { unreachable!() @@ -1088,7 +1394,7 @@ fn typed_schema_revision_and_lifetime_failures_are_atomic() { crate::composition::reset_source_over_evaluation_count(); let (result, allocations) = crate::test_support::measured_allocations(|| { session - .update_canonical_present(4, 1, |_| { + .update_canonical_present(STREAM_A, 4, 1, |_| { reads.set(reads.get() + 1); Srgb8::new([0; 3]) }) @@ -1113,10 +1419,7 @@ fn typed_schema_revision_and_lifetime_failures_are_atomic() { owner.dispose(); let (result, allocations) = crate::test_support::measured_allocations(|| { session - .update(SurfaceUpdate::Unavailable { - revision: 6, - reason: 1, - }) + .update(SurfaceUpdate::unavailable(STREAM_A, 6, 1)) .map(|_| ()) }); assert_eq!( @@ -1129,7 +1432,7 @@ fn typed_schema_revision_and_lifetime_failures_are_atomic() { #[test] fn replacement_revokes_old_sessions_and_compile_error_keeps_live_epoch() { let mut owner = compiled_exact(Srgb8::new([0x80; 3])).into_owner(); - let mut old = owner.attach().unwrap(); + let mut old = owner.attach(default_stream_binding()).unwrap(); assert_eq!( compile_error(base_program( 1.25, @@ -1148,41 +1451,61 @@ fn replacement_revokes_old_sessions_and_compile_error_keeps_live_epoch() { ProgramCompileError::OpacityOutOfDomain { input: OPACITY } ); assert!( - old.update_canonical_present(1, 1, |_| Srgb8::new([0xff; 3])) + old.update_canonical_present(STREAM_A, 1, 1, |_| Srgb8::new([0xff; 3])) .is_ok() ); - owner.replace(compiled_exact(Srgb8::new([0x80; 3]))); + owner.replace(compiled_exact_in_group(OTHER_GROUP, Srgb8::new([0x80; 3]))); + assert_eq!(owner.observation_group_id(), Some(OTHER_GROUP)); assert_eq!( - update_error(old.update_canonical_present(2, 1, |_| Srgb8::new([0xff; 3]))), + update_error(old.update_canonical_present(STREAM_A, 2, 1, |_| Srgb8::new([0xff; 3]))), SessionUpdateError::::ProgramExpired ); + + let (result, allocations) = + crate::test_support::measured_allocations(|| owner.attach(default_stream_binding())); + let Err(error) = result else { + panic!("replacement must reject a binding for the previous group"); + }; + assert_eq!( + error, + PointRenderAttachError::ObservationGroupMismatch { + expected: OTHER_GROUP, + actual: GROUP, + } + ); + assert_eq!(allocations, 0); + + let mut fresh = owner.attach(stream_binding(OTHER_GROUP, STREAM_B)).unwrap(); + fresh + .update_canonical_present(STREAM_B, 1, 1, |_| Srgb8::new([0xff; 3])) + .unwrap(); + owner.dispose(); + let (result, allocations) = crate::test_support::measured_allocations(|| { + fresh + .update(SurfaceUpdate::unavailable(STREAM_A, 2, 1)) + .map(|_| ()) + }); + assert_eq!( + result, + Err(SessionUpdateError::::ProgramExpired), + "expiry must precede even a mismatched stream" + ); + assert_eq!(allocations, 0); } #[test] fn present_ready_conflict_stale_and_recovery_allocate_zero_after_attach() { let owner = compiled_exact(Srgb8::new([0x80; 3])).into_owner(); - let mut session = owner.attach().unwrap(); + let mut session = owner.attach(default_stream_binding()).unwrap(); let white = [SurfaceSignal::new(SURFACE_PORT, Srgb8::new([0xff; 3]))]; let black = [SurfaceSignal::new(SURFACE_PORT, Srgb8::new([0; 3]))]; for update in [ - SurfaceUpdate::Present { - revision: 1, - surfaces: &white, - }, - SurfaceUpdate::Present { - revision: 2, - surfaces: &black, - }, - SurfaceUpdate::Unavailable { - revision: 3, - reason: 7, - }, - SurfaceUpdate::Present { - revision: 4, - surfaces: &white, - }, + SurfaceUpdate::present(STREAM_A, 1, &white), + SurfaceUpdate::present(STREAM_A, 2, &black), + SurfaceUpdate::unavailable(STREAM_A, 3, 7), + SurfaceUpdate::present(STREAM_A, 4, &white), ] { let (result, allocations) = crate::test_support::measured_allocations(|| session.update(update).map(|_| ())); @@ -1217,12 +1540,12 @@ fn canonical_helpers_and_checked_cardinality_fail_closed() { fn callback_is_not_read_before_lifetime_cardinality_or_revision_admission() { let mut expired = { let owner = compiled_exact(Srgb8::new([0x80; 3])).into_owner(); - owner.attach().unwrap() + owner.attach(default_stream_binding()).unwrap() }; let reads = Cell::new(0); let (result, allocations) = crate::test_support::measured_allocations(|| { expired - .update_canonical_present(1, 1, |_| { + .update_canonical_present(STREAM_B, 1, 1, |_| { reads.set(reads.get() + 1); Srgb8::new([0xff; 3]) }) @@ -1236,12 +1559,12 @@ fn callback_is_not_read_before_lifetime_cardinality_or_revision_admission() { assert_eq!(allocations, 0); let owner = compiled_exact(Srgb8::new([0x80; 3])).into_owner(); - let mut session = owner.attach().unwrap(); + let mut session = owner.attach(default_stream_binding()).unwrap(); for actual in [0, 2] { let reads = Cell::new(0); let (result, allocations) = crate::test_support::measured_allocations(|| { session - .update_canonical_present(1, actual, |_| { + .update_canonical_present(STREAM_A, 1, actual, |_| { reads.set(reads.get() + 1); Srgb8::new([0xff; 3]) }) @@ -1261,15 +1584,12 @@ fn callback_is_not_read_before_lifetime_cardinality_or_revision_admission() { } session - .update(SurfaceUpdate::Unavailable { - revision: 7, - reason: 12, - }) + .update(SurfaceUpdate::unavailable(STREAM_A, 7, 12)) .unwrap(); let reads = Cell::new(0); let (result, allocations) = crate::test_support::measured_allocations(|| { session - .update_canonical_present(7, 1, |_| { + .update_canonical_present(STREAM_A, 7, 1, |_| { reads.set(reads.get() + 1); Srgb8::new([0xff; 3]) }) @@ -1284,13 +1604,16 @@ fn callback_is_not_read_before_lifetime_cardinality_or_revision_admission() { } #[test] -fn same_paint_can_be_assessed_in_multiple_occurrences_but_is_one_terminal_output() { +fn canonical_two_port_group_assesses_multiple_occurrences_but_emits_one_paint() { const OTHER_PORT: SurfaceInputPortId = SurfaceInputPortId::new(4); const OTHER_SURFACE: SurfaceId = SurfaceId::new(22); const OTHER_OCCURRENCE: OccurrenceId = OccurrenceId::new(31); + let group = ObservationGroup::new(GROUP, vec![OTHER_PORT, SURFACE_PORT]); + assert_eq!(group.id(), GROUP); + assert_eq!(group.surface_input_ports(), &[OTHER_PORT, SURFACE_PORT]); let program = Program::new( vec![ColorInput::new(COLOR, Srgb8::new([0; 3]))], - vec![OTHER_PORT, SURFACE_PORT], + group, vec![OpacityInput::new(OPACITY, 0.5)], vec![ Paint::Solid { @@ -1342,21 +1665,23 @@ fn same_paint_can_be_assessed_in_multiple_occurrences_but_is_one_terminal_output vec![OutputBinding::new(OUTPUT, TRANSLUCENT)], ExactSrgb8IdentityV1, ); - let owner = program.compile().unwrap().into_owner(); + let compiled = program.compile().unwrap(); + assert_eq!(compiled.observation_group_id(), GROUP); + assert_eq!(compiled.surface_input_ports(), &[SURFACE_PORT, OTHER_PORT]); + let owner = compiled.into_owner(); + assert_eq!(owner.observation_group_id(), Some(GROUP)); assert_eq!( owner.surface_input_ports(), Some(&[SURFACE_PORT, OTHER_PORT][..]) ); - let mut session = owner.attach().unwrap(); + let mut session = owner.attach(default_stream_binding()).unwrap(); + assert_eq!(session.stream(), STREAM_A); let signals = [ SurfaceSignal::new(SURFACE_PORT, Srgb8::new([0xff; 3])), SurfaceSignal::new(OTHER_PORT, Srgb8::new([0; 3])), ]; let SessionState::Ready { current } = session - .update(SurfaceUpdate::Present { - revision: 1, - surfaces: &signals, - }) + .update(SurfaceUpdate::present(STREAM_A, 1, &signals)) .unwrap() else { panic!("both exact occurrence contracts should pass"); diff --git a/scripts/verify_point_support_surplus.py b/scripts/verify_point_support_surplus.py index 84c7a663..dd1b6ffc 100644 --- a/scripts/verify_point_support_surplus.py +++ b/scripts/verify_point_support_surplus.py @@ -57,7 +57,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 = ( - "d95e75277933ecf04f72cef57d287a1c4c4ad373445552af110fae17d730d592" + "51b242a0ff319f34357fbeef89037118515172dcdc088a09b9ff28e0cc38e860" ) EXPECTED_Q55_PROOF_SHA256 = ( "ac59cf89503170c789223b91d775213a19d4e571ef930f2ea609fcd51b14defd" From 6be73afd6bdfcb184380eab173f3c5d2dd34f8b4 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Wed, 22 Jul 2026 16:58:24 +0300 Subject: [PATCH 24/58] refactor(core): share one encoded point Paint value --- .../src/generic_boundary_tests.rs | 111 ++++++++++++++++++ crates/labcolors-core/src/program_session.rs | 51 +++----- .../src/program_session_tests.rs | 95 +++++++++++++-- 3 files changed, 211 insertions(+), 46 deletions(-) diff --git a/crates/labcolors-core/src/generic_boundary_tests.rs b/crates/labcolors-core/src/generic_boundary_tests.rs index 77bec8d8..a265a5ca 100644 --- a/crates/labcolors-core/src/generic_boundary_tests.rs +++ b/crates/labcolors-core/src/generic_boundary_tests.rs @@ -1,6 +1,7 @@ const APPEARANCE_SOURCE: &str = include_str!("appearance.rs"); const LCS_OCCURRENCE_SOURCE: &str = include_str!("lcs_occurrence.rs"); const PROGRAM_SESSION_SOURCE: &str = include_str!("program_session.rs"); +const LIB_SOURCE: &str = include_str!("lib.rs"); const GENERIC_SOURCES: [(&str, &str); 3] = [ ("appearance.rs", APPEARANCE_SOURCE), @@ -65,6 +66,116 @@ fn program_session_module_docs_disclaim_transport_only_scope() { } } +#[test] +fn pre_f2_outputs_reuse_the_shared_encoded_point_paint_without_a_parallel_value() { + fn declaration<'a>(source: &'a str, start: &str, end: &str) -> &'a str { + let start = source + .find(start) + .unwrap_or_else(|| panic!("missing S0 declaration start `{start}`")); + let end = source[start..] + .find(end) + .map(|offset| start + offset) + .unwrap_or_else(|| panic!("missing S0 declaration end `{end}`")); + &source[start..end] + } + + assert!( + LIB_SOURCE.contains("pub(crate) mod program_session;"), + "the pre-F2 execution slice must remain crate-private", + ); + assert!( + !PROGRAM_SESSION_SOURCE.contains("OutputPaintV1"), + "S0 allows only appearance::EncodedPointPaintV1 as the physical point Paint value", + ); + + let output_value = declaration( + PROGRAM_SESSION_SOURCE, + "pub struct OutputValueV1 {", + "impl OutputValueV1 {", + ); + let fields_start = output_value + .find('{') + .expect("OutputValueV1 declaration must open its field list"); + let fields_end = output_value + .rfind('}') + .expect("OutputValueV1 declaration must close its field list"); + let fields = output_value[fields_start + 1..fields_end] + .lines() + .map(str::trim) + .filter(|line| !line.is_empty()) + .collect::>(); + assert_eq!( + fields, + ["output: OutputSlotId,", "value: EncodedPointPaintV1,"], + "OutputValueV1 must own exactly one route and the shared S0 Paint", + ); + assert!( + !output_value.contains("PaintId"), + "OutputValueV1 must not store any PaintId beside the same ID in EncodedPointPaintV1", + ); + + let output_value_impl = declaration( + PROGRAM_SESSION_SOURCE, + "impl OutputValueV1 {", + "struct ExecutionFrame", + ); + for required in [ + "self.value.id()", + "pub const fn value(self) -> EncodedPointPaintV1", + ] { + assert!( + output_value_impl.contains(required), + "OutputValueV1 must project directly from its shared S0 Paint; missing `{required}`", + ); + } + + let materialization = declaration( + PROGRAM_SESSION_SOURCE, + "for (index, output) in epoch.outputs.iter().enumerate() {", + "if has_hard_violation", + ); + let nominal_guard = materialization + .find("if paint.id() != output.paint_id {") + .expect("routed output must fail closed on compiled/materialized Paint ID drift"); + let exact_copy = materialization + .find("value: *paint,") + .expect("routed output must copy the exact graph-materialized Paint"); + assert!( + nominal_guard < exact_copy, + "nominal Paint ID drift must be rejected before the graph Paint is routed", + ); + assert!( + materialization[nominal_guard..exact_copy] + .contains("return Err(SessionUpdateError::InternalInvariant);"), + "compiled/materialized Paint ID drift must fail closed", + ); + assert!( + materialization[exact_copy..].contains("value: *paint,"), + "the routed value must copy the exact graph-materialized EncodedPointPaintV1", + ); + for forbidden in ["source: paint.source()", "straight_alpha: paint.opacity()"] { + assert!( + !materialization.contains(forbidden), + "graph materialization must not be reconstructed through `{forbidden}`", + ); + } + + let module_docs = PROGRAM_SESSION_SOURCE + .lines() + .take_while(|line| line.starts_with("//!")) + .collect::>() + .join("\n"); + for required in [ + "private pre-F2", + "does not mint a terminal output certificate", + ] { + assert!( + module_docs.contains(required), + "the routed pre-F2 value must not claim a public terminal certificate; missing `{required}`", + ); + } +} + #[test] fn current_og0_program_epoch_has_one_explicit_group_without_scenario_scope_creep() { fn declaration<'a>(source: &'a str, start: &str, end: &str) -> &'a str { diff --git a/crates/labcolors-core/src/program_session.rs b/crates/labcolors-core/src/program_session.rs index c5eb7290..e099e5b9 100644 --- a/crates/labcolors-core/src/program_session.rs +++ b/crates/labcolors-core/src/program_session.rs @@ -8,6 +8,9 @@ //! //! This is the encoded-sRGB8 transport-only executable slice, not the LCS //! observation/evidence layer. +//! This is a private pre-F2 execution slice. It materializes routed Paint +//! values but does not mint a terminal output certificate or define a public +//! transport contract. use std::marker::PhantomData; use std::mem; @@ -17,10 +20,10 @@ use crate::Srgb8; use crate::appearance::{ AdmittedAppearanceBindings, AppearanceBindings, AppearanceGraphSpec, AppearanceWorkspace, BindingError, ColorInputId, CompileError, CompiledAppearanceGraph, CompiledOccurrenceSlotV1, - CompiledPaintSlotV1, OccurrenceId, OccurrenceSpec, OpacityInputId, PaintId, PaintSpec, - SurfaceId, SurfaceInputPortId, SurfaceSpec, + CompiledPaintSlotV1, EncodedPointPaintV1, OccurrenceId, OccurrenceSpec, OpacityInputId, + PaintId, PaintSpec, SurfaceId, SurfaceInputPortId, SurfaceSpec, }; -use crate::composition::{AdmittedOpacityV1, CompositionProfileV1}; +use crate::composition::CompositionProfileV1; use crate::constraints::{ HardDecision, PointEvaluationError, PointEvaluatorV1, PointInvocation, VisiblePointPassEvidence, VisiblePointViolationEvidence, assess_visible_point_hard, @@ -1166,34 +1169,12 @@ where } } -/// Pure terminal Paint value. Routing identities are intentionally outside it. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct OutputPaintV1 { - source: Srgb8, - straight_alpha: AdmittedOpacityV1, -} - -impl OutputPaintV1 { - pub const fn source(self) -> Srgb8 { - self.source - } - - pub const fn straight_alpha(self) -> f64 { - self.straight_alpha.value() - } - - pub const fn straight_alpha_bits(self) -> u64 { - self.straight_alpha.bits() - } -} - -/// One routed terminal cell: opaque client slot, authored Paint identity and -/// the independent physical Paint value produced for it. +/// One routed private output cell: an opaque output slot and the exact +/// materialized Paint produced for it. The Paint owns its sole authored identity. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct OutputValueV1 { output: OutputSlotId, - paint: PaintId, - value: OutputPaintV1, + value: EncodedPointPaintV1, } impl OutputValueV1 { @@ -1202,10 +1183,10 @@ impl OutputValueV1 { } pub const fn paint(self) -> PaintId { - self.paint + self.value.id() } - pub const fn value(self) -> OutputPaintV1 { + pub const fn value(self) -> EncodedPointPaintV1 { self.value } } @@ -1711,13 +1692,13 @@ where put_free_frame(&mut self.free_frames, frame); return Err(SessionUpdateError::InternalInvariant); }; + if paint.id() != output.paint_id { + put_free_frame(&mut self.free_frames, frame); + return Err(SessionUpdateError::InternalInvariant); + } frame.outputs[index] = Some(OutputValueV1 { output: output.output, - paint: output.paint_id, - value: OutputPaintV1 { - source: paint.source(), - straight_alpha: paint.opacity(), - }, + value: *paint, }); } } diff --git a/crates/labcolors-core/src/program_session_tests.rs b/crates/labcolors-core/src/program_session_tests.rs index b7907486..3ef5852a 100644 --- a/crates/labcolors-core/src/program_session_tests.rs +++ b/crates/labcolors-core/src/program_session_tests.rs @@ -3,7 +3,8 @@ use std::convert::Infallible; use crate::Srgb8; use crate::appearance::{ - ColorInputId, OccurrenceId, OpacityInputId, PaintId, SurfaceId, SurfaceInputPortId, + ColorInputId, EncodedPointPaintV1, OccurrenceId, OpacityInputId, PaintId, SurfaceId, + SurfaceInputPortId, }; use crate::constraints::{ ExactSrgb8IdentityV1, PointEvaluatorV1, PointInvocation, ProgramTestEvaluationErrorV1, @@ -778,8 +779,8 @@ fn wcag_report_only_uses_visible_808080_but_emits_black_half_alpha_paint() { assert_eq!(output.output(), OUTPUT); assert_eq!(output.paint(), TRANSLUCENT); assert_eq!(output.value().source(), Srgb8::new([0; 3])); - assert_eq!(output.value().straight_alpha(), 0.5); - assert_eq!(output.value().straight_alpha_bits(), 0.5f64.to_bits()); + assert_eq!(output.value().opacity().value(), 0.5); + assert_eq!(output.value().opacity().bits(), 0.5f64.to_bits()); let mut report = current.report(); let Some(ConstraintReportEntry::ReportOnly(assessment)) = report.next() else { @@ -880,7 +881,7 @@ fn all_constraints_run_before_hard_gate_and_report_order_is_canonical() { } #[test] -fn report_only_violation_never_gates_terminal_paint() { +fn report_only_violation_never_gates_the_routed_encoded_paint() { let program = exact_program( vec![ConstraintInvocation::hard( REQUIRED, @@ -910,7 +911,7 @@ fn report_only_violation_never_gates_terminal_paint() { } #[test] -fn output_slot_renaming_changes_routing_not_physical_paint() { +fn output_slot_renaming_changes_only_route_and_reuses_the_exact_encoded_paint() { fn resolve(output: OutputSlotId) -> crate::program_session::OutputValueV1 { let program = base_program( 0.5, @@ -942,19 +943,91 @@ fn output_slot_renaming_changes_routing_not_physical_paint() { assert_ne!(left.output(), renamed.output()); assert_eq!(left.paint(), renamed.paint()); assert_eq!(left.value(), renamed.value()); - assert_eq!(left.value().source(), Srgb8::new([0; 3])); - assert_eq!(left.value().straight_alpha_bits(), 0.5f64.to_bits()); + assert_eq!(left.paint(), left.value().id()); + assert_eq!(renamed.paint(), renamed.value().id()); + assert_eq!( + left.value(), + EncodedPointPaintV1::from_admitted( + TRANSLUCENT, + Srgb8::new([0; 3]), + crate::composition::AdmittedOpacityV1::new(0.5).unwrap(), + ) + ); + assert_eq!(left.value().opacity().bits(), 0.5f64.to_bits()); +} + +#[test] +fn consistent_paint_id_renaming_changes_only_nominal_identity() { + fn resolve(solid: PaintId, translucent: PaintId) -> EncodedPointPaintV1 { + let program = Program::new( + vec![ColorInput::new(COLOR, Srgb8::new([0; 3]))], + observation_group(vec![SURFACE_PORT]), + vec![OpacityInput::new(OPACITY, 0.5)], + vec![ + Paint::Solid { + id: solid, + color: COLOR, + }, + Paint::Opacity { + id: translucent, + source: solid, + opacity: OPACITY, + }, + ], + vec![Surface::Input { + id: BACKDROP, + input: SURFACE_PORT, + }], + vec![Occurrence::new( + OCCURRENCE, + translucent, + BACKDROP, + CompositionProfile::EncodedSrgb8SourceOverV1, + )], + ConstraintSet::new( + vec![ConstraintInvocation::hard( + REQUIRED, + OCCURRENCE, + Srgb8::new([0x80; 3]), + )], + vec![], + ), + vec![OutputBinding::new(OUTPUT, translucent)], + ExactSrgb8IdentityV1, + ); + let owner = program.compile().unwrap().into_owner(); + let mut session = owner.attach(default_stream_binding()).unwrap(); + let SessionState::Ready { current } = session + .update_canonical_present(STREAM_A, 1, 1, |_| Srgb8::new([0xff; 3])) + .unwrap() + else { + unreachable!() + }; + let output = current.output(OUTPUT).unwrap(); + let value: EncodedPointPaintV1 = output.value(); + assert_eq!(output.paint(), value.id()); + value + } + + let original = resolve(SOLID, TRANSLUCENT); + let renamed = resolve(PaintId::new(1_010), PaintId::new(1_011)); + + assert_ne!(original.id(), renamed.id()); + assert_eq!(original.source(), renamed.source()); + assert_eq!(original.opacity(), renamed.opacity()); + assert_eq!(original.source(), Srgb8::new([0; 3])); + assert_eq!(original.opacity().bits(), 0.5f64.to_bits()); } #[test] fn point_transport_values_and_classifier_payload_are_nominal_id_invariant_before_f2_binding() { - type TerminalPaintProjection = (PaintId, Srgb8, u64); + type PreF2PaintProjection = (PaintId, Srgb8, u64); type ClassifierPayloadProjection = (ConstraintId, OccurrenceId, Srgb8, Srgb8); fn resolve( group: ObservationGroupId, stream: ObservationStreamId, - ) -> (TerminalPaintProjection, ClassifierPayloadProjection) { + ) -> (PreF2PaintProjection, ClassifierPayloadProjection) { let program = base_program_in_group( group, 0.5, @@ -987,7 +1060,7 @@ fn point_transport_values_and_classifier_payload_are_nominal_id_invariant_before ( output.paint(), output.value().source(), - output.value().straight_alpha_bits(), + output.value().opacity().bits(), ), (assessment.constraint(), assessment.target(), target, actual), ) @@ -1690,5 +1763,5 @@ fn canonical_two_port_group_assesses_multiple_occurrences_but_emits_one_paint() let outputs = current.outputs().collect::>(); assert_eq!(outputs.len(), 1); assert_eq!(outputs[0].value().source(), Srgb8::new([0; 3])); - assert_eq!(outputs[0].value().straight_alpha_bits(), 0.5f64.to_bits()); + assert_eq!(outputs[0].value().opacity().bits(), 0.5f64.to_bits()); } From b58b2edc56553d197fa57aa915a314c3244b4b07 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Wed, 22 Jul 2026 18:39:49 +0300 Subject: [PATCH 25/58] refactor(core): share revision-bound observation state - admit correlated scenario sets once behind shared immutable backing - make joint selection domain-safe, linear, and ownership-retryable - refresh source-bound release proof gates --- .github/workflows/publish.yml | 2 +- ...rt-reference-surplus-q55-bps-proof-v1.json | 2 +- .../src/generic_boundary_tests.rs | 253 +++++++++++ crates/labcolors-core/src/joint.rs | 410 ++++++++++++------ crates/labcolors-core/src/joint_tests.rs | 324 ++++++++++++-- crates/labcolors-core/src/observation.rs | 344 ++++++++++----- .../labcolors-core/src/observation_tests.rs | 255 ++++++++--- crates/labcolors-core/src/pair.rs | 30 +- crates/labcolors-core/src/point_support.rs | 57 +-- .../labcolors-core/src/point_support_tests.rs | 113 +++++ crates/labcolors-core/src/session.rs | 147 ++++--- crates/labcolors-core/src/session_tests.rs | 386 ++++++++++++++--- ...st_point_support_surplus_source_binding.py | 8 +- scripts/verify-package-release.mjs | 2 +- scripts/verify_point_support_surplus.py | 17 +- 15 files changed, 1858 insertions(+), 492 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 8d6188f9..cdea5f5a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -674,7 +674,7 @@ jobs: JSON.stringify(point.sourceBinding.exclusions) !== JSON.stringify(pointProof.source_binding_exclusions) || point.sourceBinding.closureSha256 !== pointProof.source_closure_sha256 || - pointProof.source_negative_controls !== 33 || + pointProof.source_negative_controls !== 40 || pointAlgebra?.method !== "exact-sparse-integer-polynomial-identities-plus-positive-denominator-order-lemma-v1" || pointAlgebra?.wolfram_language_cross_check?.query_sha256 !== 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 19170185..3dce01bb 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":"03f21bc3ced62e400748cae568ea2313c68a7bf5a46d5e800d501e6db61880ba","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":"51b242a0ff319f34357fbeef89037118515172dcdc088a09b9ff28e0cc38e860","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":"57e6feeab15182322c258829d10214c31bb3c93fa58d8d47e37127db204cdc28"},{"kind":"rust-source","path":"crates/labcolors-core/src/composition.rs","sha256":"195a67327a3bd86d7816b634481389930bf68577bb1202fad14c2ea152df8625"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/exact.rs","sha256":"45bd09cd8f6f74860a0a3ffeb410ee2c3cf91b4868877abbc949e724b61e1ccd"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/mod.rs","sha256":"2f3e44b2db837deed0df3e34063df9cfc6af82a912373da488c9fe2137a119c0"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/wcag22.rs","sha256":"985e21abccd1fcb4a3f9624d5947ff3d1af7a1bf26b6b170ea8bbef77d6f1798"},{"kind":"rust-source","path":"crates/labcolors-core/src/hash.rs","sha256":"f97a0fd7d6ad3162f0f1dfb326fccfb7ed40da9a8fa67a5b8a239a1ae2ae49c3"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"d87d4959e28556de54f815b7f833d5b9310cf4f4209916257e2d1ed8ad160810"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"abddb1cbc5b799428da1e1b15ff6bfa9631eea4e19c06825abaed76e74c266ef"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"56c06b49f4687b3def69c247978da1b0cb45b42c09515a001d196cb1457c93a6"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"d4b06651e07cf4f93eccf9866f6e63c35ea72fa7125bdb41e222b19b70dd2f32"},{"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":33,"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":"0e51a601f9ca4a386b9141b764606ddbdeafb53c4c5fcb6170b0cb26fc65863a"} +{"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":"17cd1c8afb755d5ede7621bd405c8c35a85e5066d4ea7ef1c4d3305e41d467b6","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":"06b0017fcfad05f582bae0d9ddc1fe1bb5240ca01fc30795a469c093cf9c1f87","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":"57e6feeab15182322c258829d10214c31bb3c93fa58d8d47e37127db204cdc28"},{"kind":"rust-source","path":"crates/labcolors-core/src/composition.rs","sha256":"195a67327a3bd86d7816b634481389930bf68577bb1202fad14c2ea152df8625"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/exact.rs","sha256":"45bd09cd8f6f74860a0a3ffeb410ee2c3cf91b4868877abbc949e724b61e1ccd"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/mod.rs","sha256":"2f3e44b2db837deed0df3e34063df9cfc6af82a912373da488c9fe2137a119c0"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/wcag22.rs","sha256":"985e21abccd1fcb4a3f9624d5947ff3d1af7a1bf26b6b170ea8bbef77d6f1798"},{"kind":"rust-source","path":"crates/labcolors-core/src/hash.rs","sha256":"f97a0fd7d6ad3162f0f1dfb326fccfb7ed40da9a8fa67a5b8a239a1ae2ae49c3"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"d87d4959e28556de54f815b7f833d5b9310cf4f4209916257e2d1ed8ad160810"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"2bf4c805fb95f5b091f6c30ceb4b425f4c8dffef5127da2853156d86a16ad9cd"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"974116bbbb797063d98fcce4f23b089fc05a588a765432c15a82315d4489ef94"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"2363bfbedcaf619c5b7d964cb3667095a030ce4a6cfd7b8db9a34a6dd300e102"},{"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":40,"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":"d82019a4b5d3fd8bde4d8731118a4d5369a2e2b3e676d5d251b0a7504ca00a30"} diff --git a/crates/labcolors-core/src/generic_boundary_tests.rs b/crates/labcolors-core/src/generic_boundary_tests.rs index a265a5ca..7ab5ee32 100644 --- a/crates/labcolors-core/src/generic_boundary_tests.rs +++ b/crates/labcolors-core/src/generic_boundary_tests.rs @@ -1,6 +1,9 @@ const APPEARANCE_SOURCE: &str = include_str!("appearance.rs"); const LCS_OCCURRENCE_SOURCE: &str = include_str!("lcs_occurrence.rs"); +const OBSERVATION_SOURCE: &str = include_str!("observation.rs"); +const POINT_SUPPORT_SOURCE: &str = include_str!("point_support.rs"); const PROGRAM_SESSION_SOURCE: &str = include_str!("program_session.rs"); +const SESSION_SOURCE: &str = include_str!("session.rs"); const LIB_SOURCE: &str = include_str!("lib.rs"); const GENERIC_SOURCES: [(&str, &str); 3] = [ @@ -27,6 +30,24 @@ const CLIENT_OR_LEGACY_VOCABULARY: [&str; 15] = [ "Danger", ]; +fn source_scope<'a>(source: &'a str, start: &str, end: &str) -> &'a str { + let start = source + .find(start) + .unwrap_or_else(|| panic!("missing source boundary start `{start}`")); + let tail = &source[start..]; + let end = tail + .find(end) + .unwrap_or_else(|| panic!("missing source boundary end `{end}` after `{start}`")); + &tail[..end] +} + +fn normalized_source_scope(source: &str, start: &str, end: &str) -> String { + source_scope(source, start, end) + .split_whitespace() + .collect::>() + .join(" ") +} + #[test] fn generic_physical_and_transport_modules_contain_no_client_or_legacy_vocabulary() { for (path, source) in GENERIC_SOURCES { @@ -39,6 +60,238 @@ fn generic_physical_and_transport_modules_contain_no_client_or_legacy_vocabulary } } +#[test] +fn shared_observation_ssot_has_one_backing_without_lifecycle_or_adapter_facades() { + assert_eq!( + normalized_source_scope( + OBSERVATION_SOURCE, + "struct ObservedScenarioSet {", + "impl ObservedScenarioSet", + ), + concat!( + "struct ObservedScenarioSet { ", + "cases: Box<[PhysicalScenario]>, ", + "values: Box<[Srgb8]>, ", + "provenance: Box<[ScenarioId]>, ", + "}", + ), + "the canonical scenario set must keep one flat physical backing", + ); + assert_eq!( + normalized_source_scope( + OBSERVATION_SOURCE, + "struct ObservationBackingV1 {", + "/// Sealed observation admitted", + ), + concat!( + "struct ObservationBackingV1 { ", + "schema: CanonicalObservationSchemaV1, ", + "set: ObservedScenarioSet, ", + "}", + ), + "the shared backing must own only canonical schema and scenario data", + ); + assert_eq!( + normalized_source_scope( + OBSERVATION_SOURCE, + "pub(crate) struct RevisionBoundObservationV1 {", + "impl RevisionBoundObservationV1", + ), + concat!( + "pub(crate) struct RevisionBoundObservationV1 { ", + "stream: ObservationStreamId, ", + "revision: Revision, ", + "backing: Rc, ", + "}", + ), + "revision identity must wrap exactly one shared immutable backing", + ); + assert_eq!( + OBSERVATION_SOURCE + .matches("pub(crate) struct RevisionBoundObservationV1") + .count(), + 1, + "production must define exactly one revision-bound observation value", + ); + assert!( + OBSERVATION_SOURCE.contains("use std::rc::Rc;"), + "single-threaded Core observations must use Rc", + ); + assert!( + OBSERVATION_SOURCE + .contains("pub(crate) struct CanonicalObservationSchemaV1(Rc<[SurfaceInputPortId]>);"), + "compiled schema and observations must share the same Rc-backed schema", + ); + for forbidden in ["std::sync::Arc", "Arc<", "RefCell<", "Mutex<", "RwLock<"] { + assert!( + !OBSERVATION_SOURCE.contains(forbidden), + "immutable single-threaded observation backing must not acquire `{forbidden}`", + ); + } + + assert_eq!( + normalized_source_scope( + SESSION_SOURCE, + "pub(crate) enum PointSupportSessionStateV1 {", + "impl PointSupportSessionStateV1", + ), + concat!( + "pub(crate) enum PointSupportSessionStateV1 { ", + "Waiting, ", + "Ready { current: VerifiedPointSupportV1, }, ", + "Stale { previous: VerifiedPointSupportV1, }, ", + "Failed { cause: PointSupportViolationV1, previous: Option, }, ", + "}", + ), + "lifecycle state must not duplicate the current raw observation", + ); + assert_eq!( + normalized_source_scope( + SESSION_SOURCE, + "enum SessionObservationHeadV1 {", + "impl SessionObservationHeadV1", + ), + concat!( + "enum SessionObservationHeadV1 { ", + "Empty, ", + "Unknown(RevisionBoundUnknownV1), ", + "Observed(crate::observation::RevisionBoundObservationV1), ", + "}", + ), + "raw Empty/Unknown/Observed must remain separate from lifecycle state", + ); + let session_owner = source_scope( + SESSION_SOURCE, + "pub(crate) struct PointSupportSessionV1 {", + "impl PointSupportSessionV1", + ); + for required in [ + "raw_head: SessionObservationHeadV1,", + "state: PointSupportSessionStateV1,", + ] { + assert_eq!( + session_owner.matches(required).count(), + 1, + "Session must own exactly one `{required}` field", + ); + } + for forbidden in [ + "current_unknown", + "observation: RevisionBoundObservationV1", + "unknown: RevisionBoundUnknownV1", + ] { + assert!( + !session_owner.contains(forbidden), + "Session owner must not duplicate raw storage through `{forbidden}`", + ); + } + + let consuming_entry = source_scope( + POINT_SUPPORT_SOURCE, + "impl BoundPointSupportRecheckV1 {", + "pub(crate) enum PointSupportEvaluationErrorV1", + ); + for required in [ + "observation: RevisionBoundObservationV1,", + "evaluate_bound_point_support(self, &observation)?", + "assessment.bind(observation)", + ] { + assert!( + consuming_entry.contains(required), + "point support must consume the shared observation directly; missing `{required}`", + ); + } + for forbidden in [ + ".clone()", + ".to_vec()", + "ObservationAdapter", + "adapt_observation", + ] { + assert!( + !consuming_entry.contains(forbidden), + "point-support entry must not introduce observation façade `{forbidden}`", + ); + } + + let evaluator = source_scope( + POINT_SUPPORT_SOURCE, + "fn evaluate_bound_point_support(", + "fn reference_distance(", + ); + assert!( + evaluator.contains("observation: &RevisionBoundObservationV1"), + "the evaluator must borrow the shared revision-bound observation", + ); + assert_eq!( + evaluator.matches(".physical_values(case_index)").count(), + 1, + "point support must read each canonical physical case through the observation API", + ); + let physical_values = evaluator + .find(".physical_values(case_index)") + .expect("physical-values route must exist"); + let indexed_surface = evaluator[physical_values..] + .find("values.get(surface_index)") + .expect("the prebound surface index must read from physical values"); + assert!( + indexed_surface > 0, + "surface lookup must follow the canonical physical-values projection", + ); + for forbidden in [ + "physical_bindings", + "SurfaceInputBinding", + ".clone()", + ".to_vec()", + "ObservationAdapter", + "adapt_observation", + ] { + assert!( + !evaluator.contains(forbidden), + "point evaluator must not reconstruct or adapt observations through `{forbidden}`", + ); + } + + let report = source_scope( + POINT_SUPPORT_SOURCE, + "pub(crate) struct RevisionBoundPointSupportReportV1 {", + "impl RevisionBoundPointSupportReportV1", + ); + assert_eq!( + report + .matches("observation: RevisionBoundObservationV1,") + .count(), + 1, + "a report must own the same revision-bound observation without a parallel snapshot", + ); + + for (path, source) in [ + ("observation.rs", OBSERVATION_SOURCE), + ("session.rs", SESSION_SOURCE), + ("point_support.rs", POINT_SUPPORT_SOURCE), + ] { + for forbidden in [ + "FrozenObservationV1", + "PriorObservation", + "Availability", + "ObservationSnapshot", + "WaitingRecheckV1", + "StaleRecheckV1", + "PresentationHoldV1", + "HoldErrorV1", + "reuse_for", + "ReuseErrorV1", + "FinalRecheckOutcomeV1", + "ObservationAdapter", + "adapt_observation", + ] { + assert!( + !source.contains(forbidden), + "{path} must not restore compatibility or adapter API `{forbidden}`", + ); + } + } +} + #[test] fn encoded_point_transport_does_not_claim_lcs_observation_types() { for forbidden in ["TristimulusSample", "LcsOccurrence", "AppearanceState"] { diff --git a/crates/labcolors-core/src/joint.rs b/crates/labcolors-core/src/joint.rs index 9cbde260..7c25657d 100644 --- a/crates/labcolors-core/src/joint.rs +++ b/crates/labcolors-core/src/joint.rs @@ -25,7 +25,10 @@ use crate::session::SessionObservationBindingPermitV1; /// каждого target occurrence. Конкретные Exact/WCAG/readability payload-и /// остаются в evaluator-модулях и не образуют центральный enum. pub(crate) trait JointPointEvaluatorV1: Clone + Debug + PartialEq { - type Invocation: Clone + Debug + PartialEq; + /// Joint execution repeats one invocation across the complete physical + /// matrix. Requiring a value type here keeps that repetition allocation-free + /// instead of hiding an arbitrary `Clone` behind the engine's preflight. + type Invocation: Copy + Debug + PartialEq; type PassEvidence: Clone + Debug + PartialEq; type ViolationEvidence: Clone + Debug + PartialEq; type Error: Clone + Debug + PartialEq; @@ -44,7 +47,7 @@ where + PartialEq + Evaluator + HardClassifier, PointMeasurement>, - PointInvocation: Clone + Debug + PartialEq, + PointInvocation: Copy + Debug + PartialEq, VisiblePointPassEvidence: Clone + Debug + PartialEq, VisiblePointViolationEvidence: Clone + Debug + PartialEq, >::Error: Clone + Debug + PartialEq, @@ -99,7 +102,7 @@ impl JointCandidateTupleV1 { /// Order-free candidate domain. Policy не участвует в его construction. #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct JointCandidateSetV1 { - candidates: Box<[JointCandidateTupleV1]>, + candidates: Vec, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -125,20 +128,26 @@ impl JointCandidateSetV1 { return Err(CandidateSetErrorV1::DuplicateOrdinal(pair[0].ordinal)); } } - for (index, first) in candidates.iter().enumerate() { - if let Some(second) = candidates[index + 1..] - .iter() - .find(|second| first.lower == second.lower && first.upper == second.upper) - { - return Err(CandidateSetErrorV1::DuplicatePhysicalTuple { - first: first.ordinal, - second: second.ordinal, - }); - } + // Group equal physical tuples without auxiliary storage. The explicit + // ordinal tie-break makes every group canonical even though the sort is + // unstable. We inspect every duplicate group and retain the same error + // precedence as the former ordinal-order scan: the smallest first + // ordinal, followed by the smallest matching second ordinal. + candidates.sort_unstable_by(|left, right| { + candidate_physical_key(left) + .cmp(&candidate_physical_key(right)) + .then_with(|| left.ordinal.cmp(&right.ordinal)) + }); + let duplicate = candidates + .windows(2) + .filter(|pair| pair[0].lower == pair[1].lower && pair[0].upper == pair[1].upper) + .map(|pair| (pair[0].ordinal, pair[1].ordinal)) + .min(); + if let Some((first, second)) = duplicate { + return Err(CandidateSetErrorV1::DuplicatePhysicalTuple { first, second }); } - Ok(Self { - candidates: candidates.into_boxed_slice(), - }) + candidates.sort_unstable_by_key(|candidate| candidate.ordinal); + Ok(Self { candidates }) } pub(crate) fn candidates(&self) -> &[JointCandidateTupleV1] { @@ -146,6 +155,19 @@ impl JointCandidateSetV1 { } } +fn candidate_physical_key( + candidate: &JointCandidateTupleV1, +) -> (PaintId, Srgb8, u64, PaintId, Srgb8, u64) { + ( + candidate.lower.id(), + candidate.lower.source(), + candidate.lower.opacity().bits(), + candidate.upper.id(), + candidate.upper.source(), + candidate.upper.opacity().bits(), + ) +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub(crate) struct JointConstraintIdV1(u32); @@ -191,8 +213,6 @@ where } } -pub(crate) type JointHardConstraintV1 = PointwiseJointHardConstraintV1; - impl PointwiseJointHardConstraintV1 { pub(crate) fn exact( id: JointConstraintIdV1, @@ -218,7 +238,8 @@ mod observation_seal { pub(crate) trait JointObservationV1: observation_seal::Sealed + Debug + PartialEq { fn case_count(&self) -> usize; - fn surface_at(&self, case_index: usize, surface: SurfaceInputPortId) -> Option; + fn bind_surface(&self, surface: SurfaceInputPortId) -> Option; + fn value_at_bound(&self, case_index: usize, bound_index: usize) -> Option; fn provenance(&self, case_index: usize) -> Option<&[ScenarioId]>; } @@ -226,15 +247,15 @@ impl observation_seal::Sealed for RevisionBoundObservationV1 {} impl JointObservationV1 for RevisionBoundObservationV1 { fn case_count(&self) -> usize { - self.set().cases().len() + self.physical_case_count() + } + + fn bind_surface(&self, surface: SurfaceInputPortId) -> Option { + self.schema().binary_search(&surface).ok() } - fn surface_at(&self, case_index: usize, surface: SurfaceInputPortId) -> Option { - let bindings = self.physical_bindings(case_index)?; - let index = bindings - .binary_search_by_key(&surface, |binding| binding.port()) - .ok()?; - Some(bindings[index].value()) + fn value_at_bound(&self, case_index: usize, bound_index: usize) -> Option { + self.physical_values(case_index)?.get(bound_index).copied() } fn provenance(&self, case_index: usize) -> Option<&[ScenarioId]> { @@ -264,8 +285,12 @@ impl JointObservationV1 for StaticJointObservationV1 { 1 } - fn surface_at(&self, case_index: usize, surface: SurfaceInputPortId) -> Option { - (case_index == 0 && surface == self.root_surface).then_some(self.root) + fn bind_surface(&self, surface: SurfaceInputPortId) -> Option { + (surface == self.root_surface).then_some(0) + } + + fn value_at_bound(&self, case_index: usize, bound_index: usize) -> Option { + (case_index == 0 && bound_index == 0).then_some(self.root) } fn provenance(&self, _case_index: usize) -> Option<&[ScenarioId]> { @@ -283,11 +308,9 @@ where root_surface: SurfaceInputPortId, lower_paint: PaintId, upper_paint: PaintId, - constraints: Box<[PointwiseJointHardConstraintV1]>, + constraints: Vec>, } -pub(crate) type JointPointProgramV1 = PointwiseJointPointProgramV1; - #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum JointProgramErrorV1 { SamePaintIdentity(PaintId), @@ -299,7 +322,7 @@ impl PointwiseJointPointProgramV1 { root_surface: SurfaceInputPortId, lower_paint: PaintId, upper_paint: PaintId, - constraints: Vec, + constraints: Vec>, ) -> Result { Self::with_evaluator( ExactSrgb8IdentityV1, @@ -336,7 +359,7 @@ where root_surface, lower_paint, upper_paint, - constraints: constraints.into_boxed_slice(), + constraints, }) } @@ -345,7 +368,7 @@ where } pub(crate) fn evaluate_static( - &self, + self, candidates: JointCandidateSetV1, observation: StaticJointObservationV1, ) -> Result< @@ -356,7 +379,7 @@ where } pub(crate) fn evaluate_revision_bound( - &self, + self, candidates: JointCandidateSetV1, observation: RevisionBoundObservationV1, _permit: SessionObservationBindingPermitV1, @@ -368,7 +391,7 @@ where } fn evaluate_owned( - &self, + self, candidates: JointCandidateSetV1, observation: Observation, ) -> Result< @@ -379,46 +402,64 @@ where Observation: JointObservationV1, { self.validate_candidates(&candidates)?; - self.validate_observation(&observation)?; + let root_binding = self.bind_observation_surface(&observation)?; let (execution_count, cell_count) = checked_joint_cardinality_raw( candidates.candidates.len(), observation.case_count(), self.constraints.len(), ) .map_err(|_| PointwiseJointReportErrorV1::ResourceExhausted)?; + let mut feasible_ordinals = Vec::new(); + feasible_ordinals + .try_reserve_exact(candidates.candidates.len()) + .map_err(|_| PointwiseJointReportErrorV1::ResourceExhausted)?; + feasible_ordinals.extend( + candidates + .candidates + .iter() + .map(|candidate| candidate.ordinal), + ); let matrices = self.execute( candidates.candidates(), &observation, + root_binding, execution_count, cell_count, )?; + retain_feasible_ordinals(&mut feasible_ordinals, &matrices.cells); Ok(PointwiseFullHardReportV1 { program_identity: self.identity(), - program: self.clone(), + program: self, candidates, observation, executions: matrices.executions, cells: matrices.cells, + feasible_ordinals, }) } - fn validate_observation( + fn bind_observation_surface( &self, observation: &Observation, - ) -> Result<(), PointwiseJointReportErrorV1> + ) -> Result> where Observation: JointObservationV1, { + let Some(root_binding) = observation.bind_surface(self.root_surface) else { + return Err(PointwiseJointReportErrorV1::MissingRootSurface( + self.root_surface, + )); + }; if (0..observation.case_count()).any(|case_index| { observation - .surface_at(case_index, self.root_surface) + .value_at_bound(case_index, root_binding) .is_none() }) { return Err(PointwiseJointReportErrorV1::MissingRootSurface( self.root_surface, )); } - Ok(()) + Ok(root_binding) } fn validate_candidates( @@ -450,6 +491,7 @@ where &self, candidates: &[JointCandidateTupleV1], observation: &Observation, + root_binding: usize, execution_count: usize, cell_count: usize, ) -> Result< @@ -470,9 +512,9 @@ where for candidate in candidates { for case_index in 0..observation.case_count() { - let root = observation - .surface_at(case_index, self.root_surface) - .unwrap_or_else(|| unreachable!("joint observation passed keyed preflight")); + let root = observation.value_at_bound(case_index, root_binding).ok_or( + PointwiseJointReportErrorV1::MissingRootSurface(self.root_surface), + )?; let lower = PointOpacityOverSurfaceV1::evaluate_admitted( candidate.lower.source().bytes(), candidate.lower.opacity(), @@ -494,7 +536,7 @@ where upper, }); - for constraint in self.constraints.iter().cloned() { + for constraint in &self.constraints { let occurrence = match constraint.target { JointVisibleTargetV1::Lower => &lower, JointVisibleTargetV1::Upper => &upper, @@ -503,7 +545,7 @@ where // поэтому частичная матрица не называется FullHardReport. let decision = match self .evaluator - .assess(occurrence, constraint.invocation.clone()) + .assess(occurrence, constraint.invocation) .map_err(PointwiseJointReportErrorV1::Evaluator)? { HardDecision::Pass(evidence) => { @@ -526,10 +568,7 @@ where debug_assert_eq!(executions.len(), execution_count); debug_assert_eq!(cells.len(), cell_count); - Ok(PointwiseJointEvaluationMatricesV1 { - executions: executions.into_boxed_slice(), - cells: cells.into_boxed_slice(), - }) + Ok(PointwiseJointEvaluationMatricesV1 { executions, cells }) } } @@ -549,8 +588,6 @@ where ResourceExhausted, } -pub(crate) type JointReportErrorV1 = PointwiseJointReportErrorV1; - #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum JointCapacityErrorV1 { ResourceExhausted, @@ -574,18 +611,41 @@ pub(crate) fn checked_joint_cardinality( candidates: usize, cases: usize, constraints: usize, -) -> Result<(usize, usize), JointReportErrorV1> { +) -> Result<(usize, usize), PointwiseJointReportErrorV1> { checked_joint_cardinality_raw(candidates, cases, constraints) - .map_err(|_| JointReportErrorV1::ResourceExhausted) + .map_err(|_| PointwiseJointReportErrorV1::ResourceExhausted) } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, PartialEq)] struct PointwiseJointEvaluationMatricesV1 where Evaluation: JointPointEvaluatorV1, { - executions: Box<[JointExecutionRecordV1]>, - cells: Box<[PointwiseJointConstraintCellV1]>, + executions: Vec, + cells: Vec>, +} + +fn retain_feasible_ordinals( + feasible: &mut Vec, + cells: &[PointwiseJointConstraintCellV1], +) where + Evaluation: JointPointEvaluatorV1, +{ + // Both vectors are candidate-major and ordinal-canonical. One cursor keeps + // classification O(candidates + cells), including the empty-constraint case. + let mut cell_index = 0; + feasible.retain(|ordinal| { + let mut passes = true; + while let Some(cell) = cells + .get(cell_index) + .filter(|cell| cell.ordinal == *ordinal) + { + passes &= cell.decision.is_pass(); + cell_index += 1; + } + passes + }); + debug_assert_eq!(cell_index, cells.len()); } /// Один execution record существует независимо от наличия constraint на lower. @@ -647,9 +707,6 @@ where Violation(Evaluation::ViolationEvidence), } -pub(crate) type JointConstraintDecisionV1 = - PointwiseJointConstraintDecisionV1; - impl PointwiseJointConstraintDecisionV1 where Evaluation: JointPointEvaluatorV1, @@ -714,7 +771,7 @@ where /// Полная матрица candidate x constraint x unique physical case плюс отдельная /// joint execution matrix candidate x case. Report не знает selection policy. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, PartialEq)] pub(crate) struct PointwiseFullHardReportV1 where Evaluation: JointPointEvaluatorV1, @@ -724,8 +781,9 @@ where program: PointwiseJointPointProgramV1, candidates: JointCandidateSetV1, observation: Observation, - executions: Box<[JointExecutionRecordV1]>, - cells: Box<[PointwiseJointConstraintCellV1]>, + executions: Vec, + cells: Vec>, + feasible_ordinals: Vec, } impl PointwiseFullHardReportV1 @@ -758,29 +816,17 @@ where } pub(crate) fn classify(self) -> PointwiseHardFeasibilityV1 { - let mut feasible = Vec::new(); - for candidate in self.candidates.candidates() { - if self - .cells - .iter() - .filter(|cell| cell.ordinal == candidate.ordinal) - .all(|cell| cell.decision.is_pass()) - { - feasible.push(candidate.ordinal); - } - } - if feasible.is_empty() { + if self.feasible_ordinals.is_empty() { PointwiseHardFeasibilityV1::Infeasible(self) } else { PointwiseHardFeasibilityV1::NonEmpty(PointwiseNonEmptyFeasibleJointTuplesV1 { report: self, - feasible: feasible.into_boxed_slice(), }) } } } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, PartialEq)] pub(crate) enum PointwiseHardFeasibilityV1 where Evaluation: JointPointEvaluatorV1, @@ -790,17 +836,13 @@ where NonEmpty(PointwiseNonEmptyFeasibleJointTuplesV1), } -pub(crate) type HardFeasibilityV1 = - PointwiseHardFeasibilityV1; - -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, PartialEq)] pub(crate) struct PointwiseNonEmptyFeasibleJointTuplesV1 where Evaluation: JointPointEvaluatorV1, Observation: JointObservationV1, { report: PointwiseFullHardReportV1, - feasible: Box<[CandidateOrdinalV1]>, } impl PointwiseNonEmptyFeasibleJointTuplesV1 @@ -809,42 +851,129 @@ where Observation: JointObservationV1, { pub(crate) fn feasible(&self) -> &[CandidateOrdinalV1] { - &self.feasible + &self.report.feasible_ordinals } pub(crate) fn candidate_set(&self) -> &JointCandidateSetV1 { self.report.candidate_set() } + #[expect( + clippy::result_large_err, + reason = "ownership-preserving rejection keeps the expensive report retryable without heap allocation or recomputation" + )] pub(crate) fn select( self, policy: DeclaredTotalOrderV1, - ) -> PointwiseSelectedJointTupleV1 { - let ordinal = policy - .order - .iter() - .copied() - .find(|ordinal| self.feasible.binary_search(ordinal).is_ok()) - .unwrap_or_else(|| unreachable!("validated total order covers nonempty feasible set")); - let candidate = *self + ) -> Result< + PointwiseSelectedJointTupleV1, + PointwiseSelectionFailureV1, + > { + if !policy.is_bound_to(self.report.candidate_set()) { + return Err(PointwiseSelectionFailureV1 { + feasible: self, + policy, + reason: SelectionPolicyErrorV1::CandidateDomainMismatch, + }); + } + // Canonical policy entries and feasible ordinals are both sorted by + // ordinal. One merge scan finds the feasible entry with minimum + // client-declared rank without C×log(F) lookup or auxiliary storage. + let mut feasible_index = 0; + let mut selected: Option<(usize, usize)> = None; + for (candidate_index, entry) in policy.domain.iter().enumerate() { + if self.report.feasible_ordinals.get(feasible_index) == Some(&entry.ordinal) { + if selected.is_none_or(|(rank, _)| entry.rank < rank) { + selected = Some((entry.rank, candidate_index)); + } + feasible_index += 1; + } + } + let Some((_, candidate_index)) = + selected.filter(|_| feasible_index == self.report.feasible_ordinals.len()) + else { + return Err(PointwiseSelectionFailureV1 { + feasible: self, + policy, + reason: SelectionPolicyErrorV1::InternalInvariant, + }); + }; + let Some(candidate) = self .report .candidates .candidates() - .iter() - .find(|candidate| candidate.ordinal == ordinal) - .unwrap_or_else(|| unreachable!("validated ordinal belongs to candidate set")); - PointwiseSelectedJointTupleV1 { + .get(candidate_index) + .copied() + else { + return Err(PointwiseSelectionFailureV1 { + feasible: self, + policy, + reason: SelectionPolicyErrorV1::InternalInvariant, + }); + }; + Ok(PointwiseSelectedJointTupleV1 { report: self.report, policy, candidate, - } + }) + } +} + +/// Recoverable selection rejection. A foreign/malformed policy cannot destroy +/// the expensive full report: the caller can replace only the policy and retry +/// without recomposition or evaluator execution. +#[derive(Debug, PartialEq)] +pub(crate) struct PointwiseSelectionFailureV1 +where + Evaluation: JointPointEvaluatorV1, + Observation: JointObservationV1, +{ + feasible: PointwiseNonEmptyFeasibleJointTuplesV1, + policy: DeclaredTotalOrderV1, + reason: SelectionPolicyErrorV1, +} + +impl PointwiseSelectionFailureV1 +where + Evaluation: JointPointEvaluatorV1, + Observation: JointObservationV1, +{ + pub(crate) const fn feasible( + &self, + ) -> &PointwiseNonEmptyFeasibleJointTuplesV1 { + &self.feasible + } + + pub(crate) const fn policy(&self) -> &DeclaredTotalOrderV1 { + &self.policy + } + + pub(crate) const fn reason(&self) -> SelectionPolicyErrorV1 { + self.reason + } + + pub(crate) fn into_parts( + self, + ) -> ( + PointwiseNonEmptyFeasibleJointTuplesV1, + DeclaredTotalOrderV1, + SelectionPolicyErrorV1, + ) { + (self.feasible, self.policy, self.reason) } } /// Полный client-declared tie-break. Он не участвует в measurement/report. #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct DeclaredTotalOrderV1 { - order: Box<[CandidateOrdinalV1]>, + order: Vec, + domain: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct DeclaredPolicyDomainEntryV1 { + ordinal: CandidateOrdinalV1, + rank: usize, } impl DeclaredTotalOrderV1 { @@ -855,33 +984,60 @@ impl DeclaredTotalOrderV1 { if order.len() != candidates.candidates.len() { return Err(SelectionPolicyErrorV1::NotATotalOrder); } - let mut canonical = order.clone(); - canonical.sort_unstable(); - for pair in canonical.windows(2) { - if pair[0] == pair[1] { - return Err(SelectionPolicyErrorV1::DuplicateOrdinal(pair[0])); - } + let mut domain = Vec::new(); + domain + .try_reserve_exact(candidates.candidates.len()) + .map_err(|_| SelectionPolicyErrorV1::ResourceExhausted)?; + domain.extend( + order + .iter() + .copied() + .enumerate() + .map(|(rank, ordinal)| DeclaredPolicyDomainEntryV1 { ordinal, rank }), + ); + domain.sort_unstable_by_key(|entry| entry.ordinal); + if let Some(pair) = domain + .windows(2) + .find(|pair| pair[0].ordinal == pair[1].ordinal) + { + return Err(SelectionPolicyErrorV1::DuplicateOrdinal(pair[0].ordinal)); } - if canonical.iter().copied().ne(candidates + if domain.iter().map(|entry| entry.ordinal).ne(candidates .candidates .iter() .map(|candidate| candidate.ordinal)) { return Err(SelectionPolicyErrorV1::NotATotalOrder); } - Ok(Self { - order: order.into_boxed_slice(), - }) + Ok(Self { order, domain }) + } + + pub(crate) fn order(&self) -> &[CandidateOrdinalV1] { + &self.order + } + + pub(crate) fn into_order(self) -> Vec { + self.order + } + + fn is_bound_to(&self, candidates: &JointCandidateSetV1) -> bool { + self.domain.iter().map(|entry| entry.ordinal).eq(candidates + .candidates + .iter() + .map(|candidate| candidate.ordinal)) } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum SelectionPolicyErrorV1 { + ResourceExhausted, DuplicateOrdinal(CandidateOrdinalV1), NotATotalOrder, + CandidateDomainMismatch, + InternalInvariant, } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, PartialEq)] pub(crate) struct PointwiseSelectedJointTupleV1 where Evaluation: JointPointEvaluatorV1, @@ -910,9 +1066,10 @@ where // A revision-bound report can enter this consuming chain only through // `evaluate_revision_bound` with a Session-minted linear permit. Static // reports have a different concrete observation type. - self.report + let root_binding = self + .report .program - .validate_observation(&self.report.observation) + .bind_observation_surface(&self.report.observation) .map_err(|_| PointwiseSelectedRecheckErrorV1::InvariantDrift)?; let cases = self.report.observation.case_count(); let (execution_count, cell_count) = @@ -924,6 +1081,7 @@ where .execute( core::slice::from_ref(&self.candidate), &self.report.observation, + root_binding, execution_count, cell_count, ) @@ -939,15 +1097,18 @@ where PointwiseSelectedRecheckErrorV1::InvariantDrift } })?; - if let Some(violation) = matrices + if let Some(violation_index) = matrices .cells .iter() - .find(|cell| !cell.decision.is_pass()) - .cloned() + .position(|cell| !cell.decision.is_pass()) { - return Err(PointwiseSelectedRecheckErrorV1::Violation(Box::new( - violation, - ))); + return Err(PointwiseSelectedRecheckErrorV1::Violation { + evidence: PointwiseFreshJointRecheckV1 { + executions: matrices.executions, + cells: matrices.cells, + }, + violation_index, + }); } Ok(PointwiseVerifiedSelectionV1 { selected: self, @@ -959,7 +1120,7 @@ where } } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, PartialEq)] pub(crate) enum PointwiseSelectedRecheckErrorV1 where Evaluation: JointPointEvaluatorV1, @@ -967,19 +1128,22 @@ where ResourceExhausted, InvariantDrift, Evaluator(Evaluation::Error), - Violation(Box>), + Violation { + evidence: PointwiseFreshJointRecheckV1, + violation_index: usize, + }, } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, PartialEq)] pub(crate) struct PointwiseFreshJointRecheckV1 where Evaluation: JointPointEvaluatorV1, { - executions: Box<[JointExecutionRecordV1]>, - cells: Box<[PointwiseJointConstraintCellV1]>, + executions: Vec, + cells: Vec>, } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, PartialEq)] pub(crate) struct PointwiseVerifiedSelectionV1 where Evaluation: JointPointEvaluatorV1, diff --git a/crates/labcolors-core/src/joint_tests.rs b/crates/labcolors-core/src/joint_tests.rs index 992f61b2..025ac076 100644 --- a/crates/labcolors-core/src/joint_tests.rs +++ b/crates/labcolors-core/src/joint_tests.rs @@ -1,20 +1,20 @@ use crate::Srgb8; use crate::appearance::{EncodedPointPaintV1, PaintId, SurfaceInputPortId}; use crate::composition::AdmittedOpacityV1; -use crate::constraints::{HardDecision, Wcag22Srgb8V1}; +use crate::constraints::{ExactSrgb8IdentityV1, HardDecision, Wcag22Srgb8V1}; use crate::joint::{ - CandidateOrdinalV1, CandidateSetErrorV1, DeclaredTotalOrderV1, HardFeasibilityV1, - JointCandidateSetV1, JointCandidateTupleV1, JointConstraintDecisionV1, JointConstraintIdV1, - JointHardConstraintV1, JointPointEvaluatorV1, JointPointProgramIdentityV1, JointPointProgramV1, - JointProgramErrorV1, JointReportErrorV1, JointVisibleTargetV1, PointwiseHardFeasibilityV1, - PointwiseJointHardConstraintV1, PointwiseJointPointProgramV1, PointwiseJointReportErrorV1, - PointwiseSelectedRecheckErrorV1, SelectionPolicyErrorV1, checked_joint_cardinality, + CandidateOrdinalV1, CandidateSetErrorV1, DeclaredTotalOrderV1, JointCandidateSetV1, + JointCandidateTupleV1, JointConstraintIdV1, JointPointEvaluatorV1, JointPointProgramIdentityV1, + JointProgramErrorV1, JointVisibleTargetV1, PointwiseHardFeasibilityV1, + PointwiseJointConstraintDecisionV1, PointwiseJointHardConstraintV1, + PointwiseJointPointProgramV1, PointwiseJointReportErrorV1, PointwiseSelectedRecheckErrorV1, + SelectionPolicyErrorV1, checked_joint_cardinality, }; use crate::observation::{ ObservationHeadViewV1, ObservationOwnerV1, ObservationPayloadInput, ObservationStreamId, ObservationUpdateInput, ObservedScenarioSetInput, PreparedObservationUpdateV1, Revision, RevisionBoundObservationV1, ScenarioId, ScenarioInput, SurfaceInputBinding, - prepare_observation, + canonicalize_observation_schema, prepare_observation, }; use crate::session::SessionObservationBindingPermitV1; use crate::wcag22::Wcag22CriterionV1; @@ -58,20 +58,22 @@ fn candidates(values: Vec) -> JointCandidateSetV1 { JointCandidateSetV1::new(values).unwrap() } -fn program(constraints: Vec) -> JointPointProgramV1 { - JointPointProgramV1::new(ROOT, LOWER, UPPER, constraints).unwrap() +fn program( + constraints: Vec>, +) -> PointwiseJointPointProgramV1 { + PointwiseJointPointProgramV1::new(ROOT, LOWER, UPPER, constraints).unwrap() } -fn exact_upper(id: u32, target: [u8; 3]) -> JointHardConstraintV1 { - JointHardConstraintV1::exact( +fn exact_upper(id: u32, target: [u8; 3]) -> PointwiseJointHardConstraintV1 { + PointwiseJointHardConstraintV1::exact( JointConstraintIdV1::new(id), JointVisibleTargetV1::Upper, Srgb8::new(target), ) } -fn exact_lower(id: u32, target: [u8; 3]) -> JointHardConstraintV1 { - JointHardConstraintV1::exact( +fn exact_lower(id: u32, target: [u8; 3]) -> PointwiseJointHardConstraintV1 { + PointwiseJointHardConstraintV1::exact( JointConstraintIdV1::new(id), JointVisibleTargetV1::Lower, Srgb8::new(target), @@ -80,10 +82,11 @@ fn exact_lower(id: u32, target: [u8; 3]) -> JointHardConstraintV1 { fn observation(revision: u64, cases: Vec<(u32, [u8; 3])>) -> RevisionBoundObservationV1 { let mut owner = EmptyObservationOwner; + let schema = canonicalize_observation_schema(vec![ROOT]).unwrap(); let prepared = prepare_observation( &mut owner, STREAM, - &[ROOT], + &schema, ObservationUpdateInput { stream: STREAM, revision: Revision::new(revision), @@ -109,6 +112,46 @@ fn observation(revision: u64, cases: Vec<(u32, [u8; 3])>) -> RevisionBoundObserv observation } +fn observation_with_unrelated_surface( + revision: u64, + unrelated: [u8; 3], + root: [u8; 3], +) -> RevisionBoundObservationV1 { + let unrelated_surface = SurfaceInputPortId::new(ROOT.value() - 1); + let mut owner = EmptyObservationOwner; + let schema = canonicalize_observation_schema(vec![ROOT, unrelated_surface]).unwrap(); + let prepared = prepare_observation( + &mut owner, + STREAM, + &schema, + ObservationUpdateInput { + stream: STREAM, + revision: Revision::new(revision), + payload: ObservationPayloadInput::Scenarios(ObservedScenarioSetInput { + scenarios: vec![ScenarioInput { + id: ScenarioId::new(17), + bindings: vec![ + SurfaceInputBinding { + port: ROOT, + value: Srgb8::new(root), + }, + SurfaceInputBinding { + port: unrelated_surface, + value: Srgb8::new(unrelated), + }, + ], + }], + }), + }, + ) + .unwrap(); + let PreparedObservationUpdateV1::Observed(prepared) = prepared else { + panic!("fresh observed update must prepare an observation"); + }; + let (_owner, observation) = prepared.into_parts(); + observation +} + #[test] fn linked_candidate_is_selected_only_after_upper_sees_lower_visible_surface() { let observed = observation(1, vec![(1, [0; 3])]); @@ -145,14 +188,14 @@ fn linked_candidate_is_selected_only_after_upper_sees_lower_visible_surface() { assert_eq!(report.cells()[0].decision().target(), Srgb8::new([192; 3])); assert!(matches!( report.cells()[0].decision(), - JointConstraintDecisionV1::Violation(_) + PointwiseJointConstraintDecisionV1::Violation(_) )); assert!(matches!( report.cells()[1].decision(), - JointConstraintDecisionV1::Pass(_) + PointwiseJointConstraintDecisionV1::Pass(_) )); - let HardFeasibilityV1::NonEmpty(feasible) = report.classify() else { + let PointwiseHardFeasibilityV1::NonEmpty(feasible) = report.classify() else { panic!("second joint tuple must be feasible"); }; assert_eq!(feasible.feasible(), &[CandidateOrdinalV1::new(1)]); @@ -161,7 +204,7 @@ fn linked_candidate_is_selected_only_after_upper_sees_lower_visible_surface() { vec![CandidateOrdinalV1::new(0), CandidateOrdinalV1::new(1)], ) .unwrap(); - let selected = feasible.select(policy); + let selected = feasible.select(policy).unwrap(); assert_eq!(selected.ordinal(), CandidateOrdinalV1::new(1)); let verified = selected.recheck().unwrap(); assert_eq!(verified.ordinal(), CandidateOrdinalV1::new(1)); @@ -181,7 +224,7 @@ fn every_unique_physical_case_must_pass_without_worst_or_average_reduction() { assert_eq!(report.cells().len(), 2); assert_eq!(report.cells()[0].decision().actual(), Srgb8::new([128; 3])); assert_eq!(report.cells()[1].decision().actual(), Srgb8::new([192; 3])); - let HardFeasibilityV1::Infeasible(report) = report.classify() else { + let PointwiseHardFeasibilityV1::Infeasible(report) = report.classify() else { panic!("one violated case must exclude the whole tuple"); }; assert_eq!( @@ -275,6 +318,25 @@ fn scenario_declaration_permutation_is_canonical() { assert_eq!(first, second); } +#[test] +fn revision_bound_root_uses_its_schema_ordinal_and_retains_case_provenance() { + let report = program(vec![exact_lower(1, [128; 3])]) + .evaluate_revision_bound( + candidates(vec![candidate(0, ([0; 3], 0.5), ([255; 3], 1.0))]), + observation_with_unrelated_surface(6, [0; 3], [255; 3]), + session_permit(), + ) + .unwrap(); + + assert_eq!(report.executions().len(), 1); + assert_eq!(report.executions()[0].lower_visible(), Srgb8::new([128; 3])); + assert_eq!(report.provenance(0), Some(&[ScenarioId::new(17)][..])); + assert!(matches!( + report.cells()[0].decision(), + PointwiseJointConstraintDecisionV1::Pass(_) + )); +} + #[test] fn static_joint_evaluation_is_explicitly_lifecycle_free() { let report = program(vec![exact_upper(1, [255; 3])]) @@ -301,7 +363,7 @@ fn static_joint_key_mismatch_fails_before_compositing() { assert!(matches!( result, - Err(JointReportErrorV1::MissingRootSurface(ROOT)) + Err(PointwiseJointReportErrorV1::MissingRootSurface(ROOT)) )); assert_eq!(crate::composition::source_over_evaluation_count(), 0); } @@ -340,7 +402,7 @@ fn declared_policy_is_separate_from_report_and_is_the_only_tie_break() { .unwrap() }; - let HardFeasibilityV1::NonEmpty(first) = make_report().classify() else { + let PointwiseHardFeasibilityV1::NonEmpty(first) = make_report().classify() else { panic!("both tuples must pass"); }; let first_policy = DeclaredTotalOrderV1::new( @@ -348,7 +410,7 @@ fn declared_policy_is_separate_from_report_and_is_the_only_tie_break() { vec![CandidateOrdinalV1::new(7), CandidateOrdinalV1::new(4)], ) .unwrap(); - let HardFeasibilityV1::NonEmpty(second) = make_report().classify() else { + let PointwiseHardFeasibilityV1::NonEmpty(second) = make_report().classify() else { panic!("both tuples must pass"); }; let second_policy = DeclaredTotalOrderV1::new( @@ -357,15 +419,165 @@ fn declared_policy_is_separate_from_report_and_is_the_only_tie_break() { ) .unwrap(); assert_eq!( - first.select(first_policy).ordinal(), + first.select(first_policy).unwrap().ordinal(), CandidateOrdinalV1::new(7) ); assert_eq!( - second.select(second_policy).ordinal(), + second.select(second_policy).unwrap().ordinal(), CandidateOrdinalV1::new(4) ); } +#[test] +fn foreign_disjoint_and_partially_overlapping_policy_domains_are_typed_errors() { + let make_actual = || { + let report = program(vec![]) + .evaluate_revision_bound( + candidates(vec![ + candidate(1, ([11; 3], 1.0), ([111; 3], 1.0)), + candidate(2, ([22; 3], 1.0), ([122; 3], 1.0)), + ]), + observation(70, vec![(1, [0; 3])]), + session_permit(), + ) + .unwrap(); + let PointwiseHardFeasibilityV1::NonEmpty(feasible) = report.classify() else { + panic!("an unconstrained nonempty domain must be feasible"); + }; + feasible + }; + + let disjoint_domain = candidates(vec![ + candidate(10, ([10; 3], 1.0), ([210; 3], 1.0)), + candidate(11, ([11; 3], 1.0), ([211; 3], 1.0)), + ]); + let disjoint_policy = DeclaredTotalOrderV1::new( + &disjoint_domain, + vec![CandidateOrdinalV1::new(10), CandidateOrdinalV1::new(11)], + ) + .unwrap(); + let disjoint_feasible = make_actual(); + crate::composition::reset_source_over_evaluation_count(); + let (disjoint, disjoint_allocations) = + crate::test_support::measured_allocations(|| disjoint_feasible.select(disjoint_policy)); + let Err(disjoint_failure) = disjoint else { + panic!("a disjoint policy domain must be rejected"); + }; + assert_eq!( + disjoint_failure.reason(), + SelectionPolicyErrorV1::CandidateDomainMismatch + ); + assert_eq!( + disjoint_failure.feasible().feasible(), + &[CandidateOrdinalV1::new(1), CandidateOrdinalV1::new(2)] + ); + assert_eq!( + disjoint_failure.policy().order(), + &[CandidateOrdinalV1::new(10), CandidateOrdinalV1::new(11)] + ); + assert_eq!(disjoint_allocations, 0); + assert_eq!(crate::composition::source_over_evaluation_count(), 0); + + // Ordinal 1 overlaps and is first in the foreign order. The old selector + // silently accepted it; exact-domain validation must reject the policy. + let partial_domain = candidates(vec![ + candidate(1, ([31; 3], 1.0), ([131; 3], 1.0)), + candidate(3, ([33; 3], 1.0), ([133; 3], 1.0)), + ]); + let partial_policy = DeclaredTotalOrderV1::new( + &partial_domain, + vec![CandidateOrdinalV1::new(1), CandidateOrdinalV1::new(3)], + ) + .unwrap(); + let partial_feasible = make_actual(); + crate::composition::reset_source_over_evaluation_count(); + let (partial, partial_allocations) = + crate::test_support::measured_allocations(|| partial_feasible.select(partial_policy)); + let Err(partial_failure) = partial else { + panic!("a partially overlapping policy domain must be rejected"); + }; + assert_eq!( + partial_failure.reason(), + SelectionPolicyErrorV1::CandidateDomainMismatch + ); + assert_eq!(partial_allocations, 0); + assert_eq!(crate::composition::source_over_evaluation_count(), 0); + + // Recover the expensive report and the caller's original Vec allocation, + // repair only the order, then retry without re-running physical evaluation. + let (recovered_feasible, rejected_policy, reason) = partial_failure.into_parts(); + assert_eq!(reason, SelectionPolicyErrorV1::CandidateDomainMismatch); + let order_backing = rejected_policy.order().as_ptr(); + let mut corrected_order = rejected_policy.into_order(); + corrected_order[1] = CandidateOrdinalV1::new(2); + corrected_order.swap(0, 1); + assert_eq!(corrected_order.as_ptr(), order_backing); + let corrected_policy = + DeclaredTotalOrderV1::new(recovered_feasible.candidate_set(), corrected_order).unwrap(); + assert_eq!(corrected_policy.order().as_ptr(), order_backing); + let selected = recovered_feasible.select(corrected_policy).unwrap(); + assert_eq!(selected.ordinal(), CandidateOrdinalV1::new(2)); + assert_eq!(crate::composition::source_over_evaluation_count(), 0); +} + +#[test] +fn policy_is_reusable_for_the_same_ordinal_domain_without_owning_candidate_physics() { + let make_actual = || { + let report = program(vec![]) + .evaluate_revision_bound( + candidates(vec![ + candidate(1, ([11; 3], 1.0), ([111; 3], 1.0)), + candidate(2, ([22; 3], 1.0), ([222; 3], 1.0)), + ]), + observation(71, vec![(1, [0; 3])]), + session_permit(), + ) + .unwrap(); + let PointwiseHardFeasibilityV1::NonEmpty(feasible) = report.classify() else { + panic!("an unconstrained nonempty domain must be feasible"); + }; + feasible + }; + + let different_physics = candidates(vec![ + candidate(2, ([202; 3], 0.5), ([72; 3], 1.0)), + candidate(1, ([201; 3], 0.5), ([71; 3], 1.0)), + ]); + let reusable_policy = DeclaredTotalOrderV1::new( + &different_physics, + vec![CandidateOrdinalV1::new(2), CandidateOrdinalV1::new(1)], + ) + .unwrap(); + let verified = make_actual() + .select(reusable_policy) + .unwrap() + .recheck() + .unwrap(); + assert_eq!(verified.ordinal(), CandidateOrdinalV1::new(2)); + assert_eq!( + verified.fresh_executions()[0].lower_visible(), + Srgb8::new([22; 3]) + ); + assert_eq!( + verified.fresh_executions()[0].upper_visible(), + Srgb8::new([222; 3]) + ); + + let independently_identical = candidates(vec![ + candidate(2, ([22; 3], 1.0), ([222; 3], 1.0)), + candidate(1, ([11; 3], 1.0), ([111; 3], 1.0)), + ]); + let identical_policy = DeclaredTotalOrderV1::new( + &independently_identical, + vec![CandidateOrdinalV1::new(1), CandidateOrdinalV1::new(2)], + ) + .unwrap(); + assert_eq!( + make_actual().select(identical_policy).unwrap().ordinal(), + CandidateOrdinalV1::new(1) + ); +} + #[test] fn fresh_recheck_executes_the_selected_joint_program_again_on_the_same_revision() { let observed = observation(8, vec![(1, [0; 3]), (2, [255; 3])]); @@ -377,13 +589,13 @@ fn fresh_recheck_executes_the_selected_joint_program_again_on_the_same_revision( ) .unwrap(); crate::composition::reset_source_over_evaluation_count(); - let HardFeasibilityV1::NonEmpty(feasible) = report.classify() else { + let PointwiseHardFeasibilityV1::NonEmpty(feasible) = report.classify() else { panic!("opaque upper must pass on both roots"); }; let policy = DeclaredTotalOrderV1::new(feasible.candidate_set(), vec![CandidateOrdinalV1::new(0)]) .unwrap(); - let selected = feasible.select(policy); + let selected = feasible.select(policy).unwrap(); let verified = selected.recheck().unwrap(); assert_eq!(crate::composition::source_over_evaluation_count(), 4); @@ -403,14 +615,14 @@ fn empty_hard_constraint_set_is_non_vacuously_feasible() { assert_eq!(report.executions().len(), 1); assert!(report.cells().is_empty()); - let HardFeasibilityV1::NonEmpty(feasible) = report.classify() else { + let PointwiseHardFeasibilityV1::NonEmpty(feasible) = report.classify() else { panic!("a tuple with no hard violations must be feasible"); }; assert_eq!(feasible.feasible(), &[CandidateOrdinalV1::new(0)]); let policy = DeclaredTotalOrderV1::new(feasible.candidate_set(), vec![CandidateOrdinalV1::new(0)]) .unwrap(); - let verified = feasible.select(policy).recheck().unwrap(); + let verified = feasible.select(policy).unwrap().recheck().unwrap(); assert_eq!(verified.fresh_executions().len(), 1); assert!(verified.fresh_cells().is_empty()); } @@ -451,7 +663,7 @@ fn generic_wcag_evaluator_can_constrain_the_derived_lower_occurrence() { vec![CandidateOrdinalV1::new(1), CandidateOrdinalV1::new(2)], ) .unwrap(); - let verified = feasible.select(policy).recheck().unwrap(); + let verified = feasible.select(policy).unwrap().recheck().unwrap(); assert_eq!(verified.ordinal(), CandidateOrdinalV1::new(2)); assert_eq!(verified.fresh_cells().len(), 1); assert!(verified.fresh_cells()[0].decision().is_pass()); @@ -535,7 +747,7 @@ fn evaluator_error_invalidates_the_full_report_and_fresh_recheck() { DeclaredTotalOrderV1::new(feasible.candidate_set(), vec![CandidateOrdinalV1::new(0)]) .unwrap(); assert!(matches!( - feasible.select(policy).recheck(), + feasible.select(policy).unwrap().recheck(), Err(PointwiseSelectedRecheckErrorV1::Evaluator( "evaluator-fault" )) @@ -568,7 +780,7 @@ fn invalid_domains_and_policies_fail_before_compositing() { }) ); assert_eq!( - JointPointProgramV1::new(ROOT, LOWER, LOWER, vec![exact_upper(1, [0; 3])]), + PointwiseJointPointProgramV1::new(ROOT, LOWER, LOWER, vec![exact_upper(1, [0; 3])]), Err(JointProgramErrorV1::SamePaintIdentity(LOWER)) ); let observed = observation(9, vec![(1, [0; 3])]); @@ -585,7 +797,7 @@ fn invalid_domains_and_policies_fail_before_compositing() { observed, session_permit() ), - Err(JointReportErrorV1::CandidatePaintMismatch { + Err(PointwiseJointReportErrorV1::CandidatePaintMismatch { stage: JointVisibleTargetV1::Lower, .. }) @@ -606,14 +818,56 @@ fn invalid_domains_and_policies_fail_before_compositing() { ); } +#[test] +fn duplicate_physical_detection_preserves_canonical_ordinal_precedence() { + let first_order = vec![ + candidate(4, ([0; 3], 1.0), ([10; 3], 1.0)), + candidate(9, ([250; 3], 1.0), ([240; 3], 1.0)), + candidate(3, ([0; 3], 1.0), ([10; 3], 1.0)), + candidate(1, ([250; 3], 1.0), ([240; 3], 1.0)), + ]; + let reverse_order = first_order.iter().rev().copied().collect(); + let expected = Err(CandidateSetErrorV1::DuplicatePhysicalTuple { + first: CandidateOrdinalV1::new(1), + second: CandidateOrdinalV1::new(9), + }); + assert_eq!(JointCandidateSetV1::new(first_order), expected); + assert_eq!(JointCandidateSetV1::new(reverse_order), expected); + + assert_eq!( + JointCandidateSetV1::new(vec![ + candidate(7, ([70; 3], 0.5), ([170; 3], 1.0)), + candidate(2, ([70; 3], 0.5), ([170; 3], 1.0)), + candidate(5, ([70; 3], 0.5), ([170; 3], 1.0)), + ]), + Err(CandidateSetErrorV1::DuplicatePhysicalTuple { + first: CandidateOrdinalV1::new(2), + second: CandidateOrdinalV1::new(5), + }) + ); +} + +#[test] +fn large_candidate_domain_remains_ordinal_canonical() { + const COUNT: u32 = 4_096; + let make = |ordinal: u32| { + let bytes = [(ordinal >> 8) as u8, ordinal as u8, 17]; + candidate(ordinal, (bytes, 1.0), ([255, 0, 19], 1.0)) + }; + let input = (0..COUNT).rev().map(make).collect(); + let expected: Vec<_> = (0..COUNT).map(make).collect(); + let domain = JointCandidateSetV1::new(input).unwrap(); + assert_eq!(domain.candidates(), expected); +} + #[test] fn cardinality_overflow_is_rejected_by_preflight() { assert_eq!( checked_joint_cardinality(usize::MAX, 2, 1), - Err(JointReportErrorV1::ResourceExhausted) + Err(PointwiseJointReportErrorV1::ResourceExhausted) ); assert_eq!( checked_joint_cardinality(usize::MAX / 2 + 1, 2, 2), - Err(JointReportErrorV1::ResourceExhausted) + Err(PointwiseJointReportErrorV1::ResourceExhausted) ); } diff --git a/crates/labcolors-core/src/observation.rs b/crates/labcolors-core/src/observation.rs index ae09a7e4..789a4a8e 100644 --- a/crates/labcolors-core/src/observation.rs +++ b/crates/labcolors-core/src/observation.rs @@ -5,6 +5,7 @@ //! only code allowed to bind an admitted observation to evaluator evidence. use core::ops::Range; +use std::rc::Rc; use crate::Srgb8; use crate::appearance::SurfaceInputPortId; @@ -70,14 +71,6 @@ impl SurfaceInputBinding { pub(crate) const fn new(port: SurfaceInputPortId, value: Srgb8) -> Self { Self { port, value } } - - pub(crate) const fn port(self) -> SurfaceInputPortId { - self.port - } - - pub(crate) const fn value(self) -> Srgb8 { - self.value - } } /// Raw tuple: every binding was observed simultaneously. @@ -108,22 +101,16 @@ pub(crate) struct ObservationUpdateInput { pub(crate) payload: ObservationPayloadInput, } -/// One unique physical tuple as canonical keyed bindings. Port identity travels -/// with every value, so an observation cannot be reinterpreted through a -/// different positional schema. +/// One unique physical tuple inside the shared canonical backing. +/// +/// Values are stored once in schema order. The schema remains attached to the +/// backing itself instead of being repeated beside every value. #[derive(Debug, PartialEq, Eq)] -#[cfg_attr(test, derive(Clone))] -pub(crate) struct PhysicalScenario { - bindings: Vec, +struct PhysicalScenario { + values: Range, provenance: Range, } -impl PhysicalScenario { - pub(crate) fn bindings(&self) -> &[SurfaceInputBinding] { - &self.bindings - } -} - /// First canonical ordinal where an observation's intrinsic keyed schema and a /// compiled consumer schema differ. `None` represents an exhausted side. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -150,56 +137,59 @@ impl ObservationSchemaMismatchV1 { } } -/// Canonical nonempty set. Provenance is one flat allocation shared by ranges, -/// not one allocation per physical case. +/// Canonical nonempty correlated set. Values and provenance each use one flat +/// allocation; a physical case owns only ranges into those arrays. #[derive(Debug, PartialEq, Eq)] -#[cfg_attr(test, derive(Clone))] -pub(crate) struct ObservedScenarioSet { - cases: Vec, - provenance: Vec, +struct ObservedScenarioSet { + cases: Box<[PhysicalScenario]>, + values: Box<[Srgb8]>, + provenance: Box<[ScenarioId]>, } impl ObservedScenarioSet { - pub(crate) fn cases(&self) -> &[PhysicalScenario] { - &self.cases + fn values(&self, case_index: usize) -> Option<&[Srgb8]> { + let values = &self.cases.get(case_index)?.values; + self.values.get(values.start..values.end) } - pub(crate) fn provenance(&self, case_index: usize) -> Option<&[ScenarioId]> { + fn provenance(&self, case_index: usize) -> Option<&[ScenarioId]> { let provenance = &self.cases.get(case_index)?.provenance; - let range = provenance.start..provenance.end; - self.provenance.get(range) + self.provenance.get(provenance.start..provenance.end) } +} - fn validate_surface_schema( - &self, - expected: &[SurfaceInputPortId], - ) -> Result<(), ObservationSchemaMismatchV1> { - for (case_index, case) in self.cases.iter().enumerate() { - let schema_len = expected.len().max(case.bindings.len()); - for binding_index in 0..schema_len { - let expected_input = expected.get(binding_index).copied(); - let actual_input = case.bindings.get(binding_index).map(|binding| binding.port); - if expected_input != actual_input { - return Err(ObservationSchemaMismatchV1::new( - case_index, - binding_index, - expected_input, - actual_input, - )); - } - } - } - Ok(()) +/// Canonical immutable schema shared by the compiled recheck and every +/// admitted observation backing created for it. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct CanonicalObservationSchemaV1(Rc<[SurfaceInputPortId]>); + +impl CanonicalObservationSchemaV1 { + pub(crate) fn as_slice(&self) -> &[SurfaceInputPortId] { + &self.0 + } + + fn shares_backing_with(&self, other: &Self) -> bool { + Rc::ptr_eq(&self.0, &other.0) + } + + #[cfg(test)] + pub(crate) fn backing_ptr_for_test(&self) -> *const SurfaceInputPortId { + self.0.as_ptr() } } -/// Sealed observation admitted against the Session-owned compiled schema. #[derive(Debug, PartialEq, Eq)] -#[cfg_attr(test, derive(Clone))] +struct ObservationBackingV1 { + schema: CanonicalObservationSchemaV1, + set: ObservedScenarioSet, +} + +/// Sealed observation admitted against the Session-owned compiled schema. +#[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct RevisionBoundObservationV1 { stream: ObservationStreamId, revision: Revision, - set: ObservedScenarioSet, + backing: Rc, } impl RevisionBoundObservationV1 { @@ -211,30 +201,66 @@ impl RevisionBoundObservationV1 { self.revision } - pub(crate) const fn set(&self) -> &ObservedScenarioSet { - &self.set + pub(crate) fn schema(&self) -> &[SurfaceInputPortId] { + self.backing.schema.as_slice() } pub(crate) fn physical_case_count(&self) -> usize { - self.set.cases().len() + self.backing.set.cases.len() } - pub(crate) fn physical_bindings(&self, case_index: usize) -> Option<&[SurfaceInputBinding]> { - self.set - .cases() - .get(case_index) - .map(PhysicalScenario::bindings) + pub(crate) fn physical_values(&self, case_index: usize) -> Option<&[Srgb8]> { + self.backing.set.values(case_index) } pub(crate) fn provenance(&self, case_index: usize) -> Option<&[ScenarioId]> { - self.set.provenance(case_index) + self.backing.set.provenance(case_index) } pub(crate) fn validate_surface_schema( &self, expected: &[SurfaceInputPortId], ) -> Result<(), ObservationSchemaMismatchV1> { - self.set.validate_surface_schema(expected) + let actual = self.schema(); + let schema_len = expected.len().max(actual.len()); + for binding_index in 0..schema_len { + let expected_input = expected.get(binding_index).copied(); + let actual_input = actual.get(binding_index).copied(); + if expected_input != actual_input { + return Err(ObservationSchemaMismatchV1::new( + 0, + binding_index, + expected_input, + actual_input, + )); + } + } + Ok(()) + } + + pub(crate) fn shares_schema_backing_with( + &self, + expected: &CanonicalObservationSchemaV1, + ) -> bool { + self.backing.schema.shares_backing_with(expected) + } + + fn has_canonical_input( + &self, + schema: &CanonicalObservationSchemaV1, + scenarios: &[ScenarioInput], + ) -> bool { + &self.backing.schema == schema && canonical_input_matches_set(&self.backing.set, scenarios) + } + + #[cfg(test)] + pub(crate) fn backing_ptr_for_test(&self) -> *const () { + Rc::as_ptr(&self.backing).cast() + } + + #[cfg(test)] + pub(crate) fn schema_ptr_for_test(&self) -> *const SurfaceInputPortId { + self.backing.schema.backing_ptr_for_test() } } @@ -375,11 +401,15 @@ pub(crate) enum PreparedObservationUpdateV1<'owner, Owner> { Observed(PreparedObservedV1<'owner, Owner>), } -/// Canonicalize and validate a compiled surface-input schema without any new -/// allocation. The supplied Vec remains the unique schema owner. +/// Canonicalize and validate a compiled surface-input schema. +/// +/// Sorting and duplicate detection reuse the supplied `Vec`. Converting its +/// storage into the immutable `Rc`-backed schema can still allocate through the +/// global allocator; this function does not claim allocator-wide recoverable +/// OOM semantics. pub(crate) fn canonicalize_observation_schema( mut ports: Vec, -) -> Result, ObservationError> { +) -> Result { if ports.is_empty() { return Err(ObservationError::EmptyCompiledSurfaceInputSchema); } @@ -389,16 +419,20 @@ pub(crate) fn canonicalize_observation_schema( input: duplicate[0], }); } - Ok(ports) + Ok(CanonicalObservationSchemaV1(Rc::from( + ports.into_boxed_slice(), + ))) } -/// Prepare without mutation. Cheap stream/lower-revision/payload-kind failures -/// precede scenario canonicalization; exact same-revision observed replay still -/// canonicalizes so equality is content-complete. +/// Prepare without mutation. Stream identity always precedes payload handling. +/// `Scenarios` are fully admitted before revision comparison so malformed +/// correlated input is never masked by an otherwise valid revision error. +/// `Unknown` has no payload structure to admit and takes the cheap revision +/// path directly. pub(crate) fn prepare_observation<'owner, Owner: ObservationOwnerV1>( owner: &'owner mut Owner, stream: ObservationStreamId, - schema: &[SurfaceInputPortId], + schema: &CanonicalObservationSchemaV1, update: ObservationUpdateInput, ) -> Result, ObservationError> { if update.stream != stream { @@ -408,18 +442,17 @@ pub(crate) fn prepare_observation<'owner, Owner: ObservationOwnerV1>( }); } - let current_revision = owner.observation_head().revision(); - if let Some(current) = current_revision { - if update.revision < current { - return Err(ObservationError::RevisionOutOfOrder { - current, - incoming: update.revision, - }); - } - } - match update.payload { ObservationPayloadInput::Unknown(reason) => { + let current_revision = owner.observation_head().revision(); + if let Some(current) = current_revision { + if update.revision < current { + return Err(ObservationError::RevisionOutOfOrder { + current, + incoming: update.revision, + }); + } + } if current_revision == Some(update.revision) { let exact = matches!( owner.observation_head(), @@ -445,6 +478,16 @@ pub(crate) fn prepare_observation<'owner, Owner: ObservationOwnerV1>( })) } ObservationPayloadInput::Scenarios(raw) => { + let scenarios = canonicalize_scenarios_input(schema.as_slice(), raw)?; + let current_revision = owner.observation_head().revision(); + if let Some(current) = current_revision { + if update.revision < current { + return Err(ObservationError::RevisionOutOfOrder { + current, + incoming: update.revision, + }); + } + } if current_revision == Some(update.revision) && !matches!(owner.observation_head(), ObservationHeadViewV1::Observed(_)) { @@ -453,11 +496,10 @@ pub(crate) fn prepare_observation<'owner, Owner: ObservationOwnerV1>( }); } - let set = admit_scenarios(schema, raw)?; if current_revision == Some(update.revision) { let exact = matches!( - owner.observation_head(), - ObservationHeadViewV1::Observed(current) if current.set == set + owner.observation_head(), ObservationHeadViewV1::Observed(current) + if current.has_canonical_input(schema, &scenarios) ); return if exact { Ok(PreparedObservationUpdateV1::Idempotent( @@ -470,22 +512,26 @@ pub(crate) fn prepare_observation<'owner, Owner: ObservationOwnerV1>( }; } + let set = materialize_scenarios(schema.as_slice(), scenarios)?; Ok(PreparedObservationUpdateV1::Observed(PreparedObservedV1 { owner, observation: RevisionBoundObservationV1 { stream, revision: update.revision, - set, + backing: Rc::new(ObservationBackingV1 { + schema: schema.clone(), + set, + }), }, })) } } } -fn admit_scenarios( +fn canonicalize_scenarios_input( schema: &[SurfaceInputPortId], raw: ObservedScenarioSetInput, -) -> Result { +) -> Result, ObservationError> { if raw.scenarios.is_empty() { return Err(ObservationError::EmptyScenarioSet); } @@ -501,11 +547,7 @@ fn admit_scenarios( }); } - let mut tuples = Vec::new(); - tuples - .try_reserve_exact(scenarios.len()) - .map_err(|_| ObservationError::ResourceExhausted)?; - for mut scenario in scenarios { + for scenario in &mut scenarios { scenario .bindings .sort_unstable_by_key(|binding| binding.port); @@ -541,42 +583,120 @@ fn admit_scenarios( input: unexpected.port, }); } + } - // Move the already allocated, sorted keyed tuple directly. Keeping the - // port beside the value makes schema identity part of observation - // equality and removes any later positional reinterpretation seam. - tuples.push((scenario.bindings, scenario.id)); + // Reuse the caller-owned outer Vec and every bindings Vec. This complete + // canonical input is enough for lower/same-revision decisions without any + // new allocation; only an applied higher revision materializes backing. + scenarios.sort_unstable_by(|left, right| { + left.bindings + .cmp(&right.bindings) + .then_with(|| left.id.cmp(&right.id)) + }); + Ok(scenarios) +} + +fn canonical_input_matches_set(set: &ObservedScenarioSet, scenarios: &[ScenarioInput]) -> bool { + let mut case_index = 0; + let mut scenario_index = 0; + while scenario_index < scenarios.len() { + let bindings = &scenarios[scenario_index].bindings; + let Some(values) = set.values(case_index) else { + return false; + }; + if bindings.len() != values.len() + || bindings + .iter() + .zip(values) + .any(|(binding, value)| binding.value != *value) + { + return false; + } + + let first = scenario_index; + scenario_index += 1; + while scenario_index < scenarios.len() + && scenarios[scenario_index].bindings.as_slice() == bindings.as_slice() + { + scenario_index += 1; + } + let Some(provenance) = set.provenance(case_index) else { + return false; + }; + if provenance.len() != scenario_index - first + || scenarios[first..scenario_index] + .iter() + .zip(provenance) + .any(|(scenario, provenance)| scenario.id != *provenance) + { + return false; + } + case_index += 1; } + case_index == set.cases.len() +} - tuples.sort_unstable_by(|left, right| left.0.cmp(&right.0).then_with(|| left.1.cmp(&right.1))); +fn materialize_scenarios( + schema: &[SurfaceInputPortId], + scenarios: Vec, +) -> Result { + debug_assert!(!scenarios.is_empty()); - // Both output allocations are reserved before grouping. Moving tuples and - // pushing into these capacities cannot allocate afterward. + let unique_case_count = 1 + scenarios + .windows(2) + .filter(|window| window[0].bindings.as_slice() != window[1].bindings.as_slice()) + .count(); + let value_count = unique_case_count + .checked_mul(schema.len()) + .ok_or(ObservationError::ResourceExhausted)?; + + // Capacity for every variable-sized Vec used by grouping is fallibly + // reserved before the first push/extend, so grouping cannot grow one of + // those Vecs. The final boxed representation and its later Rc owner still + // follow the global allocator's OOM behavior. let mut cases = Vec::new(); cases - .try_reserve_exact(tuples.len()) + .try_reserve_exact(unique_case_count) + .map_err(|_| ObservationError::ResourceExhausted)?; + let mut values = Vec::new(); + values + .try_reserve_exact(value_count) .map_err(|_| ObservationError::ResourceExhausted)?; let mut provenance = Vec::new(); provenance - .try_reserve_exact(tuples.len()) + .try_reserve_exact(scenarios.len()) .map_err(|_| ObservationError::ResourceExhausted)?; - let mut tuples = tuples.into_iter().peekable(); - while let Some((bindings, first_id)) = tuples.next() { - let start = provenance.len(); + let mut scenarios = scenarios.into_iter().peekable(); + while let Some(ScenarioInput { + id: first_id, + bindings, + }) = scenarios.next() + { + let values_start = values.len(); + values.extend(bindings.iter().map(|binding| binding.value)); + let values_end = values.len(); + let provenance_start = provenance.len(); provenance.push(first_id); - while matches!(tuples.peek(), Some((candidate, _)) if candidate == &bindings) { - let (_, id) = tuples + while matches!(scenarios.peek(), Some(candidate) if candidate.bindings.as_slice() == bindings.as_slice()) + { + let ScenarioInput { id, .. } = scenarios .next() - .unwrap_or_else(|| unreachable!("peek observed the next tuple")); + .unwrap_or_else(|| unreachable!("peek observed the next scenario")); provenance.push(id); } - let end = provenance.len(); + let provenance_end = provenance.len(); cases.push(PhysicalScenario { - bindings, - provenance: start..end, + values: values_start..values_end, + provenance: provenance_start..provenance_end, }); } - Ok(ObservedScenarioSet { cases, provenance }) + debug_assert_eq!(cases.len(), unique_case_count); + debug_assert_eq!(values.len(), value_count); + Ok(ObservedScenarioSet { + cases: cases.into_boxed_slice(), + values: values.into_boxed_slice(), + provenance: provenance.into_boxed_slice(), + }) } diff --git a/crates/labcolors-core/src/observation_tests.rs b/crates/labcolors-core/src/observation_tests.rs index 33b0f9ed..01a4495d 100644 --- a/crates/labcolors-core/src/observation_tests.rs +++ b/crates/labcolors-core/src/observation_tests.rs @@ -5,10 +5,10 @@ use crate::appearance::{ ResolvedOccurrence, SurfaceId, SurfaceInputPortId, SurfaceSpec, }; use crate::observation::{ - ObservationError, ObservationHeadViewV1, ObservationOwnerV1, ObservationPayloadInput, - ObservationSchemaMismatchV1, ObservationStreamId, ObservationUpdateInput, ObservedScenarioSet, - ObservedScenarioSetInput, PreparedObservationUpdateV1, Revision, RevisionBoundObservationV1, - RevisionBoundUnknownV1, ScenarioId, ScenarioInput, SurfaceInputBinding, UnknownReasonId, + CanonicalObservationSchemaV1, ObservationError, ObservationHeadViewV1, ObservationOwnerV1, + ObservationPayloadInput, ObservationStreamId, ObservationUpdateInput, ObservedScenarioSetInput, + PreparedObservationUpdateV1, Revision, RevisionBoundObservationV1, RevisionBoundUnknownV1, + ScenarioId, ScenarioInput, SurfaceInputBinding, UnknownReasonId, canonicalize_observation_schema, prepare_observation, }; @@ -42,7 +42,7 @@ impl ObservationOwnerV1 for TestOwner { #[derive(Debug, Clone, PartialEq, Eq)] struct TestState { stream: ObservationStreamId, - schema: Vec, + schema: CanonicalObservationSchemaV1, owner: TestOwner, } @@ -154,13 +154,6 @@ fn paired_set(first: ([u8; 3], [u8; 3]), second: ([u8; 3], [u8; 3])) -> Observed ]) } -fn observed_set(state: &TestState) -> &ObservedScenarioSet { - state - .current_observation() - .expect("expected admitted observation") - .set() -} - fn revision_bound(state: &TestState) -> &RevisionBoundObservationV1 { state .current_observation() @@ -232,14 +225,16 @@ fn admission_preserves_correlated_tuples_without_cartesian_product() { )) .unwrap(); - let cases: Vec> = observed_set(&state) - .cases() - .iter() - .map(|case| { - case.bindings() + let observation = revision_bound(&state); + assert_eq!(observation.schema(), &[PORT_A, PORT_B]); + let cases: Vec> = (0..observation.physical_case_count()) + .map(|case_index| { + observation + .physical_values(case_index) + .unwrap() .iter() .copied() - .map(|binding| binding.value().bytes()) + .map(Srgb8::bytes) .collect() }) .collect(); @@ -269,27 +264,112 @@ fn canonicalization_ignores_declaration_order_and_groups_duplicate_physics() { right.apply(observed_update(STREAM, 1, second)).unwrap(); assert_eq!(left, right); - let set = observed_set(&left); - assert_eq!(set.cases().len(), 2); + let observation = revision_bound(&left); + assert_eq!(observation.schema(), &[PORT_A, PORT_B]); + assert_eq!(observation.physical_case_count(), 2); assert_eq!( - set.provenance(0).unwrap(), + observation.provenance(0).unwrap(), &[ScenarioId::new(3), ScenarioId::new(9)] ); - assert_eq!(set.provenance(1).unwrap(), &[ScenarioId::new(4)]); - let physical_bindings: Vec> = set - .cases() - .iter() - .map(|case| case.bindings().to_vec()) + assert_eq!(observation.provenance(1).unwrap(), &[ScenarioId::new(4)]); + let physical_values: Vec> = (0..observation.physical_case_count()) + .map(|case_index| observation.physical_values(case_index).unwrap().to_vec()) .collect(); assert_eq!( - physical_bindings, + physical_values, vec![ - vec![binding(PORT_A, [1, 2, 3]), binding(PORT_B, [4, 5, 6])], - vec![binding(PORT_A, [9, 8, 7]), binding(PORT_B, [6, 5, 4])], + vec![Srgb8::new([1, 2, 3]), Srgb8::new([4, 5, 6])], + vec![Srgb8::new([9, 8, 7]), Srgb8::new([6, 5, 4])], ] ); } +#[test] +fn revision_bound_clone_is_allocation_free_and_shares_all_canonical_backing() { + let mut state = TestState::new(STREAM, vec![PORT_B, PORT_A]).unwrap(); + state + .apply(observed_update( + STREAM, + 1, + scenarios([ + scenario(9, [binding(PORT_B, [4, 5, 6]), binding(PORT_A, [1, 2, 3])]), + scenario(3, [binding(PORT_A, [1, 2, 3]), binding(PORT_B, [4, 5, 6])]), + ]), + )) + .unwrap(); + + let observation = revision_bound(&state); + let backing_ptr = observation.backing_ptr_for_test(); + let schema_ptr = observation.schema_ptr_for_test(); + let (cloned, allocations) = crate::test_support::measured_allocations(|| observation.clone()); + + assert_eq!(allocations, 0); + assert_eq!(&cloned, observation); + assert_eq!(cloned.backing_ptr_for_test(), backing_ptr); + assert_eq!(cloned.schema_ptr_for_test(), schema_ptr); + assert_eq!(cloned.schema(), observation.schema()); + assert_eq!(cloned.physical_values(0), observation.physical_values(0)); + assert_eq!(cloned.provenance(0), observation.provenance(0)); +} + +#[test] +fn independent_equal_admissions_do_not_alias_observation_or_schema_backing() { + let first = scenarios([ + scenario(9, [binding(PORT_B, [4, 5, 6]), binding(PORT_A, [1, 2, 3])]), + scenario(3, [binding(PORT_A, [1, 2, 3]), binding(PORT_B, [4, 5, 6])]), + ]); + let second = scenarios([ + scenario(3, [binding(PORT_B, [4, 5, 6]), binding(PORT_A, [1, 2, 3])]), + scenario(9, [binding(PORT_A, [1, 2, 3]), binding(PORT_B, [4, 5, 6])]), + ]); + let mut left = TestState::new(STREAM, vec![PORT_B, PORT_A]).unwrap(); + let mut right = TestState::new(STREAM, vec![PORT_A, PORT_B]).unwrap(); + left.apply(observed_update(STREAM, 1, first)).unwrap(); + right.apply(observed_update(STREAM, 1, second)).unwrap(); + + let left = revision_bound(&left); + let right = revision_bound(&right); + assert_eq!(left, right); + assert_ne!(left.backing_ptr_for_test(), right.backing_ptr_for_test()); + assert_ne!(left.schema_ptr_for_test(), right.schema_ptr_for_test()); +} + +#[test] +fn schema_and_values_are_aligned_once_while_provenance_remains_complete() { + let mut state = TestState::new(STREAM, vec![PORT_B, PORT_A]).unwrap(); + state + .apply(observed_update( + STREAM, + 1, + scenarios([ + scenario(9, [binding(PORT_B, [4, 5, 6]), binding(PORT_A, [1, 2, 3])]), + scenario(4, [binding(PORT_A, [9, 8, 7]), binding(PORT_B, [6, 5, 4])]), + scenario(3, [binding(PORT_A, [1, 2, 3]), binding(PORT_B, [4, 5, 6])]), + ]), + )) + .unwrap(); + + let observation = revision_bound(&state); + let first_values: &[Srgb8] = observation.physical_values(0).unwrap(); + let second_values: &[Srgb8] = observation.physical_values(1).unwrap(); + assert_eq!(observation.schema(), &[PORT_A, PORT_B]); + assert_eq!( + first_values, + &[Srgb8::new([1, 2, 3]), Srgb8::new([4, 5, 6])] + ); + assert_eq!( + second_values, + &[Srgb8::new([9, 8, 7]), Srgb8::new([6, 5, 4])] + ); + assert_eq!( + observation.provenance(0), + Some(&[ScenarioId::new(3), ScenarioId::new(9)][..]) + ); + assert_eq!(observation.provenance(1), Some(&[ScenarioId::new(4)][..])); + assert_eq!(observation.physical_values(2), None); + assert_eq!(observation.provenance(2), None); +} + #[test] fn keyed_schema_is_intrinsic_to_revision_bound_observation_identity() { let mut left = TestState::new(STREAM, vec![PORT_A]).unwrap(); @@ -309,26 +389,24 @@ fn keyed_schema_is_intrinsic_to_revision_bound_observation_identity() { .unwrap(); assert_ne!(revision_bound(&left), revision_bound(&right)); + assert_eq!(revision_bound(&left).schema(), &[PORT_A]); + assert_eq!(revision_bound(&right).schema(), &[PORT_B]); assert_eq!( - revision_bound(&left).validate_surface_schema(&[PORT_A, PORT_B]), - Err(ObservationSchemaMismatchV1::new(0, 1, Some(PORT_B), None,)) + revision_bound(&left).physical_values(0), + Some(&[Srgb8::new([7, 8, 9])][..]) ); assert_eq!( - revision_bound(&left).validate_surface_schema(&[PORT_B]), - Err(ObservationSchemaMismatchV1::new( - 0, - 0, - Some(PORT_B), - Some(PORT_A), - )) + revision_bound(&right).physical_values(0), + Some(&[Srgb8::new([7, 8, 9])][..]) ); + let alternate_schema = canonicalize_observation_schema(vec![PORT_B]).unwrap(); let before = left.clone(); assert!(matches!( prepare_observation( &mut left.owner, STREAM, - &[PORT_B], + &alternate_schema, observed_update( STREAM, 1, @@ -342,21 +420,27 @@ fn keyed_schema_is_intrinsic_to_revision_bound_observation_identity() { } #[test] -fn lower_revision_is_rejected_before_malformed_payload_scan_and_never_moves_head() { +fn stream_precedes_full_scenario_admission_which_precedes_revision_checks() { let mut state = TestState::new(STREAM, vec![PORT_A, PORT_B]).unwrap(); state.apply(unknown_update(STREAM, 4, 1)).unwrap(); let before = state.clone(); - let malformed = scenarios([scenario(1, [binding(PORT_A, [1; 3])])]); + let malformed = || scenarios([scenario(1, [binding(PORT_A, [1; 3])])]); + assert_eq!( - state.apply(observed_update(STREAM, 2, malformed.clone())), - Err(ObservationError::RevisionOutOfOrder { - current: Revision::new(4), - incoming: Revision::new(2), + state.apply(observed_update( + ObservationStreamId::new(99), + 2, + malformed(), + )), + Err(ObservationError::StreamMismatch { + expected: STREAM, + actual: ObservationStreamId::new(99), }) ); assert_eq!(state, before); + assert_eq!( - state.apply(observed_update(STREAM, 5, malformed)), + state.apply(observed_update(STREAM, 2, malformed())), Err(ObservationError::MissingSurfaceInputBinding { scenario: ScenarioId::new(1), input: PORT_B, @@ -364,12 +448,32 @@ fn lower_revision_is_rejected_before_malformed_payload_scan_and_never_moves_head ); assert_eq!(state, before); - let corrected = scenarios([scenario( - 1, - [binding(PORT_A, [1; 3]), binding(PORT_B, [2; 3])], - )]); assert_eq!( - state.apply(observed_update(STREAM, 5, corrected)), + state.apply(observed_update(STREAM, 4, malformed())), + Err(ObservationError::MissingSurfaceInputBinding { + scenario: ScenarioId::new(1), + input: PORT_B, + }) + ); + assert_eq!(state, before); + + let valid = || { + scenarios([scenario( + 1, + [binding(PORT_A, [1; 3]), binding(PORT_B, [2; 3])], + )]) + }; + assert_eq!( + state.apply(observed_update(STREAM, 2, valid())), + Err(ObservationError::RevisionOutOfOrder { + current: Revision::new(4), + incoming: Revision::new(2), + }) + ); + assert_eq!(state, before); + + assert_eq!( + state.apply(observed_update(STREAM, 5, valid())), Ok(UpdateDisposition::Applied) ); assert!(state.current_observation().is_some()); @@ -427,13 +531,15 @@ fn observed_to_unknown_replaces_raw_payload_instead_of_duplicating_it() { )) .unwrap(); let old_revision = revision_bound(&state).revision(); - let old_bindings = revision_bound(&state).set().cases()[0].bindings().to_vec(); + let old_schema = revision_bound(&state).schema().to_vec(); + let old_values = revision_bound(&state).physical_values(0).unwrap().to_vec(); state.apply(unknown_update(STREAM, 2, 9)).unwrap(); assert!(state.current_observation().is_none()); assert!(matches!(state.head(), ObservationHeadViewV1::Unknown(_))); assert_eq!(old_revision, Revision::new(1)); - assert_eq!(old_bindings, vec![binding(PORT_A, [3, 4, 5])]); + assert_eq!(old_schema, vec![PORT_A]); + assert_eq!(old_values, vec![Srgb8::new([3, 4, 5])]); } #[test] @@ -459,6 +565,36 @@ fn same_revision_exact_payload_is_idempotent_and_conflict_is_rejected() { assert_eq!(state, before); } +#[test] +fn same_revision_permuted_replay_is_idempotent_without_replacing_backing() { + let first = scenarios([ + scenario(9, [binding(PORT_B, [4, 5, 6]), binding(PORT_A, [1, 2, 3])]), + scenario(4, [binding(PORT_A, [9, 8, 7]), binding(PORT_B, [6, 5, 4])]), + scenario(3, [binding(PORT_A, [1, 2, 3]), binding(PORT_B, [4, 5, 6])]), + ]); + let replay = scenarios([ + scenario(3, [binding(PORT_B, [4, 5, 6]), binding(PORT_A, [1, 2, 3])]), + scenario(9, [binding(PORT_A, [1, 2, 3]), binding(PORT_B, [4, 5, 6])]), + scenario(4, [binding(PORT_B, [6, 5, 4]), binding(PORT_A, [9, 8, 7])]), + ]); + let mut state = TestState::new(STREAM, vec![PORT_B, PORT_A]).unwrap(); + assert_eq!( + state.apply(observed_update(STREAM, 7, first)), + Ok(UpdateDisposition::Applied) + ); + let before = state.clone(); + let backing_ptr = revision_bound(&state).backing_ptr_for_test(); + let schema_ptr = revision_bound(&state).schema_ptr_for_test(); + + assert_eq!( + state.apply(observed_update(STREAM, 7, replay)), + Ok(UpdateDisposition::Idempotent) + ); + assert_eq!(state, before); + assert_eq!(revision_bound(&state).backing_ptr_for_test(), backing_ptr); + assert_eq!(revision_bound(&state).schema_ptr_for_test(), schema_ptr); +} + #[test] fn lower_revision_and_foreign_stream_are_atomic_rejections() { let mut state = TestState::new(STREAM, vec![PORT_A]).unwrap(); @@ -532,7 +668,18 @@ fn two_streams_have_independent_watermarks_and_same_physics() { left.apply(observed_update(STREAM, 5, set.clone())).unwrap(); right.apply(observed_update(other, 1, set)).unwrap(); - assert_eq!(revision_bound(&left).set(), revision_bound(&right).set()); + assert_eq!( + revision_bound(&left).schema(), + revision_bound(&right).schema() + ); + assert_eq!( + revision_bound(&left).physical_values(0), + revision_bound(&right).physical_values(0) + ); + assert_eq!( + revision_bound(&left).provenance(0), + revision_bound(&right).provenance(0) + ); assert_ne!( revision_bound(&left).stream(), revision_bound(&right).stream() diff --git a/crates/labcolors-core/src/pair.rs b/crates/labcolors-core/src/pair.rs index 83423c50..e19f9348 100644 --- a/crates/labcolors-core/src/pair.rs +++ b/crates/labcolors-core/src/pair.rs @@ -73,27 +73,22 @@ pub(crate) enum PairLabelRequirementV1 { Wcag22(Wcag22CriterionV1), } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, PartialEq)] enum PairSelectionEvidenceV1 { Unconstrained(PointwiseVerifiedSelectionV1), Wcag22(PointwiseVerifiedSelectionV1), } /// Полное fresh evidence одной выбранной Pair-кандидатуры. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, PartialEq)] pub(crate) struct VerifiedPairV1 { evidence: PairSelectionEvidenceV1, + execution: JointExecutionRecordV1, } impl VerifiedPairV1 { fn execution(&self) -> &JointExecutionRecordV1 { - let executions = match &self.evidence { - PairSelectionEvidenceV1::Unconstrained(evidence) => evidence.fresh_executions(), - PairSelectionEvidenceV1::Wcag22(evidence) => evidence.fresh_executions(), - }; - executions.first().unwrap_or_else(|| { - unreachable!("one selected Pair tuple over one case has one execution") - }) + &self.execution } pub(crate) fn ordinal(&self) -> CandidateOrdinalV1 { @@ -120,7 +115,7 @@ impl VerifiedPairV1 { } } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, PartialEq)] pub(crate) enum PairLoweringErrorV1 { Candidate(CandidateSetErrorV1), Program(JointProgramErrorV1), @@ -131,6 +126,7 @@ pub(crate) enum PairLoweringErrorV1 { WcagInfeasible(Box>), ExactRecheck(PointwiseSelectedRecheckErrorV1), WcagRecheck(PointwiseSelectedRecheckErrorV1), + InternalInvariant, } /// Материализовать fill occurrence без role-specific эвристики. @@ -196,10 +192,17 @@ pub(crate) fn select_label_candidates( }; let verified = feasible .select(policy) + .map_err(|failure| PairLoweringErrorV1::Policy(failure.reason()))? .recheck() .map_err(PairLoweringErrorV1::ExactRecheck)?; + let execution = verified + .fresh_executions() + .first() + .copied() + .ok_or(PairLoweringErrorV1::InternalInvariant)?; Ok(VerifiedPairV1 { evidence: PairSelectionEvidenceV1::Unconstrained(verified), + execution, }) } PairLabelRequirementV1::Wcag22(criterion) => { @@ -227,10 +230,17 @@ pub(crate) fn select_label_candidates( }; let verified = feasible .select(policy) + .map_err(|failure| PairLoweringErrorV1::Policy(failure.reason()))? .recheck() .map_err(PairLoweringErrorV1::WcagRecheck)?; + let execution = verified + .fresh_executions() + .first() + .copied() + .ok_or(PairLoweringErrorV1::InternalInvariant)?; Ok(VerifiedPairV1 { evidence: PairSelectionEvidenceV1::Wcag22(verified), + execution, }) } } diff --git a/crates/labcolors-core/src/point_support.rs b/crates/labcolors-core/src/point_support.rs index 15116897..ce49f64f 100644 --- a/crates/labcolors-core/src/point_support.rs +++ b/crates/labcolors-core/src/point_support.rs @@ -26,7 +26,10 @@ use crate::numerics::{ NumericalEvidenceClassV2, NumericalFallbackStatusV1, NumericalProofIdV2, NumericalSiteIdV2, StableNumericalOutcomeV2, numerical_registry_v2, }; -use crate::observation::{ObservationSchemaMismatchV1, RevisionBoundObservationV1, ScenarioId}; +use crate::observation::{ + CanonicalObservationSchemaV1, ObservationError, ObservationSchemaMismatchV1, + RevisionBoundObservationV1, ScenarioId, canonicalize_observation_schema, +}; use crate::session::SessionObservationBindingPermitV1; use crate::wcag22::{Wcag22CriterionV1, Wcag22MeasurementV1, measure_wcag22_srgb8}; @@ -139,18 +142,18 @@ pub(crate) enum PointSupportCompileErrorV1 { NumericalRegistryInvariant, } -/// Private compiler output for one role table. It owns its canonical surface -/// schema, but preserves declared occurrence order because that order is the -/// client-owned packed ordinal and witness order. Prebound surface indices are -/// only a cache: runtime keyed bindings remain truth and every cached position -/// is checked against its exact port before use. +/// Private compiler output for one role table. It owns the canonical shared +/// surface schema, but preserves declared occurrence order because that order +/// is the client-owned packed ordinal and witness order. Runtime observations +/// share this exact schema backing, so prebound positions require no keyed +/// lookup or per-case schema scan. #[derive(Debug, PartialEq, Eq)] #[cfg_attr(test, derive(Clone))] pub(crate) struct CompiledPointSupportRecheckV1 { physical_program: PhysicalProgramIdentityV1, composition_profile: CompositionProfileV1, occurrences: Vec, - surface_schema: Vec, + surface_schema: CanonicalObservationSchemaV1, surface_indices: Vec, baselines: Vec>, } @@ -233,6 +236,13 @@ impl CompiledPointSupportRecheckV1 { .map_err(|_| PointSupportCompileErrorV1::SurfaceSchemaInvariant)?; surface_indices.push(index); } + let surface_schema = canonicalize_observation_schema(surface_schema).map_err(|error| { + if matches!(error, ObservationError::ResourceExhausted) { + PointSupportCompileErrorV1::ResourceExhausted + } else { + PointSupportCompileErrorV1::SurfaceSchemaInvariant + } + })?; let mut baselines = Vec::new(); baselines @@ -297,7 +307,7 @@ impl CompiledPointSupportRecheckV1 { } pub(crate) fn surface_schema(&self) -> &[SurfaceInputPortId] { - &self.surface_schema + self.surface_schema.as_slice() } pub(crate) fn into_session_recheck(self) -> BoundPointSupportRecheckV1 { @@ -326,7 +336,7 @@ pub(crate) struct BoundPointSupportRecheckV1 { physical_program: PhysicalProgramIdentityV1, composition_profile: CompositionProfileV1, occurrences: Vec, - surface_schema: Vec, + surface_schema: CanonicalObservationSchemaV1, surface_indices: Vec, baselines: Vec>, } @@ -336,7 +346,7 @@ impl BoundPointSupportRecheckV1 { self.composition_profile } - pub(crate) fn surface_schema(&self) -> &[SurfaceInputPortId] { + pub(crate) const fn observation_schema(&self) -> &CanonicalObservationSchemaV1 { &self.surface_schema } @@ -787,9 +797,12 @@ fn evaluate_bound_point_support( { return Err(PointSupportEvaluationErrorV1::CompiledPlanInvariant); } - observation - .validate_surface_schema(&plan.surface_schema) - .map_err(PointSupportEvaluationErrorV1::ObservationSchemaMismatch)?; + if !observation.shares_schema_backing_with(&plan.surface_schema) { + observation + .validate_surface_schema(plan.surface_schema.as_slice()) + .map_err(PointSupportEvaluationErrorV1::ObservationSchemaMismatch)?; + return Err(PointSupportEvaluationErrorV1::CompiledPlanInvariant); + } let cell_count = observation .physical_case_count() @@ -807,7 +820,10 @@ fn evaluate_bound_point_support( let mut first_required_failure_index = None; let mut first_stability_failure_index = None; - for (case_index, case) in observation.set().cases().iter().enumerate() { + for case_index in 0..observation.physical_case_count() { + let values = observation + .physical_values(case_index) + .ok_or(PointSupportEvaluationErrorV1::CompiledPlanInvariant)?; for (occurrence_index, requirement) in plan.occurrences.iter().enumerate() { let surface_index = *plan .surface_indices @@ -817,7 +833,7 @@ fn evaluate_bound_point_support( .baselines .get(occurrence_index) .ok_or(PointSupportEvaluationErrorV1::CompiledPlanInvariant)?; - let binding = case.bindings().get(surface_index).ok_or( + let backdrop = values.get(surface_index).copied().ok_or( PointSupportEvaluationErrorV1::ObservationSchemaMismatch( ObservationSchemaMismatchV1::new( case_index, @@ -827,17 +843,6 @@ fn evaluate_bound_point_support( ), ), )?; - if binding.port() != requirement.surface { - return Err(PointSupportEvaluationErrorV1::ObservationSchemaMismatch( - ObservationSchemaMismatchV1::new( - case_index, - surface_index, - Some(requirement.surface), - Some(binding.port()), - ), - )); - } - let backdrop = binding.value(); let physical = PointOpacityOverSurfaceV1::evaluate_admitted( requirement.paint.source().bytes(), requirement.paint.opacity(), diff --git a/crates/labcolors-core/src/point_support_tests.rs b/crates/labcolors-core/src/point_support_tests.rs index b377db60..5a0d042e 100644 --- a/crates/labcolors-core/src/point_support_tests.rs +++ b/crates/labcolors-core/src/point_support_tests.rs @@ -142,6 +142,119 @@ fn multi_paint_declared_order_and_direct_provenance_are_preserved() { assert_eq!(cells[1].provenance(), &[ScenarioId::new(9)]); } +#[test] +fn duplicate_raw_scenarios_share_one_physical_case_without_cartesian_expansion() { + let requirements = compiled(vec![ + occurrence( + OCCURRENCE_A, + SURFACE_A, + paint(PAINT_A, [0; 3], 0.5), + Some([128; 3]), + PointSupportCriterionRequirementV1::NotRequested, + PointSupportStabilityPolicyV1::Disabled, + ), + occurrence( + OCCURRENCE_B, + SURFACE_B, + paint(PAINT_B, [255; 3], 0.5), + Some([128; 3]), + PointSupportCriterionRequirementV1::NotRequested, + PointSupportStabilityPolicyV1::Disabled, + ), + ]); + let mut session = PointSupportSessionV1::new(STREAM, requirements); + + crate::composition::reset_source_over_evaluation_count(); + let PointSupportSessionStateV1::Failed { cause, previous } = session + .update(observed_update( + 1, + [ + // Same complete physical tuple, deliberately repeated with + // non-canonical IDs and binding order. + (90, vec![(SURFACE_B, [0; 3]), (SURFACE_A, [255; 3])]), + (10, vec![(SURFACE_A, [255; 3]), (SURFACE_B, [0; 3])]), + // A second anti-correlated tuple must remain one whole case; + // it must not be crossed with either value from the first. + (50, vec![(SURFACE_B, [255; 3]), (SURFACE_A, [0; 3])]), + ], + )) + .unwrap() + else { + panic!("the second physical case violates both required exact identities"); + }; + assert!(previous.is_none()); + + let report = cause.report(); + assert_eq!(report.observation().physical_case_count(), 2); + assert_eq!( + report.observation().physical_values(0), + Some(&[Srgb8::new([0; 3]), Srgb8::new([255; 3])][..]) + ); + assert_eq!( + report.observation().physical_values(1), + Some(&[Srgb8::new([255; 3]), Srgb8::new([0; 3])][..]) + ); + assert_eq!( + report.observation().provenance(0), + Some(&[ScenarioId::new(50)][..]) + ); + assert_eq!( + report.observation().provenance(1), + Some(&[ScenarioId::new(10), ScenarioId::new(90)][..]) + ); + + let cells: Vec<_> = report.cells().collect(); + assert_eq!( + cells.len(), + 4, + "two cases times two occurrences, not six raw cells" + ); + assert_eq!( + crate::composition::source_over_evaluation_count(), + 4, + "compose exactly once per (unique physical case, occurrence)" + ); + + assert_eq!(cells[0].case_index(), 0); + assert_eq!(cells[0].occurrence(), OCCURRENCE_A); + assert_eq!(cells[0].composition().backdrop_rgb(), [0; 3]); + assert_eq!(cells[0].provenance(), &[ScenarioId::new(50)]); + assert!(matches!( + cells[0].exact(), + PointSupportExactAssessmentV1::RequiredFailure(_) + )); + assert_eq!(cells[1].case_index(), 0); + assert_eq!(cells[1].occurrence(), OCCURRENCE_B); + assert_eq!(cells[1].composition().backdrop_rgb(), [255; 3]); + assert_eq!(cells[1].provenance(), &[ScenarioId::new(50)]); + assert!(matches!( + cells[1].exact(), + PointSupportExactAssessmentV1::RequiredFailure(_) + )); + + for (cell, occurrence, backdrop) in [ + (cells[2], OCCURRENCE_A, [255; 3]), + (cells[3], OCCURRENCE_B, [0; 3]), + ] { + assert_eq!(cell.case_index(), 1); + assert_eq!(cell.occurrence(), occurrence); + assert_eq!(cell.composition().backdrop_rgb(), backdrop); + assert_eq!( + cell.provenance(), + &[ScenarioId::new(10), ScenarioId::new(90)] + ); + assert!(matches!( + cell.exact(), + PointSupportExactAssessmentV1::RequiredPass(_) + )); + } + assert_eq!( + report.exact_aggregate(), + PointSupportExactAggregateV1::RequiredFailure, + "one unique violating case fails the whole recheck" + ); +} + #[test] fn exact_wcag_and_stability_are_independent_axes_and_baseline_binds_once() { let drop_all = PointSupportDropFractionV1::try_from_basis_points(10_000).unwrap(); diff --git a/crates/labcolors-core/src/session.rs b/crates/labcolors-core/src/session.rs index e10d8003..35067274 100644 --- a/crates/labcolors-core/src/session.rs +++ b/crates/labcolors-core/src/session.rs @@ -1,8 +1,8 @@ //! Single lifecycle and observation owner for private F2/C8d full support. //! -//! The closed state below owns the one current raw payload through either its -//! revision-bound report or its current `Unknown`. A separate raw head does not -//! exist; [`ObservationHeadViewV1`] is derived by borrow. At most one previous +//! The Session owns one concrete raw head separately from its evaluator +//! lifecycle. An observed raw head and its report share the same immutable +//! observation backing; `Unknown` owns no evidence. At most one previous //! verified report is retained and no transition builds a history chain. use std::mem; @@ -39,16 +39,12 @@ impl SessionObservationBindingPermitV1 { #[derive(Debug, PartialEq)] #[cfg_attr(test, derive(Clone))] pub(crate) enum PointSupportSessionStateV1 { - /// Initial state or a current Unknown without any previous verified report. - Waiting { - current_unknown: Option, - }, + Waiting, Ready { current: VerifiedPointSupportV1, }, Stale { previous: VerifiedPointSupportV1, - current_unknown: RevisionBoundUnknownV1, }, Failed { cause: PointSupportViolationV1, @@ -59,7 +55,7 @@ pub(crate) enum PointSupportSessionStateV1 { impl PointSupportSessionStateV1 { pub(crate) fn last_verified(&self) -> Option<&VerifiedPointSupportV1> { match self { - Self::Waiting { .. } => None, + Self::Waiting => None, Self::Ready { current } => Some(current), Self::Stale { previous, .. } => Some(previous), Self::Failed { previous, .. } => previous.as_ref(), @@ -67,25 +63,25 @@ impl PointSupportSessionStateV1 { } } -impl ObservationOwnerV1 for PointSupportSessionStateV1 { +#[derive(Debug, Clone, PartialEq, Eq)] +enum SessionObservationHeadV1 { + Empty, + Unknown(RevisionBoundUnknownV1), + Observed(crate::observation::RevisionBoundObservationV1), +} + +impl SessionObservationHeadV1 { + fn view(&self) -> ObservationHeadViewV1<'_> { + self.observation_head() + } +} + +impl ObservationOwnerV1 for SessionObservationHeadV1 { fn observation_head(&self) -> ObservationHeadViewV1<'_> { match self { - Self::Waiting { - current_unknown: None, - } => ObservationHeadViewV1::Empty, - Self::Waiting { - current_unknown: Some(unknown), - } - | Self::Stale { - current_unknown: unknown, - .. - } => ObservationHeadViewV1::Unknown(unknown), - Self::Ready { current } => { - ObservationHeadViewV1::Observed(current.report().observation()) - } - Self::Failed { cause, .. } => { - ObservationHeadViewV1::Observed(cause.report().observation()) - } + Self::Empty => ObservationHeadViewV1::Empty, + Self::Unknown(unknown) => ObservationHeadViewV1::Unknown(unknown), + Self::Observed(observation) => ObservationHeadViewV1::Observed(observation), } } } @@ -103,9 +99,12 @@ pub(crate) enum PointSupportSessionUpdateErrorV1 { pub(crate) struct PointSupportSessionV1 { stream: ObservationStreamId, recheck: BoundPointSupportRecheckV1, + raw_head: SessionObservationHeadV1, state: PointSupportSessionStateV1, #[cfg(test)] force_resource_failure: bool, + #[cfg(test)] + force_evaluator_failure: bool, } impl PointSupportSessionV1 { @@ -118,11 +117,12 @@ impl PointSupportSessionV1 { Self { stream, recheck: compiled.into_session_recheck(), - state: PointSupportSessionStateV1::Waiting { - current_unknown: None, - }, + raw_head: SessionObservationHeadV1::Empty, + state: PointSupportSessionStateV1::Waiting, #[cfg(test)] force_resource_failure: false, + #[cfg(test)] + force_evaluator_failure: false, } } @@ -131,7 +131,7 @@ impl PointSupportSessionV1 { } pub(crate) fn raw_head(&self) -> ObservationHeadViewV1<'_> { - self.state.observation_head() + self.raw_head.view() } pub(crate) const fn composition_profile(&self) -> CompositionProfileV1 { @@ -143,6 +143,11 @@ impl PointSupportSessionV1 { self.force_resource_failure = true; } + #[cfg(test)] + pub(crate) fn force_next_evaluator_failure(&mut self) { + self.force_evaluator_failure = true; + } + /// One transaction: prepare/canonicalize without mutation, transfer the /// exact observation under a Session-only permit to the consuming /// evaluator, then replace the closed owner with infallible moves only. @@ -151,28 +156,27 @@ impl PointSupportSessionV1 { update: ObservationUpdateInput, ) -> Result<&PointSupportSessionStateV1, PointSupportSessionUpdateErrorV1> { let prepared = prepare_observation( - &mut self.state, + &mut self.raw_head, self.stream, - self.recheck.surface_schema(), + self.recheck.observation_schema(), update, ) .map_err(PointSupportSessionUpdateErrorV1::Observation)?; match prepared { - PreparedObservationUpdateV1::Idempotent(prepared) => Ok(prepared.into_owner()), + PreparedObservationUpdateV1::Idempotent(prepared) => { + let _raw_head = prepared.into_owner(); + Ok(&self.state) + } PreparedObservationUpdateV1::Unknown(prepared) => { - let (state, unknown) = prepared.into_parts(); - let previous = take_last_verified(state); - *state = match previous { - Some(previous) => PointSupportSessionStateV1::Stale { - previous, - current_unknown: unknown, - }, - None => PointSupportSessionStateV1::Waiting { - current_unknown: Some(unknown), - }, + let (raw_head, unknown) = prepared.into_parts(); + let next_state = match take_last_verified(&mut self.state) { + Some(previous) => PointSupportSessionStateV1::Stale { previous }, + None => PointSupportSessionStateV1::Waiting, }; - Ok(state) + *raw_head = SessionObservationHeadV1::Unknown(unknown); + self.state = next_state; + Ok(&self.state) } PreparedObservationUpdateV1::Observed(prepared) => { #[cfg(test)] @@ -180,16 +184,30 @@ impl PointSupportSessionV1 { return Err(PointSupportSessionUpdateErrorV1::ResourceExhausted); } - // Evaluation consumes the exact admitted observation and can - // return only an already revision-bound decision. The mutable - // owner is retained unchanged until that fallible work succeeds. - let (state, observation) = prepared.into_parts(); - let decision = self - .recheck - .evaluate(observation, SessionObservationBindingPermitV1::mint()) - .map_err(map_evaluation_error)?; - let previous = take_last_verified(state); - *state = match decision { + // The raw head clone shares the exact immutable payload. Both + // raw head and lifecycle remain unchanged until the fallible + // recheck has produced a complete revision-bound decision. + let (raw_head, observation) = prepared.into_parts(); + let next_raw_head = SessionObservationHeadV1::Observed(observation.clone()); + let evaluation = { + #[cfg(test)] + { + if mem::take(&mut self.force_evaluator_failure) { + Err(PointSupportEvaluationErrorV1::CompiledPlanInvariant) + } else { + self.recheck + .evaluate(observation, SessionObservationBindingPermitV1::mint()) + } + } + #[cfg(not(test))] + { + self.recheck + .evaluate(observation, SessionObservationBindingPermitV1::mint()) + } + }; + let decision = evaluation.map_err(map_evaluation_error)?; + let previous = take_last_verified(&mut self.state); + let next_state = match decision { PointSupportDecisionV1::Verified(current) => { PointSupportSessionStateV1::Ready { current } } @@ -197,7 +215,9 @@ impl PointSupportSessionV1 { PointSupportSessionStateV1::Failed { cause, previous } } }; - Ok(state) + *raw_head = next_raw_head; + self.state = next_state; + Ok(&self.state) } } } @@ -221,13 +241,8 @@ fn map_evaluation_error(error: PointSupportEvaluationErrorV1) -> PointSupportSes /// Move exactly one retained verified witness out of the old closed owner. fn take_last_verified(state: &mut PointSupportSessionStateV1) -> Option { - match mem::replace( - state, - PointSupportSessionStateV1::Waiting { - current_unknown: None, - }, - ) { - PointSupportSessionStateV1::Waiting { .. } => None, + match mem::replace(state, PointSupportSessionStateV1::Waiting) { + PointSupportSessionStateV1::Waiting => None, PointSupportSessionStateV1::Ready { current } => Some(current), PointSupportSessionStateV1::Stale { previous, .. } => Some(previous), PointSupportSessionStateV1::Failed { previous, .. } => previous, @@ -245,7 +260,7 @@ mod structural_tests { ObservationSchemaMismatchV1, ObservationStreamId, ObservationUpdateInput, ObservedScenarioSetInput, PreparedObservationUpdateV1, Revision, RevisionBoundObservationV1, ScenarioId, ScenarioInput, SurfaceInputBinding, - prepare_observation, + canonicalize_observation_schema, prepare_observation, }; use crate::point_support::{ CompiledPointSupportRecheckV1, PointSupportCriterionRequirementV1, @@ -267,10 +282,11 @@ mod structural_tests { fn wrong_schema_observation() -> RevisionBoundObservationV1 { let mut owner = EmptyOwner; + let schema = canonicalize_observation_schema(vec![WRONG_SURFACE]).unwrap(); let prepared = prepare_observation( &mut owner, STREAM, - &[WRONG_SURFACE], + &schema, ObservationUpdateInput { stream: STREAM, revision: Revision::new(1), @@ -295,10 +311,11 @@ mod structural_tests { fn narrow_schema_observation() -> RevisionBoundObservationV1 { let mut owner = EmptyOwner; + let schema = canonicalize_observation_schema(vec![REQUIRED_SURFACE]).unwrap(); let prepared = prepare_observation( &mut owner, STREAM, - &[REQUIRED_SURFACE], + &schema, ObservationUpdateInput { stream: STREAM, revision: Revision::new(1), diff --git a/crates/labcolors-core/src/session_tests.rs b/crates/labcolors-core/src/session_tests.rs index 6574981f..a1be65b6 100644 --- a/crates/labcolors-core/src/session_tests.rs +++ b/crates/labcolors-core/src/session_tests.rs @@ -5,8 +5,8 @@ use crate::appearance::{EncodedPointPaintV1, OccurrenceId, PaintId, SurfaceInput use crate::composition::{AdmittedOpacityV1, CompositionProfileV1}; use crate::observation::{ ObservationError, ObservationHeadViewV1, ObservationPayloadInput, ObservationStreamId, - ObservationUpdateInput, ObservedScenarioSetInput, Revision, ScenarioId, ScenarioInput, - SurfaceInputBinding, UnknownReasonId, + ObservationUpdateInput, ObservedScenarioSetInput, Revision, RevisionBoundObservationV1, + ScenarioId, ScenarioInput, SurfaceInputBinding, UnknownReasonId, }; use crate::point_support::{ CompiledPointSupportRecheckV1, PointSupportCriterionRequirementV1, @@ -92,6 +92,75 @@ fn verified_revision(state: &PointSupportSessionStateV1) -> Option { .map(|verified| verified.report().observation().revision()) } +fn raw_observed(session: &PointSupportSessionV1) -> &RevisionBoundObservationV1 { + let ObservationHeadViewV1::Observed(observation) = session.raw_head() else { + panic!("the concrete raw head must be Observed"); + }; + observation +} + +fn assert_shared_observation_backing( + raw: &RevisionBoundObservationV1, + report: &RevisionBoundObservationV1, +) { + assert_eq!(raw, report); + assert_eq!( + raw.backing_ptr_for_test(), + report.backing_ptr_for_test(), + "raw head and report must share one immutable observation backing", + ); + assert_eq!( + raw.schema().as_ptr(), + report.schema().as_ptr(), + "raw head and report must share immutable schema backing", + ); + assert_eq!(raw.physical_case_count(), report.physical_case_count()); + for case_index in 0..raw.physical_case_count() { + let raw_values = raw + .physical_values(case_index) + .expect("raw case must exist"); + let report_values = report + .physical_values(case_index) + .expect("report case must exist"); + assert_eq!(raw_values, report_values); + assert_eq!( + raw_values.as_ptr(), + report_values.as_ptr(), + "raw head and report must share immutable value backing", + ); + + let raw_provenance = raw + .provenance(case_index) + .expect("raw provenance must exist"); + let report_provenance = report + .provenance(case_index) + .expect("report provenance must exist"); + assert_eq!(raw_provenance, report_provenance); + assert_eq!( + raw_provenance.as_ptr(), + report_provenance.as_ptr(), + "raw head and report must share immutable provenance backing", + ); + } +} + +fn observation_backing_signature( + observation: &RevisionBoundObservationV1, +) -> (*const SurfaceInputPortId, *const Srgb8, *const ScenarioId) { + assert_eq!(observation.physical_case_count(), 1); + ( + observation.schema().as_ptr(), + observation + .physical_values(0) + .expect("fixture must have one physical case") + .as_ptr(), + observation + .provenance(0) + .expect("fixture must have provenance") + .as_ptr(), + ) +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum StateKind { Waiting, @@ -102,7 +171,7 @@ enum StateKind { fn state_kind(state: &PointSupportSessionStateV1) -> StateKind { match state { - PointSupportSessionStateV1::Waiting { .. } => StateKind::Waiting, + PointSupportSessionStateV1::Waiting => StateKind::Waiting, PointSupportSessionStateV1::Ready { .. } => StateKind::Ready, PointSupportSessionStateV1::Stale { .. } => StateKind::Stale, PointSupportSessionStateV1::Failed { .. } => StateKind::Failed, @@ -114,9 +183,7 @@ fn construction_uses_only_the_compiled_schema_and_profile() { let session = session(); assert!(matches!( session.state(), - PointSupportSessionStateV1::Waiting { - current_unknown: None, - } + PointSupportSessionStateV1::Waiting )); assert_eq!(session.raw_head(), ObservationHeadViewV1::Empty); assert_eq!( @@ -128,9 +195,8 @@ fn construction_uses_only_the_compiled_schema_and_profile() { #[test] fn ready_violation_unknown_preserves_exactly_one_verified_witness() { let mut session = session(); - let PointSupportSessionStateV1::Ready { current } = - session.update(observed_update(1, [255; 3])).unwrap() - else { + session.update(observed_update(1, [255; 3])).unwrap(); + let PointSupportSessionStateV1::Ready { current } = session.state() else { panic!("white backdrop must verify #808080 target"); }; assert_eq!(current.report().observation().revision(), Revision::new(1)); @@ -138,10 +204,10 @@ fn ready_violation_unknown_preserves_exactly_one_verified_witness() { current.report().cells().next().unwrap().provenance(), &[ScenarioId::new(1)] ); + assert_shared_observation_backing(raw_observed(&session), current.report().observation()); - let PointSupportSessionStateV1::Failed { cause, previous } = - session.update(observed_update(2, [0; 3])).unwrap() - else { + session.update(observed_update(2, [0; 3])).unwrap(); + let PointSupportSessionStateV1::Failed { cause, previous } = session.state() else { panic!("black backdrop must violate #808080 target"); }; assert_eq!(cause.report().observation().revision(), Revision::new(2)); @@ -149,23 +215,19 @@ fn ready_violation_unknown_preserves_exactly_one_verified_witness() { previous.as_ref().unwrap().report().observation().revision(), Revision::new(1) ); + assert_shared_observation_backing(raw_observed(&session), cause.report().observation()); - let PointSupportSessionStateV1::Stale { - previous, - current_unknown, - } = session.update(unknown_update(3, 9)).unwrap() - else { + session.update(unknown_update(3, 9)).unwrap(); + let PointSupportSessionStateV1::Stale { previous } = session.state() else { panic!("unknown after a verified result must become Stale"); }; - let expected_unknown = *current_unknown; assert_eq!(previous.report().observation().revision(), Revision::new(1)); + let ObservationHeadViewV1::Unknown(current_unknown) = session.raw_head() else { + panic!("lifecycle state must not own the current raw Unknown"); + }; assert_eq!(current_unknown.stream(), STREAM); assert_eq!(current_unknown.revision(), Revision::new(3)); assert_eq!(current_unknown.reason(), UnknownReasonId::new(9)); - assert_eq!( - session.raw_head(), - ObservationHeadViewV1::Unknown(&expected_unknown) - ); } #[test] @@ -175,20 +237,17 @@ fn violation_without_prior_then_unknown_is_waiting() { session.update(observed_update(1, [0; 3])).unwrap(), PointSupportSessionStateV1::Failed { previous: None, .. } )); - let PointSupportSessionStateV1::Waiting { - current_unknown: Some(current_unknown), - } = session.update(unknown_update(2, 1)).unwrap() - else { - panic!("unknown without a verified result must be retained by Waiting"); + session.update(unknown_update(2, 1)).unwrap(); + assert!(matches!( + session.state(), + PointSupportSessionStateV1::Waiting + )); + let ObservationHeadViewV1::Unknown(current_unknown) = session.raw_head() else { + panic!("Waiting must not embed the current raw Unknown"); }; - let expected_unknown = *current_unknown; assert_eq!(current_unknown.stream(), STREAM); assert_eq!(current_unknown.revision(), Revision::new(2)); assert_eq!(current_unknown.reason(), UnknownReasonId::new(1)); - assert_eq!( - session.raw_head(), - ObservationHeadViewV1::Unknown(&expected_unknown) - ); assert_eq!(verified_revision(session.state()), None); } @@ -216,23 +275,114 @@ fn stale_and_failed_transitions_move_one_previous_without_history() { assert_eq!(session.raw_head().revision(), Some(Revision::new(5))); } +#[test] +fn observation_clone_and_exact_replay_reuse_the_same_backing_without_allocation() { + let mut session = session(); + session.update(observed_update(1, [255; 3])).unwrap(); + let raw = raw_observed(&session); + let raw_signature = observation_backing_signature(raw); + let PointSupportSessionStateV1::Ready { current } = session.state() else { + panic!("white backdrop must verify"); + }; + assert_shared_observation_backing(raw, current.report().observation()); + + let (snapshot, clone_allocations) = crate::test_support::measured_allocations(|| raw.clone()); + assert_eq!(clone_allocations, 0); + assert_eq!(snapshot, *raw); + assert_eq!(observation_backing_signature(&snapshot), raw_signature); + + crate::composition::reset_source_over_evaluation_count(); + let exact_replay = observed_update(1, [255; 3]); + let (result, replay_allocations) = + crate::test_support::measured_allocations(|| session.update(exact_replay).map(|_| ())); + assert_eq!(result, Ok(())); + assert_eq!(replay_allocations, 0); + assert_eq!(crate::composition::source_over_evaluation_count(), 0); + assert_eq!( + observation_backing_signature(raw_observed(&session)), + raw_signature, + ); + let PointSupportSessionStateV1::Ready { current } = session.state() else { + panic!("exact replay must retain Ready"); + }; + assert_shared_observation_backing(raw_observed(&session), current.report().observation()); +} + +#[test] +fn higher_revision_with_equal_content_rechecks_and_binds_new_evidence() { + let mut session = session(); + crate::composition::reset_source_over_evaluation_count(); + session.update(observed_update(1, [255; 3])).unwrap(); + assert_eq!(crate::composition::source_over_evaluation_count(), 1); + + session.update(observed_update(2, [255; 3])).unwrap(); + assert_eq!(crate::composition::source_over_evaluation_count(), 2); + assert_eq!(session.raw_head().revision(), Some(Revision::new(2))); + let PointSupportSessionStateV1::Ready { current } = session.state() else { + panic!("equal content at a higher revision must produce fresh Ready evidence"); + }; + assert_eq!(current.report().observation().revision(), Revision::new(2)); + assert_shared_observation_backing(raw_observed(&session), current.report().observation()); +} + +#[test] +fn newer_unknown_replaces_only_raw_head_and_retains_one_verified_witness() { + let mut session = session(); + session.update(observed_update(1, [255; 3])).unwrap(); + let verified = raw_observed(&session).clone(); + let verified_signature = observation_backing_signature(&verified); + + session.update(unknown_update(2, 10)).unwrap(); + let PointSupportSessionStateV1::Stale { previous } = session.state() else { + panic!("Unknown after Ready must become Stale"); + }; + assert_eq!(previous.report().observation(), &verified); + assert_eq!( + observation_backing_signature(previous.report().observation()), + verified_signature, + ); + + session.update(unknown_update(3, 11)).unwrap(); + let PointSupportSessionStateV1::Stale { previous } = session.state() else { + panic!("a newer Unknown must remain Stale"); + }; + assert_eq!(previous.report().observation(), &verified); + assert_eq!( + observation_backing_signature(previous.report().observation()), + verified_signature, + ); + let ObservationHeadViewV1::Unknown(raw_unknown) = session.raw_head() else { + panic!("the current Unknown belongs only to the raw head"); + }; + assert_eq!(raw_unknown.revision(), Revision::new(3)); + assert_eq!(raw_unknown.reason(), UnknownReasonId::new(11)); +} + #[test] fn unknown_idempotent_and_rejected_updates_never_evaluate() { let mut session = session(); crate::composition::reset_source_over_evaluation_count(); - let unknown = unknown_update(1, 1); - session.update(unknown.clone()).unwrap(); + session.update(unknown_update(1, 1)).unwrap(); assert_eq!(crate::composition::source_over_evaluation_count(), 0); let before = session.clone(); - session.update(unknown).unwrap(); + let exact_unknown_replay = unknown_update(1, 1); + let (result, allocations) = crate::test_support::measured_allocations(|| { + session.update(exact_unknown_replay).map(|_| ()) + }); + assert!(result.is_ok()); + assert_eq!(allocations, 0); assert_eq!(crate::composition::source_over_evaluation_count(), 0); assert_eq!(session, before); - let update = observed_update(2, [255; 3]); - session.update(update.clone()).unwrap(); + session.update(observed_update(2, [255; 3])).unwrap(); assert_eq!(crate::composition::source_over_evaluation_count(), 1); let before = session.clone(); - session.update(update).unwrap(); + let exact_observed_replay = observed_update(2, [255; 3]); + let (result, allocations) = crate::test_support::measured_allocations(|| { + session.update(exact_observed_replay).map(|_| ()) + }); + assert!(result.is_ok()); + assert_eq!(allocations, 0); assert_eq!(crate::composition::source_over_evaluation_count(), 1); assert_eq!(session, before); @@ -240,38 +390,116 @@ fn unknown_idempotent_and_rejected_updates_never_evaluate() { crate::composition::reset_source_over_evaluation_count(); assert_eq!( session.update(malformed_update(1)), + Err(PointSupportSessionUpdateErrorV1::Observation( + ObservationError::MissingSurfaceInputBinding { + scenario: ScenarioId::new(1), + input: SURFACE, + }, + )), + "malformed payload admission must precede lower-revision comparison", + ); + assert_eq!(crate::composition::source_over_evaluation_count(), 0); + assert_eq!(session, before); + + let before = session.clone(); + assert_eq!( + session.update(malformed_update(2)), + Err(PointSupportSessionUpdateErrorV1::Observation( + ObservationError::MissingSurfaceInputBinding { + scenario: ScenarioId::new(1), + input: SURFACE, + }, + )), + "malformed payload admission must precede same-revision equality", + ); + assert_eq!(crate::composition::source_over_evaluation_count(), 0); + assert_eq!(session, before); + + let before = session.clone(); + assert_eq!( + session.update(malformed_update(3)), + Err(PointSupportSessionUpdateErrorV1::Observation( + ObservationError::MissingSurfaceInputBinding { + scenario: ScenarioId::new(1), + input: SURFACE, + }, + )), + "malformed payload admission must precede applying a higher revision", + ); + assert_eq!(crate::composition::source_over_evaluation_count(), 0); + assert_eq!(session, before); + + let before = session.clone(); + assert_eq!( + session.update(ObservationUpdateInput { + stream: ObservationStreamId::new(99), + revision: Revision::new(1), + payload: ObservationPayloadInput::Scenarios(ObservedScenarioSetInput { + scenarios: vec![ScenarioInput { + id: ScenarioId::new(1), + bindings: vec![], + }], + }), + }), + Err(PointSupportSessionUpdateErrorV1::Observation( + ObservationError::StreamMismatch { + expected: STREAM, + actual: ObservationStreamId::new(99), + }, + )), + "stream affinity must be checked before parsing foreign payloads", + ); + assert_eq!(crate::composition::source_over_evaluation_count(), 0); + assert_eq!(session, before); + + let before = session.clone(); + let lower_revision = observed_update(1, [255; 3]); + let (result, allocations) = + crate::test_support::measured_allocations(|| session.update(lower_revision).map(|_| ())); + assert_eq!( + result, Err(PointSupportSessionUpdateErrorV1::Observation( ObservationError::RevisionOutOfOrder { current: Revision::new(2), incoming: Revision::new(1), }, - )) + )), ); + assert_eq!(allocations, 0); assert_eq!(crate::composition::source_over_evaluation_count(), 0); assert_eq!(session, before); - for rejected in [ - malformed_update(3), - ObservationUpdateInput { - stream: ObservationStreamId::new(99), - revision: Revision::new(3), - payload: ObservationPayloadInput::Unknown(UnknownReasonId::new(1)), - }, - observed_update(2, [0; 3]), - ] { - let before = session.clone(); - crate::composition::reset_source_over_evaluation_count(); - assert!(session.update(rejected).is_err()); - assert_eq!(crate::composition::source_over_evaluation_count(), 0); - assert_eq!(session, before); - } + let before = session.clone(); + let same_revision_conflict = observed_update(2, [0; 3]); + let (result, allocations) = crate::test_support::measured_allocations(|| { + session.update(same_revision_conflict).map(|_| ()) + }); + assert_eq!( + result, + Err(PointSupportSessionUpdateErrorV1::Observation( + ObservationError::RevisionConflict { + revision: Revision::new(2), + }, + )), + ); + assert_eq!(allocations, 0); + assert_eq!(crate::composition::source_over_evaluation_count(), 0); + assert_eq!(session, before); } #[test] -fn resource_preflight_failure_is_atomic_and_retryable() { +fn synthetic_post_admission_pre_evaluation_failure_is_atomic_and_retryable() { let mut session = session(); session.update(observed_update(1, [255; 3])).unwrap(); + let raw_signature = observation_backing_signature(raw_observed(&session)); + let PointSupportSessionStateV1::Ready { current } = session.state() else { + panic!("white backdrop must verify"); + }; + assert_shared_observation_backing(raw_observed(&session), current.report().observation()); let before = session.clone(); + // This hook fires after observation admission and before evaluation. It + // proves transaction rollback at that Session boundary, not recoverable + // failure of every allocation performed by Rust's global allocator. session.force_next_resource_failure(); crate::composition::reset_source_over_evaluation_count(); assert_eq!( @@ -283,9 +511,57 @@ fn resource_preflight_failure_is_atomic_and_retryable() { assert_eq!(session.state(), before.state()); assert_eq!(session.raw_head(), before.raw_head()); assert_eq!(session.composition_profile(), before.composition_profile()); + assert_eq!( + observation_backing_signature(raw_observed(&session)), + raw_signature, + ); + let PointSupportSessionStateV1::Ready { current } = session.state() else { + panic!("synthetic pre-evaluation failure must retain Ready"); + }; + assert_shared_observation_backing(raw_observed(&session), current.report().observation()); + + session.update(observed_update(2, [255; 3])).unwrap(); + assert_eq!(crate::composition::source_over_evaluation_count(), 1); + assert_eq!(verified_revision(session.state()), Some(Revision::new(2))); + let PointSupportSessionStateV1::Ready { current } = session.state() else { + panic!("retry must commit Ready"); + }; + assert_shared_observation_backing(raw_observed(&session), current.report().observation()); +} + +#[test] +fn evaluator_failure_is_atomic_and_retryable() { + let mut session = session(); + session.update(observed_update(1, [255; 3])).unwrap(); + let raw_signature = observation_backing_signature(raw_observed(&session)); + let before = session.clone(); + + session.force_next_evaluator_failure(); + crate::composition::reset_source_over_evaluation_count(); + assert_eq!( + session.update(observed_update(2, [255; 3])), + Err(PointSupportSessionUpdateErrorV1::InternalInvariant), + ); + assert_eq!(crate::composition::source_over_evaluation_count(), 0); + assert_eq!(session, before); + assert_eq!(session.state(), before.state()); + assert_eq!(session.raw_head(), before.raw_head()); + assert_eq!( + observation_backing_signature(raw_observed(&session)), + raw_signature, + ); + let PointSupportSessionStateV1::Ready { current } = session.state() else { + panic!("evaluator failure must retain Ready"); + }; + assert_shared_observation_backing(raw_observed(&session), current.report().observation()); session.update(observed_update(2, [255; 3])).unwrap(); + assert_eq!(crate::composition::source_over_evaluation_count(), 1); assert_eq!(verified_revision(session.state()), Some(Revision::new(2))); + let PointSupportSessionStateV1::Ready { current } = session.state() else { + panic!("retry after evaluator failure must commit Ready"); + }; + assert_shared_observation_backing(raw_observed(&session), current.report().observation()); } proptest! { diff --git a/scripts/test_point_support_surplus_source_binding.py b/scripts/test_point_support_surplus_source_binding.py index 4949cf53..9e43f10d 100644 --- a/scripts/test_point_support_surplus_source_binding.py +++ b/scripts/test_point_support_surplus_source_binding.py @@ -55,13 +55,13 @@ def test_complete_production_dependency_cone_is_bound(self) -> None: ), ( self.observation_path, - b" &self.cases\n", - b" &[]\n", + b" self.backing.set.values(case_index)\n", + b" None\n", ), ( self.session_path, - b"ObservationHeadViewV1::Observed(current.report().observation())", - b"ObservationHeadViewV1::Empty", + b" Self::Observed(observation) => ObservationHeadViewV1::Observed(observation),\n", + b" Self::Observed(_) => ObservationHeadViewV1::Empty,\n", ), ( self.numerics_path, diff --git a/scripts/verify-package-release.mjs b/scripts/verify-package-release.mjs index d03f1c78..a97bed02 100644 --- a/scripts/verify-package-release.mjs +++ b/scripts/verify-package-release.mjs @@ -335,7 +335,7 @@ async function validatePointSupportEvidence(artifacts, numericalCapabilities) { POINT_SUPPORT_SOURCE_BINDING_EXCLUSIONS, ) || !/^[0-9a-f]{64}$/u.test(proof.source_closure_sha256 ?? "") || - proof.source_negative_controls !== 33 || + proof.source_negative_controls !== 40 || !/^[0-9a-f]{64}$/u.test(proof.proof_payload_sha256 ?? "") || !/^[0-9a-f]{64}$/u.test(proof.verifier_sha256 ?? "") || !Array.isArray(proof.source_files) || diff --git a/scripts/verify_point_support_surplus.py b/scripts/verify_point_support_surplus.py index dd1b6ffc..eb541352 100644 --- a/scripts/verify_point_support_surplus.py +++ b/scripts/verify_point_support_surplus.py @@ -57,7 +57,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 = ( - "51b242a0ff319f34357fbeef89037118515172dcdc088a09b9ff28e0cc38e860" + "06b0017fcfad05f582bae0d9ddc1fe1bb5240ca01fc30795a469c093cf9c1f87" ) EXPECTED_Q55_PROOF_SHA256 = ( "ac59cf89503170c789223b91d775213a19d4e571ef930f2ea609fcd51b14defd" @@ -192,15 +192,22 @@ def verify_source_binding() -> tuple[str, int]: (POINT_SOURCE, b"NumericalSiteIdV2::PointSupportRetainedReferenceSurplusV1;", b"NumericalSiteIdV2::Wcag22Srgb8ContrastV1;"), (POINT_SOURCE, b"let current_distance = reference_distance(current_measurement)?;", b"let current_distance = baseline.distance;"), (POINT_SOURCE, b"Ok(assessment.bind(observation))", b"Ok(assessment.bind_unchecked(observation))"), + (POINT_SOURCE, b" let backdrop = values.get(surface_index).copied().ok_or(\n", b" let backdrop = values.first().copied().ok_or(\n"), + (POINT_SOURCE, b" if !observation.shares_schema_backing_with(&plan.surface_schema) {\n", b" if observation.shares_schema_backing_with(&plan.surface_schema) {\n"), + (POINT_SOURCE, b" _permit: SessionObservationBindingPermitV1,\n", b" _permit: (),\n"), (POINT_SOURCE, b"use crate::wcag22::{Wcag22CriterionV1, Wcag22MeasurementV1, measure_wcag22_srgb8};", b"use crate::wcag22::{Wcag22CriterionV1, Wcag22MeasurementV1, measure_wcag22_srgb8 as canonical_measure_wcag22_srgb8};\nfn measure_wcag22_srgb8(foreground: [u8; 3], background: [u8; 3]) -> Wcag22MeasurementV1 { canonical_measure_wcag22_srgb8(background, foreground) }"), - (OBSERVATION_SOURCE, b" &self.cases\n", b" &[]\n"), + (OBSERVATION_SOURCE, b" self.backing.set.values(case_index)\n", b" None\n"), + (OBSERVATION_SOURCE, b" values.extend(bindings.iter().map(|binding| binding.value));\n", b" values.extend(bindings.iter().map(|_| Srgb8::new([0, 0, 0])));\n"), + (OBSERVATION_SOURCE, b" Rc::ptr_eq(&self.0, &other.0)\n", b" self == other\n"), + (OBSERVATION_SOURCE, b" schema: schema.clone(),\n", b" schema: CanonicalObservationSchemaV1(Rc::from(schema.as_slice())),\n"), (OBSERVATION_SOURCE, b"if expected_input != actual_input", b"if expected_input == actual_input"), (OBSERVATION_SOURCE, b"Some(observation.revision)", b"None"), (OBSERVATION_SOURCE, b"(self.owner, self.observation)", b"unreachable!()"), - (OBSERVATION_SOURCE, b"tuples.push((scenario.bindings, scenario.id));", b"tuples.push((Vec::new(), scenario.id));"), - (SESSION_SOURCE, b"ObservationHeadViewV1::Observed(current.report().observation())", b"ObservationHeadViewV1::Empty"), + (SESSION_SOURCE, b" Self::Observed(observation) => ObservationHeadViewV1::Observed(observation),\n", b" Self::Observed(_) => ObservationHeadViewV1::Empty,\n"), + (SESSION_SOURCE, b" let next_raw_head = SessionObservationHeadV1::Observed(observation.clone());\n", b" let next_raw_head = SessionObservationHeadV1::Empty;\n"), + (SESSION_SOURCE, b" *raw_head = next_raw_head;\n", b" *raw_head = SessionObservationHeadV1::Empty;\n"), (SESSION_SOURCE, b"recheck: compiled.into_session_recheck(),", b"recheck: unreachable!(),"), - (SESSION_SOURCE, b".evaluate(observation, SessionObservationBindingPermitV1::mint())", b".evaluate(observation, SessionObservationBindingPermitV1::bypass())"), + (SESSION_SOURCE, b" #[cfg(not(test))]\n {\n self.recheck\n .evaluate(observation, SessionObservationBindingPermitV1::mint())\n }", b" #[cfg(not(test))]\n {\n self.recheck\n .evaluate(observation, SessionObservationBindingPermitV1::bypass())\n }"), (SESSION_SOURCE, b"PointSupportEvaluationErrorV1::ResourceExhausted => {\n PointSupportSessionUpdateErrorV1::ResourceExhausted", b"PointSupportEvaluationErrorV1::ResourceExhausted => {\n PointSupportSessionUpdateErrorV1::InternalInvariant"), (SESSION_SOURCE, b"PointSupportSessionStateV1::Ready { current } => Some(current),", b"PointSupportSessionStateV1::Ready { .. } => None,"), (NUMERICS_SOURCE, b"proof_ids: [NumericalProofIdV2::PointSupportReferenceSurplusIntegerV1],\n bound_status: Available", b"proof_ids: [NumericalProofIdV2::PointSupportReferenceSurplusIntegerV1],\n bound_status: Unavailable"), From 69e431203360e642d37cbe5ed37c9d42e53d6c56 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Wed, 22 Jul 2026 19:00:39 +0300 Subject: [PATCH 26/58] fix(ci): bind V2a budget to measured artifact - keep allocation tests ownership-preserving without a large Result closure - pin the zero-headroom WASM ratchet to the reproducible V2a artifact --- crates/labcolors-core/src/joint_tests.rs | 23 +++++++++++++---------- packages/colors/bench/wasm.json | 8 ++++---- scripts/check-wasm-size-budget.mjs | 2 +- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/crates/labcolors-core/src/joint_tests.rs b/crates/labcolors-core/src/joint_tests.rs index 025ac076..aef33acc 100644 --- a/crates/labcolors-core/src/joint_tests.rs +++ b/crates/labcolors-core/src/joint_tests.rs @@ -458,11 +458,13 @@ fn foreign_disjoint_and_partially_overlapping_policy_domains_are_typed_errors() .unwrap(); let disjoint_feasible = make_actual(); crate::composition::reset_source_over_evaluation_count(); - let (disjoint, disjoint_allocations) = - crate::test_support::measured_allocations(|| disjoint_feasible.select(disjoint_policy)); - let Err(disjoint_failure) = disjoint else { - panic!("a disjoint policy domain must be rejected"); - }; + let (disjoint_failure, disjoint_allocations) = + crate::test_support::measured_allocations(|| { + match disjoint_feasible.select(disjoint_policy) { + Ok(_) => panic!("a disjoint policy domain must be rejected"), + Err(failure) => failure, + } + }); assert_eq!( disjoint_failure.reason(), SelectionPolicyErrorV1::CandidateDomainMismatch @@ -491,11 +493,12 @@ fn foreign_disjoint_and_partially_overlapping_policy_domains_are_typed_errors() .unwrap(); let partial_feasible = make_actual(); crate::composition::reset_source_over_evaluation_count(); - let (partial, partial_allocations) = - crate::test_support::measured_allocations(|| partial_feasible.select(partial_policy)); - let Err(partial_failure) = partial else { - panic!("a partially overlapping policy domain must be rejected"); - }; + let (partial_failure, partial_allocations) = crate::test_support::measured_allocations(|| { + match partial_feasible.select(partial_policy) { + Ok(_) => panic!("a partially overlapping policy domain must be rejected"), + Err(failure) => failure, + } + }); assert_eq!( partial_failure.reason(), SelectionPolicyErrorV1::CandidateDomainMismatch diff --git a/packages/colors/bench/wasm.json b/packages/colors/bench/wasm.json index 812941ff..ddf14019 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-29875865333", + "source": "github-actions-run-29934462817", "platform": "linux-x64", - "rawBytes": 421398 + "rawBytes": 424971 }, "policy": { - "maxRawBytes": 421398, - "basis": "c8d-revision-bound-point-support", + "maxRawBytes": 424971, + "basis": "v2a-domain-safe-joint-selection", "gzip": "diagnostic-only" } } diff --git a/scripts/check-wasm-size-budget.mjs b/scripts/check-wasm-size-budget.mjs index 11b2d54d..8ceb346b 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 = - "1fc218f1ed02aabe298f43a484bebef3d046269e05eb479a145b86cfde8a7a30"; + "c51c0bd3d62bf3b1d57ea5f9b65da48f0b50621472071eaa37ff8ba145ab1bc7"; const SCHEMA_VERSION = 1; const CANONICAL_ARTIFACT = "packages/colors/pkg/labcolors_bg.wasm"; From 186e45dce7a2fa390a38f6a0f2cf56ef2abb94b1 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Wed, 22 Jul 2026 19:36:58 +0300 Subject: [PATCH 27/58] refactor(core): remove superseded Program runtime Keep only the private Program compiler/lowering payload for the direct sole-Session bridge. Delete the duplicate owner, lifecycle, output materialization, and test evaluator scaffolding. Pin the hard cut with a negative facade gate and refresh the exact point-support source receipt. --- ...rt-reference-surplus-q55-bps-proof-v1.json | 2 +- crates/labcolors-core/src/constraints/mod.rs | 129 -- .../src/generic_boundary_tests.rs | 115 +- crates/labcolors-core/src/lib.rs | 2 +- crates/labcolors-core/src/program_session.rs | 976 +----------- .../src/program_session_tests.rs | 1339 +---------------- scripts/verify_point_support_surplus.py | 2 +- 7 files changed, 42 insertions(+), 2523 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 3dce01bb..e2e61e9f 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":"17cd1c8afb755d5ede7621bd405c8c35a85e5066d4ea7ef1c4d3305e41d467b6","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":"06b0017fcfad05f582bae0d9ddc1fe1bb5240ca01fc30795a469c093cf9c1f87","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":"57e6feeab15182322c258829d10214c31bb3c93fa58d8d47e37127db204cdc28"},{"kind":"rust-source","path":"crates/labcolors-core/src/composition.rs","sha256":"195a67327a3bd86d7816b634481389930bf68577bb1202fad14c2ea152df8625"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/exact.rs","sha256":"45bd09cd8f6f74860a0a3ffeb410ee2c3cf91b4868877abbc949e724b61e1ccd"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/mod.rs","sha256":"2f3e44b2db837deed0df3e34063df9cfc6af82a912373da488c9fe2137a119c0"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/wcag22.rs","sha256":"985e21abccd1fcb4a3f9624d5947ff3d1af7a1bf26b6b170ea8bbef77d6f1798"},{"kind":"rust-source","path":"crates/labcolors-core/src/hash.rs","sha256":"f97a0fd7d6ad3162f0f1dfb326fccfb7ed40da9a8fa67a5b8a239a1ae2ae49c3"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"d87d4959e28556de54f815b7f833d5b9310cf4f4209916257e2d1ed8ad160810"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"2bf4c805fb95f5b091f6c30ceb4b425f4c8dffef5127da2853156d86a16ad9cd"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"974116bbbb797063d98fcce4f23b089fc05a588a765432c15a82315d4489ef94"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"2363bfbedcaf619c5b7d964cb3667095a030ce4a6cfd7b8db9a34a6dd300e102"},{"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":40,"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":"d82019a4b5d3fd8bde4d8731118a4d5369a2e2b3e676d5d251b0a7504ca00a30"} +{"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":"e366d40bbddcf1d82b47884f3cd4c18c67d2a19a5b8e76a11489e636a9fa4351","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":"c399666d9db623b1f0879912ab2ed7878d99d75d760327ad022b00e2eb30eb5e","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":"57e6feeab15182322c258829d10214c31bb3c93fa58d8d47e37127db204cdc28"},{"kind":"rust-source","path":"crates/labcolors-core/src/composition.rs","sha256":"195a67327a3bd86d7816b634481389930bf68577bb1202fad14c2ea152df8625"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/exact.rs","sha256":"45bd09cd8f6f74860a0a3ffeb410ee2c3cf91b4868877abbc949e724b61e1ccd"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/mod.rs","sha256":"8b78b9a31e7ade3f7b99edf3fbcf2bc5d912509cd851e82f7380e2a19e25431e"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/wcag22.rs","sha256":"985e21abccd1fcb4a3f9624d5947ff3d1af7a1bf26b6b170ea8bbef77d6f1798"},{"kind":"rust-source","path":"crates/labcolors-core/src/hash.rs","sha256":"f97a0fd7d6ad3162f0f1dfb326fccfb7ed40da9a8fa67a5b8a239a1ae2ae49c3"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"80db959a77ecfcd6e2383f9097a3cd1e903edf5171f6cbd232aa3485043cabf1"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"2bf4c805fb95f5b091f6c30ceb4b425f4c8dffef5127da2853156d86a16ad9cd"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"974116bbbb797063d98fcce4f23b089fc05a588a765432c15a82315d4489ef94"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"2363bfbedcaf619c5b7d964cb3667095a030ce4a6cfd7b8db9a34a6dd300e102"},{"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":40,"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":"f1f4966b59d9314c0f1a10b4f7979d39539ca0e0fd0a44ff2fbf4437012337ed"} diff --git a/crates/labcolors-core/src/constraints/mod.rs b/crates/labcolors-core/src/constraints/mod.rs index 8a956161..c5618c13 100644 --- a/crates/labcolors-core/src/constraints/mod.rs +++ b/crates/labcolors-core/src/constraints/mod.rs @@ -27,13 +27,6 @@ pub(crate) use wcag22::{ Wcag22ViolationV1, }; -#[cfg(test)] -pub(crate) use program_test_evaluator::{ - ProgramTestEvaluationErrorV1, ProgramTestEvaluatorV1, ProgramTestInvocationV1, - arm_program_test_failure_once, program_test_evaluation_count, - reset_program_test_evaluation_count, -}; - /// Seals недоступны внешним crate-ам: новые evaluator/classifier families /// добавляются только вместе с code-owned physical adapter-ом. mod private { @@ -160,9 +153,6 @@ pub(crate) type PointViolation = , >>::Violation; -pub(crate) type PointEvaluationError = - >::Error; - /// One statically dispatched point evaluator/classifier family. /// /// The first executable Program slice is deliberately homogeneous: every @@ -264,125 +254,6 @@ impl } } -/// Code-owned fallible evaluator used only to prove Program transactionality. -/// Its measurement intentionally does not implement `Clone` or `Copy`. -#[cfg(test)] -mod program_test_evaluator { - use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; - - use super::{Evaluator, HardClassifier, HardDecision, private}; - use crate::Srgb8; - use crate::appearance::ModeledSrgb8PointOccurrence; - - static EVALUATIONS: AtomicUsize = AtomicUsize::new(0); - static FAIL_ONCE_ARMED: AtomicBool = AtomicBool::new(false); - - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub(crate) struct ProgramTestEvaluatorV1; - - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub(crate) struct ProgramTestInvocationV1 { - expected: Srgb8, - fail_once_when_armed: bool, - } - - impl ProgramTestInvocationV1 { - pub(crate) const fn exact(expected: Srgb8) -> Self { - Self { - expected, - fail_once_when_armed: false, - } - } - - pub(crate) const fn fail_once_when_armed(expected: Srgb8) -> Self { - Self { - expected, - fail_once_when_armed: true, - } - } - } - - #[derive(Debug, PartialEq, Eq)] - pub(crate) struct ProgramTestMeasurementV1 { - visible: Srgb8, - backdrop: Srgb8, - } - - #[derive(Debug, Clone, Copy, PartialEq, Eq)] - pub(crate) enum ProgramTestEvaluationErrorV1 { - Forced, - } - - #[derive(Debug, PartialEq, Eq)] - pub(crate) struct ProgramTestPassV1; - - #[derive(Debug, PartialEq, Eq)] - pub(crate) struct ProgramTestViolationV1; - - impl private::EvaluatorSealed for ProgramTestEvaluatorV1 {} - impl private::HardClassifierSealed for ProgramTestEvaluatorV1 {} - - impl Evaluator for ProgramTestEvaluatorV1 { - type Invocation = ProgramTestInvocationV1; - type Identity = (); - type Release = (); - type Capability = (); - type Measurement = ProgramTestMeasurementV1; - type Error = ProgramTestEvaluationErrorV1; - - fn identity(&self) {} - - fn release(&self) {} - - fn capability(&self) {} - - fn evaluate( - &self, - target: &ModeledSrgb8PointOccurrence, - invocation: &Self::Invocation, - ) -> Result { - EVALUATIONS.fetch_add(1, Ordering::Relaxed); - if invocation.fail_once_when_armed && FAIL_ONCE_ARMED.swap(false, Ordering::Relaxed) { - return Err(ProgramTestEvaluationErrorV1::Forced); - } - Ok(ProgramTestMeasurementV1 { - visible: Srgb8::new(target.visible()), - backdrop: Srgb8::new(target.backdrop()), - }) - } - } - - impl HardClassifier for ProgramTestEvaluatorV1 { - type Pass = ProgramTestPassV1; - type Violation = ProgramTestViolationV1; - - fn classify( - &self, - invocation: &ProgramTestInvocationV1, - measurement: &ProgramTestMeasurementV1, - ) -> HardDecision { - if measurement.visible == invocation.expected { - HardDecision::Pass(ProgramTestPassV1) - } else { - HardDecision::Violation(ProgramTestViolationV1) - } - } - } - - pub(crate) fn reset_program_test_evaluation_count() { - EVALUATIONS.store(0, Ordering::Relaxed); - FAIL_ONCE_ARMED.store(false, Ordering::Relaxed); - } - - pub(crate) fn program_test_evaluation_count() -> usize { - EVALUATIONS.load(Ordering::Relaxed) - } - - pub(crate) fn arm_program_test_failure_once() { - FAIL_ONCE_ARMED.store(true, Ordering::Relaxed); - } -} - #[cfg(test)] mod tests { use super::{ diff --git a/crates/labcolors-core/src/generic_boundary_tests.rs b/crates/labcolors-core/src/generic_boundary_tests.rs index 7ab5ee32..9f6fe2a8 100644 --- a/crates/labcolors-core/src/generic_boundary_tests.rs +++ b/crates/labcolors-core/src/generic_boundary_tests.rs @@ -4,7 +4,6 @@ const OBSERVATION_SOURCE: &str = include_str!("observation.rs"); const POINT_SUPPORT_SOURCE: &str = include_str!("point_support.rs"); const PROGRAM_SESSION_SOURCE: &str = include_str!("program_session.rs"); const SESSION_SOURCE: &str = include_str!("session.rs"); -const LIB_SOURCE: &str = include_str!("lib.rs"); const GENERIC_SOURCES: [(&str, &str); 3] = [ ("appearance.rs", APPEARANCE_SOURCE), @@ -320,111 +319,19 @@ fn program_session_module_docs_disclaim_transport_only_scope() { } #[test] -fn pre_f2_outputs_reuse_the_shared_encoded_point_paint_without_a_parallel_value() { - fn declaration<'a>(source: &'a str, start: &str, end: &str) -> &'a str { - let start = source - .find(start) - .unwrap_or_else(|| panic!("missing S0 declaration start `{start}`")); - let end = source[start..] - .find(end) - .map(|offset| start + offset) - .unwrap_or_else(|| panic!("missing S0 declaration end `{end}`")); - &source[start..end] - } - - assert!( - LIB_SOURCE.contains("pub(crate) mod program_session;"), - "the pre-F2 execution slice must remain crate-private", - ); - assert!( - !PROGRAM_SESSION_SOURCE.contains("OutputPaintV1"), - "S0 allows only appearance::EncodedPointPaintV1 as the physical point Paint value", - ); - - let output_value = declaration( - PROGRAM_SESSION_SOURCE, - "pub struct OutputValueV1 {", - "impl OutputValueV1 {", - ); - let fields_start = output_value - .find('{') - .expect("OutputValueV1 declaration must open its field list"); - let fields_end = output_value - .rfind('}') - .expect("OutputValueV1 declaration must close its field list"); - let fields = output_value[fields_start + 1..fields_end] - .lines() - .map(str::trim) - .filter(|line| !line.is_empty()) - .collect::>(); - assert_eq!( - fields, - ["output: OutputSlotId,", "value: EncodedPointPaintV1,"], - "OutputValueV1 must own exactly one route and the shared S0 Paint", - ); - assert!( - !output_value.contains("PaintId"), - "OutputValueV1 must not store any PaintId beside the same ID in EncodedPointPaintV1", - ); - - let output_value_impl = declaration( - PROGRAM_SESSION_SOURCE, - "impl OutputValueV1 {", - "struct ExecutionFrame", - ); - for required in [ - "self.value.id()", - "pub const fn value(self) -> EncodedPointPaintV1", - ] { - assert!( - output_value_impl.contains(required), - "OutputValueV1 must project directly from its shared S0 Paint; missing `{required}`", - ); - } - - let materialization = declaration( - PROGRAM_SESSION_SOURCE, - "for (index, output) in epoch.outputs.iter().enumerate() {", - "if has_hard_violation", - ); - let nominal_guard = materialization - .find("if paint.id() != output.paint_id {") - .expect("routed output must fail closed on compiled/materialized Paint ID drift"); - let exact_copy = materialization - .find("value: *paint,") - .expect("routed output must copy the exact graph-materialized Paint"); - assert!( - nominal_guard < exact_copy, - "nominal Paint ID drift must be rejected before the graph Paint is routed", - ); - assert!( - materialization[nominal_guard..exact_copy] - .contains("return Err(SessionUpdateError::InternalInvariant);"), - "compiled/materialized Paint ID drift must fail closed", - ); - assert!( - materialization[exact_copy..].contains("value: *paint,"), - "the routed value must copy the exact graph-materialized EncodedPointPaintV1", - ); - for forbidden in ["source: paint.source()", "straight_alpha: paint.opacity()"] { - assert!( - !materialization.contains(forbidden), - "graph materialization must not be reconstructed through `{forbidden}`", - ); - } - - let module_docs = PROGRAM_SESSION_SOURCE - .lines() - .take_while(|line| line.starts_with("//!")) - .collect::>() - .join("\n"); - for required in [ - "private pre-F2", - "does not mint a terminal output certificate", +fn program_compiler_cannot_regrow_the_superseded_runtime_facade() { + for forbidden in [ + "PointRenderOwner", + "PointRenderAttachError", + "ObservationStreamBinding", + "SurfaceUpdate", + "OutputValueV1", + "ExecutionFrame", + "SessionState", ] { assert!( - module_docs.contains(required), - "the routed pre-F2 value must not claim a public terminal certificate; missing `{required}`", + !PROGRAM_SESSION_SOURCE.contains(forbidden), + "program_session.rs must remain compiler/lowering-only; found superseded `{forbidden}`" ); } } diff --git a/crates/labcolors-core/src/lib.rs b/crates/labcolors-core/src/lib.rs index a6900d9b..0a152b33 100644 --- a/crates/labcolors-core/src/lib.rs +++ b/crates/labcolors-core/src/lib.rs @@ -49,7 +49,7 @@ pub(crate) mod point_support; not(test), expect( dead_code, - reason = "private encoded point-render Session precedes its package-private WASM bridge" + reason = "private Program compiler/lowering precedes its direct sole-Session bridge" ) )] pub(crate) mod program_session; diff --git a/crates/labcolors-core/src/program_session.rs b/crates/labcolors-core/src/program_session.rs index e099e5b9..8276c3d7 100644 --- a/crates/labcolors-core/src/program_session.rs +++ b/crates/labcolors-core/src/program_session.rs @@ -1,34 +1,26 @@ -//! Private generic point Program, constraint recheck and terminal Paint path. +//! Private generic point Program compiler and lowering path. //! //! The authored graph has no client/UI role vocabulary. Paints are physical //! source-plus-straight-alpha programs, occurrences are modeled applications of -//! Paint to Surface, constraints assess those exact occurrences, and outputs -//! bind opaque slots back to Paints. A visible occurrence is evidence, never a -//! terminal emitted value. -//! -//! This is the encoded-sRGB8 transport-only executable slice, not the LCS -//! observation/evidence layer. -//! This is a private pre-F2 execution slice. It materializes routed Paint -//! values but does not mint a terminal output certificate or define a public -//! transport contract. +//! Paint to Surface, constraints declare assessments of those exact +//! occurrences, and outputs bind opaque slots back to Paints. The compiled +//! result owns only admitted, canonical topology; runtime observation, +//! lifecycle and terminal emission belong to the sole revision-bound Session. +//! Its current values are encoded point transport-only; they are not LCS +//! observation or evidence. use std::marker::PhantomData; -use std::mem; -use std::rc::{Rc, Weak}; use crate::Srgb8; use crate::appearance::{ - AdmittedAppearanceBindings, AppearanceBindings, AppearanceGraphSpec, AppearanceWorkspace, - BindingError, ColorInputId, CompileError, CompiledAppearanceGraph, CompiledOccurrenceSlotV1, - CompiledPaintSlotV1, EncodedPointPaintV1, OccurrenceId, OccurrenceSpec, OpacityInputId, - PaintId, PaintSpec, SurfaceId, SurfaceInputPortId, SurfaceSpec, + AdmittedAppearanceBindings, AppearanceBindings, AppearanceGraphSpec, BindingError, + ColorInputId, CompileError, CompiledAppearanceGraph, CompiledOccurrenceSlotV1, + CompiledPaintSlotV1, OccurrenceId, OccurrenceSpec, OpacityInputId, PaintId, PaintSpec, + SurfaceId, SurfaceInputPortId, SurfaceSpec, }; use crate::composition::CompositionProfileV1; -use crate::constraints::{ - HardDecision, PointEvaluationError, PointEvaluatorV1, PointInvocation, - VisiblePointPassEvidence, VisiblePointViolationEvidence, assess_visible_point_hard, -}; -use crate::observation::{ObservationGroupId, ObservationStreamId}; +use crate::constraints::{PointEvaluatorV1, PointInvocation}; +use crate::observation::ObservationGroupId; /// One immutable encoded colour binding owned by a [`Program`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -308,27 +300,6 @@ impl ObservationGroup { } } -/// Attachment-time binding of a compiled group to one runtime stream epoch. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct ObservationStreamBinding { - group: ObservationGroupId, - stream: ObservationStreamId, -} - -impl ObservationStreamBinding { - pub const fn new(group: ObservationGroupId, stream: ObservationStreamId) -> Self { - Self { group, stream } - } - - pub const fn group(self) -> ObservationGroupId { - self.group - } - - pub const fn stream(self) -> ObservationStreamId { - self.stream - } -} - /// Immutable generic point Program. pub struct Program where @@ -470,6 +441,10 @@ enum CompiledConstraintModeV1 { ReportOnly, } +#[expect( + dead_code, + reason = "the compiled constraint payload is retained for the direct sole-Session bridge; erasing it would reduce lowering to a shape-only placeholder" +)] struct CompiledPointConstraint { id: ConstraintId, target_id: OccurrenceId, @@ -490,6 +465,10 @@ struct CompiledObservationGroupV1 { surface_input_ports: Box<[SurfaceInputPortId]>, } +#[expect( + dead_code, + reason = "the executable graph, admitted bindings and evaluator are retained for the direct sole-Session bridge in the next stack" +)] struct ProgramEpochV1 where Evaluation: PointEvaluatorV1, @@ -538,124 +517,6 @@ where .iter() .map(|output| (output.output, output.paint_id)) } - - pub fn into_owner(self) -> PointRenderOwner { - PointRenderOwner::new(self) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum PointRenderAttachError { - Disposed, - ObservationGroupMismatch { - expected: ObservationGroupId, - actual: ObservationGroupId, - }, - ResourceExhausted, - InternalInvariant, -} - -/// Sole strong owner of one non-reusable compiled epoch. -pub struct PointRenderOwner -where - Evaluation: PointEvaluatorV1, - PointInvocation: Copy, -{ - current: Option>>, -} - -impl PointRenderOwner -where - Evaluation: PointEvaluatorV1, - PointInvocation: Copy, -{ - pub fn new(compiled: CompiledProgram) -> Self { - Self { - current: Some(Rc::new(compiled.epoch)), - } - } - - pub fn replace(&mut self, compiled: CompiledProgram) { - self.current = Some(Rc::new(compiled.epoch)); - } - - pub fn dispose(&mut self) { - self.current = None; - } - - pub fn observation_group_id(&self) -> Option { - self.current - .as_deref() - .map(|epoch| epoch.observation_group.id) - } - - pub fn surface_input_ports(&self) -> Option<&[SurfaceInputPortId]> { - self.current - .as_deref() - .map(|epoch| epoch.observation_group.surface_input_ports.as_ref()) - } - - pub fn constraint_ids(&self) -> Option + '_> { - self.current - .as_deref() - .map(|epoch| epoch.constraints.iter().map(|constraint| constraint.id)) - } - - pub fn outputs(&self) -> Option + '_> { - self.current.as_deref().map(|epoch| { - epoch - .outputs - .iter() - .map(|output| (output.output, output.paint_id)) - }) - } - - /// Fallibly allocate every hot-path frame before the Session escapes. - pub fn attach( - &self, - binding: ObservationStreamBinding, - ) -> Result, PointRenderAttachError> { - let epoch = self - .current - .as_ref() - .ok_or(PointRenderAttachError::Disposed)?; - if binding.group != epoch.observation_group.id { - return Err(PointRenderAttachError::ObservationGroupMismatch { - expected: epoch.observation_group.id, - actual: binding.group, - }); - } - let workspace = epoch - .graph - .new_workspace() - .map_err(map_attach_binding_error)?; - let bindings = epoch - .binding_template - .try_clone_v1() - .map_err(map_attach_binding_error)?; - let free_frames = [ - Some(ExecutionFrame::try_new(epoch)?), - Some(ExecutionFrame::try_new(epoch)?), - Some(ExecutionFrame::try_new(epoch)?), - ]; - Ok(Session { - epoch: Rc::downgrade(epoch), - stream: binding.stream, - bindings, - workspace, - free_frames, - state: SessionState::Waiting { - current_unavailable: None, - }, - }) - } -} - -fn map_attach_binding_error(error: BindingError) -> PointRenderAttachError { - match error { - BindingError::ResourceExhausted => PointRenderAttachError::ResourceExhausted, - _ => PointRenderAttachError::InternalInvariant, - } } fn prepare_program( @@ -1003,798 +864,3 @@ fn map_binding_compile_error(error: BindingError) -> ProgramCompileError { _ => ProgramCompileError::InternalInvariant, } } - -/// Revision-bound unavailable input descriptor. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct SurfaceUnavailable { - revision: u64, - reason: u32, -} - -impl SurfaceUnavailable { - pub const fn revision(self) -> u64 { - self.revision - } - - pub const fn reason(self) -> u32 { - self.reason - } -} - -/// One typed runtime Surface input signal. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct SurfaceSignal { - input: SurfaceInputPortId, - value: Srgb8, -} - -impl SurfaceSignal { - pub const fn new(input: SurfaceInputPortId, value: Srgb8) -> Self { - Self { input, value } - } - - pub const fn input(self) -> SurfaceInputPortId { - self.input - } - - pub const fn value(self) -> Srgb8 { - self.value - } -} - -/// Transport-only payload carried by one stream-affine point update. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum SurfaceUpdatePayload<'input> { - Unavailable { reason: u32 }, - Present { surfaces: &'input [SurfaceSignal] }, -} - -/// Borrowed, correlated runtime update for one attached Session. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct SurfaceUpdate<'input> { - stream: ObservationStreamId, - revision: u64, - payload: SurfaceUpdatePayload<'input>, -} - -impl<'input> SurfaceUpdate<'input> { - pub const fn unavailable(stream: ObservationStreamId, revision: u64, reason: u32) -> Self { - Self { - stream, - revision, - payload: SurfaceUpdatePayload::Unavailable { reason }, - } - } - - pub const fn present( - stream: ObservationStreamId, - revision: u64, - surfaces: &'input [SurfaceSignal], - ) -> Self { - Self { - stream, - revision, - payload: SurfaceUpdatePayload::Present { surfaces }, - } - } - - pub const fn stream(self) -> ObservationStreamId { - self.stream - } - - pub const fn revision(self) -> u64 { - self.revision - } - - pub const fn payload(self) -> SurfaceUpdatePayload<'input> { - self.payload - } -} - -/// Evaluator classification with the exact bound occurrence evidence. -pub enum ConstraintOutcome -where - Evaluation: PointEvaluatorV1, -{ - Pass(VisiblePointPassEvidence), - Violation(VisiblePointViolationEvidence), -} - -/// One mode-refined report cell. -pub struct ConstraintAssessment -where - Evaluation: PointEvaluatorV1, -{ - constraint: ConstraintId, - target: OccurrenceId, - outcome: ConstraintOutcome, - mode: PhantomData Mode>, -} - -impl ConstraintAssessment -where - Evaluation: PointEvaluatorV1, -{ - fn new( - constraint: ConstraintId, - target: OccurrenceId, - outcome: ConstraintOutcome, - ) -> Self { - Self { - constraint, - target, - outcome, - mode: PhantomData, - } - } - - pub const fn constraint(&self) -> ConstraintId { - self.constraint - } - - pub const fn target(&self) -> OccurrenceId { - self.target - } - - pub const fn outcome(&self) -> &ConstraintOutcome { - &self.outcome - } -} - -/// Canonical full-report entry; authored mode remains visible in the type. -pub enum ConstraintReportEntry -where - Evaluation: PointEvaluatorV1, -{ - Hard(ConstraintAssessment), - ReportOnly(ConstraintAssessment), -} - -impl ConstraintReportEntry -where - Evaluation: PointEvaluatorV1, -{ - pub const fn constraint(&self) -> ConstraintId { - match self { - Self::Hard(assessment) => assessment.constraint, - Self::ReportOnly(assessment) => assessment.constraint, - } - } - - pub const fn target(&self) -> OccurrenceId { - match self { - Self::Hard(assessment) => assessment.target, - Self::ReportOnly(assessment) => assessment.target, - } - } -} - -/// One routed private output cell: an opaque output slot and the exact -/// materialized Paint produced for it. The Paint owns its sole authored identity. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct OutputValueV1 { - output: OutputSlotId, - value: EncodedPointPaintV1, -} - -impl OutputValueV1 { - pub const fn output(self) -> OutputSlotId { - self.output - } - - pub const fn paint(self) -> PaintId { - self.value.id() - } - - pub const fn value(self) -> EncodedPointPaintV1 { - self.value - } -} - -struct ExecutionFrame -where - Evaluation: PointEvaluatorV1, -{ - surfaces: Box<[SurfaceSignal]>, - reports: Vec>>, - outputs: Vec>, -} - -impl ExecutionFrame -where - Evaluation: PointEvaluatorV1, - PointInvocation: Copy, -{ - fn try_new(epoch: &ProgramEpochV1) -> Result { - let mut surfaces = Vec::new(); - surfaces - .try_reserve_exact(epoch.observation_group.surface_input_ports.len()) - .map_err(|_| PointRenderAttachError::ResourceExhausted)?; - surfaces.extend( - epoch - .observation_group - .surface_input_ports - .iter() - .copied() - .map(|input| SurfaceSignal::new(input, Srgb8::new([0; 3]))), - ); - - let mut reports = Vec::new(); - reports - .try_reserve_exact(epoch.constraints.len()) - .map_err(|_| PointRenderAttachError::ResourceExhausted)?; - reports.resize_with(epoch.constraints.len(), || None); - - let mut outputs = Vec::new(); - outputs - .try_reserve_exact(epoch.outputs.len()) - .map_err(|_| PointRenderAttachError::ResourceExhausted)?; - outputs.resize_with(epoch.outputs.len(), || None); - - Ok(Self { - surfaces: surfaces.into_boxed_slice(), - reports, - outputs, - }) - } - - fn clear_dynamic(&mut self) { - for report in &mut self.reports { - report.take(); - } - for output in &mut self.outputs { - output.take(); - } - } - - fn report(&self) -> impl ExactSizeIterator> + '_ { - self.reports.iter().map(|report| { - report - .as_ref() - .unwrap_or_else(|| unreachable!("committed report is complete")) - }) - } - - fn outputs(&self) -> impl ExactSizeIterator + '_ { - self.outputs.iter().map(|output| { - output - .as_ref() - .copied() - .unwrap_or_else(|| unreachable!("verified output set is complete")) - }) - } - - fn present_payload_matches(&self, mut value_at: impl FnMut(usize) -> Srgb8) -> bool { - let mut exact = true; - for (index, signal) in self.surfaces.iter().enumerate() { - if value_at(index) != signal.value { - exact = false; - } - } - exact - } -} - -/// One hard-admitted snapshot with full evidence and terminal Paints. -pub struct Snapshot -where - Evaluation: PointEvaluatorV1, -{ - revision: u64, - frame: ExecutionFrame, -} - -impl Snapshot -where - Evaluation: PointEvaluatorV1, - PointInvocation: Copy, -{ - pub const fn revision(&self) -> u64 { - self.revision - } - - pub fn surfaces(&self) -> impl ExactSizeIterator + '_ { - self.frame.surfaces.iter().copied() - } - - pub fn report(&self) -> impl ExactSizeIterator> + '_ { - self.frame.report() - } - - pub fn outputs(&self) -> impl ExactSizeIterator + '_ { - self.frame.outputs() - } - - pub fn output(&self, output: OutputSlotId) -> Option { - let index = self - .frame - .outputs - .binary_search_by_key(&output, |slot| { - slot.as_ref() - .unwrap_or_else(|| unreachable!("verified output set is complete")) - .output - }) - .ok()?; - self.frame.outputs[index] - } - - #[cfg(test)] - pub(crate) fn storage_pointers_for_test(&self) -> (*const SurfaceSignal, *const ()) { - ( - self.frame.surfaces.as_ptr(), - self.frame.reports.as_ptr().cast(), - ) - } -} - -/// Complete current report containing at least one hard violation. -pub struct ConstraintConflict -where - Evaluation: PointEvaluatorV1, -{ - revision: u64, - frame: ExecutionFrame, -} - -impl ConstraintConflict -where - Evaluation: PointEvaluatorV1, - PointInvocation: Copy, -{ - pub const fn revision(&self) -> u64 { - self.revision - } - - pub fn surfaces(&self) -> impl ExactSizeIterator + '_ { - self.frame.surfaces.iter().copied() - } - - pub fn report(&self) -> impl ExactSizeIterator> + '_ { - self.frame.report() - } - - #[cfg(test)] - pub(crate) fn storage_pointers_for_test(&self) -> (*const SurfaceSignal, *const ()) { - ( - self.frame.surfaces.as_ptr(), - self.frame.reports.as_ptr().cast(), - ) - } -} - -/// Current lifecycle state of one generation-bound Session. -pub enum SessionState -where - Evaluation: PointEvaluatorV1, -{ - Waiting { - current_unavailable: Option, - }, - Ready { - current: Snapshot, - }, - Stale { - previous: Snapshot, - current_unavailable: SurfaceUnavailable, - }, - Conflict { - current: ConstraintConflict, - previous: Option>, - }, -} - -impl SessionState -where - Evaluation: PointEvaluatorV1, - PointInvocation: Copy, -{ - const fn head_revision(&self) -> Option { - match self { - Self::Waiting { - current_unavailable: None, - } => None, - Self::Waiting { - current_unavailable: Some(unavailable), - } - | Self::Stale { - current_unavailable: unavailable, - .. - } => Some(unavailable.revision), - Self::Ready { current } => Some(current.revision), - Self::Conflict { current, .. } => Some(current.revision), - } - } -} - -/// Failure to admit or evaluate one Session update. A hard violation is not an -/// error; it commits [`SessionState::Conflict`] with its full report. -#[derive(Debug, PartialEq, Eq)] -pub enum SessionUpdateError { - ProgramExpired, - ObservationStreamMismatch { - expected: ObservationStreamId, - actual: ObservationStreamId, - }, - SurfaceInputPortLengthMismatch { - expected: usize, - actual: usize, - }, - SurfaceInputPortMismatch { - index: usize, - expected: SurfaceInputPortId, - actual: SurfaceInputPortId, - }, - RevisionOutOfOrder { - current: u64, - incoming: u64, - }, - RevisionConflict { - revision: u64, - }, - Evaluator { - constraint: ConstraintId, - source: EvaluationError, - }, - InternalInvariant, -} - -/// Mutable runtime with three attach-allocated transactional frames. -pub struct Session -where - Evaluation: PointEvaluatorV1, - PointInvocation: Copy, -{ - epoch: Weak>, - stream: ObservationStreamId, - bindings: AdmittedAppearanceBindings, - workspace: AppearanceWorkspace, - free_frames: [Option>; 3], - state: SessionState, -} - -impl Session -where - Evaluation: PointEvaluatorV1, - PointInvocation: Copy, -{ - pub const fn state(&self) -> &SessionState { - &self.state - } - - pub const fn stream(&self) -> ObservationStreamId { - self.stream - } - - pub fn update( - &mut self, - update: SurfaceUpdate<'_>, - ) -> Result<&SessionState, SessionUpdateError>> - { - let epoch = self - .epoch - .upgrade() - .ok_or(SessionUpdateError::ProgramExpired)?; - if update.stream != self.stream { - return Err(SessionUpdateError::ObservationStreamMismatch { - expected: self.stream, - actual: update.stream, - }); - } - match update.payload { - SurfaceUpdatePayload::Unavailable { reason } => { - self.apply_unavailable(SurfaceUnavailable { - revision: update.revision, - reason, - }) - } - SurfaceUpdatePayload::Present { surfaces } => { - if surfaces.len() != epoch.observation_group.surface_input_ports.len() { - return Err(SessionUpdateError::SurfaceInputPortLengthMismatch { - expected: epoch.observation_group.surface_input_ports.len(), - actual: surfaces.len(), - }); - } - for (index, (&expected, actual)) in epoch - .observation_group - .surface_input_ports - .iter() - .zip(surfaces.iter()) - .enumerate() - { - if actual.input != expected { - return Err(SessionUpdateError::SurfaceInputPortMismatch { - index, - expected, - actual: actual.input, - }); - } - } - self.apply_canonical_present(&epoch, update.revision, surfaces.len(), |index| { - surfaces[index].value - }) - } - } - } - - pub(crate) fn update_canonical_present( - &mut self, - stream: ObservationStreamId, - revision: u64, - surface_input_port_count: usize, - value_at: impl FnMut(usize) -> Srgb8, - ) -> Result<&SessionState, SessionUpdateError>> - { - let epoch = self - .epoch - .upgrade() - .ok_or(SessionUpdateError::ProgramExpired)?; - if stream != self.stream { - return Err(SessionUpdateError::ObservationStreamMismatch { - expected: self.stream, - actual: stream, - }); - } - self.apply_canonical_present(&epoch, revision, surface_input_port_count, value_at) - } - - fn apply_unavailable( - &mut self, - unavailable: SurfaceUnavailable, - ) -> Result<&SessionState, SessionUpdateError>> - { - let incoming_revision = unavailable.revision; - if let Some(current) = self.state.head_revision() { - if incoming_revision < current { - return Err(SessionUpdateError::RevisionOutOfOrder { - current, - incoming: incoming_revision, - }); - } - if incoming_revision == current { - let exact = match &self.state { - SessionState::Waiting { - current_unavailable: Some(current), - } - | SessionState::Stale { - current_unavailable: current, - .. - } => unavailable == *current, - _ => false, - }; - return if exact { - Ok(&self.state) - } else { - Err(SessionUpdateError::RevisionConflict { revision: current }) - }; - } - } - - let previous = self.take_last_verified_and_recycle_current(); - self.state = match previous { - Some(previous) => SessionState::Stale { - previous, - current_unavailable: unavailable, - }, - None => SessionState::Waiting { - current_unavailable: Some(unavailable), - }, - }; - Ok(&self.state) - } - - fn apply_canonical_present( - &mut self, - epoch: &ProgramEpochV1, - revision: u64, - surface_input_port_count: usize, - mut value_at: impl FnMut(usize) -> Srgb8, - ) -> Result<&SessionState, SessionUpdateError>> - { - if surface_input_port_count != epoch.observation_group.surface_input_ports.len() { - return Err(SessionUpdateError::SurfaceInputPortLengthMismatch { - expected: epoch.observation_group.surface_input_ports.len(), - actual: surface_input_port_count, - }); - } - if let Some(current) = self.state.head_revision() { - if revision < current { - return Err(SessionUpdateError::RevisionOutOfOrder { - current, - incoming: revision, - }); - } - if revision == current { - return self.admit_same_revision_present(revision, value_at); - } - } - - let mut frame = - take_free_frame(&mut self.free_frames).ok_or(SessionUpdateError::InternalInvariant)?; - frame.clear_dynamic(); - if frame.surfaces.len() != epoch.observation_group.surface_input_ports.len() - || frame.reports.len() != epoch.constraints.len() - || frame.outputs.len() != epoch.outputs.len() - { - put_free_frame(&mut self.free_frames, frame); - return Err(SessionUpdateError::InternalInvariant); - } - - let surface_slots = &mut frame.surfaces; - if self - .bindings - .overwrite_surface_inputs_canonical( - epoch.observation_group.surface_input_ports.iter().copied(), - &mut |index| { - let value = value_at(index); - surface_slots[index] = SurfaceSignal::new( - epoch.observation_group.surface_input_ports[index], - value, - ); - value - }, - ) - .is_err() - { - put_free_frame(&mut self.free_frames, frame); - return Err(SessionUpdateError::InternalInvariant); - } - - let evaluation = match epoch - .graph - .evaluate_admitted_into(&self.bindings, &mut self.workspace) - { - Ok(evaluation) => evaluation, - Err(_) => { - put_free_frame(&mut self.free_frames, frame); - return Err(SessionUpdateError::InternalInvariant); - } - }; - - let mut has_hard_violation = false; - for (index, constraint) in epoch.constraints.iter().enumerate() { - let Some(source) = evaluation.occurrence_at(constraint.target) else { - put_free_frame(&mut self.free_frames, frame); - return Err(SessionUpdateError::InternalInvariant); - }; - let decision = - match assess_visible_point_hard(source, &epoch.evaluator, constraint.invocation) { - Ok(decision) => decision, - Err(source) => { - put_free_frame(&mut self.free_frames, frame); - return Err(SessionUpdateError::Evaluator { - constraint: constraint.id, - source, - }); - } - }; - let (outcome, violation) = match decision { - HardDecision::Pass(evidence) => (ConstraintOutcome::Pass(evidence), false), - HardDecision::Violation(evidence) => (ConstraintOutcome::Violation(evidence), true), - }; - frame.reports[index] = Some(match constraint.mode { - CompiledConstraintModeV1::Hard => { - has_hard_violation |= violation; - ConstraintReportEntry::Hard(ConstraintAssessment::new( - constraint.id, - constraint.target_id, - outcome, - )) - } - CompiledConstraintModeV1::ReportOnly => ConstraintReportEntry::ReportOnly( - ConstraintAssessment::new(constraint.id, constraint.target_id, outcome), - ), - }); - } - - if !has_hard_violation { - for (index, output) in epoch.outputs.iter().enumerate() { - let Some(paint) = evaluation.paint_at(output.paint) else { - put_free_frame(&mut self.free_frames, frame); - return Err(SessionUpdateError::InternalInvariant); - }; - if paint.id() != output.paint_id { - put_free_frame(&mut self.free_frames, frame); - return Err(SessionUpdateError::InternalInvariant); - } - frame.outputs[index] = Some(OutputValueV1 { - output: output.output, - value: *paint, - }); - } - } - if has_hard_violation { - let previous = self.take_last_verified_and_recycle_current(); - self.state = SessionState::Conflict { - current: ConstraintConflict { revision, frame }, - previous, - }; - } else { - self.recycle_entire_state(); - self.state = SessionState::Ready { - current: Snapshot { revision, frame }, - }; - } - Ok(&self.state) - } - - fn admit_same_revision_present( - &self, - revision: u64, - value_at: impl FnMut(usize) -> Srgb8, - ) -> Result<&SessionState, SessionUpdateError>> - { - let exact = match &self.state { - SessionState::Ready { current } => current.frame.present_payload_matches(value_at), - SessionState::Conflict { current, .. } => { - current.frame.present_payload_matches(value_at) - } - _ => return Err(SessionUpdateError::RevisionConflict { revision }), - }; - if exact { - Ok(&self.state) - } else { - Err(SessionUpdateError::RevisionConflict { revision }) - } - } - - fn take_last_verified_and_recycle_current(&mut self) -> Option> { - match mem::replace( - &mut self.state, - SessionState::Waiting { - current_unavailable: None, - }, - ) { - SessionState::Waiting { .. } => None, - SessionState::Ready { current } => Some(current), - SessionState::Stale { previous, .. } => Some(previous), - SessionState::Conflict { current, previous } => { - put_free_frame(&mut self.free_frames, current.frame); - previous - } - } - } - - fn recycle_entire_state(&mut self) { - match mem::replace( - &mut self.state, - SessionState::Waiting { - current_unavailable: None, - }, - ) { - SessionState::Waiting { .. } => {} - SessionState::Ready { current } => { - put_free_frame(&mut self.free_frames, current.frame); - } - SessionState::Stale { previous, .. } => { - put_free_frame(&mut self.free_frames, previous.frame); - } - SessionState::Conflict { current, previous } => { - put_free_frame(&mut self.free_frames, current.frame); - if let Some(previous) = previous { - put_free_frame(&mut self.free_frames, previous.frame); - } - } - } - } -} - -fn take_free_frame( - pool: &mut [Option>; 3], -) -> Option> -where - Evaluation: PointEvaluatorV1, -{ - pool.iter_mut().find_map(Option::take) -} - -fn put_free_frame( - pool: &mut [Option>; 3], - frame: ExecutionFrame, -) where - Evaluation: PointEvaluatorV1, -{ - let Some(slot) = pool.iter_mut().find(|slot| slot.is_none()) else { - unreachable!("three-frame ownership invariant exceeded") - }; - *slot = Some(frame); -} diff --git a/crates/labcolors-core/src/program_session_tests.rs b/crates/labcolors-core/src/program_session_tests.rs index 3ef5852a..f5873868 100644 --- a/crates/labcolors-core/src/program_session_tests.rs +++ b/crates/labcolors-core/src/program_session_tests.rs @@ -1,26 +1,15 @@ -use std::cell::Cell; -use std::convert::Infallible; - use crate::Srgb8; use crate::appearance::{ - ColorInputId, EncodedPointPaintV1, OccurrenceId, OpacityInputId, PaintId, SurfaceId, - SurfaceInputPortId, -}; -use crate::constraints::{ - ExactSrgb8IdentityV1, PointEvaluatorV1, PointInvocation, ProgramTestEvaluationErrorV1, - ProgramTestEvaluatorV1, ProgramTestInvocationV1, Wcag22Srgb8V1, arm_program_test_failure_once, - program_test_evaluation_count, reset_program_test_evaluation_count, + ColorInputId, OccurrenceId, OpacityInputId, PaintId, SurfaceId, SurfaceInputPortId, }; -use crate::observation::{ObservationGroupId, ObservationStreamId}; +use crate::constraints::{ExactSrgb8IdentityV1, PointEvaluatorV1, PointInvocation}; +use crate::observation::ObservationGroupId; use crate::program_session::{ - ColorInput, CompiledProgram, CompositionProfile, ConstraintAssessment, ConstraintId, - ConstraintInvocation, ConstraintOutcome, ConstraintReportEntry, ConstraintSet, HardModeV1, - ObservationGroup, ObservationStreamBinding, Occurrence, OpacityInput, OutputBinding, - OutputSlotId, Paint, PointRenderAttachError, Program, ProgramCompileError, ReportModeV1, - SessionState, SessionUpdateError, Surface, SurfaceSignal, SurfaceUpdate, SurfaceUpdatePayload, - canonical_surface_input_port_sequence_matches, check_render_node_count, + ColorInput, CompositionProfile, ConstraintId, ConstraintInvocation, ConstraintSet, + ObservationGroup, Occurrence, OpacityInput, OutputBinding, OutputSlotId, Paint, Program, + ProgramCompileError, Surface, canonical_surface_input_port_sequence_matches, + check_render_node_count, }; -use crate::wcag22::{Wcag22ApplicableDecisionV1, Wcag22CriterionV1}; const COLOR: ColorInputId = ColorInputId::new(1); const SURFACE_PORT: SurfaceInputPortId = SurfaceInputPortId::new(2); @@ -33,25 +22,11 @@ const OCCURRENCE: OccurrenceId = OccurrenceId::new(30); const OUTPUT: OutputSlotId = OutputSlotId::new(40); const REQUIRED: ConstraintId = ConstraintId::new(50); const GROUP: ObservationGroupId = ObservationGroupId::new(60); -const OTHER_GROUP: ObservationGroupId = ObservationGroupId::new(61); -const STREAM_A: ObservationStreamId = ObservationStreamId::new(70); -const STREAM_B: ObservationStreamId = ObservationStreamId::new(71); fn observation_group(surface_input_ports: Vec) -> ObservationGroup { ObservationGroup::new(GROUP, surface_input_ports) } -const fn stream_binding( - group: ObservationGroupId, - stream: ObservationStreamId, -) -> ObservationStreamBinding { - ObservationStreamBinding::new(group, stream) -} - -const fn default_stream_binding() -> ObservationStreamBinding { - stream_binding(GROUP, STREAM_A) -} - fn base_program( opacity: f64, against: SurfaceId, @@ -115,62 +90,6 @@ where ) } -fn exact_program( - hard: Vec>, - report_only: Vec>, -) -> Program { - base_program( - 0.5, - BACKDROP, - ConstraintSet::new(hard, report_only), - vec![OutputBinding::new(OUTPUT, TRANSLUCENT)], - ExactSrgb8IdentityV1, - ) -} - -fn exact_required(expected: Srgb8) -> Program { - exact_program( - vec![ConstraintInvocation::hard(REQUIRED, OCCURRENCE, expected)], - vec![], - ) -} - -fn compiled_exact(expected: Srgb8) -> CompiledProgram { - exact_required(expected).compile().unwrap() -} - -fn compiled_exact_in_group( - group: ObservationGroupId, - expected: Srgb8, -) -> CompiledProgram { - base_program_in_group( - group, - 0.5, - BACKDROP, - ConstraintSet::new( - vec![ConstraintInvocation::hard(REQUIRED, OCCURRENCE, expected)], - vec![], - ), - vec![OutputBinding::new(OUTPUT, TRANSLUCENT)], - ExactSrgb8IdentityV1, - ) - .compile() - .unwrap() -} - -fn wcag_program( - hard: Vec>, - report_only: Vec>, -) -> Program { - base_program( - 0.5, - BACKDROP, - ConstraintSet::new(hard, report_only), - vec![OutputBinding::new(OUTPUT, TRANSLUCENT)], - Wcag22Srgb8V1, - ) -} - fn compile_error(program: Program) -> ProgramCompileError where Evaluation: PointEvaluatorV1, @@ -182,47 +101,6 @@ where } } -fn update_error(result: Result) -> Error { - match result { - Ok(_) => panic!("invalid update committed"), - Err(error) => error, - } -} - -struct ReadProbe { - values: [Srgb8; N], - reads: Cell<[usize; N]>, -} - -impl ReadProbe { - fn new(values: [Srgb8; N]) -> Self { - Self { - values, - reads: Cell::new([0; N]), - } - } - - fn read(&self, index: usize) -> Srgb8 { - let mut reads = self.reads.get(); - reads[index] += 1; - self.reads.set(reads); - self.values[index] - } - - fn reads(&self) -> [usize; N] { - self.reads.get() - } -} - -fn exact_outcome( - assessment: &ConstraintAssessment, -) -> (Srgb8, Srgb8) { - match assessment.outcome() { - ConstraintOutcome::Pass(evidence) => (evidence.target(), evidence.actual()), - ConstraintOutcome::Violation(evidence) => (evidence.target(), evidence.actual()), - } -} - #[test] fn authored_modes_are_marker_typed_and_values_preserve_exact_ids() { let hard = ConstraintInvocation::hard(REQUIRED, OCCURRENCE, Srgb8::new([0x80; 3])); @@ -261,28 +139,9 @@ fn authored_modes_are_marker_typed_and_values_preserve_exact_ids() { CompositionProfile::EncodedSrgb8SourceOverV1 ); - let signal = SurfaceSignal::new(SURFACE_PORT, Srgb8::new([4, 5, 6])); - assert_eq!(signal.input(), SURFACE_PORT); - assert_eq!(signal.value(), Srgb8::new([4, 5, 6])); - let group = observation_group(vec![SURFACE_PORT]); assert_eq!(group.id(), GROUP); assert_eq!(group.surface_input_ports(), &[SURFACE_PORT]); - let binding = default_stream_binding(); - assert_eq!(binding.group(), GROUP); - assert_eq!(binding.stream(), STREAM_A); - let signals = [signal]; - let update = SurfaceUpdate::present(STREAM_A, 17, &signals); - assert_eq!(update.stream(), STREAM_A); - assert_eq!(update.revision(), 17); - assert_eq!( - update.payload(), - SurfaceUpdatePayload::Present { surfaces: &signals } - ); - assert_eq!( - SurfaceUpdate::unavailable(STREAM_A, 18, 9).payload(), - SurfaceUpdatePayload::Unavailable { reason: 9 } - ); } #[test] @@ -558,1033 +417,6 @@ fn compile_canonicalizes_constraints_and_outputs_independent_of_mode_lists() { compiled.outputs().collect::>(), vec![(output_low, TRANSLUCENT), (output_high, TRANSLUCENT)] ); - let owner = compiled.into_owner(); - assert_eq!(owner.observation_group_id(), Some(GROUP)); - assert_eq!( - owner.constraint_ids().unwrap().collect::>(), - vec![low, high] - ); - assert_eq!( - owner.outputs().unwrap().collect::>(), - vec![(output_low, TRANSLUCENT), (output_high, TRANSLUCENT)] - ); -} - -#[test] -fn attach_rejects_the_wrong_group_without_allocation_and_disposed_wins() { - let program = base_program( - 0.5, - BACKDROP, - ConstraintSet::new( - vec![ConstraintInvocation::hard( - REQUIRED, - OCCURRENCE, - ProgramTestInvocationV1::exact(Srgb8::new([0x80; 3])), - )], - vec![], - ), - vec![OutputBinding::new(OUTPUT, TRANSLUCENT)], - ProgramTestEvaluatorV1, - ); - let mut owner = program.compile().unwrap().into_owner(); - let wrong_binding = stream_binding(OTHER_GROUP, STREAM_A); - - reset_program_test_evaluation_count(); - let (result, allocations) = - crate::test_support::measured_allocations(|| owner.attach(wrong_binding)); - let Err(error) = result else { - panic!("a stream from another observation group must not attach"); - }; - assert_eq!( - error, - PointRenderAttachError::ObservationGroupMismatch { - expected: GROUP, - actual: OTHER_GROUP, - } - ); - assert_eq!(allocations, 0); - assert_eq!(program_test_evaluation_count(), 0); - - let retry = owner - .attach(default_stream_binding()) - .expect("a rejected foreign group must leave attach retryable"); - assert_eq!(retry.stream(), STREAM_A); - assert_eq!(program_test_evaluation_count(), 0); - drop(retry); - - owner.dispose(); - let (result, allocations) = - crate::test_support::measured_allocations(|| owner.attach(wrong_binding)); - let Err(error) = result else { - panic!("a disposed owner must not attach"); - }; - assert_eq!(error, PointRenderAttachError::Disposed); - assert_eq!(allocations, 0); -} - -#[test] -fn wrong_stream_precedes_shape_revision_callback_evaluation_and_state_mutation() { - let program = base_program( - 0.5, - BACKDROP, - ConstraintSet::new( - vec![ConstraintInvocation::hard( - REQUIRED, - OCCURRENCE, - ProgramTestInvocationV1::exact(Srgb8::new([0x80; 3])), - )], - vec![], - ), - vec![OutputBinding::new(OUTPUT, TRANSLUCENT)], - ProgramTestEvaluatorV1, - ); - let owner = program.compile().unwrap().into_owner(); - let mut session = owner.attach(default_stream_binding()).unwrap(); - session - .update_canonical_present(STREAM_A, 10, 1, |_| Srgb8::new([0xff; 3])) - .unwrap(); - let SessionState::Ready { current } = session.state() else { - unreachable!() - }; - let retained_storage = current.storage_pointers_for_test(); - let retained_output = current.output(OUTPUT); - - let wrong_present = SurfaceUpdate::present(STREAM_B, 9, &[]); - let wrong_unavailable = SurfaceUpdate::unavailable(STREAM_B, 9, 77); - for update in [wrong_present, wrong_unavailable] { - reset_program_test_evaluation_count(); - crate::composition::reset_source_over_evaluation_count(); - let (result, allocations) = - crate::test_support::measured_allocations(|| session.update(update).map(|_| ())); - assert_eq!( - result, - Err(SessionUpdateError::ObservationStreamMismatch { - expected: STREAM_A, - actual: STREAM_B, - }) - ); - assert_eq!(allocations, 0); - assert_eq!(program_test_evaluation_count(), 0); - assert_eq!(crate::composition::source_over_evaluation_count(), 0); - let SessionState::Ready { current } = session.state() else { - panic!("wrong-stream public update mutated the session state"); - }; - assert_eq!(current.revision(), 10); - assert_eq!(current.storage_pointers_for_test(), retained_storage); - assert_eq!(current.output(OUTPUT), retained_output); - } - - reset_program_test_evaluation_count(); - crate::composition::reset_source_over_evaluation_count(); - let reads = Cell::new(0); - let (result, allocations) = crate::test_support::measured_allocations(|| { - session - .update_canonical_present(STREAM_B, 9, 0, |_| { - reads.set(reads.get() + 1); - Srgb8::new([0; 3]) - }) - .map(|_| ()) - }); - assert_eq!( - result, - Err(SessionUpdateError::ObservationStreamMismatch { - expected: STREAM_A, - actual: STREAM_B, - }) - ); - assert_eq!(reads.get(), 0); - assert_eq!(allocations, 0); - assert_eq!(program_test_evaluation_count(), 0); - assert_eq!(crate::composition::source_over_evaluation_count(), 0); - let SessionState::Ready { current } = session.state() else { - panic!("wrong-stream canonical update mutated the session state"); - }; - assert_eq!(current.revision(), 10); - assert_eq!(current.storage_pointers_for_test(), retained_storage); - assert_eq!(current.output(OUTPUT), retained_output); -} - -#[test] -fn sessions_bound_to_distinct_streams_keep_independent_revision_watermarks() { - let owner = compiled_exact(Srgb8::new([0x80; 3])).into_owner(); - let mut session_a = owner.attach(stream_binding(GROUP, STREAM_A)).unwrap(); - let mut session_b = owner.attach(stream_binding(GROUP, STREAM_B)).unwrap(); - assert_eq!(session_a.stream(), STREAM_A); - assert_eq!(session_b.stream(), STREAM_B); - - for (session, stream, revision) in [ - (&mut session_a, STREAM_A, 100), - (&mut session_b, STREAM_B, 1), - ] { - let (result, allocations) = crate::test_support::measured_allocations(|| { - session - .update_canonical_present(stream, revision, 1, |_| Srgb8::new([0xff; 3])) - .map(|_| ()) - }); - assert!(result.is_ok()); - assert_eq!(allocations, 0); - } - - assert_eq!( - update_error( - session_a.update_canonical_present(STREAM_A, 99, 1, |_| Srgb8::new([0xff; 3])) - ), - SessionUpdateError::::RevisionOutOfOrder { - current: 100, - incoming: 99, - } - ); - session_b - .update_canonical_present(STREAM_B, 2, 1, |_| Srgb8::new([0xff; 3])) - .unwrap(); - - let SessionState::Ready { current: current_a } = session_a.state() else { - unreachable!() - }; - let SessionState::Ready { current: current_b } = session_b.state() else { - unreachable!() - }; - assert_eq!(current_a.revision(), 100); - assert_eq!(current_b.revision(), 2); -} - -#[test] -fn wcag_report_only_uses_visible_808080_but_emits_black_half_alpha_paint() { - let criterion = Wcag22CriterionV1::Sc143TextDefault; - let program = wcag_program( - vec![], - vec![ConstraintInvocation::report_only( - REQUIRED, OCCURRENCE, criterion, - )], - ); - let owner = program.compile().unwrap().into_owner(); - let mut session = owner.attach(default_stream_binding()).unwrap(); - let (result, allocations) = crate::test_support::measured_allocations(|| { - session - .update_canonical_present(STREAM_A, 1, 1, |_| Srgb8::new([0xff; 3])) - .map(|_| ()) - }); - assert!(result.is_ok()); - assert_eq!(allocations, 0); - let SessionState::Ready { current } = session.state() else { - panic!("report-only violation must commit Ready"); - }; - assert!( - current - .surfaces() - .eq([SurfaceSignal::new(SURFACE_PORT, Srgb8::new([0xff; 3]))]) - ); - - let output = current.output(OUTPUT).expect("compiled output slot"); - assert_eq!(output.output(), OUTPUT); - assert_eq!(output.paint(), TRANSLUCENT); - assert_eq!(output.value().source(), Srgb8::new([0; 3])); - assert_eq!(output.value().opacity().value(), 0.5); - assert_eq!(output.value().opacity().bits(), 0.5f64.to_bits()); - - let mut report = current.report(); - let Some(ConstraintReportEntry::ReportOnly(assessment)) = report.next() else { - panic!("WCAG diagnostic must retain report-only mode"); - }; - assert_eq!(assessment.constraint(), REQUIRED); - assert_eq!(assessment.target(), OCCURRENCE); - let ConstraintOutcome::Violation(evidence) = assessment.outcome() else { - panic!("#808080 on white must violate text-default WCAG"); - }; - let applicable = evidence.measurement().value(); - assert_eq!(applicable.criterion(), criterion); - assert_eq!(applicable.decision(), Wcag22ApplicableDecisionV1::Fail); - assert_eq!(applicable.measurement().foreground, [0x80; 3]); - assert_eq!(applicable.measurement().background, [0xff; 3]); - assert!(report.next().is_none()); -} - -#[test] -fn wcag_hard_violation_commits_conflict_with_full_report_and_no_current_output() { - let program = wcag_program( - vec![ConstraintInvocation::hard( - REQUIRED, - OCCURRENCE, - Wcag22CriterionV1::Sc143TextDefault, - )], - vec![], - ); - let owner = program.compile().unwrap().into_owner(); - let mut session = owner.attach(default_stream_binding()).unwrap(); - let SessionState::Conflict { current, previous } = session - .update_canonical_present(STREAM_A, 1, 1, |_| Srgb8::new([0xff; 3])) - .unwrap() - else { - panic!("mandatory WCAG violation must commit Conflict"); - }; - assert!(previous.is_none()); - assert_eq!(current.revision(), 1); - assert!( - current - .surfaces() - .eq([SurfaceSignal::new(SURFACE_PORT, Srgb8::new([0xff; 3]))]) - ); - let entries = current.report().collect::>(); - assert_eq!(entries.len(), 1); - let ConstraintReportEntry::Hard(assessment) = entries[0] else { - panic!("mandatory result lost its hard type"); - }; - assert!(matches!( - assessment.outcome(), - ConstraintOutcome::Violation(_) - )); -} - -#[test] -fn all_constraints_run_before_hard_gate_and_report_order_is_canonical() { - let low_hard = ConstraintId::new(1); - let middle_report = ConstraintId::new(2); - let high_hard = ConstraintId::new(3); - let program = exact_program( - vec![ - ConstraintInvocation::hard(high_hard, OCCURRENCE, Srgb8::new([0x80; 3])), - ConstraintInvocation::hard(low_hard, OCCURRENCE, Srgb8::new([0x81; 3])), - ], - vec![ConstraintInvocation::report_only( - middle_report, - OCCURRENCE, - Srgb8::new([0x80; 3]), - )], - ); - let owner = program.compile().unwrap().into_owner(); - let mut session = owner.attach(default_stream_binding()).unwrap(); - let SessionState::Conflict { current, .. } = session - .update_canonical_present(STREAM_A, 1, 1, |_| Srgb8::new([0xff; 3])) - .unwrap() - else { - panic!("one hard violation must gate the complete result"); - }; - let entries = current.report().collect::>(); - assert_eq!( - entries - .iter() - .map(|entry| entry.constraint()) - .collect::>(), - vec![low_hard, middle_report, high_hard] - ); - assert!(entries.iter().all(|entry| entry.target() == OCCURRENCE)); - assert!(matches!(entries[0], ConstraintReportEntry::Hard(_))); - assert!(matches!(entries[1], ConstraintReportEntry::ReportOnly(_))); - assert!(matches!(entries[2], ConstraintReportEntry::Hard(_))); - let ConstraintReportEntry::Hard(last) = entries[2] else { - unreachable!() - }; - assert_eq!( - exact_outcome(last), - (Srgb8::new([0x80; 3]), Srgb8::new([0x80; 3])) - ); -} - -#[test] -fn report_only_violation_never_gates_the_routed_encoded_paint() { - let program = exact_program( - vec![ConstraintInvocation::hard( - REQUIRED, - OCCURRENCE, - Srgb8::new([0x80; 3]), - )], - vec![ConstraintInvocation::report_only( - ConstraintId::new(51), - OCCURRENCE, - Srgb8::new([0x81; 3]), - )], - ); - let owner = program.compile().unwrap().into_owner(); - let mut session = owner.attach(default_stream_binding()).unwrap(); - let SessionState::Ready { current } = session - .update_canonical_present(STREAM_A, 1, 1, |_| Srgb8::new([0xff; 3])) - .unwrap() - else { - panic!("report-only violation cannot gate"); - }; - assert_eq!(current.outputs().count(), 1); - assert!(matches!( - current.report().nth(1), - Some(ConstraintReportEntry::ReportOnly(assessment)) - if matches!(assessment.outcome(), ConstraintOutcome::Violation(_)) - )); -} - -#[test] -fn output_slot_renaming_changes_only_route_and_reuses_the_exact_encoded_paint() { - fn resolve(output: OutputSlotId) -> crate::program_session::OutputValueV1 { - let program = base_program( - 0.5, - BACKDROP, - ConstraintSet::new( - vec![ConstraintInvocation::hard( - REQUIRED, - OCCURRENCE, - Srgb8::new([0x80; 3]), - )], - vec![], - ), - vec![OutputBinding::new(output, TRANSLUCENT)], - ExactSrgb8IdentityV1, - ); - let owner = program.compile().unwrap().into_owner(); - let mut session = owner.attach(default_stream_binding()).unwrap(); - let SessionState::Ready { current } = session - .update_canonical_present(STREAM_A, 1, 1, |_| Srgb8::new([0xff; 3])) - .unwrap() - else { - unreachable!() - }; - current.outputs().next().unwrap() - } - - let left = resolve(OutputSlotId::new(1)); - let renamed = resolve(OutputSlotId::new(999)); - assert_ne!(left.output(), renamed.output()); - assert_eq!(left.paint(), renamed.paint()); - assert_eq!(left.value(), renamed.value()); - assert_eq!(left.paint(), left.value().id()); - assert_eq!(renamed.paint(), renamed.value().id()); - assert_eq!( - left.value(), - EncodedPointPaintV1::from_admitted( - TRANSLUCENT, - Srgb8::new([0; 3]), - crate::composition::AdmittedOpacityV1::new(0.5).unwrap(), - ) - ); - assert_eq!(left.value().opacity().bits(), 0.5f64.to_bits()); -} - -#[test] -fn consistent_paint_id_renaming_changes_only_nominal_identity() { - fn resolve(solid: PaintId, translucent: PaintId) -> EncodedPointPaintV1 { - let program = Program::new( - vec![ColorInput::new(COLOR, Srgb8::new([0; 3]))], - observation_group(vec![SURFACE_PORT]), - vec![OpacityInput::new(OPACITY, 0.5)], - vec![ - Paint::Solid { - id: solid, - color: COLOR, - }, - Paint::Opacity { - id: translucent, - source: solid, - opacity: OPACITY, - }, - ], - vec![Surface::Input { - id: BACKDROP, - input: SURFACE_PORT, - }], - vec![Occurrence::new( - OCCURRENCE, - translucent, - BACKDROP, - CompositionProfile::EncodedSrgb8SourceOverV1, - )], - ConstraintSet::new( - vec![ConstraintInvocation::hard( - REQUIRED, - OCCURRENCE, - Srgb8::new([0x80; 3]), - )], - vec![], - ), - vec![OutputBinding::new(OUTPUT, translucent)], - ExactSrgb8IdentityV1, - ); - let owner = program.compile().unwrap().into_owner(); - let mut session = owner.attach(default_stream_binding()).unwrap(); - let SessionState::Ready { current } = session - .update_canonical_present(STREAM_A, 1, 1, |_| Srgb8::new([0xff; 3])) - .unwrap() - else { - unreachable!() - }; - let output = current.output(OUTPUT).unwrap(); - let value: EncodedPointPaintV1 = output.value(); - assert_eq!(output.paint(), value.id()); - value - } - - let original = resolve(SOLID, TRANSLUCENT); - let renamed = resolve(PaintId::new(1_010), PaintId::new(1_011)); - - assert_ne!(original.id(), renamed.id()); - assert_eq!(original.source(), renamed.source()); - assert_eq!(original.opacity(), renamed.opacity()); - assert_eq!(original.source(), Srgb8::new([0; 3])); - assert_eq!(original.opacity().bits(), 0.5f64.to_bits()); -} - -#[test] -fn point_transport_values_and_classifier_payload_are_nominal_id_invariant_before_f2_binding() { - type PreF2PaintProjection = (PaintId, Srgb8, u64); - type ClassifierPayloadProjection = (ConstraintId, OccurrenceId, Srgb8, Srgb8); - - fn resolve( - group: ObservationGroupId, - stream: ObservationStreamId, - ) -> (PreF2PaintProjection, ClassifierPayloadProjection) { - let program = base_program_in_group( - group, - 0.5, - BACKDROP, - ConstraintSet::new( - vec![ConstraintInvocation::hard( - REQUIRED, - OCCURRENCE, - Srgb8::new([0x80; 3]), - )], - vec![], - ), - vec![OutputBinding::new(OUTPUT, TRANSLUCENT)], - ExactSrgb8IdentityV1, - ); - let owner = program.compile().unwrap().into_owner(); - let mut session = owner.attach(stream_binding(group, stream)).unwrap(); - let SessionState::Ready { current } = session - .update_canonical_present(stream, 1, 1, |_| Srgb8::new([0xff; 3])) - .unwrap() - else { - unreachable!() - }; - let output = current.output(OUTPUT).unwrap(); - let Some(ConstraintReportEntry::Hard(assessment)) = current.report().next() else { - unreachable!() - }; - let (target, actual) = exact_outcome(assessment); - ( - ( - output.paint(), - output.value().source(), - output.value().opacity().bits(), - ), - (assessment.constraint(), assessment.target(), target, actual), - ) - } - - let original = resolve(GROUP, STREAM_A); - let renamed = resolve(OTHER_GROUP, STREAM_B); - assert_eq!(original, renamed); -} - -#[test] -fn evaluator_error_preserves_prior_snapshot_head_and_all_owned_evidence() { - reset_program_test_evaluation_count(); - let first = ConstraintId::new(1); - let failing = ConstraintId::new(2); - let program = base_program( - 0.5, - BACKDROP, - ConstraintSet::new( - vec![ConstraintInvocation::hard( - first, - OCCURRENCE, - ProgramTestInvocationV1::exact(Srgb8::new([0x80; 3])), - )], - vec![ConstraintInvocation::report_only( - failing, - OCCURRENCE, - ProgramTestInvocationV1::fail_once_when_armed(Srgb8::new([0x80; 3])), - )], - ), - vec![OutputBinding::new(OUTPUT, TRANSLUCENT)], - ProgramTestEvaluatorV1, - ); - let owner = program.compile().unwrap().into_owner(); - let mut session = owner.attach(default_stream_binding()).unwrap(); - session - .update_canonical_present(STREAM_A, 1, 1, |_| Srgb8::new([0xff; 3])) - .unwrap(); - let SessionState::Ready { current } = session.state() else { - panic!("control update must be Ready"); - }; - let retained_storage = current.storage_pointers_for_test(); - let retained_output = current.output(OUTPUT); - let retained_report_ids = current - .report() - .map(ConstraintReportEntry::constraint) - .collect::>(); - - arm_program_test_failure_once(); - let error = - update_error(session.update_canonical_present(STREAM_A, 2, 1, |_| Srgb8::new([0xff; 3]))); - assert_eq!( - error, - SessionUpdateError::Evaluator { - constraint: failing, - source: ProgramTestEvaluationErrorV1::Forced, - } - ); - let SessionState::Ready { current } = session.state() else { - panic!("evaluator Err must leave the prior state exact"); - }; - assert_eq!(current.revision(), 1); - assert_eq!(current.storage_pointers_for_test(), retained_storage); - assert_eq!(current.output(OUTPUT), retained_output); - assert_eq!( - current - .report() - .map(ConstraintReportEntry::constraint) - .collect::>(), - retained_report_ids - ); - - let SessionState::Ready { current } = session - .update_canonical_present(STREAM_A, 2, 1, |_| Srgb8::new([0xff; 3])) - .unwrap() - else { - panic!("same incoming revision must be retryable after evaluator Err"); - }; - assert_eq!(current.revision(), 2); - - session - .update_canonical_present(STREAM_A, 3, 1, |_| Srgb8::new([0; 3])) - .unwrap(); - let SessionState::Conflict { current, previous } = session.state() else { - panic!("black backdrop must create the retained Conflict control"); - }; - let retained_conflict = current.storage_pointers_for_test(); - let retained_previous = previous - .as_ref() - .expect("Conflict after Ready must retain full prior evidence") - .storage_pointers_for_test(); - - arm_program_test_failure_once(); - let error = - update_error(session.update_canonical_present(STREAM_A, 4, 1, |_| Srgb8::new([0; 3]))); - assert_eq!( - error, - SessionUpdateError::Evaluator { - constraint: failing, - source: ProgramTestEvaluationErrorV1::Forced, - } - ); - let SessionState::Conflict { current, previous } = session.state() else { - panic!("evaluator Err must preserve retained Conflict exactly"); - }; - assert_eq!(current.revision(), 3); - assert_eq!(current.storage_pointers_for_test(), retained_conflict); - assert_eq!( - previous.as_ref().unwrap().storage_pointers_for_test(), - retained_previous - ); - - let SessionState::Conflict { current, previous } = session - .update_canonical_present(STREAM_A, 4, 1, |_| Srgb8::new([0; 3])) - .unwrap() - else { - panic!("retry after evaluator Err must execute and commit Conflict"); - }; - assert_eq!(current.revision(), 4); - assert_eq!( - previous.as_ref().unwrap().storage_pointers_for_test(), - retained_previous - ); - assert_eq!(program_test_evaluation_count(), 12); -} - -#[test] -fn three_frames_preserve_verified_and_replace_complete_conflict_reports_without_clone() { - let owner = compiled_exact(Srgb8::new([0x80; 3])).into_owner(); - let mut session = owner.attach(default_stream_binding()).unwrap(); - session - .update_canonical_present(STREAM_A, 1, 1, |_| Srgb8::new([0xff; 3])) - .unwrap(); - let SessionState::Ready { current } = session.state() else { - unreachable!() - }; - let verified_storage = current.storage_pointers_for_test(); - - session - .update_canonical_present(STREAM_A, 2, 1, |_| Srgb8::new([0; 3])) - .unwrap(); - let SessionState::Conflict { current, previous } = session.state() else { - panic!("black backdrop must violate exact #808080"); - }; - let conflict_two_storage = current.storage_pointers_for_test(); - assert_ne!(conflict_two_storage, verified_storage); - assert_eq!( - previous - .as_ref() - .expect("Conflict retains prior verified evidence") - .storage_pointers_for_test(), - verified_storage - ); - - session - .update_canonical_present(STREAM_A, 3, 1, |_| Srgb8::new([0x20; 3])) - .unwrap(); - let SessionState::Conflict { current, previous } = session.state() else { - unreachable!() - }; - let conflict_three_storage = current.storage_pointers_for_test(); - assert_ne!(conflict_three_storage, verified_storage); - assert_ne!(conflict_three_storage, conflict_two_storage); - assert_eq!( - previous - .as_ref() - .expect("new Conflict must retain the one verified witness") - .storage_pointers_for_test(), - verified_storage - ); - assert_eq!(current.report().count(), 1); - - session - .update_canonical_present(STREAM_A, 4, 1, |_| Srgb8::new([0xff; 3])) - .unwrap(); - let SessionState::Ready { current } = session.state() else { - panic!("Conflict must recover directly to a new verified Snapshot"); - }; - let recovered_storage = current.storage_pointers_for_test(); - assert_ne!(recovered_storage, verified_storage); - assert_ne!(recovered_storage, conflict_three_storage); - - session - .update_canonical_present(STREAM_A, 5, 1, |_| Srgb8::new([0; 3])) - .unwrap(); - let SessionState::Conflict { previous, .. } = session.state() else { - unreachable!() - }; - assert_eq!( - previous.as_ref().unwrap().storage_pointers_for_test(), - recovered_storage - ); - - let SessionState::Stale { - previous, - current_unavailable, - } = session - .update(SurfaceUpdate::unavailable(STREAM_A, 6, 9)) - .unwrap() - else { - panic!("Unknown after Conflict(previous) must retain that full Snapshot"); - }; - assert_eq!(previous.storage_pointers_for_test(), recovered_storage); - assert_eq!(current_unavailable.revision(), 6); - assert_eq!(current_unavailable.reason(), 9); -} - -#[test] -fn same_revision_conflict_replay_is_idempotent_and_changed_payload_conflicts() { - let owner = compiled_exact(Srgb8::new([0x80; 3])).into_owner(); - let mut session = owner.attach(default_stream_binding()).unwrap(); - let black = Srgb8::new([0; 3]); - session - .update_canonical_present(STREAM_A, 1, 1, |_| black) - .unwrap(); - let SessionState::Conflict { current, .. } = session.state() else { - unreachable!() - }; - let storage = current.storage_pointers_for_test(); - crate::composition::reset_source_over_evaluation_count(); - - assert!( - session - .update_canonical_present(STREAM_A, 1, 1, |_| black) - .is_ok() - ); - assert_eq!(crate::composition::source_over_evaluation_count(), 0); - let SessionState::Conflict { current, .. } = session.state() else { - unreachable!() - }; - assert_eq!(current.storage_pointers_for_test(), storage); - - let error = - update_error(session.update_canonical_present(STREAM_A, 1, 1, |_| Srgb8::new([0xff; 3]))); - assert_eq!( - error, - SessionUpdateError::::RevisionConflict { revision: 1 } - ); - assert_eq!(crate::composition::source_over_evaluation_count(), 0); -} - -#[test] -fn three_port_same_revision_conflict_reads_every_value_without_execution_or_allocation() { - const PORT_A: SurfaceInputPortId = SurfaceInputPortId::new(10); - const PORT_B: SurfaceInputPortId = SurfaceInputPortId::new(20); - const PORT_C: SurfaceInputPortId = SurfaceInputPortId::new(30); - const SURFACE_A: SurfaceId = SurfaceId::new(110); - const SURFACE_B: SurfaceId = SurfaceId::new(120); - const SURFACE_C: SurfaceId = SurfaceId::new(130); - let program = Program::new( - vec![ColorInput::new(COLOR, Srgb8::new([0; 3]))], - observation_group(vec![PORT_C, PORT_A, PORT_B]), - vec![OpacityInput::new(OPACITY, 0.5)], - vec![ - Paint::Solid { - id: SOLID, - color: COLOR, - }, - Paint::Opacity { - id: TRANSLUCENT, - source: SOLID, - opacity: OPACITY, - }, - ], - vec![ - Surface::Input { - id: SURFACE_C, - input: PORT_C, - }, - Surface::Input { - id: SURFACE_A, - input: PORT_A, - }, - Surface::Input { - id: SURFACE_B, - input: PORT_B, - }, - ], - vec![Occurrence::new( - OCCURRENCE, - TRANSLUCENT, - SURFACE_B, - CompositionProfile::EncodedSrgb8SourceOverV1, - )], - ConstraintSet::new( - vec![ConstraintInvocation::hard( - REQUIRED, - OCCURRENCE, - Srgb8::new([0x80; 3]), - )], - vec![], - ), - vec![OutputBinding::new(OUTPUT, TRANSLUCENT)], - ExactSrgb8IdentityV1, - ); - let compiled = program.compile().unwrap(); - assert_eq!(compiled.surface_input_ports(), &[PORT_A, PORT_B, PORT_C]); - let owner = compiled.into_owner(); - let mut session = owner.attach(default_stream_binding()).unwrap(); - let committed = [ - Srgb8::new([0x10; 3]), - Srgb8::new([0; 3]), - Srgb8::new([0x30; 3]), - ]; - session - .update_canonical_present(STREAM_A, 5, 3, |index| committed[index]) - .unwrap(); - let SessionState::Conflict { current, .. } = session.state() else { - unreachable!() - }; - let storage = current.storage_pointers_for_test(); - - crate::composition::reset_source_over_evaluation_count(); - let replay = ReadProbe::new(committed); - let (result, allocations) = crate::test_support::measured_allocations(|| { - session - .update_canonical_present(STREAM_A, 5, 3, |index| replay.read(index)) - .map(|_| ()) - }); - assert!(result.is_ok()); - assert_eq!(replay.reads(), [1, 1, 1]); - assert_eq!(allocations, 0); - assert_eq!(crate::composition::source_over_evaluation_count(), 0); - let SessionState::Conflict { current, .. } = session.state() else { - unreachable!() - }; - assert_eq!(current.storage_pointers_for_test(), storage); - - let mut mismatched = committed; - mismatched[0] = Srgb8::new([0xff; 3]); - let mismatch = ReadProbe::new(mismatched); - let (result, allocations) = crate::test_support::measured_allocations(|| { - session - .update_canonical_present(STREAM_A, 5, 3, |index| mismatch.read(index)) - .map(|_| ()) - }); - assert_eq!( - result, - Err(SessionUpdateError::::RevisionConflict { revision: 5 }) - ); - assert_eq!(mismatch.reads(), [1, 1, 1]); - assert_eq!(allocations, 0); - assert_eq!(crate::composition::source_over_evaluation_count(), 0); - let SessionState::Conflict { current, .. } = session.state() else { - unreachable!() - }; - assert_eq!(current.storage_pointers_for_test(), storage); - - let (result, allocations) = crate::test_support::measured_allocations(|| { - session - .update(SurfaceUpdate::unavailable(STREAM_A, 5, 91)) - .map(|_| ()) - }); - assert_eq!( - result, - Err(SessionUpdateError::::RevisionConflict { revision: 5 }) - ); - assert_eq!(allocations, 0); - let SessionState::Conflict { current, .. } = session.state() else { - unreachable!() - }; - assert_eq!(current.storage_pointers_for_test(), storage); -} - -#[test] -fn typed_schema_revision_and_lifetime_failures_are_atomic() { - let mut owner = compiled_exact(Srgb8::new([0x80; 3])).into_owner(); - let mut session = owner.attach(default_stream_binding()).unwrap(); - let wrong = [SurfaceSignal::new( - SurfaceInputPortId::new(999), - Srgb8::new([0xff; 3]), - )]; - crate::composition::reset_source_over_evaluation_count(); - let (result, allocations) = crate::test_support::measured_allocations(|| { - session - .update(SurfaceUpdate::present(STREAM_A, 1, &wrong)) - .map(|_| ()) - }); - assert_eq!( - result, - Err(SessionUpdateError::::SurfaceInputPortMismatch { - index: 0, - expected: SURFACE_PORT, - actual: SurfaceInputPortId::new(999), - }) - ); - assert_eq!(allocations, 0); - assert_eq!(crate::composition::source_over_evaluation_count(), 0); - assert!(matches!( - session.state(), - SessionState::Waiting { - current_unavailable: None - } - )); - - session - .update_canonical_present(STREAM_A, 5, 1, |_| Srgb8::new([0xff; 3])) - .unwrap(); - let SessionState::Ready { current } = session.state() else { - unreachable!() - }; - let retained_storage = current.storage_pointers_for_test(); - let reads = Cell::new(0); - crate::composition::reset_source_over_evaluation_count(); - let (result, allocations) = crate::test_support::measured_allocations(|| { - session - .update_canonical_present(STREAM_A, 4, 1, |_| { - reads.set(reads.get() + 1); - Srgb8::new([0; 3]) - }) - .map(|_| ()) - }); - assert_eq!( - result, - Err(SessionUpdateError::::RevisionOutOfOrder { - current: 5, - incoming: 4, - }) - ); - assert_eq!(reads.get(), 0); - assert_eq!(allocations, 0); - assert_eq!(crate::composition::source_over_evaluation_count(), 0); - let SessionState::Ready { current } = session.state() else { - unreachable!() - }; - assert_eq!(current.revision(), 5); - assert_eq!(current.storage_pointers_for_test(), retained_storage); - - owner.dispose(); - let (result, allocations) = crate::test_support::measured_allocations(|| { - session - .update(SurfaceUpdate::unavailable(STREAM_A, 6, 1)) - .map(|_| ()) - }); - assert_eq!( - result, - Err(SessionUpdateError::::ProgramExpired) - ); - assert_eq!(allocations, 0); -} - -#[test] -fn replacement_revokes_old_sessions_and_compile_error_keeps_live_epoch() { - let mut owner = compiled_exact(Srgb8::new([0x80; 3])).into_owner(); - let mut old = owner.attach(default_stream_binding()).unwrap(); - assert_eq!( - compile_error(base_program( - 1.25, - BACKDROP, - ConstraintSet::new( - vec![ConstraintInvocation::hard( - REQUIRED, - OCCURRENCE, - Srgb8::new([0x80; 3]), - )], - vec![], - ), - vec![OutputBinding::new(OUTPUT, TRANSLUCENT)], - ExactSrgb8IdentityV1, - )), - ProgramCompileError::OpacityOutOfDomain { input: OPACITY } - ); - assert!( - old.update_canonical_present(STREAM_A, 1, 1, |_| Srgb8::new([0xff; 3])) - .is_ok() - ); - - owner.replace(compiled_exact_in_group(OTHER_GROUP, Srgb8::new([0x80; 3]))); - assert_eq!(owner.observation_group_id(), Some(OTHER_GROUP)); - assert_eq!( - update_error(old.update_canonical_present(STREAM_A, 2, 1, |_| Srgb8::new([0xff; 3]))), - SessionUpdateError::::ProgramExpired - ); - - let (result, allocations) = - crate::test_support::measured_allocations(|| owner.attach(default_stream_binding())); - let Err(error) = result else { - panic!("replacement must reject a binding for the previous group"); - }; - assert_eq!( - error, - PointRenderAttachError::ObservationGroupMismatch { - expected: OTHER_GROUP, - actual: GROUP, - } - ); - assert_eq!(allocations, 0); - - let mut fresh = owner.attach(stream_binding(OTHER_GROUP, STREAM_B)).unwrap(); - fresh - .update_canonical_present(STREAM_B, 1, 1, |_| Srgb8::new([0xff; 3])) - .unwrap(); - owner.dispose(); - let (result, allocations) = crate::test_support::measured_allocations(|| { - fresh - .update(SurfaceUpdate::unavailable(STREAM_A, 2, 1)) - .map(|_| ()) - }); - assert_eq!( - result, - Err(SessionUpdateError::::ProgramExpired), - "expiry must precede even a mismatched stream" - ); - assert_eq!(allocations, 0); -} - -#[test] -fn present_ready_conflict_stale_and_recovery_allocate_zero_after_attach() { - let owner = compiled_exact(Srgb8::new([0x80; 3])).into_owner(); - let mut session = owner.attach(default_stream_binding()).unwrap(); - let white = [SurfaceSignal::new(SURFACE_PORT, Srgb8::new([0xff; 3]))]; - let black = [SurfaceSignal::new(SURFACE_PORT, Srgb8::new([0; 3]))]; - - for update in [ - SurfaceUpdate::present(STREAM_A, 1, &white), - SurfaceUpdate::present(STREAM_A, 2, &black), - SurfaceUpdate::unavailable(STREAM_A, 3, 7), - SurfaceUpdate::present(STREAM_A, 4, &white), - ] { - let (result, allocations) = - crate::test_support::measured_allocations(|| session.update(update).map(|_| ())); - assert!(result.is_ok()); - assert_eq!(allocations, 0); - } } #[test] @@ -1608,160 +440,3 @@ fn canonical_helpers_and_checked_cardinality_fail_closed() { &canonical, )); } - -#[test] -fn callback_is_not_read_before_lifetime_cardinality_or_revision_admission() { - let mut expired = { - let owner = compiled_exact(Srgb8::new([0x80; 3])).into_owner(); - owner.attach(default_stream_binding()).unwrap() - }; - let reads = Cell::new(0); - let (result, allocations) = crate::test_support::measured_allocations(|| { - expired - .update_canonical_present(STREAM_B, 1, 1, |_| { - reads.set(reads.get() + 1); - Srgb8::new([0xff; 3]) - }) - .map(|_| ()) - }); - assert_eq!( - result, - Err(SessionUpdateError::::ProgramExpired) - ); - assert_eq!(reads.get(), 0); - assert_eq!(allocations, 0); - - let owner = compiled_exact(Srgb8::new([0x80; 3])).into_owner(); - let mut session = owner.attach(default_stream_binding()).unwrap(); - for actual in [0, 2] { - let reads = Cell::new(0); - let (result, allocations) = crate::test_support::measured_allocations(|| { - session - .update_canonical_present(STREAM_A, 1, actual, |_| { - reads.set(reads.get() + 1); - Srgb8::new([0xff; 3]) - }) - .map(|_| ()) - }); - assert_eq!( - result, - Err( - SessionUpdateError::::SurfaceInputPortLengthMismatch { - expected: 1, - actual, - } - ) - ); - assert_eq!(reads.get(), 0); - assert_eq!(allocations, 0); - } - - session - .update(SurfaceUpdate::unavailable(STREAM_A, 7, 12)) - .unwrap(); - let reads = Cell::new(0); - let (result, allocations) = crate::test_support::measured_allocations(|| { - session - .update_canonical_present(STREAM_A, 7, 1, |_| { - reads.set(reads.get() + 1); - Srgb8::new([0xff; 3]) - }) - .map(|_| ()) - }); - assert_eq!( - result, - Err(SessionUpdateError::::RevisionConflict { revision: 7 }) - ); - assert_eq!(reads.get(), 0); - assert_eq!(allocations, 0); -} - -#[test] -fn canonical_two_port_group_assesses_multiple_occurrences_but_emits_one_paint() { - const OTHER_PORT: SurfaceInputPortId = SurfaceInputPortId::new(4); - const OTHER_SURFACE: SurfaceId = SurfaceId::new(22); - const OTHER_OCCURRENCE: OccurrenceId = OccurrenceId::new(31); - let group = ObservationGroup::new(GROUP, vec![OTHER_PORT, SURFACE_PORT]); - assert_eq!(group.id(), GROUP); - assert_eq!(group.surface_input_ports(), &[OTHER_PORT, SURFACE_PORT]); - let program = Program::new( - vec![ColorInput::new(COLOR, Srgb8::new([0; 3]))], - group, - vec![OpacityInput::new(OPACITY, 0.5)], - vec![ - Paint::Solid { - id: SOLID, - color: COLOR, - }, - Paint::Opacity { - id: TRANSLUCENT, - source: SOLID, - opacity: OPACITY, - }, - ], - vec![ - Surface::Input { - id: BACKDROP, - input: SURFACE_PORT, - }, - Surface::Input { - id: OTHER_SURFACE, - input: OTHER_PORT, - }, - ], - vec![ - Occurrence::new( - OCCURRENCE, - TRANSLUCENT, - BACKDROP, - CompositionProfile::EncodedSrgb8SourceOverV1, - ), - Occurrence::new( - OTHER_OCCURRENCE, - TRANSLUCENT, - OTHER_SURFACE, - CompositionProfile::EncodedSrgb8SourceOverV1, - ), - ], - ConstraintSet::new( - vec![ConstraintInvocation::hard( - REQUIRED, - OCCURRENCE, - Srgb8::new([0x80; 3]), - )], - vec![ConstraintInvocation::report_only( - ConstraintId::new(51), - OTHER_OCCURRENCE, - Srgb8::new([0; 3]), - )], - ), - vec![OutputBinding::new(OUTPUT, TRANSLUCENT)], - ExactSrgb8IdentityV1, - ); - let compiled = program.compile().unwrap(); - assert_eq!(compiled.observation_group_id(), GROUP); - assert_eq!(compiled.surface_input_ports(), &[SURFACE_PORT, OTHER_PORT]); - let owner = compiled.into_owner(); - assert_eq!(owner.observation_group_id(), Some(GROUP)); - assert_eq!( - owner.surface_input_ports(), - Some(&[SURFACE_PORT, OTHER_PORT][..]) - ); - let mut session = owner.attach(default_stream_binding()).unwrap(); - assert_eq!(session.stream(), STREAM_A); - let signals = [ - SurfaceSignal::new(SURFACE_PORT, Srgb8::new([0xff; 3])), - SurfaceSignal::new(OTHER_PORT, Srgb8::new([0; 3])), - ]; - let SessionState::Ready { current } = session - .update(SurfaceUpdate::present(STREAM_A, 1, &signals)) - .unwrap() - else { - panic!("both exact occurrence contracts should pass"); - }; - assert_eq!(current.report().count(), 2); - let outputs = current.outputs().collect::>(); - assert_eq!(outputs.len(), 1); - assert_eq!(outputs[0].value().source(), Srgb8::new([0; 3])); - assert_eq!(outputs[0].value().opacity().bits(), 0.5f64.to_bits()); -} diff --git a/scripts/verify_point_support_surplus.py b/scripts/verify_point_support_surplus.py index eb541352..0b4feae4 100644 --- a/scripts/verify_point_support_surplus.py +++ b/scripts/verify_point_support_surplus.py @@ -57,7 +57,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 = ( - "06b0017fcfad05f582bae0d9ddc1fe1bb5240ca01fc30795a469c093cf9c1f87" + "c399666d9db623b1f0879912ab2ed7878d99d75d760327ad022b00e2eb30eb5e" ) EXPECTED_Q55_PROOF_SHA256 = ( "ac59cf89503170c789223b91d775213a19d4e571ef930f2ea609fcd51b14defd" From d4a5f38f4fa902ea22b5aefa6b6a7a88ec5862c9 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Wed, 22 Jul 2026 20:27:14 +0300 Subject: [PATCH 28/58] feat(core): execute compiled plans through sole Session Replace the point-support-specific lifecycle with one sealed monomorphized Session, attach reusable CompiledProgram epochs through strong ownership, and retain complete case-by-constraint reports. Bind every decision to the exact admitted observation before atomic commit and refresh release proof pins. --- .github/workflows/publish.yml | 2 +- ...rt-reference-surplus-q55-bps-proof-v1.json | 2 +- .../src/generic_boundary_tests.rs | 73 +- crates/labcolors-core/src/lib.rs | 2 +- crates/labcolors-core/src/observation.rs | 6 + crates/labcolors-core/src/point_support.rs | 158 ++-- .../labcolors-core/src/point_support_tests.rs | 26 +- crates/labcolors-core/src/program_session.rs | 459 +++++++++- .../src/program_session_tests.rs | 344 +++++++- crates/labcolors-core/src/session.rs | 445 +++------- crates/labcolors-core/src/session_tests.rs | 794 +++++++----------- scripts/verify-package-release.mjs | 2 +- scripts/verify_point_support_surplus.py | 14 +- 13 files changed, 1383 insertions(+), 944 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index cdea5f5a..d6761735 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -674,7 +674,7 @@ jobs: JSON.stringify(point.sourceBinding.exclusions) !== JSON.stringify(pointProof.source_binding_exclusions) || point.sourceBinding.closureSha256 !== pointProof.source_closure_sha256 || - pointProof.source_negative_controls !== 40 || + pointProof.source_negative_controls !== 42 || pointAlgebra?.method !== "exact-sparse-integer-polynomial-identities-plus-positive-denominator-order-lemma-v1" || pointAlgebra?.wolfram_language_cross_check?.query_sha256 !== 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 e2e61e9f..9dd07d2a 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":"e366d40bbddcf1d82b47884f3cd4c18c67d2a19a5b8e76a11489e636a9fa4351","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":"c399666d9db623b1f0879912ab2ed7878d99d75d760327ad022b00e2eb30eb5e","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":"57e6feeab15182322c258829d10214c31bb3c93fa58d8d47e37127db204cdc28"},{"kind":"rust-source","path":"crates/labcolors-core/src/composition.rs","sha256":"195a67327a3bd86d7816b634481389930bf68577bb1202fad14c2ea152df8625"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/exact.rs","sha256":"45bd09cd8f6f74860a0a3ffeb410ee2c3cf91b4868877abbc949e724b61e1ccd"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/mod.rs","sha256":"8b78b9a31e7ade3f7b99edf3fbcf2bc5d912509cd851e82f7380e2a19e25431e"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/wcag22.rs","sha256":"985e21abccd1fcb4a3f9624d5947ff3d1af7a1bf26b6b170ea8bbef77d6f1798"},{"kind":"rust-source","path":"crates/labcolors-core/src/hash.rs","sha256":"f97a0fd7d6ad3162f0f1dfb326fccfb7ed40da9a8fa67a5b8a239a1ae2ae49c3"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"80db959a77ecfcd6e2383f9097a3cd1e903edf5171f6cbd232aa3485043cabf1"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"2bf4c805fb95f5b091f6c30ceb4b425f4c8dffef5127da2853156d86a16ad9cd"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"974116bbbb797063d98fcce4f23b089fc05a588a765432c15a82315d4489ef94"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"2363bfbedcaf619c5b7d964cb3667095a030ce4a6cfd7b8db9a34a6dd300e102"},{"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":40,"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":"f1f4966b59d9314c0f1a10b4f7979d39539ca0e0fd0a44ff2fbf4437012337ed"} +{"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":"62871d3b874e4b4601c11db97f5ab73cd4e9781e462330bccda0d9dfdadcb5be","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":"2dba7f59bd0f8d665b79d3286527cc67a99d1dfe1f7604e6d19be3643e39ed5d","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":"57e6feeab15182322c258829d10214c31bb3c93fa58d8d47e37127db204cdc28"},{"kind":"rust-source","path":"crates/labcolors-core/src/composition.rs","sha256":"195a67327a3bd86d7816b634481389930bf68577bb1202fad14c2ea152df8625"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/exact.rs","sha256":"45bd09cd8f6f74860a0a3ffeb410ee2c3cf91b4868877abbc949e724b61e1ccd"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/mod.rs","sha256":"8b78b9a31e7ade3f7b99edf3fbcf2bc5d912509cd851e82f7380e2a19e25431e"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/wcag22.rs","sha256":"985e21abccd1fcb4a3f9624d5947ff3d1af7a1bf26b6b170ea8bbef77d6f1798"},{"kind":"rust-source","path":"crates/labcolors-core/src/hash.rs","sha256":"f97a0fd7d6ad3162f0f1dfb326fccfb7ed40da9a8fa67a5b8a239a1ae2ae49c3"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"9e7b2c8e8a1d02d387c5c92c995de8f8148296c74efadf895e71318fbf0f4b0e"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"bb5fc6bf4ad79e15a31dc6ec28341994f7cdd5e28efe60f9c5d7ae8bed9a9a63"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"b3be8242586c0d9952332e7c830551af1c033739e8148f836f873e6d9e304315"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"0a1fdf10c9ecae497cfcd447a6af937f2b07d43b81b2cd28a50d998e11197fe9"},{"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":42,"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":"d137ef2c6c780e4d0a8ee40b5f379c139ae116c1eaf753ce6060ac4e1f60d51d"} diff --git a/crates/labcolors-core/src/generic_boundary_tests.rs b/crates/labcolors-core/src/generic_boundary_tests.rs index 9f6fe2a8..13952cfd 100644 --- a/crates/labcolors-core/src/generic_boundary_tests.rs +++ b/crates/labcolors-core/src/generic_boundary_tests.rs @@ -131,15 +131,15 @@ fn shared_observation_ssot_has_one_backing_without_lifecycle_or_adapter_facades( assert_eq!( normalized_source_scope( SESSION_SOURCE, - "pub(crate) enum PointSupportSessionStateV1 {", - "impl PointSupportSessionStateV1", + "pub(crate) enum SessionState {", + "impl SessionState", ), concat!( - "pub(crate) enum PointSupportSessionStateV1 { ", + "pub(crate) enum SessionState { ", "Waiting, ", - "Ready { current: VerifiedPointSupportV1, }, ", - "Stale { previous: VerifiedPointSupportV1, }, ", - "Failed { cause: PointSupportViolationV1, previous: Option, }, ", + "Ready { current: Verified, }, ", + "Stale { previous: Verified, }, ", + "Failed { cause: Violation, previous: Option, }, ", "}", ), "lifecycle state must not duplicate the current raw observation", @@ -148,25 +148,26 @@ fn shared_observation_ssot_has_one_backing_without_lifecycle_or_adapter_facades( normalized_source_scope( SESSION_SOURCE, "enum SessionObservationHeadV1 {", - "impl SessionObservationHeadV1", + "impl ObservationOwnerV1 for SessionObservationHeadV1", ), concat!( "enum SessionObservationHeadV1 { ", "Empty, ", "Unknown(RevisionBoundUnknownV1), ", - "Observed(crate::observation::RevisionBoundObservationV1), ", + "Observed(RevisionBoundObservationV1), ", "}", ), "raw Empty/Unknown/Observed must remain separate from lifecycle state", ); let session_owner = source_scope( SESSION_SOURCE, - "pub(crate) struct PointSupportSessionV1 {", - "impl PointSupportSessionV1", + "pub(crate) struct Session {", + "impl Session", ); for required in [ + "schema: CanonicalObservationSchemaV1,", "raw_head: SessionObservationHeadV1,", - "state: PointSupportSessionStateV1,", + "state: SessionState,", ] { assert_eq!( session_owner.matches(required).count(), @@ -184,10 +185,56 @@ fn shared_observation_ssot_has_one_backing_without_lifecycle_or_adapter_facades( "Session owner must not duplicate raw storage through `{forbidden}`", ); } + assert_eq!( + SESSION_SOURCE.matches("pub(crate) struct Session<").count(), + 1, + "production must have exactly one generic revision-bound Session owner", + ); + for required in [ + "type Verified: SessionEvidenceV1;", + "type Violation: SessionEvidenceV1;", + ".is_same_binding_as(expected_observation)", + "SessionUpdateError::EvidenceBindingInvariant", + ] { + assert!( + SESSION_SOURCE.contains(required), + "Session must reject detached evaluator evidence; missing `{required}`", + ); + } + assert_eq!( + POINT_SUPPORT_SOURCE + .matches("impl SessionPlanV1 for CompiledPointSupportRecheckV1") + .count() + + PROGRAM_SESSION_SOURCE + .matches("SessionPlanV1 for ProgramSessionPlan") + .count(), + 2, + "only the point-support and Program compiled plans may inhabit Session", + ); + for (path, source) in [ + ("session.rs", SESSION_SOURCE), + ("point_support.rs", POINT_SUPPORT_SOURCE), + ("program_session.rs", PROGRAM_SESSION_SOURCE), + ] { + for forbidden in [ + "PointSupportSessionV1", + "PointSupportSessionStateV1", + "BoundPointSupportRecheckV1", + "into_session_recheck", + "ObservationStreamBinding", + "ProgramExpired", + "Weak<", + ] { + assert!( + !source.contains(forbidden), + "{path} must not restore a second owner or adapter `{forbidden}`", + ); + } + } let consuming_entry = source_scope( POINT_SUPPORT_SOURCE, - "impl BoundPointSupportRecheckV1 {", + "impl SessionPlanV1 for CompiledPointSupportRecheckV1 {", "pub(crate) enum PointSupportEvaluationErrorV1", ); for required in [ @@ -230,7 +277,7 @@ fn shared_observation_ssot_has_one_backing_without_lifecycle_or_adapter_facades( .find(".physical_values(case_index)") .expect("physical-values route must exist"); let indexed_surface = evaluator[physical_values..] - .find("values.get(surface_index)") + .find("values.get(*surface_index)") .expect("the prebound surface index must read from physical values"); assert!( indexed_surface > 0, diff --git a/crates/labcolors-core/src/lib.rs b/crates/labcolors-core/src/lib.rs index 0a152b33..14c9df24 100644 --- a/crates/labcolors-core/src/lib.rs +++ b/crates/labcolors-core/src/lib.rs @@ -370,7 +370,7 @@ pub struct NoPublicPairRecipeApi; /// ``` /// /// ```compile_fail -/// use labcolors_core::session::PointSupportSessionV1; +/// use labcolors_core::session::Session; /// ``` #[cfg(doctest)] pub struct NoPrematurePointSupportApi; diff --git a/crates/labcolors-core/src/observation.rs b/crates/labcolors-core/src/observation.rs index 789a4a8e..6f2b9c86 100644 --- a/crates/labcolors-core/src/observation.rs +++ b/crates/labcolors-core/src/observation.rs @@ -245,6 +245,12 @@ impl RevisionBoundObservationV1 { self.backing.schema.shares_backing_with(expected) } + pub(crate) fn is_same_binding_as(&self, other: &Self) -> bool { + self.stream == other.stream + && self.revision == other.revision + && Rc::ptr_eq(&self.backing, &other.backing) + } + fn has_canonical_input( &self, schema: &CanonicalObservationSchemaV1, diff --git a/crates/labcolors-core/src/point_support.rs b/crates/labcolors-core/src/point_support.rs index ce49f64f..d36106b9 100644 --- a/crates/labcolors-core/src/point_support.rs +++ b/crates/labcolors-core/src/point_support.rs @@ -30,7 +30,10 @@ use crate::observation::{ CanonicalObservationSchemaV1, ObservationError, ObservationSchemaMismatchV1, RevisionBoundObservationV1, ScenarioId, canonicalize_observation_schema, }; -use crate::session::SessionObservationBindingPermitV1; +use crate::session::{ + SessionDecision, SessionEvidenceV1, SessionObservationBindingPermitV1, SessionPlanV1, + private as session_private, +}; use crate::wcag22::{Wcag22CriterionV1, Wcag22MeasurementV1, measure_wcag22_srgb8}; const DROP_BASIS_POINTS_SCALE: u16 = 10_000; @@ -180,34 +183,39 @@ impl CompiledPointSupportRecheckV1 { return Err(PointSupportCompileErrorV1::InactivePlan); } - let mut paint_definitions = Vec::new(); - paint_definitions - .try_reserve_exact(occurrences.len()) - .map_err(|_| PointSupportCompileErrorV1::ResourceExhausted)?; - paint_definitions.extend(occurrences.iter().map(|occurrence| occurrence.paint)); - paint_definitions.sort_unstable_by_key(|paint| paint.id()); - if let Some(drift) = paint_definitions - .windows(2) - .find(|window| window[0].id() == window[1].id() && window[0] != window[1]) { - return Err(PointSupportCompileErrorV1::PaintDefinitionMismatch( - drift[0].id(), - )); + let mut paint_definitions = Vec::new(); + paint_definitions + .try_reserve_exact(occurrences.len()) + .map_err(|_| PointSupportCompileErrorV1::ResourceExhausted)?; + paint_definitions.extend(occurrences.iter().map(|occurrence| occurrence.paint)); + paint_definitions.sort_unstable_by_key(|paint| paint.id()); + if let Some(drift) = paint_definitions + .windows(2) + .find(|window| window[0].id() == window[1].id() && window[0] != window[1]) + { + return Err(PointSupportCompileErrorV1::PaintDefinitionMismatch( + drift[0].id(), + )); + } } - let mut occurrence_identities = Vec::new(); - occurrence_identities - .try_reserve_exact(occurrences.len()) - .map_err(|_| PointSupportCompileErrorV1::ResourceExhausted)?; - occurrence_identities.extend(occurrences.iter().map(|occurrence| occurrence.occurrence)); - occurrence_identities.sort_unstable(); - if let Some(duplicate) = occurrence_identities - .windows(2) - .find(|window| window[0] == window[1]) { - return Err(PointSupportCompileErrorV1::DuplicateOccurrence( - duplicate[0], - )); + let mut occurrence_identities = Vec::new(); + occurrence_identities + .try_reserve_exact(occurrences.len()) + .map_err(|_| PointSupportCompileErrorV1::ResourceExhausted)?; + occurrence_identities + .extend(occurrences.iter().map(|occurrence| occurrence.occurrence)); + occurrence_identities.sort_unstable(); + if let Some(duplicate) = occurrence_identities + .windows(2) + .find(|window| window[0] == window[1]) + { + return Err(PointSupportCompileErrorV1::DuplicateOccurrence( + duplicate[0], + )); + } } let actual_profile = PointOpacityOverSurfaceV1::composition_profile(); @@ -309,52 +317,24 @@ impl CompiledPointSupportRecheckV1 { pub(crate) fn surface_schema(&self) -> &[SurfaceInputPortId] { self.surface_schema.as_slice() } - - pub(crate) fn into_session_recheck(self) -> BoundPointSupportRecheckV1 { - let Self { - physical_program, - composition_profile, - occurrences, - surface_schema, - surface_indices, - baselines, - } = self; - BoundPointSupportRecheckV1 { - physical_program, - composition_profile, - occurrences, - surface_schema, - surface_indices, - baselines, - } - } } -#[derive(Debug, PartialEq, Eq)] -#[cfg_attr(test, derive(Clone))] -pub(crate) struct BoundPointSupportRecheckV1 { - physical_program: PhysicalProgramIdentityV1, - composition_profile: CompositionProfileV1, - occurrences: Vec, - surface_schema: CanonicalObservationSchemaV1, - surface_indices: Vec, - baselines: Vec>, -} +impl session_private::PlanSealed for CompiledPointSupportRecheckV1 {} -impl BoundPointSupportRecheckV1 { - pub(crate) const fn composition_profile(&self) -> CompositionProfileV1 { - self.composition_profile - } +impl SessionPlanV1 for CompiledPointSupportRecheckV1 { + type Verified = VerifiedPointSupportV1; + type Violation = PointSupportViolationV1; + type Error = PointSupportEvaluationErrorV1; - pub(crate) const fn observation_schema(&self) -> &CanonicalObservationSchemaV1 { + fn observation_schema(&self) -> &CanonicalObservationSchemaV1 { &self.surface_schema } - pub(crate) fn evaluate( - &self, + fn evaluate( + &mut self, observation: RevisionBoundObservationV1, _permit: SessionObservationBindingPermitV1, - ) -> Result { + ) -> Result, Self::Error> { let assessment = evaluate_bound_point_support(self, &observation)?; Ok(assessment.bind(observation)) } @@ -639,7 +619,10 @@ impl PointSupportAssessmentV1 { /// Bind by move only. There is no allocation, recomputation or other /// fallible work after the consuming evaluator has completed assessment. - fn bind(self, observation: RevisionBoundObservationV1) -> PointSupportDecisionV1 { + fn bind( + self, + observation: RevisionBoundObservationV1, + ) -> SessionDecision { let failed = self.has_failure(); let Self { physical_program, @@ -665,9 +648,9 @@ impl PointSupportAssessmentV1 { first_stability_failure_index, }; if failed { - PointSupportDecisionV1::Violation(PointSupportViolationV1(report)) + SessionDecision::Violation(PointSupportViolationV1(report)) } else { - PointSupportDecisionV1::Verified(VerifiedPointSupportV1(report)) + SessionDecision::Verified(VerifiedPointSupportV1(report)) } } } @@ -754,6 +737,14 @@ impl RevisionBoundPointSupportReportV1 { #[cfg_attr(test, derive(Clone))] pub(crate) struct VerifiedPointSupportV1(RevisionBoundPointSupportReportV1); +impl session_private::EvidenceSealed for VerifiedPointSupportV1 {} + +impl SessionEvidenceV1 for VerifiedPointSupportV1 { + fn observation(&self) -> &RevisionBoundObservationV1 { + self.report().observation() + } +} + impl VerifiedPointSupportV1 { pub(crate) const fn report(&self) -> &RevisionBoundPointSupportReportV1 { &self.0 @@ -764,19 +755,20 @@ impl VerifiedPointSupportV1 { #[cfg_attr(test, derive(Clone))] pub(crate) struct PointSupportViolationV1(RevisionBoundPointSupportReportV1); +impl session_private::EvidenceSealed for PointSupportViolationV1 {} + +impl SessionEvidenceV1 for PointSupportViolationV1 { + fn observation(&self) -> &RevisionBoundObservationV1 { + self.report().observation() + } +} + impl PointSupportViolationV1 { pub(crate) const fn report(&self) -> &RevisionBoundPointSupportReportV1 { &self.0 } } -#[derive(Debug, PartialEq)] -#[cfg_attr(test, derive(Clone))] -pub(crate) enum PointSupportDecisionV1 { - Verified(VerifiedPointSupportV1), - Violation(PointSupportViolationV1), -} - #[derive(Debug, Clone, Copy, PartialEq, Eq)] struct EnabledBaselineV1 { composition: SourceOverCertificateV1, @@ -789,7 +781,7 @@ struct EnabledBaselineV1 { } fn evaluate_bound_point_support( - plan: &BoundPointSupportRecheckV1, + plan: &CompiledPointSupportRecheckV1, observation: &RevisionBoundObservationV1, ) -> Result { if plan.occurrences.len() != plan.surface_indices.len() @@ -824,20 +816,18 @@ fn evaluate_bound_point_support( let values = observation .physical_values(case_index) .ok_or(PointSupportEvaluationErrorV1::CompiledPlanInvariant)?; - for (occurrence_index, requirement) in plan.occurrences.iter().enumerate() { - let surface_index = *plan - .surface_indices - .get(occurrence_index) - .ok_or(PointSupportEvaluationErrorV1::CompiledPlanInvariant)?; - let baseline = plan - .baselines - .get(occurrence_index) - .ok_or(PointSupportEvaluationErrorV1::CompiledPlanInvariant)?; - let backdrop = values.get(surface_index).copied().ok_or( + for (occurrence_index, ((requirement, surface_index), baseline)) in plan + .occurrences + .iter() + .zip(&plan.surface_indices) + .zip(&plan.baselines) + .enumerate() + { + let backdrop = values.get(*surface_index).copied().ok_or( PointSupportEvaluationErrorV1::ObservationSchemaMismatch( ObservationSchemaMismatchV1::new( case_index, - surface_index, + *surface_index, Some(requirement.surface), None, ), diff --git a/crates/labcolors-core/src/point_support_tests.rs b/crates/labcolors-core/src/point_support_tests.rs index 5a0d042e..63b1b030 100644 --- a/crates/labcolors-core/src/point_support_tests.rs +++ b/crates/labcolors-core/src/point_support_tests.rs @@ -15,7 +15,7 @@ use crate::point_support::{ PointSupportStabilityAnchorV1, PointSupportStabilityAssessmentV1, PointSupportStabilityDecisionV1, PointSupportStabilityPolicyV1, }; -use crate::session::{PointSupportSessionStateV1, PointSupportSessionV1}; +use crate::session::{Session, SessionState}; use crate::wcag22::Wcag22CriterionV1; const STREAM: ObservationStreamId = ObservationStreamId::new(31); @@ -106,8 +106,8 @@ fn multi_paint_declared_order_and_direct_provenance_are_preserved() { assert_eq!(SURFACE_A.value(), 21); assert_eq!(OCCURRENCE_A.value(), 11); - let mut session = PointSupportSessionV1::new(STREAM, requirements); - let PointSupportSessionStateV1::Ready { current } = session + let mut session = Session::new(STREAM, requirements); + let SessionState::Ready { current } = session .update(observed_update( 1, [(9, vec![(SURFACE_A, [0; 3]), (SURFACE_B, [255; 3])])], @@ -162,10 +162,10 @@ fn duplicate_raw_scenarios_share_one_physical_case_without_cartesian_expansion() PointSupportStabilityPolicyV1::Disabled, ), ]); - let mut session = PointSupportSessionV1::new(STREAM, requirements); + let mut session = Session::new(STREAM, requirements); crate::composition::reset_source_over_evaluation_count(); - let PointSupportSessionStateV1::Failed { cause, previous } = session + let SessionState::Failed { cause, previous } = session .update(observed_update( 1, [ @@ -277,8 +277,8 @@ fn exact_wcag_and_stability_are_independent_axes_and_baseline_binds_once() { "the baseline is composed exactly once at compile/bind" ); - let mut session = PointSupportSessionV1::new(STREAM, requirements); - let PointSupportSessionStateV1::Failed { cause, previous } = session + let mut session = Session::new(STREAM, requirements); + let SessionState::Failed { cause, previous } = session .update(observed_update(1, [(44, vec![(SURFACE_A, [255; 3])])])) .unwrap() else { @@ -386,8 +386,8 @@ fn all_four_wcag_criterion_identities_survive_the_full_support_path() { }) .collect(), ); - let mut session = PointSupportSessionV1::new(STREAM, requirements); - let PointSupportSessionStateV1::Ready { current } = session + let mut session = Session::new(STREAM, requirements); + let SessionState::Ready { current } = session .update(observed_update(1, [(1, vec![(SURFACE_A, [255; 3])])])) .unwrap() else { @@ -448,8 +448,8 @@ fn wholly_inactive_plan_is_rejected_but_an_inactive_composition_cell_is_allowed( ) .expect("one active axis makes the whole full-support plan meaningful"); assert_eq!(mixed.surface_schema(), &[SURFACE_A, SURFACE_B]); - let mut mixed_session = PointSupportSessionV1::new(STREAM, mixed); - let PointSupportSessionStateV1::Ready { current } = mixed_session + let mut mixed_session = Session::new(STREAM, mixed); + let SessionState::Ready { current } = mixed_session .update(observed_update( 1, [(1, vec![(SURFACE_A, [17; 3]), (SURFACE_B, [3; 3])])], @@ -536,8 +536,8 @@ fn every_stability_anchor_survives_compile_evaluate_and_typed_evidence() { }) .collect(), ); - let mut session = PointSupportSessionV1::new(STREAM, requirements); - let PointSupportSessionStateV1::Ready { current } = session + let mut session = Session::new(STREAM, requirements); + let SessionState::Ready { current } = session .update(observed_update(1, [(91, vec![(SURFACE_A, [255; 3])])])) .unwrap() else { diff --git a/crates/labcolors-core/src/program_session.rs b/crates/labcolors-core/src/program_session.rs index 8276c3d7..bfb21f81 100644 --- a/crates/labcolors-core/src/program_session.rs +++ b/crates/labcolors-core/src/program_session.rs @@ -10,17 +10,29 @@ //! observation or evidence. use std::marker::PhantomData; +use std::rc::Rc; use crate::Srgb8; use crate::appearance::{ - AdmittedAppearanceBindings, AppearanceBindings, AppearanceGraphSpec, BindingError, - ColorInputId, CompileError, CompiledAppearanceGraph, CompiledOccurrenceSlotV1, - CompiledPaintSlotV1, OccurrenceId, OccurrenceSpec, OpacityInputId, PaintId, PaintSpec, - SurfaceId, SurfaceInputPortId, SurfaceSpec, + AdmittedAppearanceBindings, AppearanceBindings, AppearanceGraphSpec, AppearanceWorkspace, + BindingError, ColorInputId, CompileError, CompiledAppearanceGraph, CompiledOccurrenceSlotV1, + CompiledPaintSlotV1, EncodedPointPaintV1, OccurrenceId, OccurrenceSpec, OpacityInputId, + PaintId, PaintSpec, SurfaceId, SurfaceInputPortId, SurfaceSpec, }; use crate::composition::CompositionProfileV1; -use crate::constraints::{PointEvaluatorV1, PointInvocation}; -use crate::observation::ObservationGroupId; +use crate::constraints::{ + HardDecision, PointEvaluatorV1, PointInvocation, VisiblePointPassEvidence, + VisiblePointViolationEvidence, assess_visible_point_hard, +}; +use crate::observation::{ + CanonicalObservationSchemaV1, ObservationError, ObservationGroupId, + ObservationSchemaMismatchV1, ObservationStreamId, RevisionBoundObservationV1, + canonicalize_observation_schema, +}; +use crate::session::{ + Session, SessionDecision, SessionEvidenceV1, SessionObservationBindingPermitV1, SessionPlanV1, + private as session_private, +}; /// One immutable encoded colour binding owned by a [`Program`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -348,7 +360,9 @@ where } pub fn compile(self) -> Result, ProgramCompileError> { - prepare_program(self).map(|epoch| CompiledProgram { epoch }) + prepare_program(self).map(|epoch| CompiledProgram { + epoch: Rc::new(epoch), + }) } } @@ -441,10 +455,6 @@ enum CompiledConstraintModeV1 { ReportOnly, } -#[expect( - dead_code, - reason = "the compiled constraint payload is retained for the direct sole-Session bridge; erasing it would reduce lowering to a shape-only placeholder" -)] struct CompiledPointConstraint { id: ConstraintId, target_id: OccurrenceId, @@ -462,13 +472,9 @@ struct CompiledOutputBinding { struct CompiledObservationGroupV1 { id: ObservationGroupId, - surface_input_ports: Box<[SurfaceInputPortId]>, + schema: CanonicalObservationSchemaV1, } -#[expect( - dead_code, - reason = "the executable graph, admitted bindings and evaluator are retained for the direct sole-Session bridge in the next stack" -)] struct ProgramEpochV1 where Evaluation: PointEvaluatorV1, @@ -488,7 +494,7 @@ where Evaluation: PointEvaluatorV1, PointInvocation: Copy, { - epoch: ProgramEpochV1, + epoch: Rc>, } impl CompiledProgram @@ -496,12 +502,12 @@ where Evaluation: PointEvaluatorV1, PointInvocation: Copy, { - pub const fn observation_group_id(&self) -> ObservationGroupId { + pub fn observation_group_id(&self) -> ObservationGroupId { self.epoch.observation_group.id } pub fn surface_input_ports(&self) -> &[SurfaceInputPortId] { - &self.epoch.observation_group.surface_input_ports + self.epoch.observation_group.schema.as_slice() } pub fn constraint_ids(&self) -> impl ExactSizeIterator + '_ { @@ -517,6 +523,410 @@ where .iter() .map(|output| (output.output, output.paint_id)) } + + /// Create one independent stream-affine Session from the immutable + /// compiled epoch. The graph/evaluator/schema stay shared by strong + /// ownership; mutable bindings and workspace belong only to this Session. + pub(crate) fn instantiate( + &self, + stream: ObservationStreamId, + ) -> Result>, ProgramSessionInstantiateError> { + let bindings = self + .epoch + .binding_template + .try_clone_v1() + .map_err(map_session_instantiate_error)?; + let workspace = self + .epoch + .graph + .new_workspace() + .map_err(map_session_instantiate_error)?; + Ok(Session::new( + stream, + ProgramSessionPlan { + epoch: Rc::clone(&self.epoch), + bindings, + workspace, + }, + )) + } +} + +/// Failure while preparing mutable storage for one independent Session. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ProgramSessionInstantiateError { + ResourceExhausted, + InternalInvariant, +} + +fn map_session_instantiate_error(error: BindingError) -> ProgramSessionInstantiateError { + match error { + BindingError::ResourceExhausted => ProgramSessionInstantiateError::ResourceExhausted, + _ => ProgramSessionInstantiateError::InternalInvariant, + } +} + +/// One evaluator classification retained in the complete Program report. +pub enum ProgramConstraintResultV1 +where + Evaluation: PointEvaluatorV1, +{ + Pass(VisiblePointPassEvidence), + Violation(VisiblePointViolationEvidence), +} + +impl ProgramConstraintResultV1 +where + Evaluation: PointEvaluatorV1, +{ + pub const fn is_violation(&self) -> bool { + matches!(self, Self::Violation(_)) + } +} + +/// One canonical `physical case × constraint` report cell. +pub struct ProgramConstraintCellV1 +where + Evaluation: PointEvaluatorV1, +{ + case_index: usize, + constraint: ConstraintId, + target: OccurrenceId, + mode: CompiledConstraintModeV1, + result: ProgramConstraintResultV1, +} + +impl ProgramConstraintCellV1 +where + Evaluation: PointEvaluatorV1, +{ + pub const fn case_index(&self) -> usize { + self.case_index + } + + pub const fn constraint(&self) -> ConstraintId { + self.constraint + } + + pub const fn target(&self) -> OccurrenceId { + self.target + } + + pub const fn is_hard(&self) -> bool { + matches!(self.mode, CompiledConstraintModeV1::Hard) + } + + pub const fn result(&self) -> &ProgramConstraintResultV1 { + &self.result + } +} + +/// Complete revision-bound assessment in case-major, constraint-ID order. +pub struct ProgramReportV1 +where + Evaluation: PointEvaluatorV1, +{ + observation: RevisionBoundObservationV1, + cells: Vec>, +} + +impl ProgramReportV1 +where + Evaluation: PointEvaluatorV1, +{ + pub const fn observation(&self) -> &RevisionBoundObservationV1 { + &self.observation + } + + pub fn cells(&self) -> &[ProgramConstraintCellV1] { + &self.cells + } +} + +/// One emitted Program Paint routed to an opaque output slot. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ProgramOutputV1 { + output: OutputSlotId, + paint: EncodedPointPaintV1, +} + +impl ProgramOutputV1 { + pub const fn output(self) -> OutputSlotId { + self.output + } + + pub const fn paint(self) -> EncodedPointPaintV1 { + self.paint + } +} + +/// All hard cells passed over the complete admitted physical support. +pub struct ProgramVerifiedV1 +where + Evaluation: PointEvaluatorV1, +{ + report: ProgramReportV1, + outputs: Vec, +} + +impl session_private::EvidenceSealed for ProgramVerifiedV1 where + Evaluation: PointEvaluatorV1 +{ +} + +impl SessionEvidenceV1 for ProgramVerifiedV1 +where + Evaluation: PointEvaluatorV1, +{ + fn observation(&self) -> &RevisionBoundObservationV1 { + self.report().observation() + } +} + +impl ProgramVerifiedV1 +where + Evaluation: PointEvaluatorV1, +{ + pub const fn report(&self) -> &ProgramReportV1 { + &self.report + } + + pub fn outputs(&self) -> &[ProgramOutputV1] { + &self.outputs + } +} + +/// Complete report containing at least one hard violation. Outputs are absent +/// by construction and therefore cannot be mistaken for committed Paints. +pub struct ProgramViolationV1 +where + Evaluation: PointEvaluatorV1, +{ + report: ProgramReportV1, +} + +impl session_private::EvidenceSealed for ProgramViolationV1 where + Evaluation: PointEvaluatorV1 +{ +} + +impl SessionEvidenceV1 for ProgramViolationV1 +where + Evaluation: PointEvaluatorV1, +{ + fn observation(&self) -> &RevisionBoundObservationV1 { + self.report().observation() + } +} + +impl ProgramViolationV1 +where + Evaluation: PointEvaluatorV1, +{ + pub const fn report(&self) -> &ProgramReportV1 { + &self.report + } +} + +/// Program execution failure before Session commit. +#[derive(Debug, PartialEq, Eq)] +pub enum ProgramSessionEvaluationError { + ObservationSchemaMismatch(ObservationSchemaMismatchV1), + ResourceExhausted, + Evaluator { + case_index: usize, + constraint: ConstraintId, + source: EvaluationError, + }, + OutputVariesAcrossCases { + output: OutputSlotId, + first_case: usize, + actual_case: usize, + }, + InternalInvariant, +} + +type ProgramEvaluatorError = >::Error; + +type ProgramSessionEvaluationResult = Result< + SessionDecision, ProgramViolationV1>, + ProgramSessionEvaluationError>, +>; + +/// Per-Session mutable execution state backed by one strong immutable epoch. +pub struct ProgramSessionPlan +where + Evaluation: PointEvaluatorV1, + PointInvocation: Copy, +{ + epoch: Rc>, + bindings: AdmittedAppearanceBindings, + workspace: AppearanceWorkspace, +} + +impl session_private::PlanSealed for ProgramSessionPlan +where + Evaluation: PointEvaluatorV1, + PointInvocation: Copy, +{ +} + +impl SessionPlanV1 for ProgramSessionPlan +where + Evaluation: PointEvaluatorV1, + PointInvocation: Copy, +{ + type Verified = ProgramVerifiedV1; + type Violation = ProgramViolationV1; + type Error = ProgramSessionEvaluationError>; + + fn observation_schema(&self) -> &CanonicalObservationSchemaV1 { + &self.epoch.observation_group.schema + } + + fn evaluate( + &mut self, + observation: RevisionBoundObservationV1, + _permit: SessionObservationBindingPermitV1, + ) -> Result, Self::Error> { + evaluate_program_session(self, observation) + } +} + +fn evaluate_program_session( + plan: &mut ProgramSessionPlan, + observation: RevisionBoundObservationV1, +) -> ProgramSessionEvaluationResult +where + Evaluation: PointEvaluatorV1, + PointInvocation: Copy, +{ + let epoch = &plan.epoch; + let schema = &epoch.observation_group.schema; + if !observation.shares_schema_backing_with(schema) { + observation + .validate_surface_schema(schema.as_slice()) + .map_err(ProgramSessionEvaluationError::ObservationSchemaMismatch)?; + return Err(ProgramSessionEvaluationError::InternalInvariant); + } + + let case_count = observation.physical_case_count(); + let cell_count = case_count + .checked_mul(epoch.constraints.len()) + .ok_or(ProgramSessionEvaluationError::ResourceExhausted)?; + let mut cells = Vec::new(); + cells + .try_reserve_exact(cell_count) + .map_err(|_| ProgramSessionEvaluationError::ResourceExhausted)?; + let mut outputs = Vec::new(); + outputs + .try_reserve_exact(epoch.outputs.len()) + .map_err(|_| ProgramSessionEvaluationError::ResourceExhausted)?; + + let mut has_hard_violation = false; + let mut output_mismatch = None; + for case_index in 0..case_count { + let values = observation + .physical_values(case_index) + .ok_or(ProgramSessionEvaluationError::InternalInvariant)?; + if values.len() != schema.as_slice().len() { + let binding_index = values.len().min(schema.as_slice().len()); + return Err(ProgramSessionEvaluationError::ObservationSchemaMismatch( + ObservationSchemaMismatchV1::new( + case_index, + binding_index, + schema.as_slice().get(binding_index).copied(), + None, + ), + )); + } + plan.bindings + .overwrite_surface_inputs_canonical(schema.as_slice().iter().copied(), |index| { + values[index] + }) + .map_err(map_program_execution_binding_error)?; + let evaluation = epoch + .graph + .evaluate_admitted_into(&plan.bindings, &mut plan.workspace) + .map_err(map_program_execution_binding_error)?; + + for constraint in epoch.constraints.iter() { + let source = evaluation + .occurrence_at(constraint.target) + .ok_or(ProgramSessionEvaluationError::InternalInvariant)?; + let decision = + assess_visible_point_hard(source, &epoch.evaluator, constraint.invocation) + .map_err(|source| ProgramSessionEvaluationError::Evaluator { + case_index, + constraint: constraint.id, + source, + })?; + let result = match decision { + HardDecision::Pass(evidence) => ProgramConstraintResultV1::Pass(evidence), + HardDecision::Violation(evidence) => { + if matches!(constraint.mode, CompiledConstraintModeV1::Hard) { + has_hard_violation = true; + } + ProgramConstraintResultV1::Violation(evidence) + } + }; + cells.push(ProgramConstraintCellV1 { + case_index, + constraint: constraint.id, + target: constraint.target_id, + mode: constraint.mode, + result, + }); + } + + for (output_index, output) in epoch.outputs.iter().enumerate() { + let paint = evaluation + .paint_at(output.paint) + .copied() + .ok_or(ProgramSessionEvaluationError::InternalInvariant)?; + if paint.id() != output.paint_id { + return Err(ProgramSessionEvaluationError::InternalInvariant); + } + let routed = ProgramOutputV1 { + output: output.output, + paint, + }; + if case_index == 0 { + outputs.push(routed); + } else if outputs.get(output_index).copied() != Some(routed) + && output_mismatch.is_none() + { + output_mismatch = Some(ProgramSessionEvaluationError::OutputVariesAcrossCases { + output: output.output, + first_case: 0, + actual_case: case_index, + }); + } + } + } + + let report = ProgramReportV1 { observation, cells }; + if let Some(error) = output_mismatch { + Err(error) + } else if has_hard_violation { + Ok(SessionDecision::Violation(ProgramViolationV1 { report })) + } else { + Ok(SessionDecision::Verified(ProgramVerifiedV1 { + report, + outputs, + })) + } +} + +fn map_program_execution_binding_error( + error: BindingError, +) -> ProgramSessionEvaluationError { + match error { + BindingError::ResourceExhausted => ProgramSessionEvaluationError::ResourceExhausted, + _ => ProgramSessionEvaluationError::InternalInvariant, + } } fn prepare_program( @@ -563,6 +973,8 @@ where ) { return Err(ProgramCompileError::InternalInvariant); } + let observation_schema = canonicalize_observation_schema(surface_input_ports) + .map_err(map_observation_schema_compile_error)?; let constraints = compile_constraints::(&graph, program.constraints)?; let outputs = compile_outputs(&graph, program.outputs)?; @@ -572,13 +984,20 @@ where binding_template, observation_group: CompiledObservationGroupV1 { id: program.observation_group.id, - surface_input_ports: surface_input_ports.into_boxed_slice(), + schema: observation_schema, }, constraints, outputs, }) } +fn map_observation_schema_compile_error(error: ObservationError) -> ProgramCompileError { + match error { + ObservationError::ResourceExhausted => ProgramCompileError::ResourceExhausted, + _ => ProgramCompileError::InternalInvariant, + } +} + struct LoweredConstraint { id: ConstraintId, target: OccurrenceId, diff --git a/crates/labcolors-core/src/program_session_tests.rs b/crates/labcolors-core/src/program_session_tests.rs index f5873868..c80befe3 100644 --- a/crates/labcolors-core/src/program_session_tests.rs +++ b/crates/labcolors-core/src/program_session_tests.rs @@ -3,13 +3,18 @@ use crate::appearance::{ ColorInputId, OccurrenceId, OpacityInputId, PaintId, SurfaceId, SurfaceInputPortId, }; use crate::constraints::{ExactSrgb8IdentityV1, PointEvaluatorV1, PointInvocation}; -use crate::observation::ObservationGroupId; +use crate::observation::{ + ObservationGroupId, ObservationHeadViewV1, ObservationPayloadInput, ObservationStreamId, + ObservationUpdateInput, ObservedScenarioSetInput, Revision, ScenarioId, ScenarioInput, + SurfaceInputBinding, +}; use crate::program_session::{ ColorInput, CompositionProfile, ConstraintId, ConstraintInvocation, ConstraintSet, ObservationGroup, Occurrence, OpacityInput, OutputBinding, OutputSlotId, Paint, Program, ProgramCompileError, Surface, canonical_surface_input_port_sequence_matches, check_render_node_count, }; +use crate::session::SessionState; const COLOR: ColorInputId = ColorInputId::new(1); const SURFACE_PORT: SurfaceInputPortId = SurfaceInputPortId::new(2); @@ -22,6 +27,8 @@ const OCCURRENCE: OccurrenceId = OccurrenceId::new(30); const OUTPUT: OutputSlotId = OutputSlotId::new(40); const REQUIRED: ConstraintId = ConstraintId::new(50); const GROUP: ObservationGroupId = ObservationGroupId::new(60); +const STREAM_A: ObservationStreamId = ObservationStreamId::new(70); +const STREAM_B: ObservationStreamId = ObservationStreamId::new(71); fn observation_group(surface_input_ports: Vec) -> ObservationGroup { ObservationGroup::new(GROUP, surface_input_ports) @@ -101,6 +108,43 @@ where } } +fn observed_update( + stream: ObservationStreamId, + revision: u64, + scenarios: &[(u32, [u8; 3])], +) -> ObservationUpdateInput { + ObservationUpdateInput { + stream, + revision: Revision::new(revision), + payload: ObservationPayloadInput::Scenarios(ObservedScenarioSetInput { + scenarios: scenarios + .iter() + .map(|(scenario, backdrop)| ScenarioInput { + id: ScenarioId::new(*scenario), + bindings: vec![SurfaceInputBinding::new( + SURFACE_PORT, + Srgb8::new(*backdrop), + )], + }) + .collect(), + }), + } +} + +fn exact_compiled( + constraints: ConstraintSet, +) -> crate::program_session::CompiledProgram { + base_program( + 0.5, + BACKDROP, + constraints, + vec![OutputBinding::new(OUTPUT, TRANSLUCENT)], + ExactSrgb8IdentityV1, + ) + .compile() + .unwrap() +} + #[test] fn authored_modes_are_marker_typed_and_values_preserve_exact_ids() { let hard = ConstraintInvocation::hard(REQUIRED, OCCURRENCE, Srgb8::new([0x80; 3])); @@ -440,3 +484,301 @@ fn canonical_helpers_and_checked_cardinality_fail_closed() { &canonical, )); } + +#[test] +fn independently_instantiated_streams_survive_the_compiled_handle() { + let compiled = exact_compiled(ConstraintSet::new( + vec![ConstraintInvocation::hard( + REQUIRED, + OCCURRENCE, + Srgb8::new([0x80; 3]), + )], + vec![], + )); + let mut first = compiled.instantiate(STREAM_A).unwrap(); + let mut second = compiled.instantiate(STREAM_B).unwrap(); + drop(compiled); + + let first_state = first + .update(observed_update(STREAM_A, 1, &[(1, [0xFF; 3])])) + .unwrap(); + let SessionState::Ready { current: first } = first_state else { + panic!("first independent stream must verify"); + }; + assert_eq!(first.outputs().len(), 1); + + let second_state = second + .update(observed_update(STREAM_B, 9, &[(2, [0xFF; 3])])) + .unwrap(); + let SessionState::Ready { current: second } = second_state else { + panic!("second independent stream must verify after compiled handle drop"); + }; + assert_eq!(second.outputs().len(), 1); + assert_eq!(first.report().observation().revision(), Revision::new(1)); + assert_eq!(second.report().observation().revision(), Revision::new(9)); +} + +#[test] +fn multi_case_hard_failure_retains_the_full_matrix_without_outputs() { + let low = ConstraintId::new(1); + let high = ConstraintId::new(2); + let compiled = exact_compiled(ConstraintSet::new( + vec![ + ConstraintInvocation::hard(high, OCCURRENCE, Srgb8::new([0x00; 3])), + ConstraintInvocation::hard(low, OCCURRENCE, Srgb8::new([0x80; 3])), + ], + vec![], + )); + let mut session = compiled.instantiate(STREAM_A).unwrap(); + let state = session + .update(observed_update( + STREAM_A, + 1, + &[(11, [0x00; 3]), (12, [0xFF; 3])], + )) + .unwrap(); + let SessionState::Failed { cause, previous } = state else { + panic!("each candidate target fails on one admitted physical case"); + }; + assert!(previous.is_none()); + let cells = cause.report().cells(); + assert_eq!( + cells.len(), + 4, + "two cases × two constraints must be complete" + ); + assert_eq!( + cells + .iter() + .map(|cell| (cell.case_index(), cell.constraint())) + .collect::>(), + vec![(0, low), (0, high), (1, low), (1, high)], + ); + assert!(cells.iter().all(|cell| cell.is_hard())); + assert!(cells.iter().all(|cell| cell.target() == OCCURRENCE)); + assert_eq!( + cells + .iter() + .filter(|cell| cell.result().is_violation()) + .count(), + 2, + ); + // `ProgramViolationV1` owns only the complete report; no output accessor or + // output storage exists on the failure type. +} + +#[test] +fn mixed_modes_retain_the_full_canonical_matrix_without_outputs_on_hard_failure() { + let diagnostic = ConstraintId::new(1); + let required = ConstraintId::new(2); + let compiled = exact_compiled(ConstraintSet::new( + vec![ConstraintInvocation::hard( + required, + OCCURRENCE, + Srgb8::new([0x00; 3]), + )], + vec![ConstraintInvocation::report_only( + diagnostic, + OCCURRENCE, + Srgb8::new([0x80; 3]), + )], + )); + let mut session = compiled.instantiate(STREAM_A).unwrap(); + let state = session + .update(observed_update( + STREAM_A, + 1, + &[(11, [0x00; 3]), (12, [0xFF; 3])], + )) + .unwrap(); + let SessionState::Failed { cause, previous } = state else { + panic!("the hard constraint must gate the otherwise complete mixed report"); + }; + assert!(previous.is_none()); + + let cells = cause.report().cells(); + assert_eq!(cells.len(), 4); + assert_eq!( + cells + .iter() + .map(|cell| (cell.case_index(), cell.constraint(), cell.is_hard())) + .collect::>(), + vec![ + (0, diagnostic, false), + (0, required, true), + (1, diagnostic, false), + (1, required, true), + ], + ); + assert_eq!( + cells + .iter() + .map(|cell| cell.result().is_violation()) + .collect::>(), + vec![true, false, false, true], + ); + assert!(cells.iter().all(|cell| cell.target() == OCCURRENCE)); + // `cause` is `ProgramViolationV1`: the failure surface exposes only this + // complete report, while Paint outputs exist only on `ProgramVerifiedV1`. +} + +#[test] +fn report_only_violations_do_not_block_program_scope_paint_outputs() { + let diagnostic = ConstraintId::new(7); + let compiled = exact_compiled(ConstraintSet::new( + vec![], + vec![ConstraintInvocation::report_only( + diagnostic, + OCCURRENCE, + Srgb8::new([0x7F; 3]), + )], + )); + let mut session = compiled.instantiate(STREAM_A).unwrap(); + let state = session + .update(observed_update( + STREAM_A, + 1, + &[(1, [0x00; 3]), (2, [0xFF; 3])], + )) + .unwrap(); + let SessionState::Ready { current } = state else { + panic!("report-only violations must not gate outputs"); + }; + assert_eq!(current.report().cells().len(), 2); + assert!( + current + .report() + .cells() + .iter() + .all(|cell| !cell.is_hard() && cell.result().is_violation()), + ); + let [output] = current.outputs() else { + panic!("one canonical output must be emitted"); + }; + assert_eq!(output.output(), OUTPUT); + assert_eq!(output.paint().id(), TRANSLUCENT); + assert_eq!(output.paint().source(), Srgb8::new([0; 3])); + assert_eq!(output.paint().opacity_bits(), 0.5_f64.to_bits()); +} + +#[test] +fn nested_surface_uses_the_lower_occurrence_before_assessing_the_upper() { + const LOWER_COLOR: ColorInputId = ColorInputId::new(101); + const UPPER_COLOR: ColorInputId = ColorInputId::new(102); + const HALF: OpacityInputId = OpacityInputId::new(103); + const LOWER_PAINT: PaintId = PaintId::new(110); + const UPPER_SOLID: PaintId = PaintId::new(111); + const UPPER_PAINT: PaintId = PaintId::new(112); + const ROOT: SurfaceId = SurfaceId::new(120); + const DERIVED: SurfaceId = SurfaceId::new(121); + const LOWER: OccurrenceId = OccurrenceId::new(130); + const UPPER: OccurrenceId = OccurrenceId::new(131); + const NESTED_OUTPUT: OutputSlotId = OutputSlotId::new(140); + + let program = Program::new( + vec![ + ColorInput::new(LOWER_COLOR, Srgb8::new([0x80; 3])), + ColorInput::new(UPPER_COLOR, Srgb8::new([0xFF; 3])), + ], + observation_group(vec![SURFACE_PORT]), + vec![OpacityInput::new(HALF, 0.5)], + vec![ + Paint::Solid { + id: LOWER_PAINT, + color: LOWER_COLOR, + }, + Paint::Solid { + id: UPPER_SOLID, + color: UPPER_COLOR, + }, + Paint::Opacity { + id: UPPER_PAINT, + source: UPPER_SOLID, + opacity: HALF, + }, + ], + vec![ + Surface::Input { + id: ROOT, + input: SURFACE_PORT, + }, + Surface::FromOccurrence { + id: DERIVED, + occurrence: LOWER, + }, + ], + vec![ + Occurrence::new( + LOWER, + LOWER_PAINT, + ROOT, + CompositionProfile::EncodedSrgb8SourceOverV1, + ), + Occurrence::new( + UPPER, + UPPER_PAINT, + DERIVED, + CompositionProfile::EncodedSrgb8SourceOverV1, + ), + ], + ConstraintSet::new( + vec![ConstraintInvocation::hard( + REQUIRED, + UPPER, + Srgb8::new([0xC0; 3]), + )], + vec![], + ), + vec![OutputBinding::new(NESTED_OUTPUT, UPPER_PAINT)], + ExactSrgb8IdentityV1, + ); + let compiled = program.compile().unwrap(); + let mut session = compiled.instantiate(STREAM_A).unwrap(); + let state = session + .update(observed_update(STREAM_A, 1, &[(1, [0x00; 3])])) + .unwrap(); + let SessionState::Ready { current } = state else { + panic!("upper occurrence must compose over the lower visible result"); + }; + assert!(!current.report().cells()[0].result().is_violation()); + let [output] = current.outputs() else { + panic!("nested program must emit its Paint, not visible composite"); + }; + assert_eq!(output.output(), NESTED_OUTPUT); + assert_eq!(output.paint().id(), UPPER_PAINT); + assert_eq!(output.paint().source(), Srgb8::new([0xFF; 3])); +} + +#[test] +fn raw_head_and_program_report_share_one_observation_backing() { + let compiled = exact_compiled(ConstraintSet::new( + vec![ConstraintInvocation::hard( + REQUIRED, + OCCURRENCE, + Srgb8::new([0x80; 3]), + )], + vec![], + )); + let mut session = compiled.instantiate(STREAM_A).unwrap(); + session + .update(observed_update(STREAM_A, 1, &[(9, [0xFF; 3])])) + .unwrap(); + let ObservationHeadViewV1::Observed(raw) = session.raw_head() else { + panic!("successful observed update must own a raw observed head"); + }; + let SessionState::Ready { current } = session.state() else { + panic!("fixture must verify"); + }; + let report = current.report().observation(); + assert_eq!(raw, report); + assert_eq!(raw.backing_ptr_for_test(), report.backing_ptr_for_test()); + assert_eq!(raw.schema_ptr_for_test(), report.schema_ptr_for_test()); + assert_eq!( + raw.physical_values(0).unwrap().as_ptr(), + report.physical_values(0).unwrap().as_ptr(), + ); + assert_eq!( + raw.provenance(0).unwrap().as_ptr(), + report.provenance(0).unwrap().as_ptr(), + ); +} diff --git a/crates/labcolors-core/src/session.rs b/crates/labcolors-core/src/session.rs index 35067274..2497219b 100644 --- a/crates/labcolors-core/src/session.rs +++ b/crates/labcolors-core/src/session.rs @@ -1,26 +1,29 @@ -//! Single lifecycle and observation owner for private F2/C8d full support. +//! Sole revision-bound runtime lifecycle for compiled point programs. //! -//! The Session owns one concrete raw head separately from its evaluator -//! lifecycle. An observed raw head and its report share the same immutable -//! observation backing; `Unknown` owns no evidence. At most one previous -//! verified report is retained and no transition builds a history chain. +//! [`Session`] owns the one concrete raw observation head and the one +//! evaluator lifecycle. A plan supplies only its canonical observation schema +//! and consuming evaluation; it cannot admit updates or commit lifecycle +//! state. The plan type is sealed and statically dispatched, so sharing this +//! lifecycle across compiled plans adds neither a runtime tag nor a trait +//! object. use std::mem; -use crate::composition::CompositionProfileV1; use crate::observation::{ - ObservationError, ObservationHeadViewV1, ObservationOwnerV1, ObservationSchemaMismatchV1, + CanonicalObservationSchemaV1, ObservationError, ObservationHeadViewV1, ObservationOwnerV1, ObservationStreamId, ObservationUpdateInput, PreparedObservationUpdateV1, - RevisionBoundUnknownV1, prepare_observation, -}; -use crate::point_support::{ - BoundPointSupportRecheckV1, CompiledPointSupportRecheckV1, PointSupportDecisionV1, - PointSupportEvaluationErrorV1, PointSupportViolationV1, VerifiedPointSupportV1, + RevisionBoundObservationV1, RevisionBoundUnknownV1, prepare_observation, }; -/// Linear authority to consume and revision-bind an observation. The type is -/// visible to the evaluator only as a parameter; its private field and private -/// constructor make safe construction exclusive to this Session module. +/// Crate-private sealing prevents an additional runtime owner from being +/// smuggled in through a public extension point. +pub(crate) mod private { + pub(crate) trait PlanSealed {} + pub(crate) trait EvidenceSealed {} +} + +/// Linear authority to revision-bind one admitted observation to evaluator +/// evidence. Safe construction remains exclusive to this module. pub(crate) struct SessionObservationBindingPermitV1 { _private: (), } @@ -36,28 +39,70 @@ impl SessionObservationBindingPermitV1 { } } -#[derive(Debug, PartialEq)] -#[cfg_attr(test, derive(Clone))] -pub(crate) enum PointSupportSessionStateV1 { +/// Complete result of evaluating one admitted observation. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum SessionDecision { + Verified(Verified), + Violation(Violation), +} + +pub(crate) trait SessionEvidenceV1: private::EvidenceSealed { + fn observation(&self) -> &RevisionBoundObservationV1; +} + +impl SessionDecision +where + Verified: SessionEvidenceV1, + Violation: SessionEvidenceV1, +{ + fn observation(&self) -> &RevisionBoundObservationV1 { + match self { + Self::Verified(evidence) => evidence.observation(), + Self::Violation(evidence) => evidence.observation(), + } + } +} + +/// A compiled, statically dispatched evaluator used by the sole [`Session`] +/// lifecycle. Implementations own their per-Session scratch directly. +pub(crate) trait SessionPlanV1: private::PlanSealed { + type Verified: SessionEvidenceV1; + type Violation: SessionEvidenceV1; + type Error; + + fn observation_schema(&self) -> &CanonicalObservationSchemaV1; + + fn evaluate( + &mut self, + observation: RevisionBoundObservationV1, + permit: SessionObservationBindingPermitV1, + ) -> Result, Self::Error>; +} + +/// Evaluator lifecycle. The current raw payload is deliberately not embedded +/// here: `Unknown` carries no evidence, while `Stale` retains at most one +/// previous verified witness. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum SessionState { Waiting, Ready { - current: VerifiedPointSupportV1, + current: Verified, }, Stale { - previous: VerifiedPointSupportV1, + previous: Verified, }, Failed { - cause: PointSupportViolationV1, - previous: Option, + cause: Violation, + previous: Option, }, } -impl PointSupportSessionStateV1 { - pub(crate) fn last_verified(&self) -> Option<&VerifiedPointSupportV1> { +impl SessionState { + pub(crate) fn last_verified(&self) -> Option<&Verified> { match self { Self::Waiting => None, Self::Ready { current } => Some(current), - Self::Stale { previous, .. } => Some(previous), + Self::Stale { previous } => Some(previous), Self::Failed { previous, .. } => previous.as_ref(), } } @@ -67,13 +112,7 @@ impl PointSupportSessionStateV1 { enum SessionObservationHeadV1 { Empty, Unknown(RevisionBoundUnknownV1), - Observed(crate::observation::RevisionBoundObservationV1), -} - -impl SessionObservationHeadV1 { - fn view(&self) -> ObservationHeadViewV1<'_> { - self.observation_head() - } + Observed(RevisionBoundObservationV1), } impl ObservationOwnerV1 for SessionObservationHeadV1 { @@ -86,82 +125,62 @@ impl ObservationOwnerV1 for SessionObservationHeadV1 { } } +/// An update failed before either raw-head or lifecycle commit. #[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) enum PointSupportSessionUpdateErrorV1 { +pub(crate) enum SessionUpdateError { Observation(ObservationError), - ObservationSchemaMismatch(ObservationSchemaMismatchV1), - ResourceExhausted, - InternalInvariant, + Plan(PlanError), + EvidenceBindingInvariant, } -#[derive(Debug, PartialEq)] -#[cfg_attr(test, derive(Clone))] -pub(crate) struct PointSupportSessionV1 { +type SessionUpdateResult<'session, Plan> = Result< + &'session SessionState<::Verified, ::Violation>, + SessionUpdateError<::Error>, +>; + +/// The only production owner of revision admission and evaluator lifecycle. +/// `Plan` is monomorphized; there is no plan enum, dynamic dispatch, adapter, +/// weak owner or expiration branch. +#[derive(Debug)] +pub(crate) struct Session { stream: ObservationStreamId, - recheck: BoundPointSupportRecheckV1, + schema: CanonicalObservationSchemaV1, + plan: Plan, raw_head: SessionObservationHeadV1, - state: PointSupportSessionStateV1, - #[cfg(test)] - force_resource_failure: bool, - #[cfg(test)] - force_evaluator_failure: bool, + state: SessionState, } -impl PointSupportSessionV1 { - /// The compiled recheck owns the only canonical schema and is moved into - /// the Session. No second schema or replacement Paint can enter updates. - pub(crate) fn new( - stream: ObservationStreamId, - compiled: CompiledPointSupportRecheckV1, - ) -> Self { +impl Session { + pub(crate) fn new(stream: ObservationStreamId, plan: Plan) -> Self { + let schema = plan.observation_schema().clone(); Self { stream, - recheck: compiled.into_session_recheck(), + schema, + plan, raw_head: SessionObservationHeadV1::Empty, - state: PointSupportSessionStateV1::Waiting, - #[cfg(test)] - force_resource_failure: false, - #[cfg(test)] - force_evaluator_failure: false, + state: SessionState::Waiting, } } - pub(crate) const fn state(&self) -> &PointSupportSessionStateV1 { + pub(crate) const fn state(&self) -> &SessionState { &self.state } pub(crate) fn raw_head(&self) -> ObservationHeadViewV1<'_> { - self.raw_head.view() - } - - pub(crate) const fn composition_profile(&self) -> CompositionProfileV1 { - self.recheck.composition_profile() - } - - #[cfg(test)] - pub(crate) fn force_next_resource_failure(&mut self) { - self.force_resource_failure = true; + self.raw_head.observation_head() } - #[cfg(test)] - pub(crate) fn force_next_evaluator_failure(&mut self) { - self.force_evaluator_failure = true; - } - - /// One transaction: prepare/canonicalize without mutation, transfer the - /// exact observation under a Session-only permit to the consuming - /// evaluator, then replace the closed owner with infallible moves only. + /// Prepare, evaluate and commit one update transaction. Admission and plan + /// errors leave both the concrete raw head and lifecycle state untouched. + /// Plan-local scratch may have been overwritten by a failed evaluation, + /// but it is not observable lifecycle state and every evaluation must + /// completely initialize the scratch it consumes. pub(crate) fn update( &mut self, update: ObservationUpdateInput, - ) -> Result<&PointSupportSessionStateV1, PointSupportSessionUpdateErrorV1> { - let prepared = prepare_observation( - &mut self.raw_head, - self.stream, - self.recheck.observation_schema(), - update, - ) - .map_err(PointSupportSessionUpdateErrorV1::Observation)?; + ) -> SessionUpdateResult<'_, Plan> { + let prepared = prepare_observation(&mut self.raw_head, self.stream, &self.schema, update) + .map_err(SessionUpdateError::Observation)?; match prepared { PreparedObservationUpdateV1::Idempotent(prepared) => { @@ -171,49 +190,39 @@ impl PointSupportSessionV1 { PreparedObservationUpdateV1::Unknown(prepared) => { let (raw_head, unknown) = prepared.into_parts(); let next_state = match take_last_verified(&mut self.state) { - Some(previous) => PointSupportSessionStateV1::Stale { previous }, - None => PointSupportSessionStateV1::Waiting, + Some(previous) => SessionState::Stale { previous }, + None => SessionState::Waiting, }; *raw_head = SessionObservationHeadV1::Unknown(unknown); self.state = next_state; Ok(&self.state) } PreparedObservationUpdateV1::Observed(prepared) => { - #[cfg(test)] - if mem::take(&mut self.force_resource_failure) { - return Err(PointSupportSessionUpdateErrorV1::ResourceExhausted); - } - - // The raw head clone shares the exact immutable payload. Both - // raw head and lifecycle remain unchanged until the fallible - // recheck has produced a complete revision-bound decision. + // Clone only the small Rc-backed observation handle. Both the + // committed raw head and returned evidence then share the exact + // immutable observation backing. let (raw_head, observation) = prepared.into_parts(); let next_raw_head = SessionObservationHeadV1::Observed(observation.clone()); - let evaluation = { - #[cfg(test)] - { - if mem::take(&mut self.force_evaluator_failure) { - Err(PointSupportEvaluationErrorV1::CompiledPlanInvariant) - } else { - self.recheck - .evaluate(observation, SessionObservationBindingPermitV1::mint()) - } - } - #[cfg(not(test))] - { - self.recheck - .evaluate(observation, SessionObservationBindingPermitV1::mint()) - } + let decision = self + .plan + .evaluate(observation, SessionObservationBindingPermitV1::mint()) + .map_err(SessionUpdateError::Plan)?; + let SessionObservationHeadV1::Observed(expected_observation) = &next_raw_head + else { + unreachable!("the pending raw head was constructed as Observed") }; - let decision = evaluation.map_err(map_evaluation_error)?; + if !decision + .observation() + .is_same_binding_as(expected_observation) + { + return Err(SessionUpdateError::EvidenceBindingInvariant); + } + + // All fallible work is complete. Commit with moves only. let previous = take_last_verified(&mut self.state); let next_state = match decision { - PointSupportDecisionV1::Verified(current) => { - PointSupportSessionStateV1::Ready { current } - } - PointSupportDecisionV1::Violation(cause) => { - PointSupportSessionStateV1::Failed { cause, previous } - } + SessionDecision::Verified(current) => SessionState::Ready { current }, + SessionDecision::Violation(cause) => SessionState::Failed { cause, previous }, }; *raw_head = next_raw_head; self.state = next_state; @@ -223,200 +232,14 @@ impl PointSupportSessionV1 { } } -fn map_evaluation_error(error: PointSupportEvaluationErrorV1) -> PointSupportSessionUpdateErrorV1 { - match error { - PointSupportEvaluationErrorV1::ObservationSchemaMismatch(mismatch) => { - PointSupportSessionUpdateErrorV1::ObservationSchemaMismatch(mismatch) - } - PointSupportEvaluationErrorV1::ResourceExhausted => { - PointSupportSessionUpdateErrorV1::ResourceExhausted - } - PointSupportEvaluationErrorV1::CompiledPlanInvariant - | PointSupportEvaluationErrorV1::Wcag22Invariant - | PointSupportEvaluationErrorV1::StabilityArithmeticInvariant => { - PointSupportSessionUpdateErrorV1::InternalInvariant - } - } -} - /// Move exactly one retained verified witness out of the old closed owner. -fn take_last_verified(state: &mut PointSupportSessionStateV1) -> Option { - match mem::replace(state, PointSupportSessionStateV1::Waiting) { - PointSupportSessionStateV1::Waiting => None, - PointSupportSessionStateV1::Ready { current } => Some(current), - PointSupportSessionStateV1::Stale { previous, .. } => Some(previous), - PointSupportSessionStateV1::Failed { previous, .. } => previous, - } -} - -#[cfg(test)] -mod structural_tests { - use super::SessionObservationBindingPermitV1; - use crate::Srgb8; - use crate::appearance::{EncodedPointPaintV1, OccurrenceId, PaintId, SurfaceInputPortId}; - use crate::composition::{AdmittedOpacityV1, CompositionProfileV1}; - use crate::observation::{ - ObservationHeadViewV1, ObservationOwnerV1, ObservationPayloadInput, - ObservationSchemaMismatchV1, ObservationStreamId, ObservationUpdateInput, - ObservedScenarioSetInput, PreparedObservationUpdateV1, Revision, - RevisionBoundObservationV1, ScenarioId, ScenarioInput, SurfaceInputBinding, - canonicalize_observation_schema, prepare_observation, - }; - use crate::point_support::{ - CompiledPointSupportRecheckV1, PointSupportCriterionRequirementV1, - PointSupportEvaluationErrorV1, PointSupportOccurrenceRequirementV1, - PointSupportStabilityPolicyV1, - }; - - const STREAM: ObservationStreamId = ObservationStreamId::new(700); - const REQUIRED_SURFACE: SurfaceInputPortId = SurfaceInputPortId::new(10); - const WRONG_SURFACE: SurfaceInputPortId = SurfaceInputPortId::new(20); - - struct EmptyOwner; - - impl ObservationOwnerV1 for EmptyOwner { - fn observation_head(&self) -> ObservationHeadViewV1<'_> { - ObservationHeadViewV1::Empty - } - } - - fn wrong_schema_observation() -> RevisionBoundObservationV1 { - let mut owner = EmptyOwner; - let schema = canonicalize_observation_schema(vec![WRONG_SURFACE]).unwrap(); - let prepared = prepare_observation( - &mut owner, - STREAM, - &schema, - ObservationUpdateInput { - stream: STREAM, - revision: Revision::new(1), - payload: ObservationPayloadInput::Scenarios(ObservedScenarioSetInput { - scenarios: vec![ScenarioInput { - id: ScenarioId::new(1), - bindings: vec![SurfaceInputBinding::new( - WRONG_SURFACE, - Srgb8::new([255; 3]), - )], - }], - }), - }, - ) - .unwrap(); - let PreparedObservationUpdateV1::Observed(prepared) = prepared else { - panic!("fresh observed update must prepare an observation"); - }; - let (_owner, observation) = prepared.into_parts(); - observation - } - - fn narrow_schema_observation() -> RevisionBoundObservationV1 { - let mut owner = EmptyOwner; - let schema = canonicalize_observation_schema(vec![REQUIRED_SURFACE]).unwrap(); - let prepared = prepare_observation( - &mut owner, - STREAM, - &schema, - ObservationUpdateInput { - stream: STREAM, - revision: Revision::new(1), - payload: ObservationPayloadInput::Scenarios(ObservedScenarioSetInput { - scenarios: vec![ScenarioInput { - id: ScenarioId::new(1), - bindings: vec![SurfaceInputBinding::new( - REQUIRED_SURFACE, - Srgb8::new([255; 3]), - )], - }], - }), - }, - ) - .unwrap(); - let PreparedObservationUpdateV1::Observed(prepared) = prepared else { - panic!("fresh observed update must prepare an observation"); - }; - let (_owner, observation) = prepared.into_parts(); - observation - } - - #[test] - fn consuming_evaluator_rejects_wrong_keyed_schema_before_composition() { - let paint = EncodedPointPaintV1::from_admitted( - PaintId::new(1), - Srgb8::new([0; 3]), - AdmittedOpacityV1::new(1.0).unwrap(), - ); - let compiled = CompiledPointSupportRecheckV1::new( - CompositionProfileV1::EncodedSrgb8SourceOverV1, - vec![PointSupportOccurrenceRequirementV1::new( - OccurrenceId::new(1), - REQUIRED_SURFACE, - paint, - Some(Srgb8::new([0; 3])), - PointSupportCriterionRequirementV1::NotRequested, - PointSupportStabilityPolicyV1::Disabled, - )], - ) - .unwrap(); - let recheck = compiled.into_session_recheck(); - - crate::composition::reset_source_over_evaluation_count(); - assert_eq!( - recheck - .evaluate( - wrong_schema_observation(), - SessionObservationBindingPermitV1::mint(), - ) - .unwrap_err(), - PointSupportEvaluationErrorV1::ObservationSchemaMismatch( - ObservationSchemaMismatchV1::new(0, 0, Some(REQUIRED_SURFACE), Some(WRONG_SURFACE),), - ) - ); - assert_eq!(crate::composition::source_over_evaluation_count(), 0); - } - - #[test] - fn consuming_evaluator_rejects_narrow_schema_without_indexing_panic() { - let paint = EncodedPointPaintV1::from_admitted( - PaintId::new(1), - Srgb8::new([0; 3]), - AdmittedOpacityV1::new(1.0).unwrap(), - ); - let compiled = CompiledPointSupportRecheckV1::new( - CompositionProfileV1::EncodedSrgb8SourceOverV1, - vec![ - PointSupportOccurrenceRequirementV1::new( - OccurrenceId::new(1), - REQUIRED_SURFACE, - paint, - Some(Srgb8::new([0; 3])), - PointSupportCriterionRequirementV1::NotRequested, - PointSupportStabilityPolicyV1::Disabled, - ), - PointSupportOccurrenceRequirementV1::new( - OccurrenceId::new(2), - WRONG_SURFACE, - paint, - Some(Srgb8::new([0; 3])), - PointSupportCriterionRequirementV1::NotRequested, - PointSupportStabilityPolicyV1::Disabled, - ), - ], - ) - .unwrap(); - let recheck = compiled.into_session_recheck(); - - crate::composition::reset_source_over_evaluation_count(); - assert_eq!( - recheck - .evaluate( - narrow_schema_observation(), - SessionObservationBindingPermitV1::mint(), - ) - .unwrap_err(), - PointSupportEvaluationErrorV1::ObservationSchemaMismatch( - ObservationSchemaMismatchV1::new(0, 1, Some(WRONG_SURFACE), None), - ) - ); - assert_eq!(crate::composition::source_over_evaluation_count(), 0); +fn take_last_verified( + state: &mut SessionState, +) -> Option { + match mem::replace(state, SessionState::Waiting) { + SessionState::Waiting => None, + SessionState::Ready { current } => Some(current), + SessionState::Stale { previous } => Some(previous), + SessionState::Failed { previous, .. } => previous, } } diff --git a/crates/labcolors-core/src/session_tests.rs b/crates/labcolors-core/src/session_tests.rs index a1be65b6..18b4f834 100644 --- a/crates/labcolors-core/src/session_tests.rs +++ b/crates/labcolors-core/src/session_tests.rs @@ -1,65 +1,162 @@ -use proptest::prelude::*; +use std::cell::{Cell, RefCell}; +use std::rc::Rc; use crate::Srgb8; -use crate::appearance::{EncodedPointPaintV1, OccurrenceId, PaintId, SurfaceInputPortId}; -use crate::composition::{AdmittedOpacityV1, CompositionProfileV1}; +use crate::appearance::SurfaceInputPortId; use crate::observation::{ - ObservationError, ObservationHeadViewV1, ObservationPayloadInput, ObservationStreamId, - ObservationUpdateInput, ObservedScenarioSetInput, Revision, RevisionBoundObservationV1, - ScenarioId, ScenarioInput, SurfaceInputBinding, UnknownReasonId, -}; -use crate::point_support::{ - CompiledPointSupportRecheckV1, PointSupportCriterionRequirementV1, - PointSupportOccurrenceRequirementV1, PointSupportStabilityPolicyV1, + CanonicalObservationSchemaV1, ObservationError, ObservationHeadViewV1, ObservationPayloadInput, + ObservationStreamId, ObservationUpdateInput, ObservedScenarioSetInput, Revision, + RevisionBoundObservationV1, ScenarioId, ScenarioInput, SurfaceInputBinding, UnknownReasonId, + canonicalize_observation_schema, }; use crate::session::{ - PointSupportSessionStateV1, PointSupportSessionUpdateErrorV1, PointSupportSessionV1, + Session, SessionDecision, SessionEvidenceV1, SessionObservationBindingPermitV1, SessionPlanV1, + SessionState, SessionUpdateError, private as session_private, }; -const PAINT: PaintId = PaintId::new(7); -const OCCURRENCE: OccurrenceId = OccurrenceId::new(11); -const SURFACE: SurfaceInputPortId = SurfaceInputPortId::new(21); const STREAM: ObservationStreamId = ObservationStreamId::new(31); -const TARGET: [u8; 3] = [128; 3]; +const FOREIGN_STREAM: ObservationStreamId = ObservationStreamId::new(32); +const SURFACE: SurfaceInputPortId = SurfaceInputPortId::new(21); -fn candidate() -> EncodedPointPaintV1 { - EncodedPointPaintV1::from_admitted( - PAINT, - Srgb8::new([0; 3]), - AdmittedOpacityV1::new(0.5).unwrap(), - ) +#[derive(Debug, Clone, PartialEq, Eq)] +struct SentinelVerified { + observation: RevisionBoundObservationV1, } -fn requirement() -> CompiledPointSupportRecheckV1 { - CompiledPointSupportRecheckV1::new( - CompositionProfileV1::EncodedSrgb8SourceOverV1, - vec![PointSupportOccurrenceRequirementV1::new( - OCCURRENCE, - SURFACE, - candidate(), - Some(Srgb8::new(TARGET)), - PointSupportCriterionRequirementV1::NotRequested, - PointSupportStabilityPolicyV1::Disabled, - )], - ) - .unwrap() +#[derive(Debug, Clone, PartialEq, Eq)] +struct SentinelViolation { + observation: RevisionBoundObservationV1, +} + +impl session_private::EvidenceSealed for SentinelVerified {} + +impl SessionEvidenceV1 for SentinelVerified { + fn observation(&self) -> &RevisionBoundObservationV1 { + &self.observation + } +} + +impl session_private::EvidenceSealed for SentinelViolation {} + +impl SessionEvidenceV1 for SentinelViolation { + fn observation(&self) -> &RevisionBoundObservationV1 { + &self.observation + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum SentinelError { + Forced, + SchemaBackingMismatch, + EmptyObservation, +} + +#[derive(Debug, Clone)] +struct SentinelControl { + evaluations: Rc>, + fail_next: Rc>, + substitute_next: Rc>>, +} + +impl SentinelControl { + fn evaluation_count(&self) -> usize { + self.evaluations.get() + } + + fn fail_next(&self) { + self.fail_next.set(true); + } + + fn substitute_next_with(&self, observation: RevisionBoundObservationV1) { + *self.substitute_next.borrow_mut() = Some(observation); + } +} + +#[derive(Debug)] +struct SentinelPlan { + schema: CanonicalObservationSchemaV1, + control: SentinelControl, +} + +impl session_private::PlanSealed for SentinelPlan {} + +impl SessionPlanV1 for SentinelPlan { + type Verified = SentinelVerified; + type Violation = SentinelViolation; + type Error = SentinelError; + + fn observation_schema(&self) -> &CanonicalObservationSchemaV1 { + &self.schema + } + + fn evaluate( + &mut self, + observation: RevisionBoundObservationV1, + _permit: SessionObservationBindingPermitV1, + ) -> Result, Self::Error> { + self.control + .evaluations + .set(self.control.evaluations.get() + 1); + if self.control.fail_next.replace(false) { + return Err(SentinelError::Forced); + } + if !observation.shares_schema_backing_with(&self.schema) { + return Err(SentinelError::SchemaBackingMismatch); + } + let first = observation + .physical_values(0) + .and_then(|values| values.first()) + .copied() + .ok_or(SentinelError::EmptyObservation)?; + let observation = self + .control + .substitute_next + .borrow_mut() + .take() + .unwrap_or(observation); + if first == Srgb8::new([255; 3]) { + Ok(SessionDecision::Verified(SentinelVerified { observation })) + } else { + Ok(SessionDecision::Violation(SentinelViolation { + observation, + })) + } + } } -fn session() -> PointSupportSessionV1 { - PointSupportSessionV1::new(STREAM, requirement()) +fn session() -> ( + Session, + SentinelControl, + *const SurfaceInputPortId, +) { + let schema = canonicalize_observation_schema(vec![SURFACE]).unwrap(); + let schema_ptr = schema.backing_ptr_for_test(); + let control = SentinelControl { + evaluations: Rc::new(Cell::new(0)), + fail_next: Rc::new(Cell::new(false)), + substitute_next: Rc::new(RefCell::new(None)), + }; + ( + Session::new( + STREAM, + SentinelPlan { + schema, + control: control.clone(), + }, + ), + control, + schema_ptr, + ) } -fn observed_update(revision: u64, backdrop: [u8; 3]) -> ObservationUpdateInput { +fn observed_update(revision: u64, value: [u8; 3]) -> ObservationUpdateInput { ObservationUpdateInput { stream: STREAM, revision: Revision::new(revision), payload: ObservationPayloadInput::Scenarios(ObservedScenarioSetInput { scenarios: vec![ScenarioInput { id: ScenarioId::new(1), - bindings: vec![SurfaceInputBinding { - port: SURFACE, - value: Srgb8::new(backdrop), - }], + bindings: vec![SurfaceInputBinding::new(SURFACE, Srgb8::new(value))], }], }), } @@ -80,541 +177,254 @@ fn malformed_update(revision: u64) -> ObservationUpdateInput { payload: ObservationPayloadInput::Scenarios(ObservedScenarioSetInput { scenarios: vec![ScenarioInput { id: ScenarioId::new(1), - bindings: vec![], + bindings: Vec::new(), }], }), } } -fn verified_revision(state: &PointSupportSessionStateV1) -> Option { - state - .last_verified() - .map(|verified| verified.report().observation().revision()) -} - -fn raw_observed(session: &PointSupportSessionV1) -> &RevisionBoundObservationV1 { +fn raw_observed(session: &Session) -> &RevisionBoundObservationV1 { let ObservationHeadViewV1::Observed(observation) = session.raw_head() else { - panic!("the concrete raw head must be Observed"); + panic!("raw head must be Observed"); }; observation } -fn assert_shared_observation_backing( - raw: &RevisionBoundObservationV1, - report: &RevisionBoundObservationV1, -) { - assert_eq!(raw, report); - assert_eq!( - raw.backing_ptr_for_test(), - report.backing_ptr_for_test(), - "raw head and report must share one immutable observation backing", - ); - assert_eq!( - raw.schema().as_ptr(), - report.schema().as_ptr(), - "raw head and report must share immutable schema backing", - ); - assert_eq!(raw.physical_case_count(), report.physical_case_count()); - for case_index in 0..raw.physical_case_count() { - let raw_values = raw - .physical_values(case_index) - .expect("raw case must exist"); - let report_values = report - .physical_values(case_index) - .expect("report case must exist"); - assert_eq!(raw_values, report_values); - assert_eq!( - raw_values.as_ptr(), - report_values.as_ptr(), - "raw head and report must share immutable value backing", - ); - - let raw_provenance = raw - .provenance(case_index) - .expect("raw provenance must exist"); - let report_provenance = report - .provenance(case_index) - .expect("report provenance must exist"); - assert_eq!(raw_provenance, report_provenance); - assert_eq!( - raw_provenance.as_ptr(), - report_provenance.as_ptr(), - "raw head and report must share immutable provenance backing", - ); - } -} - -fn observation_backing_signature( - observation: &RevisionBoundObservationV1, -) -> (*const SurfaceInputPortId, *const Srgb8, *const ScenarioId) { - assert_eq!(observation.physical_case_count(), 1); - ( - observation.schema().as_ptr(), - observation - .physical_values(0) - .expect("fixture must have one physical case") - .as_ptr(), - observation - .provenance(0) - .expect("fixture must have provenance") - .as_ptr(), - ) -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum StateKind { - Waiting, - Ready, - Stale, - Failed, +fn verified_revision( + state: &SessionState, +) -> Option { + state + .last_verified() + .map(|verified| verified.observation.revision()) } -fn state_kind(state: &PointSupportSessionStateV1) -> StateKind { - match state { - PointSupportSessionStateV1::Waiting => StateKind::Waiting, - PointSupportSessionStateV1::Ready { .. } => StateKind::Ready, - PointSupportSessionStateV1::Stale { .. } => StateKind::Stale, - PointSupportSessionStateV1::Failed { .. } => StateKind::Failed, - } +fn assert_shared_observation( + raw: &RevisionBoundObservationV1, + evidence: &RevisionBoundObservationV1, +) { + assert_eq!(raw, evidence); + assert_eq!(raw.backing_ptr_for_test(), evidence.backing_ptr_for_test()); + assert_eq!(raw.schema_ptr_for_test(), evidence.schema_ptr_for_test()); } #[test] -fn construction_uses_only_the_compiled_schema_and_profile() { - let session = session(); - assert!(matches!( - session.state(), - PointSupportSessionStateV1::Waiting - )); +fn construction_is_waiting_and_owns_no_raw_evidence() { + let (session, control, _) = session(); + assert!(matches!(session.state(), SessionState::Waiting)); assert_eq!(session.raw_head(), ObservationHeadViewV1::Empty); - assert_eq!( - session.composition_profile(), - CompositionProfileV1::EncodedSrgb8SourceOverV1 - ); + assert_eq!(control.evaluation_count(), 0); } #[test] -fn ready_violation_unknown_preserves_exactly_one_verified_witness() { - let mut session = session(); - session.update(observed_update(1, [255; 3])).unwrap(); - let PointSupportSessionStateV1::Ready { current } = session.state() else { - panic!("white backdrop must verify #808080 target"); - }; - assert_eq!(current.report().observation().revision(), Revision::new(1)); - assert_eq!( - current.report().cells().next().unwrap().provenance(), - &[ScenarioId::new(1)] - ); - assert_shared_observation_backing(raw_observed(&session), current.report().observation()); +fn ready_failed_unknown_retains_exactly_one_verified_witness() { + let (mut session, control, schema_ptr) = session(); - session.update(observed_update(2, [0; 3])).unwrap(); - let PointSupportSessionStateV1::Failed { cause, previous } = session.state() else { - panic!("black backdrop must violate #808080 target"); + let current_observation = match session.update(observed_update(1, [255; 3])).unwrap() { + SessionState::Ready { current } => current.observation.clone(), + _ => panic!("white sentinel input must verify"), }; - assert_eq!(cause.report().observation().revision(), Revision::new(2)); - assert_eq!( - previous.as_ref().unwrap().report().observation().revision(), - Revision::new(1) - ); - assert_shared_observation_backing(raw_observed(&session), cause.report().observation()); - - session.update(unknown_update(3, 9)).unwrap(); - let PointSupportSessionStateV1::Stale { previous } = session.state() else { - panic!("unknown after a verified result must become Stale"); + assert_eq!(current_observation.revision(), Revision::new(1)); + assert_eq!(current_observation.schema_ptr_for_test(), schema_ptr); + assert_shared_observation(raw_observed(&session), ¤t_observation); + + let (cause_observation, previous_revision) = + match session.update(observed_update(2, [0; 3])).unwrap() { + SessionState::Failed { cause, previous } => ( + cause.observation.clone(), + previous.as_ref().unwrap().observation.revision(), + ), + _ => panic!("black sentinel input must violate"), + }; + assert_eq!(cause_observation.revision(), Revision::new(2)); + assert_eq!(previous_revision, Revision::new(1)); + assert_shared_observation(raw_observed(&session), &cause_observation); + + let previous_revision = match session.update(unknown_update(3, 9)).unwrap() { + SessionState::Stale { previous } => previous.observation.revision(), + _ => panic!("Unknown after a verified result must become Stale"), }; - assert_eq!(previous.report().observation().revision(), Revision::new(1)); - let ObservationHeadViewV1::Unknown(current_unknown) = session.raw_head() else { - panic!("lifecycle state must not own the current raw Unknown"); + assert_eq!(previous_revision, Revision::new(1)); + let ObservationHeadViewV1::Unknown(unknown) = session.raw_head() else { + panic!("Unknown belongs to the separate raw head"); }; - assert_eq!(current_unknown.stream(), STREAM); - assert_eq!(current_unknown.revision(), Revision::new(3)); - assert_eq!(current_unknown.reason(), UnknownReasonId::new(9)); + assert_eq!(unknown.stream(), STREAM); + assert_eq!(unknown.revision(), Revision::new(3)); + assert_eq!(unknown.reason(), UnknownReasonId::new(9)); + assert_eq!(control.evaluation_count(), 2); } #[test] -fn violation_without_prior_then_unknown_is_waiting() { - let mut session = session(); +fn violation_without_verified_then_unknown_returns_to_waiting() { + let (mut session, control, _) = session(); assert!(matches!( session.update(observed_update(1, [0; 3])).unwrap(), - PointSupportSessionStateV1::Failed { previous: None, .. } + SessionState::Failed { previous: None, .. } )); - session.update(unknown_update(2, 1)).unwrap(); assert!(matches!( - session.state(), - PointSupportSessionStateV1::Waiting + session.update(unknown_update(2, 7)).unwrap(), + SessionState::Waiting )); - let ObservationHeadViewV1::Unknown(current_unknown) = session.raw_head() else { - panic!("Waiting must not embed the current raw Unknown"); - }; - assert_eq!(current_unknown.stream(), STREAM); - assert_eq!(current_unknown.revision(), Revision::new(2)); - assert_eq!(current_unknown.reason(), UnknownReasonId::new(1)); assert_eq!(verified_revision(session.state()), None); + assert_eq!(control.evaluation_count(), 1); } #[test] -fn stale_and_failed_transitions_move_one_previous_without_history() { - let mut session = session(); +fn exact_replay_is_idempotent_and_never_invokes_the_plan() { + let (mut session, control, _) = session(); session.update(observed_update(1, [255; 3])).unwrap(); - session.update(unknown_update(2, 1)).unwrap(); - assert_eq!(verified_revision(session.state()), Some(Revision::new(1))); - assert_eq!(session.raw_head().revision(), Some(Revision::new(2))); - - session.update(observed_update(3, [0; 3])).unwrap(); - assert_eq!(state_kind(session.state()), StateKind::Failed); - assert_eq!(verified_revision(session.state()), Some(Revision::new(1))); - assert_eq!(session.raw_head().revision(), Some(Revision::new(3))); - - session.update(unknown_update(4, 2)).unwrap(); - assert_eq!(state_kind(session.state()), StateKind::Stale); - assert_eq!(verified_revision(session.state()), Some(Revision::new(1))); - assert_eq!(session.raw_head().revision(), Some(Revision::new(4))); - - session.update(observed_update(5, [255; 3])).unwrap(); - assert_eq!(state_kind(session.state()), StateKind::Ready); - assert_eq!(verified_revision(session.state()), Some(Revision::new(5))); - assert_eq!(session.raw_head().revision(), Some(Revision::new(5))); -} + let raw_backing = raw_observed(&session).backing_ptr_for_test(); + assert_eq!(control.evaluation_count(), 1); -#[test] -fn observation_clone_and_exact_replay_reuse_the_same_backing_without_allocation() { - let mut session = session(); session.update(observed_update(1, [255; 3])).unwrap(); - let raw = raw_observed(&session); - let raw_signature = observation_backing_signature(raw); - let PointSupportSessionStateV1::Ready { current } = session.state() else { - panic!("white backdrop must verify"); - }; - assert_shared_observation_backing(raw, current.report().observation()); - - let (snapshot, clone_allocations) = crate::test_support::measured_allocations(|| raw.clone()); - assert_eq!(clone_allocations, 0); - assert_eq!(snapshot, *raw); - assert_eq!(observation_backing_signature(&snapshot), raw_signature); - - crate::composition::reset_source_over_evaluation_count(); - let exact_replay = observed_update(1, [255; 3]); - let (result, replay_allocations) = - crate::test_support::measured_allocations(|| session.update(exact_replay).map(|_| ())); - assert_eq!(result, Ok(())); - assert_eq!(replay_allocations, 0); - assert_eq!(crate::composition::source_over_evaluation_count(), 0); - assert_eq!( - observation_backing_signature(raw_observed(&session)), - raw_signature, - ); - let PointSupportSessionStateV1::Ready { current } = session.state() else { + assert_eq!(control.evaluation_count(), 1); + assert_eq!(raw_observed(&session).backing_ptr_for_test(), raw_backing); + let SessionState::Ready { current } = session.state() else { panic!("exact replay must retain Ready"); }; - assert_shared_observation_backing(raw_observed(&session), current.report().observation()); -} + assert_shared_observation(raw_observed(&session), ¤t.observation); -#[test] -fn higher_revision_with_equal_content_rechecks_and_binds_new_evidence() { - let mut session = session(); - crate::composition::reset_source_over_evaluation_count(); - session.update(observed_update(1, [255; 3])).unwrap(); - assert_eq!(crate::composition::source_over_evaluation_count(), 1); - - session.update(observed_update(2, [255; 3])).unwrap(); - assert_eq!(crate::composition::source_over_evaluation_count(), 2); + session.update(unknown_update(2, 11)).unwrap(); + session.update(unknown_update(2, 11)).unwrap(); + assert_eq!(control.evaluation_count(), 1); assert_eq!(session.raw_head().revision(), Some(Revision::new(2))); - let PointSupportSessionStateV1::Ready { current } = session.state() else { - panic!("equal content at a higher revision must produce fresh Ready evidence"); - }; - assert_eq!(current.report().observation().revision(), Revision::new(2)); - assert_shared_observation_backing(raw_observed(&session), current.report().observation()); } #[test] -fn newer_unknown_replaces_only_raw_head_and_retains_one_verified_witness() { - let mut session = session(); +fn equal_content_at_a_higher_revision_rebinds_fresh_evidence() { + let (mut session, control, _) = session(); session.update(observed_update(1, [255; 3])).unwrap(); - let verified = raw_observed(&session).clone(); - let verified_signature = observation_backing_signature(&verified); + let first_backing = raw_observed(&session).backing_ptr_for_test(); - session.update(unknown_update(2, 10)).unwrap(); - let PointSupportSessionStateV1::Stale { previous } = session.state() else { - panic!("Unknown after Ready must become Stale"); - }; - assert_eq!(previous.report().observation(), &verified); - assert_eq!( - observation_backing_signature(previous.report().observation()), - verified_signature, - ); - - session.update(unknown_update(3, 11)).unwrap(); - let PointSupportSessionStateV1::Stale { previous } = session.state() else { - panic!("a newer Unknown must remain Stale"); - }; - assert_eq!(previous.report().observation(), &verified); - assert_eq!( - observation_backing_signature(previous.report().observation()), - verified_signature, - ); - let ObservationHeadViewV1::Unknown(raw_unknown) = session.raw_head() else { - panic!("the current Unknown belongs only to the raw head"); + session.update(observed_update(2, [255; 3])).unwrap(); + assert_eq!(control.evaluation_count(), 2); + assert_ne!(raw_observed(&session).backing_ptr_for_test(), first_backing); + let SessionState::Ready { current } = session.state() else { + panic!("higher revision must produce fresh Ready evidence"); }; - assert_eq!(raw_unknown.revision(), Revision::new(3)); - assert_eq!(raw_unknown.reason(), UnknownReasonId::new(11)); + assert_eq!(current.observation.revision(), Revision::new(2)); + assert_shared_observation(raw_observed(&session), ¤t.observation); } #[test] -fn unknown_idempotent_and_rejected_updates_never_evaluate() { - let mut session = session(); - crate::composition::reset_source_over_evaluation_count(); - session.update(unknown_update(1, 1)).unwrap(); - assert_eq!(crate::composition::source_over_evaluation_count(), 0); - let before = session.clone(); - let exact_unknown_replay = unknown_update(1, 1); - let (result, allocations) = crate::test_support::measured_allocations(|| { - session.update(exact_unknown_replay).map(|_| ()) - }); - assert!(result.is_ok()); - assert_eq!(allocations, 0); - assert_eq!(crate::composition::source_over_evaluation_count(), 0); - assert_eq!(session, before); +fn rejected_admission_neither_invokes_plan_nor_mutates_closed_state() { + let (mut session, control, _) = session(); + session.update(observed_update(1, [255; 3])).unwrap(); + let raw_backing = raw_observed(&session).backing_ptr_for_test(); - session.update(observed_update(2, [255; 3])).unwrap(); - assert_eq!(crate::composition::source_over_evaluation_count(), 1); - let before = session.clone(); - let exact_observed_replay = observed_update(2, [255; 3]); - let (result, allocations) = crate::test_support::measured_allocations(|| { - session.update(exact_observed_replay).map(|_| ()) - }); - assert!(result.is_ok()); - assert_eq!(allocations, 0); - assert_eq!(crate::composition::source_over_evaluation_count(), 1); - assert_eq!(session, before); - - let before = session.clone(); - crate::composition::reset_source_over_evaluation_count(); + let mut foreign = observed_update(2, [0; 3]); + foreign.stream = FOREIGN_STREAM; assert_eq!( - session.update(malformed_update(1)), - Err(PointSupportSessionUpdateErrorV1::Observation( - ObservationError::MissingSurfaceInputBinding { - scenario: ScenarioId::new(1), - input: SURFACE, - }, - )), - "malformed payload admission must precede lower-revision comparison", + session.update(foreign), + Err(SessionUpdateError::Observation( + ObservationError::StreamMismatch { + expected: STREAM, + actual: FOREIGN_STREAM, + } + )) ); - assert_eq!(crate::composition::source_over_evaluation_count(), 0); - assert_eq!(session, before); - - let before = session.clone(); assert_eq!( session.update(malformed_update(2)), - Err(PointSupportSessionUpdateErrorV1::Observation( + Err(SessionUpdateError::Observation( ObservationError::MissingSurfaceInputBinding { scenario: ScenarioId::new(1), input: SURFACE, - }, - )), - "malformed payload admission must precede same-revision equality", - ); - assert_eq!(crate::composition::source_over_evaluation_count(), 0); - assert_eq!(session, before); - - let before = session.clone(); - assert_eq!( - session.update(malformed_update(3)), - Err(PointSupportSessionUpdateErrorV1::Observation( - ObservationError::MissingSurfaceInputBinding { - scenario: ScenarioId::new(1), - input: SURFACE, - }, - )), - "malformed payload admission must precede applying a higher revision", - ); - assert_eq!(crate::composition::source_over_evaluation_count(), 0); - assert_eq!(session, before); - - let before = session.clone(); - assert_eq!( - session.update(ObservationUpdateInput { - stream: ObservationStreamId::new(99), - revision: Revision::new(1), - payload: ObservationPayloadInput::Scenarios(ObservedScenarioSetInput { - scenarios: vec![ScenarioInput { - id: ScenarioId::new(1), - bindings: vec![], - }], - }), - }), - Err(PointSupportSessionUpdateErrorV1::Observation( - ObservationError::StreamMismatch { - expected: STREAM, - actual: ObservationStreamId::new(99), - }, - )), - "stream affinity must be checked before parsing foreign payloads", + } + )) ); - assert_eq!(crate::composition::source_over_evaluation_count(), 0); - assert_eq!(session, before); - - let before = session.clone(); - let lower_revision = observed_update(1, [255; 3]); - let (result, allocations) = - crate::test_support::measured_allocations(|| session.update(lower_revision).map(|_| ())); assert_eq!( - result, - Err(PointSupportSessionUpdateErrorV1::Observation( + session.update(observed_update(0, [255; 3])), + Err(SessionUpdateError::Observation( ObservationError::RevisionOutOfOrder { - current: Revision::new(2), - incoming: Revision::new(1), - }, - )), + current: Revision::new(1), + incoming: Revision::new(0), + } + )) ); - assert_eq!(allocations, 0); - assert_eq!(crate::composition::source_over_evaluation_count(), 0); - assert_eq!(session, before); - - let before = session.clone(); - let same_revision_conflict = observed_update(2, [0; 3]); - let (result, allocations) = crate::test_support::measured_allocations(|| { - session.update(same_revision_conflict).map(|_| ()) - }); assert_eq!( - result, - Err(PointSupportSessionUpdateErrorV1::Observation( + session.update(observed_update(1, [0; 3])), + Err(SessionUpdateError::Observation( ObservationError::RevisionConflict { - revision: Revision::new(2), - }, - )), + revision: Revision::new(1), + } + )) ); - assert_eq!(allocations, 0); - assert_eq!(crate::composition::source_over_evaluation_count(), 0); - assert_eq!(session, before); + + assert_eq!(control.evaluation_count(), 1); + assert_eq!(raw_observed(&session).backing_ptr_for_test(), raw_backing); + assert_eq!(verified_revision(session.state()), Some(Revision::new(1))); } #[test] -fn synthetic_post_admission_pre_evaluation_failure_is_atomic_and_retryable() { - let mut session = session(); +fn plan_failure_commits_neither_raw_head_nor_lifecycle_and_retry_is_fresh() { + let (mut session, control, _) = session(); session.update(observed_update(1, [255; 3])).unwrap(); - let raw_signature = observation_backing_signature(raw_observed(&session)); - let PointSupportSessionStateV1::Ready { current } = session.state() else { - panic!("white backdrop must verify"); - }; - assert_shared_observation_backing(raw_observed(&session), current.report().observation()); - let before = session.clone(); - // This hook fires after observation admission and before evaluation. It - // proves transaction rollback at that Session boundary, not recoverable - // failure of every allocation performed by Rust's global allocator. - session.force_next_resource_failure(); - crate::composition::reset_source_over_evaluation_count(); - assert_eq!( - session.update(observed_update(2, [255; 3])), - Err(PointSupportSessionUpdateErrorV1::ResourceExhausted) - ); - assert_eq!(crate::composition::source_over_evaluation_count(), 0); - assert_eq!(session, before); - assert_eq!(session.state(), before.state()); - assert_eq!(session.raw_head(), before.raw_head()); - assert_eq!(session.composition_profile(), before.composition_profile()); + let raw_backing = raw_observed(&session).backing_ptr_for_test(); + control.fail_next(); + assert_eq!( - observation_backing_signature(raw_observed(&session)), - raw_signature, + session.update(observed_update(2, [0; 3])), + Err(SessionUpdateError::Plan(SentinelError::Forced)) ); - let PointSupportSessionStateV1::Ready { current } = session.state() else { - panic!("synthetic pre-evaluation failure must retain Ready"); - }; - assert_shared_observation_backing(raw_observed(&session), current.report().observation()); + assert_eq!(control.evaluation_count(), 2); + assert_eq!(raw_observed(&session).backing_ptr_for_test(), raw_backing); + assert_eq!(session.raw_head().revision(), Some(Revision::new(1))); + assert_eq!(verified_revision(session.state()), Some(Revision::new(1))); - session.update(observed_update(2, [255; 3])).unwrap(); - assert_eq!(crate::composition::source_over_evaluation_count(), 1); - assert_eq!(verified_revision(session.state()), Some(Revision::new(2))); - let PointSupportSessionStateV1::Ready { current } = session.state() else { - panic!("retry must commit Ready"); - }; - assert_shared_observation_backing(raw_observed(&session), current.report().observation()); + let (cause_observation, previous_revision) = + match session.update(observed_update(2, [0; 3])).unwrap() { + SessionState::Failed { cause, previous } => ( + cause.observation.clone(), + previous.as_ref().unwrap().observation.revision(), + ), + _ => panic!("retry must re-prepare, re-evaluate and commit"), + }; + assert_eq!(control.evaluation_count(), 3); + assert_eq!(cause_observation.revision(), Revision::new(2)); + assert_eq!(previous_revision, Revision::new(1)); + assert_shared_observation(raw_observed(&session), &cause_observation); } #[test] -fn evaluator_failure_is_atomic_and_retryable() { - let mut session = session(); +fn detached_plan_evidence_is_rejected_before_raw_or_lifecycle_commit() { + let (mut session, control, _) = session(); session.update(observed_update(1, [255; 3])).unwrap(); - let raw_signature = observation_backing_signature(raw_observed(&session)); - let before = session.clone(); + let first_observation = raw_observed(&session).clone(); + control.substitute_next_with(first_observation); - session.force_next_evaluator_failure(); - crate::composition::reset_source_over_evaluation_count(); assert_eq!( session.update(observed_update(2, [255; 3])), - Err(PointSupportSessionUpdateErrorV1::InternalInvariant), + Err(SessionUpdateError::EvidenceBindingInvariant) ); - assert_eq!(crate::composition::source_over_evaluation_count(), 0); - assert_eq!(session, before); - assert_eq!(session.state(), before.state()); - assert_eq!(session.raw_head(), before.raw_head()); - assert_eq!( - observation_backing_signature(raw_observed(&session)), - raw_signature, - ); - let PointSupportSessionStateV1::Ready { current } = session.state() else { - panic!("evaluator failure must retain Ready"); - }; - assert_shared_observation_backing(raw_observed(&session), current.report().observation()); + assert_eq!(control.evaluation_count(), 2); + assert_eq!(session.raw_head().revision(), Some(Revision::new(1))); + assert_eq!(verified_revision(session.state()), Some(Revision::new(1))); - session.update(observed_update(2, [255; 3])).unwrap(); - assert_eq!(crate::composition::source_over_evaluation_count(), 1); - assert_eq!(verified_revision(session.state()), Some(Revision::new(2))); - let PointSupportSessionStateV1::Ready { current } = session.state() else { - panic!("retry after evaluator failure must commit Ready"); + let current_observation = match session.update(observed_update(2, [255; 3])).unwrap() { + SessionState::Ready { current } => current.observation.clone(), + _ => panic!("a fresh retry must bind evidence from the current observation"), }; - assert_shared_observation_backing(raw_observed(&session), current.report().observation()); + assert_eq!(control.evaluation_count(), 3); + assert_eq!(current_observation.revision(), Revision::new(2)); + assert_shared_observation(raw_observed(&session), ¤t_observation); } -proptest! { - #[test] - fn lifecycle_matches_pure_last_verified_model(ops in prop::collection::vec(0u8..5, 1..60)) { - let mut session = session(); - session.update(observed_update(1, [255; 3])).unwrap(); - let mut raw_revision = 1u64; - let mut expected_kind = StateKind::Ready; - let mut expected_verified = Some(Revision::new(1)); - let mut last_applied = observed_update(1, [255; 3]); - - for (next_revision, op) in (2u64..).zip(ops) { - let before = session.clone(); - match op { - 0 => { - let update = observed_update(next_revision, [255; 3]); - session.update(update.clone()).unwrap(); - raw_revision = next_revision; - expected_kind = StateKind::Ready; - expected_verified = Some(Revision::new(next_revision)); - last_applied = update; - } - 1 => { - let update = observed_update(next_revision, [0; 3]); - session.update(update.clone()).unwrap(); - raw_revision = next_revision; - expected_kind = StateKind::Failed; - last_applied = update; - } - 2 => { - let update = unknown_update(next_revision, u32::from(op)); - session.update(update.clone()).unwrap(); - raw_revision = next_revision; - expected_kind = if expected_verified.is_some() { - StateKind::Stale - } else { - StateKind::Waiting - }; - last_applied = update; - } - 3 => { - session.update(last_applied.clone()).unwrap(); - prop_assert_eq!(&session, &before); - } - _ => { - let rejected = observed_update(raw_revision, [17; 3]); - prop_assert!(session.update(rejected).is_err()); - prop_assert_eq!(&session, &before); - } - } - prop_assert_eq!(state_kind(session.state()), expected_kind); - prop_assert_eq!(verified_revision(session.state()), expected_verified); - } +#[test] +fn session_source_contains_one_generic_update_owner_and_no_legacy_runtime() { + let source = include_str!("session.rs"); + assert_eq!(source.matches("pub(crate) fn update(").count(), 1); + for forbidden in [ + "PointSupportSessionV1", + "PointSupportSessionStateV1", + "PointSupportSessionUpdateErrorV1", + "Weak<", + "ProgramExpired", + "ObservationStreamBinding", + "SurfaceUpdate", + "Box tuple[str, int]: (POINT_SOURCE, b"NumericalSiteIdV2::PointSupportRetainedReferenceSurplusV1;", b"NumericalSiteIdV2::Wcag22Srgb8ContrastV1;"), (POINT_SOURCE, b"let current_distance = reference_distance(current_measurement)?;", b"let current_distance = baseline.distance;"), (POINT_SOURCE, b"Ok(assessment.bind(observation))", b"Ok(assessment.bind_unchecked(observation))"), - (POINT_SOURCE, b" let backdrop = values.get(surface_index).copied().ok_or(\n", b" let backdrop = values.first().copied().ok_or(\n"), + (POINT_SOURCE, b" let backdrop = values.get(*surface_index).copied().ok_or(\n", b" let backdrop = values.first().copied().ok_or(\n"), (POINT_SOURCE, b" if !observation.shares_schema_backing_with(&plan.surface_schema) {\n", b" if observation.shares_schema_backing_with(&plan.surface_schema) {\n"), (POINT_SOURCE, b" _permit: SessionObservationBindingPermitV1,\n", b" _permit: (),\n"), (POINT_SOURCE, b"use crate::wcag22::{Wcag22CriterionV1, Wcag22MeasurementV1, measure_wcag22_srgb8};", b"use crate::wcag22::{Wcag22CriterionV1, Wcag22MeasurementV1, measure_wcag22_srgb8 as canonical_measure_wcag22_srgb8};\nfn measure_wcag22_srgb8(foreground: [u8; 3], background: [u8; 3]) -> Wcag22MeasurementV1 { canonical_measure_wcag22_srgb8(background, foreground) }"), @@ -203,13 +203,15 @@ def verify_source_binding() -> tuple[str, int]: (OBSERVATION_SOURCE, b"if expected_input != actual_input", b"if expected_input == actual_input"), (OBSERVATION_SOURCE, b"Some(observation.revision)", b"None"), (OBSERVATION_SOURCE, b"(self.owner, self.observation)", b"unreachable!()"), + (OBSERVATION_SOURCE, b" && Rc::ptr_eq(&self.backing, &other.backing)\n", b" && self.backing == other.backing\n"), (SESSION_SOURCE, b" Self::Observed(observation) => ObservationHeadViewV1::Observed(observation),\n", b" Self::Observed(_) => ObservationHeadViewV1::Empty,\n"), (SESSION_SOURCE, b" let next_raw_head = SessionObservationHeadV1::Observed(observation.clone());\n", b" let next_raw_head = SessionObservationHeadV1::Empty;\n"), (SESSION_SOURCE, b" *raw_head = next_raw_head;\n", b" *raw_head = SessionObservationHeadV1::Empty;\n"), - (SESSION_SOURCE, b"recheck: compiled.into_session_recheck(),", b"recheck: unreachable!(),"), - (SESSION_SOURCE, b" #[cfg(not(test))]\n {\n self.recheck\n .evaluate(observation, SessionObservationBindingPermitV1::mint())\n }", b" #[cfg(not(test))]\n {\n self.recheck\n .evaluate(observation, SessionObservationBindingPermitV1::bypass())\n }"), - (SESSION_SOURCE, b"PointSupportEvaluationErrorV1::ResourceExhausted => {\n PointSupportSessionUpdateErrorV1::ResourceExhausted", b"PointSupportEvaluationErrorV1::ResourceExhausted => {\n PointSupportSessionUpdateErrorV1::InternalInvariant"), - (SESSION_SOURCE, b"PointSupportSessionStateV1::Ready { current } => Some(current),", b"PointSupportSessionStateV1::Ready { .. } => None,"), + (SESSION_SOURCE, b" Some(previous) => SessionState::Stale { previous },", b" Some(_) => SessionState::Waiting,"), + (SESSION_SOURCE, b" .evaluate(observation, SessionObservationBindingPermitV1::mint())", b" .evaluate(observation, SessionObservationBindingPermitV1::for_test())"), + (SESSION_SOURCE, b" SessionDecision::Verified(current) => SessionState::Ready { current },", b" SessionDecision::Verified(current) => SessionState::Stale { previous: current },"), + (SESSION_SOURCE, b" SessionDecision::Violation(cause) => SessionState::Failed { cause, previous },", b" SessionDecision::Violation(_) => SessionState::Waiting,"), + (SESSION_SOURCE, b" return Err(SessionUpdateError::EvidenceBindingInvariant);\n", b" unreachable!();\n"), (NUMERICS_SOURCE, b"proof_ids: [NumericalProofIdV2::PointSupportReferenceSurplusIntegerV1],\n bound_status: Available", b"proof_ids: [NumericalProofIdV2::PointSupportReferenceSurplusIntegerV1],\n bound_status: Unavailable"), (COMPOSITION_SOURCE, b"f64::from(backdrop) + alpha * (f64::from(tint) - f64::from(backdrop))", b"f64::from(tint)"), (APPEARANCE_SOURCE, b"self.opacity\n", b"crate::composition::AdmittedOpacityV1::OPAQUE\n"), From a244a61abfc36b76188713d551b997294fd1141d Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Wed, 22 Jul 2026 21:59:43 +0300 Subject: [PATCH 29/58] feat(core): bind Program assessments to context-bound LCS --- .github/workflows/publish.yml | 2 +- ...rt-reference-surplus-q55-bps-proof-v1.json | 2 +- .../src/appearance_graph_tests.rs | 4 +- .../labcolors-core/src/constraints/exact.rs | 35 +- crates/labcolors-core/src/constraints/mod.rs | 182 +++++- .../labcolors-core/src/constraints/wcag22.rs | 35 +- .../src/generic_boundary_tests.rs | 297 ++++++++- crates/labcolors-core/src/joint.rs | 5 +- crates/labcolors-core/src/joint_tests.rs | 7 +- crates/labcolors-core/src/lcs_occurrence.rs | 120 +++- crates/labcolors-core/src/lib.rs | 3 + crates/labcolors-core/src/observation.rs | 12 +- .../labcolors-core/src/observation_tests.rs | 39 +- .../labcolors-core/src/output_projection.rs | 107 +--- .../src/output_projection_tests.rs | 25 +- crates/labcolors-core/src/point_support.rs | 2 +- .../labcolors-core/src/point_support_tests.rs | 22 +- .../src/program_lcs_integration_tests.rs | 588 ++++++++++++++++++ crates/labcolors-core/src/program_session.rs | 330 ++++++++-- .../src/program_session_tests.rs | 65 +- crates/labcolors-core/src/session_tests.rs | 7 +- scripts/point-support-release-contract.cjs | 1 + scripts/verify-package-release.mjs | 2 +- scripts/verify_point_support_surplus.py | 7 +- 24 files changed, 1653 insertions(+), 246 deletions(-) create mode 100644 crates/labcolors-core/src/program_lcs_integration_tests.rs diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d6761735..2acc34b9 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -674,7 +674,7 @@ jobs: JSON.stringify(point.sourceBinding.exclusions) !== JSON.stringify(pointProof.source_binding_exclusions) || point.sourceBinding.closureSha256 !== pointProof.source_closure_sha256 || - pointProof.source_negative_controls !== 42 || + pointProof.source_negative_controls !== 43 || pointAlgebra?.method !== "exact-sparse-integer-polynomial-identities-plus-positive-denominator-order-lemma-v1" || pointAlgebra?.wolfram_language_cross_check?.query_sha256 !== 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 9dd07d2a..bd2b783d 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":"62871d3b874e4b4601c11db97f5ab73cd4e9781e462330bccda0d9dfdadcb5be","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":"2dba7f59bd0f8d665b79d3286527cc67a99d1dfe1f7604e6d19be3643e39ed5d","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":"57e6feeab15182322c258829d10214c31bb3c93fa58d8d47e37127db204cdc28"},{"kind":"rust-source","path":"crates/labcolors-core/src/composition.rs","sha256":"195a67327a3bd86d7816b634481389930bf68577bb1202fad14c2ea152df8625"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/exact.rs","sha256":"45bd09cd8f6f74860a0a3ffeb410ee2c3cf91b4868877abbc949e724b61e1ccd"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/mod.rs","sha256":"8b78b9a31e7ade3f7b99edf3fbcf2bc5d912509cd851e82f7380e2a19e25431e"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/wcag22.rs","sha256":"985e21abccd1fcb4a3f9624d5947ff3d1af7a1bf26b6b170ea8bbef77d6f1798"},{"kind":"rust-source","path":"crates/labcolors-core/src/hash.rs","sha256":"f97a0fd7d6ad3162f0f1dfb326fccfb7ed40da9a8fa67a5b8a239a1ae2ae49c3"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"9e7b2c8e8a1d02d387c5c92c995de8f8148296c74efadf895e71318fbf0f4b0e"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"bb5fc6bf4ad79e15a31dc6ec28341994f7cdd5e28efe60f9c5d7ae8bed9a9a63"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"b3be8242586c0d9952332e7c830551af1c033739e8148f836f873e6d9e304315"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"0a1fdf10c9ecae497cfcd447a6af937f2b07d43b81b2cd28a50d998e11197fe9"},{"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":42,"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":"d137ef2c6c780e4d0a8ee40b5f379c139ae116c1eaf753ce6060ac4e1f60d51d"} +{"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":"46e2826dee60e6cf66e4aa5a4cc05dbe6c5c359eaf1eb44337d796f3f280ad77","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":"1845ff9cb4584f752d576e87a456c758f280b447410f5c0666e8c35d3f0f199b","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":"57e6feeab15182322c258829d10214c31bb3c93fa58d8d47e37127db204cdc28"},{"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":"15b6437ed59c92d82b928d9d913d25c14b430ba8c75db54a02f7da18abcde684"},{"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":"a58acbae5b06a8cd9a45adf93ddfbdfb569c5916b60140bd4046182aab2a9518"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"6b6338802ff32cd4ac4c21a7f468721ba09e112adc208b2170dd8becdb9462be"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"abed15a542b5e8031f0ebfe36138232920c7898c9eae1a2037c6455033ecd91e"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"945755f2eb91eb21aeee9b7a7b0e67b093d7e766af35a121480003669d88a053"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"0a1fdf10c9ecae497cfcd447a6af937f2b07d43b81b2cd28a50d998e11197fe9"},{"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":"3f1461805ba0edc17b8b167da43d61dc1e6f73da24bb99a80bf1d7401bccc110"} diff --git a/crates/labcolors-core/src/appearance_graph_tests.rs b/crates/labcolors-core/src/appearance_graph_tests.rs index 14a047af..109e8f4c 100644 --- a/crates/labcolors-core/src/appearance_graph_tests.rs +++ b/crates/labcolors-core/src/appearance_graph_tests.rs @@ -159,7 +159,9 @@ fn static_exact_program_is_declarative_topology_plus_typed_constraint() { crate::appearance::PhysicalProgramIdentityV1::SolidOpacityOverSurfaceEncodedSrgb8V1 ); assert_eq!( - crate::constraints::ExactSrgb8IdentityV1.identity(), + >::identity(&crate::constraints::ExactSrgb8IdentityV1), crate::constraints::ExactConstraintIdentityV1::FinalSrgb8IdentityV1 ); } diff --git a/crates/labcolors-core/src/constraints/exact.rs b/crates/labcolors-core/src/constraints/exact.rs index c3e06d59..34fed4fb 100644 --- a/crates/labcolors-core/src/constraints/exact.rs +++ b/crates/labcolors-core/src/constraints/exact.rs @@ -1,7 +1,7 @@ use crate::Srgb8; use crate::appearance::ModeledSrgb8PointOccurrence; use crate::constraints::{ - Evaluator, HardClassifier, HardDecision, VisiblePointPassEvidence, + Evaluator, HardClassifier, HardDecision, ProgramPointTargetV1, VisiblePointPassEvidence, VisiblePointViolationEvidence, private, }; use core::convert::Infallible; @@ -78,6 +78,39 @@ impl Evaluator for ExactSrgb8IdentityV1 { } } +impl Evaluator for ExactSrgb8IdentityV1 { + type Invocation = Srgb8; + type Identity = ExactConstraintIdentityV1; + type Release = ExactIdentityReleaseV1; + type Capability = ExactIdentityCapabilityV1; + type Measurement = Srgb8; + type Error = Infallible; + + fn identity(&self) -> Self::Identity { + Self::IDENTITY + } + + fn release(&self) -> Self::Release { + ExactIdentityReleaseV1::V1 + } + + fn capability(&self) -> Self::Capability { + ExactIdentityCapabilityV1::FinalOccurrenceSrgb8IdentityV1 + } + + fn evaluate( + &self, + occurrence: &ProgramPointTargetV1, + invocation: &Self::Invocation, + ) -> Result { + >::evaluate( + self, + &occurrence.encoded(), + 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 c5618c13..ec44ffe9 100644 --- a/crates/labcolors-core/src/constraints/mod.rs +++ b/crates/labcolors-core/src/constraints/mod.rs @@ -7,6 +7,7 @@ use crate::Srgb8; use crate::appearance::{ModeledSrgb8PointOccurrence, ResolvedOccurrence, VisiblePointBindingV1}; +use crate::lcs_occurrence::ModeledLcsOccurrenceV1; mod exact; pub(crate) use exact::{ @@ -153,26 +154,183 @@ pub(crate) type PointViolation = , >>::Violation; -/// One statically dispatched point evaluator/classifier family. -/// -/// The first executable Program slice is deliberately homogeneous: every -/// compiled invocation has this evaluator's one typed invocation and result -/// family. The trait remains sealed through both parent protocols; this slice -/// contains no dynamic registry or open payload enum. -pub(crate) trait PointEvaluatorV1: +/// Program-only target: the exact encoded point used by physical evaluators +/// and the context-bound LCS occurrence derived from that same visible signal. +/// Neither half is optional, and construction remains inside the sole Program +/// execution path. +#[derive(Debug, Clone, Copy)] +pub(crate) struct ProgramPointTargetV1 { + encoded: ModeledSrgb8PointOccurrence, + modeled_lcs: ModeledLcsOccurrenceV1, +} + +impl ProgramPointTargetV1 { + pub(crate) const fn new( + encoded: ModeledSrgb8PointOccurrence, + modeled_lcs: ModeledLcsOccurrenceV1, + ) -> Self { + Self { + encoded, + modeled_lcs, + } + } + + pub(crate) const fn encoded(self) -> ModeledSrgb8PointOccurrence { + self.encoded + } + + pub(crate) const fn modeled_lcs(self) -> ModeledLcsOccurrenceV1 { + self.modeled_lcs + } +} + +pub(crate) type ProgramPointInvocation = + >::Invocation; +pub(crate) type ProgramPointMeasurement = + >::Measurement; +pub(crate) type ProgramPointPass = , + ProgramPointMeasurement, +>>::Pass; +pub(crate) type ProgramPointViolation = , + ProgramPointMeasurement, +>>::Violation; + +/// Sealed, statically dispatched evaluator family for context-bound Program +/// occurrences. Fixed point-support intentionally keeps its narrower encoded +/// target and therefore cannot silently satisfy an LCS-aware invocation. +pub(crate) trait ProgramPointEvaluatorV1: Sized - + Evaluator - + HardClassifier, PointMeasurement> + + Evaluator + + HardClassifier, ProgramPointMeasurement> { } -impl PointEvaluatorV1 for Evaluation where +impl ProgramPointEvaluatorV1 for Evaluation where Evaluation: Sized - + Evaluator - + HardClassifier, PointMeasurement> + + Evaluator + + HardClassifier, ProgramPointMeasurement> { } +/// Program evidence binds the physical source-over certificate and the exact +/// modeled LCS provenance/context used by the evaluator in one non-forgeable +/// value. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct ProgramVisiblePointBindingV1 { + physical: VisiblePointBindingV1, + modeled_lcs: ModeledLcsOccurrenceV1, +} + +impl ProgramVisiblePointBindingV1 { + pub(crate) const fn physical(self) -> VisiblePointBindingV1 { + self.physical + } + + pub(crate) const fn modeled_lcs(self) -> ModeledLcsOccurrenceV1 { + self.modeled_lcs + } +} + +type BoundProgramPointMeasurement = BoundEvidence< + ProgramVisiblePointBindingV1, + >::Identity, + >::Release, + >::Capability, + >::Invocation, + Measurement, +>; + +pub(crate) type ProgramVisiblePointPassEvidence = BoundProgramPointMeasurement< + Evaluation, + ClassifiedMeasurement, ProgramPointPass>, +>; +pub(crate) type ProgramVisiblePointViolationEvidence = BoundProgramPointMeasurement< + Evaluation, + ClassifiedMeasurement, ProgramPointViolation>, +>; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct ProgramPointBindingMismatchV1 { + physical: Srgb8, + modeled: Srgb8, +} + +impl ProgramPointBindingMismatchV1 { + pub(crate) const fn physical(self) -> Srgb8 { + self.physical + } + + pub(crate) const fn modeled(self) -> Srgb8 { + self.modeled + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum ProgramPointAssessmentErrorV1 { + Binding(ProgramPointBindingMismatchV1), + Evaluator(EvaluatorError), +} + +pub(crate) type ProgramPointAssessmentResultV1 = Result< + HardDecision< + ProgramVisiblePointPassEvidence, + ProgramVisiblePointViolationEvidence, + >, + ProgramPointAssessmentErrorV1<>::Error>, +>; + +pub(crate) fn assess_program_point_hard( + source: &ResolvedOccurrence, + modeled_lcs: ModeledLcsOccurrenceV1, + evaluator: &Evaluation, + invocation: ProgramPointInvocation, +) -> ProgramPointAssessmentResultV1 +where + Evaluation: Evaluator + + HardClassifier, ProgramPointMeasurement>, +{ + let physical = Srgb8::new(source.visible()); + let modeled = modeled_lcs.signal().srgb8(); + if physical != modeled { + return Err(ProgramPointAssessmentErrorV1::Binding( + ProgramPointBindingMismatchV1 { physical, modeled }, + )); + } + let target = ProgramPointTargetV1::new(source.modeled_srgb8_point(), modeled_lcs); + let binding = ProgramVisiblePointBindingV1 { + physical: source.visible_point_binding(), + modeled_lcs: target.modeled_lcs(), + }; + let measurement = + >::evaluate(evaluator, &target, &invocation) + .map_err(ProgramPointAssessmentErrorV1::Evaluator)?; + let classification = evaluator.classify(&invocation, &measurement); + let identity = >::identity(evaluator); + let release = >::release(evaluator); + let capability = >::capability(evaluator); + + Ok(match classification { + HardDecision::Pass(payload) => HardDecision::Pass(BoundEvidence { + binding, + identity, + release, + capability, + invocation, + measurement: ClassifiedMeasurement::new(measurement, payload), + }), + HardDecision::Violation(payload) => HardDecision::Violation(BoundEvidence { + binding, + identity, + release, + capability, + invocation, + measurement: ClassifiedMeasurement::new(measurement, payload), + }), + }) +} + type BoundVisiblePointMeasurement = BoundEvidence< VisiblePointBindingV1, >::Identity, diff --git a/crates/labcolors-core/src/constraints/wcag22.rs b/crates/labcolors-core/src/constraints/wcag22.rs index 347014ce..d57d854a 100644 --- a/crates/labcolors-core/src/constraints/wcag22.rs +++ b/crates/labcolors-core/src/constraints/wcag22.rs @@ -1,5 +1,5 @@ use crate::appearance::ModeledSrgb8PointOccurrence; -use crate::constraints::{Evaluator, HardClassifier, HardDecision, private}; +use crate::constraints::{Evaluator, HardClassifier, HardDecision, ProgramPointTargetV1, private}; use crate::numerics::NumericalDecisionEvidenceV1; use crate::wcag22::{ Wcag22ApplicableDecisionV1, Wcag22AssessmentV1, Wcag22ClientDeclaredNotApplicableV1, @@ -142,6 +142,39 @@ impl Evaluator for Wcag22Srgb8V1 { } } +impl Evaluator for Wcag22Srgb8V1 { + type Invocation = Wcag22CriterionV1; + type Identity = Wcag22Srgb8EvaluatorIdentityV1; + type Release = Wcag22ProfileIdV1; + type Capability = Wcag22Srgb8CapabilityV1; + type Measurement = ApplicableWcag22MeasurementV1; + type Error = ApplicableWcag22EvaluationErrorV1; + + fn identity(&self) -> Self::Identity { + Wcag22Srgb8EvaluatorIdentityV1 + } + + fn release(&self) -> Self::Release { + wcag22_profile_v1().profile_id + } + + fn capability(&self) -> Self::Capability { + Wcag22Srgb8CapabilityV1 + } + + fn evaluate( + &self, + target: &ProgramPointTargetV1, + invocation: &Self::Invocation, + ) -> Result { + >::evaluate( + self, + &target.encoded(), + 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 13952cfd..a78bb86a 100644 --- a/crates/labcolors-core/src/generic_boundary_tests.rs +++ b/crates/labcolors-core/src/generic_boundary_tests.rs @@ -1,9 +1,15 @@ const APPEARANCE_SOURCE: &str = include_str!("appearance.rs"); +const CONSTRAINTS_SOURCE: &str = include_str!("constraints/mod.rs"); +const EXACT_CONSTRAINT_SOURCE: &str = include_str!("constraints/exact.rs"); +const JOINT_SOURCE: &str = include_str!("joint.rs"); +const LIB_SOURCE: &str = include_str!("lib.rs"); const LCS_OCCURRENCE_SOURCE: &str = include_str!("lcs_occurrence.rs"); const OBSERVATION_SOURCE: &str = include_str!("observation.rs"); +const OUTPUT_PROJECTION_SOURCE: &str = include_str!("output_projection.rs"); const POINT_SUPPORT_SOURCE: &str = include_str!("point_support.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] = [ ("appearance.rs", APPEARANCE_SOURCE), @@ -47,6 +53,18 @@ fn normalized_source_scope(source: &str, start: &str, end: &str) -> String { .join(" ") } +fn contains_rust_identifier(source: &str, identifier: &str) -> bool { + fn is_continue(character: char) -> bool { + character == '_' || character.is_ascii_alphanumeric() + } + + source.match_indices(identifier).any(|(start, _)| { + let before = source[..start].chars().next_back(); + let after = source[start + identifier.len()..].chars().next(); + !before.is_some_and(is_continue) && !after.is_some_and(is_continue) + }) +} + #[test] fn generic_physical_and_transport_modules_contain_no_client_or_legacy_vocabulary() { for (path, source) in GENERIC_SOURCES { @@ -70,7 +88,7 @@ fn shared_observation_ssot_has_one_backing_without_lifecycle_or_adapter_facades( concat!( "struct ObservedScenarioSet { ", "cases: Box<[PhysicalScenario]>, ", - "values: Box<[Srgb8]>, ", + "values: Box<[ColorSignal]>, ", "provenance: Box<[ScenarioId]>, ", "}", ), @@ -339,28 +357,279 @@ fn shared_observation_ssot_has_one_backing_without_lifecycle_or_adapter_facades( } #[test] -fn encoded_point_transport_does_not_claim_lcs_observation_types() { - for forbidden in ["TristimulusSample", "LcsOccurrence", "AppearanceState"] { +fn program_session_owns_context_bound_lcs_evidence_and_one_session_scratch_cache() { + for required in [ + "ProgramPointTargetV1", + "ModeledLcsOccurrenceV1", + "AppearanceContextId", + "ProgramVisiblePointPassEvidence", + "ProgramVisiblePointViolationEvidence", + "ModeledLcsOccurrenceV1::from_signal_in_context(", + "source.visible() != source.certificate().output_rgb()", + "binding.context,", + "assess_program_point_hard(", + ] { + assert!( + PROGRAM_SESSION_SOURCE.contains(required), + "Program must retain physical-source and context-bound LCS evidence; missing `{required}`", + ); + } + + let program_evidence_binding = normalized_source_scope( + CONSTRAINTS_SOURCE, + "pub(crate) struct ProgramVisiblePointBindingV1 {", + "impl ProgramVisiblePointBindingV1", + ); + for required in [ + "physical: VisiblePointBindingV1,", + "modeled_lcs: ModeledLcsOccurrenceV1,", + ] { + assert!( + program_evidence_binding.contains(required), + "Program evidence must bind source-over and LCS context in one value; missing `{required}`", + ); + } + + let result = source_scope( + PROGRAM_SESSION_SOURCE, + "impl ProgramConstraintResultV1", + "/// One canonical `physical case × constraint` report cell.", + ); + for required in [ + "fn binding(&self) -> ProgramVisiblePointBindingV1", + "fn modeled_lcs_occurrence(&self) -> ModeledLcsOccurrenceV1", + "self.binding().modeled_lcs()", + ] { + assert!( + result.contains(required), + "Program result must project modeled LCS from its evidence SSOT; missing `{required}`", + ); + } + let cell = source_scope( + PROGRAM_SESSION_SOURCE, + "pub struct ProgramConstraintCellV1", + "impl ProgramConstraintCellV1", + ); + assert!( + cell.contains("result: ProgramConstraintResultV1,"), + "each Program cell must own the typed evidence result", + ); + assert!( + !cell.contains("modeled_lcs_occurrence: ModeledLcsOccurrenceV1,"), + "a Program cell must not duplicate the modeled occurrence already owned by evidence", + ); + + let plan = source_scope( + PROGRAM_SESSION_SOURCE, + "pub struct ProgramSessionPlan", + "impl session_private::PlanSealed for ProgramSessionPlan", + ); + assert_eq!( + plan.matches("modeled_occurrences: Vec>,") + .count(), + 1, + "each Program Session must own exactly one reusable modeled-occurrence scratch cache", + ); + assert_eq!( + PROGRAM_SESSION_SOURCE + .matches("modeled_occurrences: Vec>,") + .count(), + 1, + "the modeled-occurrence scratch cache must not be duplicated outside the Session plan", + ); + + let preparation = normalized_source_scope( + PROGRAM_SESSION_SOURCE, + "let all_occurrence_contexts = compile_occurrence_contexts(", + "let outputs = compile_outputs(", + ); + for required in [ + "compile_constraints::(&graph, &all_occurrence_contexts, program.constraints)?", + "compact_constraint_contexts(&all_occurrence_contexts, &mut constraints)?", + ] { + assert!( + preparation.contains(required), + "Program compilation must compact full occurrence metadata to constrained targets; missing `{required}`", + ); + } + + let compaction = normalized_source_scope( + PROGRAM_SESSION_SOURCE, + "fn compact_constraint_contexts(", + "fn compile_outputs(", + ); + for required in [ + "targets.sort_unstable(); targets.dedup();", + ".binary_search_by_key(&constraint.target_id, |binding| binding.occurrence)", + "constraint.modeled_occurrence_index = index;", + ] { + assert!( + compaction.contains(required), + "cold compilation must deduplicate targets and remap every constraint; missing `{required}`", + ); + } + + let hot_evaluation = source_scope( + PROGRAM_SESSION_SOURCE, + "fn evaluate_program_session(", + "fn map_program_execution_binding_error(", + ); + assert!( + !hot_evaluation.contains("binary_search"), + "hot Program evaluation must consume compile-time direct indices without searching", + ); + for required in [ + "plan.modeled_occurrences.fill(None);", + ".get(constraint.modeled_occurrence_index)", + ".get_mut(constraint.modeled_occurrence_index)", + ] { + assert!( + hot_evaluation.contains(required), + "hot Program evaluation must reuse the compact direct-index cache; missing `{required}`", + ); + } +} + +#[test] +fn program_lcs_boundary_has_no_legacy_color_or_projection_shortcuts() { + for forbidden in [ + "LcsColor", + "ViewingConditions", + "from_hex", + "to_hex", + "is_dark_theme", + "CAM16-UCS", + "ucs_", + "ProjectionSourceV1", + "default_context", + "default context", + "DefaultContext", + ] { assert!( !PROGRAM_SESSION_SOURCE.contains(forbidden), - "program_session.rs is encoded point transport, not `{forbidden}` evidence" + "program_session.rs must not bypass explicit source/context admission through `{forbidden}`", ); } + + for (path, source) in [ + ("point_support.rs", POINT_SUPPORT_SOURCE), + ("joint.rs", JOINT_SOURCE), + ] { + for forbidden in [ + "ProgramPointTargetV1", + "ProgramPointEvaluatorV1", + "ProgramPointInvocation", + "ProgramVisiblePointBindingV1", + "ProgramVisiblePointPassEvidence", + "ProgramVisiblePointViolationEvidence", + "ModeledLcsOccurrenceV1", + "AppearanceContextId", + "LcsOccurrence", + ] { + assert!( + !contains_rust_identifier(source, forbidden), + "{path} must not absorb Program-only contextual LCS type `{forbidden}`", + ); + } + } + + for forbidden in [ + "PointEvaluatorV1", + "PointInvocation", + "VisiblePointBindingV1", + "VisiblePointPassEvidence", + "VisiblePointViolationEvidence", + "assess_visible_point_hard", + ] { + assert!( + !contains_rust_identifier(PROGRAM_SESSION_SOURCE, forbidden), + "program_session.rs must not route Program through legacy point evidence `{forbidden}`", + ); + } + + for forbidden in [ + "AppearanceState", + "Cam16ViewV1", + "Cam16SurroundV1", + "ViewingConditions", + "derive_cam16_view_v1", + "forward_correlates_v1", + ] { + assert!( + !contains_rust_identifier(PROGRAM_SESSION_SOURCE, forbidden), + "program_session.rs hot lowering must not derive CAM16 through `{forbidden}`", + ); + } + assert!( + !PROGRAM_SESSION_SOURCE.contains(".cam16("), + "program_session.rs hot lowering must not request a CAM16 view", + ); + + assert!( + !OUTPUT_PROJECTION_SOURCE.contains("ProjectionSourceV1"), + "output_projection.rs must consume ModeledLcsOccurrenceV1 directly, not define ProjectionSourceV1", + ); } #[test] -fn program_session_module_docs_disclaim_transport_only_scope() { - let module_docs = PROGRAM_SESSION_SOURCE - .lines() - .take_while(|line| line.starts_with("//!")) - .collect::>() - .join("\n") - .to_ascii_lowercase(); +fn existing_encoded_evaluators_delegate_program_targets_without_parallel_formulae() { + for (path, source, evaluator, next_impl) in [ + ( + "constraints/exact.rs", + EXACT_CONSTRAINT_SOURCE, + "ExactSrgb8IdentityV1", + "impl HardClassifier for ExactSrgb8IdentityV1", + ), + ( + "constraints/wcag22.rs", + WCAG22_CONSTRAINT_SOURCE, + "Wcag22Srgb8V1", + "impl HardClassifier for Wcag22Srgb8V1", + ), + ] { + let start = format!("impl Evaluator for {evaluator} {{"); + let implementation = source_scope(source, &start, next_impl); + assert_eq!( + implementation + .matches(">::evaluate(") + .count(), + 1, + "{path} Program evaluator must delegate exactly once to its encoded-target SSOT", + ); + assert_eq!( + implementation.matches(".encoded(),").count(), + 1, + "{path} Program evaluator must pass only the typed encoded target to its SSOT", + ); + assert!( + !implementation.contains(".modeled_lcs()"), + "{path} encoded evaluator must not silently grow a contextual LCS formula", + ); + } +} - for required in ["transport-only", "encoded", "not", "lcs", "evidence"] { +#[test] +fn private_program_and_lcs_occurrence_types_are_not_publicly_exported() { + for required in [ + "pub(crate) mod lcs_occurrence;", + "pub(crate) mod program_session;", + ] { + assert!( + LIB_SOURCE.contains(required), + "the pre-hard-cut boundary must remain crate-private; missing `{required}`", + ); + } + for forbidden in [ + "pub mod lcs_occurrence;", + "pub mod program_session;", + "pub use lcs_occurrence::", + "pub use crate::lcs_occurrence::", + "pub use program_session::", + "pub use crate::program_session::", + ] { assert!( - module_docs.contains(required), - "program_session.rs module docs must explicitly disclaim transport-only scope; missing `{required}`" + !LIB_SOURCE.contains(forbidden), + "lib.rs must not expose private Program/LCS occurrence surface `{forbidden}`", ); } } diff --git a/crates/labcolors-core/src/joint.rs b/crates/labcolors-core/src/joint.rs index 7c25657d..bf1ed677 100644 --- a/crates/labcolors-core/src/joint.rs +++ b/crates/labcolors-core/src/joint.rs @@ -255,7 +255,10 @@ impl JointObservationV1 for RevisionBoundObservationV1 { } fn value_at_bound(&self, case_index: usize, bound_index: usize) -> Option { - self.physical_values(case_index)?.get(bound_index).copied() + self.physical_values(case_index)? + .get(bound_index) + .copied() + .map(crate::lcs_occurrence::ColorSignal::srgb8) } fn provenance(&self, case_index: usize) -> Option<&[ScenarioId]> { diff --git a/crates/labcolors-core/src/joint_tests.rs b/crates/labcolors-core/src/joint_tests.rs index aef33acc..e58051af 100644 --- a/crates/labcolors-core/src/joint_tests.rs +++ b/crates/labcolors-core/src/joint_tests.rs @@ -10,6 +10,7 @@ use crate::joint::{ PointwiseJointPointProgramV1, PointwiseJointReportErrorV1, PointwiseSelectedRecheckErrorV1, SelectionPolicyErrorV1, checked_joint_cardinality, }; +use crate::lcs_occurrence::ColorSignal; use crate::observation::{ ObservationHeadViewV1, ObservationOwnerV1, ObservationPayloadInput, ObservationStreamId, ObservationUpdateInput, ObservedScenarioSetInput, PreparedObservationUpdateV1, Revision, @@ -97,7 +98,7 @@ fn observation(revision: u64, cases: Vec<(u32, [u8; 3])>) -> RevisionBoundObserv id: ScenarioId::new(id), bindings: vec![SurfaceInputBinding { port: ROOT, - value: Srgb8::new(value), + value: ColorSignal::from_srgb8(Srgb8::new(value)), }], }) .collect(), @@ -133,11 +134,11 @@ fn observation_with_unrelated_surface( bindings: vec![ SurfaceInputBinding { port: ROOT, - value: Srgb8::new(root), + value: ColorSignal::from_srgb8(Srgb8::new(root)), }, SurfaceInputBinding { port: unrelated_surface, - value: Srgb8::new(unrelated), + value: ColorSignal::from_srgb8(Srgb8::new(unrelated)), }, ], }], diff --git a/crates/labcolors-core/src/lcs_occurrence.rs b/crates/labcolors-core/src/lcs_occurrence.rs index bc051d57..32eb9147 100644 --- a/crates/labcolors-core/src/lcs_occurrence.rs +++ b/crates/labcolors-core/src/lcs_occurrence.rs @@ -335,6 +335,15 @@ fn derive_sample_with_binding( TristimulusSample::try_from_registered_xyz(xyz, binding.result_frame()) } +#[cfg(test)] +thread_local! { + /// Per-thread count of modeled signal-to-tristimulus derivations. Program + /// regression tests use this deterministic metric to pin one derivation + /// per unique target occurrence and physical case without timing noise. + pub(crate) static MODELED_TRISTIMULUS_DERIVATION_CALLS: std::cell::Cell = + const { std::cell::Cell::new(0) }; +} + /// Derive one modeled tristimulus from an encoded sRGB8 point. /// /// Composition provenance remains the responsibility of the upstream @@ -343,6 +352,8 @@ fn derive_sample_with_binding( pub(crate) fn derive_modeled_tristimulus_v1( signal: ColorSignal, ) -> Result { + #[cfg(test)] + MODELED_TRISTIMULUS_DERIVATION_CALLS.with(|calls| calls.set(calls.get() + 1)); let binding = admitted_binding(signal.output_profile()); let sample = derive_sample_with_binding(signal, binding)?; Ok(ModeledTristimulusDerivationV1 { @@ -574,6 +585,111 @@ impl LcsOccurrence { } } +/// Failure while binding replayable modeled provenance to one context-bound +/// occurrence. Every mismatch is explicit; no convenient encoded signal may be +/// attached to unrelated XYZ/context identity. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ModeledLcsOccurrenceFormationErrorV1 { + Tristimulus(TristimulusDomainErrorV1), + Formation(OccurrenceFormationError), + ProvenanceReplayFailed(TristimulusDomainErrorV1), + RecordedSampleDoesNotReplay { + recorded: TristimulusSample, + replayed: TristimulusSample, + }, + OccurrenceSampleMismatch { + occurrence: TristimulusSample, + modeled: TristimulusSample, + }, +} + +/// One replayable modeled signal derivation bound to exactly one immutable +/// appearance context. +/// +/// The provenance is retained beside the LCS identity: a bare XYZ triple or a +/// derived appearance coordinate can never impersonate the encoded signal +/// which produced this occurrence. This value is still a deterministic model, +/// not evidence that a renderer or observer produced the stimulus. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ModeledLcsOccurrenceV1 { + derivation: ModeledTristimulusDerivationV1, + occurrence: LcsOccurrence, +} + +impl ModeledLcsOccurrenceV1 { + pub(crate) fn from_signal_in_context( + signal: ColorSignal, + context: AppearanceContextId, + ) -> Result { + let derivation = derive_modeled_tristimulus_v1(signal) + .map_err(ModeledLcsOccurrenceFormationErrorV1::Tristimulus)?; + let occurrence = LcsOccurrence::in_context(derivation.sample(), context) + .map_err(ModeledLcsOccurrenceFormationErrorV1::Formation)?; + // Both values are formed in this function from the same admitted + // sample. Replaying the transform here would derive sRGB -> XYZ twice + // on the Program hot path; replay remains mandatory for `bind`, whose + // two pre-existing inputs may be unrelated. + Ok(Self { + derivation, + occurrence, + }) + } + + pub(crate) fn bind( + occurrence: LcsOccurrence, + derivation: ModeledTristimulusDerivationV1, + ) -> Result { + let modeled = Self { + derivation, + occurrence, + }; + modeled.verify()?; + Ok(modeled) + } + + pub(crate) fn verify(self) -> Result<(), ModeledLcsOccurrenceFormationErrorV1> { + let replayed = self + .derivation + .replay() + .map_err(ModeledLcsOccurrenceFormationErrorV1::ProvenanceReplayFailed)?; + let recorded = self.derivation.sample(); + if replayed != recorded { + return Err( + ModeledLcsOccurrenceFormationErrorV1::RecordedSampleDoesNotReplay { + recorded, + replayed, + }, + ); + } + let occurrence = self.occurrence.sample(); + if occurrence != recorded { + return Err( + ModeledLcsOccurrenceFormationErrorV1::OccurrenceSampleMismatch { + occurrence, + modeled: recorded, + }, + ); + } + Ok(()) + } + + pub(crate) const fn derivation(self) -> ModeledTristimulusDerivationV1 { + self.derivation + } + + pub(crate) const fn occurrence(self) -> LcsOccurrence { + self.occurrence + } + + pub(crate) const fn provenance(self) -> ModeledTristimulusProvenanceV1 { + self.derivation.provenance() + } + + pub(crate) const fn signal(self) -> ColorSignal { + self.provenance().source_signal() + } +} + /// Formula and operation-order release of the rectangular Oklab view. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum OklabViewReleaseId { @@ -774,7 +890,9 @@ fn view_numeric_error( } } -fn derive_oklab_view_v1(xyz: [f64; 3]) -> Result { +pub(crate) fn derive_oklab_view_v1( + xyz: [f64; 3], +) -> Result { let [l, a, b] = xyz_d65_to_oklab_v1(xyz); let release = AppearanceViewReleaseIdV1::Oklab(OKLAB_VIEW_RELEASE_V1); Ok(OklabViewV1 { diff --git a/crates/labcolors-core/src/lib.rs b/crates/labcolors-core/src/lib.rs index 14c9df24..a437281e 100644 --- a/crates/labcolors-core/src/lib.rs +++ b/crates/labcolors-core/src/lib.rs @@ -92,6 +92,9 @@ mod output_projection_tests; #[cfg(test)] mod program_session_tests; +#[cfg(test)] +mod program_lcs_integration_tests; + #[cfg(test)] mod release_registry_tests; diff --git a/crates/labcolors-core/src/observation.rs b/crates/labcolors-core/src/observation.rs index 6f2b9c86..eca73378 100644 --- a/crates/labcolors-core/src/observation.rs +++ b/crates/labcolors-core/src/observation.rs @@ -7,8 +7,8 @@ use core::ops::Range; use std::rc::Rc; -use crate::Srgb8; use crate::appearance::SurfaceInputPortId; +use crate::lcs_occurrence::ColorSignal; /// Stable compile-time identity of one atomic observation boundary. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -64,11 +64,11 @@ impl UnknownReasonId { #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub(crate) struct SurfaceInputBinding { pub(crate) port: SurfaceInputPortId, - pub(crate) value: Srgb8, + pub(crate) value: ColorSignal, } impl SurfaceInputBinding { - pub(crate) const fn new(port: SurfaceInputPortId, value: Srgb8) -> Self { + pub(crate) const fn new(port: SurfaceInputPortId, value: ColorSignal) -> Self { Self { port, value } } } @@ -142,12 +142,12 @@ impl ObservationSchemaMismatchV1 { #[derive(Debug, PartialEq, Eq)] struct ObservedScenarioSet { cases: Box<[PhysicalScenario]>, - values: Box<[Srgb8]>, + values: Box<[ColorSignal]>, provenance: Box<[ScenarioId]>, } impl ObservedScenarioSet { - fn values(&self, case_index: usize) -> Option<&[Srgb8]> { + fn values(&self, case_index: usize) -> Option<&[ColorSignal]> { let values = &self.cases.get(case_index)?.values; self.values.get(values.start..values.end) } @@ -209,7 +209,7 @@ impl RevisionBoundObservationV1 { self.backing.set.cases.len() } - pub(crate) fn physical_values(&self, case_index: usize) -> Option<&[Srgb8]> { + pub(crate) fn physical_values(&self, case_index: usize) -> Option<&[ColorSignal]> { self.backing.set.values(case_index) } diff --git a/crates/labcolors-core/src/observation_tests.rs b/crates/labcolors-core/src/observation_tests.rs index 01a4495d..1aea99f0 100644 --- a/crates/labcolors-core/src/observation_tests.rs +++ b/crates/labcolors-core/src/observation_tests.rs @@ -4,6 +4,7 @@ use crate::appearance::{ OccurrenceId, OccurrenceSpec, PaintId, PaintSpec, PointOpacityError, PointOpacityOverSurfaceV1, ResolvedOccurrence, SurfaceId, SurfaceInputPortId, SurfaceSpec, }; +use crate::lcs_occurrence::ColorSignal; use crate::observation::{ CanonicalObservationSchemaV1, ObservationError, ObservationHeadViewV1, ObservationOwnerV1, ObservationPayloadInput, ObservationStreamId, ObservationUpdateInput, ObservedScenarioSetInput, @@ -107,7 +108,7 @@ impl TestState { } fn binding(port: SurfaceInputPortId, bytes: [u8; 3]) -> SurfaceInputBinding { - SurfaceInputBinding::new(port, Srgb8::new(bytes)) + SurfaceInputBinding::new(port, ColorSignal::from_srgb8(Srgb8::new(bytes))) } fn scenario(id: u32, bindings: impl IntoIterator) -> ScenarioInput { @@ -234,6 +235,7 @@ fn admission_preserves_correlated_tuples_without_cartesian_product() { .unwrap() .iter() .copied() + .map(ColorSignal::srgb8) .map(Srgb8::bytes) .collect() }) @@ -272,14 +274,20 @@ fn canonicalization_ignores_declaration_order_and_groups_duplicate_physics() { &[ScenarioId::new(3), ScenarioId::new(9)] ); assert_eq!(observation.provenance(1).unwrap(), &[ScenarioId::new(4)]); - let physical_values: Vec> = (0..observation.physical_case_count()) + let physical_values: Vec> = (0..observation.physical_case_count()) .map(|case_index| observation.physical_values(case_index).unwrap().to_vec()) .collect(); assert_eq!( physical_values, vec![ - vec![Srgb8::new([1, 2, 3]), Srgb8::new([4, 5, 6])], - vec![Srgb8::new([9, 8, 7]), Srgb8::new([6, 5, 4])], + vec![ + ColorSignal::from_srgb8(Srgb8::new([1, 2, 3])), + ColorSignal::from_srgb8(Srgb8::new([4, 5, 6])), + ], + vec![ + ColorSignal::from_srgb8(Srgb8::new([9, 8, 7])), + ColorSignal::from_srgb8(Srgb8::new([6, 5, 4])), + ], ] ); } @@ -350,16 +358,22 @@ fn schema_and_values_are_aligned_once_while_provenance_remains_complete() { .unwrap(); let observation = revision_bound(&state); - let first_values: &[Srgb8] = observation.physical_values(0).unwrap(); - let second_values: &[Srgb8] = observation.physical_values(1).unwrap(); + let first_values: &[ColorSignal] = observation.physical_values(0).unwrap(); + let second_values: &[ColorSignal] = observation.physical_values(1).unwrap(); assert_eq!(observation.schema(), &[PORT_A, PORT_B]); assert_eq!( first_values, - &[Srgb8::new([1, 2, 3]), Srgb8::new([4, 5, 6])] + &[ + ColorSignal::from_srgb8(Srgb8::new([1, 2, 3])), + ColorSignal::from_srgb8(Srgb8::new([4, 5, 6])), + ] ); assert_eq!( second_values, - &[Srgb8::new([9, 8, 7]), Srgb8::new([6, 5, 4])] + &[ + ColorSignal::from_srgb8(Srgb8::new([9, 8, 7])), + ColorSignal::from_srgb8(Srgb8::new([6, 5, 4])), + ] ); assert_eq!( observation.provenance(0), @@ -393,11 +407,11 @@ fn keyed_schema_is_intrinsic_to_revision_bound_observation_identity() { assert_eq!(revision_bound(&right).schema(), &[PORT_B]); assert_eq!( revision_bound(&left).physical_values(0), - Some(&[Srgb8::new([7, 8, 9])][..]) + Some(&[ColorSignal::from_srgb8(Srgb8::new([7, 8, 9]))][..]) ); assert_eq!( revision_bound(&right).physical_values(0), - Some(&[Srgb8::new([7, 8, 9])][..]) + Some(&[ColorSignal::from_srgb8(Srgb8::new([7, 8, 9]))][..]) ); let alternate_schema = canonicalize_observation_schema(vec![PORT_B]).unwrap(); @@ -539,7 +553,10 @@ fn observed_to_unknown_replaces_raw_payload_instead_of_duplicating_it() { assert!(matches!(state.head(), ObservationHeadViewV1::Unknown(_))); assert_eq!(old_revision, Revision::new(1)); assert_eq!(old_schema, vec![PORT_A]); - assert_eq!(old_values, vec![Srgb8::new([3, 4, 5])]); + assert_eq!( + old_values, + vec![ColorSignal::from_srgb8(Srgb8::new([3, 4, 5]))] + ); } #[test] diff --git a/crates/labcolors-core/src/output_projection.rs b/crates/labcolors-core/src/output_projection.rs index 58e6c5a4..e0bf949f 100644 --- a/crates/labcolors-core/src/output_projection.rs +++ b/crates/labcolors-core/src/output_projection.rs @@ -16,11 +16,10 @@ //! slice. use crate::lcs_occurrence::{ - ColorSignal, HueAngle, HueState, LcsOccurrence, ModeledTristimulusDerivationV1, - ModeledTristimulusProvenanceV1, NumericDomainError, OKLAB_VIEW_RELEASE_V1, OklabViewReleaseId, - TristimulusDomainErrorV1, TristimulusSample, + AppearanceStateDerivationErrorV1, ColorSignal, HueAngle, HueState, LcsOccurrence, + ModeledLcsOccurrenceFormationErrorV1, ModeledLcsOccurrenceV1, ModeledTristimulusProvenanceV1, + NumericDomainError, OKLAB_VIEW_RELEASE_V1, OklabViewReleaseId, derive_oklab_view_v1, }; -use crate::spaces::oklab::xyz_d65_to_oklab_v1; /// Formula, policy and operation-order identity of an output projection. /// @@ -233,101 +232,22 @@ pub(crate) enum OutputGamutTreatmentV1 { NoExplicitProjectionGamutMapV1, } -/// A modeled derivation and the occurrence which claims that exact sample. -/// -/// Construction is sealed so a caller cannot attach convenient source bytes to -/// an unrelated XYZ occurrence. Context is retained inside the occurrence and -/// therefore remains part of certificate identity even though this output edge -/// uses only the occurrence's stimulus coordinates. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) struct ProjectionSourceV1 { - occurrence: LcsOccurrence, - modeled: ModeledTristimulusDerivationV1, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum ProjectionSourceFormationErrorV1 { - ProvenanceReplayFailed(TristimulusDomainErrorV1), - RecordedSampleDoesNotReplay { - recorded: TristimulusSample, - replayed: TristimulusSample, - }, - OccurrenceSampleMismatch { - occurrence: TristimulusSample, - modeled: TristimulusSample, - }, -} - -impl ProjectionSourceV1 { - pub(crate) fn bind( - occurrence: LcsOccurrence, - modeled: ModeledTristimulusDerivationV1, - ) -> Result { - let source = Self { - occurrence, - modeled, - }; - source.verify()?; - Ok(source) - } - - fn verify(self) -> Result<(), ProjectionSourceFormationErrorV1> { - let replayed = self - .modeled - .replay() - .map_err(ProjectionSourceFormationErrorV1::ProvenanceReplayFailed)?; - let recorded = self.modeled.sample(); - if replayed != recorded { - return Err( - ProjectionSourceFormationErrorV1::RecordedSampleDoesNotReplay { - recorded, - replayed, - }, - ); - } - let occurrence = self.occurrence.sample(); - if occurrence != recorded { - return Err(ProjectionSourceFormationErrorV1::OccurrenceSampleMismatch { - occurrence, - modeled: recorded, - }); - } - Ok(()) - } - - pub(crate) const fn occurrence(self) -> LcsOccurrence { - self.occurrence - } - - pub(crate) const fn modeled(self) -> ModeledTristimulusDerivationV1 { - self.modeled - } - - pub(crate) const fn provenance(self) -> ModeledTristimulusProvenanceV1 { - self.modeled.provenance() - } - - pub(crate) const fn signal(self) -> ColorSignal { - self.provenance().source_signal() - } -} - /// The release choice is made before any coordinates or output bytes exist. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) struct OutputProjectionRequestV1 { - source: ProjectionSourceV1, + source: ModeledLcsOccurrenceV1, release: OutputProjectionReleaseIdV1, } impl OutputProjectionRequestV1 { pub(crate) const fn new( - source: ProjectionSourceV1, + source: ModeledLcsOccurrenceV1, release: OutputProjectionReleaseIdV1, ) -> Self { Self { source, release } } - pub(crate) const fn source(self) -> ProjectionSourceV1 { + pub(crate) const fn source(self) -> ModeledLcsOccurrenceV1 { self.source } @@ -360,7 +280,8 @@ pub(crate) enum OutputProjectionNumericErrorV1 { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum OutputProjectionErrorV1 { - Source(ProjectionSourceFormationErrorV1), + Source(ModeledLcsOccurrenceFormationErrorV1), + OklabView(AppearanceStateDerivationErrorV1), OklchView(OklchViewDerivationErrorV1), Numeric { release: OutputProjectionReleaseIdV1, @@ -377,7 +298,7 @@ pub(crate) enum OutputProjectionErrorV1 { /// All inputs and versioned policies needed to replay one projection. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) struct OutputProjectionCertificateV1 { - source: ProjectionSourceV1, + source: ModeledLcsOccurrenceV1, release: OutputProjectionReleaseIdV1, oklab_release: OklabViewReleaseId, oklch_release: OklchViewReleaseId, @@ -387,7 +308,7 @@ pub(crate) struct OutputProjectionCertificateV1 { } impl OutputProjectionCertificateV1 { - pub(crate) const fn source(self) -> ProjectionSourceV1 { + pub(crate) const fn source(self) -> ModeledLcsOccurrenceV1 { self.source } @@ -468,7 +389,7 @@ fn numeric_error( } fn project_css_color4_oklch_d65_from_modeled_srgb8_solid_v1( - source: ProjectionSourceV1, + source: ModeledLcsOccurrenceV1, release: OutputProjectionReleaseIdV1, ) -> Result { source.verify().map_err(OutputProjectionErrorV1::Source)?; @@ -476,9 +397,11 @@ fn project_css_color4_oklch_d65_from_modeled_srgb8_solid_v1( // The occurrence sample, not the encoded source channels, owns appearance // geometry. Provenance is used only to prove that this narrow output // release may serialize the occurrence under its registered policies. - let oklab = xyz_d65_to_oklab_v1(source.occurrence().sample().xyz()); + let oklab = derive_oklab_view_v1(source.occurrence().sample().xyz()) + .map_err(OutputProjectionErrorV1::OklabView)?; let source_srgb8 = source.signal().srgb8(); - let oklch_view = derive_oklch_view_v1(oklab).map_err(OutputProjectionErrorV1::OklchView)?; + let oklch_view = derive_oklch_view_v1([oklab.l(), oklab.a(), oklab.b()]) + .map_err(OutputProjectionErrorV1::OklchView)?; let l = oklch_view.l(); if !(0.0..=1.0).contains(&l) { return Err(numeric_error( diff --git a/crates/labcolors-core/src/output_projection_tests.rs b/crates/labcolors-core/src/output_projection_tests.rs index b3e19d1a..0beeeb4d 100644 --- a/crates/labcolors-core/src/output_projection_tests.rs +++ b/crates/labcolors-core/src/output_projection_tests.rs @@ -4,15 +4,14 @@ use crate::Srgb8; use crate::lcs_occurrence::{ AdaptingLuminanceCdM2, AppearanceContextId, AppearanceContextSchemaReleaseId, BackgroundLuminanceRatio, ColorSignal, IEC_SRGB_D65_XYZ_FRAME_V1, LcsOccurrence, - ModeledTristimulusDerivationV1, OKLAB_VIEW_RELEASE_V1, SurroundProfileId, - derive_modeled_tristimulus_v1, + ModeledLcsOccurrenceFormationErrorV1, ModeledLcsOccurrenceV1, ModeledTristimulusDerivationV1, + OKLAB_VIEW_RELEASE_V1, SurroundProfileId, derive_modeled_tristimulus_v1, }; use crate::output_projection::{ CSS_COLOR_4_OKLCH_D65_FROM_MODELED_SRGB8_SOLID_V1, CssOklchHueSerializationReleaseIdV1, CssOklchNumberEncodingReleaseIdV1, DifferenceCalibrationReleaseIdV1, OKLCH_VIEW_RELEASE_V1, OutputGamutTreatmentV1, OutputProjectionErrorV1, OutputProjectionReleaseIdV1, - OutputProjectionRequestV1, OutputProjectionV1, ProjectionSourceFormationErrorV1, - ProjectionSourceV1, project_output_v1, + OutputProjectionRequestV1, OutputProjectionV1, project_output_v1, }; use crate::spaces::oklab::oklab_to_srgb_linear; use crate::spaces::srgb::srgb8_from_linear; @@ -31,11 +30,11 @@ fn modeled(bytes: [u8; 3]) -> ModeledTristimulusDerivationV1 { derive_modeled_tristimulus_v1(ColorSignal::from_srgb8(Srgb8::new(bytes))).unwrap() } -fn source(bytes: [u8; 3], adapting_luminance_cd_m2: f64) -> ProjectionSourceV1 { +fn source(bytes: [u8; 3], adapting_luminance_cd_m2: f64) -> ModeledLcsOccurrenceV1 { let modeled = modeled(bytes); let occurrence = LcsOccurrence::in_context(modeled.sample(), context(adapting_luminance_cd_m2)).unwrap(); - ProjectionSourceV1::bind(occurrence, modeled).unwrap() + ModeledLcsOccurrenceV1::bind(occurrence, modeled).unwrap() } fn project(bytes: [u8; 3], adapting_luminance_cd_m2: f64) -> OutputProjectionV1 { @@ -84,11 +83,13 @@ fn source_binding_rejects_bytes_from_an_unrelated_modeled_derivation() { let occurrence = LcsOccurrence::in_context(occurrence_model.sample(), context(64.0)).unwrap(); assert_eq!( - ProjectionSourceV1::bind(occurrence, unrelated_model), - Err(ProjectionSourceFormationErrorV1::OccurrenceSampleMismatch { - occurrence: occurrence_model.sample(), - modeled: unrelated_model.sample(), - }), + ModeledLcsOccurrenceV1::bind(occurrence, unrelated_model), + Err( + ModeledLcsOccurrenceFormationErrorV1::OccurrenceSampleMismatch { + occurrence: occurrence_model.sample(), + modeled: unrelated_model.sample(), + } + ), ); } @@ -126,7 +127,7 @@ fn certificate_replays_source_view_formula_and_exact_css_bytes() { ); assert_eq!( certificate.source_provenance(), - certificate.source().modeled().provenance(), + certificate.source().derivation().provenance(), ); } diff --git a/crates/labcolors-core/src/point_support.rs b/crates/labcolors-core/src/point_support.rs index d36106b9..bb997588 100644 --- a/crates/labcolors-core/src/point_support.rs +++ b/crates/labcolors-core/src/point_support.rs @@ -836,7 +836,7 @@ fn evaluate_bound_point_support( let physical = PointOpacityOverSurfaceV1::evaluate_admitted( requirement.paint.source().bytes(), requirement.paint.opacity(), - backdrop.bytes(), + backdrop.srgb8().bytes(), ); let exact = match requirement.exact_invocation { diff --git a/crates/labcolors-core/src/point_support_tests.rs b/crates/labcolors-core/src/point_support_tests.rs index 63b1b030..83b39fda 100644 --- a/crates/labcolors-core/src/point_support_tests.rs +++ b/crates/labcolors-core/src/point_support_tests.rs @@ -3,6 +3,7 @@ use crate::appearance::{ EncodedPointPaintV1, OccurrenceId, PaintId, PhysicalProgramIdentityV1, SurfaceInputPortId, }; use crate::composition::{AdmittedOpacityV1, CompositionProfileV1}; +use crate::lcs_occurrence::ColorSignal; use crate::observation::{ ObservationPayloadInput, ObservationStreamId, ObservationUpdateInput, ObservedScenarioSetInput, Revision, ScenarioId, ScenarioInput, SurfaceInputBinding, @@ -73,7 +74,12 @@ fn observed_update( id: ScenarioId::new(id), bindings: bindings .into_iter() - .map(|(port, value)| SurfaceInputBinding::new(port, Srgb8::new(value))) + .map(|(port, value)| { + SurfaceInputBinding::new( + port, + ColorSignal::from_srgb8(Srgb8::new(value)), + ) + }) .collect(), }) .collect(), @@ -188,11 +194,21 @@ fn duplicate_raw_scenarios_share_one_physical_case_without_cartesian_expansion() assert_eq!(report.observation().physical_case_count(), 2); assert_eq!( report.observation().physical_values(0), - Some(&[Srgb8::new([0; 3]), Srgb8::new([255; 3])][..]) + Some( + &[ + ColorSignal::from_srgb8(Srgb8::new([0; 3])), + ColorSignal::from_srgb8(Srgb8::new([255; 3])), + ][..] + ) ); assert_eq!( report.observation().physical_values(1), - Some(&[Srgb8::new([255; 3]), Srgb8::new([0; 3])][..]) + Some( + &[ + ColorSignal::from_srgb8(Srgb8::new([255; 3])), + ColorSignal::from_srgb8(Srgb8::new([0; 3])), + ][..] + ) ); assert_eq!( report.observation().provenance(0), diff --git a/crates/labcolors-core/src/program_lcs_integration_tests.rs b/crates/labcolors-core/src/program_lcs_integration_tests.rs new file mode 100644 index 00000000..3ac51463 --- /dev/null +++ b/crates/labcolors-core/src/program_lcs_integration_tests.rs @@ -0,0 +1,588 @@ +use crate::Srgb8; +use crate::appearance::{ + ColorInputId, OccurrenceId, OpacityInputId, PaintId, PointOpacityOverSurfaceV1, SurfaceId, + SurfaceInputPortId, +}; +use crate::constraints::{ + ExactSrgb8IdentityV1, ProgramPointAssessmentErrorV1, ProgramVisiblePointBindingV1, + Wcag22Srgb8V1, assess_program_point_hard, +}; +use crate::lcs_occurrence::{ + AdaptingLuminanceCdM2, AppearanceContextId, AppearanceContextSchemaReleaseId, AppearanceState, + BackgroundLuminanceRatio, ColorSignal, HueState, IEC_SRGB_D65_XYZ_FRAME_V1, + MODELED_TRISTIMULUS_DERIVATION_CALLS, ModeledLcsOccurrenceV1, SurroundProfileId, +}; +use crate::observation::{ + ObservationGroupId, ObservationPayloadInput, ObservationStreamId, ObservationUpdateInput, + ObservedScenarioSetInput, Revision, ScenarioId, ScenarioInput, SurfaceInputBinding, +}; +use crate::program_session::{ + ColorInput, CompiledProgram, CompositionProfile, ConstraintId, ConstraintInvocation, + ConstraintSet, ObservationGroup, Occurrence, OpacityInput, OutputBinding, OutputSlotId, Paint, + Program, ProgramConstraintResultV1, Surface, +}; +use crate::session::SessionState; +use crate::spaces::cam16::FORWARD_CALLS; +use crate::wcag22::{Wcag22ApplicableDecisionV1, Wcag22CriterionV1}; + +const BLACK_INPUT: ColorInputId = ColorInputId::new(1); +const WHITE_INPUT: ColorInputId = ColorInputId::new(2); +const SURFACE_PORT: SurfaceInputPortId = SurfaceInputPortId::new(3); +const HALF: OpacityInputId = OpacityInputId::new(4); + +const BLACK_SOLID: PaintId = PaintId::new(10); +const TRANSLUCENT_BLACK: PaintId = PaintId::new(11); +const WHITE_SOLID: PaintId = PaintId::new(12); +const BACKDROP: SurfaceId = SurfaceId::new(20); + +const AVERAGE_OCCURRENCE: OccurrenceId = OccurrenceId::new(30); +const DIM_OCCURRENCE: OccurrenceId = OccurrenceId::new(31); +const AVERAGE_CONSTRAINT: ConstraintId = ConstraintId::new(40); +const DIM_CONSTRAINT: ConstraintId = ConstraintId::new(41); + +const BLACK_OUTPUT: OutputSlotId = OutputSlotId::new(50); +const WHITE_OUTPUT: OutputSlotId = OutputSlotId::new(51); +const GROUP: ObservationGroupId = ObservationGroupId::new(60); +const STREAM_A: ObservationStreamId = ObservationStreamId::new(70); +const STREAM_B: ObservationStreamId = ObservationStreamId::new(71); + +fn signal(bytes: [u8; 3]) -> ColorSignal { + ColorSignal::from_srgb8(Srgb8::new(bytes)) +} + +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, + ) +} + +fn observed_white(stream: ObservationStreamId) -> ObservationUpdateInput { + ObservationUpdateInput { + stream, + revision: Revision::new(1), + payload: ObservationPayloadInput::Scenarios(ObservedScenarioSetInput { + scenarios: vec![ScenarioInput { + id: ScenarioId::new(1), + bindings: vec![SurfaceInputBinding::new(SURFACE_PORT, signal([0xFF; 3]))], + }], + }), + } +} + +fn observed_two_backdrops(stream: ObservationStreamId) -> ObservationUpdateInput { + ObservationUpdateInput { + stream, + revision: Revision::new(1), + payload: ObservationPayloadInput::Scenarios(ObservedScenarioSetInput { + scenarios: vec![ + ScenarioInput { + id: ScenarioId::new(1), + bindings: vec![SurfaceInputBinding::new(SURFACE_PORT, signal([0xFF; 3]))], + }, + ScenarioInput { + id: ScenarioId::new(2), + bindings: vec![SurfaceInputBinding::new(SURFACE_PORT, signal([0x80; 3]))], + }, + ], + }), + } +} + +fn compiled_program( + declarations: &[(OccurrenceId, ConstraintId, AppearanceContextId)], + expected_visible: Srgb8, + opacity: f64, + permuted: bool, +) -> CompiledProgram { + let mut colors = vec![ + ColorInput::new(BLACK_INPUT, signal([0; 3])), + ColorInput::new(WHITE_INPUT, signal([0xFF; 3])), + ]; + let mut paints = vec![ + Paint::Solid { + id: BLACK_SOLID, + color: BLACK_INPUT, + }, + Paint::Opacity { + id: TRANSLUCENT_BLACK, + source: BLACK_SOLID, + opacity: HALF, + }, + Paint::Solid { + id: WHITE_SOLID, + color: WHITE_INPUT, + }, + ]; + let mut occurrences = declarations + .iter() + .map(|(occurrence, _, occurrence_context)| { + Occurrence::new( + *occurrence, + TRANSLUCENT_BLACK, + BACKDROP, + CompositionProfile::EncodedSrgb8SourceOverV1, + *occurrence_context, + ) + }) + .collect::>(); + let mut hard = declarations + .iter() + .map(|(occurrence, constraint, _)| { + ConstraintInvocation::hard(*constraint, *occurrence, expected_visible) + }) + .collect::>(); + let mut outputs = vec![ + OutputBinding::new(BLACK_OUTPUT, TRANSLUCENT_BLACK), + OutputBinding::new(WHITE_OUTPUT, WHITE_SOLID), + ]; + + if permuted { + colors.reverse(); + paints.reverse(); + occurrences.reverse(); + hard.reverse(); + outputs.reverse(); + } + + Program::new( + colors, + ObservationGroup::new(GROUP, vec![SURFACE_PORT]), + vec![OpacityInput::new(HALF, opacity)], + paints, + vec![Surface::Input { + id: BACKDROP, + input: SURFACE_PORT, + }], + occurrences, + ConstraintSet::new(hard, vec![]), + outputs, + ExactSrgb8IdentityV1, + ) + .compile() + .unwrap() +} + +fn compiled_wcag_program(opacity: f64) -> CompiledProgram { + Program::new( + vec![ + ColorInput::new(BLACK_INPUT, signal([0; 3])), + ColorInput::new(WHITE_INPUT, signal([0xFF; 3])), + ], + ObservationGroup::new(GROUP, vec![SURFACE_PORT]), + vec![OpacityInput::new(HALF, opacity)], + vec![ + Paint::Solid { + id: BLACK_SOLID, + color: BLACK_INPUT, + }, + Paint::Opacity { + id: TRANSLUCENT_BLACK, + source: BLACK_SOLID, + opacity: HALF, + }, + Paint::Solid { + id: WHITE_SOLID, + color: WHITE_INPUT, + }, + ], + vec![Surface::Input { + id: BACKDROP, + input: SURFACE_PORT, + }], + vec![Occurrence::new( + AVERAGE_OCCURRENCE, + TRANSLUCENT_BLACK, + BACKDROP, + CompositionProfile::EncodedSrgb8SourceOverV1, + context(SurroundProfileId::AverageV1), + )], + ConstraintSet::new( + vec![ConstraintInvocation::hard( + AVERAGE_CONSTRAINT, + AVERAGE_OCCURRENCE, + Wcag22CriterionV1::Sc143TextDefault, + )], + vec![], + ), + vec![OutputBinding::new(BLACK_OUTPUT, TRANSLUCENT_BLACK)], + Wcag22Srgb8V1, + ) + .compile() + .unwrap() +} + +fn compiled_duplicate_constraint_program() -> CompiledProgram { + Program::new( + vec![ColorInput::new(BLACK_INPUT, signal([0; 3]))], + ObservationGroup::new(GROUP, vec![SURFACE_PORT]), + vec![OpacityInput::new(HALF, 0.5)], + vec![ + Paint::Solid { + id: BLACK_SOLID, + color: BLACK_INPUT, + }, + Paint::Opacity { + id: TRANSLUCENT_BLACK, + source: BLACK_SOLID, + opacity: HALF, + }, + ], + vec![Surface::Input { + id: BACKDROP, + input: SURFACE_PORT, + }], + vec![Occurrence::new( + AVERAGE_OCCURRENCE, + TRANSLUCENT_BLACK, + BACKDROP, + CompositionProfile::EncodedSrgb8SourceOverV1, + context(SurroundProfileId::AverageV1), + )], + ConstraintSet::new( + vec![], + vec![ + ConstraintInvocation::report_only( + AVERAGE_CONSTRAINT, + AVERAGE_OCCURRENCE, + Srgb8::new([0x80; 3]), + ), + ConstraintInvocation::report_only( + DIM_CONSTRAINT, + AVERAGE_OCCURRENCE, + Srgb8::new([0x80; 3]), + ), + ], + ), + vec![OutputBinding::new(BLACK_OUTPUT, TRANSLUCENT_BLACK)], + ExactSrgb8IdentityV1, + ) + .compile() + .unwrap() +} + +fn assert_binding_matches_modeled( + binding: ProgramVisiblePointBindingV1, + modeled: ModeledLcsOccurrenceV1, +) { + assert_eq!(binding.modeled_lcs(), modeled); + assert_eq!( + binding.physical().occurrence().output_rgb(), + modeled.signal().srgb8().bytes(), + ); +} + +#[test] +fn ready_cell_binds_the_actual_visible_signal_to_the_exact_authored_context() { + let average = context(SurroundProfileId::AverageV1); + let compiled = compiled_program( + &[(AVERAGE_OCCURRENCE, AVERAGE_CONSTRAINT, average)], + Srgb8::new([0x80; 3]), + 0.5, + false, + ); + let mut session = compiled.instantiate(STREAM_A).unwrap(); + let state = session.update(observed_white(STREAM_A)).unwrap(); + let SessionState::Ready { current } = state else { + panic!("the exact visible composite must pass"); + }; + let [cell] = current.report().cells() else { + panic!("one physical case times one constraint must produce one cell"); + }; + + let modeled = cell.modeled_lcs_occurrence(); + assert_eq!( + modeled.derivation().provenance().source_signal(), + signal([0x80; 3]), + "provenance must name the visible source-over result, not the Paint source or backdrop", + ); + assert_eq!(modeled.occurrence().context(), average); + let ProgramConstraintResultV1::Pass(evidence) = cell.result() else { + panic!("exact equality must retain typed pass evidence"); + }; + assert_binding_matches_modeled(*evidence.binding(), modeled); + assert_eq!(*evidence.measurement().value(), Srgb8::new([0x80; 3])); + assert_eq!(*evidence.invocation(), Srgb8::new([0x80; 3])); +} + +#[test] +fn identical_physical_bytes_share_tristimulus_but_contextual_cam16_views_diverge() { + let average = context(SurroundProfileId::AverageV1); + let dim = context(SurroundProfileId::DimV1); + let compiled = compiled_program( + &[ + (AVERAGE_OCCURRENCE, AVERAGE_CONSTRAINT, average), + (DIM_OCCURRENCE, DIM_CONSTRAINT, dim), + ], + Srgb8::new([0x80; 3]), + 0.5, + false, + ); + let mut session = compiled.instantiate(STREAM_A).unwrap(); + let state = session.update(observed_white(STREAM_A)).unwrap(); + let SessionState::Ready { current } = state else { + panic!("both context-bound declarations assess the same passing physical point"); + }; + let [average_cell, dim_cell] = current.report().cells() else { + panic!("one case times two constraints must produce two canonical cells"); + }; + let average_modeled = average_cell.modeled_lcs_occurrence(); + let dim_modeled = dim_cell.modeled_lcs_occurrence(); + + assert_eq!( + average_modeled.derivation().provenance().source_signal(), + dim_modeled.derivation().provenance().source_signal(), + ); + assert_eq!( + average_modeled.derivation().sample(), + dim_modeled.derivation().sample(), + ); + assert_eq!(average_modeled.occurrence().context(), average); + assert_eq!(dim_modeled.occurrence().context(), dim); + assert_ne!(average_modeled.occurrence(), dim_modeled.occurrence()); + + let average_state = AppearanceState::derive_v1(average_modeled.occurrence()).unwrap(); + let dim_state = AppearanceState::derive_v1(dim_modeled.occurrence()).unwrap(); + assert_eq!(average_state.oklab(), dim_state.oklab()); + assert_ne!( + average_state.cam16().j().to_bits(), + dim_state.cam16().j().to_bits(), + "CAM16 must be evaluated under each occurrence's own context", + ); +} + +#[test] +fn exact_black_visible_occurrence_keeps_undefined_hue_in_lcs() { + let average = context(SurroundProfileId::AverageV1); + let compiled = compiled_program( + &[(AVERAGE_OCCURRENCE, AVERAGE_CONSTRAINT, average)], + Srgb8::new([0x00; 3]), + 1.0, + false, + ); + let mut session = compiled.instantiate(STREAM_A).unwrap(); + let state = session.update(observed_white(STREAM_A)).unwrap(); + let SessionState::Ready { current } = state else { + panic!("fixture must verify"); + }; + let [cell] = current.report().cells() else { + panic!("the complete report must retain its sole visible occurrence"); + }; + let modeled = cell.modeled_lcs_occurrence(); + assert_eq!(modeled.signal(), signal([0x00; 3])); + let state = AppearanceState::derive_v1(modeled.occurrence()).unwrap(); + assert_eq!(state.cam16().hue(), HueState::UndefinedExact); +} + +#[test] +fn hard_violation_retains_modeled_lcs_and_commits_no_current_outputs() { + let average = context(SurroundProfileId::AverageV1); + let compiled = compiled_program( + &[(AVERAGE_OCCURRENCE, AVERAGE_CONSTRAINT, average)], + Srgb8::new([0x7F; 3]), + 0.5, + false, + ); + let mut session = compiled.instantiate(STREAM_A).unwrap(); + let state = session.update(observed_white(STREAM_A)).unwrap(); + let SessionState::Failed { cause, previous } = state else { + panic!("the mandatory exact constraint must fail"); + }; + assert!( + previous.is_none(), + "a fresh hard failure must not expose any previous Ready outputs", + ); + let [cell] = cause.report().cells() else { + panic!("the complete failed report must retain its sole cell"); + }; + assert!(cell.result().is_violation()); + let modeled = cell.modeled_lcs_occurrence(); + assert_eq!( + modeled.derivation().provenance().source_signal(), + signal([0x80; 3]), + ); + assert_eq!(modeled.occurrence().context(), average); + let ProgramConstraintResultV1::Violation(evidence) = cell.result() else { + panic!("exact mismatch must retain typed violation evidence"); + }; + assert_binding_matches_modeled(*evidence.binding(), modeled); + assert_eq!(*evidence.measurement().value(), Srgb8::new([0x80; 3])); + assert_eq!(*evidence.invocation(), Srgb8::new([0x7F; 3])); + // `ProgramViolationV1` intentionally exposes only `report()`: current + // outputs exist exclusively on the `ProgramVerifiedV1` Ready branch. +} + +#[test] +fn program_wcag_pass_binds_physical_and_modeled_occurrence_to_one_evidence() { + let compiled = compiled_wcag_program(1.0); + let mut session = compiled.instantiate(STREAM_A).unwrap(); + let state = session.update(observed_white(STREAM_A)).unwrap(); + let SessionState::Ready { current } = state else { + panic!("opaque black on white must pass WCAG default text"); + }; + let [cell] = current.report().cells() else { + panic!("one WCAG declaration must produce one report cell"); + }; + let ProgramConstraintResultV1::Pass(evidence) = cell.result() else { + panic!("WCAG pass must retain typed pass evidence"); + }; + let modeled = cell.modeled_lcs_occurrence(); + assert_binding_matches_modeled(*evidence.binding(), modeled); + let measurement = evidence.measurement().value(); + assert_eq!(measurement.decision(), Wcag22ApplicableDecisionV1::Pass); + assert_eq!(measurement.measurement().foreground, [0; 3]); + assert_eq!(measurement.measurement().background, [0xFF; 3]); + assert_eq!( + evidence.binding().physical().occurrence().backdrop_rgb(), + measurement.measurement().background, + ); + assert_eq!(*evidence.invocation(), Wcag22CriterionV1::Sc143TextDefault,); +} + +#[test] +fn program_wcag_violation_retains_coherent_evidence_without_current_outputs() { + let compiled = compiled_wcag_program(0.5); + let mut session = compiled.instantiate(STREAM_A).unwrap(); + let state = session.update(observed_white(STREAM_A)).unwrap(); + let SessionState::Failed { cause, previous } = state else { + panic!("half-black composite on white must violate WCAG default text"); + }; + assert!(previous.is_none()); + let [cell] = cause.report().cells() else { + panic!("one WCAG declaration must produce one failed report cell"); + }; + let ProgramConstraintResultV1::Violation(evidence) = cell.result() else { + panic!("WCAG failure must retain typed violation evidence"); + }; + let modeled = cell.modeled_lcs_occurrence(); + assert_binding_matches_modeled(*evidence.binding(), modeled); + let measurement = evidence.measurement().value(); + assert_eq!(measurement.decision(), Wcag22ApplicableDecisionV1::Fail); + assert_eq!(measurement.measurement().foreground, [0x80; 3]); + assert_eq!(measurement.measurement().background, [0xFF; 3]); + assert_eq!( + evidence.binding().physical().occurrence().backdrop_rgb(), + measurement.measurement().background, + ); + assert_eq!( + modeled.signal(), + signal(measurement.measurement().foreground) + ); +} + +#[test] +fn mismatched_modeled_signal_is_rejected_before_program_evaluation() { + let physical = PointOpacityOverSurfaceV1::evaluate([0; 3], 1.0, [0xFF; 3]).unwrap(); + let mismatched = ModeledLcsOccurrenceV1::from_signal_in_context( + signal([0xFF; 3]), + context(SurroundProfileId::AverageV1), + ) + .unwrap(); + + let error = assess_program_point_hard( + &physical, + mismatched, + &ExactSrgb8IdentityV1, + Srgb8::new([0; 3]), + ) + .expect_err("binding mismatch must be rejected before the evaluator can run"); + let mismatch = match error { + ProgramPointAssessmentErrorV1::Binding(mismatch) => mismatch, + ProgramPointAssessmentErrorV1::Evaluator(unreachable) => match unreachable {}, + }; + assert_eq!(mismatch.physical(), Srgb8::new([0; 3])); + assert_eq!(mismatch.modeled(), Srgb8::new([0xFF; 3])); +} + +#[test] +fn program_derives_lcs_once_per_target_and_case_without_eager_cam16() { + let compiled = compiled_duplicate_constraint_program(); + let mut session = compiled.instantiate(STREAM_A).unwrap(); + MODELED_TRISTIMULUS_DERIVATION_CALLS.with(|calls| calls.set(0)); + FORWARD_CALLS.with(|calls| calls.set(0)); + + let state = session.update(observed_two_backdrops(STREAM_A)).unwrap(); + + let derivations = MODELED_TRISTIMULUS_DERIVATION_CALLS.with(|calls| calls.get()); + let cam16_forwards = FORWARD_CALLS.with(|calls| calls.get()); + let SessionState::Ready { current } = state else { + panic!("report-only constraints must retain the complete two-case result"); + }; + assert_eq!(current.report().cells().len(), 4); + assert_eq!( + derivations, 2, + "two constraints sharing one target must reuse one LCS derivation in each physical case", + ); + assert_eq!( + cam16_forwards, 0, + "Program lowering must not eagerly derive the contextual CAM16 view", + ); +} + +#[test] +fn declaration_permutations_preserve_canonical_lcs_cells_and_output_signals() { + let declarations = [ + ( + AVERAGE_OCCURRENCE, + AVERAGE_CONSTRAINT, + context(SurroundProfileId::AverageV1), + ), + ( + DIM_OCCURRENCE, + DIM_CONSTRAINT, + context(SurroundProfileId::DimV1), + ), + ]; + let canonical = compiled_program(&declarations, Srgb8::new([0x80; 3]), 0.5, false); + let permuted = compiled_program(&declarations, Srgb8::new([0x80; 3]), 0.5, true); + let mut canonical_session = canonical.instantiate(STREAM_A).unwrap(); + let mut permuted_session = permuted.instantiate(STREAM_B).unwrap(); + + let canonical_state = canonical_session.update(observed_white(STREAM_A)).unwrap(); + let permuted_state = permuted_session.update(observed_white(STREAM_B)).unwrap(); + let SessionState::Ready { current: canonical } = canonical_state else { + panic!("canonical declarations must verify"); + }; + let SessionState::Ready { current: permuted } = permuted_state else { + panic!("permuted declarations must verify"); + }; + + let cell_signature = |cell: &crate::program_session::ProgramConstraintCellV1<_>| { + ( + cell.case_index(), + cell.constraint(), + cell.target(), + cell.modeled_lcs_occurrence(), + cell.result().is_violation(), + ) + }; + assert_eq!( + canonical + .report() + .cells() + .iter() + .map(cell_signature) + .collect::>(), + permuted + .report() + .cells() + .iter() + .map(cell_signature) + .collect::>(), + ); + assert_eq!( + canonical + .outputs() + .iter() + .map(|output| (output.output(), output.source_signal())) + .collect::>(), + permuted + .outputs() + .iter() + .map(|output| (output.output(), output.source_signal())) + .collect::>(), + ); +} diff --git a/crates/labcolors-core/src/program_session.rs b/crates/labcolors-core/src/program_session.rs index bfb21f81..5441e440 100644 --- a/crates/labcolors-core/src/program_session.rs +++ b/crates/labcolors-core/src/program_session.rs @@ -6,8 +6,9 @@ //! occurrences, and outputs bind opaque slots back to Paints. The compiled //! result owns only admitted, canonical topology; runtime observation, //! lifecycle and terminal emission belong to the sole revision-bound Session. -//! Its current values are encoded point transport-only; they are not LCS -//! observation or evidence. +//! Output transport remains encoded, while every assessed visible occurrence +//! carries deterministic, context-bound modeled LCS provenance. Neither claim +//! is renderer observation or human-subject evidence. use std::marker::PhantomData; use std::rc::Rc; @@ -21,8 +22,12 @@ use crate::appearance::{ }; use crate::composition::CompositionProfileV1; use crate::constraints::{ - HardDecision, PointEvaluatorV1, PointInvocation, VisiblePointPassEvidence, - VisiblePointViolationEvidence, assess_visible_point_hard, + HardDecision, ProgramPointAssessmentErrorV1, ProgramPointEvaluatorV1, ProgramPointInvocation, + ProgramPointTargetV1, ProgramVisiblePointBindingV1, ProgramVisiblePointPassEvidence, + ProgramVisiblePointViolationEvidence, assess_program_point_hard, +}; +use crate::lcs_occurrence::{ + AppearanceContextId, ColorSignal, ModeledLcsOccurrenceFormationErrorV1, ModeledLcsOccurrenceV1, }; use crate::observation::{ CanonicalObservationSchemaV1, ObservationError, ObservationGroupId, @@ -38,11 +43,11 @@ use crate::session::{ #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ColorInput { id: ColorInputId, - value: Srgb8, + value: ColorSignal, } impl ColorInput { - pub const fn new(id: ColorInputId, value: Srgb8) -> Self { + pub const fn new(id: ColorInputId, value: ColorSignal) -> Self { Self { id, value } } @@ -50,7 +55,7 @@ impl ColorInput { self.id } - pub const fn value(self) -> Srgb8 { + pub const fn value(self) -> ColorSignal { self.value } } @@ -116,6 +121,7 @@ pub struct Occurrence { subject: PaintId, against: SurfaceId, composition: CompositionProfile, + context: AppearanceContextId, } impl Occurrence { @@ -124,12 +130,14 @@ impl Occurrence { subject: PaintId, against: SurfaceId, composition: CompositionProfile, + context: AppearanceContextId, ) -> Self { Self { id, subject, against, composition, + context, } } @@ -148,6 +156,10 @@ impl Occurrence { pub const fn composition(self) -> CompositionProfile { self.composition } + + pub const fn context(self) -> AppearanceContextId { + self.context + } } /// Opaque authored constraint identity. @@ -315,8 +327,8 @@ impl ObservationGroup { /// Immutable generic point Program. pub struct Program where - Evaluation: PointEvaluatorV1, - PointInvocation: Copy, + Evaluation: ProgramPointEvaluatorV1, + ProgramPointInvocation: Copy, { colors: Vec, observation_group: ObservationGroup, @@ -324,15 +336,15 @@ where paints: Vec, surfaces: Vec, occurrences: Vec, - constraints: ConstraintSet>, + constraints: ConstraintSet>, outputs: Vec, evaluator: Evaluation, } impl Program where - Evaluation: PointEvaluatorV1, - PointInvocation: Copy, + Evaluation: ProgramPointEvaluatorV1, + ProgramPointInvocation: Copy, { #[allow(clippy::too_many_arguments)] pub fn new( @@ -342,7 +354,7 @@ where paints: Vec, surfaces: Vec, occurrences: Vec, - constraints: ConstraintSet>, + constraints: ConstraintSet>, outputs: Vec, evaluator: Evaluation, ) -> Self { @@ -459,6 +471,7 @@ struct CompiledPointConstraint { id: ConstraintId, target_id: OccurrenceId, target: CompiledOccurrenceSlotV1, + modeled_occurrence_index: usize, mode: CompiledConstraintModeV1, invocation: Invocation, } @@ -470,6 +483,13 @@ struct CompiledOutputBinding { paint: CompiledPaintSlotV1, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct CompiledOccurrenceContextV1 { + occurrence: OccurrenceId, + target: CompiledOccurrenceSlotV1, + context: AppearanceContextId, +} + struct CompiledObservationGroupV1 { id: ObservationGroupId, schema: CanonicalObservationSchemaV1, @@ -477,30 +497,31 @@ struct CompiledObservationGroupV1 { struct ProgramEpochV1 where - Evaluation: PointEvaluatorV1, - PointInvocation: Copy, + Evaluation: ProgramPointEvaluatorV1, + ProgramPointInvocation: Copy, { evaluator: Evaluation, graph: CompiledAppearanceGraph, binding_template: AdmittedAppearanceBindings, observation_group: CompiledObservationGroupV1, - constraints: Box<[CompiledPointConstraint>]>, + occurrence_contexts: Box<[CompiledOccurrenceContextV1]>, + constraints: Box<[CompiledPointConstraint>]>, outputs: Box<[CompiledOutputBinding]>, } /// Fully validated immutable Program, not yet attached to runtime. pub struct CompiledProgram where - Evaluation: PointEvaluatorV1, - PointInvocation: Copy, + Evaluation: ProgramPointEvaluatorV1, + ProgramPointInvocation: Copy, { epoch: Rc>, } impl CompiledProgram where - Evaluation: PointEvaluatorV1, - PointInvocation: Copy, + Evaluation: ProgramPointEvaluatorV1, + ProgramPointInvocation: Copy, { pub fn observation_group_id(&self) -> ObservationGroupId { self.epoch.observation_group.id @@ -541,12 +562,18 @@ where .graph .new_workspace() .map_err(map_session_instantiate_error)?; + let mut modeled_occurrences = Vec::new(); + modeled_occurrences + .try_reserve_exact(self.epoch.occurrence_contexts.len()) + .map_err(|_| ProgramSessionInstantiateError::ResourceExhausted)?; + modeled_occurrences.resize(self.epoch.occurrence_contexts.len(), None); Ok(Session::new( stream, ProgramSessionPlan { epoch: Rc::clone(&self.epoch), bindings, workspace, + modeled_occurrences, }, )) } @@ -569,25 +596,36 @@ fn map_session_instantiate_error(error: BindingError) -> ProgramSessionInstantia /// One evaluator classification retained in the complete Program report. pub enum ProgramConstraintResultV1 where - Evaluation: PointEvaluatorV1, + Evaluation: ProgramPointEvaluatorV1, { - Pass(VisiblePointPassEvidence), - Violation(VisiblePointViolationEvidence), + Pass(ProgramVisiblePointPassEvidence), + Violation(ProgramVisiblePointViolationEvidence), } impl ProgramConstraintResultV1 where - Evaluation: PointEvaluatorV1, + Evaluation: ProgramPointEvaluatorV1, { pub const fn is_violation(&self) -> bool { matches!(self, Self::Violation(_)) } + + fn binding(&self) -> ProgramVisiblePointBindingV1 { + match self { + Self::Pass(evidence) => *evidence.binding(), + Self::Violation(evidence) => *evidence.binding(), + } + } + + fn modeled_lcs_occurrence(&self) -> ModeledLcsOccurrenceV1 { + self.binding().modeled_lcs() + } } /// One canonical `physical case × constraint` report cell. pub struct ProgramConstraintCellV1 where - Evaluation: PointEvaluatorV1, + Evaluation: ProgramPointEvaluatorV1, { case_index: usize, constraint: ConstraintId, @@ -598,7 +636,7 @@ where impl ProgramConstraintCellV1 where - Evaluation: PointEvaluatorV1, + Evaluation: ProgramPointEvaluatorV1, { pub const fn case_index(&self) -> usize { self.case_index @@ -612,6 +650,10 @@ where self.target } + pub fn modeled_lcs_occurrence(&self) -> ModeledLcsOccurrenceV1 { + self.result.modeled_lcs_occurrence() + } + pub const fn is_hard(&self) -> bool { matches!(self.mode, CompiledConstraintModeV1::Hard) } @@ -624,7 +666,7 @@ where /// Complete revision-bound assessment in case-major, constraint-ID order. pub struct ProgramReportV1 where - Evaluation: PointEvaluatorV1, + Evaluation: ProgramPointEvaluatorV1, { observation: RevisionBoundObservationV1, cells: Vec>, @@ -632,7 +674,7 @@ where impl ProgramReportV1 where - Evaluation: PointEvaluatorV1, + Evaluation: ProgramPointEvaluatorV1, { pub const fn observation(&self) -> &RevisionBoundObservationV1 { &self.observation @@ -658,25 +700,29 @@ impl ProgramOutputV1 { pub const fn paint(self) -> EncodedPointPaintV1 { self.paint } + + pub const fn source_signal(self) -> ColorSignal { + ColorSignal::from_srgb8(self.paint.source()) + } } /// All hard cells passed over the complete admitted physical support. pub struct ProgramVerifiedV1 where - Evaluation: PointEvaluatorV1, + Evaluation: ProgramPointEvaluatorV1, { report: ProgramReportV1, outputs: Vec, } impl session_private::EvidenceSealed for ProgramVerifiedV1 where - Evaluation: PointEvaluatorV1 + Evaluation: ProgramPointEvaluatorV1 { } impl SessionEvidenceV1 for ProgramVerifiedV1 where - Evaluation: PointEvaluatorV1, + Evaluation: ProgramPointEvaluatorV1, { fn observation(&self) -> &RevisionBoundObservationV1 { self.report().observation() @@ -685,7 +731,7 @@ where impl ProgramVerifiedV1 where - Evaluation: PointEvaluatorV1, + Evaluation: ProgramPointEvaluatorV1, { pub const fn report(&self) -> &ProgramReportV1 { &self.report @@ -700,19 +746,19 @@ where /// by construction and therefore cannot be mistaken for committed Paints. pub struct ProgramViolationV1 where - Evaluation: PointEvaluatorV1, + Evaluation: ProgramPointEvaluatorV1, { report: ProgramReportV1, } impl session_private::EvidenceSealed for ProgramViolationV1 where - Evaluation: PointEvaluatorV1 + Evaluation: ProgramPointEvaluatorV1 { } impl SessionEvidenceV1 for ProgramViolationV1 where - Evaluation: PointEvaluatorV1, + Evaluation: ProgramPointEvaluatorV1, { fn observation(&self) -> &RevisionBoundObservationV1 { self.report().observation() @@ -721,7 +767,7 @@ where impl ProgramViolationV1 where - Evaluation: PointEvaluatorV1, + Evaluation: ProgramPointEvaluatorV1, { pub const fn report(&self) -> &ProgramReportV1 { &self.report @@ -738,6 +784,17 @@ pub enum ProgramSessionEvaluationError { constraint: ConstraintId, source: EvaluationError, }, + ProgramTargetBinding { + case_index: usize, + constraint: ConstraintId, + physical: Srgb8, + modeled: Srgb8, + }, + ModeledOccurrence { + case_index: usize, + target: OccurrenceId, + source: ModeledLcsOccurrenceFormationErrorV1, + }, OutputVariesAcrossCases { output: OutputSlotId, first_case: usize, @@ -746,9 +803,8 @@ pub enum ProgramSessionEvaluationError { InternalInvariant, } -type ProgramEvaluatorError = >::Error; +type ProgramEvaluatorError = + >::Error; type ProgramSessionEvaluationResult = Result< SessionDecision, ProgramViolationV1>, @@ -758,25 +814,26 @@ type ProgramSessionEvaluationResult = Result< /// Per-Session mutable execution state backed by one strong immutable epoch. pub struct ProgramSessionPlan where - Evaluation: PointEvaluatorV1, - PointInvocation: Copy, + Evaluation: ProgramPointEvaluatorV1, + ProgramPointInvocation: Copy, { epoch: Rc>, bindings: AdmittedAppearanceBindings, workspace: AppearanceWorkspace, + modeled_occurrences: Vec>, } impl session_private::PlanSealed for ProgramSessionPlan where - Evaluation: PointEvaluatorV1, - PointInvocation: Copy, + Evaluation: ProgramPointEvaluatorV1, + ProgramPointInvocation: Copy, { } impl SessionPlanV1 for ProgramSessionPlan where - Evaluation: PointEvaluatorV1, - PointInvocation: Copy, + Evaluation: ProgramPointEvaluatorV1, + ProgramPointInvocation: Copy, { type Verified = ProgramVerifiedV1; type Violation = ProgramViolationV1; @@ -800,8 +857,8 @@ fn evaluate_program_session( observation: RevisionBoundObservationV1, ) -> ProgramSessionEvaluationResult where - Evaluation: PointEvaluatorV1, - PointInvocation: Copy, + Evaluation: ProgramPointEvaluatorV1, + ProgramPointInvocation: Copy, { let epoch = &plan.epoch; let schema = &epoch.observation_group.schema; @@ -844,25 +901,82 @@ where } plan.bindings .overwrite_surface_inputs_canonical(schema.as_slice().iter().copied(), |index| { - values[index] + values[index].srgb8() }) .map_err(map_program_execution_binding_error)?; let evaluation = epoch .graph .evaluate_admitted_into(&plan.bindings, &mut plan.workspace) .map_err(map_program_execution_binding_error)?; + plan.modeled_occurrences.fill(None); for constraint in epoch.constraints.iter() { let source = evaluation .occurrence_at(constraint.target) .ok_or(ProgramSessionEvaluationError::InternalInvariant)?; - let decision = - assess_visible_point_hard(source, &epoch.evaluator, constraint.invocation) - .map_err(|source| ProgramSessionEvaluationError::Evaluator { + if source.visible() != source.certificate().output_rgb() { + return Err(ProgramSessionEvaluationError::InternalInvariant); + } + let modeled_lcs_occurrence = match plan + .modeled_occurrences + .get(constraint.modeled_occurrence_index) + .copied() + .flatten() + { + Some(modeled) => modeled, + None => { + let binding = epoch + .occurrence_contexts + .get(constraint.modeled_occurrence_index) + .ok_or(ProgramSessionEvaluationError::InternalInvariant)?; + if binding.occurrence != constraint.target_id + || binding.target != constraint.target + { + return Err(ProgramSessionEvaluationError::InternalInvariant); + } + let modeled = ModeledLcsOccurrenceV1::from_signal_in_context( + ColorSignal::from_srgb8(Srgb8::new(source.visible())), + binding.context, + ) + .map_err(|source| { + ProgramSessionEvaluationError::ModeledOccurrence { + case_index, + target: constraint.target_id, + source, + } + })?; + let slot = plan + .modeled_occurrences + .get_mut(constraint.modeled_occurrence_index) + .ok_or(ProgramSessionEvaluationError::InternalInvariant)?; + *slot = Some(modeled); + modeled + } + }; + let decision = assess_program_point_hard( + source, + modeled_lcs_occurrence, + &epoch.evaluator, + constraint.invocation, + ) + .map_err(|error| match error { + ProgramPointAssessmentErrorV1::Binding(source) => { + debug_assert_ne!(source.physical(), source.modeled()); + ProgramSessionEvaluationError::ProgramTargetBinding { + case_index, + constraint: constraint.id, + physical: source.physical(), + modeled: source.modeled(), + } + } + ProgramPointAssessmentErrorV1::Evaluator(source) => { + ProgramSessionEvaluationError::Evaluator { case_index, constraint: constraint.id, source, - })?; + } + } + })?; let result = match decision { HardDecision::Pass(evidence) => ProgramConstraintResultV1::Pass(evidence), HardDecision::Violation(evidence) => { @@ -872,6 +986,8 @@ where ProgramConstraintResultV1::Violation(evidence) } }; + debug_assert_eq!(result.binding().physical(), source.visible_point_binding()); + debug_assert_eq!(result.binding().modeled_lcs(), modeled_lcs_occurrence); cells.push(ProgramConstraintCellV1 { case_index, constraint: constraint.id, @@ -933,8 +1049,8 @@ fn prepare_program( program: Program, ) -> Result, ProgramCompileError> where - Evaluation: PointEvaluatorV1, - PointInvocation: Copy, + Evaluation: ProgramPointEvaluatorV1, + ProgramPointInvocation: Copy, { if program.observation_group.surface_input_ports.is_empty() { return Err(ProgramCompileError::EmptyObservationGroup { @@ -976,7 +1092,11 @@ where let observation_schema = canonicalize_observation_schema(surface_input_ports) .map_err(map_observation_schema_compile_error)?; - let constraints = compile_constraints::(&graph, program.constraints)?; + let all_occurrence_contexts = compile_occurrence_contexts(&graph, &program.occurrences)?; + let mut 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)?; Ok(ProgramEpochV1 { evaluator: program.evaluator, @@ -986,6 +1106,7 @@ where id: program.observation_group.id, schema: observation_schema, }, + occurrence_contexts, constraints, outputs, }) @@ -1005,13 +1126,52 @@ struct LoweredConstraint { invocation: Invocation, } +fn compile_occurrence_contexts( + graph: &CompiledAppearanceGraph, + authored: &[Occurrence], +) -> Result, ProgramCompileError> { + let mut contexts = Vec::new(); + contexts + .try_reserve_exact(authored.len()) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + contexts.extend( + authored + .iter() + .map(|occurrence| (occurrence.id(), occurrence.context())), + ); + contexts.sort_unstable_by_key(|(occurrence, _)| *occurrence); + + let mut compiled = Vec::new(); + compiled + .try_reserve_exact(authored.len()) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + for occurrence in graph.occurrence_ids() { + let index = contexts + .binary_search_by_key(&occurrence, |(declared, _)| *declared) + .map_err(|_| ProgramCompileError::InternalInvariant)?; + let target = graph + .bind_occurrence(occurrence) + .ok_or(ProgramCompileError::InternalInvariant)?; + compiled.push(CompiledOccurrenceContextV1 { + occurrence, + target, + context: contexts[index].1, + }); + } + if compiled.len() != authored.len() { + return Err(ProgramCompileError::InternalInvariant); + } + Ok(compiled.into_boxed_slice()) +} + fn compile_constraints( graph: &CompiledAppearanceGraph, - authored: ConstraintSet>, -) -> Result>]>, ProgramCompileError> + occurrence_contexts: &[CompiledOccurrenceContextV1], + authored: ConstraintSet>, +) -> Result>]>, ProgramCompileError> where - Evaluation: PointEvaluatorV1, - PointInvocation: Copy, + Evaluation: ProgramPointEvaluatorV1, + ProgramPointInvocation: Copy, { let total = authored .hard @@ -1071,10 +1231,17 @@ where let target = graph .bind_occurrence(constraint.target) .ok_or(ProgramCompileError::InternalInvariant)?; + let modeled_occurrence_index = occurrence_contexts + .binary_search_by_key(&constraint.target, |binding| binding.occurrence) + .map_err(|_| ProgramCompileError::InternalInvariant)?; + if occurrence_contexts[modeled_occurrence_index].target != target { + return Err(ProgramCompileError::InternalInvariant); + } compiled.push(CompiledPointConstraint { id: constraint.id, target_id: constraint.target, target, + modeled_occurrence_index, mode: constraint.mode, invocation: constraint.invocation, }); @@ -1082,6 +1249,41 @@ where Ok(compiled.into_boxed_slice()) } +fn compact_constraint_contexts( + all: &[CompiledOccurrenceContextV1], + constraints: &mut [CompiledPointConstraint], +) -> Result, ProgramCompileError> { + let mut targets = Vec::new(); + targets + .try_reserve_exact(constraints.len()) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + targets.extend(constraints.iter().map(|constraint| constraint.target_id)); + targets.sort_unstable(); + targets.dedup(); + + let mut compact = Vec::new(); + compact + .try_reserve_exact(targets.len()) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + for occurrence in targets { + let index = all + .binary_search_by_key(&occurrence, |binding| binding.occurrence) + .map_err(|_| ProgramCompileError::InternalInvariant)?; + compact.push(all[index]); + } + + for constraint in constraints { + let index = compact + .binary_search_by_key(&constraint.target_id, |binding| binding.occurrence) + .map_err(|_| ProgramCompileError::InternalInvariant)?; + if compact[index].target != constraint.target { + return Err(ProgramCompileError::InternalInvariant); + } + constraint.modeled_occurrence_index = index; + } + Ok(compact.into_boxed_slice()) +} + fn compile_outputs( graph: &CompiledAppearanceGraph, authored: Vec, @@ -1141,8 +1343,8 @@ pub(crate) fn canonical_surface_input_port_sequence_matches( fn lower_graph(program: &Program) -> AppearanceGraphSpec where - Evaluation: PointEvaluatorV1, - PointInvocation: Copy, + Evaluation: ProgramPointEvaluatorV1, + ProgramPointInvocation: Copy, { AppearanceGraphSpec::new( program.colors.iter().map(|input| input.id).collect(), @@ -1193,14 +1395,14 @@ where fn lower_bindings(program: &Program) -> AppearanceBindings where - Evaluation: PointEvaluatorV1, - PointInvocation: Copy, + Evaluation: ProgramPointEvaluatorV1, + ProgramPointInvocation: Copy, { AppearanceBindings::new( program .colors .iter() - .map(|input| (input.id, input.value)) + .map(|input| (input.id, input.value.srgb8())) .collect(), program .observation_group diff --git a/crates/labcolors-core/src/program_session_tests.rs b/crates/labcolors-core/src/program_session_tests.rs index c80befe3..73418806 100644 --- a/crates/labcolors-core/src/program_session_tests.rs +++ b/crates/labcolors-core/src/program_session_tests.rs @@ -2,7 +2,11 @@ use crate::Srgb8; use crate::appearance::{ ColorInputId, OccurrenceId, OpacityInputId, PaintId, SurfaceId, SurfaceInputPortId, }; -use crate::constraints::{ExactSrgb8IdentityV1, PointEvaluatorV1, PointInvocation}; +use crate::constraints::{ExactSrgb8IdentityV1, ProgramPointEvaluatorV1, ProgramPointInvocation}; +use crate::lcs_occurrence::{ + AdaptingLuminanceCdM2, AppearanceContextId, AppearanceContextSchemaReleaseId, + BackgroundLuminanceRatio, ColorSignal, IEC_SRGB_D65_XYZ_FRAME_V1, SurroundProfileId, +}; use crate::observation::{ ObservationGroupId, ObservationHeadViewV1, ObservationPayloadInput, ObservationStreamId, ObservationUpdateInput, ObservedScenarioSetInput, Revision, ScenarioId, ScenarioInput, @@ -30,6 +34,16 @@ const GROUP: ObservationGroupId = ObservationGroupId::new(60); const STREAM_A: ObservationStreamId = ObservationStreamId::new(70); const STREAM_B: ObservationStreamId = ObservationStreamId::new(71); +fn appearance_context() -> AppearanceContextId { + AppearanceContextId::from_inputs( + AppearanceContextSchemaReleaseId::Ciecam16ViewingInputsV1, + IEC_SRGB_D65_XYZ_FRAME_V1, + AdaptingLuminanceCdM2::try_new(64.0).unwrap(), + BackgroundLuminanceRatio::try_new(0.2).unwrap(), + SurroundProfileId::AverageV1, + ) +} + fn observation_group(surface_input_ports: Vec) -> ObservationGroup { ObservationGroup::new(GROUP, surface_input_ports) } @@ -37,13 +51,13 @@ fn observation_group(surface_input_ports: Vec) -> Observatio fn base_program( opacity: f64, against: SurfaceId, - constraints: ConstraintSet>, + constraints: ConstraintSet>, outputs: Vec, evaluator: Evaluation, ) -> Program where - Evaluation: PointEvaluatorV1, - PointInvocation: Copy, + Evaluation: ProgramPointEvaluatorV1, + ProgramPointInvocation: Copy, { base_program_in_group(GROUP, opacity, against, constraints, outputs, evaluator) } @@ -52,16 +66,19 @@ fn base_program_in_group( group: ObservationGroupId, opacity: f64, against: SurfaceId, - constraints: ConstraintSet>, + constraints: ConstraintSet>, outputs: Vec, evaluator: Evaluation, ) -> Program where - Evaluation: PointEvaluatorV1, - PointInvocation: Copy, + Evaluation: ProgramPointEvaluatorV1, + ProgramPointInvocation: Copy, { Program::new( - vec![ColorInput::new(COLOR, Srgb8::new([0; 3]))], + vec![ColorInput::new( + COLOR, + ColorSignal::from_srgb8(Srgb8::new([0; 3])), + )], ObservationGroup::new(group, vec![SURFACE_PORT]), vec![OpacityInput::new(OPACITY, opacity)], vec![ @@ -90,6 +107,7 @@ where TRANSLUCENT, against, CompositionProfile::EncodedSrgb8SourceOverV1, + appearance_context(), )], constraints, outputs, @@ -99,8 +117,8 @@ where fn compile_error(program: Program) -> ProgramCompileError where - Evaluation: PointEvaluatorV1, - PointInvocation: Copy, + Evaluation: ProgramPointEvaluatorV1, + ProgramPointInvocation: Copy, { match program.compile() { Ok(_) => panic!("invalid declaration compiled"), @@ -123,7 +141,7 @@ fn observed_update( id: ScenarioId::new(*scenario), bindings: vec![SurfaceInputBinding::new( SURFACE_PORT, - Srgb8::new(*backdrop), + ColorSignal::from_srgb8(Srgb8::new(*backdrop)), )], }) .collect(), @@ -162,9 +180,12 @@ fn authored_modes_are_marker_typed_and_values_preserve_exact_ids() { assert_eq!(ConstraintId::new(7).value(), 7); assert_eq!(OutputSlotId::new(8).value(), 8); - let color = ColorInput::new(COLOR, Srgb8::new([1, 2, 3])); + let color = ColorInput::new(COLOR, ColorSignal::from_srgb8(Srgb8::new([1, 2, 3]))); assert_eq!(color.id(), COLOR); - assert_eq!(color.value(), Srgb8::new([1, 2, 3])); + assert_eq!( + color.value(), + ColorSignal::from_srgb8(Srgb8::new([1, 2, 3])) + ); let opacity = OpacityInput::new(OPACITY, 0.375); assert_eq!(opacity.id(), OPACITY); assert_eq!(opacity.value(), 0.375); @@ -174,6 +195,7 @@ fn authored_modes_are_marker_typed_and_values_preserve_exact_ids() { TRANSLUCENT, BACKDROP, CompositionProfile::EncodedSrgb8SourceOverV1, + appearance_context(), ); assert_eq!(occurrence.id(), OCCURRENCE); assert_eq!(occurrence.subject(), TRANSLUCENT); @@ -191,7 +213,10 @@ fn authored_modes_are_marker_typed_and_values_preserve_exact_ids() { #[test] fn empty_domains_have_stable_precedence() { let empty_surface = Program::new( - vec![ColorInput::new(COLOR, Srgb8::new([0; 3]))], + vec![ColorInput::new( + COLOR, + ColorSignal::from_srgb8(Srgb8::new([0; 3])), + )], observation_group(vec![]), vec![], vec![Paint::Solid { @@ -207,6 +232,7 @@ fn empty_domains_have_stable_precedence() { SOLID, BACKDROP, CompositionProfile::EncodedSrgb8SourceOverV1, + appearance_context(), )], ConstraintSet::new( vec![ConstraintInvocation::hard( @@ -225,7 +251,10 @@ fn empty_domains_have_stable_precedence() { ); let empty_occurrence = Program::new( - vec![ColorInput::new(COLOR, Srgb8::new([0; 3]))], + vec![ColorInput::new( + COLOR, + ColorSignal::from_srgb8(Srgb8::new([0; 3])), + )], observation_group(vec![SURFACE_PORT]), vec![], vec![Paint::Solid { @@ -677,8 +706,8 @@ fn nested_surface_uses_the_lower_occurrence_before_assessing_the_upper() { let program = Program::new( vec![ - ColorInput::new(LOWER_COLOR, Srgb8::new([0x80; 3])), - ColorInput::new(UPPER_COLOR, Srgb8::new([0xFF; 3])), + ColorInput::new(LOWER_COLOR, ColorSignal::from_srgb8(Srgb8::new([0x80; 3]))), + ColorInput::new(UPPER_COLOR, ColorSignal::from_srgb8(Srgb8::new([0xFF; 3]))), ], observation_group(vec![SURFACE_PORT]), vec![OpacityInput::new(HALF, 0.5)], @@ -713,12 +742,14 @@ fn nested_surface_uses_the_lower_occurrence_before_assessing_the_upper() { LOWER_PAINT, ROOT, CompositionProfile::EncodedSrgb8SourceOverV1, + appearance_context(), ), Occurrence::new( UPPER, UPPER_PAINT, DERIVED, CompositionProfile::EncodedSrgb8SourceOverV1, + appearance_context(), ), ], ConstraintSet::new( diff --git a/crates/labcolors-core/src/session_tests.rs b/crates/labcolors-core/src/session_tests.rs index 18b4f834..454090b9 100644 --- a/crates/labcolors-core/src/session_tests.rs +++ b/crates/labcolors-core/src/session_tests.rs @@ -3,6 +3,7 @@ use std::rc::Rc; use crate::Srgb8; use crate::appearance::SurfaceInputPortId; +use crate::lcs_occurrence::ColorSignal; use crate::observation::{ CanonicalObservationSchemaV1, ObservationError, ObservationHeadViewV1, ObservationPayloadInput, ObservationStreamId, ObservationUpdateInput, ObservedScenarioSetInput, Revision, @@ -107,6 +108,7 @@ impl SessionPlanV1 for SentinelPlan { .physical_values(0) .and_then(|values| values.first()) .copied() + .map(ColorSignal::srgb8) .ok_or(SentinelError::EmptyObservation)?; let observation = self .control @@ -156,7 +158,10 @@ fn observed_update(revision: u64, value: [u8; 3]) -> ObservationUpdateInput { payload: ObservationPayloadInput::Scenarios(ObservedScenarioSetInput { scenarios: vec![ScenarioInput { id: ScenarioId::new(1), - bindings: vec![SurfaceInputBinding::new(SURFACE, Srgb8::new(value))], + bindings: vec![SurfaceInputBinding::new( + SURFACE, + ColorSignal::from_srgb8(Srgb8::new(value)), + )], }], }), } diff --git a/scripts/point-support-release-contract.cjs b/scripts/point-support-release-contract.cjs index 8f8faef0..d8b222f5 100644 --- a/scripts/point-support-release-contract.cjs +++ b/scripts/point-support-release-contract.cjs @@ -21,6 +21,7 @@ const POINT_SUPPORT_SOURCE_PATHS = Object.freeze( "crates/labcolors-core/src/constraints/mod.rs", "crates/labcolors-core/src/constraints/wcag22.rs", "crates/labcolors-core/src/hash.rs", + "crates/labcolors-core/src/lcs_occurrence.rs", "crates/labcolors-core/src/lib.rs", "crates/labcolors-core/src/numerics.rs", "crates/labcolors-core/src/observation.rs", diff --git a/scripts/verify-package-release.mjs b/scripts/verify-package-release.mjs index bc0a0186..5b64dc1b 100644 --- a/scripts/verify-package-release.mjs +++ b/scripts/verify-package-release.mjs @@ -335,7 +335,7 @@ async function validatePointSupportEvidence(artifacts, numericalCapabilities) { POINT_SUPPORT_SOURCE_BINDING_EXCLUSIONS, ) || !/^[0-9a-f]{64}$/u.test(proof.source_closure_sha256 ?? "") || - proof.source_negative_controls !== 42 || + proof.source_negative_controls !== 43 || !/^[0-9a-f]{64}$/u.test(proof.proof_payload_sha256 ?? "") || !/^[0-9a-f]{64}$/u.test(proof.verifier_sha256 ?? "") || !Array.isArray(proof.source_files) || diff --git a/scripts/verify_point_support_surplus.py b/scripts/verify_point_support_surplus.py index 8d88725e..47db06cd 100644 --- a/scripts/verify_point_support_surplus.py +++ b/scripts/verify_point_support_surplus.py @@ -23,6 +23,7 @@ REPO_ROOT = Path(__file__).resolve().parents[1] POINT_SOURCE = REPO_ROOT / "crates/labcolors-core/src/point_support.rs" OBSERVATION_SOURCE = REPO_ROOT / "crates/labcolors-core/src/observation.rs" +LCS_OCCURRENCE_SOURCE = REPO_ROOT / "crates/labcolors-core/src/lcs_occurrence.rs" NUMERICS_SOURCE = REPO_ROOT / "crates/labcolors-core/src/numerics.rs" SESSION_SOURCE = REPO_ROOT / "crates/labcolors-core/src/session.rs" COMPOSITION_SOURCE = REPO_ROOT / "crates/labcolors-core/src/composition.rs" @@ -57,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 = ( - "2dba7f59bd0f8d665b79d3286527cc67a99d1dfe1f7604e6d19be3643e39ed5d" + "1845ff9cb4584f752d576e87a456c758f280b447410f5c0666e8c35d3f0f199b" ) EXPECTED_Q55_PROOF_SHA256 = ( "ac59cf89503170c789223b91d775213a19d4e571ef930f2ea609fcd51b14defd" @@ -93,6 +94,7 @@ SOURCE_CONE_PATHS = ( POINT_SOURCE, OBSERVATION_SOURCE, + LCS_OCCURRENCE_SOURCE, SESSION_SOURCE, NUMERICS_SOURCE, COMPOSITION_SOURCE, @@ -204,6 +206,7 @@ def verify_source_binding() -> tuple[str, int]: (OBSERVATION_SOURCE, b"Some(observation.revision)", b"None"), (OBSERVATION_SOURCE, b"(self.owner, self.observation)", b"unreachable!()"), (OBSERVATION_SOURCE, b" && Rc::ptr_eq(&self.backing, &other.backing)\n", b" && self.backing == other.backing\n"), + (LCS_OCCURRENCE_SOURCE, b" pub(crate) const fn srgb8(self) -> Srgb8 {\n self.srgb8\n }", b" pub(crate) const fn srgb8(self) -> Srgb8 {\n Srgb8::new([0, 0, 0])\n }"), (SESSION_SOURCE, b" Self::Observed(observation) => ObservationHeadViewV1::Observed(observation),\n", b" Self::Observed(_) => ObservationHeadViewV1::Empty,\n"), (SESSION_SOURCE, b" let next_raw_head = SessionObservationHeadV1::Observed(observation.clone());\n", b" let next_raw_head = SessionObservationHeadV1::Empty;\n"), (SESSION_SOURCE, b" *raw_head = next_raw_head;\n", b" *raw_head = SessionObservationHeadV1::Empty;\n"), @@ -215,7 +218,7 @@ def verify_source_binding() -> tuple[str, int]: (NUMERICS_SOURCE, b"proof_ids: [NumericalProofIdV2::PointSupportReferenceSurplusIntegerV1],\n bound_status: Available", b"proof_ids: [NumericalProofIdV2::PointSupportReferenceSurplusIntegerV1],\n bound_status: Unavailable"), (COMPOSITION_SOURCE, b"f64::from(backdrop) + alpha * (f64::from(tint) - f64::from(backdrop))", b"f64::from(tint)"), (APPEARANCE_SOURCE, b"self.opacity\n", b"crate::composition::AdmittedOpacityV1::OPAQUE\n"), - (CONSTRAINTS_SOURCE, b"let classification = evaluator.classify(&invocation, &measurement);", b"let classification = unreachable!();"), + (CONSTRAINTS_SOURCE, b"let measurement = evaluator.evaluate(&target, &invocation)?;\n let classification = evaluator.classify(&invocation, &measurement);\n let identity = evaluator.identity();", b"let measurement = evaluator.evaluate(&target, &invocation)?;\n let classification = unreachable!();\n let identity = evaluator.identity();"), (EXACT_CONSTRAINT_SOURCE, b"if actual == *invocation", b"if actual != *invocation"), (WCAG22_CONSTRAINT_SOURCE, b"Wcag22ApplicableDecisionV1::Pass => HardDecision::Pass(Wcag22PassV1(()))", b"Wcag22ApplicableDecisionV1::Pass => HardDecision::Violation(Wcag22ViolationV1(()))"), (WCAG22_SOURCE, b"foreground_luminance: kernel::luminance_bounds(foreground),", b"foreground_luminance: kernel::luminance_bounds(background),"), From e84d07ef1c0d7d8b897fd50185fbd32dae14f41b Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Wed, 22 Jul 2026 23:32:05 +0300 Subject: [PATCH 30/58] Hard-delete Pair taxonomy from shipping surfaces --- .cargo/mutants.toml | 1 - ...rt-reference-surplus-q55-bps-proof-v1.json | 2 +- crates/labcolors-core/src/agnostic_gates.rs | 46 +-- crates/labcolors-core/src/config.rs | 55 ---- crates/labcolors-core/src/config/preset.rs | 37 +-- crates/labcolors-core/src/config/tests.rs | 78 +---- .../src/generic_boundary_tests.rs | 4 +- crates/labcolors-core/src/joint.rs | 8 - crates/labcolors-core/src/lib.rs | 15 +- crates/labcolors-core/src/pair.rs | 247 ---------------- crates/labcolors-core/src/pair_label_tests.rs | 174 ----------- crates/labcolors-core/src/semantic.rs | 274 +----------------- .../tests/data/labui_emission_golden.txt | 108 ------- .../tests/empirical_inventory.rs | 7 +- crates/labcolors-wasm/src/config_dto.rs | 59 +--- crates/labcolors-wasm/src/lib.rs | 2 - .../tests/data/labui.config.json | 78 +---- .../tests/data/labui.config.prod.json | 78 +---- docs/whitepaper.md | 11 +- packages/colors/bench/wasm.json | 8 +- .../colors/test/wasm-boundary.golden.json | 2 +- scripts/check-wasm-size-budget.mjs | 2 +- scripts/verify_point_support_surplus.py | 2 +- 23 files changed, 36 insertions(+), 1262 deletions(-) delete mode 100644 crates/labcolors-core/src/pair.rs delete mode 100644 crates/labcolors-core/src/pair_label_tests.rs diff --git a/.cargo/mutants.toml b/.cargo/mutants.toml index 381ac469..f7365186 100644 --- a/.cargo/mutants.toml +++ b/.cargo/mutants.toml @@ -23,7 +23,6 @@ examine_globs = [ "crates/labcolors-core/src/numerical_plan.rs", "crates/labcolors-core/src/numerics.rs", "crates/labcolors-core/src/observation.rs", - "crates/labcolors-core/src/pair.rs", "crates/labcolors-core/src/recheck.rs", "crates/labcolors-core/src/srgb8.rs", "crates/labcolors-core/src/wcag22.rs", 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 bd2b783d..1b99b1db 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":"46e2826dee60e6cf66e4aa5a4cc05dbe6c5c359eaf1eb44337d796f3f280ad77","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":"1845ff9cb4584f752d576e87a456c758f280b447410f5c0666e8c35d3f0f199b","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":"57e6feeab15182322c258829d10214c31bb3c93fa58d8d47e37127db204cdc28"},{"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":"15b6437ed59c92d82b928d9d913d25c14b430ba8c75db54a02f7da18abcde684"},{"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":"a58acbae5b06a8cd9a45adf93ddfbdfb569c5916b60140bd4046182aab2a9518"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"6b6338802ff32cd4ac4c21a7f468721ba09e112adc208b2170dd8becdb9462be"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"abed15a542b5e8031f0ebfe36138232920c7898c9eae1a2037c6455033ecd91e"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"945755f2eb91eb21aeee9b7a7b0e67b093d7e766af35a121480003669d88a053"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"0a1fdf10c9ecae497cfcd447a6af937f2b07d43b81b2cd28a50d998e11197fe9"},{"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":"3f1461805ba0edc17b8b167da43d61dc1e6f73da24bb99a80bf1d7401bccc110"} +{"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":"535001a983b26b2ba9a157417e4d42de3517c0bf784c9d1863f0e4943981e959","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":"6a541c168d9a5c266367050caec0114ea137a8f20f11f9868a6e46c821ba2ec2","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":"57e6feeab15182322c258829d10214c31bb3c93fa58d8d47e37127db204cdc28"},{"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":"15b6437ed59c92d82b928d9d913d25c14b430ba8c75db54a02f7da18abcde684"},{"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":"a58acbae5b06a8cd9a45adf93ddfbdfb569c5916b60140bd4046182aab2a9518"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"c1adca0472a97272fbeb1eec9e110aec93768a3b898c4d34a8afdd54cafcc535"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"abed15a542b5e8031f0ebfe36138232920c7898c9eae1a2037c6455033ecd91e"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"945755f2eb91eb21aeee9b7a7b0e67b093d7e766af35a121480003669d88a053"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"0a1fdf10c9ecae497cfcd447a6af937f2b07d43b81b2cd28a50d998e11197fe9"},{"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":"2e192f355564c93d83a8f43ae648e06b34cbcb20d347137a998608657c98c434"} diff --git a/crates/labcolors-core/src/agnostic_gates.rs b/crates/labcolors-core/src/agnostic_gates.rs index 7130def7..147eb137 100644 --- a/crates/labcolors-core/src/agnostic_gates.rs +++ b/crates/labcolors-core/src/agnostic_gates.rs @@ -306,8 +306,8 @@ fn acme_config() -> ThemeConfig { ("night".to_string(), VcPreset::Dim), ], }, - // A small but real role set: a text ladder, a neutral fill, a brand fill, - // a hued brand label, a badge label, a brand focus ring, a brand glow. + // A small but real role set: a text ladder, a neutral fill, a hued brand + // label, a brand focus ring and a brand glow. roles: vec![ ("text-strong".to_string(), text(0.968, Floor::AaText)), ("text-weak".to_string(), text(0.461, Floor::AaUi)), @@ -319,12 +319,6 @@ fn acme_config() -> ThemeConfig { floor: None, }, ), - ( - "brand-fill".to_string(), - RoleRecipe::PairFill { - source: LadderSource::Brand, - }, - ), ( "brand-label".to_string(), RoleRecipe::TextAnchor { @@ -333,16 +327,6 @@ fn acme_config() -> ThemeConfig { hue: Some(LadderSource::Brand), }, ), - // Лейбл пары: любой клиент получает жёсткий контраст против - // фактически emitted PairFill Surface через общий joint engine. - ( - "badge-label".to_string(), - RoleRecipe::PairLabel { - source: LadderSource::Brand, - fraction: 0.461, - floor: Floor::AaUi, - }, - ), ("focus".to_string(), brand_ladder(LadderPosition::FocusRing)), // Свечение бренда (#292): numerical-decision профиль обязателен // ЯВНО и у чужого клиента — implicit legacy непредставим; выбор @@ -369,8 +353,8 @@ fn a_second_company_config_compiles_and_emits_a_valid_system() { .expect("a well-formed foreign config must compile"); assert_eq!( table.entries().len(), - 8, - "acme declared eight roles; the table carries exactly them" + 6, + "acme declared six roles; the table carries exactly them" ); // Численный план (#292) второго клиента — derived-проекция той же таблицы: @@ -394,7 +378,7 @@ fn a_second_company_config_compiles_and_emits_a_valid_system() { let bg = BgInput::solid(bg_hex).unwrap(); let set = resolve_named_set(&bg, &table, &vc) .expect("валидный второй клиент обязан резолвиться атомарно"); - assert_eq!(set.len(), 8, "every declared role resolves to an outcome"); + assert_eq!(set.len(), 6, "every declared role resolves to an outcome"); // The text ladder is real: strong is a solved colour that clears its AA // text floor and reads stronger than the weak rung. @@ -419,26 +403,6 @@ fn a_second_company_config_compiles_and_emits_a_valid_system() { matches!(brand_label.1, Resolved::Color { .. }), "hued brand-label must resolve to a solved colour on {bg_hex}" ); - - // PairLabel — агностичный жёсткий контраст: решается цветом и держит - // UI-пол (3:1) против фактически emitted PairFill Surface, а не - // страницы или скрытой синтетической подложки. - let badge_label = set.iter().find(|(n, _)| n == "badge-label").unwrap(); - let Resolved::Color { solved, .. } = &badge_label.1 else { - panic!("badge-label must resolve to a solved colour on {bg_hex}"); - }; - let brand_fill = set.iter().find(|(n, _)| n == "brand-fill").unwrap(); - let surface_hex = brand_fill - .1 - .translucent() - .expect("brand-fill is the emitted PairFill surface") - .composite_hex(); - let enc = |h: &str| crate::spaces::srgb::srgb_encoded_from_hex(h).unwrap(); - let ratio = crate::wcag::contrast_ratio(enc(solved.hex()), enc(surface_hex)); - assert!( - ratio >= 3.0 - 1e-9, - "badge-label must clear 3:1 against emitted PairFill on {bg_hex}: got {ratio:.2}:1" - ); } } diff --git a/crates/labcolors-core/src/config.rs b/crates/labcolors-core/src/config.rs index 31d5e8d7..ca8ac53b 100644 --- a/crates/labcolors-core/src/config.rs +++ b/crates/labcolors-core/src/config.rs @@ -435,26 +435,6 @@ pub enum RoleRecipe { /// Обязательный numerical-decision profile; implicit legacy запрещён. decision_profile: crate::glow::GlowDecisionProfileV1, }, - /// Frozen PairFill frontend до C7c. Источник эмитится opaque Paint через - /// общий point occurrence; отдельной Pair-эвристики и скрытой роли нет. - PairFill { - /// Источник якоря: бренд, семейство или нейтраль. - source: LadderSource, - }, - /// Frozen PairLabel frontend до C7c. Label-кандидаты проверяются против - /// фактически emitted opaque [`PairFill`](Self::PairFill) Surface общим - /// joint hard-report и fresh recheck. - PairLabel { - /// Источник физической цветовой идентичности: бренд, семейство или нейтраль. - source: LadderSource, - /// Доля максимума контраста PairFill Surface `(0, 1]` (как у - /// [`TextAnchor`](Self::TextAnchor)): низкая доля оставляет больше места - /// для хромы источника у пола, высокая тянет к контрастному пределу. - /// Точный серый source при любой доле остаётся нейтральным. - fraction: f64, - /// WCAG-пол против emitted PairFill Surface, не страницы. - floor: Floor, - }, /// Альфа-аналог solid-источника через точечную композит-инверсию /// ([`crate::alpha`]): `(tint, α)`, чей композит на объявленном фоне равен /// solid-цели `of`. Компилируется в [`RoleSpec::AlphaAnalog`]. @@ -508,8 +488,6 @@ fn reserved_css_suffixes(recipe: &RoleRecipe) -> &'static [&'static str] { | RoleRecipe::DjAnchor { .. } | RoleRecipe::DecorativeLc { .. } | RoleRecipe::Ladder { .. } - | RoleRecipe::PairFill { .. } - | RoleRecipe::PairLabel { .. } | RoleRecipe::AlphaAnalog { .. } | RoleRecipe::Zero => PRIMARY, } @@ -938,19 +916,6 @@ impl ThemeConfig { } // Ступень — закрытый enum, числовой валидации не требует; источник — как у лестницы. RoleRecipe::Glow { source, .. } => self.check_ladder_source(role, source), - RoleRecipe::PairFill { source } => self.check_ladder_source(role, source), - RoleRecipe::PairLabel { - source, fraction, .. - } => { - self.check_ladder_source(role, source)?; - check_in_excl_incl( - &format!("roles.{role}.fraction"), - *fraction, - FRACTION_MIN_EXCLUSIVE, - FRACTION_MAX_INCLUSIVE, - "0 < fraction ≤ 1 (доля максимального контраста PairFill Surface)", - ) - } RoleRecipe::AlphaAnalog { of, alpha } => { self.check_ladder_source(role, of)?; check_in_excl_incl( @@ -1174,26 +1139,6 @@ impl ThemeConfig { floor: *floor, }) } - RoleRecipe::PairFill { source } => Ok(RoleSpec::PairFill { - tint: self.compile_ladder_tint(role, source)?, - }), - RoleRecipe::PairLabel { - source, - fraction, - floor, - } => { - // P1 унифицирует PairFill/PairLabel на единственной поверхности, - // которую публичный PairFill уже эмитил: opaque source Paint. - // Representation не выводится из клиентского имени позиции. - let (surface_alpha_light, surface_alpha_dark) = (1.0, 1.0); - Ok(RoleSpec::PairLabel { - tint: self.compile_ladder_tint(role, source)?, - fraction: *fraction, - floor: *floor, - surface_alpha_light, - surface_alpha_dark, - }) - } } } diff --git a/crates/labcolors-core/src/config/preset.rs b/crates/labcolors-core/src/config/preset.rs index 3a315f7b..f54b3c2e 100644 --- a/crates/labcolors-core/src/config/preset.rs +++ b/crates/labcolors-core/src/config/preset.rs @@ -297,9 +297,7 @@ pub fn labui_preset_roles() -> Vec<(String, RoleRecipe)> { // НЕЙТРАЛЬНЫЙ (стаб: fill-neutral солид-литерал; fill-neutral-tinted и // border-neutral алиасят нейтральные core-роли fill-primary/border-base). // - // Словарный канон labui#92: `fill-accent`/`fill-danger` — НЕ роли, а - // deprecation-алиасы на `badge-fill-brand`/`badge-fill-danger` (закон пары; - // labui_preset_aliases). `-tinted` остаётся РОЛЬЮ: ЗАЛИВКА при низкой альфе + // `-tinted` остаётся РОЛЬЮ: ЗАЛИВКА при низкой альфе // (тинт×альфа напрямую), то есть Ladder FillPrimary — тинт = якорь источника, // α = @12 (солид над белым дал бы α_min≈1 и «-tinted» перестал быть // полупрозрачным, поэтому Ladder, а не инверсия). @@ -341,31 +339,6 @@ pub fn labui_preset_roles() -> Vec<(String, RoleRecipe)> { brand_pos(LadderPosition::FocusRing), )); - // Frozen Pair frontend: PairFill эмитит exact source как opaque occurrence; - // PairLabel строит конечный candidate domain на фактически emitted Surface - // и проверяет его общим joint evaluator/recheck. Статики проходят тот же путь. - let pair = |source| RoleRecipe::PairFill { source }; - roles.push(("badge-fill-brand".to_string(), pair(LadderSource::Brand))); - for (client_name, family_key) in [ - ("danger", "red"), - ("warning", "orange"), - ("success", "green"), - ("info", "blue"), - ] { - roles.push(( - format!("badge-fill-{client_name}"), - pair(LadderSource::Family(family_key.to_string())), - )); - } - roles.push(( - "badge-fill-static-dark".to_string(), - pair(LadderSource::Neutral(NeutralPick::Dark)), - )); - roles.push(( - "badge-fill-static-light".to_string(), - pair(LadderSource::Neutral(NeutralPick::Light)), - )); - roles } @@ -375,8 +348,7 @@ pub fn labui_preset_roles() -> Vec<(String, RoleRecipe)> { /// Нейтральные компонент-роли, которые стаб алиасит через `var()` на /// нейтральные core-роли (одна истина, ноль дублирования значений): /// fill-neutral-tinted = var(--lab-fill-primary); border-neutral = -/// var(--lab-border-base). Плюс deprecation-алиасы канона #92: fill-accent/ -/// fill-danger → badge-fill-brand/danger (закон пары), icon → label-tertiary +/// var(--lab-border-base). Плюс алиасы канона #92: icon → label-tertiary /// (глиф красится Labels), border-ghost → border-none (честный ноль). Пресет /// наполняет роли И алиасы как единое целое. pub fn labui_preset_aliases() -> Vec<(String, String)> { @@ -391,10 +363,7 @@ pub fn labui_preset_aliases() -> Vec<(String, String)> { "fill-quaternary".to_string(), ), // Словарный канон labui#92 (порядок = passport.aliases labui; отпечаток - // тонкий==полный чувствителен к порядку). Акцент/данжер-заливки — закон - // пары; icon — глиф (label-tertiary); border-ghost — честный ноль. - ("fill-accent".to_string(), "badge-fill-brand".to_string()), - ("fill-danger".to_string(), "badge-fill-danger".to_string()), + // тонкий==полный чувствителен к порядку). ("icon".to_string(), "label-tertiary".to_string()), ("border-ghost".to_string(), "border-none".to_string()), ] diff --git a/crates/labcolors-core/src/config/tests.rs b/crates/labcolors-core/src/config/tests.rs index ad1397a9..bad8b291 100644 --- a/crates/labcolors-core/src/config/tests.rs +++ b/crates/labcolors-core/src/config/tests.rs @@ -647,9 +647,7 @@ const LABUI_CONSUMED_ROLES: &[&str] = &[ "fx-shadow-penumbra", "fx-shadow-major", // Component. - "fill-accent", "fill-neutral", - "fill-danger", "fill-accent-tinted", "fill-neutral-tinted", "fill-danger-tinted", @@ -659,14 +657,6 @@ const LABUI_CONSUMED_ROLES: &[&str] = &[ "border-neutral", "border-danger", "border-focus", - // Пары бейджа: exact opaque fill и joint-verified label на emitted Surface. - "badge-fill-brand", - "badge-fill-danger", - "badge-fill-warning", - "badge-fill-success", - "badge-fill-info", - "badge-fill-static-dark", - "badge-fill-static-light", // Прочие эмитируемые нейтральные (none — core; icon снят с контракта каноном // #92 — глиф красится label-tertiary; separator НЕ токен: бордер и сепаратор // едины, компонент применяет бордер-токен). @@ -716,14 +706,11 @@ const COLLAPSED_ROLES: &[(&str, &str)] = &[ "bg-overlay-*", "оверлеи → alpha.rs-роли (вне поглощаемого GAP)", ), - // Компонентные алиасы — конфиг-алиасы, не рецепты. Бейдж сузился законом - // пары: badge-fill-* — первоклассная эмиссия RoleRecipe::PairFill; - // label frontend использует фактически emitted fill Surface без зависимости - // по имени токена. - ( - "badge-label-*", - "лейбл бейджа — joint-verified foreground на emitted PairFill Surface", - ), + // Компонентная композиция принадлежит клиентскому Program, а не закрытому + // ролевому меню Core. + ("badge-*", "client-owned Program composition"), + ("fill-accent", "client-owned alias"), + ("fill-danger", "client-owned alias"), ("control-bg", "компонентный алиас, не рецепт эмиссии"), ]; @@ -896,8 +883,6 @@ fn renaming_family_id_and_references_does_not_change_the_compiled_graph() { } RoleRecipe::Ladder { source, .. } | RoleRecipe::Glow { source, .. } - | RoleRecipe::PairFill { source } - | RoleRecipe::PairLabel { source, .. } | RoleRecipe::Material { source, .. } => { rename_source(source, "red", "client-family-42"); } @@ -1310,42 +1295,6 @@ fn value_test_bites_on_alpha_mutation() { ); } -/// P1 не выводит representation из client-owned имени позиции и не двигает -/// authored source скрытой Pair-эвристикой. Во всех VC PairFill эмитит точный -/// выбранный source как opaque Paint; смена темы меняет только authored anchor. -#[test] -fn pair_fill_is_exact_opaque_source_across_viewing_conditions() { - let table = labui_reference().compile_named_role_table().unwrap(); - let cases = [ - (ViewingConditions::srgb(), "#FFFFFF", "#007AFF"), - (ViewingConditions::dim_surround(), "#101012", "#4A8FFF"), - ( - ViewingConditions::srgb_high_contrast(), - "#FFFFFF", - "#0040DD", - ), - ( - ViewingConditions::dim_surround_high_contrast(), - "#101012", - "#409CFF", - ), - ]; - - for (vc, background, expected_source) in cases { - let set = resolve_named_set(&BgInput::solid(background).unwrap(), &table, &vc) - .expect("валидная PairFill fixture обязана резолвиться"); - let (_, resolved) = set - .iter() - .find(|(name, _)| name == "badge-fill-brand") - .expect("паспорт несёт badge-fill-brand"); - let fill = resolved - .translucent() - .expect("PairFill эмитится общей rgba-формой"); - assert_eq!(fill.tint_hex(), expected_source); - assert_eq!(fill.alpha().to_bits(), 1.0_f64.to_bits()); - } -} - /// Дубликаты ключей всех словарей отвергаются (повтор имени = неоднозначный /// lookup), включая алиас, затеняющий роль. #[test] @@ -2016,10 +1965,10 @@ fn every_hue_consuming_path_preserves_achromatic_source_identity() { for (name, recipe) in &mut config.roles { match name.as_str() { "label-brand-secondary" => { - *recipe = RoleRecipe::PairLabel { - source: LadderSource::Brand, + *recipe = RoleRecipe::TextAnchor { fraction: 0.5, floor: Floor::AaUi, + hue: Some(LadderSource::Brand), }; } "fill-brand-secondary" => { @@ -2080,10 +2029,10 @@ fn nearest_chromatic_source_survives_every_current_source_consuming_path() { for (name, recipe) in &mut config.roles { match name.as_str() { "label-brand-secondary" => { - *recipe = RoleRecipe::PairLabel { - source: LadderSource::Brand, + *recipe = RoleRecipe::TextAnchor { fraction: 0.5, floor: Floor::AaUi, + hue: Some(LadderSource::Brand), }; } "fill-brand-secondary" => { @@ -2125,15 +2074,6 @@ fn nearest_chromatic_source_survives_every_current_source_consuming_path() { panic!("fill-brand-secondary must resolve to Material"); }; assert_chromatic_hex(material.base_hex(), "Material"); - - let (_, Resolved::Translucent(pair_fill)) = set - .iter() - .find(|(name, _)| name == "badge-fill-brand") - .expect("pair fill exists") - else { - panic!("badge-fill-brand must resolve to Translucent"); - }; - assert_chromatic_hex(pair_fill.tint_hex(), "PairFill"); } #[test] diff --git a/crates/labcolors-core/src/generic_boundary_tests.rs b/crates/labcolors-core/src/generic_boundary_tests.rs index a78bb86a..38e750e4 100644 --- a/crates/labcolors-core/src/generic_boundary_tests.rs +++ b/crates/labcolors-core/src/generic_boundary_tests.rs @@ -17,14 +17,12 @@ const GENERIC_SOURCES: [(&str, &str); 3] = [ ("program_session.rs", PROGRAM_SESSION_SOURCE), ]; -const CLIENT_OR_LEGACY_VOCABULARY: [&str; 15] = [ +const CLIENT_OR_LEGACY_VOCABULARY: [&str; 13] = [ "Lab UI", "ThemeConfig", "RoleRecipe", "RoleSpec", "NamedRoleTable", - "PairFill", - "PairLabel", "Glow", "Material", "Ladder", diff --git a/crates/labcolors-core/src/joint.rs b/crates/labcolors-core/src/joint.rs index bf1ed677..5188ac9d 100644 --- a/crates/labcolors-core/src/joint.rs +++ b/crates/labcolors-core/src/joint.rs @@ -680,14 +680,6 @@ impl JointExecutionRecordV1 { self.upper_paint } - pub(crate) const fn lower_occurrence(&self) -> &ResolvedOccurrence { - &self.lower - } - - pub(crate) const fn upper_occurrence(&self) -> &ResolvedOccurrence { - &self.upper - } - pub(crate) fn lower_visible(&self) -> Srgb8 { Srgb8::new(self.lower.visible()) } diff --git a/crates/labcolors-core/src/lib.rs b/crates/labcolors-core/src/lib.rs index a437281e..ec828672 100644 --- a/crates/labcolors-core/src/lib.rs +++ b/crates/labcolors-core/src/lib.rs @@ -36,7 +36,6 @@ pub mod numerical_plan; reason = "private F0 output-projection release firewall precedes the atomic public hard cut" )] pub(crate) mod output_projection; -pub(crate) mod pair; #[cfg_attr( not(test), expect( @@ -132,7 +131,7 @@ mod session_tests; not(test), expect( dead_code, - reason = "private V2a joint selection is production-compiled before Pair lowering or a public Program exists" + reason = "private V2a joint selection is production-compiled before a public Program exists" ) )] pub(crate) mod joint; @@ -162,9 +161,6 @@ mod continuity_tests; #[cfg(test)] mod dim_tinted_tests; -#[cfg(test)] -mod pair_label_tests; - #[cfg(test)] mod r3_byte_identity_tests; @@ -352,15 +348,6 @@ pub struct NoHybridLpcSurfaceMetric; #[cfg(doctest)] pub struct NoPrematureScalarLpcApi; -/// Frozen Pair frontend не публикует собственную физику или solver extension -/// point; Pair lowering остаётся private до общего Program cutover. -/// -/// ```compile_fail -/// use labcolors_core::pair::pair_side; -/// ``` -#[cfg(doctest)] -pub struct NoPublicPairRecipeApi; - /// C8d recheck and F2 observation remain one private Session-owned protocol; /// they do not create a second public authoring root before C7c. /// diff --git a/crates/labcolors-core/src/pair.rs b/crates/labcolors-core/src/pair.rs deleted file mode 100644 index e19f9348..00000000 --- a/crates/labcolors-core/src/pair.rs +++ /dev/null @@ -1,247 +0,0 @@ -//! P1: frozen Pair frontend поверх общей point graph algebra. -//! -//! Модуль не выбирает «сторону пары», не двигает цвет по Oklab и не содержит -//! собственного compositor-а или evaluator switch. Он только лоуверит -//! замороженный authoring tag в одну физическую цепочку и передаёт конечный -//! candidate domain общему joint engine. - -use crate::Srgb8; -use crate::appearance::{ - EncodedPointPaintV1, PaintId, PointOpacityOverSurfaceV1, ResolvedOccurrence, SurfaceInputPortId, -}; -use crate::composition::AdmittedOpacityV1; -use crate::constraints::{ExactSrgb8IdentityV1, Wcag22Srgb8V1}; -use crate::joint::{ - CandidateOrdinalV1, CandidateSetErrorV1, DeclaredTotalOrderV1, JointCandidateSetV1, - JointCandidateTupleV1, JointConstraintIdV1, JointExecutionRecordV1, JointProgramErrorV1, - JointVisibleTargetV1, PointwiseFullHardReportV1, PointwiseHardFeasibilityV1, - PointwiseJointHardConstraintV1, PointwiseJointPointProgramV1, PointwiseJointReportErrorV1, - PointwiseSelectedRecheckErrorV1, PointwiseVerifiedSelectionV1, SelectionPolicyErrorV1, - StaticJointObservationV1, -}; -use crate::wcag22::Wcag22CriterionV1; - -const ROOT_SURFACE: SurfaceInputPortId = SurfaceInputPortId::new(1); -const FILL_PAINT: PaintId = PaintId::new(1); -const LABEL_PAINT: PaintId = PaintId::new(2); -const LABEL_CONSTRAINT: JointConstraintIdV1 = JointConstraintIdV1::new(1); - -/// Один канонический fill Paint, действительно применённый к page Surface. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) struct LoweredPairFillV1 { - paint: EncodedPointPaintV1, - occurrence: ResolvedOccurrence, -} - -impl LoweredPairFillV1 { - pub(crate) const fn paint(self) -> EncodedPointPaintV1 { - self.paint - } - - pub(crate) const fn occurrence(&self) -> &ResolvedOccurrence { - &self.occurrence - } - - pub(crate) fn visible(self) -> Srgb8 { - Srgb8::new(self.occurrence.visible()) - } -} - -/// Один frontend-proposed label Paint. Ordinal задаёт только identity; порядок -/// предпочтения приходит отдельным declared total order. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) struct PairLabelCandidateV1 { - ordinal: CandidateOrdinalV1, - source: Srgb8, -} - -impl PairLabelCandidateV1 { - pub(crate) const fn new(ordinal: CandidateOrdinalV1, source: Srgb8) -> Self { - Self { ordinal, source } - } - - pub(crate) const fn source(self) -> Srgb8 { - self.source - } -} - -/// Hard requirement PairLabel. Отсутствие пола означает пустой hard-set, а не -/// тождественный `Exact(candidate, candidate)`. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum PairLabelRequirementV1 { - None, - Wcag22(Wcag22CriterionV1), -} - -#[derive(Debug, PartialEq)] -enum PairSelectionEvidenceV1 { - Unconstrained(PointwiseVerifiedSelectionV1), - Wcag22(PointwiseVerifiedSelectionV1), -} - -/// Полное fresh evidence одной выбранной Pair-кандидатуры. -#[derive(Debug, PartialEq)] -pub(crate) struct VerifiedPairV1 { - evidence: PairSelectionEvidenceV1, - execution: JointExecutionRecordV1, -} - -impl VerifiedPairV1 { - fn execution(&self) -> &JointExecutionRecordV1 { - &self.execution - } - - pub(crate) fn ordinal(&self) -> CandidateOrdinalV1 { - match &self.evidence { - PairSelectionEvidenceV1::Unconstrained(evidence) => evidence.ordinal(), - PairSelectionEvidenceV1::Wcag22(evidence) => evidence.ordinal(), - } - } - - pub(crate) fn fill_paint(&self) -> EncodedPointPaintV1 { - self.execution().lower_paint() - } - - pub(crate) fn label_paint(&self) -> EncodedPointPaintV1 { - self.execution().upper_paint() - } - - pub(crate) fn fill_occurrence(&self) -> &ResolvedOccurrence { - self.execution().lower_occurrence() - } - - pub(crate) fn label_occurrence(&self) -> &ResolvedOccurrence { - self.execution().upper_occurrence() - } -} - -#[derive(Debug, PartialEq)] -pub(crate) enum PairLoweringErrorV1 { - Candidate(CandidateSetErrorV1), - Program(JointProgramErrorV1), - ExactReport(PointwiseJointReportErrorV1), - WcagReport(PointwiseJointReportErrorV1), - Policy(SelectionPolicyErrorV1), - ExactInfeasible(Box>), - WcagInfeasible(Box>), - ExactRecheck(PointwiseSelectedRecheckErrorV1), - WcagRecheck(PointwiseSelectedRecheckErrorV1), - InternalInvariant, -} - -/// Материализовать fill occurrence без role-specific эвристики. -pub(crate) fn lower_fill( - source: Srgb8, - opacity: AdmittedOpacityV1, - backdrop: Srgb8, -) -> LoweredPairFillV1 { - let paint = EncodedPointPaintV1::from_admitted(FILL_PAINT, source, opacity); - let occurrence = - PointOpacityOverSurfaceV1::evaluate_admitted(source.bytes(), opacity, backdrop.bytes()); - LoweredPairFillV1 { paint, occurrence } -} - -/// Выбрать один label Paint из полного frontend candidate domain. Fill Paint -/// фиксирован authoring representation-ом, но каждый tuple исполняет обе -/// linked occurrences; hard report и fresh recheck используют тот же kernel. -pub(crate) fn select_label_candidates( - fill_source: Srgb8, - fill_opacity: AdmittedOpacityV1, - candidates: Vec, - declared_order: Vec, - backdrop: Srgb8, - requirement: PairLabelRequirementV1, -) -> Result { - let tuples = candidates - .into_iter() - .map(|candidate| { - JointCandidateTupleV1::new( - candidate.ordinal, - EncodedPointPaintV1::from_admitted(FILL_PAINT, fill_source, fill_opacity), - EncodedPointPaintV1::from_admitted( - LABEL_PAINT, - candidate.source, - AdmittedOpacityV1::OPAQUE, - ), - ) - }) - .collect(); - let candidates = JointCandidateSetV1::new(tuples).map_err(PairLoweringErrorV1::Candidate)?; - let policy = DeclaredTotalOrderV1::new(&candidates, declared_order) - .map_err(PairLoweringErrorV1::Policy)?; - let observation = StaticJointObservationV1::one_case(ROOT_SURFACE, backdrop); - - match requirement { - PairLabelRequirementV1::None => { - let program = PointwiseJointPointProgramV1::with_evaluator( - ExactSrgb8IdentityV1, - ROOT_SURFACE, - FILL_PAINT, - LABEL_PAINT, - Vec::new(), - ) - .map_err(PairLoweringErrorV1::Program)?; - let report = program - .evaluate_static(candidates, observation) - .map_err(PairLoweringErrorV1::ExactReport)?; - let feasible = match report.classify() { - PointwiseHardFeasibilityV1::NonEmpty(feasible) => feasible, - PointwiseHardFeasibilityV1::Infeasible(report) => { - return Err(PairLoweringErrorV1::ExactInfeasible(Box::new(report))); - } - }; - let verified = feasible - .select(policy) - .map_err(|failure| PairLoweringErrorV1::Policy(failure.reason()))? - .recheck() - .map_err(PairLoweringErrorV1::ExactRecheck)?; - let execution = verified - .fresh_executions() - .first() - .copied() - .ok_or(PairLoweringErrorV1::InternalInvariant)?; - Ok(VerifiedPairV1 { - evidence: PairSelectionEvidenceV1::Unconstrained(verified), - execution, - }) - } - PairLabelRequirementV1::Wcag22(criterion) => { - let constraints = vec![PointwiseJointHardConstraintV1::new( - LABEL_CONSTRAINT, - JointVisibleTargetV1::Upper, - criterion, - )]; - let program = PointwiseJointPointProgramV1::with_evaluator( - Wcag22Srgb8V1, - ROOT_SURFACE, - FILL_PAINT, - LABEL_PAINT, - constraints, - ) - .map_err(PairLoweringErrorV1::Program)?; - let report = program - .evaluate_static(candidates, observation) - .map_err(PairLoweringErrorV1::WcagReport)?; - let feasible = match report.classify() { - PointwiseHardFeasibilityV1::NonEmpty(feasible) => feasible, - PointwiseHardFeasibilityV1::Infeasible(report) => { - return Err(PairLoweringErrorV1::WcagInfeasible(Box::new(report))); - } - }; - let verified = feasible - .select(policy) - .map_err(|failure| PairLoweringErrorV1::Policy(failure.reason()))? - .recheck() - .map_err(PairLoweringErrorV1::WcagRecheck)?; - let execution = verified - .fresh_executions() - .first() - .copied() - .ok_or(PairLoweringErrorV1::InternalInvariant)?; - Ok(VerifiedPairV1 { - evidence: PairSelectionEvidenceV1::Wcag22(verified), - execution, - }) - } - } -} diff --git a/crates/labcolors-core/src/pair_label_tests.rs b/crates/labcolors-core/src/pair_label_tests.rs deleted file mode 100644 index b3371a2b..00000000 --- a/crates/labcolors-core/src/pair_label_tests.rs +++ /dev/null @@ -1,174 +0,0 @@ -//! P1 Pair frontend: emitted fill Surface, joint hard selection and anti-vacuum. - -use crate::Srgb8; -use crate::composition::AdmittedOpacityV1; -use crate::config::fixture::labui_reference; -use crate::config::{LadderSource, RoleRecipe}; -use crate::joint::CandidateOrdinalV1; -use crate::pair::{ - PairLabelCandidateV1, PairLabelRequirementV1, lower_fill, select_label_candidates, -}; -use crate::semantic::{NamedRoleTable, RoleChroma, RoleSpec, resolve_named_set}; -use crate::solve::{BgInput, Floor, SolveFailure}; -use crate::spaces::vc::ViewingConditions; -use crate::wcag22::Wcag22CriterionV1; - -fn enc(hex: &str) -> [f64; 3] { - crate::spaces::srgb::srgb_encoded_from_hex(hex).unwrap() -} - -#[test] -fn fill_is_one_exact_opaque_occurrence_without_pair_heuristic() { - let source = Srgb8::new([0x00, 0x7A, 0xFF]); - let backdrop = Srgb8::new([0xFF; 3]); - let fill = lower_fill(source, AdmittedOpacityV1::OPAQUE, backdrop); - - assert_eq!(fill.paint().source(), source); - assert_eq!(fill.paint().opacity(), AdmittedOpacityV1::OPAQUE); - assert_eq!(fill.visible(), source); - assert_eq!( - fill.occurrence().certificate().backdrop_rgb(), - backdrop.bytes() - ); -} - -#[test] -fn wcag_joint_selection_rejects_preferred_tuple_and_selects_legal_tuple() { - let dark_fill = Srgb8::new([0x10; 3]); - let preferred_dark = Srgb8::new([0x20; 3]); - let legal_light = Srgb8::new([0xFF; 3]); - let verified = select_label_candidates( - dark_fill, - AdmittedOpacityV1::OPAQUE, - vec![ - PairLabelCandidateV1::new(CandidateOrdinalV1::new(1), preferred_dark), - PairLabelCandidateV1::new(CandidateOrdinalV1::new(2), legal_light), - ], - vec![CandidateOrdinalV1::new(1), CandidateOrdinalV1::new(2)], - Srgb8::new([0xEE; 3]), - PairLabelRequirementV1::Wcag22(Wcag22CriterionV1::Sc143TextDefault), - ) - .unwrap(); - - assert_eq!(verified.ordinal(), CandidateOrdinalV1::new(2)); - assert_eq!(verified.fill_occurrence().visible(), dark_fill.bytes()); - assert_eq!(verified.label_occurrence().visible(), legal_light.bytes()); - assert_eq!( - verified.label_occurrence().certificate().backdrop_rgb(), - dark_fill.bytes() - ); -} - -#[test] -fn pair_fill_and_pair_label_share_the_same_emitted_surface() { - let mut config = labui_reference(); - config.roles.push(( - "badge-label-brand".into(), - RoleRecipe::PairLabel { - source: LadderSource::Brand, - fraction: 0.461, - floor: Floor::AaUi, - }, - )); - let table = config.compile_named_role_table().unwrap(); - - for (background, vc) in [ - ("#FFFFFF", ViewingConditions::srgb()), - ("#101012", ViewingConditions::dim_surround()), - ] { - let set = resolve_named_set(&BgInput::solid(background).unwrap(), &table, &vc).unwrap(); - let fill = set - .iter() - .find(|(name, _)| name == "badge-fill-brand") - .unwrap() - .1 - .translucent() - .unwrap(); - let label = set - .iter() - .find(|(name, _)| name == "badge-label-brand") - .unwrap() - .1 - .solved() - .unwrap(); - - assert_eq!(fill.alpha().to_bits(), 1.0_f64.to_bits()); - assert_eq!(fill.tint_hex(), fill.composite_hex()); - let ratio = crate::wcag::contrast_ratio(enc(label.hex()), enc(fill.composite_hex())); - assert!(ratio >= 3.0, "{background}: got {ratio}"); - } -} - -#[test] -fn pair_result_is_independent_of_client_role_names() { - let blue = [0.0, 0.47843137254901963, 1.0]; - let tint = crate::ladder::LadderTint::new([blue; 4]).unwrap(); - let entries = |fill: &str, label: &str| { - vec![ - (fill.into(), RoleSpec::PairFill { tint }), - ( - label.into(), - RoleSpec::PairLabel { - tint, - fraction: 0.461, - floor: Floor::AaUi, - surface_alpha_light: 1.0, - surface_alpha_dark: 1.0, - }, - ), - ] - }; - let left = NamedRoleTable::new(entries("x", "y"), Vec::new(), RoleChroma::Neutral).unwrap(); - let right = NamedRoleTable::new( - entries("danger-primary", "surface-hover"), - Vec::new(), - RoleChroma::Neutral, - ) - .unwrap(); - let bg = BgInput::solid("#FFFFFF").unwrap(); - let a = resolve_named_set(&bg, &left, &ViewingConditions::srgb()).unwrap(); - let b = resolve_named_set(&bg, &right, &ViewingConditions::srgb()).unwrap(); - - assert_eq!(a[0].1, b[0].1); - assert_eq!(a[1].1, b[1].1); -} - -#[test] -fn nonopaque_pairlabel_transport_is_rejected_before_execution() { - let tint = crate::ladder::LadderTint::new([[0.0, 0.0, 0.0]; 4]).unwrap(); - let error = NamedRoleTable::new( - vec![( - "label".into(), - RoleSpec::PairLabel { - tint, - fraction: 0.5, - floor: Floor::AaUi, - surface_alpha_light: 0.122, - surface_alpha_dark: 0.122, - }, - )], - Vec::new(), - RoleChroma::Neutral, - ) - .unwrap_err(); - - assert!(matches!(error, SolveFailure::InvalidInput(message) if message.contains("opaque"))); -} - -#[test] -fn floor_none_has_no_tautological_exact_constraint() { - let verified = select_label_candidates( - Srgb8::new([0x80; 3]), - AdmittedOpacityV1::OPAQUE, - vec![PairLabelCandidateV1::new( - CandidateOrdinalV1::new(1), - Srgb8::new([0x81; 3]), - )], - vec![CandidateOrdinalV1::new(1)], - Srgb8::new([0x00; 3]), - PairLabelRequirementV1::None, - ) - .unwrap(); - - assert_eq!(verified.ordinal(), CandidateOrdinalV1::new(1)); -} diff --git a/crates/labcolors-core/src/semantic.rs b/crates/labcolors-core/src/semantic.rs index 00ad0bad..033f1768 100644 --- a/crates/labcolors-core/src/semantic.rs +++ b/crates/labcolors-core/src/semantic.rs @@ -546,29 +546,6 @@ pub enum RoleSpec { /// path. Wire-ключ — его boundary-проекция, а не декоративный metadata. mode: crate::numerical_plan::NumericalExecutionModeV1, }, - /// Frozen PairFill frontend до C7c. Источник становится opaque Paint; - /// единственный source-over occurrence создаёт фактически emitted Surface. - /// Отдельной Pair-эвристики, собственного compositor-а и выбора стороны нет. - PairFill { - /// Пер-темный кодированный якорь источника. - tint: LadderTint, - }, - /// Frozen PairLabel frontend до C7c. Кандидаты label Paint проверяются на - /// фактически emitted opaque PairFill Surface общим joint evaluator/recheck. - /// Поля alpha сохраняют замороженную форму публичного RoleSpec, но после P1 - /// обязаны быть opaque: представление не выводится из client role names. - PairLabel { - /// Пер-темный кодированный якорь источника fill и hue-направления label. - tint: LadderTint, - /// Доля максимума контраста PairFill Surface `(0, 1]`. - fraction: f64, - /// Hard floor против emitted PairFill Surface. - floor: Floor, - /// Замороженное поле representation light; P1 принимает только `1.0`. - surface_alpha_light: f64, - /// Замороженное поле representation dark; P1 принимает только `1.0`. - surface_alpha_dark: f64, - }, Ladder { /// Пер-темный кодированный тинт (якорь источника). tint: LadderTint, @@ -2153,26 +2130,6 @@ fn resolve_spec_in( }); } RoleSpec::Decorative { magnitude } => ctx.decorative_contract(magnitude), - RoleSpec::PairFill { tint } => { - return lower_pair_fill_frontend(bg, tint, vc); - } - RoleSpec::PairLabel { - tint, - fraction, - floor, - surface_alpha_light, - surface_alpha_dark, - } => { - return lower_pair_label_frontend( - bg, - tint, - fraction, - floor, - surface_alpha_light, - surface_alpha_dark, - vc, - ); - } RoleSpec::Ladder { tint, alpha_light, @@ -2590,196 +2547,6 @@ fn resolve_solid_with_ui_floor( } } -/// Exact page background as the root Surface of the Pair point graph. -fn pair_root_surface(bg: &BgInput) -> Result { - crate::alpha::encoded_to_srgb8(bg.encoded_display(), "pair page background") - .map(Srgb8::new) - .map_err(|error| { - SolveFailure::InternalInvariant(format!( - "validated Pair background left encoded-sRGB8 domain: {error}" - )) - }) -} - -/// Frozen Pair representation is opaque. The old PairSide/Oklab adjustment and -/// the hidden FillPrimary opacity are both absent: representation no longer -/// depends on a client vocabulary term. -fn lower_pair_fill_occurrence( - bg: &BgInput, - tint: LadderTint, - vc: &ViewingConditions, -) -> Result { - let source = tint.srgb8_for_vc(vc); - let backdrop = pair_root_surface(bg)?; - Ok(crate::pair::lower_fill( - source, - crate::composition::AdmittedOpacityV1::OPAQUE, - backdrop, - )) -} - -fn lower_pair_fill_frontend( - bg: &BgInput, - tint: LadderTint, - vc: &ViewingConditions, -) -> PendingResolution { - let fill = lower_pair_fill_occurrence(bg, tint, vc)?; - finish_rgba_from_certificate( - fill.paint().source(), - crate::composition::AdmittedOpacityV1::OPAQUE.value(), - fill.occurrence().certificate(), - vc, - false, - false, - ) -} - -fn pair_requirement(floor: Floor) -> crate::pair::PairLabelRequirementV1 { - match floor { - Floor::AaText => crate::pair::PairLabelRequirementV1::Wcag22( - crate::wcag22::Wcag22CriterionV1::Sc143TextDefault, - ), - Floor::AaUi => crate::pair::PairLabelRequirementV1::Wcag22( - crate::wcag22::Wcag22CriterionV1::Sc1411UiComponentOrState, - ), - Floor::None => crate::pair::PairLabelRequirementV1::None, - } -} - -fn resolved_label_bytes(resolved: &Resolved) -> Result { - let Resolved::Color { solved, .. } = resolved else { - return Err(SolveFailure::InternalInvariant( - "Pair label proposal did not produce a Color".into(), - )); - }; - crate::srgb8::hex_bytes(solved.hex()) - .map(Srgb8::new) - .map_err(|error| { - SolveFailure::InternalInvariant(format!( - "Pair label solver emitted invalid sRGB8: {error}" - )) - }) -} - -fn pair_candidate( - surface_bg: &BgInput, - fraction: f64, - floor: Floor, - source: Srgb8, - vc: &ViewingConditions, - surface_ctx: &ResolveContext, -) -> Result<(Resolved, Srgb8), SolveFailure> { - let resolved = resolve_hued_anchor_from_srgb8( - surface_bg, - TextAnchor::new(fraction, floor)?, - source, - vc, - surface_ctx, - )?; - let bytes = resolved_label_bytes(&resolved)?; - Ok((resolved, bytes)) -} - -/// PairLabel proposes the exact authored fraction first, then a floor-aware -/// fallback. The common joint engine—not the proposal stage—forms the feasible -/// set, applies the declared order and performs fresh final-occurrence recheck. -#[allow(clippy::too_many_arguments)] -fn lower_pair_label_frontend( - bg: &BgInput, - tint: LadderTint, - fraction: f64, - floor: Floor, - surface_alpha_light: f64, - surface_alpha_dark: f64, - vc: &ViewingConditions, -) -> PendingResolution { - if surface_alpha_light.to_bits() != 1.0_f64.to_bits() - || surface_alpha_dark.to_bits() != 1.0_f64.to_bits() - { - return Err(SolveFailure::InvalidInput( - "PairLabel surface representation must be opaque after P1".into(), - )); - } - - let source = tint.srgb8_for_vc(vc); - let backdrop = pair_root_surface(bg)?; - let fill = lower_pair_fill_occurrence(bg, tint, vc)?; - let surface = fill.visible(); - let surface_bg = BgInput::solid(&surface.to_hex()).map_err(|error| { - SolveFailure::InternalInvariant(format!("generated PairFill Surface was rejected: {error}")) - })?; - let surface_ctx = ResolveContext::new(&surface_bg, vc); - - let mut resolved_candidates = Vec::new(); - let mut physical_candidates = Vec::new(); - let mut order = Vec::new(); - - match pair_candidate(&surface_bg, fraction, Floor::None, source, vc, &surface_ctx) { - Ok((resolved, bytes)) => { - let ordinal = crate::joint::CandidateOrdinalV1::new(1); - resolved_candidates.push((ordinal, resolved)); - physical_candidates.push(crate::pair::PairLabelCandidateV1::new(ordinal, bytes)); - order.push(ordinal); - } - Err(error) if matches!(floor, Floor::None) => return Err(error), - Err(error) if error.boundary().is_none() => return Err(error), - Err(_) => {} - } - - if !matches!(floor, Floor::None) { - let (resolved, bytes) = - pair_candidate(&surface_bg, fraction, floor, source, vc, &surface_ctx)?; - if physical_candidates - .iter() - .all(|candidate| candidate.source() != bytes) - { - let ordinal = crate::joint::CandidateOrdinalV1::new(2); - resolved_candidates.push((ordinal, resolved)); - physical_candidates.push(crate::pair::PairLabelCandidateV1::new(ordinal, bytes)); - order.push(ordinal); - } - } - - if physical_candidates.is_empty() { - return Err(SolveFailure::InternalInvariant( - "Pair frontend produced an empty candidate domain".into(), - )); - } - - let verified = crate::pair::select_label_candidates( - source, - crate::composition::AdmittedOpacityV1::OPAQUE, - physical_candidates, - order, - backdrop, - pair_requirement(floor), - ) - .map_err(|error| { - SolveFailure::InternalInvariant(format!( - "Pair joint selection failed after typed proposal admission: {error:?}" - )) - })?; - - if verified.fill_occurrence() != fill.occurrence() - || verified.fill_paint() != fill.paint() - || verified.label_occurrence().visible() != verified.label_paint().source().bytes() - { - return Err(SolveFailure::InternalInvariant( - "Pair joint evidence drifted from the selected physical chain".into(), - )); - } - - resolved_candidates - .into_iter() - .find(|(ordinal, _)| *ordinal == verified.ordinal()) - .map(|(_, resolved)| resolved) - .ok_or_else(|| { - SolveFailure::InternalInvariant( - "Pair selection returned an ordinal outside the proposal domain".into(), - ) - }) -} - /// Альфа-аналог: солид-цель `solid` (кодированный, по теме) на фоне резолва /// инвертируется в `(tint, фактическая α)`. Перед инверсией цель квантуется до /// эмитируемой sRGB8-сетки; production-композитор обязан побайтно вернуть её. @@ -3308,10 +3075,7 @@ impl RoleSpec { }; match self { - RoleSpec::Anchor(_) - | RoleSpec::Glow { .. } - | RoleSpec::PairFill { .. } - | RoleSpec::Zero => Ok(()), + RoleSpec::Anchor(_) | RoleSpec::Glow { .. } | RoleSpec::Zero => Ok(()), RoleSpec::DecorativeDj { magnitude_dj } => { positive("decorative dJ light magnitude", magnitude_dj.light())?; positive("decorative dJ dark magnitude", magnitude_dj.dark()) @@ -3325,26 +3089,6 @@ impl RoleSpec { )) } } - RoleSpec::PairLabel { - fraction, - surface_alpha_light, - surface_alpha_dark, - .. - } => { - if !fraction.is_finite() || fraction <= 0.0 || fraction > 1.0 { - return Err(format!( - "pair-label fraction must be finite and inside (0, 1], got {fraction}" - )); - } - alpha("pair-label light surface alpha", surface_alpha_light)?; - alpha("pair-label dark surface alpha", surface_alpha_dark)?; - if surface_alpha_light.to_bits() != 1.0_f64.to_bits() - || surface_alpha_dark.to_bits() != 1.0_f64.to_bits() - { - return Err("pair-label surface representation must be opaque after P1".into()); - } - Ok(()) - } RoleSpec::Ladder { alpha_light, alpha_dark, @@ -3409,10 +3153,6 @@ impl RoleSpec { pub fn legal_floor(&self) -> Option { match self { RoleSpec::Anchor(anchor) => anchor.conformance().min_ratio(), - // Лейбл тинт-бейджа несёт свой пол против тинт-поверхности — семантика - // контракта, как у текст/UI-якоря (иерархия-пасс его не трогает: он - // singleton, не ступень лестницы). - RoleSpec::PairLabel { floor, .. } => floor.min_ratio(), _ => None, } } @@ -4627,13 +4367,6 @@ mod tests { #[test] fn named_table_validates_every_raw_role_field_before_resolution() { let tint = LadderTint::new([[0.25, 0.5, 0.75]; 4]).unwrap(); - let pair = |fraction, light, dark| RoleSpec::PairLabel { - tint, - fraction, - floor: Floor::AaText, - surface_alpha_light: light, - surface_alpha_dark: dark, - }; let ladder = |light, dark| RoleSpec::Ladder { tint, alpha_light: light, @@ -4658,10 +4391,6 @@ mod tests { RoleSpec::DecorativeDj { magnitude_dj: DjMagnitude::new(1.0, 0.0), }, - pair(f64::NAN, 0.5, 0.5), - pair(0.5, 0.0, 0.5), - pair(0.5, 0.5, f64::INFINITY), - pair(0.5, 0.25, 1.0), ladder(0.0, 0.5), ladder(0.5, f64::NAN), ladder_with_floor(1.0, 1.0, Some(Floor::None)), @@ -4689,7 +4418,6 @@ mod tests { RoleSpec::DecorativeDj { magnitude_dj: DjMagnitude::new(1.0, 2.0), }, - pair(0.5, 1.0, 1.0), ladder(0.25, 1.0), ladder_with_floor(1.0, 1.0, Some(Floor::AaUi)), RoleSpec::AlphaAnalog { diff --git a/crates/labcolors-core/tests/data/labui_emission_golden.txt b/crates/labcolors-core/tests/data/labui_emission_golden.txt index 30d0a3c3..183c13ff 100644 --- a/crates/labcolors-core/tests/data/labui_emission_golden.txt +++ b/crates/labcolors-core/tests/data/labui_emission_golden.txt @@ -91,18 +91,9 @@ srgb|#FFFFFF|label-danger=rgba(#FF3B30,1) srgb|#FFFFFF|border-accent=rgba(#007AFF,0.2) srgb|#FFFFFF|border-danger=rgba(#FF3B30,0.2) srgb|#FFFFFF|border-focus=rgba(#007AFF,1) -srgb|#FFFFFF|badge-fill-brand=rgba(#007AFF,1) -srgb|#FFFFFF|badge-fill-danger=rgba(#FF3B30,1) -srgb|#FFFFFF|badge-fill-warning=rgba(#FFA100,1) -srgb|#FFFFFF|badge-fill-success=rgba(#34C759,1) -srgb|#FFFFFF|badge-fill-info=rgba(#3E87FF,1) -srgb|#FFFFFF|badge-fill-static-dark=rgba(#101012,1) -srgb|#FFFFFF|badge-fill-static-light=rgba(#FFFFFF,1) srgb|#FFFFFF|fill-neutral-tinted->fill-primary srgb|#FFFFFF|border-neutral->border-base srgb|#FFFFFF|fx-skeleton-base->fill-quaternary -srgb|#FFFFFF|fill-accent->badge-fill-brand -srgb|#FFFFFF|fill-danger->badge-fill-danger srgb|#FFFFFF|icon->label-tertiary srgb|#FFFFFF|border-ghost->border-none srgb|#F2F2F7|bg-tone-2=#EBEBF5 @@ -198,18 +189,9 @@ srgb|#F2F2F7|label-danger=rgba(#FF3B30,1) srgb|#F2F2F7|border-accent=rgba(#007AFF,0.2) srgb|#F2F2F7|border-danger=rgba(#FF3B30,0.2) srgb|#F2F2F7|border-focus=rgba(#007AFF,1) -srgb|#F2F2F7|badge-fill-brand=rgba(#007AFF,1) -srgb|#F2F2F7|badge-fill-danger=rgba(#FF3B30,1) -srgb|#F2F2F7|badge-fill-warning=rgba(#FFA100,1) -srgb|#F2F2F7|badge-fill-success=rgba(#34C759,1) -srgb|#F2F2F7|badge-fill-info=rgba(#3E87FF,1) -srgb|#F2F2F7|badge-fill-static-dark=rgba(#101012,1) -srgb|#F2F2F7|badge-fill-static-light=rgba(#FFFFFF,1) srgb|#F2F2F7|fill-neutral-tinted->fill-primary srgb|#F2F2F7|border-neutral->border-base srgb|#F2F2F7|fx-skeleton-base->fill-quaternary -srgb|#F2F2F7|fill-accent->badge-fill-brand -srgb|#F2F2F7|fill-danger->badge-fill-danger srgb|#F2F2F7|icon->label-tertiary srgb|#F2F2F7|border-ghost->border-none srgb|#7F7F7F|bg-tone-2=#797981 @@ -305,18 +287,9 @@ srgb|#7F7F7F|label-danger=rgba(#FF3B30,1) srgb|#7F7F7F|border-accent=rgba(#007AFF,0.2) srgb|#7F7F7F|border-danger=rgba(#FF3B30,0.2) srgb|#7F7F7F|border-focus=rgba(#007AFF,1) -srgb|#7F7F7F|badge-fill-brand=rgba(#007AFF,1) -srgb|#7F7F7F|badge-fill-danger=rgba(#FF3B30,1) -srgb|#7F7F7F|badge-fill-warning=rgba(#FFA100,1) -srgb|#7F7F7F|badge-fill-success=rgba(#34C759,1) -srgb|#7F7F7F|badge-fill-info=rgba(#3E87FF,1) -srgb|#7F7F7F|badge-fill-static-dark=rgba(#101012,1) -srgb|#7F7F7F|badge-fill-static-light=rgba(#FFFFFF,1) srgb|#7F7F7F|fill-neutral-tinted->fill-primary srgb|#7F7F7F|border-neutral->border-base srgb|#7F7F7F|fx-skeleton-base->fill-quaternary -srgb|#7F7F7F|fill-accent->badge-fill-brand -srgb|#7F7F7F|fill-danger->badge-fill-danger srgb|#7F7F7F|icon->label-tertiary srgb|#7F7F7F|border-ghost->border-none srgb|#1C1C1E|bg-tone-2=#202028 @@ -412,18 +385,9 @@ srgb|#1C1C1E|label-danger=rgba(#FF3B30,1) srgb|#1C1C1E|border-accent=rgba(#007AFF,0.2) srgb|#1C1C1E|border-danger=rgba(#FF3B30,0.2) srgb|#1C1C1E|border-focus=rgba(#007AFF,1) -srgb|#1C1C1E|badge-fill-brand=rgba(#007AFF,1) -srgb|#1C1C1E|badge-fill-danger=rgba(#FF3B30,1) -srgb|#1C1C1E|badge-fill-warning=rgba(#FFA100,1) -srgb|#1C1C1E|badge-fill-success=rgba(#34C759,1) -srgb|#1C1C1E|badge-fill-info=rgba(#3E87FF,1) -srgb|#1C1C1E|badge-fill-static-dark=rgba(#101012,1) -srgb|#1C1C1E|badge-fill-static-light=rgba(#FFFFFF,1) srgb|#1C1C1E|fill-neutral-tinted->fill-primary srgb|#1C1C1E|border-neutral->border-base srgb|#1C1C1E|fx-skeleton-base->fill-quaternary -srgb|#1C1C1E|fill-accent->badge-fill-brand -srgb|#1C1C1E|fill-danger->badge-fill-danger srgb|#1C1C1E|icon->label-tertiary srgb|#1C1C1E|border-ghost->border-none srgb|#101012|bg-tone-2=#15151B @@ -519,18 +483,9 @@ srgb|#101012|label-danger=rgba(#FF3B30,1) srgb|#101012|border-accent=rgba(#007AFF,0.2) srgb|#101012|border-danger=rgba(#FF3B30,0.2) srgb|#101012|border-focus=rgba(#007AFF,1) -srgb|#101012|badge-fill-brand=rgba(#007AFF,1) -srgb|#101012|badge-fill-danger=rgba(#FF3B30,1) -srgb|#101012|badge-fill-warning=rgba(#FFA100,1) -srgb|#101012|badge-fill-success=rgba(#34C759,1) -srgb|#101012|badge-fill-info=rgba(#3E87FF,1) -srgb|#101012|badge-fill-static-dark=rgba(#101012,1) -srgb|#101012|badge-fill-static-light=rgba(#FFFFFF,1) srgb|#101012|fill-neutral-tinted->fill-primary srgb|#101012|border-neutral->border-base srgb|#101012|fx-skeleton-base->fill-quaternary -srgb|#101012|fill-accent->badge-fill-brand -srgb|#101012|fill-danger->badge-fill-danger srgb|#101012|icon->label-tertiary srgb|#101012|border-ghost->border-none srgb|#3478F6|bg-tone-2=#797981 @@ -626,18 +581,9 @@ srgb|#3478F6|label-danger=rgba(#FF3B30,1) srgb|#3478F6|border-accent=rgba(#007AFF,0.2) srgb|#3478F6|border-danger=rgba(#FF3B30,0.2) srgb|#3478F6|border-focus=rgba(#007AFF,1) -srgb|#3478F6|badge-fill-brand=rgba(#007AFF,1) -srgb|#3478F6|badge-fill-danger=rgba(#FF3B30,1) -srgb|#3478F6|badge-fill-warning=rgba(#FFA100,1) -srgb|#3478F6|badge-fill-success=rgba(#34C759,1) -srgb|#3478F6|badge-fill-info=rgba(#3E87FF,1) -srgb|#3478F6|badge-fill-static-dark=rgba(#101012,1) -srgb|#3478F6|badge-fill-static-light=rgba(#FFFFFF,1) srgb|#3478F6|fill-neutral-tinted->fill-primary srgb|#3478F6|border-neutral->border-base srgb|#3478F6|fx-skeleton-base->fill-quaternary -srgb|#3478F6|fill-accent->badge-fill-brand -srgb|#3478F6|fill-danger->badge-fill-danger srgb|#3478F6|icon->label-tertiary srgb|#3478F6|border-ghost->border-none dim|#FFFFFF|bg-tone-2=#E7E7F1 @@ -733,18 +679,9 @@ dim|#FFFFFF|label-danger=rgba(#FF3A3A,1) dim|#FFFFFF|border-accent=rgba(#4A8FFF,0.2) dim|#FFFFFF|border-danger=rgba(#FF3A3A,0.2) dim|#FFFFFF|border-focus=rgba(#4A8FFF,1) -dim|#FFFFFF|badge-fill-brand=rgba(#4A8FFF,1) -dim|#FFFFFF|badge-fill-danger=rgba(#FF3A3A,1) -dim|#FFFFFF|badge-fill-warning=rgba(#FF9008,1) -dim|#FFFFFF|badge-fill-success=rgba(#30D158,1) -dim|#FFFFFF|badge-fill-info=rgba(#5696FF,1) -dim|#FFFFFF|badge-fill-static-dark=rgba(#101012,1) -dim|#FFFFFF|badge-fill-static-light=rgba(#FFFFFF,1) dim|#FFFFFF|fill-neutral-tinted->fill-primary dim|#FFFFFF|border-neutral->border-base dim|#FFFFFF|fx-skeleton-base->fill-quaternary -dim|#FFFFFF|fill-accent->badge-fill-brand -dim|#FFFFFF|fill-danger->badge-fill-danger dim|#FFFFFF|icon->label-tertiary dim|#FFFFFF|border-ghost->border-none dim|#F2F2F7|bg-tone-2=#DBDBE5 @@ -840,18 +777,9 @@ dim|#F2F2F7|label-danger=rgba(#FF3A3A,1) dim|#F2F2F7|border-accent=rgba(#4A8FFF,0.2) dim|#F2F2F7|border-danger=rgba(#FF3A3A,0.2) dim|#F2F2F7|border-focus=rgba(#4A8FFF,1) -dim|#F2F2F7|badge-fill-brand=rgba(#4A8FFF,1) -dim|#F2F2F7|badge-fill-danger=rgba(#FF3A3A,1) -dim|#F2F2F7|badge-fill-warning=rgba(#FF9008,1) -dim|#F2F2F7|badge-fill-success=rgba(#30D158,1) -dim|#F2F2F7|badge-fill-info=rgba(#5696FF,1) -dim|#F2F2F7|badge-fill-static-dark=rgba(#101012,1) -dim|#F2F2F7|badge-fill-static-light=rgba(#FFFFFF,1) dim|#F2F2F7|fill-neutral-tinted->fill-primary dim|#F2F2F7|border-neutral->border-base dim|#F2F2F7|fx-skeleton-base->fill-quaternary -dim|#F2F2F7|fill-accent->badge-fill-brand -dim|#F2F2F7|fill-danger->badge-fill-danger dim|#F2F2F7|icon->label-tertiary dim|#F2F2F7|border-ghost->border-none dim|#7F7F7F|bg-tone-2=#6F6F77 @@ -947,18 +875,9 @@ dim|#7F7F7F|label-danger=rgba(#FF3A3A,1) dim|#7F7F7F|border-accent=rgba(#4A8FFF,0.2) dim|#7F7F7F|border-danger=rgba(#FF3A3A,0.2) dim|#7F7F7F|border-focus=rgba(#4A8FFF,1) -dim|#7F7F7F|badge-fill-brand=rgba(#4A8FFF,1) -dim|#7F7F7F|badge-fill-danger=rgba(#FF3A3A,1) -dim|#7F7F7F|badge-fill-warning=rgba(#FF9008,1) -dim|#7F7F7F|badge-fill-success=rgba(#30D158,1) -dim|#7F7F7F|badge-fill-info=rgba(#5696FF,1) -dim|#7F7F7F|badge-fill-static-dark=rgba(#101012,1) -dim|#7F7F7F|badge-fill-static-light=rgba(#FFFFFF,1) dim|#7F7F7F|fill-neutral-tinted->fill-primary dim|#7F7F7F|border-neutral->border-base dim|#7F7F7F|fx-skeleton-base->fill-quaternary -dim|#7F7F7F|fill-accent->badge-fill-brand -dim|#7F7F7F|fill-danger->badge-fill-danger dim|#7F7F7F|icon->label-tertiary dim|#7F7F7F|border-ghost->border-none dim|#1C1C1E|bg-tone-2=#28282E @@ -1054,18 +973,9 @@ dim|#1C1C1E|label-danger=rgba(#FF3A3A,1) dim|#1C1C1E|border-accent=rgba(#4A8FFF,0.2) dim|#1C1C1E|border-danger=rgba(#FF3A3A,0.2) dim|#1C1C1E|border-focus=rgba(#4A8FFF,1) -dim|#1C1C1E|badge-fill-brand=rgba(#4A8FFF,1) -dim|#1C1C1E|badge-fill-danger=rgba(#FF3A3A,1) -dim|#1C1C1E|badge-fill-warning=rgba(#FF9008,1) -dim|#1C1C1E|badge-fill-success=rgba(#30D158,1) -dim|#1C1C1E|badge-fill-info=rgba(#5696FF,1) -dim|#1C1C1E|badge-fill-static-dark=rgba(#101012,1) -dim|#1C1C1E|badge-fill-static-light=rgba(#FFFFFF,1) dim|#1C1C1E|fill-neutral-tinted->fill-primary dim|#1C1C1E|border-neutral->border-base dim|#1C1C1E|fx-skeleton-base->fill-quaternary -dim|#1C1C1E|fill-accent->badge-fill-brand -dim|#1C1C1E|fill-danger->badge-fill-danger dim|#1C1C1E|icon->label-tertiary dim|#1C1C1E|border-ghost->border-none dim|#101012|bg-tone-2=#1C1C22 @@ -1161,18 +1071,9 @@ dim|#101012|label-danger=rgba(#FF3A3A,1) dim|#101012|border-accent=rgba(#4A8FFF,0.2) dim|#101012|border-danger=rgba(#FF3A3A,0.2) dim|#101012|border-focus=rgba(#4A8FFF,1) -dim|#101012|badge-fill-brand=rgba(#4A8FFF,1) -dim|#101012|badge-fill-danger=rgba(#FF3A3A,1) -dim|#101012|badge-fill-warning=rgba(#FF9008,1) -dim|#101012|badge-fill-success=rgba(#30D158,1) -dim|#101012|badge-fill-info=rgba(#5696FF,1) -dim|#101012|badge-fill-static-dark=rgba(#101012,1) -dim|#101012|badge-fill-static-light=rgba(#FFFFFF,1) dim|#101012|fill-neutral-tinted->fill-primary dim|#101012|border-neutral->border-base dim|#101012|fx-skeleton-base->fill-quaternary -dim|#101012|fill-accent->badge-fill-brand -dim|#101012|fill-danger->badge-fill-danger dim|#101012|icon->label-tertiary dim|#101012|border-ghost->border-none dim|#3478F6|bg-tone-2=#6E6E77 @@ -1268,17 +1169,8 @@ dim|#3478F6|label-danger=rgba(#FF3A3A,1) dim|#3478F6|border-accent=rgba(#4A8FFF,0.2) dim|#3478F6|border-danger=rgba(#FF3A3A,0.2) dim|#3478F6|border-focus=rgba(#4A8FFF,1) -dim|#3478F6|badge-fill-brand=rgba(#4A8FFF,1) -dim|#3478F6|badge-fill-danger=rgba(#FF3A3A,1) -dim|#3478F6|badge-fill-warning=rgba(#FF9008,1) -dim|#3478F6|badge-fill-success=rgba(#30D158,1) -dim|#3478F6|badge-fill-info=rgba(#5696FF,1) -dim|#3478F6|badge-fill-static-dark=rgba(#101012,1) -dim|#3478F6|badge-fill-static-light=rgba(#FFFFFF,1) dim|#3478F6|fill-neutral-tinted->fill-primary dim|#3478F6|border-neutral->border-base dim|#3478F6|fx-skeleton-base->fill-quaternary -dim|#3478F6|fill-accent->badge-fill-brand -dim|#3478F6|fill-danger->badge-fill-danger dim|#3478F6|icon->label-tertiary dim|#3478F6|border-ghost->border-none diff --git a/crates/labcolors-core/tests/empirical_inventory.rs b/crates/labcolors-core/tests/empirical_inventory.rs index 9ea9deba..91142c5a 100644 --- a/crates/labcolors-core/tests/empirical_inventory.rs +++ b/crates/labcolors-core/tests/empirical_inventory.rs @@ -57,9 +57,7 @@ const PERCEPTUAL_MODULES: [&str; 7] = [ // // Why a SUBSET: the two modules below are POLICY modules — their magnitudes are // tunable perceptual policy (role fractions and neutral thresholds). The remaining -// modules are STANDARD-MODEL or bounded numeric-search transforms. Pair after P1 -// contains only typed topology/lowering and no perceptual policy constants, so it -// is intentionally outside both inventory scan surfaces. +// modules are STANDARD-MODEL or bounded numeric-search transforms. const POLICY_LITERAL_MODULES: &[&str] = &["semantic.rs", "neutral.rs"]; /// Bare float literal VALUES that are NOT tunable perceptual policy — universal @@ -171,9 +169,6 @@ const STRUCTURAL_NONPOLICY_ALLOWLIST: &[&str] = &[ "NEIGHBOR_STEPS", "DJ_NEIGHBOR_STEPS", "MAX_PROBES", - // Bisection iteration count in the pair-fill minimal-nudge search — an - // iteration budget, not a perceptual magnitude (pair.rs). - "BISECTION_STEPS", ]; // ───────────────────────────────────────────────────────────────────────────── diff --git a/crates/labcolors-wasm/src/config_dto.rs b/crates/labcolors-wasm/src/config_dto.rs index 0e6abaa7..55f8fa93 100644 --- a/crates/labcolors-wasm/src/config_dto.rs +++ b/crates/labcolors-wasm/src/config_dto.rs @@ -155,14 +155,6 @@ pub enum RoleRecipeDto { step: String, decision_profile: String, }, - PairFill { - source: LadderSourceDto, - }, - PairLabel { - source: LadderSourceDto, - fraction: f64, - floor: FloorDto, - }, AlphaAnalog { of: LadderSourceDto, alpha: f64, @@ -354,18 +346,6 @@ impl TryFrom for RoleRecipe { position: position_from_key(&position)?, floor: floor.map(Floor::from), }, - RoleRecipeDto::PairFill { source } => RoleRecipe::PairFill { - source: source.into(), - }, - RoleRecipeDto::PairLabel { - source, - fraction, - floor, - } => RoleRecipe::PairLabel { - source: source.into(), - fraction, - floor: floor.into(), - }, RoleRecipeDto::AlphaAnalog { of, alpha } => RoleRecipe::AlphaAnalog { of: of.into(), alpha, @@ -515,18 +495,6 @@ impl TryFrom<&RoleRecipe> for RoleRecipeDto { position: position.key().to_string(), floor: floor.map(floor_to_dto).transpose()?, }, - RoleRecipe::PairFill { source } => RoleRecipeDto::PairFill { - source: source.try_into()?, - }, - RoleRecipe::PairLabel { - source, - fraction, - floor, - } => RoleRecipeDto::PairLabel { - source: source.try_into()?, - fraction: *fraction, - floor: floor_to_dto(*floor)?, - }, RoleRecipe::AlphaAnalog { of, alpha } => RoleRecipeDto::AlphaAnalog { of: of.try_into()?, alpha: *alpha, @@ -663,31 +631,6 @@ mod tests { .expect("восстановленный конфиг компилируется"); } - /// Recipe-адаптер `pair-label` гоняется через JSON без - /// потерь: kebab-тег `pair-label`, источник/доля/пол целы туда-обратно. - /// Это только доказательство DTO round-trip, а не наличия pair-label - /// в целевом graph API. - #[test] - fn pair_label_recipe_round_trips_through_json() { - use labcolors_core::solve::Floor; - let json = r#"{"kind":"pair-label","source":{"kind":"family","key":"warning"},"fraction":0.461,"floor":"aa-ui"}"#; - let dto: RoleRecipeDto = serde_json::from_str(json).expect("pair-label парсится"); - let core = RoleRecipe::try_from(dto).expect("DTO → RoleRecipe"); - assert!( - matches!( - &core, - RoleRecipe::PairLabel { fraction, floor: Floor::AaUi, .. } - if (*fraction - 0.461).abs() < 1e-12 - ), - "pair-label конвертируется в ядро с целыми полями" - ); - let back = RoleRecipeDto::try_from(&core).expect("RoleRecipe → DTO"); - let re = serde_json::to_string(&back).expect("сериализуем"); - assert!(re.contains(r#""kind":"pair-label""#), "kebab-тег цел: {re}"); - assert!(re.contains(r#""floor":"aa-ui""#), "пол цел: {re}"); - assert!(re.contains(r#""key":"warning""#), "источник цел: {re}"); - } - /// C6 RED: удалённая специальная sentiment-схема обязана стать неизвестной, /// а не тихо игнорироваться serde после удаления поля/варианта. #[test] @@ -876,7 +819,7 @@ mod tests { let full = labui_dto(); assert_eq!( format!("{:016x}", fingerprint(&full)), - "1adb2876102d77f3", + "bce14f09e43c705a", "пин паспорта main; при легитимной смене паспорта обнови это число" ); } diff --git a/crates/labcolors-wasm/src/lib.rs b/crates/labcolors-wasm/src/lib.rs index 45c562a1..10722928 100644 --- a/crates/labcolors-wasm/src/lib.rs +++ b/crates/labcolors-wasm/src/lib.rs @@ -417,8 +417,6 @@ export type RoleRecipe = step: "subtle" | "base" | "bloom"; decision_profile: GlowDecisionProfileV1; } - | { kind: "pair-fill"; source: LadderSource } - | { kind: "pair-label"; source: LadderSource; fraction: number; floor: "aa-text" | "aa-ui" | "none" } | { kind: "alpha-analog"; of: LadderSource; alpha: number } | { kind: "material"; source: LadderSource; tone_light: number; tone_dark: number; floor: "aa-text" | "aa-ui" } | { kind: "zero" }; diff --git a/crates/labcolors-wasm/tests/data/labui.config.json b/crates/labcolors-wasm/tests/data/labui.config.json index 69fc367b..8c6b9b17 100644 --- a/crates/labcolors-wasm/tests/data/labui.config.json +++ b/crates/labcolors-wasm/tests/data/labui.config.json @@ -1138,76 +1138,8 @@ }, "position": "focus-ring" } - }, - { - "name": "badge-fill-brand", - "recipe": { - "kind": "pair-fill", - "source": { - "kind": "brand" - } - } - }, - { - "name": "badge-fill-danger", - "recipe": { - "kind": "pair-fill", - "source": { - "kind": "family", - "key": "red" - } - } - }, - { - "name": "badge-fill-warning", - "recipe": { - "kind": "pair-fill", - "source": { - "kind": "family", - "key": "orange" - } - } - }, - { - "name": "badge-fill-success", - "recipe": { - "kind": "pair-fill", - "source": { - "kind": "family", - "key": "green" - } - } - }, - { - "name": "badge-fill-info", - "recipe": { - "kind": "pair-fill", - "source": { - "kind": "family", - "key": "blue" - } - } - }, - { - "name": "badge-fill-static-dark", - "recipe": { - "kind": "pair-fill", - "source": { - "kind": "neutral", - "pick": "dark" - } - } - }, - { - "name": "badge-fill-static-light", - "recipe": { - "kind": "pair-fill", - "source": { - "kind": "neutral", - "pick": "light" - } - } } + ], "aliases": [ { @@ -1222,14 +1154,6 @@ "alias": "fx-skeleton-base", "target": "fill-quaternary" }, - { - "alias": "fill-accent", - "target": "badge-fill-brand" - }, - { - "alias": "fill-danger", - "target": "badge-fill-danger" - }, { "alias": "icon", "target": "label-tertiary" diff --git a/crates/labcolors-wasm/tests/data/labui.config.prod.json b/crates/labcolors-wasm/tests/data/labui.config.prod.json index 24c4ddea..e147e4ae 100644 --- a/crates/labcolors-wasm/tests/data/labui.config.prod.json +++ b/crates/labcolors-wasm/tests/data/labui.config.prod.json @@ -1109,76 +1109,8 @@ }, "position": "focus-ring" } - }, - { - "name": "badge-fill-brand", - "recipe": { - "kind": "pair-fill", - "source": { - "kind": "brand" - } - } - }, - { - "name": "badge-fill-danger", - "recipe": { - "kind": "pair-fill", - "source": { - "kind": "family", - "key": "red" - } - } - }, - { - "name": "badge-fill-warning", - "recipe": { - "kind": "pair-fill", - "source": { - "kind": "family", - "key": "orange" - } - } - }, - { - "name": "badge-fill-success", - "recipe": { - "kind": "pair-fill", - "source": { - "kind": "family", - "key": "green" - } - } - }, - { - "name": "badge-fill-info", - "recipe": { - "kind": "pair-fill", - "source": { - "kind": "family", - "key": "blue" - } - } - }, - { - "name": "badge-fill-static-dark", - "recipe": { - "kind": "pair-fill", - "source": { - "kind": "neutral", - "pick": "dark" - } - } - }, - { - "name": "badge-fill-static-light", - "recipe": { - "kind": "pair-fill", - "source": { - "kind": "neutral", - "pick": "light" - } - } } + ], "aliases": [ { @@ -1193,14 +1125,6 @@ "alias": "fx-skeleton-base", "target": "fill-quaternary" }, - { - "alias": "fill-accent", - "target": "badge-fill-brand" - }, - { - "alias": "fill-danger", - "target": "badge-fill-danger" - }, { "alias": "icon", "target": "label-tertiary" diff --git a/docs/whitepaper.md b/docs/whitepaper.md index 63eeedd3..6fb80095 100644 --- a/docs/whitepaper.md +++ b/docs/whitepaper.md @@ -79,8 +79,6 @@ fingerprint считается по распарсенной поддержив | `DecorativeLc` | декоративная контрастная величина без нормативного текстового смысла | | `Ladder` | якорь источника при alpha закрытой позиции | | `AlphaAnalog` | прозрачная форма объявленной solid-цели на локальном фоне | -| `PairFill` | frozen frontend: exact authored source как opaque Paint/Occurrence | -| `PairLabel` | finite label candidate domain против фактически emitted PairFill Surface | | `Material` | точечная двухслойная композиция с выведенной alpha | | `Glow` | точечные screen-слои с явным numerical profile | | `Zero` | явное отсутствие цветового значения | @@ -108,11 +106,10 @@ point-пути scratch принадлежит caller-у и размещён на compiler-а проверяется тестом. Compiler принадлежит proof-поверхности и отсутствует в production-артефакте. -`PairLabel` использует фактически emitted `PairFill` occurrence как derived -Surface. Frontend формирует конечный label candidate domain; общий joint engine -исполняет `fill → surfaceFrom → label`, строит полный hard-report, выбирает по -явному total order и повторяет fresh recheck. Имена клиентских токенов и -`FillPrimary` в physical lowering не участвуют. +Связь foreground с производной Surface задаёт client-owned Program. Core +лоуверит её в общие occurrence и joint-constraint примитивы, строит полный +hard-report и выполняет fresh recheck. Закрытых UI-рецептов для конкретного +компонента в физическом графе нет. Публичный API не принимает произвольный client-authored graph. `NamedRoleTable` остаётся boundary-представлением и не служит extension point для новых доменных diff --git a/packages/colors/bench/wasm.json b/packages/colors/bench/wasm.json index ddf14019..212f1cba 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-29934462817", + "source": "github-actions-run-29955098154", "platform": "linux-x64", - "rawBytes": 424971 + "rawBytes": 376977 }, "policy": { - "maxRawBytes": 424971, - "basis": "v2a-domain-safe-joint-selection", + "maxRawBytes": 376977, + "basis": "pair-taxonomy-hard-delete", "gzip": "diagnostic-only" } } diff --git a/packages/colors/test/wasm-boundary.golden.json b/packages/colors/test/wasm-boundary.golden.json index 40188b7d..ae76d7fb 100644 --- a/packages/colors/test/wasm-boundary.golden.json +++ b/packages/colors/test/wasm-boundary.golden.json @@ -1 +1 @@ -{"_meta":{"purpose":"Байт-точный golden границы JS↔WASM: recheckContrast/_recheckContrastMulti и полный snapshot resolveTheme.vars.","regenerated":"2026-07-10","regen_reason":"ADR-0004: point-glow решается по конечным encoded-sRGB8 screen-state; alpha эмитируется кратчайшей decimal-записью без повторного округления.","drift_scope":"Относительно origin/main изменены ровно 24 листа resolveVars: только --lab-fx-glow-{brand,danger,warning,neutral}-alpha на 6 фонах. P1 (2026-07-21) дополнительно изменил 30 fill-leaves: fill-accent, fill-danger, badge-fill-{brand,danger,info} на 6 light-фонах — старый pair_fill притемняющий nudge удалён, эмиссия fill теперь identity-якорь бренда; recheck по-прежнему 0/2752 drift (замерено).","recheck_unchanged":"Секция recheck: 2752/2752 числовых значений, то есть 1376/1376 пар (lc, wcag), байт-идентичны origin/main; ни одно значение recheckContrast не изменено.","numeric_provenance":{"glow_alpha":"Старый fixed-48 bisection + 4-decimal alpha заменён перебором конечных sRGB8-state, внутренней alpha выбранного интервала и shortest-roundtrip binary64 CSS.","scope_check":"Фактический структурный diff вычислен по каждому листу resolveVars; 4 alpha-ключа × 6 записей = 24."},"wcag_floors_not_weakened":"Glow — декоративная роль без WCAG-пола. Отдельно подтверждено: все 1376 пар (2752 числовых значения) recheck неизменны, поэтому измеренный контраст ни одной проверяемой роли не ослаблен.","isolated_verification_required":"Регенерация принимается только после независимой проверки scope, recheck-инварианта и полного Rust/npm/browser gate.","partition_correction":{"regenerated":"2026-07-11","reason":"Коррекция канонической binary64 alpha по фактическому midpoint encoded-sRGB8 partition без дополнительного округления.","drift_scope":"Ровно 17 alpha leaves: light/#000000 danger и четыре Glow alpha на каждом из #808080, #3A3A3C, #007AFF, #FF3B30; остальные resolveVars-листья неизменны.","recheck_unchanged":"2752/2752 числовых значений, то есть 1376/1376 пар (lc, wcag), побитно неизменны.","numeric_provenance":{"halo":"#FF3B30","background":"#000000","state":"#030101","lower_bits":"0x3f85555555555555","upper_bits":"0x3f8c1c1c1c1c1c1c","midpoint_bits":"0x3f88b8b8b8b8b8b8","midpoint":"0.012071078431372548","previous_bits":"0x3f88b8b8b8b8b8b9","previous_relation":"1 ULP выше, внутри той же partition"},"exhaustive_proof":"65 536 channel partitions и seam: state/composite outputs неизменны.","performance":"Full-range → bracketed exact search: base 19.7→4.37 µs, bloom 74.1→16.2 µs, long 656→139 µs."},"history":{"previous_regenerated":"2026-07-10","previous_regen_reason":"Глава #64 (ADR-0003): ось читаемости активирована в домене Ys — реактивный recheck (measure_contrast/recheck_against) и solver-вывод считаются в Ys. recheck flat и resolveVars регенерированы против пересобранного движка; inputs (theme/bg/fgs) сохранены. semver-major по ADR.","previous_drift_scope":"resolveVars: exactly 24 leaves changed, all --lab-fx-glow-{brand,danger,warning,neutral}-core (glow-centre colour, background-independent → 4 distinct values). No other var moved. glow base/alpha, labels, borders, fills, backgrounds all byte-identical.","previous_recheck_unchanged":"Исторический regen также не менял recheck; прежний текст ошибочно называл 2752 числа парами. Фактический объём: 1376 пар (lc, wcag).","previous_numeric_provenance":{"glow-brand-core":"oklch(78.84894% 0.062240 265.472) -> oklch(78.76611% 0.108476 257.256): chroma rises to the blue gamut wall at the functional lightness (fixed-fraction under-saturated it); L ±0.08pp, hue shifts to the cusp.","glow-danger-core":"oklch(81.62208% 0.103545 27.533) -> oklch(81.77103% 0.102764 29.025): near-identical (red already sat near its wall).","glow-warning-core":"oklch(88.80612% 0.081204 68.866) -> oklch(88.88181% 0.082838 68.777): near-identical.","glow-neutral-core":"oklch(99.97226% 0.001314 106.423) -> oklch(100.00000% 0.000000 89.876): neutral hue is achromatic → gamut wall chroma is 0 (was fixed-fraction residue noise); exact achromatic source stays on the neutral ray."},"previous_wave1_scope":"resolveVars: 69 info-* leaves changed across 6 theme×bg entries. Zero non-info drift (scope-checked). recheck untouched."},"endpoint_recovery":{"regenerated":"2026-07-17","reason":"Acceptance is checked before low-contrast dead-zone and physical-endpoint rejection, so previously false-failed opaque border endpoints now resolve.","drift_scope":"Exactly 9 resolveVars leaves added: border-{brand,danger,info}-strong on light #808080, #007AFF and #FF3B30. No existing leaf changed or disappeared.","recheck_unchanged":"Every committed recheck value remains bit-identical; the parity test compares all 2752 numbers before resolveVars.","proof":"agnostic_gates::accepted_endpoint_recovery_keeps_previously_false_failed_borders_legal plus full Rust workspace and WASM boundary parity."}},"recheck":[{"theme":"light","bg":"#FFFFFF","fgs":["#F7F7FF","#F7F7FF","#0A0A10","#71717A","#94949E","#BDBDC7","#94949E","#0A0A10","#00030E","#0057BB","#037BFF","#78AFFF","#150000","#BE0005","#FF070E","#FF9A8C","#1B0D00","#A06300","#D28300","#FFAA3A","#000B01","#007C2D","#00A73F","#00D452","#020013","#4D00F9","#6D6EFF","#9FA9FF"],"flat":[0,1.0658201232723763,0,1.0658201232723763,104.65963437608701,19.74046779866239,70.1623092547118,4.832895561154151,53.96706418364319,3.004779237664955,33.34341874861831,1.8637664731545371,53.96706418364319,3.004779237664955,104.65963437608701,19.74046779866239,105.59876563358553,20.60103636973117,79.91552226990729,6.816302360996974,63.95198968935827,3.97647793086433,41.89234722606997,2.2400949113548063,105.32470288110206,20.35108757719671,78.97384099389187,6.579727828852003,63.90360859563009,3.970705427078051,37.83221395673913,2.048194921340976,103.87315757816087,19.018778265876662,70.62528347734393,4.90716956443272,53.97808064225952,3.0056474129326713,34.232350404909745,1.8982213760304238,104.97702200816887,20.032297013031588,73.2226464627747,5.355840815772706,56.123729301186145,3.182173571701684,36.54497093010938,1.9925549350335838,105.76444562516849,20.751302844974063,83.48658341134008,7.825697912025839,63.67798680778349,3.9439477616440737,40.86459333479343,2.1890112231357013]},{"theme":"light","bg":"#000000","fgs":["#010103","#010103","#F1F1FC","#B3B3BC","#909099","#65656D","#909099","#F1F1FC","#E6F0FF","#60A1FF","#0074F3","#004CA4","#FFF4F2","#FF8273","#F40009","#A80004","#FFF7EE","#F79C00","#C97E00","#8F5800","#C2FFC9","#00C84D","#00A03C","#006E26","#EEF0FF","#9099FF","#6662FF","#4300DB"],"flat":[0,1.0069471255994658,0,1.0069471255994658,-99.67218963360757,18.727928195985914,-63.80513707672789,10.090949031745104,-45.863656882792206,6.635143500688325,-26.33477890397734,3.635637824948841,-45.863656882792206,6.635143500688325,-99.67218963360757,18.727928195985914,-97.97941453819666,18.27263411531394,-53.55180732861014,8.039306941172763,-34.54692380627581,4.792369408161928,-17.418806999279397,2.56984376276454,-102.41236835704379,19.474434420712562,-56.92703132210305,8.692613650917824,-34.93266406694178,4.850563950189369,-18.308962222075785,2.6667193472488013,-103.5613239139708,19.790915044333452,-61.97487030298502,9.71021814570805,-44.901407836051874,6.467843223430967,-25.783665577412222,3.5638173443979557,-98.60837652742211,18.44127602271432,-60.30793382928855,9.368875012646658,-42.707239087904505,6.093571182387199,-23.370874926227668,3.2583602660985775,-98.9883689300502,18.54346384276384,-54.32153113615954,8.18635152260471,-34.304902287955215,4.756029101219059,-14.435475219756622,2.261552965854495]},{"theme":"light","bg":"#808080","fgs":["#7A7A82","#7A7A82","#010103","#18171E","#37363E","#5B5B64","#37363E","#010103","#000002","#00163A","#003272","#004494","#040000","#3A0000","#760002","#990003","#0B0400","#261300","#512F00","#845100","#000200","#001D05","#004114","#006322","#000002","#160059","#2B0097","#3B00C5"],"flat":[0,1.0777393702545288,0,1.0777393702545288,40.18994509287661,5.280525528202377,36.81932911616927,4.505560397043893,27.93898600302036,3.0217055248761144,13.845788611978202,1.7013528597992067,27.93898600302036,3.0217055248761144,40.18994509287661,5.280525528202377,40.3306726916817,5.312553092992316,36.89312592554248,4.521969910173767,28.754774555141132,3.1292028976206594,22.090354756391577,2.35851620111828,40.23106479600237,5.289901483963394,36.823385380889306,4.5064610721321205,27.786796188195932,3.002060282138028,21.02583740042693,2.2570951143110336,39.64148780057383,5.154352713354357,36.89828467875601,4.523118840482662,27.981450040156208,3.027209986332121,13.567318350507929,1.6836851754208233,40.150141663678234,5.271436537678366,36.85731353296706,4.514000513189092,27.852320260403722,3.0105025419148825,16.643886294769857,1.8937047658181707,40.3306726916817,5.312553092992316,36.84988795298528,4.512349526507044,30.916517468784825,3.4313626667845316,25.033605894158644,2.6690076358452903]},{"theme":"light","bg":"#3A3A3C","fgs":["#3E3E45","#3E3E45","#F2F2FD","#BBBBC5","#9E9EA7","#7A7A82","#9E9EA7","#F2F2FD","#E7F1FF","#73ADFF","#2985FF","#0060CB","#FFF5F4","#FF9687","#FF4336","#CD0006","#FFF8F0","#FFA62C","#DC8A00","#AC6B00","#C7FFCC","#00D251","#00B043","#008631","#EFF1FF","#9CA6FF","#767AFF","#532BFF"],"flat":[0,1.0699484563148034,0,1.0699484563148034,-89.06520303275792,10.211241554507897,-56.964055924386784,5.959497869408446,-41.60773958742316,4.2707825803649335,-24.25691832785775,2.6663016206717356,-41.60773958742316,4.2707825803649335,-89.06520303275792,10.211241554507897,-87.32090210121694,9.956824779623638,-48.0046310941281,4.945145596338778,-30.285195552724065,3.1849116126306156,-14.342034884792934,1.9106934427040745,-91.71639880150975,10.602849175762582,-51.97724928251108,5.385053682831725,-31.570923815646545,3.301015045785777,-14.82740446463819,1.94471301206827,-92.86717481243348,10.774663089384916,-55.64998948221256,5.805759082702303,-40.41406674606042,4.149700467279057,-23.7537806706175,2.6249801387527425,-87.99158651504085,10.054343074329001,-53.75183524673264,5.586666992924044,-38.313874917805634,3.940371955014153,-21.037647044419426,2.407284189061088,-88.33220875070053,10.104016013712561,-48.80525652513833,5.032519885671211,-30.915189659062865,3.2415642790980215,-11.323318014385267,1.7063261265170042]},{"theme":"light","bg":"#007AFF","fgs":["#797981","#797981","#020205","#16151C","#36353D","#72727A","#36353D","#020205","#000103","#001536","#003374","#0058BC","#070000","#360000","#730002","#BE0005","#0F0500","#241200","#4F2F00","#A16400","#000400","#001B05","#003F13","#007C2D","#000005","#140055","#3300AC","#4E00FA"],"flat":[0,1.0745907693903731,0,1.0745907693903731,39.47891404105317,5.158402202047827,36.63906407938327,4.513939148043301,27.726113654933034,3.015791844568742,0,1.187212547453551,27.726113654933034,3.015791844568742,39.47891404105317,5.158402202047827,39.65687576920386,5.198408498908002,36.59714090951085,4.504660010775197,27.83078380746869,3.029349552004155,13.31049770996219,1.6744770124926043,39.57933742278427,5.181007308853048,36.66186584356392,4.51899176883356,27.77510172821011,3.0221297438766705,12.721590912296357,1.6379804580418844,38.8564182220264,5.016999835671664,36.579572970042136,4.500775701626332,27.65113132417139,3.006116231224861,0,1.205042331050678,39.39108202991847,5.138573889881769,36.624715730830154,4.510761781161014,27.902773646365304,3.038708829709556,0,1.3333017445114326,39.737197591996,5.216381929144518,36.59037871155487,4.5031645842195545,27.68585404738298,3.010592990868115,17.038043883395044,1.933101474636693]},{"theme":"light","bg":"#FF3B30","fgs":["#8A8A92","#8A8A92","#030306","#212129","#3E3D45","#76767F","#3E3D45","#030306","#000104","#00204D","#003B83","#005CC4","#080000","#4C0001","#830002","#C70006","#0F0600","#331C00","#5B3600","#A76800","#000400","#002809","#004917","#00822F","#000005","#1F0075","#3A00C3","#511AFF"],"flat":[0,1.0356140935636031,0,1.0356140935636031,43.57584669970237,5.806877937534237,38.3004661979112,4.504747008392574,29.108000765094282,3.0232304751839747,0,1.268226675946831,29.108000765094282,3.0232304751839747,43.57584669970237,5.806877937534237,43.87966672163739,5.884401548408216,38.30767260595269,4.50632138120243,29.16726657041677,3.0308664461202253,15.823096179531001,1.7789458759332724,43.782795788734404,5.8597651770218455,38.402450836988166,4.527103297945568,29.016037258963053,3.011424325654164,14.9864556348382,1.7254955647131072,42.99821792375369,5.657954629703501,38.41622407485322,4.5301350068404425,28.93769973409828,3.0014083479801514,0,1.28054929135364,43.62397616937584,5.819206103341516,38.406807106466914,4.528061863682665,29.022563392947294,3.012260432085235,8.92751562582044,1.3982348229239339,43.970091731453365,5.907320242919889,38.28628402517786,4.501651045763603,28.99214635836292,3.0083657301901288,19.11088367642911,2.0125842767555064]},{"theme":"light","bg":"#101012","fgs":["#15151B","#15151B","#F2F2FC","#B4B4BE","#93939C","#696972","#93939C","#F2F2FC","#E6F0FF","#64A4FF","#0077F9","#0050AC","#FFF4F3","#FF8777","#F9000A","#B00005","#FFF7EF","#FA9E00","#CD8000","#955C00","#C3FFC9","#00CA4E","#00A33D","#007329","#EEF0FF","#939CFF","#6967FF","#4600E6"],"flat":[0,1.045340264415452,0,1.045340264415452,-98.5838326466521,17.08955684747884,-62.72698384194396,9.243246994613658,-45.67498009680545,6.239717532288448,-26.395025771311026,3.4974414103185016,-45.67498009680545,6.239717532288448,-98.5838326466521,17.08955684747884,-96.3221346483494,16.53807546575917,-53.23458352949761,7.5085332653696355,-34.299151794570456,4.531377660721796,-17.34040000814869,2.4827755446650244,-100.79897840003042,17.636700891614694,-56.73966900010428,8.131153426700068,-34.4661942600266,4.554628420782135,-18.26813498707333,2.5778416349364046,-101.94670279716274,17.922901715066683,-61.50032492399525,9.010497025774457,-44.48245067627942,6.049054371037485,-25.983086445749187,3.447217107352778,-97.04991927160123,16.714741751700544,-59.578454823729174,8.650894122604043,-42.30409033566848,5.707629679225331,-23.50941826359813,3.1535587190594883,-97.33108904020293,16.783196253636362,-53.9780650339559,7.638815670143083,-34.15649094558268,4.511565332868746,-14.178333309149869,2.174938899863266]},{"theme":"light","bg":"#F6F8FA","fgs":["#F0F0FA","#F0F0FA","#0A0910","#707079","#8F8F99","#BCBCC5","#8F8F99","#0A0910","#00030D","#0057B9","#0079FD","#74ADFF","#150000","#BC0005","#FD000A","#FF9788","#1B0D00","#9F6200","#CB7F00","#FFA730","#000B01","#007B2C","#00A63E","#00D251","#020013","#4C00F7","#6C6CFF","#9DA6FF"],"flat":[0,1.0634215248159875,0,1.0634215248159875,100.62201386979996,18.61882254505944,66.47667620763235,4.605948460093869,52.23653324893716,3.0084294463712395,29.780700684603257,1.7710808257265196,52.23653324893716,3.0084294463712395,100.62201386979996,18.61882254505944,101.48497819102282,19.361251289328656,75.9364891028185,6.438321694919453,60.636932359482174,3.8278153397206016,38.76699274290234,2.152541489746932,101.19913515757631,19.11640864166299,75.28055256994783,6.281220843840182,60.46094873358925,3.807428346232839,34.70184973776556,1.9658801332164664,99.74758985463511,17.864929125608796,66.9127104243937,4.673002507305443,52.13428535623209,3.0001216660302674,31.16352525448906,1.8227359456035261,100.85145428464311,18.816958763489637,69.48045983543288,5.0977335979175304,52.421867335088024,3.0235758695027473,33.32486172802245,1.9082022515525083,101.63887790164272,19.492343272893066,79.67393134344084,7.442688338927282,60.17031912503092,3.7741072518393395,37.967753096619255,2.113772626279229]},{"theme":"light","bg":"#1C1C1E","fgs":["#202028","#202028","#F1F1FD","#B6B6BF","#95959E","#6D6C75","#95959E","#F1F1FD","#E6F1FF","#67A5FF","#0079FD","#0053B2","#FFF5F3","#FF8A7A","#FD000A","#B60005","#FFF7EF","#FC9F00","#D08200","#9A5F00","#C4FFCA","#00CB4E","#00A53E","#00772A","#EEF1FF","#949EFF","#6B6BFF","#4900EE"],"flat":[0,1.0522076023272475,0,1.0522076023272475,-96.27140987076255,15.184146579953019,-61.97466067394124,8.452688508186561,-44.87094637123084,5.728792540137694,-25.977188885961883,3.283145241842075,-44.87094637123084,5.728792540137694,-96.27140987076255,15.184146579953019,-94.97224440690107,14.900628115594163,-51.99577035710509,6.808135431331698,-33.457001357615106,4.175350120738554,-16.755883343489252,2.3335896778079332,-99.44736446714371,15.886233758087014,-55.869159717403925,7.428487313030341,-33.63398343987836,4.1977071628099685,-17.709012021143003,2.4235530906296177,-100.15616825242344,16.04465607216319,-60.39258141645385,8.181935837052826,-43.77686148964125,5.57032618168592,-25.67861999448382,3.2497240951569792,-95.39392806862337,14.992417327878547,-58.246405974587134,7.820609198173194,-41.3596549106396,5.227261702394524,-23.16505433719768,2.9752806858720673,-95.97861011098902,15.120061263203354,-52.9765437482588,6.963016139185695,-33.57839540296167,4.190679013723035,-13.485138200601865,2.040069034387161]},{"theme":"light","bg":"#7C3AED","fgs":["#707078","#707078","#F4F4FE","#E3E4ED","#BDBDC6","#A6A6B0","#BDBDC6","#F4F4FE","#EBF3FF","#D5E6FF","#92BFFF","#4291FF","#FFF8F7","#FFDDD8","#FFA597","#FF6253","#FFFAF5","#FFE0BE","#FFAA39","#E79100","#D0FFD4","#7DFF93","#00DA54","#00B947","#F1F4FF","#DFE4FF","#AEB8FF","#8087FF"],"flat":[0,1.1608373681184396,0,1.1608373681184396,-73.98396246220521,5.215032461901156,-64.0643931275358,4.500912896664839,-41.75099800228171,3.055051474145459,-29.486682699107686,2.3616035737730514,-41.75099800228171,3.055051474145459,-73.98396246220521,5.215032461901156,-72.43421936609613,5.100687932363686,-64.08586870464498,4.502412953317652,-41.11294430990105,3.017135229848019,-18.888486914529935,1.8251241221961068,-76.90365537296871,5.4332059719499135,-64.06334225320066,4.50083949888721,-40.940708789235714,3.0069340578770496,-21.122834074579163,1.9331720160112165,-77.70409895709017,5.493642615376888,-64.34147182620212,4.520282197700913,-40.849183913847796,3.0015190917179395,-28.18413111501551,2.292444746213784,-72.9506558335481,5.138679113149092,-64.10668565953954,4.5038671985064,-41.2870784084647,3.027463512408998,-26.013778316259767,2.1791858289450103,-73.64084437140687,5.1896286230945075,-64.42349258712335,4.5260223420377566,-41.02780633656069,3.0120908713744514,-19.464187683477977,1.8526993919522168]},{"theme":"light","bg":"#123456","fgs":["#36363D","#36363D","#F2F2FD","#BEBEC8","#A2A2AC","#808089","#A2A2AC","#F2F2FD","#E8F1FF","#79B0FF","#368BFF","#0065D6","#FFF6F4","#FF9B8D","#FF5446","#D80007","#FFF8F1","#FFAB3F","#E18E00","#B47000","#C8FFCD","#00D552","#00B545","#008D34","#EFF1FF","#A0AAFF","#7B81FF","#573FFF"],"flat":[0,1.0609289970892934,0,1.0609289970892934,-91.39519035537737,11.4423466076867,-60.94367397154904,6.896920022598847,-46.020165423777584,5.026435795944436,-29.36949737122932,3.250019698594834,-46.020165423777584,5.026435795944436,-91.39519035537737,11.4423466076867,-89.77514075602531,11.177468536043195,-51.89752791553575,5.733156384550998,-34.996215684604906,3.8114980409485155,-18.92394323577027,2.320895932067837,-94.48648764812391,11.954651961610136,-56.02375667309079,6.252768618847473,-36.81873931987068,4.002056146272223,-19.591172380553846,2.375625103310219,-95.24016771148194,12.080915286992608,-59.85145182180837,6.751641124414219,-44.82390104707481,4.887508677146329,-28.683175933367217,3.184369254481423,-90.45906258458693,11.288985892577246,-57.499124974387314,6.443154991463433,-42.83495757582209,4.660276446547384,-26.099142978345885,2.9428672820648862,-90.66219607331998,11.322193559065964,-52.9772743130274,5.867283737103295,-35.76790053375578,3.8916727925995533,-16.863337707955058,2.1560372721390864]},{"theme":"light","bg":"#ABCDEF","fgs":["#C2C2CB","#C2C2CB","#06060C","#55545D","#706F78","#A1A1AA","#706F78","#06060C","#000209","#00479B","#0065D5","#3289FF","#100000","#9F0004","#D70007","#FF4F41","#170A00","#7A4A00","#9F6200","#DF8C00","#000801","#006322","#00822F","#00B344","#01000D","#3E00CE","#573EFF","#797EFF"],"flat":[0,1.0705446339746656,0,1.0705446339746656,75.64582413051765,12.232204997602986,52.76667497081496,4.519371986307913,41.423484798334584,3.0020546798073307,18.127484103049046,1.5512322351556214,41.423484798334584,3.0020546798073307,75.64582413051765,12.232204997602986,76.22533906008685,12.552007092013115,56.99670466281028,5.352680994694784,44.43662180236468,3.3253647200326193,29.122033451491582,2.0641033245604916,76.01516726706762,12.436514137733148,55.97147975339624,5.133613537650522,43.91010121645754,3.2654000659595037,27.461071919951625,1.9716236837030474,74.81611736383091,11.770648316029503,52.816193854903005,4.528105409994289,41.51373908005361,3.011071841176318,19.679891349505557,1.6112041351931181,75.72958089661125,12.278654436624837,52.80881394476793,4.526802423347816,41.42233250009274,3.001939806510638,21.536768417458934,1.687644517337887,76.3513076320574,12.62087594492386,59.99704797613187,6.063965240585196,46.62694808670478,3.592268428661898,28.6381619838115,2.036506690369533]},{"theme":"light","bg":"#808080","fgs":["#F7F7FF","#0A0A10","#71717A","#fff","abcdef","#Ff3B30","#123","112233"],"flat":[-64.57463009518332,3.7055405145881393,38.97016765822309,4.9982958489854346,0,1.2236914579873204,-69.21596068658343,3.9494396480491156,-37.021615579693034,2.3904478169235754,0,1.1134158562260283,34.84722274356656,4.0887512719822805,34.84722274356656,4.0887512719822805]},{"theme":"dark","bg":"#FFFFFF","fgs":["#E7E7F1","#D9D9E2","#0D0D12","#707079","#94949D","#BCBCC6","#94949D","#0D0D12","#000414","#0053C4","#1F78FF","#79ACFF","#190000","#BC0016","#FD0021","#FF978D","#1D0B00","#A65B00","#DA7A00","#FFA658","#000E02","#007C2A","#00A63C","#00D24E","#04001A","#5900F2","#7867FF","#A4A4FF"],"flat":[10.564452802176923,1.2281904700369508,18.372353597432387,1.402460683906872,104.27090588423802,19.38311547953439,70.6022439311581,4.903434648448188,54.00450766949271,3.0077315832596954,33.86624309585679,1.8839141891835494,54.00450766949271,3.0077315832596954,104.27090588423802,19.38311547953439,105.42046624663428,20.43859328774187,80.31672424485741,6.9206271592755195,64.53598724808649,4.047139863973963,42.92136358171458,2.2930773168482466,105.12312344447312,20.1664284240786,79.32640956847692,6.666965706057784,64.47466861031803,4.0396342482899685,38.694434835096864,2.0867987099188317,103.94997559222902,19.08898290736351,71.78028267942507,5.099770465607366,55.19920557284124,3.104259347966041,35.17783363962355,1.9359471784913758,104.66191026229393,19.742561272328775,73.25578885512941,5.361938351796974,56.57179613789485,3.2209892436744982,37.49012931672191,2.03317973864909,105.58294145619523,20.58664787402852,83.26627791326895,7.757864847967472,64.57504887916579,4.051931818616954,42.090577540190225,2.2501559682241092]},{"theme":"dark","bg":"#000000","fgs":["#030205","#08080D","#F0F0FA","#B1B1BB","#8F8F98","#64646D","#8F8F98","#F0F0FA","#E3EEFF","#639EFF","#006DFE","#0048AD","#FFF4F3","#FF7F74","#F1001F","#A80012","#FFF7F1","#FF921B","#D17400","#955100","#B8FFBE","#00C649","#009F39","#006E25","#EDEEFF","#9794FF","#725BFF","#4E00D6"],"flat":[0,1.0147465549687367,0,1.0508696650285665,-99.01083964835996,18.549513785944853,-62.774763175831374,9.875849762659984,-45.37711439886134,6.5503119047070335,-25.95111170367181,3.5855589616463144,-45.37711439886134,6.5503119047070335,-99.01083964835996,18.549513785944853,-96.73192161780433,17.93999294470544,-52.63286661912575,7.865273702162647,-33.381528921151094,4.618607106385301,-17.0501576858861,2.5303719690800346,-102.4562582898777,19.486486384763445,-56.148341062851905,8.53993960492931,-34.330992536827104,4.759940239487347,-18.37234103122508,2.6737006902768288,-103.68997976483836,19.82648045930809,-60.35961607275431,9.379380118591195,-43.39157032733722,6.209218370112115,-24.93643207012505,3.4548840566407524,-97.27916136850891,18.085610527348415,-59.34389647192141,9.173841233004914,-42.25887199411116,6.018336366435839,-23.360562325895305,3.2570864547421428,-97.9872053947921,18.274719246988955,-53.323021296304695,7.99582368531923,-33.63351308594995,4.6559156780457185,-14.771084781300106,2.2949481607054216]},{"theme":"dark","bg":"#808080","fgs":["#707078","#66666E","#030305","#18171D","#37363E","#5D5D64","#37363E","#030305","#000104","#00153F","#00317C","#00429F","#070000","#3A0002","#750009","#9B0010","#0C0300","#291200","#562D00","#8B4B00","#000300","#001D05","#004112","#006521","#010005","#1A0057","#350098","#4700C4"],"flat":[0,1.24296889833444,9.274501031402123,1.4407196981948422,39.91548097777799,5.217615096019717,36.836632862345205,4.5094036578339445,27.93898600302036,3.0217055248761144,13.086665786299626,1.6537770693375544,27.93898600302036,3.0217055248761144,39.91548097777799,5.217615096019717,40.20955594591163,5.284998875760269,36.83828085440759,4.509769828130583,28.330761142327287,3.0728704517581886,21.819504217645104,2.3321848000622474,40.142120786515875,5.269603471071473,36.80831529061993,4.503115584720142,27.930310922781615,3.0205822411426944,20.559717681283935,2.214401964672886,39.70511745091868,5.169077378914306,36.80795164205375,4.503034882134433,27.78871707797606,3.002307432697674,14.025581970431295,1.7128949100718707,40.051433392362526,5.248844071123953,36.85731353296706,4.514000513189092,27.870384117529113,3.0128341396537346,15.95953035602916,1.844055168824021,40.26997047853993,5.298759395894901,36.795231110735635,4.500212645278562,30.09285126953815,3.3133648365325183,24.185949678819867,2.5748913454036786]},{"theme":"dark","bg":"#3A3A3C","fgs":["#46464E","#4F4F57","#F1F1FA","#BABAC4","#9D9DA6","#797982","#9D9DA6","#F1F1FA","#E4EFFF","#75AAFF","#3482FF","#005AD5","#FFF6F4","#FF9389","#FF3C3B","#CC0019","#FFF8F2","#FFA350","#E58000","#B36300","#BCFFC2","#00D04D","#00AF3F","#00862E","#EEEFFF","#A2A1FF","#7F74FF","#601DFF"],"flat":[0,1.2140718035663858,0,1.3988859584097761,-88.35603271482154,10.107493939375463,-56.41682578047019,5.8952705955406675,-41.10063170437542,4.219157833086703,-23.840055313660137,2.6320437618590202,-41.10063170437542,4.219157833086703,-88.35603271482154,10.107493939375463,-86.07057705487603,9.776047393674569,-47.005198509651805,4.836992692850337,-29.617188814986996,3.125340410912816,-13.465221764046625,1.8500449052652255,-92.15650032550448,10.66842694645063,-51.122226750198095,5.289032001141636,-30.69564720892069,3.221769980092269,-14.697188723997265,1.9355550836437716,-92.95341233620333,10.787583067801071,-55.01595924446716,5.732183607436098,-39.01368610071904,4.00959458617392,-22.717127042375882,2.5408179478715844,-86.56463892830851,9.847321829129962,-52.766100540364825,5.474290510931042,-37.83315401910713,3.893128667535661,-21.00389596511428,2.404636707585164,-87.32881842479753,9.957973594755346,-47.6452882768299,4.906141960838558,-29.99057738527554,3.1585747205458907,-10.990631160230787,1.6845762464445797]},{"theme":"dark","bg":"#007AFF","fgs":["#6F6F77","#65656D","#040408","#16151B","#36353D","#72727A","#36353D","#040408","#000107","#00133B","#00317C","#0054C7","#0B0000","#360002","#730009","#BF0016","#100500","#261000","#542C00","#A85D00","#000501","#001B04","#004012","#007D2B","#010009","#180052","#3B00A6","#5A00F5"],"flat":[0,1.2399763057879079,9.137153276370956,1.4379357071162235,39.199487305374596,5.095159290353486,36.655643103420836,4.517612479062309,27.726113654933034,3.015791844568742,0,1.187212547453551,27.726113654933034,3.015791844568742,39.199487305374596,5.095159290353486,39.616537893860624,5.189361828546554,36.671589094740156,4.521147554384925,27.767977778595686,3.0212072610618272,13.560368041515986,1.6902988819780091,39.46146930028483,5.1544680971312,36.646426655727396,4.515570174173857,27.733751845040576,3.016779185756929,12.426352071081123,1.6200937148513173,38.82022467282525,5.008731246970903,36.66757146424993,4.520256699490143,27.6755983348751,3.009270049666561,0,1.2356833222737893,39.28492308543262,5.114543979353608,36.632422880871374,4.512468289727634,27.610461383700073,3.00088106913278,0,1.3173837917708568,39.666598999480925,5.200587158978297,36.5946359240296,4.504106003407952,27.684199915070796,3.0103795763779906,16.582944142840912,1.898781610023956]},{"theme":"dark","bg":"#FF3B30","fgs":["#7F7F87","#74747D","#040408","#212128","#3E3D45","#76767F","#3E3D45","#040408","#000107","#001E53","#00398D","#0058CF","#0C0000","#4C0004","#83000C","#C60018","#110500","#371A00","#603300","#AE6000","#000601","#002808","#004916","#00822D","#01000A","#250070","#4400BC","#5E01FF"],"flat":[0,1.119564506502328,0,1.304916965157494,43.43238144483196,5.770041002680692,38.322111022898454,4.5094781825179275,29.108000765094282,3.0232304751839747,0,1.268226675946831,29.108000765094282,3.0232304751839747,43.43238144483196,5.770041002680692,43.84943203331798,5.876721182230661,38.357196991852035,4.517162998132957,28.969046969390032,3.0054117916551304,16.078397205708793,1.7957132708136676,43.66281217184597,5.829141814712315,38.38176722933373,4.522556047220382,28.96308732518828,3.004650205577566,15.121014183458136,1.7339400568635355,43.01534021323247,5.66238735865634,38.36369120360285,4.518587529333456,28.956857085057415,3.003854272405285,0,1.32359073643957,43.42248485866928,5.7674953973596095,38.41370496023468,4.529580289565808,29.031977011275746,3.0134669332394415,8.949958172317846,1.3992774406692907,43.88937786124038,5.8868666342953615,38.32550423955951,4.510220548286027,28.94413025668513,3.002229117722787,19.264807559936084,2.024522036885816]},{"theme":"dark","bg":"#101012","fgs":["#1C1C22","#24242A","#F0F0FA","#B3B3BD","#92929B","#686871","#92929B","#F0F0FA","#E3EEFF","#66A0FF","#0E71FF","#004CB5","#FFF5F3","#FF8378","#F60020","#AF0013","#FFF7F1","#FF962A","#D57700","#9B5500","#B9FFBF","#00C849","#00A23A","#007226","#EDEEFF","#9997FF","#7460FF","#5200DF"],"flat":[0,1.1210017368042908,0,1.2316217101091165,-97.35355975851269,16.788671896406843,-62.18912907483276,9.1408846014785,-45.183954583215964,6.160892481462449,-25.971291980892453,3.445784610130369,-45.183954583215964,6.160892481462449,-97.35355975851269,16.788671896406843,-95.07464172795706,16.23701078357789,-51.883866319617056,7.27432706128743,-33.10437599843017,4.366728841176053,-16.962076325331164,2.444617555127284,-101.23789901188303,17.745933910216884,-55.660852739640006,7.937252290960222,-33.86013630190819,4.470540437936136,-18.127983335051407,2.5633448249431416,-102.03269987499108,17.94442049171427,-60.01549050114797,8.732123022916136,-43.24478677485694,5.853976070416037,-25.11442835418257,3.342537791539289,-95.74776769254933,16.399183837627458,-58.596724506079966,8.469603286006345,-41.852118066322895,5.637910373772879,-23.122294138224145,3.108849028833141,-96.32992550494482,16.539962663017093,-52.86610846158114,7.444323350911882,-33.336065162067605,4.398429825394501,-14.383943910910213,2.194178799785328]},{"theme":"dark","bg":"#F6F8FA","fgs":["#E0E0EA","#D2D2DC","#0D0D12","#717179","#8F8F98","#BCBCC6","#8F8F98","#0D0D12","#000414","#0053C4","#1F78FF","#79ACFF","#190000","#BC0016","#FD0021","#FF978D","#1D0B00","#A65B00","#D67800","#FFA658","#000E02","#007C2A","#00A63C","#00D24E","#04001A","#5900F2","#7867FF","#A4A4FF"],"flat":[10.34292311627503,1.2313464424988367,18.024461551245693,1.4097592283337514,100.14533816071224,18.207162386274927,66.07133001527934,4.544848448940367,52.273633182943,3.011452358576948,29.740675372331037,1.769619110018133,52.273633182943,3.011452358576948,100.14533816071224,18.207162386274927,101.29489852310851,19.198605473400605,76.19115652133168,6.500760037097026,60.41041952456072,3.801604173546957,38.795795858188825,2.153958743950203,100.99755572094737,18.94295256384705,75.20084184495119,6.262487955668726,60.349100886792264,3.7945539156195487,34.56886711157111,1.9601948416949677,99.82440786870325,17.93087452582946,67.65471495589931,4.7903727911067815,52.29898260017819,3.0135204434714913,31.0522659160978,1.8184953129572932,100.53634253876818,18.544800983402446,69.13022113160365,5.036635229225181,52.44622841436909,3.025575236651027,33.364561593196164,1.9098288766402804,101.45737373266948,19.337677744697757,79.14071018974319,7.287203401685083,60.44948115564005,3.8061054053754897,37.96500981666448,2.1136413881018767]},{"theme":"dark","bg":"#1C1C1E","fgs":["#28282E","#303036","#F0F1FA","#B5B5BE","#94949D","#6C6C74","#94949D","#F0F1FA","#E3EEFF","#69A2FF","#1975FF","#004FBB","#FFF5F3","#FF877C","#FA0021","#B50014","#FFF7F1","#FF9833","#D87900","#A05800","#B9FFC0","#00CA4A","#00A43B","#007628","#EDEEFF","#9B99FF","#7664FF","#5500E7"],"flat":[0,1.1612081374032734,0,1.2977584057597347,-96.00102363023473,15.124963081358866,-61.434200333395395,8.359778481952823,-44.37695589123639,5.656999216809453,-25.852207004995158,3.269134017520218,-44.37695589123639,5.656999216809453,-96.00102363023473,15.124963081358866,-93.28410718321774,14.535439506623602,-51.011442228336165,6.654206660130301,-32.65955270222981,4.075313081844305,-16.370466566638967,2.297770413224217,-99.44736446714371,15.886233758087014,-55.07262883280597,7.299026812522581,-33.02825162884881,4.121422426501604,-17.569909707299868,2.4103015359642868,-100.24216533025177,16.063919770413232,-58.9183675206033,7.932997007393653,-42.52780974275247,5.391835753982905,-24.79898612463087,3.1522681596337323,-93.99035989085725,14.687777164248876,-57.733604751916815,7.735299184328377,-40.905085629416114,5.1638402024196015,-22.782862127638978,2.93464773079297,-94.53939096020551,14.80664328764592,-51.95162400089297,6.801199303451522,-32.707141977036414,4.081250761977603,-13.69412377461175,2.0581050526656677]},{"theme":"dark","bg":"#7C3AED","fgs":["#7A7A83","#85858D","#F2F2FD","#E3E4ED","#BBBCC5","#A6A6AF","#BBBCC5","#F2F2FD","#E8F1FF","#D5E6FF","#95BEFF","#498FFF","#FFF9F8","#FFDDD8","#FFA59B","#FF5F57","#FFFAF6","#FFDFC5","#FFA95E","#F18700","#C6FFCB","#7DFF91","#00DA51","#00B943","#F0F1FF","#E1E3FF","#B4B6FF","#8982FF"],"flat":[-8.022975593833932,1.3402714370624624,-13.06659651622355,1.5567826841029107,-72.79588351600884,5.127281558149197,-64.0643931275358,4.500912896664839,-41.08533982136385,3.0154992993085026,-29.446612137967147,2.3594628663401775,-41.08533982136385,3.0154992993085026,-72.79588351600884,5.127281558149197,-71.17583391665679,5.008590480308264,-64.08586870464498,4.502412953317652,-40.969901639859465,3.0086620775128243,-18.492326685553408,1.8062558438836862,-77.39176152066469,5.470028106981068,-64.06334225320066,4.50083949888721,-41.06186552000793,3.0141084284175577,-20.58109958398818,1.9067218663179524,-77.74802214310357,5.496966722090798,-64.16882972322054,4.508209630901898,-40.97688137217631,3.00907529195687,-26.93048349706687,2.2267208299060117,-71.5855645088526,5.038503428518723,-64.0583201718401,4.500488740880354,-41.24499881268402,3.0249663274797034,-25.961518214862377,2.176489332337808,-72.19208007215988,5.082914152992658,-64.24580556994597,4.513590807561596,-40.91738125342747,3.005553523356943,-18.74578210381434,1.818317299241165]},{"theme":"dark","bg":"#123456","fgs":["#3E3E45","#47474E","#F0F0FC","#BDBDC6","#A1A1AA","#7F7F88","#A1A1AA","#F0F0FC","#E5EFFF","#7AADFF","#3D87FF","#005FDF","#FFF6F5","#FF988E","#FF4D48","#D5001A","#FFF8F3","#FFA75A","#EA8300","#BA6700","#BDFFC3","#00D34E","#00B341","#008C31","#EEEFFF","#A5A5FF","#837AFF","#6433FF"],"flat":[0,1.1989453999458657,0,1.3802668927515667,-90.21171279539291,11.248602775503162,-60.350304841650235,6.81783465078813,-45.467647083484636,4.962060105853706,-28.90763677945104,3.2057711392885686,-45.467647083484636,4.962060105853706,-90.21171279539291,11.248602775503162,-88.52359832003891,10.974551054175622,-50.82334067294272,5.601029591424501,-33.90019185783247,3.6989242542195706,-17.94937633357589,2.2421371448335408,-94.53061684959366,11.962030249162733,-55.11693338695639,6.136943021617957,-35.65662848796355,3.880065704029081,-19.010500723408704,2.3279588656978594,-95.32685818617963,12.095472485308715,-58.832789738116276,6.617312123916825,-43.099834270009964,4.690265818580579,-27.202021451205145,3.0448373808729228,-89.02366089613628,11.055448870118658,-56.507119659393844,6.314879036055816,-41.91393002442956,4.556652813501584,-25.675484951698337,2.9041378624661793,-89.65880574741696,11.158543725868578,-51.69548126800453,5.708204299589472,-34.40239467659594,3.750316223165759,-16.078998268001875,2.0949612264666513]},{"theme":"dark","bg":"#ABCDEF","fgs":["#B4B4BE","#A8A8B1","#09090F","#55545C","#706F78","#A1A1AB","#706F78","#09090F","#00020F","#0045A5","#0060E2","#3F88FF","#140000","#A10011","#D7001B","#FF4F49","#190900","#814600","#A85D00","#EA8300","#000A01","#006320","#00822D","#00B341","#030014","#4A00CC","#6537FF","#847BFF"],"flat":[8.489900665165298,1.2445786124510558,14.633674840112363,1.428005718026211,75.26124671135368,12.018395958758797,52.798813706484495,4.5250375920814765,41.423484798334584,3.0020546798073307,18.08917030758312,1.5497949541740348,41.423484798334584,3.0020546798073307,75.26124671135368,12.018395958758797,76.15878035868923,12.51550617725489,56.792209524954714,5.308053794065892,44.82231091099692,3.370285744389143,29.010809014184062,2.057710850183084,75.84645644595844,12.343365988484052,55.500674792931,5.036843578382145,43.81499067450534,3.25473208522667,27.348178994525146,1.9655632934834584,74.80511978941351,11.764533211175078,52.793708756474935,4.52413702940673,41.44647230556496,3.0043476386355414,20.422200521634597,1.6411239547419576,75.54552640875083,12.176514522007174,52.82929323710673,4.530419449281277,41.44477504659015,3.004178254346042,21.57431352051131,1.6892460110571705,76.19495675605003,12.535354496536943,59.24767070343283,5.876074797996776,46.58341111523108,3.5866744685580465,28.54022915013225,2.030988108287992]},{"theme":"dark","bg":"#808080","fgs":["#F7F7FF","#0A0A10","#71717A","#fff","abcdef","#Ff3B30","#123","112233"],"flat":[-64.57463009518332,3.7055405145881393,38.97016765822309,4.9982958489854346,0,1.2236914579873204,-69.21596068658343,3.9494396480491156,-37.021615579693034,2.3904478169235754,0,1.1134158562260283,34.84722274356656,4.0887512719822805,34.84722274356656,4.0887512719822805]},{"theme":"light-ic","bg":"#FFFFFF","fgs":["#F7F7FF","#F7F7FF","#0A0A10","#71717A","#94949E","#BDBDC7","#94949E","#0A0A10","#000110","#0042E2","#3B78FF","#85AEFF","#150000","#BD0011","#FF001B","#FF998D","#1A0500","#B04A00","#E96400","#FFA174","#000B01","#007C2D","#00A73F","#00D452","#00040B","#00639C","#0089D6","#4DB3FF"],"flat":[0,1.0658201232723763,0,1.0658201232723763,104.65963437608701,19.74046779866239,70.1623092547118,4.832895561154151,53.96706418364319,3.004779237664955,33.34341874861831,1.8637664731545371,53.96706418364319,3.004779237664955,104.65963437608701,19.74046779866239,105.76809306682351,20.754602558566376,81.70242596755779,7.298044196933449,63.72862579130605,3.9499301799077116,41.321899652634976,2.211520376313395,105.32470288110206,20.35108757719671,79.14032751842986,6.6207260543602775,64.03544238362204,3.986463961054262,38.10332212672181,2.0602150617562085,104.62285904737456,19.706640245810163,73.93672186564804,5.489431142391079,57.895853978119064,3.3399091341000195,36.26437665881752,1.9807324696579875,104.97702200816887,20.032297013031588,73.2226464627747,5.355840815772706,56.123729301186145,3.182173571701684,36.54497093010938,1.9925549350335838,105.53601527934143,20.543945015271454,78.36297683006073,6.432254688155311,62.2832355663668,3.784280156272109,42.68471757482252,2.28072562681704]},{"theme":"light-ic","bg":"#000000","fgs":["#010103","#010103","#F1F1FC","#B3B3BC","#909099","#65656D","#909099","#F1F1FC","#E8F0FF","#709FFF","#2D6DFF","#0038C6","#FFF4F3","#FF8275","#F30019","#A7000E","#FFF6F1","#FF8C52","#DF5F00","#9D4100","#C2FFC9","#00C84D","#009F3C","#006E27","#DFF0FF","#10A5FF","#0082CB","#005689"],"flat":[0,1.0069471255994658,0,1.0069471255994658,-99.67218963360757,18.727928195985914,-63.80513707672789,10.090949031745104,-45.863656882792206,6.635143500688325,-26.33477890397734,3.635637824948841,-45.863656882792206,6.635143500688325,-99.67218963360757,18.727928195985914,-98.22794037003116,18.339196272629252,-53.8293996799751,8.092203727319841,-34.21813658812841,4.7430333536498335,-15.622162303180753,2.381112763636304,-102.4562582898777,19.486486384763445,-56.97432424245902,8.701923574139988,-34.76332175899368,4.824974728357804,-18.151652148372637,2.6494391947645526,-103.24826961310278,19.70448161329645,-59.10147589327934,9.125072654774526,-40.787696351505694,5.7744786453226835,-22.81212855995232,3.1897383862050708,-98.60837652742211,18.44127602271432,-60.30793382928855,9.368875012646658,-42.29572232514961,6.0245037012352585,-23.381466511664765,3.259668821930569,-97.12909045511768,18.04563033112125,-52.54162933208091,7.848085216280267,-36.273216344700636,5.055412083856902,-18.54110013749204,2.69234396770907]},{"theme":"light-ic","bg":"#808080","fgs":["#7A7A82","#7A7A82","#010103","#18171E","#37363E","#5B5B64","#37363E","#010103","#000002","#00104D","#002488","#0032B2","#040000","#3A0002","#750007","#98000B","#080100","#2E0E00","#602500","#8F3B00","#000200","#001D05","#004114","#006322","#000102","#00192E","#003A5F","#004D7C"],"flat":[0,1.0777393702545288,0,1.0777393702545288,40.18994509287661,5.280525528202377,36.81932911616927,4.505560397043893,27.93898600302036,3.0217055248761144,13.845788611978202,1.7013528597992067,27.93898600302036,3.0217055248761144,40.18994509287661,5.280525528202377,40.3306726916817,5.312553092992316,36.799792142283025,4.501224406456354,30.04772679653348,3.3069997994324063,23.797727835383583,2.533076638038498,40.23106479600237,5.289901483963394,36.80831529061993,4.503115584720142,27.941932493369514,3.022087143525339,21.190215752118235,2.2723970292907865,40.01441400964806,5.240352767492206,36.83929336929951,4.509994812829007,27.908315541084118,3.0177360720288915,16.77490328531677,1.9034128352995776,40.150141663678234,5.271436537678366,36.85731353296706,4.514000513189092,27.852320260403722,3.0105025419148825,16.643886294769857,1.8937047658181707,40.22977489962569,5.289607582090012,36.84755783504637,4.511831555904809,27.796281383025352,3.0032808914145437,21.058698283639867,2.260143784790762]},{"theme":"light-ic","bg":"#3A3A3C","fgs":["#3E3E45","#3E3E45","#F2F2FD","#BBBBC5","#9E9EA7","#7A7A82","#9E9EA7","#F2F2FD","#EAF1FF","#81ABFF","#4982FF","#0048F5","#FFF5F4","#FF9589","#FF403B","#CC0013","#FFF7F3","#FF9E6E","#F46900","#BD5000","#C6FFCC","#00D251","#00B043","#008631","#E1F1FF","#45B0FF","#0090E1","#006CA9"],"flat":[0,1.0699484563148034,0,1.0699484563148034,-89.06520303275792,10.211241554507897,-56.964055924386784,5.959497869408446,-41.60773958742316,4.2707825803649335,-24.25691832785775,2.6663016206717356,-41.60773958742316,4.2707825803649335,-89.06520303275792,10.211241554507897,-87.69551010135778,10.011246151186945,-48.263751486481056,4.97335266465752,-30.44825366943375,3.199530942583609,-12.043546933756078,1.753943193668956,-91.71639880150975,10.602849175762582,-51.72679347557047,5.3568510977778265,-31.21621892784593,3.268795397886287,-14.661523244642579,1.9330507439017046,-92.55409442667087,10.727809933310253,-53.89547122979923,5.603122076267749,-35.95806640240085,3.7112655792375078,-20.04367187639212,2.329915503818876,-87.89039906251095,10.039605852107476,-53.75183524673264,5.586666992924044,-38.313874917805634,3.940371955014153,-21.037647044419426,2.407284189061088,-86.58838125370931,9.850752189993974,-46.78872683576137,4.81370216625391,-31.383244337404896,3.2839492580871354,-15.736559790538532,2.009284883552937]},{"theme":"light-ic","bg":"#007AFF","fgs":["#797981","#797981","#020205","#16151C","#36353D","#72727A","#36353D","#020205","#000004","#000E48","#002895","#0042E3","#070000","#360001","#730006","#BE0011","#0C0200","#2B0D00","#5F2400","#B04A00","#000400","#001B05","#003F13","#007C2D","#000103","#00172B","#00395C","#00639D"],"flat":[0,1.0745907693903731,0,1.0745907693903731,39.47891404105317,5.158402202047827,36.63906407938327,4.513939148043301,27.726113654933034,3.015791844568742,0,1.187212547453551,27.726113654933034,3.015791844568742,39.47891404105317,5.158402202047827,39.74741500641279,5.218664231374835,36.670023859840576,4.5208004701163915,27.672031623266886,3.008810094200282,15.351341296852958,1.8098737923818007,39.57933742278427,5.181007308853048,36.654144364688555,4.517280323585742,27.751464091733354,3.019069960427185,12.671992881770905,1.6349566993987985,39.23716411822194,5.10371254671437,36.601471319299684,4.505617849545331,27.652865692456807,3.006339685224649,7.684471784052528,1.3665581876464832,39.39108202991847,5.138573889881769,36.624715730830154,4.510761781161014,27.902773646365304,3.038708829709556,0,1.3333017445114326,39.65687576920386,5.198408498908002,36.65150522712613,4.5166954713046765,27.64129393913477,3.0048491045091428,12.054359140378313,1.5979376774972005]},{"theme":"light-ic","bg":"#FF3B30","fgs":["#8A8A92","#8A8A92","#030306","#212129","#3E3D45","#76767F","#3E3D45","#030306","#000004","#001864","#002FA8","#0045ED","#080000","#4C0003","#830008","#C60012","#0D0200","#3E1500","#6C2A00","#B84D00","#000401","#002809","#004917","#00822F","#000103","#00233D","#00416A","#0068A4"],"flat":[0,1.0356140935636031,0,1.0356140935636031,43.57584669970237,5.806877937534237,38.3004661979112,4.504747008392574,29.108000765094282,3.0232304751839747,0,1.268226675946831,29.108000765094282,3.0232304751839747,43.57584669970237,5.806877937534237,43.98030914587015,5.909904848562026,38.293478372508915,4.503221171740978,28.94317626341717,3.002107337321695,17.95394033246701,1.9258322867444517,43.782795788734404,5.8597651770218455,38.38866051296957,4.524070782124866,28.984608537472,3.0074014381148784,15.156957289606964,1.7362054736619685,43.437607341847624,5.771384990646562,38.33399448678559,4.512078832150148,28.981628167432756,3.007020263830111,9.892674202630067,1.4441010369850493,43.614187977218386,5.816700203526642,38.406807106466914,4.528061863682665,29.022563392947294,3.012260432085235,8.92751562582044,1.3982348229239339,43.88976990866122,5.886966133555759,38.401524303038336,4.526899458852529,29.03631728859714,3.0140233900654434,14.306925700285792,1.683714708917422]},{"theme":"light-ic","bg":"#101012","fgs":["#15151B","#15151B","#F2F2FC","#B4B4BE","#93939C","#696972","#93939C","#F2F2FC","#E9F0FF","#74A2FF","#3371FF","#003CD0","#FFF4F3","#FF8679","#F8001A","#AF000F","#FFF6F2","#FF9058","#E36100","#A44400","#C3FFC9","#00CA4E","#00A33D","#007329","#E0F0FF","#21A8FF","#0085D0","#005B90"],"flat":[0,1.045340264415452,0,1.045340264415452,-98.5838326466521,17.08955684747884,-62.72698384194396,9.243246994613658,-45.67498009680545,6.239717532288448,-26.395025771311026,3.4974414103185016,-45.67498009680545,6.239717532288448,-98.5838326466521,17.08955684747884,-96.69585202312288,16.62869936569993,-53.53835262595417,7.561646720391368,-33.90853669724825,4.477228400656394,-15.643424374972522,2.314414627530853,-100.79897840003042,17.636700891614694,-56.5097869254783,8.089667628698802,-34.29633291417122,4.530985783372177,-18.10601775891745,2.56107712884258,-101.63443561648342,17.844851007069916,-58.71630179001587,8.4915976550775,-40.34850051574898,5.4087697654311055,-22.889197327723014,3.0820931228283226,-97.04991927160123,16.714741751700544,-59.578454823729174,8.650894122604043,-42.30409033566848,5.707629679225331,-23.50941826359813,3.1535587190594883,-95.5914257275877,16.361459308701495,-52.26386479821355,7.339891411251783,-35.96637326948896,4.765958094131511,-18.710865986535588,2.6239504003951875]},{"theme":"light-ic","bg":"#F6F8FA","fgs":["#F0F0FA","#F0F0FA","#0A0910","#707079","#8F8F99","#BCBCC5","#8F8F99","#0A0910","#000110","#0041DF","#3876FF","#82ACFF","#140000","#BB0011","#FC001B","#FF968A","#1A0500","#AE4900","#E76300","#FF9F70","#000B01","#007B2C","#00A63F","#00D252","#00040B","#00629B","#0088D4","#47B1FF"],"flat":[0,1.0634215248159875,0,1.0634215248159875,100.62201386979996,18.61882254505944,66.47667620763235,4.605948460093869,52.23653324893716,3.0084294463712395,29.780700684603257,1.7710808257265196,52.23653324893716,3.0084294463712395,100.62201386979996,18.61882254505944,101.64252534329776,19.49544279635548,78.08171358923367,6.990900026896573,60.31711036263554,3.7908833425501425,38.14751646036453,2.122399526754989,101.24542779029852,19.156162076239593,75.44574141529571,6.3202869230281635,60.62673294996029,3.82662941345977,34.94277926170305,1.9762445240799382,100.4972913238488,18.51105924752946,70.36281029021609,5.256397052662421,54.37780741212484,3.1906001595018556,32.840214506002376,1.8885138214456323,100.85145428464311,18.816958763489637,69.48045983543288,5.0977335979175304,52.4093478352768,3.0225491335598114,33.31123457222728,1.9076443919396007,101.41044755581565,19.297565623167603,74.60712675826916,6.125340559948453,58.63177587043786,3.604653750954651,39.55009522975671,2.1915837971666887]},{"theme":"light-ic","bg":"#1C1C1E","fgs":["#202028","#202028","#F1F1FD","#B6B6BF","#95959E","#6D6C75","#95959E","#F1F1FD","#E9F1FF","#76A4FF","#3875FF","#003ED7","#FFF4F3","#FF897C","#FC001B","#B50010","#FFF6F2","#FF935D","#E66300","#A94600","#C4FFCA","#00CB4F","#00A53F","#00772B","#E0F0FF","#2BA9FF","#0087D3","#005E95"],"flat":[0,1.0522076023272475,0,1.0522076023272475,-96.27140987076255,15.184146579953019,-61.97466067394124,8.452688508186561,-44.87094637123084,5.728792540137694,-25.977188885961883,3.283145241842075,-44.87094637123084,5.728792540137694,-96.27140987076255,15.184146579953019,-95.34499825068502,14.98175500729861,-52.621417231568934,6.906762415988252,-33.47849770408375,4.178062617945301,-14.893749254597887,2.163552488849563,-99.00844385529109,15.788447906049281,-55.63494162352738,7.390318911682041,-33.467257033822236,4.176644120449875,-17.547592326855693,2.408179342981014,-99.84390107174409,15.974784754119627,-57.911424929350886,7.764836527735112,-39.59013024218227,4.982347876116714,-22.33006111099907,2.8868875311347497,-95.39392806862337,14.992417327878547,-58.26045217149408,7.822951516486187,-41.372488477704024,5.229057297458628,-23.176065198031452,2.976455642863797,-93.8008911828484,14.646846343308852,-51.05892490110174,6.66159703590636,-35.06283530237813,4.380256350040106,-18.07990151348513,2.4590883821528062]},{"theme":"light-ic","bg":"#7C3AED","fgs":["#707078","#707078","#F4F4FE","#E3E4ED","#BDBDC6","#A6A6B0","#BDBDC6","#F4F4FE","#EDF3FF","#D8E6FF","#9ABDFF","#598FFF","#FFF8F7","#FFDDD8","#FFA59A","#FF6156","#FFF9F7","#FFDECE","#FFA77C","#FF7112","#D0FFD4","#7DFF93","#00D955","#00B947","#E5F3FF","#CEE8FF","#79C3FF","#0099EE"],"flat":[0,1.1608373681184396,0,1.1608373681184396,-73.98396246220521,5.215032461901156,-64.0643931275358,4.500912896664839,-41.75099800228171,3.055051474145459,-29.486682699107686,2.3616035737730514,-41.75099800228171,3.055051474145459,-73.98396246220521,5.215032461901156,-72.68698719753608,5.119268434409956,-64.43727527190525,4.526987198998707,-41.01457995095163,3.0113075347043563,-19.296469937711088,1.8446469211142276,-76.90365537296871,5.4332059719499135,-64.06334225320066,4.50083949888721,-41.0312066106476,3.012292267380192,-20.969079339398604,1.925648566411713,-77.34727635116013,5.466668081501757,-64.08510797117285,4.502359813071005,-40.89305028080999,3.004113887137535,-23.84545366268376,2.068528569757355,-72.9506558335481,5.138679113149092,-64.10668565953954,4.5038671985064,-40.827825177303865,3.0002560129496776,-26.013778316259767,2.1791858289450103,-71.69069599862902,5.046190228559761,-64.15987367011111,4.5075837056249375,-40.85863800181002,3.0020782433474276,-19.258832894479415,1.8428420367238694]},{"theme":"light-ic","bg":"#123456","fgs":["#36363D","#36363D","#F2F2FD","#BEBEC8","#A2A2AC","#808089","#A2A2AC","#F2F2FD","#EAF1FF","#86AEFF","#5189FF","#0650FF","#FFF6F4","#FF9B8F","#FF5349","#D70015","#FFF7F3","#FFA376","#FA6C00","#C75400","#C8FFCD","#00D553","#00B445","#008D34","#E2F1FF","#50B4FF","#0094E7","#0072B3"],"flat":[0,1.0609289970892934,0,1.0609289970892934,-91.39519035537737,11.4423466076867,-60.94367397154904,6.896920022598847,-46.020165423777584,5.026435795944436,-29.36949737122932,3.250019698594834,-46.020165423777584,5.026435795944436,-91.39519035537737,11.4423466076867,-90.02549742397721,11.218239018758677,-52.117590678997175,5.760385990810306,-35.453068631322715,3.8588722418561026,-17.16315496591595,2.1796295803105563,-94.48648764812391,11.954651961610136,-56.08049309972586,6.260045669338659,-36.68653001054492,3.9880923379680597,-19.42498133620683,2.3619323117143227,-94.88408174929032,12.021194381023134,-58.024655972390946,6.511549338355004,-40.19653971715428,4.366167533936967,-25.014063203920518,2.8441658803090872,-90.45906258458693,11.288985892577246,-57.51342916294683,6.44501257640493,-42.39985031117064,4.611196053798779,-26.099142978345885,2.9428672820648862,-89.03890865199516,11.05791933389335,-51.142776043878534,5.640183677550314,-35.55629387962385,3.86961291577043,-20.57349953874105,2.4573829383449617]},{"theme":"light-ic","bg":"#ABCDEF","fgs":["#C2C2CB","#C2C2CB","#06060C","#55545D","#706F78","#A1A1AA","#706F78","#06060C","#00010B","#0035BB","#044EFF","#4E86FF","#0F0000","#9F000C","#D60015","#FF4D45","#150400","#903B00","#BB4F00","#F86B00","#000801","#006322","#008230","#00B344","#000307","#005182","#0071B2","#0093E5"],"flat":[0,1.0705446339746656,0,1.0705446339746656,75.64582413051765,12.232204997602986,52.76667497081496,4.519371986307913,41.423484798334584,3.0020546798073307,18.127484103049046,1.5512322351556214,41.423484798334584,3.0020546798073307,75.64582413051765,12.232204997602986,76.30385323493446,12.594966222228084,58.55230095328762,5.70794052541694,46.62204712410211,3.5916381056717506,29.005214449343097,2.0573900879913625,76.05323451380515,12.457481581934719,55.93606418979168,5.126251708978212,44.077718465236906,3.284322001440957,27.730078776096413,1.986177453897667,75.43283879959024,12.113874624281086,52.77586207835023,4.520990578468311,41.44187963136357,3.003889327255304,23.868980516769618,1.7916338607346896,75.72958089661125,12.278654436624837,52.80881394476793,4.526802423347816,41.41072061989075,3.0007825637465393,21.536768417458934,1.687644517337887,76.14729893560808,12.509202438867769,55.739334564053664,5.085602713702602,43.07798550292857,3.173723442083943,28.271676351762963,2.015968674447834]},{"theme":"light-ic","bg":"#808080","fgs":["#F7F7FF","#0A0A10","#71717A","#fff","abcdef","#Ff3B30","#123","112233"],"flat":[-64.57463009518332,3.7055405145881393,38.97016765822309,4.9982958489854346,0,1.2236914579873204,-69.21596068658343,3.9494396480491156,-37.021615579693034,2.3904478169235754,0,1.1134158562260283,34.84722274356656,4.0887512719822805,34.84722274356656,4.0887512719822805]},{"theme":"dark-ic","bg":"#FFFFFF","fgs":["#E7E7F1","#D9D9E2","#0D0D12","#707079","#94949D","#BCBCC6","#94949D","#0D0D12","#000511","#005DAD","#0080EB","#67AEFF","#180001","#BB0023","#FB0032","#FF9690","#1D0D00","#A16200","#D38200","#FFA93F","#000E02","#007C2A","#00A63B","#00D24D","#060018","#6E00E2","#8F5BFF","#B49FFF"],"flat":[10.564452802176923,1.2281904700369508,18.372353597432387,1.402460683906872,104.27090588423802,19.38311547953439,70.6022439311581,4.903434648448188,54.00450766949271,3.0077315832596954,33.86624309585679,1.8839141891835494,54.00450766949271,3.0077315832596954,104.27090588423802,19.38311547953439,105.36973966240839,20.39226096069594,79.16304803673168,6.626348275559002,63.912632944603466,3.971781216419874,43.318131952880584,2.3140155027501375,105.16667360757414,20.20636839933495,79.43361118518992,6.693805381831162,64.79709340122825,4.079330310903514,38.90470830731088,2.096381256670562,103.76723276112338,18.92212884965907,70.7453629185683,4.926701923645705,54.109273202798526,3.0160152686700097,34.512224892089854,1.9092708836328856,104.66191026229393,19.742561272328775,73.25578885512941,5.361938351796974,56.58363991444959,3.2220248289870597,37.502971296697396,2.0337403797475795,105.5639998705614,20.569417077144905,82.76169093861434,7.605398027541734,65.05029978451972,4.11090668779679,42.03743585537794,2.2474520733771812]},{"theme":"dark-ic","bg":"#000000","fgs":["#030205","#08080D","#F0F0FA","#B1B1BB","#8F8F98","#64646D","#8F8F98","#F0F0FA","#DFEEFF","#4AA0FF","#007AE0","#005198","#FFF4F3","#FF7D79","#EF0030","#A6001E","#FFF7EF","#F99A00","#CA7D00","#915800","#B8FFBE","#00C648","#009F38","#006E24","#F0EDFF","#A98DFF","#894CFF","#6000C7"],"flat":[0,1.0147465549687367,0,1.0508696650285665,-99.01083964835996,18.549513785944853,-62.774763175831374,9.875849762659984,-45.37711439886134,6.5503119047070335,-25.95111170367181,3.5855589616463144,-45.37711439886134,6.5503119047070335,-99.01083964835996,18.549513785944853,-96.24720863865953,17.81140932471069,-52.09132740819334,7.763491481432673,-34.99617079306692,4.860177125212619,-17.977340963301042,2.6303715448994067,-102.4562582898777,19.486486384763445,-55.7461567674855,8.461541475204797,-34.016004840410574,4.712824540535038,-18.066789785866806,2.6401456993132153,-103.60398268701002,19.802704734429785,-61.68353878496926,9.650187857341214,-44.76775468944945,6.444757556916112,-26.060791619056427,3.599837857825526,-97.27916136850891,18.085610527348415,-59.33082759509665,9.171209294707863,-42.24705692654848,6.016359585841944,-23.350526713793172,3.2558471191579894,-97.94239544297146,18.262727694350296,-53.03355221198167,7.94095381214551,-32.857248309508414,4.541449578831631,-15.041171256747345,2.322063533056465]},{"theme":"dark-ic","bg":"#808080","fgs":["#707078","#66666E","#030305","#18171D","#37363E","#5D5D64","#37363E","#030305","#000103","#001834","#00376B","#004A8C","#070000","#390005","#750012","#99001B","#0D0400","#261300","#522F00","#865100","#000300","#001D05","#004112","#006521","#010005","#22004F","#43008F","#5800B7"],"flat":[0,1.24296889833444,9.274501031402123,1.4407196981948422,39.91548097777799,5.217615096019717,36.836632862345205,4.5094036578339445,27.93898600302036,3.0217055248761144,13.086665786299626,1.6537770693375544,27.93898600302036,3.0217055248761144,39.91548097777799,5.217615096019717,40.21965913293546,5.287302224624218,36.81767378273908,4.505192880903515,27.950777846769416,3.023233051937069,21.000772856623527,2.25477321245894,40.142120786515875,5.269603471071473,36.88792093057972,4.5208109211231795,27.866909320024153,3.0123854877509646,20.876190217747006,2.243276608438576,39.57947058767665,5.139984882718895,36.89828467875601,4.523118840482662,27.8635036155662,3.011945822214043,13.292640012092916,1.6665035199669143,40.051433392362526,5.248844071123953,36.85731353296706,4.514000513189092,27.870384117529113,3.0128341396537346,15.95953035602916,1.844055168824021,40.26997047853993,5.298759395894901,36.80600977189143,4.502603953746083,29.651273965169533,3.2515347547731954,23.802145579236715,2.533547958530533]},{"theme":"dark-ic","bg":"#3A3A3C","fgs":["#46464E","#4F4F57","#F1F1FA","#BABAC4","#9D9DA6","#797982","#9D9DA6","#F1F1FA","#E1EFFF","#62ACFF","#0087F7","#0065BB","#FFF5F4","#FF928C","#FF3644","#CA0027","#FFF8F1","#FFA533","#DD8900","#AD6A00","#BCFFC2","#00D04C","#00AF3F","#00862E","#F1EFFF","#B29CFF","#956AFF","#7700F5"],"flat":[0,1.2140718035663858,0,1.3988859584097761,-88.35603271482154,10.107493939375463,-56.41682578047019,5.8952705955406675,-41.10063170437542,4.219157833086703,-23.840055313660137,2.6320437618590202,-41.10063170437542,4.219157833086703,-88.35603271482154,10.107493939375463,-85.70519954500536,9.72347127561709,-46.57313915269262,4.790554592637207,-29.772868283167547,3.1391774083572677,-14.678197720495364,1.9342213705917548,-91.71639880150975,10.602849175762582,-50.907190680193835,5.264997878479995,-30.103011927821978,3.1686137953283313,-14.381529067368087,1.9134497328938256,-92.9101803888625,10.781105347058572,-55.36632323767275,5.772792653136124,-40.27550194757439,4.135743433221995,-23.629833528615176,2.614848030728214,-86.56463892830851,9.847321829129962,-52.75275099572737,5.472775232316544,-37.83315401910713,3.893128667535661,-21.00389596511428,2.404636707585164,-87.71836703917482,10.014570561299498,-47.715877818006604,4.91379345402092,-29.619990398118716,3.125589171245837,-10.872649839108671,1.6769004806806627]},{"theme":"dark-ic","bg":"#007AFF","fgs":["#6F6F77","#65656D","#040408","#16151B","#36353D","#72727A","#36353D","#040408","#000106","#001632","#00366A","#005EAF","#0B0000","#360004","#730011","#BD0023","#100600","#241200","#502E00","#A36300","#000501","#001B04","#004012","#007D2B","#020009","#20004A","#48009A","#6F00E5"],"flat":[0,1.2399763057879079,9.137153276370956,1.4379357071162235,39.199487305374596,5.095159290353486,36.655643103420836,4.517612479062309,27.726113654933034,3.015791844568742,0,1.187212547453551,27.726113654933034,3.015791844568742,39.199487305374596,5.095159290353486,39.62660370456575,5.191620542906115,36.592103069401674,4.503545882983851,27.695057695096033,3.0117807086970667,12.482735608358766,1.623488823202914,39.46146930028483,5.1544680971312,36.631002505660305,4.512153756975423,27.677944212691237,3.0095726068531357,12.751768680957815,1.6398240151911772,38.729462058672325,4.987982714010286,36.579572970042136,4.500775701626332,27.747483785710887,3.0185550274007946,0,1.2039987886628472,39.28492308543262,5.114543979353608,36.632422880871374,4.512468289727634,27.610461383700073,3.00088106913278,0,1.3173837917708568,39.63684952401951,5.1939188146581685,36.601297620325695,4.505579426399357,27.609531401873983,3.0007614645527125,16.085712499624492,1.86220122646402]},{"theme":"dark-ic","bg":"#FF3B30","fgs":["#7F7F87","#74747D","#040408","#212128","#3E3D45","#76767F","#3E3D45","#040408","#000206","#002245","#003E78","#0062B6","#0B0000","#4C0008","#820015","#C50025","#110600","#341C00","#5B3600","#A96700","#000601","#002808","#004916","#00822D","#02000A","#2E0067","#5300AD","#7400EE"],"flat":[0,1.119564506502328,0,1.304916965157494,43.43238144483196,5.770041002680692,38.322111022898454,4.5094781825179275,29.108000765094282,3.0232304751839747,0,1.268226675946831,29.108000765094282,3.0232304751839747,43.43238144483196,5.770041002680692,43.7603252928922,5.854038847974277,38.29235457140463,4.502975854531618,29.083956028514045,3.020138635078335,15.054852030501879,1.729780743830801,43.694363439742204,5.837205585263101,38.354206470247746,4.516507237210082,29.087182234169767,3.020553277003921,15.230546565278194,1.740856512459411,42.92491991360228,5.63897133999608,38.33365423714702,4.512004338988724,28.93769973409828,3.0014083479801514,0,1.2794758858630608,43.42248485866928,5.7674953973596095,38.41370496023468,4.529580289565808,29.031977011275746,3.0134669332394415,8.949958172317846,1.3992774406692907,43.85966524740055,5.879321607935794,38.335090707081115,4.512318847459295,28.952846517354917,3.003342035233188,18.7969313728254,1.9885281036480773]},{"theme":"dark-ic","bg":"#101012","fgs":["#1C1C22","#24242A","#F0F0FA","#B3B3BD","#92929B","#686871","#92929B","#F0F0FA","#E0EEFF","#4FA3FF","#007DE4","#00559F","#FFF4F3","#FF817D","#F40031","#AD001F","#FFF7EF","#FB9C00","#CE7F00","#965B00","#B9FFBF","#00C849","#00A239","#007226","#F0EEFF","#AA90FF","#8C52FF","#6500D0"],"flat":[0,1.1210017368042908,0,1.2316217101091165,-97.35355975851269,16.788671896406843,-62.18912907483276,9.1408846014785,-45.183954583215964,6.160892481462449,-25.971291980892453,3.445784610130369,-45.183954583215964,6.160892481462449,-97.35355975851269,16.788671896406843,-94.7101738760577,16.149472101206463,-51.76844025191021,7.254462182627787,-34.605057818633654,4.573999892517157,-17.84521283217997,2.534242228411278,-100.79897840003042,17.636700891614694,-55.24750846744172,7.863491890193123,-33.54190885081095,4.42668652015819,-17.815136446883052,2.5311583129351276,-101.94670279716274,17.922901715066683,-61.02785966423507,8.921519321161206,-44.34904450398409,6.02788949767693,-25.86724540779468,3.4331609270477186,-95.74776769254933,16.399183837627458,-58.596724506079966,8.469603286006345,-41.840254848173814,5.636085625741139,-23.122294138224145,3.108849028833141,-96.71860922522349,16.634224259107555,-52.4354046324141,7.369572153296626,-32.71416296796099,4.3135863728502315,-14.766203746026353,2.2302399315399106]},{"theme":"dark-ic","bg":"#F6F8FA","fgs":["#E0E0EA","#D2D2DC","#0D0D12","#717179","#8F8F98","#BCBCC6","#8F8F98","#0D0D12","#000511","#005DAD","#0080EB","#67AEFF","#180001","#BB0023","#FB0032","#FF9690","#1D0D00","#A16200","#CC7E00","#FFA93F","#000E02","#007C2A","#00A63B","#00D24D","#060018","#6E00E1","#8F5BFF","#B49FFF"],"flat":[10.34292311627503,1.2313464424988367,18.024461551245693,1.4097592283337514,100.14533816071224,18.207162386274927,66.07133001527934,4.544848448940367,52.273633182943,3.011452358576948,29.740675372331037,1.769619110018133,52.273633182943,3.011452358576948,100.14533816071224,18.207162386274927,101.24417193888262,19.155084079579783,75.03748031320593,6.224334741672483,59.78706522107772,3.730817455350078,39.19256422935483,2.1736266322828217,101.04110588404839,18.980469423093126,75.30804346166417,6.287699296730837,60.6715256777025,3.8318416601446055,34.77914058378512,1.9691960254812915,99.64166503759763,17.774143327140518,66.61979519504256,4.627804134340593,52.265155427331,3.010761189364384,30.386657168564103,1.79343744066295,100.53634253876818,18.544800983402446,69.13022113160365,5.036635229225181,52.45807219092383,3.026547994099155,33.37740357317164,1.9103555042369325,101.43843214703563,19.321492322017065,78.73738474892319,7.17242701812758,60.92473206099399,3.861502331684902,37.91186813185219,2.111101535692422]},{"theme":"dark-ic","bg":"#1C1C1E","fgs":["#28282E","#303036","#F0F1FA","#B5B5BE","#94949D","#6C6C74","#94949D","#F0F1FA","#E0EEFF","#53A4FF","#007FE8","#0058A5","#FFF4F3","#FF8580","#F80032","#B30021","#FFF7F0","#FD9D00","#D18100","#9B5E00","#B9FFC0","#00CA4A","#00A43A","#007628","#F0EEFF","#AC93FF","#8D57FF","#6800D8"],"flat":[0,1.1612081374032734,0,1.2977584057597347,-96.00102363023473,15.124963081358866,-61.434200333395395,8.359778481952823,-44.37695589123639,5.656999216809453,-25.852207004995158,3.269134017520218,-44.37695589123639,5.656999216809453,-96.00102363023473,15.124963081358866,-92.91963933131836,14.457074514504098,-50.53751898728166,6.580638229837842,-33.762466759799054,4.213972880586457,-17.265259153354464,2.3814244063048684,-99.00844385529109,15.788447906049281,-54.621704682580784,7.2261694596451385,-32.708249696736004,4.081389020198301,-17.26088719592284,2.381011454094406,-100.19905346524207,16.054261379481122,-59.919533209261445,8.101702037099825,-43.64240726631874,5.55098822531801,-25.561703474916055,3.236683846808585,-93.99035989085725,14.687777164248876,-57.733604751916815,7.735299184328377,-40.893113879964474,5.162174617584753,-22.782862127638978,2.93464773079297,-94.92807468048419,14.891026660055605,-51.83505302329671,6.7828987244211,-32.07221654016657,4.002369709649521,-14.066989001346808,2.090531498955086]},{"theme":"dark-ic","bg":"#7C3AED","fgs":["#7A7A83","#85858D","#F2F2FD","#E3E4ED","#BBBCC5","#A6A6AF","#BBBCC5","#F2F2FD","#E5F1FF","#D2E7FF","#8AC0FF","#2091FF","#FFF9F8","#FFDDDA","#FFA49E","#FF5C5D","#FFFAF5","#FFE0C0","#FFAA42","#E99000","#C6FFCB","#7DFF91","#00DA50","#00B943","#F3F1FF","#E6E2FF","#C1B2FF","#9D7AFF"],"flat":[-8.022975593833932,1.3402714370624624,-13.06659651622355,1.5567826841029107,-72.79588351600884,5.127281558149197,-64.0643931275358,4.500912896664839,-41.08533982136385,3.0154992993085026,-29.446612137967147,2.3594628663401775,-41.08533982136385,3.0154992993085026,-72.79588351600884,5.127281558149197,-70.80493468751378,4.981574213000164,-64.1741844582375,4.5085838821814495,-40.86541522684489,3.0024791016401666,-17.915271666932362,1.7789289638170434,-77.39176152066469,5.470028106981068,-64.14420274941838,4.506488572747314,-40.82594869447877,3.0001450551263997,-20.102672577261966,1.8834970356793017,-77.70409895709017,5.493642615376888,-64.40976920815079,4.525061719384059,-40.93902681557826,3.00683450902807,-28.2154352545988,2.2940964528512438,-71.5855645088526,5.038503428518723,-64.0583201718401,4.500488740880354,-41.23137463469248,3.02415799426954,-25.961518214862377,2.176489332337808,-72.5833510184408,5.111647048756453,-64.45521953810703,4.528243512077896,-40.9348520371047,3.006587427640115,-18.521762858381592,1.807654812879132]},{"theme":"dark-ic","bg":"#123456","fgs":["#3E3E45","#47474E","#F0F0FC","#BDBDC6","#A1A1AA","#7F7F88","#A1A1AA","#F0F0FC","#E1EFFF","#68AEFF","#008BFD","#006AC4","#FFF6F5","#FF9791","#FF494F","#D30029","#FFF8F1","#FFA941","#E28C00","#B46E00","#BEFFC3","#00D34D","#00B340","#008C31","#F1EFFF","#B49FFF","#9971FF","#7D09FF"],"flat":[0,1.1989453999458657,0,1.3802668927515667,-90.21171279539291,11.248602775503162,-60.350304841650235,6.81783465078813,-45.467647083484636,4.962060105853706,-28.90763677945104,3.2057711392885686,-45.467647083484636,4.962060105853706,-90.21171279539291,11.248602775503162,-88.0351868676248,10.895768939711282,-50.01919534416472,5.502978516969682,-33.92042169291382,3.7009882121251625,-19.118481017052428,2.3367853131295435,-94.53061684959366,11.962030249162733,-54.89593536437227,6.108854061876529,-35.13675470036395,3.8260433618990066,-18.695569008603144,2.3023140000481574,-95.24016771148194,12.080915286992608,-59.18458546592085,6.6635751999987605,-44.37309985295556,4.8355935478331284,-28.125674388660414,3.1315039953558483,-89.11957165172129,11.070992163190612,-56.49372778683512,6.313154752438012,-41.901500594929324,4.555261373045625,-25.675484951698337,2.9041378624661793,-90.04835436179425,11.221964232051386,-51.2922333885473,5.658542947646642,-34.21126239038315,3.7307191421666435,-15.254686874750304,2.03178289437105]},{"theme":"dark-ic","bg":"#ABCDEF","fgs":["#B4B4BE","#A8A8B1","#09090F","#55545C","#706F78","#A1A1AB","#706F78","#09090F","#00030D","#004D92","#006CC7","#008BFE","#130001","#9F001C","#D60029","#FF4B50","#190B00","#7B4A00","#A06100","#E38C00","#000A01","#006320","#00822D","#00B341","#040013","#5B00BE","#7E16FF","#9971FF"],"flat":[8.489900665165298,1.2445786124510558,14.633674840112363,1.428005718026211,75.26124671135368,12.018395958758797,52.798813706484495,4.5250375920814765,41.423484798334584,3.0020546798073307,18.08917030758312,1.5497949541740348,41.423484798334584,3.0020546798073307,75.26124671135368,12.018395958758797,76.08600684668272,12.475516218126787,55.96473752983635,5.13221099097022,43.3369682989835,3.2018590596167122,29.318326289650187,2.0754572365320043,75.88156299830736,12.362776535253154,55.82217638546274,5.102669267083783,43.91931922104849,3.266436635421996,27.87409429449102,1.9940348577564484,74.63415754799017,11.66954886455628,52.68236753448497,4.504554642142053,41.632593181405234,3.023005829909607,19.021862742942993,1.5853635699211195,75.54552640875083,12.176514522007174,52.82929323710673,4.530419449281277,41.44477504659015,3.004178254346042,21.57431352051131,1.6892460110571705,76.18160530883091,12.528031661692317,58.96992438550627,5.808209180198148,47.36214126331064,3.68860336114861,29.106756402593504,2.0632235490419992]},{"theme":"dark-ic","bg":"#808080","fgs":["#F7F7FF","#0A0A10","#71717A","#fff","abcdef","#Ff3B30","#123","112233"],"flat":[-64.57463009518332,3.7055405145881393,38.97016765822309,4.9982958489854346,0,1.2236914579873204,-69.21596068658343,3.9494396480491156,-37.021615579693034,2.3904478169235754,0,1.1134158562260283,34.84722274356656,4.0887512719822805,34.84722274356656,4.0887512719822805]}],"resolveVars":[{"theme":"light","bg":"#FFFFFF","vars":{"--lab-bg-tone-2":"oklch(97.84197% 0.010603 286.202)","--lab-bg-tone-3":"oklch(97.84197% 0.010603 286.202)","--lab-label-primary":"oklch(19.12257% 0.014100 291.556)","--lab-label-secondary":"oklch(56.54196% 0.013721 285.953)","--lab-label-tertiary":"oklch(66.97448% 0.014606 285.999)","--lab-label-quaternary":"oklch(81.39504% 0.013897 286.087)","--lab-border-strong":"oklch(19.12257% 0.014100 291.556)","--lab-border-base":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-border-soft":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-primary":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-fill-secondary":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fill-tertiary":"oklch(57.53363% 0.012136 286.012 / 0.122)","--lab-fill-quaternary":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fx-shadow-minor":"oklch(17.39406% 0.004094 285.967 / 0.012)","--lab-fx-shadow-ambient":"oklch(17.39406% 0.004094 285.967 / 0.02)","--lab-fx-shadow-penumbra":"oklch(17.39406% 0.004094 285.967 / 0.039)","--lab-fx-shadow-major":"oklch(17.39406% 0.004094 285.967 / 0.122)","--lab-label-brand-primary":"oklch(19.39430% 0.069681 257.297)","--lab-label-brand-secondary":"oklch(57.33316% 0.205939 257.291)","--lab-label-brand-tertiary":"oklch(67.12978% 0.175419 257.690)","--lab-label-brand-quaternary":"oklch(81.31388% 0.094313 257.967)","--lab-fill-brand-primary":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-brand-secondary":"oklch(60.27647% 0.217712 257.424 / 0.078)","--lab-fill-brand-tertiary":"oklch(60.27647% 0.217712 257.424 / 0.039)","--lab-fill-brand-quaternary":"oklch(60.27647% 0.217712 257.424 / 0.02)","--lab-border-brand-strong":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-border-brand-base":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-brand-soft":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-label-danger-primary":"oklch(20.16318% 0.082740 29.234)","--lab-label-danger-secondary":"oklch(59.62536% 0.244212 28.655)","--lab-label-danger-tertiary":"oklch(69.12968% 0.198650 28.574)","--lab-label-danger-quaternary":"oklch(82.55135% 0.097676 29.389)","--lab-fill-danger-primary":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-fill-danger-secondary":"oklch(65.42146% 0.232135 28.659 / 0.078)","--lab-fill-danger-tertiary":"oklch(65.42146% 0.232135 28.659 / 0.039)","--lab-fill-danger-quaternary":"oklch(65.42146% 0.232135 28.659 / 0.02)","--lab-border-danger-strong":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-danger-base":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-danger-soft":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-warning-primary":"oklch(19.32631% 0.042728 66.944)","--lab-label-warning-secondary":"oklch(57.27465% 0.125021 68.799)","--lab-label-warning-tertiary":"oklch(67.73500% 0.148484 68.161)","--lab-label-warning-quaternary":"oklch(82.04043% 0.139600 68.374)","--lab-fill-warning-primary":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-fill-warning-secondary":"oklch(78.56796% 0.171718 68.607 / 0.078)","--lab-fill-warning-tertiary":"oklch(78.56796% 0.171718 68.607 / 0.039)","--lab-fill-warning-quaternary":"oklch(78.56796% 0.171718 68.607 / 0.02)","--lab-border-warning-strong":"oklch(67.73500% 0.148484 68.161 / 1)","--lab-border-warning-base":"oklch(78.56796% 0.171718 68.607 / 0.2)","--lab-border-warning-soft":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-label-success-primary":"oklch(18.59864% 0.054816 147.311)","--lab-label-success-secondary":"oklch(54.91588% 0.161394 147.418)","--lab-label-success-tertiary":"oklch(64.88263% 0.190555 147.443)","--lab-label-success-quaternary":"oklch(79.02491% 0.232159 147.432)","--lab-fill-success-primary":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-fill-success-secondary":"oklch(73.03242% 0.194381 147.444 / 0.078)","--lab-fill-success-tertiary":"oklch(73.03242% 0.194381 147.444 / 0.039)","--lab-fill-success-quaternary":"oklch(73.03242% 0.194381 147.444 / 0.02)","--lab-border-success-strong":"oklch(64.96409% 0.172888 147.450 / 1)","--lab-border-success-base":"oklch(73.03242% 0.194381 147.444 / 0.2)","--lab-border-success-soft":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-label-info-primary":"oklch(19.54489% 0.077696 259.509)","--lab-label-info-secondary":"oklch(57.68997% 0.232691 259.838)","--lab-label-info-tertiary":"oklch(67.26428% 0.173598 259.980)","--lab-label-info-quaternary":"oklch(81.49851% 0.092914 259.576)","--lab-fill-info-primary":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fill-info-secondary":"oklch(64.04613% 0.193058 259.892 / 0.078)","--lab-fill-info-tertiary":"oklch(64.04613% 0.193058 259.892 / 0.039)","--lab-fill-info-quaternary":"oklch(64.04613% 0.193058 259.892 / 0.02)","--lab-border-info-strong":"oklch(64.04613% 0.193058 259.892 / 1)","--lab-border-info-base":"oklch(64.04613% 0.193058 259.892 / 0.2)","--lab-border-info-soft":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fx-focus-ring-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fx-focus-ring-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-fx-focus-ring-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-fx-focus-ring-neutral":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-fx-glow-brand":"oklch(60.27647% 0.217712 257.424)","--lab-fx-glow-brand-core":"oklch(78.76611% 0.108476 257.256)","--lab-fx-glow-brand-alpha":"0.5","--lab-fx-glow-danger":"oklch(65.42146% 0.232135 28.659)","--lab-fx-glow-danger-core":"oklch(81.77103% 0.102764 29.025)","--lab-fx-glow-danger-alpha":"0.5","--lab-fx-glow-warning":"oklch(78.56796% 0.171718 68.607)","--lab-fx-glow-warning-core":"oklch(88.88181% 0.082838 68.777)","--lab-fx-glow-warning-alpha":"0.5","--lab-fx-glow-neutral":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-core":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-alpha":"0.5","--lab-fx-glow-inverted":"oklch(75.99692% 0.012723 286.093 / 0.522)","--lab-fx-skeleton-highlight":"oklch(57.53363% 0.012136 286.012 / 0.039)","--lab-fill-neutral":"oklch(57.53363% 0.012136 286.012 / 1)","--lab-fill-accent-tinted":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-danger-tinted":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-label-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-accent":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-danger":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-focus":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-badge-fill-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-badge-fill-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-badge-fill-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-badge-fill-success":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-badge-fill-info":"oklch(64.04613% 0.193058 259.892 / 1)","--lab-badge-fill-static-dark":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-badge-fill-static-light":"oklch(100.00000% 0.000000 89.876 / 1)","--lab-fill-neutral-tinted":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-border-neutral":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fx-skeleton-base":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fill-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-icon":"oklch(66.97448% 0.014606 285.999)"}},{"theme":"light","bg":"#000000","vars":{"--lab-bg-tone-2":"oklch(7.11623% 0.013707 282.350)","--lab-bg-tone-3":"oklch(7.11623% 0.013707 282.350)","--lab-label-primary":"oklch(98.65156% 0.006606 286.278)","--lab-label-secondary":"oklch(80.14049% 0.013952 286.081)","--lab-label-tertiary":"oklch(69.23260% 0.013029 286.055)","--lab-label-quaternary":"oklch(55.16643% 0.013808 285.938)","--lab-border-strong":"oklch(98.65156% 0.006606 286.278)","--lab-border-base":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-border-soft":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-primary":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-fill-secondary":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fill-tertiary":"oklch(57.53363% 0.012136 286.012 / 0.122)","--lab-fill-quaternary":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fx-shadow-minor":"oklch(17.39406% 0.004094 285.967 / 0.012)","--lab-fx-shadow-ambient":"oklch(17.39406% 0.004094 285.967 / 0.02)","--lab-fx-shadow-penumbra":"oklch(17.39406% 0.004094 285.967 / 0.039)","--lab-fx-shadow-major":"oklch(17.39406% 0.004094 285.967 / 0.122)","--lab-label-brand-primary":"oklch(98.69528% 0.006207 255.474)","--lab-label-brand-secondary":"oklch(80.06451% 0.101154 257.822)","--lab-label-brand-tertiary":"oklch(69.35433% 0.162198 257.610)","--lab-label-brand-quaternary":"oklch(55.96293% 0.201872 257.393)","--lab-fill-brand-primary":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-brand-secondary":"oklch(60.27647% 0.217712 257.424 / 0.078)","--lab-fill-brand-tertiary":"oklch(60.27647% 0.217712 257.424 / 0.039)","--lab-fill-brand-quaternary":"oklch(60.27647% 0.217712 257.424 / 0.02)","--lab-border-brand-strong":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-border-brand-base":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-brand-soft":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-label-danger-primary":"oklch(98.64532% 0.006567 28.833)","--lab-label-danger-secondary":"oklch(81.38408% 0.105319 28.863)","--lab-label-danger-tertiary":"oklch(71.19739% 0.181099 28.350)","--lab-label-danger-quaternary":"oklch(58.10538% 0.238004 28.677)","--lab-fill-danger-primary":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-fill-danger-secondary":"oklch(65.42146% 0.232135 28.659 / 0.078)","--lab-fill-danger-tertiary":"oklch(65.42146% 0.232135 28.659 / 0.039)","--lab-fill-danger-quaternary":"oklch(65.42146% 0.232135 28.659 / 0.02)","--lab-border-danger-strong":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-danger-base":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-danger-soft":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-warning-primary":"oklch(98.72985% 0.009577 72.664)","--lab-label-warning-secondary":"oklch(80.98123% 0.149940 68.750)","--lab-label-warning-tertiary":"oklch(70.11346% 0.152989 68.857)","--lab-label-warning-quaternary":"oklch(55.86543% 0.122417 68.219)","--lab-fill-warning-primary":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-fill-warning-secondary":"oklch(78.56796% 0.171718 68.607 / 0.078)","--lab-fill-warning-tertiary":"oklch(78.56796% 0.171718 68.607 / 0.039)","--lab-fill-warning-quaternary":"oklch(78.56796% 0.171718 68.607 / 0.02)","--lab-border-warning-strong":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-border-warning-base":"oklch(78.56796% 0.171718 68.607 / 0.2)","--lab-border-warning-soft":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-label-success-primary":"oklch(98.31403% 0.027558 147.033)","--lab-label-success-secondary":"oklch(77.69544% 0.228159 147.448)","--lab-label-success-tertiary":"oklch(67.10085% 0.197388 147.383)","--lab-label-success-quaternary":"oklch(53.45797% 0.157009 147.442)","--lab-fill-success-primary":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-fill-success-secondary":"oklch(73.03242% 0.194381 147.444 / 0.078)","--lab-fill-success-tertiary":"oklch(73.03242% 0.194381 147.444 / 0.039)","--lab-fill-success-quaternary":"oklch(73.03242% 0.194381 147.444 / 0.02)","--lab-border-success-strong":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-border-success-base":"oklch(73.03242% 0.194381 147.444 / 0.2)","--lab-border-success-soft":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-label-info-primary":"oklch(98.69528% 0.006207 255.474)","--lab-label-info-secondary":"oklch(80.30515% 0.099317 259.806)","--lab-label-info-tertiary":"oklch(69.49027% 0.160387 260.058)","--lab-label-info-quaternary":"oklch(56.23879% 0.227606 259.853)","--lab-fill-info-primary":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fill-info-secondary":"oklch(64.04613% 0.193058 259.892 / 0.078)","--lab-fill-info-tertiary":"oklch(64.04613% 0.193058 259.892 / 0.039)","--lab-fill-info-quaternary":"oklch(64.04613% 0.193058 259.892 / 0.02)","--lab-border-info-strong":"oklch(64.04613% 0.193058 259.892 / 1)","--lab-border-info-base":"oklch(64.04613% 0.193058 259.892 / 0.2)","--lab-border-info-soft":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fx-focus-ring-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fx-focus-ring-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-fx-focus-ring-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-fx-focus-ring-neutral":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-fx-glow-brand":"oklch(60.27647% 0.217712 257.424)","--lab-fx-glow-brand-core":"oklch(78.76611% 0.108476 257.256)","--lab-fx-glow-brand-alpha":"0.013010286081645773","--lab-fx-glow-danger":"oklch(65.42146% 0.232135 28.659)","--lab-fx-glow-danger-core":"oklch(81.77103% 0.102764 29.025)","--lab-fx-glow-danger-alpha":"0.012071078431372548","--lab-fx-glow-warning":"oklch(78.56796% 0.171718 68.607)","--lab-fx-glow-warning-core":"oklch(88.88181% 0.082838 68.777)","--lab-fx-glow-warning-alpha":"0.009560345877481427","--lab-fx-glow-neutral":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-core":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-alpha":"0.00784313725490196","--lab-fx-glow-inverted":"oklch(75.99692% 0.012723 286.093 / 0.522)","--lab-fx-skeleton-highlight":"oklch(57.53363% 0.012136 286.012 / 0.039)","--lab-fill-neutral":"oklch(57.53363% 0.012136 286.012 / 1)","--lab-fill-accent-tinted":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-danger-tinted":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-label-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-accent":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-danger":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-focus":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-badge-fill-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-badge-fill-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-badge-fill-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-badge-fill-success":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-badge-fill-info":"oklch(64.04613% 0.193058 259.892 / 1)","--lab-badge-fill-static-dark":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-badge-fill-static-light":"oklch(100.00000% 0.000000 89.876 / 1)","--lab-fill-neutral-tinted":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-border-neutral":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fx-skeleton-base":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fill-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-icon":"oklch(69.23260% 0.013029 286.055)"}},{"theme":"light","bg":"#808080","vars":{"--lab-bg-tone-2":"oklch(58.21536% 0.012099 286.018)","--lab-bg-tone-3":"oklch(58.21536% 0.012099 286.018)","--lab-label-primary":"oklch(13.77177% 0.013406 284.503)","--lab-label-secondary":"oklch(20.89710% 0.013787 291.711)","--lab-label-tertiary":"oklch(33.70211% 0.013945 291.371)","--lab-label-quaternary":"oklch(49.18380% 0.012632 285.926)","--lab-border-strong":"oklch(13.77177% 0.013406 284.503)","--lab-border-base":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-border-soft":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-primary":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-fill-secondary":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fill-tertiary":"oklch(57.53363% 0.012136 286.012 / 0.122)","--lab-fill-quaternary":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fx-shadow-minor":"oklch(17.39406% 0.004094 285.967 / 0.012)","--lab-fx-shadow-ambient":"oklch(17.39406% 0.004094 285.967 / 0.02)","--lab-fx-shadow-penumbra":"oklch(17.39406% 0.004094 285.967 / 0.039)","--lab-fx-shadow-major":"oklch(17.39406% 0.004094 285.967 / 0.122)","--lab-label-brand-primary":"oklch(13.49058% 0.050205 258.119)","--lab-label-brand-secondary":"oklch(20.89968% 0.076454 257.725)","--lab-label-brand-tertiary":"oklch(34.17043% 0.122830 257.309)","--lab-label-brand-quaternary":"oklch(49.70427% 0.178878 257.337)","--lab-fill-brand-primary":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-brand-secondary":"oklch(60.27647% 0.217712 257.424 / 0.078)","--lab-fill-brand-tertiary":"oklch(60.27647% 0.217712 257.424 / 0.039)","--lab-fill-brand-quaternary":"oklch(60.27647% 0.217712 257.424 / 0.02)","--lab-border-brand-base":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-brand-soft":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-label-danger-primary":"oklch(14.22004% 0.058352 29.234)","--lab-label-danger-secondary":"oklch(21.88171% 0.089792 29.234)","--lab-label-danger-tertiary":"oklch(35.54757% 0.145582 28.627)","--lab-label-danger-quaternary":"oklch(51.73054% 0.211869 28.643)","--lab-fill-danger-primary":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-fill-danger-secondary":"oklch(65.42146% 0.232135 28.659 / 0.078)","--lab-fill-danger-tertiary":"oklch(65.42146% 0.232135 28.659 / 0.039)","--lab-fill-danger-quaternary":"oklch(65.42146% 0.232135 28.659 / 0.02)","--lab-border-danger-base":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-danger-soft":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-warning-primary":"oklch(13.56514% 0.029751 68.088)","--lab-label-warning-secondary":"oklch(20.90753% 0.046395 66.438)","--lab-label-warning-tertiary":"oklch(34.00266% 0.074904 67.452)","--lab-label-warning-quaternary":"oklch(49.79045% 0.108745 68.715)","--lab-fill-warning-primary":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-fill-warning-secondary":"oklch(78.56796% 0.171718 68.607 / 0.078)","--lab-fill-warning-tertiary":"oklch(78.56796% 0.171718 68.607 / 0.039)","--lab-fill-warning-quaternary":"oklch(78.56796% 0.171718 68.607 / 0.02)","--lab-border-warning-strong":"oklch(91.20808% 0.064865 69.118 / 1)","--lab-border-warning-base":"oklch(78.56796% 0.171718 68.607 / 0.2)","--lab-border-warning-soft":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-label-success-primary":"oklch(13.01439% 0.039836 145.937)","--lab-label-success-secondary":"oklch(20.10741% 0.059351 147.255)","--lab-label-success-tertiary":"oklch(32.71649% 0.095649 147.616)","--lab-label-success-quaternary":"oklch(47.52181% 0.139141 147.559)","--lab-fill-success-primary":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-fill-success-secondary":"oklch(73.03242% 0.194381 147.444 / 0.078)","--lab-fill-success-tertiary":"oklch(73.03242% 0.194381 147.444 / 0.039)","--lab-fill-success-quaternary":"oklch(73.03242% 0.194381 147.444 / 0.02)","--lab-border-success-strong":"oklch(88.67895% 0.198079 147.416 / 1)","--lab-border-success-base":"oklch(73.03242% 0.194381 147.444 / 0.2)","--lab-border-success-soft":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-label-info-primary":"oklch(13.83602% 0.055501 259.684)","--lab-label-info-secondary":"oklch(21.26627% 0.087439 260.146)","--lab-label-info-tertiary":"oklch(34.39884% 0.140838 260.069)","--lab-label-info-quaternary":"oklch(50.11242% 0.202893 259.861)","--lab-fill-info-primary":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fill-info-secondary":"oklch(64.04613% 0.193058 259.892 / 0.078)","--lab-fill-info-tertiary":"oklch(64.04613% 0.193058 259.892 / 0.039)","--lab-fill-info-quaternary":"oklch(64.04613% 0.193058 259.892 / 0.02)","--lab-border-info-base":"oklch(64.04613% 0.193058 259.892 / 0.2)","--lab-border-info-soft":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fx-focus-ring-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fx-focus-ring-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-fx-focus-ring-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-fx-focus-ring-neutral":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-fx-glow-brand":"oklch(60.27647% 0.217712 257.424)","--lab-fx-glow-brand-core":"oklch(78.76611% 0.108476 257.256)","--lab-fx-glow-brand-alpha":"0.11057506131405687","--lab-fx-glow-danger":"oklch(65.42146% 0.232135 28.659)","--lab-fx-glow-danger-core":"oklch(81.77103% 0.102764 29.025)","--lab-fx-glow-danger-alpha":"0.11023622047244083","--lab-fx-glow-warning":"oklch(78.56796% 0.171718 68.607)","--lab-fx-glow-warning-core":"oklch(88.88181% 0.082838 68.777)","--lab-fx-glow-warning-alpha":"0.07169755954418727","--lab-fx-glow-neutral":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-core":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-alpha":"0.04724409448818886","--lab-fx-glow-inverted":"oklch(75.99692% 0.012723 286.093 / 0.522)","--lab-fx-skeleton-highlight":"oklch(57.53363% 0.012136 286.012 / 0.039)","--lab-fill-neutral":"oklch(57.53363% 0.012136 286.012 / 1)","--lab-fill-accent-tinted":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-danger-tinted":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-label-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-accent":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-danger":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-focus":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-badge-fill-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-badge-fill-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-badge-fill-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-badge-fill-success":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-badge-fill-info":"oklch(64.04613% 0.193058 259.892 / 1)","--lab-badge-fill-static-dark":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-badge-fill-static-light":"oklch(100.00000% 0.000000 89.876 / 1)","--lab-fill-neutral-tinted":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-border-neutral":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fx-skeleton-base":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fill-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-icon":"oklch(33.70211% 0.013945 291.371)","--lab-border-brand-strong":"oklch(34.17043% 0.122830 257.309 / 1)","--lab-border-danger-strong":"oklch(35.54757% 0.145582 28.627 / 1)","--lab-border-info-strong":"oklch(34.39884% 0.140838 260.069 / 1)"}},{"theme":"light","bg":"#3A3A3C","vars":{"--lab-bg-tone-2":"oklch(36.65084% 0.011912 285.797)","--lab-bg-tone-3":"oklch(36.65084% 0.011912 285.797)","--lab-label-primary":"oklch(98.92134% 0.005280 286.303)","--lab-label-secondary":"oklch(82.53513% 0.012530 281.039)","--lab-label-tertiary":"oklch(72.82755% 0.014297 286.039)","--lab-label-quaternary":"oklch(61.25907% 0.011943 286.042)","--lab-border-strong":"oklch(98.92134% 0.005280 286.303)","--lab-border-base":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-border-soft":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-primary":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-fill-secondary":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fill-tertiary":"oklch(57.53363% 0.012136 286.012 / 0.122)","--lab-fill-quaternary":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fx-shadow-minor":"oklch(17.39406% 0.004094 285.967 / 0.012)","--lab-fx-shadow-ambient":"oklch(17.39406% 0.004094 285.967 / 0.02)","--lab-fx-shadow-penumbra":"oklch(17.39406% 0.004094 285.967 / 0.039)","--lab-fx-shadow-major":"oklch(17.39406% 0.004094 285.967 / 0.122)","--lab-label-brand-primary":"oklch(98.77036% 0.005722 264.533)","--lab-label-brand-secondary":"oklch(82.44675% 0.088464 257.008)","--lab-label-brand-tertiary":"oklch(73.08383% 0.140495 257.442)","--lab-label-brand-quaternary":"oklch(61.77245% 0.208353 257.413)","--lab-fill-brand-primary":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-brand-secondary":"oklch(60.27647% 0.217712 257.424 / 0.078)","--lab-fill-brand-tertiary":"oklch(60.27647% 0.217712 257.424 / 0.039)","--lab-fill-brand-quaternary":"oklch(60.27647% 0.217712 257.424 / 0.02)","--lab-border-brand-strong":"oklch(61.75781% 0.208491 257.338 / 1)","--lab-border-brand-base":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-brand-soft":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-label-danger-primary":"oklch(98.86605% 0.005516 31.054)","--lab-label-danger-secondary":"oklch(83.38591% 0.092088 28.223)","--lab-label-danger-tertiary":"oklch(74.63167% 0.153784 28.842)","--lab-label-danger-quaternary":"oklch(64.10554% 0.244701 28.706)","--lab-fill-danger-primary":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-fill-danger-secondary":"oklch(65.42146% 0.232135 28.659 / 0.078)","--lab-fill-danger-tertiary":"oklch(65.42146% 0.232135 28.659 / 0.039)","--lab-fill-danger-quaternary":"oklch(65.42146% 0.232135 28.659 / 0.02)","--lab-border-danger-strong":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-danger-base":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-danger-soft":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-warning-primary":"oklch(98.75683% 0.008520 67.727)","--lab-label-warning-secondary":"oklch(83.08426% 0.131366 68.787)","--lab-label-warning-tertiary":"oklch(73.86147% 0.161425 68.614)","--lab-label-warning-quaternary":"oklch(61.96370% 0.135496 68.532)","--lab-fill-warning-primary":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-fill-warning-secondary":"oklch(78.56796% 0.171718 68.607 / 0.078)","--lab-fill-warning-tertiary":"oklch(78.56796% 0.171718 68.607 / 0.039)","--lab-fill-warning-quaternary":"oklch(78.56796% 0.171718 68.607 / 0.02)","--lab-border-warning-strong":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-border-warning-base":"oklch(78.56796% 0.171718 68.607 / 0.2)","--lab-border-warning-soft":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-label-success-primary":"oklch(98.51065% 0.024195 147.300)","--lab-label-success-secondary":"oklch(80.08887% 0.235054 147.469)","--lab-label-success-tertiary":"oklch(70.67977% 0.207519 147.455)","--lab-label-success-quaternary":"oklch(59.24524% 0.173588 147.533)","--lab-fill-success-primary":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-fill-success-secondary":"oklch(73.03242% 0.194381 147.444 / 0.078)","--lab-fill-success-tertiary":"oklch(73.03242% 0.194381 147.444 / 0.039)","--lab-fill-success-quaternary":"oklch(73.03242% 0.194381 147.444 / 0.02)","--lab-border-success-strong":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-border-success-base":"oklch(73.03242% 0.194381 147.444 / 0.2)","--lab-border-success-soft":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-label-info-primary":"oklch(98.77036% 0.005722 264.533)","--lab-label-info-secondary":"oklch(82.42821% 0.087905 259.666)","--lab-label-info-tertiary":"oklch(73.17409% 0.139071 259.898)","--lab-label-info-quaternary":"oklch(61.97090% 0.205878 259.740)","--lab-fill-info-primary":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fill-info-secondary":"oklch(64.04613% 0.193058 259.892 / 0.078)","--lab-fill-info-tertiary":"oklch(64.04613% 0.193058 259.892 / 0.039)","--lab-fill-info-quaternary":"oklch(64.04613% 0.193058 259.892 / 0.02)","--lab-border-info-strong":"oklch(64.04613% 0.193058 259.892 / 1)","--lab-border-info-base":"oklch(64.04613% 0.193058 259.892 / 0.2)","--lab-border-info-soft":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fx-focus-ring-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fx-focus-ring-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-fx-focus-ring-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-fx-focus-ring-neutral":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-fx-glow-brand":"oklch(60.27647% 0.217712 257.424)","--lab-fx-glow-brand-core":"oklch(78.76611% 0.108476 257.256)","--lab-fx-glow-brand-alpha":"0.06909778454881221","--lab-fx-glow-danger":"oklch(65.42146% 0.232135 28.659)","--lab-fx-glow-danger-core":"oklch(81.77103% 0.102764 29.025)","--lab-fx-glow-danger-alpha":"0.060913705583756306","--lab-fx-glow-warning":"oklch(78.56796% 0.171718 68.607)","--lab-fx-glow-warning-core":"oklch(88.88181% 0.082838 68.777)","--lab-fx-glow-warning-alpha":"0.040609137055837526","--lab-fx-glow-neutral":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-core":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-alpha":"0.028061954965508236","--lab-fx-glow-inverted":"oklch(75.99692% 0.012723 286.093 / 0.522)","--lab-fx-skeleton-highlight":"oklch(57.53363% 0.012136 286.012 / 0.039)","--lab-fill-neutral":"oklch(57.53363% 0.012136 286.012 / 1)","--lab-fill-accent-tinted":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-danger-tinted":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-label-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-accent":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-danger":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-focus":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-badge-fill-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-badge-fill-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-badge-fill-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-badge-fill-success":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-badge-fill-info":"oklch(64.04613% 0.193058 259.892 / 1)","--lab-badge-fill-static-dark":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-badge-fill-static-light":"oklch(100.00000% 0.000000 89.876 / 1)","--lab-fill-neutral-tinted":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-border-neutral":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fx-skeleton-base":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fill-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-icon":"oklch(72.82755% 0.014297 286.039)"}},{"theme":"light","bg":"#007AFF","vars":{"--lab-bg-tone-2":"oklch(57.87475% 0.012117 286.015)","--lab-bg-tone-3":"oklch(57.87475% 0.012117 286.015)","--lab-label-primary":"oklch(13.41536% 0.016099 291.497)","--lab-label-secondary":"oklch(20.01464% 0.013939 291.638)","--lab-label-tertiary":"oklch(33.31157% 0.013986 291.359)","--lab-label-quaternary":"oklch(48.82850% 0.012656 285.922)","--lab-border-strong":"oklch(13.41536% 0.016099 291.497)","--lab-border-base":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-border-soft":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-primary":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-fill-secondary":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fill-tertiary":"oklch(57.53363% 0.012136 286.012 / 0.122)","--lab-fill-quaternary":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fx-shadow-minor":"oklch(17.39406% 0.004094 285.967 / 0.012)","--lab-fx-shadow-ambient":"oklch(17.39406% 0.004094 285.967 / 0.02)","--lab-fx-shadow-penumbra":"oklch(17.39406% 0.004094 285.967 / 0.039)","--lab-fx-shadow-major":"oklch(17.39406% 0.004094 285.967 / 0.122)","--lab-label-brand-primary":"oklch(13.49058% 0.050205 258.119)","--lab-label-brand-secondary":"oklch(20.23588% 0.071518 256.892)","--lab-label-brand-tertiary":"oklch(33.52307% 0.122654 257.729)","--lab-label-brand-quaternary":"oklch(49.33014% 0.177503 257.333)","--lab-fill-brand-primary":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-brand-secondary":"oklch(60.27647% 0.217712 257.424 / 0.078)","--lab-fill-brand-tertiary":"oklch(60.27647% 0.217712 257.424 / 0.039)","--lab-fill-brand-quaternary":"oklch(60.27647% 0.217712 257.424 / 0.02)","--lab-border-brand-base":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-brand-soft":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-label-danger-primary":"oklch(14.22004% 0.058352 29.234)","--lab-label-danger-secondary":"oklch(20.90400% 0.085780 29.234)","--lab-label-danger-tertiary":"oklch(34.90075% 0.142917 28.593)","--lab-label-danger-quaternary":"oklch(51.33845% 0.210254 28.629)","--lab-fill-danger-primary":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-fill-danger-secondary":"oklch(65.42146% 0.232135 28.659 / 0.078)","--lab-fill-danger-tertiary":"oklch(65.42146% 0.232135 28.659 / 0.039)","--lab-fill-danger-quaternary":"oklch(65.42146% 0.232135 28.659 / 0.02)","--lab-border-danger-base":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-danger-soft":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-warning-primary":"oklch(13.56514% 0.029751 68.088)","--lab-label-warning-secondary":"oklch(20.32691% 0.044866 67.175)","--lab-label-warning-tertiary":"oklch(33.70499% 0.073308 69.356)","--lab-label-warning-quaternary":"oklch(49.32505% 0.107532 68.994)","--lab-fill-warning-primary":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-fill-warning-secondary":"oklch(78.56796% 0.171718 68.607 / 0.078)","--lab-fill-warning-tertiary":"oklch(78.56796% 0.171718 68.607 / 0.039)","--lab-fill-warning-quaternary":"oklch(78.56796% 0.171718 68.607 / 0.02)","--lab-border-warning-strong":"oklch(90.50991% 0.070001 68.839 / 1)","--lab-border-warning-base":"oklch(78.56796% 0.171718 68.607 / 0.2)","--lab-border-warning-soft":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-label-success-primary":"oklch(13.01439% 0.039836 145.937)","--lab-label-success-secondary":"oklch(19.36939% 0.056254 147.869)","--lab-label-success-tertiary":"oklch(32.05045% 0.093836 147.561)","--lab-label-success-quaternary":"oklch(47.21271% 0.138736 147.423)","--lab-fill-success-primary":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-fill-success-secondary":"oklch(73.03242% 0.194381 147.444 / 0.078)","--lab-fill-success-tertiary":"oklch(73.03242% 0.194381 147.444 / 0.039)","--lab-fill-success-quaternary":"oklch(73.03242% 0.194381 147.444 / 0.02)","--lab-border-success-strong":"oklch(87.96697% 0.214930 147.350 / 1)","--lab-border-success-base":"oklch(73.03242% 0.194381 147.444 / 0.2)","--lab-border-success-soft":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-label-info-primary":"oklch(13.83602% 0.055501 259.684)","--lab-label-info-secondary":"oklch(20.33878% 0.084139 260.257)","--lab-label-info-tertiary":"oklch(33.98919% 0.139378 260.097)","--lab-label-info-quaternary":"oklch(49.73960% 0.201561 259.877)","--lab-fill-info-primary":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fill-info-secondary":"oklch(64.04613% 0.193058 259.892 / 0.078)","--lab-fill-info-tertiary":"oklch(64.04613% 0.193058 259.892 / 0.039)","--lab-fill-info-quaternary":"oklch(64.04613% 0.193058 259.892 / 0.02)","--lab-border-info-base":"oklch(64.04613% 0.193058 259.892 / 0.2)","--lab-border-info-soft":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fx-focus-ring-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fx-focus-ring-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-fx-focus-ring-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-fx-focus-ring-neutral":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-fx-glow-brand":"oklch(60.27647% 0.217712 257.424)","--lab-fx-glow-brand-core":"oklch(78.76611% 0.108476 257.256)","--lab-fx-glow-brand-alpha":"0.1414396647356093","--lab-fx-glow-danger":"oklch(65.42146% 0.232135 28.659)","--lab-fx-glow-danger-core":"oklch(81.77103% 0.102764 29.025)","--lab-fx-glow-danger-alpha":"0.1805418331471749","--lab-fx-glow-warning":"oklch(78.56796% 0.171718 68.607)","--lab-fx-glow-warning-core":"oklch(88.88181% 0.082838 68.777)","--lab-fx-glow-warning-alpha":"0.09073588245366788","--lab-fx-glow-neutral":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-core":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-alpha":"0.06430782839451565","--lab-fx-glow-inverted":"oklch(75.99692% 0.012723 286.093 / 0.522)","--lab-fx-skeleton-highlight":"oklch(57.53363% 0.012136 286.012 / 0.039)","--lab-fill-neutral":"oklch(57.53363% 0.012136 286.012 / 1)","--lab-fill-accent-tinted":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-danger-tinted":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-label-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-accent":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-danger":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-focus":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-badge-fill-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-badge-fill-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-badge-fill-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-badge-fill-success":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-badge-fill-info":"oklch(64.04613% 0.193058 259.892 / 1)","--lab-badge-fill-static-dark":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-badge-fill-static-light":"oklch(100.00000% 0.000000 89.876 / 1)","--lab-fill-neutral-tinted":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-border-neutral":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fx-skeleton-base":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fill-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-icon":"oklch(33.31157% 0.013986 291.359)","--lab-border-brand-strong":"oklch(33.52307% 0.122654 257.729 / 1)","--lab-border-danger-strong":"oklch(34.90075% 0.142917 28.593 / 1)","--lab-border-info-strong":"oklch(33.98919% 0.139378 260.097 / 1)"}},{"theme":"light","bg":"#FF3B30","vars":{"--lab-bg-tone-2":"oklch(63.60048% 0.011829 286.059)","--lab-bg-tone-3":"oklch(63.60048% 0.011829 286.059)","--lab-label-primary":"oklch(13.83495% 0.015576 284.180)","--lab-label-secondary":"oklch(25.12484% 0.015017 285.269)","--lab-label-tertiary":"oklch(36.40556% 0.013673 291.446)","--lab-label-quaternary":"oklch(51.30243% 0.012497 285.951)","--lab-border-strong":"oklch(13.83495% 0.015576 284.180)","--lab-border-base":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-border-soft":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-primary":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-fill-secondary":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fill-tertiary":"oklch(57.53363% 0.012136 286.012 / 0.122)","--lab-fill-quaternary":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fx-shadow-minor":"oklch(17.39406% 0.004094 285.967 / 0.012)","--lab-fx-shadow-ambient":"oklch(17.39406% 0.004094 285.967 / 0.02)","--lab-fx-shadow-penumbra":"oklch(17.39406% 0.004094 285.967 / 0.039)","--lab-fx-shadow-major":"oklch(17.39406% 0.004094 285.967 / 0.122)","--lab-label-brand-primary":"oklch(14.05977% 0.051776 257.879)","--lab-label-brand-secondary":"oklch(25.33060% 0.091490 257.423)","--lab-label-brand-tertiary":"oklch(36.69778% 0.133197 257.540)","--lab-label-brand-quaternary":"oklch(51.93456% 0.187074 257.359)","--lab-fill-brand-primary":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-brand-secondary":"oklch(60.27647% 0.217712 257.424 / 0.078)","--lab-fill-brand-tertiary":"oklch(60.27647% 0.217712 257.424 / 0.039)","--lab-fill-brand-quaternary":"oklch(60.27647% 0.217712 257.424 / 0.02)","--lab-border-brand-base":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-brand-soft":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-label-danger-primary":"oklch(14.49010% 0.059461 29.234)","--lab-label-danger-secondary":"oklch(26.17120% 0.107130 28.474)","--lab-label-danger-tertiary":"oklch(38.31782% 0.156989 28.748)","--lab-label-danger-quaternary":"oklch(53.87750% 0.220648 28.624)","--lab-fill-danger-primary":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-fill-danger-secondary":"oklch(65.42146% 0.232135 28.659 / 0.078)","--lab-fill-danger-tertiary":"oklch(65.42146% 0.232135 28.659 / 0.039)","--lab-fill-danger-quaternary":"oklch(65.42146% 0.232135 28.659 / 0.02)","--lab-border-danger-base":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-danger-soft":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-warning-primary":"oklch(14.09840% 0.030367 70.955)","--lab-label-warning-secondary":"oklch(25.16306% 0.055216 68.014)","--lab-label-warning-tertiary":"oklch(36.95716% 0.080989 68.209)","--lab-label-warning-quaternary":"oklch(51.83499% 0.113346 68.533)","--lab-fill-warning-primary":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-fill-warning-secondary":"oklch(78.56796% 0.171718 68.607 / 0.078)","--lab-fill-warning-tertiary":"oklch(78.56796% 0.171718 68.607 / 0.039)","--lab-fill-warning-quaternary":"oklch(78.56796% 0.171718 68.607 / 0.02)","--lab-border-warning-strong":"oklch(94.55166% 0.039779 69.746 / 1)","--lab-border-warning-base":"oklch(78.56796% 0.171718 68.607 / 0.2)","--lab-border-warning-soft":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-label-success-primary":"oklch(13.47470% 0.038038 148.984)","--lab-label-success-secondary":"oklch(24.13011% 0.070822 147.468)","--lab-label-success-tertiary":"oklch(35.33425% 0.103721 147.463)","--lab-label-success-quaternary":"oklch(49.62199% 0.145290 147.559)","--lab-fill-success-primary":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-fill-success-secondary":"oklch(73.03242% 0.194381 147.444 / 0.078)","--lab-fill-success-tertiary":"oklch(73.03242% 0.194381 147.444 / 0.039)","--lab-fill-success-quaternary":"oklch(73.03242% 0.194381 147.444 / 0.02)","--lab-border-success-strong":"oklch(93.08890% 0.111383 147.112 / 1)","--lab-border-success-base":"oklch(73.03242% 0.194381 147.444 / 0.2)","--lab-border-success-soft":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-label-info-primary":"oklch(14.07469% 0.058967 260.480)","--lab-label-info-secondary":"oklch(25.53067% 0.104920 260.137)","--lab-label-info-tertiary":"oklch(37.14168% 0.149591 259.762)","--lab-label-info-quaternary":"oklch(52.13660% 0.212021 259.943)","--lab-fill-info-primary":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fill-info-secondary":"oklch(64.04613% 0.193058 259.892 / 0.078)","--lab-fill-info-tertiary":"oklch(64.04613% 0.193058 259.892 / 0.039)","--lab-fill-info-quaternary":"oklch(64.04613% 0.193058 259.892 / 0.02)","--lab-border-info-base":"oklch(64.04613% 0.193058 259.892 / 0.2)","--lab-border-info-soft":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fx-focus-ring-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fx-focus-ring-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-fx-focus-ring-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-fx-focus-ring-neutral":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-fx-glow-brand":"oklch(60.27647% 0.217712 257.424)","--lab-fx-glow-brand-core":"oklch(78.76611% 0.108476 257.256)","--lab-fx-glow-brand-alpha":"0.1835748792270531","--lab-fx-glow-danger":"oklch(65.42146% 0.232135 28.659)","--lab-fx-glow-danger-core":"oklch(81.77103% 0.102764 29.025)","--lab-fx-glow-danger-alpha":"0.4395613333149519","--lab-fx-glow-warning":"oklch(78.56796% 0.171718 68.607)","--lab-fx-glow-warning-core":"oklch(88.88181% 0.082838 68.777)","--lab-fx-glow-warning-alpha":"0.1777791862086449","--lab-fx-glow-neutral":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-core":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-alpha":"0.1016772651089421","--lab-fx-glow-inverted":"oklch(75.99692% 0.012723 286.093 / 0.522)","--lab-fx-skeleton-highlight":"oklch(57.53363% 0.012136 286.012 / 0.039)","--lab-fill-neutral":"oklch(57.53363% 0.012136 286.012 / 1)","--lab-fill-accent-tinted":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-danger-tinted":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-label-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-accent":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-danger":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-focus":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-badge-fill-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-badge-fill-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-badge-fill-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-badge-fill-success":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-badge-fill-info":"oklch(64.04613% 0.193058 259.892 / 1)","--lab-badge-fill-static-dark":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-badge-fill-static-light":"oklch(100.00000% 0.000000 89.876 / 1)","--lab-fill-neutral-tinted":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-border-neutral":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fx-skeleton-base":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fill-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-icon":"oklch(36.40556% 0.013673 291.446)","--lab-border-brand-strong":"oklch(36.69778% 0.133197 257.540 / 1)","--lab-border-danger-strong":"oklch(38.31782% 0.156989 28.748 / 1)","--lab-border-info-strong":"oklch(37.14168% 0.149591 259.762 / 1)"}}]} +{"_meta":{"purpose":"Байт-точный golden границы JS↔WASM: recheckContrast/_recheckContrastMulti и полный snapshot resolveTheme.vars.","regenerated":"2026-07-22","regen_reason":"Hard deletion of retired component-specific recipe variants and their generated role leaves.","drift_scope":"Exactly 54 obsolete resolveVars leaves removed (9 keys × 6 theme/background cases); all remaining vars and every recheck value are unchanged.","recheck_unchanged":"Секция recheck: 2752/2752 числовых значений, то есть 1376/1376 пар (lc, wcag), байт-идентичны origin/main; ни одно значение recheckContrast не изменено.","numeric_provenance":{"glow_alpha":"Старый fixed-48 bisection + 4-decimal alpha заменён перебором конечных sRGB8-state, внутренней alpha выбранного интервала и shortest-roundtrip binary64 CSS.","scope_check":"Фактический структурный diff вычислен по каждому листу resolveVars; 4 alpha-ключа × 6 записей = 24."},"wcag_floors_not_weakened":"Glow — декоративная роль без WCAG-пола. Отдельно подтверждено: все 1376 пар (2752 числовых значения) recheck неизменны, поэтому измеренный контраст ни одной проверяемой роли не ослаблен.","isolated_verification_required":"Регенерация принимается только после независимой проверки scope, recheck-инварианта и полного Rust/npm/browser gate.","partition_correction":{"regenerated":"2026-07-11","reason":"Коррекция канонической binary64 alpha по фактическому midpoint encoded-sRGB8 partition без дополнительного округления.","drift_scope":"Ровно 17 alpha leaves: light/#000000 danger и четыре Glow alpha на каждом из #808080, #3A3A3C, #007AFF, #FF3B30; остальные resolveVars-листья неизменны.","recheck_unchanged":"2752/2752 числовых значений, то есть 1376/1376 пар (lc, wcag), побитно неизменны.","numeric_provenance":{"halo":"#FF3B30","background":"#000000","state":"#030101","lower_bits":"0x3f85555555555555","upper_bits":"0x3f8c1c1c1c1c1c1c","midpoint_bits":"0x3f88b8b8b8b8b8b8","midpoint":"0.012071078431372548","previous_bits":"0x3f88b8b8b8b8b8b9","previous_relation":"1 ULP выше, внутри той же partition"},"exhaustive_proof":"65 536 channel partitions и seam: state/composite outputs неизменны.","performance":"Full-range → bracketed exact search: base 19.7→4.37 µs, bloom 74.1→16.2 µs, long 656→139 µs."},"history":{"previous_regenerated":"2026-07-10","previous_regen_reason":"Глава #64 (ADR-0003): ось читаемости активирована в домене Ys — реактивный recheck (measure_contrast/recheck_against) и solver-вывод считаются в Ys. recheck flat и resolveVars регенерированы против пересобранного движка; inputs (theme/bg/fgs) сохранены. semver-major по ADR.","previous_drift_scope":"resolveVars: exactly 24 leaves changed, all --lab-fx-glow-{brand,danger,warning,neutral}-core (glow-centre colour, background-independent → 4 distinct values). No other var moved. glow base/alpha, labels, borders, fills, backgrounds all byte-identical.","previous_recheck_unchanged":"Исторический regen также не менял recheck; прежний текст ошибочно называл 2752 числа парами. Фактический объём: 1376 пар (lc, wcag).","previous_numeric_provenance":{"glow-brand-core":"oklch(78.84894% 0.062240 265.472) -> oklch(78.76611% 0.108476 257.256): chroma rises to the blue gamut wall at the functional lightness (fixed-fraction under-saturated it); L ±0.08pp, hue shifts to the cusp.","glow-danger-core":"oklch(81.62208% 0.103545 27.533) -> oklch(81.77103% 0.102764 29.025): near-identical (red already sat near its wall).","glow-warning-core":"oklch(88.80612% 0.081204 68.866) -> oklch(88.88181% 0.082838 68.777): near-identical.","glow-neutral-core":"oklch(99.97226% 0.001314 106.423) -> oklch(100.00000% 0.000000 89.876): neutral hue is achromatic → gamut wall chroma is 0 (was fixed-fraction residue noise); exact achromatic source stays on the neutral ray."},"previous_wave1_scope":"resolveVars: 69 info-* leaves changed across 6 theme×bg entries. Zero non-info drift (scope-checked). recheck untouched."},"endpoint_recovery":{"regenerated":"2026-07-17","reason":"Acceptance is checked before low-contrast dead-zone and physical-endpoint rejection, so previously false-failed opaque border endpoints now resolve.","drift_scope":"Exactly 9 resolveVars leaves added: border-{brand,danger,info}-strong on light #808080, #007AFF and #FF3B30. No existing leaf changed or disappeared.","recheck_unchanged":"Every committed recheck value remains bit-identical; the parity test compares all 2752 numbers before resolveVars.","proof":"agnostic_gates::accepted_endpoint_recovery_keeps_previously_false_failed_borders_legal plus full Rust workspace and WASM boundary parity."}},"recheck":[{"theme":"light","bg":"#FFFFFF","fgs":["#F7F7FF","#F7F7FF","#0A0A10","#71717A","#94949E","#BDBDC7","#94949E","#0A0A10","#00030E","#0057BB","#037BFF","#78AFFF","#150000","#BE0005","#FF070E","#FF9A8C","#1B0D00","#A06300","#D28300","#FFAA3A","#000B01","#007C2D","#00A73F","#00D452","#020013","#4D00F9","#6D6EFF","#9FA9FF"],"flat":[0,1.0658201232723763,0,1.0658201232723763,104.65963437608701,19.74046779866239,70.1623092547118,4.832895561154151,53.96706418364319,3.004779237664955,33.34341874861831,1.8637664731545371,53.96706418364319,3.004779237664955,104.65963437608701,19.74046779866239,105.59876563358553,20.60103636973117,79.91552226990729,6.816302360996974,63.95198968935827,3.97647793086433,41.89234722606997,2.2400949113548063,105.32470288110206,20.35108757719671,78.97384099389187,6.579727828852003,63.90360859563009,3.970705427078051,37.83221395673913,2.048194921340976,103.87315757816087,19.018778265876662,70.62528347734393,4.90716956443272,53.97808064225952,3.0056474129326713,34.232350404909745,1.8982213760304238,104.97702200816887,20.032297013031588,73.2226464627747,5.355840815772706,56.123729301186145,3.182173571701684,36.54497093010938,1.9925549350335838,105.76444562516849,20.751302844974063,83.48658341134008,7.825697912025839,63.67798680778349,3.9439477616440737,40.86459333479343,2.1890112231357013]},{"theme":"light","bg":"#000000","fgs":["#010103","#010103","#F1F1FC","#B3B3BC","#909099","#65656D","#909099","#F1F1FC","#E6F0FF","#60A1FF","#0074F3","#004CA4","#FFF4F2","#FF8273","#F40009","#A80004","#FFF7EE","#F79C00","#C97E00","#8F5800","#C2FFC9","#00C84D","#00A03C","#006E26","#EEF0FF","#9099FF","#6662FF","#4300DB"],"flat":[0,1.0069471255994658,0,1.0069471255994658,-99.67218963360757,18.727928195985914,-63.80513707672789,10.090949031745104,-45.863656882792206,6.635143500688325,-26.33477890397734,3.635637824948841,-45.863656882792206,6.635143500688325,-99.67218963360757,18.727928195985914,-97.97941453819666,18.27263411531394,-53.55180732861014,8.039306941172763,-34.54692380627581,4.792369408161928,-17.418806999279397,2.56984376276454,-102.41236835704379,19.474434420712562,-56.92703132210305,8.692613650917824,-34.93266406694178,4.850563950189369,-18.308962222075785,2.6667193472488013,-103.5613239139708,19.790915044333452,-61.97487030298502,9.71021814570805,-44.901407836051874,6.467843223430967,-25.783665577412222,3.5638173443979557,-98.60837652742211,18.44127602271432,-60.30793382928855,9.368875012646658,-42.707239087904505,6.093571182387199,-23.370874926227668,3.2583602660985775,-98.9883689300502,18.54346384276384,-54.32153113615954,8.18635152260471,-34.304902287955215,4.756029101219059,-14.435475219756622,2.261552965854495]},{"theme":"light","bg":"#808080","fgs":["#7A7A82","#7A7A82","#010103","#18171E","#37363E","#5B5B64","#37363E","#010103","#000002","#00163A","#003272","#004494","#040000","#3A0000","#760002","#990003","#0B0400","#261300","#512F00","#845100","#000200","#001D05","#004114","#006322","#000002","#160059","#2B0097","#3B00C5"],"flat":[0,1.0777393702545288,0,1.0777393702545288,40.18994509287661,5.280525528202377,36.81932911616927,4.505560397043893,27.93898600302036,3.0217055248761144,13.845788611978202,1.7013528597992067,27.93898600302036,3.0217055248761144,40.18994509287661,5.280525528202377,40.3306726916817,5.312553092992316,36.89312592554248,4.521969910173767,28.754774555141132,3.1292028976206594,22.090354756391577,2.35851620111828,40.23106479600237,5.289901483963394,36.823385380889306,4.5064610721321205,27.786796188195932,3.002060282138028,21.02583740042693,2.2570951143110336,39.64148780057383,5.154352713354357,36.89828467875601,4.523118840482662,27.981450040156208,3.027209986332121,13.567318350507929,1.6836851754208233,40.150141663678234,5.271436537678366,36.85731353296706,4.514000513189092,27.852320260403722,3.0105025419148825,16.643886294769857,1.8937047658181707,40.3306726916817,5.312553092992316,36.84988795298528,4.512349526507044,30.916517468784825,3.4313626667845316,25.033605894158644,2.6690076358452903]},{"theme":"light","bg":"#3A3A3C","fgs":["#3E3E45","#3E3E45","#F2F2FD","#BBBBC5","#9E9EA7","#7A7A82","#9E9EA7","#F2F2FD","#E7F1FF","#73ADFF","#2985FF","#0060CB","#FFF5F4","#FF9687","#FF4336","#CD0006","#FFF8F0","#FFA62C","#DC8A00","#AC6B00","#C7FFCC","#00D251","#00B043","#008631","#EFF1FF","#9CA6FF","#767AFF","#532BFF"],"flat":[0,1.0699484563148034,0,1.0699484563148034,-89.06520303275792,10.211241554507897,-56.964055924386784,5.959497869408446,-41.60773958742316,4.2707825803649335,-24.25691832785775,2.6663016206717356,-41.60773958742316,4.2707825803649335,-89.06520303275792,10.211241554507897,-87.32090210121694,9.956824779623638,-48.0046310941281,4.945145596338778,-30.285195552724065,3.1849116126306156,-14.342034884792934,1.9106934427040745,-91.71639880150975,10.602849175762582,-51.97724928251108,5.385053682831725,-31.570923815646545,3.301015045785777,-14.82740446463819,1.94471301206827,-92.86717481243348,10.774663089384916,-55.64998948221256,5.805759082702303,-40.41406674606042,4.149700467279057,-23.7537806706175,2.6249801387527425,-87.99158651504085,10.054343074329001,-53.75183524673264,5.586666992924044,-38.313874917805634,3.940371955014153,-21.037647044419426,2.407284189061088,-88.33220875070053,10.104016013712561,-48.80525652513833,5.032519885671211,-30.915189659062865,3.2415642790980215,-11.323318014385267,1.7063261265170042]},{"theme":"light","bg":"#007AFF","fgs":["#797981","#797981","#020205","#16151C","#36353D","#72727A","#36353D","#020205","#000103","#001536","#003374","#0058BC","#070000","#360000","#730002","#BE0005","#0F0500","#241200","#4F2F00","#A16400","#000400","#001B05","#003F13","#007C2D","#000005","#140055","#3300AC","#4E00FA"],"flat":[0,1.0745907693903731,0,1.0745907693903731,39.47891404105317,5.158402202047827,36.63906407938327,4.513939148043301,27.726113654933034,3.015791844568742,0,1.187212547453551,27.726113654933034,3.015791844568742,39.47891404105317,5.158402202047827,39.65687576920386,5.198408498908002,36.59714090951085,4.504660010775197,27.83078380746869,3.029349552004155,13.31049770996219,1.6744770124926043,39.57933742278427,5.181007308853048,36.66186584356392,4.51899176883356,27.77510172821011,3.0221297438766705,12.721590912296357,1.6379804580418844,38.8564182220264,5.016999835671664,36.579572970042136,4.500775701626332,27.65113132417139,3.006116231224861,0,1.205042331050678,39.39108202991847,5.138573889881769,36.624715730830154,4.510761781161014,27.902773646365304,3.038708829709556,0,1.3333017445114326,39.737197591996,5.216381929144518,36.59037871155487,4.5031645842195545,27.68585404738298,3.010592990868115,17.038043883395044,1.933101474636693]},{"theme":"light","bg":"#FF3B30","fgs":["#8A8A92","#8A8A92","#030306","#212129","#3E3D45","#76767F","#3E3D45","#030306","#000104","#00204D","#003B83","#005CC4","#080000","#4C0001","#830002","#C70006","#0F0600","#331C00","#5B3600","#A76800","#000400","#002809","#004917","#00822F","#000005","#1F0075","#3A00C3","#511AFF"],"flat":[0,1.0356140935636031,0,1.0356140935636031,43.57584669970237,5.806877937534237,38.3004661979112,4.504747008392574,29.108000765094282,3.0232304751839747,0,1.268226675946831,29.108000765094282,3.0232304751839747,43.57584669970237,5.806877937534237,43.87966672163739,5.884401548408216,38.30767260595269,4.50632138120243,29.16726657041677,3.0308664461202253,15.823096179531001,1.7789458759332724,43.782795788734404,5.8597651770218455,38.402450836988166,4.527103297945568,29.016037258963053,3.011424325654164,14.9864556348382,1.7254955647131072,42.99821792375369,5.657954629703501,38.41622407485322,4.5301350068404425,28.93769973409828,3.0014083479801514,0,1.28054929135364,43.62397616937584,5.819206103341516,38.406807106466914,4.528061863682665,29.022563392947294,3.012260432085235,8.92751562582044,1.3982348229239339,43.970091731453365,5.907320242919889,38.28628402517786,4.501651045763603,28.99214635836292,3.0083657301901288,19.11088367642911,2.0125842767555064]},{"theme":"light","bg":"#101012","fgs":["#15151B","#15151B","#F2F2FC","#B4B4BE","#93939C","#696972","#93939C","#F2F2FC","#E6F0FF","#64A4FF","#0077F9","#0050AC","#FFF4F3","#FF8777","#F9000A","#B00005","#FFF7EF","#FA9E00","#CD8000","#955C00","#C3FFC9","#00CA4E","#00A33D","#007329","#EEF0FF","#939CFF","#6967FF","#4600E6"],"flat":[0,1.045340264415452,0,1.045340264415452,-98.5838326466521,17.08955684747884,-62.72698384194396,9.243246994613658,-45.67498009680545,6.239717532288448,-26.395025771311026,3.4974414103185016,-45.67498009680545,6.239717532288448,-98.5838326466521,17.08955684747884,-96.3221346483494,16.53807546575917,-53.23458352949761,7.5085332653696355,-34.299151794570456,4.531377660721796,-17.34040000814869,2.4827755446650244,-100.79897840003042,17.636700891614694,-56.73966900010428,8.131153426700068,-34.4661942600266,4.554628420782135,-18.26813498707333,2.5778416349364046,-101.94670279716274,17.922901715066683,-61.50032492399525,9.010497025774457,-44.48245067627942,6.049054371037485,-25.983086445749187,3.447217107352778,-97.04991927160123,16.714741751700544,-59.578454823729174,8.650894122604043,-42.30409033566848,5.707629679225331,-23.50941826359813,3.1535587190594883,-97.33108904020293,16.783196253636362,-53.9780650339559,7.638815670143083,-34.15649094558268,4.511565332868746,-14.178333309149869,2.174938899863266]},{"theme":"light","bg":"#F6F8FA","fgs":["#F0F0FA","#F0F0FA","#0A0910","#707079","#8F8F99","#BCBCC5","#8F8F99","#0A0910","#00030D","#0057B9","#0079FD","#74ADFF","#150000","#BC0005","#FD000A","#FF9788","#1B0D00","#9F6200","#CB7F00","#FFA730","#000B01","#007B2C","#00A63E","#00D251","#020013","#4C00F7","#6C6CFF","#9DA6FF"],"flat":[0,1.0634215248159875,0,1.0634215248159875,100.62201386979996,18.61882254505944,66.47667620763235,4.605948460093869,52.23653324893716,3.0084294463712395,29.780700684603257,1.7710808257265196,52.23653324893716,3.0084294463712395,100.62201386979996,18.61882254505944,101.48497819102282,19.361251289328656,75.9364891028185,6.438321694919453,60.636932359482174,3.8278153397206016,38.76699274290234,2.152541489746932,101.19913515757631,19.11640864166299,75.28055256994783,6.281220843840182,60.46094873358925,3.807428346232839,34.70184973776556,1.9658801332164664,99.74758985463511,17.864929125608796,66.9127104243937,4.673002507305443,52.13428535623209,3.0001216660302674,31.16352525448906,1.8227359456035261,100.85145428464311,18.816958763489637,69.48045983543288,5.0977335979175304,52.421867335088024,3.0235758695027473,33.32486172802245,1.9082022515525083,101.63887790164272,19.492343272893066,79.67393134344084,7.442688338927282,60.17031912503092,3.7741072518393395,37.967753096619255,2.113772626279229]},{"theme":"light","bg":"#1C1C1E","fgs":["#202028","#202028","#F1F1FD","#B6B6BF","#95959E","#6D6C75","#95959E","#F1F1FD","#E6F1FF","#67A5FF","#0079FD","#0053B2","#FFF5F3","#FF8A7A","#FD000A","#B60005","#FFF7EF","#FC9F00","#D08200","#9A5F00","#C4FFCA","#00CB4E","#00A53E","#00772A","#EEF1FF","#949EFF","#6B6BFF","#4900EE"],"flat":[0,1.0522076023272475,0,1.0522076023272475,-96.27140987076255,15.184146579953019,-61.97466067394124,8.452688508186561,-44.87094637123084,5.728792540137694,-25.977188885961883,3.283145241842075,-44.87094637123084,5.728792540137694,-96.27140987076255,15.184146579953019,-94.97224440690107,14.900628115594163,-51.99577035710509,6.808135431331698,-33.457001357615106,4.175350120738554,-16.755883343489252,2.3335896778079332,-99.44736446714371,15.886233758087014,-55.869159717403925,7.428487313030341,-33.63398343987836,4.1977071628099685,-17.709012021143003,2.4235530906296177,-100.15616825242344,16.04465607216319,-60.39258141645385,8.181935837052826,-43.77686148964125,5.57032618168592,-25.67861999448382,3.2497240951569792,-95.39392806862337,14.992417327878547,-58.246405974587134,7.820609198173194,-41.3596549106396,5.227261702394524,-23.16505433719768,2.9752806858720673,-95.97861011098902,15.120061263203354,-52.9765437482588,6.963016139185695,-33.57839540296167,4.190679013723035,-13.485138200601865,2.040069034387161]},{"theme":"light","bg":"#7C3AED","fgs":["#707078","#707078","#F4F4FE","#E3E4ED","#BDBDC6","#A6A6B0","#BDBDC6","#F4F4FE","#EBF3FF","#D5E6FF","#92BFFF","#4291FF","#FFF8F7","#FFDDD8","#FFA597","#FF6253","#FFFAF5","#FFE0BE","#FFAA39","#E79100","#D0FFD4","#7DFF93","#00DA54","#00B947","#F1F4FF","#DFE4FF","#AEB8FF","#8087FF"],"flat":[0,1.1608373681184396,0,1.1608373681184396,-73.98396246220521,5.215032461901156,-64.0643931275358,4.500912896664839,-41.75099800228171,3.055051474145459,-29.486682699107686,2.3616035737730514,-41.75099800228171,3.055051474145459,-73.98396246220521,5.215032461901156,-72.43421936609613,5.100687932363686,-64.08586870464498,4.502412953317652,-41.11294430990105,3.017135229848019,-18.888486914529935,1.8251241221961068,-76.90365537296871,5.4332059719499135,-64.06334225320066,4.50083949888721,-40.940708789235714,3.0069340578770496,-21.122834074579163,1.9331720160112165,-77.70409895709017,5.493642615376888,-64.34147182620212,4.520282197700913,-40.849183913847796,3.0015190917179395,-28.18413111501551,2.292444746213784,-72.9506558335481,5.138679113149092,-64.10668565953954,4.5038671985064,-41.2870784084647,3.027463512408998,-26.013778316259767,2.1791858289450103,-73.64084437140687,5.1896286230945075,-64.42349258712335,4.5260223420377566,-41.02780633656069,3.0120908713744514,-19.464187683477977,1.8526993919522168]},{"theme":"light","bg":"#123456","fgs":["#36363D","#36363D","#F2F2FD","#BEBEC8","#A2A2AC","#808089","#A2A2AC","#F2F2FD","#E8F1FF","#79B0FF","#368BFF","#0065D6","#FFF6F4","#FF9B8D","#FF5446","#D80007","#FFF8F1","#FFAB3F","#E18E00","#B47000","#C8FFCD","#00D552","#00B545","#008D34","#EFF1FF","#A0AAFF","#7B81FF","#573FFF"],"flat":[0,1.0609289970892934,0,1.0609289970892934,-91.39519035537737,11.4423466076867,-60.94367397154904,6.896920022598847,-46.020165423777584,5.026435795944436,-29.36949737122932,3.250019698594834,-46.020165423777584,5.026435795944436,-91.39519035537737,11.4423466076867,-89.77514075602531,11.177468536043195,-51.89752791553575,5.733156384550998,-34.996215684604906,3.8114980409485155,-18.92394323577027,2.320895932067837,-94.48648764812391,11.954651961610136,-56.02375667309079,6.252768618847473,-36.81873931987068,4.002056146272223,-19.591172380553846,2.375625103310219,-95.24016771148194,12.080915286992608,-59.85145182180837,6.751641124414219,-44.82390104707481,4.887508677146329,-28.683175933367217,3.184369254481423,-90.45906258458693,11.288985892577246,-57.499124974387314,6.443154991463433,-42.83495757582209,4.660276446547384,-26.099142978345885,2.9428672820648862,-90.66219607331998,11.322193559065964,-52.9772743130274,5.867283737103295,-35.76790053375578,3.8916727925995533,-16.863337707955058,2.1560372721390864]},{"theme":"light","bg":"#ABCDEF","fgs":["#C2C2CB","#C2C2CB","#06060C","#55545D","#706F78","#A1A1AA","#706F78","#06060C","#000209","#00479B","#0065D5","#3289FF","#100000","#9F0004","#D70007","#FF4F41","#170A00","#7A4A00","#9F6200","#DF8C00","#000801","#006322","#00822F","#00B344","#01000D","#3E00CE","#573EFF","#797EFF"],"flat":[0,1.0705446339746656,0,1.0705446339746656,75.64582413051765,12.232204997602986,52.76667497081496,4.519371986307913,41.423484798334584,3.0020546798073307,18.127484103049046,1.5512322351556214,41.423484798334584,3.0020546798073307,75.64582413051765,12.232204997602986,76.22533906008685,12.552007092013115,56.99670466281028,5.352680994694784,44.43662180236468,3.3253647200326193,29.122033451491582,2.0641033245604916,76.01516726706762,12.436514137733148,55.97147975339624,5.133613537650522,43.91010121645754,3.2654000659595037,27.461071919951625,1.9716236837030474,74.81611736383091,11.770648316029503,52.816193854903005,4.528105409994289,41.51373908005361,3.011071841176318,19.679891349505557,1.6112041351931181,75.72958089661125,12.278654436624837,52.80881394476793,4.526802423347816,41.42233250009274,3.001939806510638,21.536768417458934,1.687644517337887,76.3513076320574,12.62087594492386,59.99704797613187,6.063965240585196,46.62694808670478,3.592268428661898,28.6381619838115,2.036506690369533]},{"theme":"light","bg":"#808080","fgs":["#F7F7FF","#0A0A10","#71717A","#fff","abcdef","#Ff3B30","#123","112233"],"flat":[-64.57463009518332,3.7055405145881393,38.97016765822309,4.9982958489854346,0,1.2236914579873204,-69.21596068658343,3.9494396480491156,-37.021615579693034,2.3904478169235754,0,1.1134158562260283,34.84722274356656,4.0887512719822805,34.84722274356656,4.0887512719822805]},{"theme":"dark","bg":"#FFFFFF","fgs":["#E7E7F1","#D9D9E2","#0D0D12","#707079","#94949D","#BCBCC6","#94949D","#0D0D12","#000414","#0053C4","#1F78FF","#79ACFF","#190000","#BC0016","#FD0021","#FF978D","#1D0B00","#A65B00","#DA7A00","#FFA658","#000E02","#007C2A","#00A63C","#00D24E","#04001A","#5900F2","#7867FF","#A4A4FF"],"flat":[10.564452802176923,1.2281904700369508,18.372353597432387,1.402460683906872,104.27090588423802,19.38311547953439,70.6022439311581,4.903434648448188,54.00450766949271,3.0077315832596954,33.86624309585679,1.8839141891835494,54.00450766949271,3.0077315832596954,104.27090588423802,19.38311547953439,105.42046624663428,20.43859328774187,80.31672424485741,6.9206271592755195,64.53598724808649,4.047139863973963,42.92136358171458,2.2930773168482466,105.12312344447312,20.1664284240786,79.32640956847692,6.666965706057784,64.47466861031803,4.0396342482899685,38.694434835096864,2.0867987099188317,103.94997559222902,19.08898290736351,71.78028267942507,5.099770465607366,55.19920557284124,3.104259347966041,35.17783363962355,1.9359471784913758,104.66191026229393,19.742561272328775,73.25578885512941,5.361938351796974,56.57179613789485,3.2209892436744982,37.49012931672191,2.03317973864909,105.58294145619523,20.58664787402852,83.26627791326895,7.757864847967472,64.57504887916579,4.051931818616954,42.090577540190225,2.2501559682241092]},{"theme":"dark","bg":"#000000","fgs":["#030205","#08080D","#F0F0FA","#B1B1BB","#8F8F98","#64646D","#8F8F98","#F0F0FA","#E3EEFF","#639EFF","#006DFE","#0048AD","#FFF4F3","#FF7F74","#F1001F","#A80012","#FFF7F1","#FF921B","#D17400","#955100","#B8FFBE","#00C649","#009F39","#006E25","#EDEEFF","#9794FF","#725BFF","#4E00D6"],"flat":[0,1.0147465549687367,0,1.0508696650285665,-99.01083964835996,18.549513785944853,-62.774763175831374,9.875849762659984,-45.37711439886134,6.5503119047070335,-25.95111170367181,3.5855589616463144,-45.37711439886134,6.5503119047070335,-99.01083964835996,18.549513785944853,-96.73192161780433,17.93999294470544,-52.63286661912575,7.865273702162647,-33.381528921151094,4.618607106385301,-17.0501576858861,2.5303719690800346,-102.4562582898777,19.486486384763445,-56.148341062851905,8.53993960492931,-34.330992536827104,4.759940239487347,-18.37234103122508,2.6737006902768288,-103.68997976483836,19.82648045930809,-60.35961607275431,9.379380118591195,-43.39157032733722,6.209218370112115,-24.93643207012505,3.4548840566407524,-97.27916136850891,18.085610527348415,-59.34389647192141,9.173841233004914,-42.25887199411116,6.018336366435839,-23.360562325895305,3.2570864547421428,-97.9872053947921,18.274719246988955,-53.323021296304695,7.99582368531923,-33.63351308594995,4.6559156780457185,-14.771084781300106,2.2949481607054216]},{"theme":"dark","bg":"#808080","fgs":["#707078","#66666E","#030305","#18171D","#37363E","#5D5D64","#37363E","#030305","#000104","#00153F","#00317C","#00429F","#070000","#3A0002","#750009","#9B0010","#0C0300","#291200","#562D00","#8B4B00","#000300","#001D05","#004112","#006521","#010005","#1A0057","#350098","#4700C4"],"flat":[0,1.24296889833444,9.274501031402123,1.4407196981948422,39.91548097777799,5.217615096019717,36.836632862345205,4.5094036578339445,27.93898600302036,3.0217055248761144,13.086665786299626,1.6537770693375544,27.93898600302036,3.0217055248761144,39.91548097777799,5.217615096019717,40.20955594591163,5.284998875760269,36.83828085440759,4.509769828130583,28.330761142327287,3.0728704517581886,21.819504217645104,2.3321848000622474,40.142120786515875,5.269603471071473,36.80831529061993,4.503115584720142,27.930310922781615,3.0205822411426944,20.559717681283935,2.214401964672886,39.70511745091868,5.169077378914306,36.80795164205375,4.503034882134433,27.78871707797606,3.002307432697674,14.025581970431295,1.7128949100718707,40.051433392362526,5.248844071123953,36.85731353296706,4.514000513189092,27.870384117529113,3.0128341396537346,15.95953035602916,1.844055168824021,40.26997047853993,5.298759395894901,36.795231110735635,4.500212645278562,30.09285126953815,3.3133648365325183,24.185949678819867,2.5748913454036786]},{"theme":"dark","bg":"#3A3A3C","fgs":["#46464E","#4F4F57","#F1F1FA","#BABAC4","#9D9DA6","#797982","#9D9DA6","#F1F1FA","#E4EFFF","#75AAFF","#3482FF","#005AD5","#FFF6F4","#FF9389","#FF3C3B","#CC0019","#FFF8F2","#FFA350","#E58000","#B36300","#BCFFC2","#00D04D","#00AF3F","#00862E","#EEEFFF","#A2A1FF","#7F74FF","#601DFF"],"flat":[0,1.2140718035663858,0,1.3988859584097761,-88.35603271482154,10.107493939375463,-56.41682578047019,5.8952705955406675,-41.10063170437542,4.219157833086703,-23.840055313660137,2.6320437618590202,-41.10063170437542,4.219157833086703,-88.35603271482154,10.107493939375463,-86.07057705487603,9.776047393674569,-47.005198509651805,4.836992692850337,-29.617188814986996,3.125340410912816,-13.465221764046625,1.8500449052652255,-92.15650032550448,10.66842694645063,-51.122226750198095,5.289032001141636,-30.69564720892069,3.221769980092269,-14.697188723997265,1.9355550836437716,-92.95341233620333,10.787583067801071,-55.01595924446716,5.732183607436098,-39.01368610071904,4.00959458617392,-22.717127042375882,2.5408179478715844,-86.56463892830851,9.847321829129962,-52.766100540364825,5.474290510931042,-37.83315401910713,3.893128667535661,-21.00389596511428,2.404636707585164,-87.32881842479753,9.957973594755346,-47.6452882768299,4.906141960838558,-29.99057738527554,3.1585747205458907,-10.990631160230787,1.6845762464445797]},{"theme":"dark","bg":"#007AFF","fgs":["#6F6F77","#65656D","#040408","#16151B","#36353D","#72727A","#36353D","#040408","#000107","#00133B","#00317C","#0054C7","#0B0000","#360002","#730009","#BF0016","#100500","#261000","#542C00","#A85D00","#000501","#001B04","#004012","#007D2B","#010009","#180052","#3B00A6","#5A00F5"],"flat":[0,1.2399763057879079,9.137153276370956,1.4379357071162235,39.199487305374596,5.095159290353486,36.655643103420836,4.517612479062309,27.726113654933034,3.015791844568742,0,1.187212547453551,27.726113654933034,3.015791844568742,39.199487305374596,5.095159290353486,39.616537893860624,5.189361828546554,36.671589094740156,4.521147554384925,27.767977778595686,3.0212072610618272,13.560368041515986,1.6902988819780091,39.46146930028483,5.1544680971312,36.646426655727396,4.515570174173857,27.733751845040576,3.016779185756929,12.426352071081123,1.6200937148513173,38.82022467282525,5.008731246970903,36.66757146424993,4.520256699490143,27.6755983348751,3.009270049666561,0,1.2356833222737893,39.28492308543262,5.114543979353608,36.632422880871374,4.512468289727634,27.610461383700073,3.00088106913278,0,1.3173837917708568,39.666598999480925,5.200587158978297,36.5946359240296,4.504106003407952,27.684199915070796,3.0103795763779906,16.582944142840912,1.898781610023956]},{"theme":"dark","bg":"#FF3B30","fgs":["#7F7F87","#74747D","#040408","#212128","#3E3D45","#76767F","#3E3D45","#040408","#000107","#001E53","#00398D","#0058CF","#0C0000","#4C0004","#83000C","#C60018","#110500","#371A00","#603300","#AE6000","#000601","#002808","#004916","#00822D","#01000A","#250070","#4400BC","#5E01FF"],"flat":[0,1.119564506502328,0,1.304916965157494,43.43238144483196,5.770041002680692,38.322111022898454,4.5094781825179275,29.108000765094282,3.0232304751839747,0,1.268226675946831,29.108000765094282,3.0232304751839747,43.43238144483196,5.770041002680692,43.84943203331798,5.876721182230661,38.357196991852035,4.517162998132957,28.969046969390032,3.0054117916551304,16.078397205708793,1.7957132708136676,43.66281217184597,5.829141814712315,38.38176722933373,4.522556047220382,28.96308732518828,3.004650205577566,15.121014183458136,1.7339400568635355,43.01534021323247,5.66238735865634,38.36369120360285,4.518587529333456,28.956857085057415,3.003854272405285,0,1.32359073643957,43.42248485866928,5.7674953973596095,38.41370496023468,4.529580289565808,29.031977011275746,3.0134669332394415,8.949958172317846,1.3992774406692907,43.88937786124038,5.8868666342953615,38.32550423955951,4.510220548286027,28.94413025668513,3.002229117722787,19.264807559936084,2.024522036885816]},{"theme":"dark","bg":"#101012","fgs":["#1C1C22","#24242A","#F0F0FA","#B3B3BD","#92929B","#686871","#92929B","#F0F0FA","#E3EEFF","#66A0FF","#0E71FF","#004CB5","#FFF5F3","#FF8378","#F60020","#AF0013","#FFF7F1","#FF962A","#D57700","#9B5500","#B9FFBF","#00C849","#00A23A","#007226","#EDEEFF","#9997FF","#7460FF","#5200DF"],"flat":[0,1.1210017368042908,0,1.2316217101091165,-97.35355975851269,16.788671896406843,-62.18912907483276,9.1408846014785,-45.183954583215964,6.160892481462449,-25.971291980892453,3.445784610130369,-45.183954583215964,6.160892481462449,-97.35355975851269,16.788671896406843,-95.07464172795706,16.23701078357789,-51.883866319617056,7.27432706128743,-33.10437599843017,4.366728841176053,-16.962076325331164,2.444617555127284,-101.23789901188303,17.745933910216884,-55.660852739640006,7.937252290960222,-33.86013630190819,4.470540437936136,-18.127983335051407,2.5633448249431416,-102.03269987499108,17.94442049171427,-60.01549050114797,8.732123022916136,-43.24478677485694,5.853976070416037,-25.11442835418257,3.342537791539289,-95.74776769254933,16.399183837627458,-58.596724506079966,8.469603286006345,-41.852118066322895,5.637910373772879,-23.122294138224145,3.108849028833141,-96.32992550494482,16.539962663017093,-52.86610846158114,7.444323350911882,-33.336065162067605,4.398429825394501,-14.383943910910213,2.194178799785328]},{"theme":"dark","bg":"#F6F8FA","fgs":["#E0E0EA","#D2D2DC","#0D0D12","#717179","#8F8F98","#BCBCC6","#8F8F98","#0D0D12","#000414","#0053C4","#1F78FF","#79ACFF","#190000","#BC0016","#FD0021","#FF978D","#1D0B00","#A65B00","#D67800","#FFA658","#000E02","#007C2A","#00A63C","#00D24E","#04001A","#5900F2","#7867FF","#A4A4FF"],"flat":[10.34292311627503,1.2313464424988367,18.024461551245693,1.4097592283337514,100.14533816071224,18.207162386274927,66.07133001527934,4.544848448940367,52.273633182943,3.011452358576948,29.740675372331037,1.769619110018133,52.273633182943,3.011452358576948,100.14533816071224,18.207162386274927,101.29489852310851,19.198605473400605,76.19115652133168,6.500760037097026,60.41041952456072,3.801604173546957,38.795795858188825,2.153958743950203,100.99755572094737,18.94295256384705,75.20084184495119,6.262487955668726,60.349100886792264,3.7945539156195487,34.56886711157111,1.9601948416949677,99.82440786870325,17.93087452582946,67.65471495589931,4.7903727911067815,52.29898260017819,3.0135204434714913,31.0522659160978,1.8184953129572932,100.53634253876818,18.544800983402446,69.13022113160365,5.036635229225181,52.44622841436909,3.025575236651027,33.364561593196164,1.9098288766402804,101.45737373266948,19.337677744697757,79.14071018974319,7.287203401685083,60.44948115564005,3.8061054053754897,37.96500981666448,2.1136413881018767]},{"theme":"dark","bg":"#1C1C1E","fgs":["#28282E","#303036","#F0F1FA","#B5B5BE","#94949D","#6C6C74","#94949D","#F0F1FA","#E3EEFF","#69A2FF","#1975FF","#004FBB","#FFF5F3","#FF877C","#FA0021","#B50014","#FFF7F1","#FF9833","#D87900","#A05800","#B9FFC0","#00CA4A","#00A43B","#007628","#EDEEFF","#9B99FF","#7664FF","#5500E7"],"flat":[0,1.1612081374032734,0,1.2977584057597347,-96.00102363023473,15.124963081358866,-61.434200333395395,8.359778481952823,-44.37695589123639,5.656999216809453,-25.852207004995158,3.269134017520218,-44.37695589123639,5.656999216809453,-96.00102363023473,15.124963081358866,-93.28410718321774,14.535439506623602,-51.011442228336165,6.654206660130301,-32.65955270222981,4.075313081844305,-16.370466566638967,2.297770413224217,-99.44736446714371,15.886233758087014,-55.07262883280597,7.299026812522581,-33.02825162884881,4.121422426501604,-17.569909707299868,2.4103015359642868,-100.24216533025177,16.063919770413232,-58.9183675206033,7.932997007393653,-42.52780974275247,5.391835753982905,-24.79898612463087,3.1522681596337323,-93.99035989085725,14.687777164248876,-57.733604751916815,7.735299184328377,-40.905085629416114,5.1638402024196015,-22.782862127638978,2.93464773079297,-94.53939096020551,14.80664328764592,-51.95162400089297,6.801199303451522,-32.707141977036414,4.081250761977603,-13.69412377461175,2.0581050526656677]},{"theme":"dark","bg":"#7C3AED","fgs":["#7A7A83","#85858D","#F2F2FD","#E3E4ED","#BBBCC5","#A6A6AF","#BBBCC5","#F2F2FD","#E8F1FF","#D5E6FF","#95BEFF","#498FFF","#FFF9F8","#FFDDD8","#FFA59B","#FF5F57","#FFFAF6","#FFDFC5","#FFA95E","#F18700","#C6FFCB","#7DFF91","#00DA51","#00B943","#F0F1FF","#E1E3FF","#B4B6FF","#8982FF"],"flat":[-8.022975593833932,1.3402714370624624,-13.06659651622355,1.5567826841029107,-72.79588351600884,5.127281558149197,-64.0643931275358,4.500912896664839,-41.08533982136385,3.0154992993085026,-29.446612137967147,2.3594628663401775,-41.08533982136385,3.0154992993085026,-72.79588351600884,5.127281558149197,-71.17583391665679,5.008590480308264,-64.08586870464498,4.502412953317652,-40.969901639859465,3.0086620775128243,-18.492326685553408,1.8062558438836862,-77.39176152066469,5.470028106981068,-64.06334225320066,4.50083949888721,-41.06186552000793,3.0141084284175577,-20.58109958398818,1.9067218663179524,-77.74802214310357,5.496966722090798,-64.16882972322054,4.508209630901898,-40.97688137217631,3.00907529195687,-26.93048349706687,2.2267208299060117,-71.5855645088526,5.038503428518723,-64.0583201718401,4.500488740880354,-41.24499881268402,3.0249663274797034,-25.961518214862377,2.176489332337808,-72.19208007215988,5.082914152992658,-64.24580556994597,4.513590807561596,-40.91738125342747,3.005553523356943,-18.74578210381434,1.818317299241165]},{"theme":"dark","bg":"#123456","fgs":["#3E3E45","#47474E","#F0F0FC","#BDBDC6","#A1A1AA","#7F7F88","#A1A1AA","#F0F0FC","#E5EFFF","#7AADFF","#3D87FF","#005FDF","#FFF6F5","#FF988E","#FF4D48","#D5001A","#FFF8F3","#FFA75A","#EA8300","#BA6700","#BDFFC3","#00D34E","#00B341","#008C31","#EEEFFF","#A5A5FF","#837AFF","#6433FF"],"flat":[0,1.1989453999458657,0,1.3802668927515667,-90.21171279539291,11.248602775503162,-60.350304841650235,6.81783465078813,-45.467647083484636,4.962060105853706,-28.90763677945104,3.2057711392885686,-45.467647083484636,4.962060105853706,-90.21171279539291,11.248602775503162,-88.52359832003891,10.974551054175622,-50.82334067294272,5.601029591424501,-33.90019185783247,3.6989242542195706,-17.94937633357589,2.2421371448335408,-94.53061684959366,11.962030249162733,-55.11693338695639,6.136943021617957,-35.65662848796355,3.880065704029081,-19.010500723408704,2.3279588656978594,-95.32685818617963,12.095472485308715,-58.832789738116276,6.617312123916825,-43.099834270009964,4.690265818580579,-27.202021451205145,3.0448373808729228,-89.02366089613628,11.055448870118658,-56.507119659393844,6.314879036055816,-41.91393002442956,4.556652813501584,-25.675484951698337,2.9041378624661793,-89.65880574741696,11.158543725868578,-51.69548126800453,5.708204299589472,-34.40239467659594,3.750316223165759,-16.078998268001875,2.0949612264666513]},{"theme":"dark","bg":"#ABCDEF","fgs":["#B4B4BE","#A8A8B1","#09090F","#55545C","#706F78","#A1A1AB","#706F78","#09090F","#00020F","#0045A5","#0060E2","#3F88FF","#140000","#A10011","#D7001B","#FF4F49","#190900","#814600","#A85D00","#EA8300","#000A01","#006320","#00822D","#00B341","#030014","#4A00CC","#6537FF","#847BFF"],"flat":[8.489900665165298,1.2445786124510558,14.633674840112363,1.428005718026211,75.26124671135368,12.018395958758797,52.798813706484495,4.5250375920814765,41.423484798334584,3.0020546798073307,18.08917030758312,1.5497949541740348,41.423484798334584,3.0020546798073307,75.26124671135368,12.018395958758797,76.15878035868923,12.51550617725489,56.792209524954714,5.308053794065892,44.82231091099692,3.370285744389143,29.010809014184062,2.057710850183084,75.84645644595844,12.343365988484052,55.500674792931,5.036843578382145,43.81499067450534,3.25473208522667,27.348178994525146,1.9655632934834584,74.80511978941351,11.764533211175078,52.793708756474935,4.52413702940673,41.44647230556496,3.0043476386355414,20.422200521634597,1.6411239547419576,75.54552640875083,12.176514522007174,52.82929323710673,4.530419449281277,41.44477504659015,3.004178254346042,21.57431352051131,1.6892460110571705,76.19495675605003,12.535354496536943,59.24767070343283,5.876074797996776,46.58341111523108,3.5866744685580465,28.54022915013225,2.030988108287992]},{"theme":"dark","bg":"#808080","fgs":["#F7F7FF","#0A0A10","#71717A","#fff","abcdef","#Ff3B30","#123","112233"],"flat":[-64.57463009518332,3.7055405145881393,38.97016765822309,4.9982958489854346,0,1.2236914579873204,-69.21596068658343,3.9494396480491156,-37.021615579693034,2.3904478169235754,0,1.1134158562260283,34.84722274356656,4.0887512719822805,34.84722274356656,4.0887512719822805]},{"theme":"light-ic","bg":"#FFFFFF","fgs":["#F7F7FF","#F7F7FF","#0A0A10","#71717A","#94949E","#BDBDC7","#94949E","#0A0A10","#000110","#0042E2","#3B78FF","#85AEFF","#150000","#BD0011","#FF001B","#FF998D","#1A0500","#B04A00","#E96400","#FFA174","#000B01","#007C2D","#00A73F","#00D452","#00040B","#00639C","#0089D6","#4DB3FF"],"flat":[0,1.0658201232723763,0,1.0658201232723763,104.65963437608701,19.74046779866239,70.1623092547118,4.832895561154151,53.96706418364319,3.004779237664955,33.34341874861831,1.8637664731545371,53.96706418364319,3.004779237664955,104.65963437608701,19.74046779866239,105.76809306682351,20.754602558566376,81.70242596755779,7.298044196933449,63.72862579130605,3.9499301799077116,41.321899652634976,2.211520376313395,105.32470288110206,20.35108757719671,79.14032751842986,6.6207260543602775,64.03544238362204,3.986463961054262,38.10332212672181,2.0602150617562085,104.62285904737456,19.706640245810163,73.93672186564804,5.489431142391079,57.895853978119064,3.3399091341000195,36.26437665881752,1.9807324696579875,104.97702200816887,20.032297013031588,73.2226464627747,5.355840815772706,56.123729301186145,3.182173571701684,36.54497093010938,1.9925549350335838,105.53601527934143,20.543945015271454,78.36297683006073,6.432254688155311,62.2832355663668,3.784280156272109,42.68471757482252,2.28072562681704]},{"theme":"light-ic","bg":"#000000","fgs":["#010103","#010103","#F1F1FC","#B3B3BC","#909099","#65656D","#909099","#F1F1FC","#E8F0FF","#709FFF","#2D6DFF","#0038C6","#FFF4F3","#FF8275","#F30019","#A7000E","#FFF6F1","#FF8C52","#DF5F00","#9D4100","#C2FFC9","#00C84D","#009F3C","#006E27","#DFF0FF","#10A5FF","#0082CB","#005689"],"flat":[0,1.0069471255994658,0,1.0069471255994658,-99.67218963360757,18.727928195985914,-63.80513707672789,10.090949031745104,-45.863656882792206,6.635143500688325,-26.33477890397734,3.635637824948841,-45.863656882792206,6.635143500688325,-99.67218963360757,18.727928195985914,-98.22794037003116,18.339196272629252,-53.8293996799751,8.092203727319841,-34.21813658812841,4.7430333536498335,-15.622162303180753,2.381112763636304,-102.4562582898777,19.486486384763445,-56.97432424245902,8.701923574139988,-34.76332175899368,4.824974728357804,-18.151652148372637,2.6494391947645526,-103.24826961310278,19.70448161329645,-59.10147589327934,9.125072654774526,-40.787696351505694,5.7744786453226835,-22.81212855995232,3.1897383862050708,-98.60837652742211,18.44127602271432,-60.30793382928855,9.368875012646658,-42.29572232514961,6.0245037012352585,-23.381466511664765,3.259668821930569,-97.12909045511768,18.04563033112125,-52.54162933208091,7.848085216280267,-36.273216344700636,5.055412083856902,-18.54110013749204,2.69234396770907]},{"theme":"light-ic","bg":"#808080","fgs":["#7A7A82","#7A7A82","#010103","#18171E","#37363E","#5B5B64","#37363E","#010103","#000002","#00104D","#002488","#0032B2","#040000","#3A0002","#750007","#98000B","#080100","#2E0E00","#602500","#8F3B00","#000200","#001D05","#004114","#006322","#000102","#00192E","#003A5F","#004D7C"],"flat":[0,1.0777393702545288,0,1.0777393702545288,40.18994509287661,5.280525528202377,36.81932911616927,4.505560397043893,27.93898600302036,3.0217055248761144,13.845788611978202,1.7013528597992067,27.93898600302036,3.0217055248761144,40.18994509287661,5.280525528202377,40.3306726916817,5.312553092992316,36.799792142283025,4.501224406456354,30.04772679653348,3.3069997994324063,23.797727835383583,2.533076638038498,40.23106479600237,5.289901483963394,36.80831529061993,4.503115584720142,27.941932493369514,3.022087143525339,21.190215752118235,2.2723970292907865,40.01441400964806,5.240352767492206,36.83929336929951,4.509994812829007,27.908315541084118,3.0177360720288915,16.77490328531677,1.9034128352995776,40.150141663678234,5.271436537678366,36.85731353296706,4.514000513189092,27.852320260403722,3.0105025419148825,16.643886294769857,1.8937047658181707,40.22977489962569,5.289607582090012,36.84755783504637,4.511831555904809,27.796281383025352,3.0032808914145437,21.058698283639867,2.260143784790762]},{"theme":"light-ic","bg":"#3A3A3C","fgs":["#3E3E45","#3E3E45","#F2F2FD","#BBBBC5","#9E9EA7","#7A7A82","#9E9EA7","#F2F2FD","#EAF1FF","#81ABFF","#4982FF","#0048F5","#FFF5F4","#FF9589","#FF403B","#CC0013","#FFF7F3","#FF9E6E","#F46900","#BD5000","#C6FFCC","#00D251","#00B043","#008631","#E1F1FF","#45B0FF","#0090E1","#006CA9"],"flat":[0,1.0699484563148034,0,1.0699484563148034,-89.06520303275792,10.211241554507897,-56.964055924386784,5.959497869408446,-41.60773958742316,4.2707825803649335,-24.25691832785775,2.6663016206717356,-41.60773958742316,4.2707825803649335,-89.06520303275792,10.211241554507897,-87.69551010135778,10.011246151186945,-48.263751486481056,4.97335266465752,-30.44825366943375,3.199530942583609,-12.043546933756078,1.753943193668956,-91.71639880150975,10.602849175762582,-51.72679347557047,5.3568510977778265,-31.21621892784593,3.268795397886287,-14.661523244642579,1.9330507439017046,-92.55409442667087,10.727809933310253,-53.89547122979923,5.603122076267749,-35.95806640240085,3.7112655792375078,-20.04367187639212,2.329915503818876,-87.89039906251095,10.039605852107476,-53.75183524673264,5.586666992924044,-38.313874917805634,3.940371955014153,-21.037647044419426,2.407284189061088,-86.58838125370931,9.850752189993974,-46.78872683576137,4.81370216625391,-31.383244337404896,3.2839492580871354,-15.736559790538532,2.009284883552937]},{"theme":"light-ic","bg":"#007AFF","fgs":["#797981","#797981","#020205","#16151C","#36353D","#72727A","#36353D","#020205","#000004","#000E48","#002895","#0042E3","#070000","#360001","#730006","#BE0011","#0C0200","#2B0D00","#5F2400","#B04A00","#000400","#001B05","#003F13","#007C2D","#000103","#00172B","#00395C","#00639D"],"flat":[0,1.0745907693903731,0,1.0745907693903731,39.47891404105317,5.158402202047827,36.63906407938327,4.513939148043301,27.726113654933034,3.015791844568742,0,1.187212547453551,27.726113654933034,3.015791844568742,39.47891404105317,5.158402202047827,39.74741500641279,5.218664231374835,36.670023859840576,4.5208004701163915,27.672031623266886,3.008810094200282,15.351341296852958,1.8098737923818007,39.57933742278427,5.181007308853048,36.654144364688555,4.517280323585742,27.751464091733354,3.019069960427185,12.671992881770905,1.6349566993987985,39.23716411822194,5.10371254671437,36.601471319299684,4.505617849545331,27.652865692456807,3.006339685224649,7.684471784052528,1.3665581876464832,39.39108202991847,5.138573889881769,36.624715730830154,4.510761781161014,27.902773646365304,3.038708829709556,0,1.3333017445114326,39.65687576920386,5.198408498908002,36.65150522712613,4.5166954713046765,27.64129393913477,3.0048491045091428,12.054359140378313,1.5979376774972005]},{"theme":"light-ic","bg":"#FF3B30","fgs":["#8A8A92","#8A8A92","#030306","#212129","#3E3D45","#76767F","#3E3D45","#030306","#000004","#001864","#002FA8","#0045ED","#080000","#4C0003","#830008","#C60012","#0D0200","#3E1500","#6C2A00","#B84D00","#000401","#002809","#004917","#00822F","#000103","#00233D","#00416A","#0068A4"],"flat":[0,1.0356140935636031,0,1.0356140935636031,43.57584669970237,5.806877937534237,38.3004661979112,4.504747008392574,29.108000765094282,3.0232304751839747,0,1.268226675946831,29.108000765094282,3.0232304751839747,43.57584669970237,5.806877937534237,43.98030914587015,5.909904848562026,38.293478372508915,4.503221171740978,28.94317626341717,3.002107337321695,17.95394033246701,1.9258322867444517,43.782795788734404,5.8597651770218455,38.38866051296957,4.524070782124866,28.984608537472,3.0074014381148784,15.156957289606964,1.7362054736619685,43.437607341847624,5.771384990646562,38.33399448678559,4.512078832150148,28.981628167432756,3.007020263830111,9.892674202630067,1.4441010369850493,43.614187977218386,5.816700203526642,38.406807106466914,4.528061863682665,29.022563392947294,3.012260432085235,8.92751562582044,1.3982348229239339,43.88976990866122,5.886966133555759,38.401524303038336,4.526899458852529,29.03631728859714,3.0140233900654434,14.306925700285792,1.683714708917422]},{"theme":"light-ic","bg":"#101012","fgs":["#15151B","#15151B","#F2F2FC","#B4B4BE","#93939C","#696972","#93939C","#F2F2FC","#E9F0FF","#74A2FF","#3371FF","#003CD0","#FFF4F3","#FF8679","#F8001A","#AF000F","#FFF6F2","#FF9058","#E36100","#A44400","#C3FFC9","#00CA4E","#00A33D","#007329","#E0F0FF","#21A8FF","#0085D0","#005B90"],"flat":[0,1.045340264415452,0,1.045340264415452,-98.5838326466521,17.08955684747884,-62.72698384194396,9.243246994613658,-45.67498009680545,6.239717532288448,-26.395025771311026,3.4974414103185016,-45.67498009680545,6.239717532288448,-98.5838326466521,17.08955684747884,-96.69585202312288,16.62869936569993,-53.53835262595417,7.561646720391368,-33.90853669724825,4.477228400656394,-15.643424374972522,2.314414627530853,-100.79897840003042,17.636700891614694,-56.5097869254783,8.089667628698802,-34.29633291417122,4.530985783372177,-18.10601775891745,2.56107712884258,-101.63443561648342,17.844851007069916,-58.71630179001587,8.4915976550775,-40.34850051574898,5.4087697654311055,-22.889197327723014,3.0820931228283226,-97.04991927160123,16.714741751700544,-59.578454823729174,8.650894122604043,-42.30409033566848,5.707629679225331,-23.50941826359813,3.1535587190594883,-95.5914257275877,16.361459308701495,-52.26386479821355,7.339891411251783,-35.96637326948896,4.765958094131511,-18.710865986535588,2.6239504003951875]},{"theme":"light-ic","bg":"#F6F8FA","fgs":["#F0F0FA","#F0F0FA","#0A0910","#707079","#8F8F99","#BCBCC5","#8F8F99","#0A0910","#000110","#0041DF","#3876FF","#82ACFF","#140000","#BB0011","#FC001B","#FF968A","#1A0500","#AE4900","#E76300","#FF9F70","#000B01","#007B2C","#00A63F","#00D252","#00040B","#00629B","#0088D4","#47B1FF"],"flat":[0,1.0634215248159875,0,1.0634215248159875,100.62201386979996,18.61882254505944,66.47667620763235,4.605948460093869,52.23653324893716,3.0084294463712395,29.780700684603257,1.7710808257265196,52.23653324893716,3.0084294463712395,100.62201386979996,18.61882254505944,101.64252534329776,19.49544279635548,78.08171358923367,6.990900026896573,60.31711036263554,3.7908833425501425,38.14751646036453,2.122399526754989,101.24542779029852,19.156162076239593,75.44574141529571,6.3202869230281635,60.62673294996029,3.82662941345977,34.94277926170305,1.9762445240799382,100.4972913238488,18.51105924752946,70.36281029021609,5.256397052662421,54.37780741212484,3.1906001595018556,32.840214506002376,1.8885138214456323,100.85145428464311,18.816958763489637,69.48045983543288,5.0977335979175304,52.4093478352768,3.0225491335598114,33.31123457222728,1.9076443919396007,101.41044755581565,19.297565623167603,74.60712675826916,6.125340559948453,58.63177587043786,3.604653750954651,39.55009522975671,2.1915837971666887]},{"theme":"light-ic","bg":"#1C1C1E","fgs":["#202028","#202028","#F1F1FD","#B6B6BF","#95959E","#6D6C75","#95959E","#F1F1FD","#E9F1FF","#76A4FF","#3875FF","#003ED7","#FFF4F3","#FF897C","#FC001B","#B50010","#FFF6F2","#FF935D","#E66300","#A94600","#C4FFCA","#00CB4F","#00A53F","#00772B","#E0F0FF","#2BA9FF","#0087D3","#005E95"],"flat":[0,1.0522076023272475,0,1.0522076023272475,-96.27140987076255,15.184146579953019,-61.97466067394124,8.452688508186561,-44.87094637123084,5.728792540137694,-25.977188885961883,3.283145241842075,-44.87094637123084,5.728792540137694,-96.27140987076255,15.184146579953019,-95.34499825068502,14.98175500729861,-52.621417231568934,6.906762415988252,-33.47849770408375,4.178062617945301,-14.893749254597887,2.163552488849563,-99.00844385529109,15.788447906049281,-55.63494162352738,7.390318911682041,-33.467257033822236,4.176644120449875,-17.547592326855693,2.408179342981014,-99.84390107174409,15.974784754119627,-57.911424929350886,7.764836527735112,-39.59013024218227,4.982347876116714,-22.33006111099907,2.8868875311347497,-95.39392806862337,14.992417327878547,-58.26045217149408,7.822951516486187,-41.372488477704024,5.229057297458628,-23.176065198031452,2.976455642863797,-93.8008911828484,14.646846343308852,-51.05892490110174,6.66159703590636,-35.06283530237813,4.380256350040106,-18.07990151348513,2.4590883821528062]},{"theme":"light-ic","bg":"#7C3AED","fgs":["#707078","#707078","#F4F4FE","#E3E4ED","#BDBDC6","#A6A6B0","#BDBDC6","#F4F4FE","#EDF3FF","#D8E6FF","#9ABDFF","#598FFF","#FFF8F7","#FFDDD8","#FFA59A","#FF6156","#FFF9F7","#FFDECE","#FFA77C","#FF7112","#D0FFD4","#7DFF93","#00D955","#00B947","#E5F3FF","#CEE8FF","#79C3FF","#0099EE"],"flat":[0,1.1608373681184396,0,1.1608373681184396,-73.98396246220521,5.215032461901156,-64.0643931275358,4.500912896664839,-41.75099800228171,3.055051474145459,-29.486682699107686,2.3616035737730514,-41.75099800228171,3.055051474145459,-73.98396246220521,5.215032461901156,-72.68698719753608,5.119268434409956,-64.43727527190525,4.526987198998707,-41.01457995095163,3.0113075347043563,-19.296469937711088,1.8446469211142276,-76.90365537296871,5.4332059719499135,-64.06334225320066,4.50083949888721,-41.0312066106476,3.012292267380192,-20.969079339398604,1.925648566411713,-77.34727635116013,5.466668081501757,-64.08510797117285,4.502359813071005,-40.89305028080999,3.004113887137535,-23.84545366268376,2.068528569757355,-72.9506558335481,5.138679113149092,-64.10668565953954,4.5038671985064,-40.827825177303865,3.0002560129496776,-26.013778316259767,2.1791858289450103,-71.69069599862902,5.046190228559761,-64.15987367011111,4.5075837056249375,-40.85863800181002,3.0020782433474276,-19.258832894479415,1.8428420367238694]},{"theme":"light-ic","bg":"#123456","fgs":["#36363D","#36363D","#F2F2FD","#BEBEC8","#A2A2AC","#808089","#A2A2AC","#F2F2FD","#EAF1FF","#86AEFF","#5189FF","#0650FF","#FFF6F4","#FF9B8F","#FF5349","#D70015","#FFF7F3","#FFA376","#FA6C00","#C75400","#C8FFCD","#00D553","#00B445","#008D34","#E2F1FF","#50B4FF","#0094E7","#0072B3"],"flat":[0,1.0609289970892934,0,1.0609289970892934,-91.39519035537737,11.4423466076867,-60.94367397154904,6.896920022598847,-46.020165423777584,5.026435795944436,-29.36949737122932,3.250019698594834,-46.020165423777584,5.026435795944436,-91.39519035537737,11.4423466076867,-90.02549742397721,11.218239018758677,-52.117590678997175,5.760385990810306,-35.453068631322715,3.8588722418561026,-17.16315496591595,2.1796295803105563,-94.48648764812391,11.954651961610136,-56.08049309972586,6.260045669338659,-36.68653001054492,3.9880923379680597,-19.42498133620683,2.3619323117143227,-94.88408174929032,12.021194381023134,-58.024655972390946,6.511549338355004,-40.19653971715428,4.366167533936967,-25.014063203920518,2.8441658803090872,-90.45906258458693,11.288985892577246,-57.51342916294683,6.44501257640493,-42.39985031117064,4.611196053798779,-26.099142978345885,2.9428672820648862,-89.03890865199516,11.05791933389335,-51.142776043878534,5.640183677550314,-35.55629387962385,3.86961291577043,-20.57349953874105,2.4573829383449617]},{"theme":"light-ic","bg":"#ABCDEF","fgs":["#C2C2CB","#C2C2CB","#06060C","#55545D","#706F78","#A1A1AA","#706F78","#06060C","#00010B","#0035BB","#044EFF","#4E86FF","#0F0000","#9F000C","#D60015","#FF4D45","#150400","#903B00","#BB4F00","#F86B00","#000801","#006322","#008230","#00B344","#000307","#005182","#0071B2","#0093E5"],"flat":[0,1.0705446339746656,0,1.0705446339746656,75.64582413051765,12.232204997602986,52.76667497081496,4.519371986307913,41.423484798334584,3.0020546798073307,18.127484103049046,1.5512322351556214,41.423484798334584,3.0020546798073307,75.64582413051765,12.232204997602986,76.30385323493446,12.594966222228084,58.55230095328762,5.70794052541694,46.62204712410211,3.5916381056717506,29.005214449343097,2.0573900879913625,76.05323451380515,12.457481581934719,55.93606418979168,5.126251708978212,44.077718465236906,3.284322001440957,27.730078776096413,1.986177453897667,75.43283879959024,12.113874624281086,52.77586207835023,4.520990578468311,41.44187963136357,3.003889327255304,23.868980516769618,1.7916338607346896,75.72958089661125,12.278654436624837,52.80881394476793,4.526802423347816,41.41072061989075,3.0007825637465393,21.536768417458934,1.687644517337887,76.14729893560808,12.509202438867769,55.739334564053664,5.085602713702602,43.07798550292857,3.173723442083943,28.271676351762963,2.015968674447834]},{"theme":"light-ic","bg":"#808080","fgs":["#F7F7FF","#0A0A10","#71717A","#fff","abcdef","#Ff3B30","#123","112233"],"flat":[-64.57463009518332,3.7055405145881393,38.97016765822309,4.9982958489854346,0,1.2236914579873204,-69.21596068658343,3.9494396480491156,-37.021615579693034,2.3904478169235754,0,1.1134158562260283,34.84722274356656,4.0887512719822805,34.84722274356656,4.0887512719822805]},{"theme":"dark-ic","bg":"#FFFFFF","fgs":["#E7E7F1","#D9D9E2","#0D0D12","#707079","#94949D","#BCBCC6","#94949D","#0D0D12","#000511","#005DAD","#0080EB","#67AEFF","#180001","#BB0023","#FB0032","#FF9690","#1D0D00","#A16200","#D38200","#FFA93F","#000E02","#007C2A","#00A63B","#00D24D","#060018","#6E00E2","#8F5BFF","#B49FFF"],"flat":[10.564452802176923,1.2281904700369508,18.372353597432387,1.402460683906872,104.27090588423802,19.38311547953439,70.6022439311581,4.903434648448188,54.00450766949271,3.0077315832596954,33.86624309585679,1.8839141891835494,54.00450766949271,3.0077315832596954,104.27090588423802,19.38311547953439,105.36973966240839,20.39226096069594,79.16304803673168,6.626348275559002,63.912632944603466,3.971781216419874,43.318131952880584,2.3140155027501375,105.16667360757414,20.20636839933495,79.43361118518992,6.693805381831162,64.79709340122825,4.079330310903514,38.90470830731088,2.096381256670562,103.76723276112338,18.92212884965907,70.7453629185683,4.926701923645705,54.109273202798526,3.0160152686700097,34.512224892089854,1.9092708836328856,104.66191026229393,19.742561272328775,73.25578885512941,5.361938351796974,56.58363991444959,3.2220248289870597,37.502971296697396,2.0337403797475795,105.5639998705614,20.569417077144905,82.76169093861434,7.605398027541734,65.05029978451972,4.11090668779679,42.03743585537794,2.2474520733771812]},{"theme":"dark-ic","bg":"#000000","fgs":["#030205","#08080D","#F0F0FA","#B1B1BB","#8F8F98","#64646D","#8F8F98","#F0F0FA","#DFEEFF","#4AA0FF","#007AE0","#005198","#FFF4F3","#FF7D79","#EF0030","#A6001E","#FFF7EF","#F99A00","#CA7D00","#915800","#B8FFBE","#00C648","#009F38","#006E24","#F0EDFF","#A98DFF","#894CFF","#6000C7"],"flat":[0,1.0147465549687367,0,1.0508696650285665,-99.01083964835996,18.549513785944853,-62.774763175831374,9.875849762659984,-45.37711439886134,6.5503119047070335,-25.95111170367181,3.5855589616463144,-45.37711439886134,6.5503119047070335,-99.01083964835996,18.549513785944853,-96.24720863865953,17.81140932471069,-52.09132740819334,7.763491481432673,-34.99617079306692,4.860177125212619,-17.977340963301042,2.6303715448994067,-102.4562582898777,19.486486384763445,-55.7461567674855,8.461541475204797,-34.016004840410574,4.712824540535038,-18.066789785866806,2.6401456993132153,-103.60398268701002,19.802704734429785,-61.68353878496926,9.650187857341214,-44.76775468944945,6.444757556916112,-26.060791619056427,3.599837857825526,-97.27916136850891,18.085610527348415,-59.33082759509665,9.171209294707863,-42.24705692654848,6.016359585841944,-23.350526713793172,3.2558471191579894,-97.94239544297146,18.262727694350296,-53.03355221198167,7.94095381214551,-32.857248309508414,4.541449578831631,-15.041171256747345,2.322063533056465]},{"theme":"dark-ic","bg":"#808080","fgs":["#707078","#66666E","#030305","#18171D","#37363E","#5D5D64","#37363E","#030305","#000103","#001834","#00376B","#004A8C","#070000","#390005","#750012","#99001B","#0D0400","#261300","#522F00","#865100","#000300","#001D05","#004112","#006521","#010005","#22004F","#43008F","#5800B7"],"flat":[0,1.24296889833444,9.274501031402123,1.4407196981948422,39.91548097777799,5.217615096019717,36.836632862345205,4.5094036578339445,27.93898600302036,3.0217055248761144,13.086665786299626,1.6537770693375544,27.93898600302036,3.0217055248761144,39.91548097777799,5.217615096019717,40.21965913293546,5.287302224624218,36.81767378273908,4.505192880903515,27.950777846769416,3.023233051937069,21.000772856623527,2.25477321245894,40.142120786515875,5.269603471071473,36.88792093057972,4.5208109211231795,27.866909320024153,3.0123854877509646,20.876190217747006,2.243276608438576,39.57947058767665,5.139984882718895,36.89828467875601,4.523118840482662,27.8635036155662,3.011945822214043,13.292640012092916,1.6665035199669143,40.051433392362526,5.248844071123953,36.85731353296706,4.514000513189092,27.870384117529113,3.0128341396537346,15.95953035602916,1.844055168824021,40.26997047853993,5.298759395894901,36.80600977189143,4.502603953746083,29.651273965169533,3.2515347547731954,23.802145579236715,2.533547958530533]},{"theme":"dark-ic","bg":"#3A3A3C","fgs":["#46464E","#4F4F57","#F1F1FA","#BABAC4","#9D9DA6","#797982","#9D9DA6","#F1F1FA","#E1EFFF","#62ACFF","#0087F7","#0065BB","#FFF5F4","#FF928C","#FF3644","#CA0027","#FFF8F1","#FFA533","#DD8900","#AD6A00","#BCFFC2","#00D04C","#00AF3F","#00862E","#F1EFFF","#B29CFF","#956AFF","#7700F5"],"flat":[0,1.2140718035663858,0,1.3988859584097761,-88.35603271482154,10.107493939375463,-56.41682578047019,5.8952705955406675,-41.10063170437542,4.219157833086703,-23.840055313660137,2.6320437618590202,-41.10063170437542,4.219157833086703,-88.35603271482154,10.107493939375463,-85.70519954500536,9.72347127561709,-46.57313915269262,4.790554592637207,-29.772868283167547,3.1391774083572677,-14.678197720495364,1.9342213705917548,-91.71639880150975,10.602849175762582,-50.907190680193835,5.264997878479995,-30.103011927821978,3.1686137953283313,-14.381529067368087,1.9134497328938256,-92.9101803888625,10.781105347058572,-55.36632323767275,5.772792653136124,-40.27550194757439,4.135743433221995,-23.629833528615176,2.614848030728214,-86.56463892830851,9.847321829129962,-52.75275099572737,5.472775232316544,-37.83315401910713,3.893128667535661,-21.00389596511428,2.404636707585164,-87.71836703917482,10.014570561299498,-47.715877818006604,4.91379345402092,-29.619990398118716,3.125589171245837,-10.872649839108671,1.6769004806806627]},{"theme":"dark-ic","bg":"#007AFF","fgs":["#6F6F77","#65656D","#040408","#16151B","#36353D","#72727A","#36353D","#040408","#000106","#001632","#00366A","#005EAF","#0B0000","#360004","#730011","#BD0023","#100600","#241200","#502E00","#A36300","#000501","#001B04","#004012","#007D2B","#020009","#20004A","#48009A","#6F00E5"],"flat":[0,1.2399763057879079,9.137153276370956,1.4379357071162235,39.199487305374596,5.095159290353486,36.655643103420836,4.517612479062309,27.726113654933034,3.015791844568742,0,1.187212547453551,27.726113654933034,3.015791844568742,39.199487305374596,5.095159290353486,39.62660370456575,5.191620542906115,36.592103069401674,4.503545882983851,27.695057695096033,3.0117807086970667,12.482735608358766,1.623488823202914,39.46146930028483,5.1544680971312,36.631002505660305,4.512153756975423,27.677944212691237,3.0095726068531357,12.751768680957815,1.6398240151911772,38.729462058672325,4.987982714010286,36.579572970042136,4.500775701626332,27.747483785710887,3.0185550274007946,0,1.2039987886628472,39.28492308543262,5.114543979353608,36.632422880871374,4.512468289727634,27.610461383700073,3.00088106913278,0,1.3173837917708568,39.63684952401951,5.1939188146581685,36.601297620325695,4.505579426399357,27.609531401873983,3.0007614645527125,16.085712499624492,1.86220122646402]},{"theme":"dark-ic","bg":"#FF3B30","fgs":["#7F7F87","#74747D","#040408","#212128","#3E3D45","#76767F","#3E3D45","#040408","#000206","#002245","#003E78","#0062B6","#0B0000","#4C0008","#820015","#C50025","#110600","#341C00","#5B3600","#A96700","#000601","#002808","#004916","#00822D","#02000A","#2E0067","#5300AD","#7400EE"],"flat":[0,1.119564506502328,0,1.304916965157494,43.43238144483196,5.770041002680692,38.322111022898454,4.5094781825179275,29.108000765094282,3.0232304751839747,0,1.268226675946831,29.108000765094282,3.0232304751839747,43.43238144483196,5.770041002680692,43.7603252928922,5.854038847974277,38.29235457140463,4.502975854531618,29.083956028514045,3.020138635078335,15.054852030501879,1.729780743830801,43.694363439742204,5.837205585263101,38.354206470247746,4.516507237210082,29.087182234169767,3.020553277003921,15.230546565278194,1.740856512459411,42.92491991360228,5.63897133999608,38.33365423714702,4.512004338988724,28.93769973409828,3.0014083479801514,0,1.2794758858630608,43.42248485866928,5.7674953973596095,38.41370496023468,4.529580289565808,29.031977011275746,3.0134669332394415,8.949958172317846,1.3992774406692907,43.85966524740055,5.879321607935794,38.335090707081115,4.512318847459295,28.952846517354917,3.003342035233188,18.7969313728254,1.9885281036480773]},{"theme":"dark-ic","bg":"#101012","fgs":["#1C1C22","#24242A","#F0F0FA","#B3B3BD","#92929B","#686871","#92929B","#F0F0FA","#E0EEFF","#4FA3FF","#007DE4","#00559F","#FFF4F3","#FF817D","#F40031","#AD001F","#FFF7EF","#FB9C00","#CE7F00","#965B00","#B9FFBF","#00C849","#00A239","#007226","#F0EEFF","#AA90FF","#8C52FF","#6500D0"],"flat":[0,1.1210017368042908,0,1.2316217101091165,-97.35355975851269,16.788671896406843,-62.18912907483276,9.1408846014785,-45.183954583215964,6.160892481462449,-25.971291980892453,3.445784610130369,-45.183954583215964,6.160892481462449,-97.35355975851269,16.788671896406843,-94.7101738760577,16.149472101206463,-51.76844025191021,7.254462182627787,-34.605057818633654,4.573999892517157,-17.84521283217997,2.534242228411278,-100.79897840003042,17.636700891614694,-55.24750846744172,7.863491890193123,-33.54190885081095,4.42668652015819,-17.815136446883052,2.5311583129351276,-101.94670279716274,17.922901715066683,-61.02785966423507,8.921519321161206,-44.34904450398409,6.02788949767693,-25.86724540779468,3.4331609270477186,-95.74776769254933,16.399183837627458,-58.596724506079966,8.469603286006345,-41.840254848173814,5.636085625741139,-23.122294138224145,3.108849028833141,-96.71860922522349,16.634224259107555,-52.4354046324141,7.369572153296626,-32.71416296796099,4.3135863728502315,-14.766203746026353,2.2302399315399106]},{"theme":"dark-ic","bg":"#F6F8FA","fgs":["#E0E0EA","#D2D2DC","#0D0D12","#717179","#8F8F98","#BCBCC6","#8F8F98","#0D0D12","#000511","#005DAD","#0080EB","#67AEFF","#180001","#BB0023","#FB0032","#FF9690","#1D0D00","#A16200","#CC7E00","#FFA93F","#000E02","#007C2A","#00A63B","#00D24D","#060018","#6E00E1","#8F5BFF","#B49FFF"],"flat":[10.34292311627503,1.2313464424988367,18.024461551245693,1.4097592283337514,100.14533816071224,18.207162386274927,66.07133001527934,4.544848448940367,52.273633182943,3.011452358576948,29.740675372331037,1.769619110018133,52.273633182943,3.011452358576948,100.14533816071224,18.207162386274927,101.24417193888262,19.155084079579783,75.03748031320593,6.224334741672483,59.78706522107772,3.730817455350078,39.19256422935483,2.1736266322828217,101.04110588404839,18.980469423093126,75.30804346166417,6.287699296730837,60.6715256777025,3.8318416601446055,34.77914058378512,1.9691960254812915,99.64166503759763,17.774143327140518,66.61979519504256,4.627804134340593,52.265155427331,3.010761189364384,30.386657168564103,1.79343744066295,100.53634253876818,18.544800983402446,69.13022113160365,5.036635229225181,52.45807219092383,3.026547994099155,33.37740357317164,1.9103555042369325,101.43843214703563,19.321492322017065,78.73738474892319,7.17242701812758,60.92473206099399,3.861502331684902,37.91186813185219,2.111101535692422]},{"theme":"dark-ic","bg":"#1C1C1E","fgs":["#28282E","#303036","#F0F1FA","#B5B5BE","#94949D","#6C6C74","#94949D","#F0F1FA","#E0EEFF","#53A4FF","#007FE8","#0058A5","#FFF4F3","#FF8580","#F80032","#B30021","#FFF7F0","#FD9D00","#D18100","#9B5E00","#B9FFC0","#00CA4A","#00A43A","#007628","#F0EEFF","#AC93FF","#8D57FF","#6800D8"],"flat":[0,1.1612081374032734,0,1.2977584057597347,-96.00102363023473,15.124963081358866,-61.434200333395395,8.359778481952823,-44.37695589123639,5.656999216809453,-25.852207004995158,3.269134017520218,-44.37695589123639,5.656999216809453,-96.00102363023473,15.124963081358866,-92.91963933131836,14.457074514504098,-50.53751898728166,6.580638229837842,-33.762466759799054,4.213972880586457,-17.265259153354464,2.3814244063048684,-99.00844385529109,15.788447906049281,-54.621704682580784,7.2261694596451385,-32.708249696736004,4.081389020198301,-17.26088719592284,2.381011454094406,-100.19905346524207,16.054261379481122,-59.919533209261445,8.101702037099825,-43.64240726631874,5.55098822531801,-25.561703474916055,3.236683846808585,-93.99035989085725,14.687777164248876,-57.733604751916815,7.735299184328377,-40.893113879964474,5.162174617584753,-22.782862127638978,2.93464773079297,-94.92807468048419,14.891026660055605,-51.83505302329671,6.7828987244211,-32.07221654016657,4.002369709649521,-14.066989001346808,2.090531498955086]},{"theme":"dark-ic","bg":"#7C3AED","fgs":["#7A7A83","#85858D","#F2F2FD","#E3E4ED","#BBBCC5","#A6A6AF","#BBBCC5","#F2F2FD","#E5F1FF","#D2E7FF","#8AC0FF","#2091FF","#FFF9F8","#FFDDDA","#FFA49E","#FF5C5D","#FFFAF5","#FFE0C0","#FFAA42","#E99000","#C6FFCB","#7DFF91","#00DA50","#00B943","#F3F1FF","#E6E2FF","#C1B2FF","#9D7AFF"],"flat":[-8.022975593833932,1.3402714370624624,-13.06659651622355,1.5567826841029107,-72.79588351600884,5.127281558149197,-64.0643931275358,4.500912896664839,-41.08533982136385,3.0154992993085026,-29.446612137967147,2.3594628663401775,-41.08533982136385,3.0154992993085026,-72.79588351600884,5.127281558149197,-70.80493468751378,4.981574213000164,-64.1741844582375,4.5085838821814495,-40.86541522684489,3.0024791016401666,-17.915271666932362,1.7789289638170434,-77.39176152066469,5.470028106981068,-64.14420274941838,4.506488572747314,-40.82594869447877,3.0001450551263997,-20.102672577261966,1.8834970356793017,-77.70409895709017,5.493642615376888,-64.40976920815079,4.525061719384059,-40.93902681557826,3.00683450902807,-28.2154352545988,2.2940964528512438,-71.5855645088526,5.038503428518723,-64.0583201718401,4.500488740880354,-41.23137463469248,3.02415799426954,-25.961518214862377,2.176489332337808,-72.5833510184408,5.111647048756453,-64.45521953810703,4.528243512077896,-40.9348520371047,3.006587427640115,-18.521762858381592,1.807654812879132]},{"theme":"dark-ic","bg":"#123456","fgs":["#3E3E45","#47474E","#F0F0FC","#BDBDC6","#A1A1AA","#7F7F88","#A1A1AA","#F0F0FC","#E1EFFF","#68AEFF","#008BFD","#006AC4","#FFF6F5","#FF9791","#FF494F","#D30029","#FFF8F1","#FFA941","#E28C00","#B46E00","#BEFFC3","#00D34D","#00B340","#008C31","#F1EFFF","#B49FFF","#9971FF","#7D09FF"],"flat":[0,1.1989453999458657,0,1.3802668927515667,-90.21171279539291,11.248602775503162,-60.350304841650235,6.81783465078813,-45.467647083484636,4.962060105853706,-28.90763677945104,3.2057711392885686,-45.467647083484636,4.962060105853706,-90.21171279539291,11.248602775503162,-88.0351868676248,10.895768939711282,-50.01919534416472,5.502978516969682,-33.92042169291382,3.7009882121251625,-19.118481017052428,2.3367853131295435,-94.53061684959366,11.962030249162733,-54.89593536437227,6.108854061876529,-35.13675470036395,3.8260433618990066,-18.695569008603144,2.3023140000481574,-95.24016771148194,12.080915286992608,-59.18458546592085,6.6635751999987605,-44.37309985295556,4.8355935478331284,-28.125674388660414,3.1315039953558483,-89.11957165172129,11.070992163190612,-56.49372778683512,6.313154752438012,-41.901500594929324,4.555261373045625,-25.675484951698337,2.9041378624661793,-90.04835436179425,11.221964232051386,-51.2922333885473,5.658542947646642,-34.21126239038315,3.7307191421666435,-15.254686874750304,2.03178289437105]},{"theme":"dark-ic","bg":"#ABCDEF","fgs":["#B4B4BE","#A8A8B1","#09090F","#55545C","#706F78","#A1A1AB","#706F78","#09090F","#00030D","#004D92","#006CC7","#008BFE","#130001","#9F001C","#D60029","#FF4B50","#190B00","#7B4A00","#A06100","#E38C00","#000A01","#006320","#00822D","#00B341","#040013","#5B00BE","#7E16FF","#9971FF"],"flat":[8.489900665165298,1.2445786124510558,14.633674840112363,1.428005718026211,75.26124671135368,12.018395958758797,52.798813706484495,4.5250375920814765,41.423484798334584,3.0020546798073307,18.08917030758312,1.5497949541740348,41.423484798334584,3.0020546798073307,75.26124671135368,12.018395958758797,76.08600684668272,12.475516218126787,55.96473752983635,5.13221099097022,43.3369682989835,3.2018590596167122,29.318326289650187,2.0754572365320043,75.88156299830736,12.362776535253154,55.82217638546274,5.102669267083783,43.91931922104849,3.266436635421996,27.87409429449102,1.9940348577564484,74.63415754799017,11.66954886455628,52.68236753448497,4.504554642142053,41.632593181405234,3.023005829909607,19.021862742942993,1.5853635699211195,75.54552640875083,12.176514522007174,52.82929323710673,4.530419449281277,41.44477504659015,3.004178254346042,21.57431352051131,1.6892460110571705,76.18160530883091,12.528031661692317,58.96992438550627,5.808209180198148,47.36214126331064,3.68860336114861,29.106756402593504,2.0632235490419992]},{"theme":"dark-ic","bg":"#808080","fgs":["#F7F7FF","#0A0A10","#71717A","#fff","abcdef","#Ff3B30","#123","112233"],"flat":[-64.57463009518332,3.7055405145881393,38.97016765822309,4.9982958489854346,0,1.2236914579873204,-69.21596068658343,3.9494396480491156,-37.021615579693034,2.3904478169235754,0,1.1134158562260283,34.84722274356656,4.0887512719822805,34.84722274356656,4.0887512719822805]}],"resolveVars":[{"theme":"light","bg":"#FFFFFF","vars":{"--lab-bg-tone-2":"oklch(97.84197% 0.010603 286.202)","--lab-bg-tone-3":"oklch(97.84197% 0.010603 286.202)","--lab-label-primary":"oklch(19.12257% 0.014100 291.556)","--lab-label-secondary":"oklch(56.54196% 0.013721 285.953)","--lab-label-tertiary":"oklch(66.97448% 0.014606 285.999)","--lab-label-quaternary":"oklch(81.39504% 0.013897 286.087)","--lab-border-strong":"oklch(19.12257% 0.014100 291.556)","--lab-border-base":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-border-soft":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-primary":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-fill-secondary":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fill-tertiary":"oklch(57.53363% 0.012136 286.012 / 0.122)","--lab-fill-quaternary":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fx-shadow-minor":"oklch(17.39406% 0.004094 285.967 / 0.012)","--lab-fx-shadow-ambient":"oklch(17.39406% 0.004094 285.967 / 0.02)","--lab-fx-shadow-penumbra":"oklch(17.39406% 0.004094 285.967 / 0.039)","--lab-fx-shadow-major":"oklch(17.39406% 0.004094 285.967 / 0.122)","--lab-label-brand-primary":"oklch(19.39430% 0.069681 257.297)","--lab-label-brand-secondary":"oklch(57.33316% 0.205939 257.291)","--lab-label-brand-tertiary":"oklch(67.12978% 0.175419 257.690)","--lab-label-brand-quaternary":"oklch(81.31388% 0.094313 257.967)","--lab-fill-brand-primary":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-brand-secondary":"oklch(60.27647% 0.217712 257.424 / 0.078)","--lab-fill-brand-tertiary":"oklch(60.27647% 0.217712 257.424 / 0.039)","--lab-fill-brand-quaternary":"oklch(60.27647% 0.217712 257.424 / 0.02)","--lab-border-brand-strong":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-border-brand-base":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-brand-soft":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-label-danger-primary":"oklch(20.16318% 0.082740 29.234)","--lab-label-danger-secondary":"oklch(59.62536% 0.244212 28.655)","--lab-label-danger-tertiary":"oklch(69.12968% 0.198650 28.574)","--lab-label-danger-quaternary":"oklch(82.55135% 0.097676 29.389)","--lab-fill-danger-primary":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-fill-danger-secondary":"oklch(65.42146% 0.232135 28.659 / 0.078)","--lab-fill-danger-tertiary":"oklch(65.42146% 0.232135 28.659 / 0.039)","--lab-fill-danger-quaternary":"oklch(65.42146% 0.232135 28.659 / 0.02)","--lab-border-danger-strong":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-danger-base":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-danger-soft":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-warning-primary":"oklch(19.32631% 0.042728 66.944)","--lab-label-warning-secondary":"oklch(57.27465% 0.125021 68.799)","--lab-label-warning-tertiary":"oklch(67.73500% 0.148484 68.161)","--lab-label-warning-quaternary":"oklch(82.04043% 0.139600 68.374)","--lab-fill-warning-primary":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-fill-warning-secondary":"oklch(78.56796% 0.171718 68.607 / 0.078)","--lab-fill-warning-tertiary":"oklch(78.56796% 0.171718 68.607 / 0.039)","--lab-fill-warning-quaternary":"oklch(78.56796% 0.171718 68.607 / 0.02)","--lab-border-warning-strong":"oklch(67.73500% 0.148484 68.161 / 1)","--lab-border-warning-base":"oklch(78.56796% 0.171718 68.607 / 0.2)","--lab-border-warning-soft":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-label-success-primary":"oklch(18.59864% 0.054816 147.311)","--lab-label-success-secondary":"oklch(54.91588% 0.161394 147.418)","--lab-label-success-tertiary":"oklch(64.88263% 0.190555 147.443)","--lab-label-success-quaternary":"oklch(79.02491% 0.232159 147.432)","--lab-fill-success-primary":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-fill-success-secondary":"oklch(73.03242% 0.194381 147.444 / 0.078)","--lab-fill-success-tertiary":"oklch(73.03242% 0.194381 147.444 / 0.039)","--lab-fill-success-quaternary":"oklch(73.03242% 0.194381 147.444 / 0.02)","--lab-border-success-strong":"oklch(64.96409% 0.172888 147.450 / 1)","--lab-border-success-base":"oklch(73.03242% 0.194381 147.444 / 0.2)","--lab-border-success-soft":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-label-info-primary":"oklch(19.54489% 0.077696 259.509)","--lab-label-info-secondary":"oklch(57.68997% 0.232691 259.838)","--lab-label-info-tertiary":"oklch(67.26428% 0.173598 259.980)","--lab-label-info-quaternary":"oklch(81.49851% 0.092914 259.576)","--lab-fill-info-primary":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fill-info-secondary":"oklch(64.04613% 0.193058 259.892 / 0.078)","--lab-fill-info-tertiary":"oklch(64.04613% 0.193058 259.892 / 0.039)","--lab-fill-info-quaternary":"oklch(64.04613% 0.193058 259.892 / 0.02)","--lab-border-info-strong":"oklch(64.04613% 0.193058 259.892 / 1)","--lab-border-info-base":"oklch(64.04613% 0.193058 259.892 / 0.2)","--lab-border-info-soft":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fx-focus-ring-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fx-focus-ring-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-fx-focus-ring-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-fx-focus-ring-neutral":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-fx-glow-brand":"oklch(60.27647% 0.217712 257.424)","--lab-fx-glow-brand-core":"oklch(78.76611% 0.108476 257.256)","--lab-fx-glow-brand-alpha":"0.5","--lab-fx-glow-danger":"oklch(65.42146% 0.232135 28.659)","--lab-fx-glow-danger-core":"oklch(81.77103% 0.102764 29.025)","--lab-fx-glow-danger-alpha":"0.5","--lab-fx-glow-warning":"oklch(78.56796% 0.171718 68.607)","--lab-fx-glow-warning-core":"oklch(88.88181% 0.082838 68.777)","--lab-fx-glow-warning-alpha":"0.5","--lab-fx-glow-neutral":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-core":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-alpha":"0.5","--lab-fx-glow-inverted":"oklch(75.99692% 0.012723 286.093 / 0.522)","--lab-fx-skeleton-highlight":"oklch(57.53363% 0.012136 286.012 / 0.039)","--lab-fill-neutral":"oklch(57.53363% 0.012136 286.012 / 1)","--lab-fill-accent-tinted":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-danger-tinted":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-label-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-accent":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-danger":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-focus":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fill-neutral-tinted":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-border-neutral":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fx-skeleton-base":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-icon":"oklch(66.97448% 0.014606 285.999)"}},{"theme":"light","bg":"#000000","vars":{"--lab-bg-tone-2":"oklch(7.11623% 0.013707 282.350)","--lab-bg-tone-3":"oklch(7.11623% 0.013707 282.350)","--lab-label-primary":"oklch(98.65156% 0.006606 286.278)","--lab-label-secondary":"oklch(80.14049% 0.013952 286.081)","--lab-label-tertiary":"oklch(69.23260% 0.013029 286.055)","--lab-label-quaternary":"oklch(55.16643% 0.013808 285.938)","--lab-border-strong":"oklch(98.65156% 0.006606 286.278)","--lab-border-base":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-border-soft":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-primary":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-fill-secondary":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fill-tertiary":"oklch(57.53363% 0.012136 286.012 / 0.122)","--lab-fill-quaternary":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fx-shadow-minor":"oklch(17.39406% 0.004094 285.967 / 0.012)","--lab-fx-shadow-ambient":"oklch(17.39406% 0.004094 285.967 / 0.02)","--lab-fx-shadow-penumbra":"oklch(17.39406% 0.004094 285.967 / 0.039)","--lab-fx-shadow-major":"oklch(17.39406% 0.004094 285.967 / 0.122)","--lab-label-brand-primary":"oklch(98.69528% 0.006207 255.474)","--lab-label-brand-secondary":"oklch(80.06451% 0.101154 257.822)","--lab-label-brand-tertiary":"oklch(69.35433% 0.162198 257.610)","--lab-label-brand-quaternary":"oklch(55.96293% 0.201872 257.393)","--lab-fill-brand-primary":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-brand-secondary":"oklch(60.27647% 0.217712 257.424 / 0.078)","--lab-fill-brand-tertiary":"oklch(60.27647% 0.217712 257.424 / 0.039)","--lab-fill-brand-quaternary":"oklch(60.27647% 0.217712 257.424 / 0.02)","--lab-border-brand-strong":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-border-brand-base":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-brand-soft":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-label-danger-primary":"oklch(98.64532% 0.006567 28.833)","--lab-label-danger-secondary":"oklch(81.38408% 0.105319 28.863)","--lab-label-danger-tertiary":"oklch(71.19739% 0.181099 28.350)","--lab-label-danger-quaternary":"oklch(58.10538% 0.238004 28.677)","--lab-fill-danger-primary":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-fill-danger-secondary":"oklch(65.42146% 0.232135 28.659 / 0.078)","--lab-fill-danger-tertiary":"oklch(65.42146% 0.232135 28.659 / 0.039)","--lab-fill-danger-quaternary":"oklch(65.42146% 0.232135 28.659 / 0.02)","--lab-border-danger-strong":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-danger-base":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-danger-soft":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-warning-primary":"oklch(98.72985% 0.009577 72.664)","--lab-label-warning-secondary":"oklch(80.98123% 0.149940 68.750)","--lab-label-warning-tertiary":"oklch(70.11346% 0.152989 68.857)","--lab-label-warning-quaternary":"oklch(55.86543% 0.122417 68.219)","--lab-fill-warning-primary":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-fill-warning-secondary":"oklch(78.56796% 0.171718 68.607 / 0.078)","--lab-fill-warning-tertiary":"oklch(78.56796% 0.171718 68.607 / 0.039)","--lab-fill-warning-quaternary":"oklch(78.56796% 0.171718 68.607 / 0.02)","--lab-border-warning-strong":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-border-warning-base":"oklch(78.56796% 0.171718 68.607 / 0.2)","--lab-border-warning-soft":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-label-success-primary":"oklch(98.31403% 0.027558 147.033)","--lab-label-success-secondary":"oklch(77.69544% 0.228159 147.448)","--lab-label-success-tertiary":"oklch(67.10085% 0.197388 147.383)","--lab-label-success-quaternary":"oklch(53.45797% 0.157009 147.442)","--lab-fill-success-primary":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-fill-success-secondary":"oklch(73.03242% 0.194381 147.444 / 0.078)","--lab-fill-success-tertiary":"oklch(73.03242% 0.194381 147.444 / 0.039)","--lab-fill-success-quaternary":"oklch(73.03242% 0.194381 147.444 / 0.02)","--lab-border-success-strong":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-border-success-base":"oklch(73.03242% 0.194381 147.444 / 0.2)","--lab-border-success-soft":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-label-info-primary":"oklch(98.69528% 0.006207 255.474)","--lab-label-info-secondary":"oklch(80.30515% 0.099317 259.806)","--lab-label-info-tertiary":"oklch(69.49027% 0.160387 260.058)","--lab-label-info-quaternary":"oklch(56.23879% 0.227606 259.853)","--lab-fill-info-primary":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fill-info-secondary":"oklch(64.04613% 0.193058 259.892 / 0.078)","--lab-fill-info-tertiary":"oklch(64.04613% 0.193058 259.892 / 0.039)","--lab-fill-info-quaternary":"oklch(64.04613% 0.193058 259.892 / 0.02)","--lab-border-info-strong":"oklch(64.04613% 0.193058 259.892 / 1)","--lab-border-info-base":"oklch(64.04613% 0.193058 259.892 / 0.2)","--lab-border-info-soft":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fx-focus-ring-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fx-focus-ring-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-fx-focus-ring-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-fx-focus-ring-neutral":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-fx-glow-brand":"oklch(60.27647% 0.217712 257.424)","--lab-fx-glow-brand-core":"oklch(78.76611% 0.108476 257.256)","--lab-fx-glow-brand-alpha":"0.013010286081645773","--lab-fx-glow-danger":"oklch(65.42146% 0.232135 28.659)","--lab-fx-glow-danger-core":"oklch(81.77103% 0.102764 29.025)","--lab-fx-glow-danger-alpha":"0.012071078431372548","--lab-fx-glow-warning":"oklch(78.56796% 0.171718 68.607)","--lab-fx-glow-warning-core":"oklch(88.88181% 0.082838 68.777)","--lab-fx-glow-warning-alpha":"0.009560345877481427","--lab-fx-glow-neutral":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-core":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-alpha":"0.00784313725490196","--lab-fx-glow-inverted":"oklch(75.99692% 0.012723 286.093 / 0.522)","--lab-fx-skeleton-highlight":"oklch(57.53363% 0.012136 286.012 / 0.039)","--lab-fill-neutral":"oklch(57.53363% 0.012136 286.012 / 1)","--lab-fill-accent-tinted":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-danger-tinted":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-label-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-accent":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-danger":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-focus":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fill-neutral-tinted":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-border-neutral":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fx-skeleton-base":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-icon":"oklch(69.23260% 0.013029 286.055)"}},{"theme":"light","bg":"#808080","vars":{"--lab-bg-tone-2":"oklch(58.21536% 0.012099 286.018)","--lab-bg-tone-3":"oklch(58.21536% 0.012099 286.018)","--lab-label-primary":"oklch(13.77177% 0.013406 284.503)","--lab-label-secondary":"oklch(20.89710% 0.013787 291.711)","--lab-label-tertiary":"oklch(33.70211% 0.013945 291.371)","--lab-label-quaternary":"oklch(49.18380% 0.012632 285.926)","--lab-border-strong":"oklch(13.77177% 0.013406 284.503)","--lab-border-base":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-border-soft":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-primary":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-fill-secondary":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fill-tertiary":"oklch(57.53363% 0.012136 286.012 / 0.122)","--lab-fill-quaternary":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fx-shadow-minor":"oklch(17.39406% 0.004094 285.967 / 0.012)","--lab-fx-shadow-ambient":"oklch(17.39406% 0.004094 285.967 / 0.02)","--lab-fx-shadow-penumbra":"oklch(17.39406% 0.004094 285.967 / 0.039)","--lab-fx-shadow-major":"oklch(17.39406% 0.004094 285.967 / 0.122)","--lab-label-brand-primary":"oklch(13.49058% 0.050205 258.119)","--lab-label-brand-secondary":"oklch(20.89968% 0.076454 257.725)","--lab-label-brand-tertiary":"oklch(34.17043% 0.122830 257.309)","--lab-label-brand-quaternary":"oklch(49.70427% 0.178878 257.337)","--lab-fill-brand-primary":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-brand-secondary":"oklch(60.27647% 0.217712 257.424 / 0.078)","--lab-fill-brand-tertiary":"oklch(60.27647% 0.217712 257.424 / 0.039)","--lab-fill-brand-quaternary":"oklch(60.27647% 0.217712 257.424 / 0.02)","--lab-border-brand-base":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-brand-soft":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-label-danger-primary":"oklch(14.22004% 0.058352 29.234)","--lab-label-danger-secondary":"oklch(21.88171% 0.089792 29.234)","--lab-label-danger-tertiary":"oklch(35.54757% 0.145582 28.627)","--lab-label-danger-quaternary":"oklch(51.73054% 0.211869 28.643)","--lab-fill-danger-primary":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-fill-danger-secondary":"oklch(65.42146% 0.232135 28.659 / 0.078)","--lab-fill-danger-tertiary":"oklch(65.42146% 0.232135 28.659 / 0.039)","--lab-fill-danger-quaternary":"oklch(65.42146% 0.232135 28.659 / 0.02)","--lab-border-danger-base":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-danger-soft":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-warning-primary":"oklch(13.56514% 0.029751 68.088)","--lab-label-warning-secondary":"oklch(20.90753% 0.046395 66.438)","--lab-label-warning-tertiary":"oklch(34.00266% 0.074904 67.452)","--lab-label-warning-quaternary":"oklch(49.79045% 0.108745 68.715)","--lab-fill-warning-primary":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-fill-warning-secondary":"oklch(78.56796% 0.171718 68.607 / 0.078)","--lab-fill-warning-tertiary":"oklch(78.56796% 0.171718 68.607 / 0.039)","--lab-fill-warning-quaternary":"oklch(78.56796% 0.171718 68.607 / 0.02)","--lab-border-warning-strong":"oklch(91.20808% 0.064865 69.118 / 1)","--lab-border-warning-base":"oklch(78.56796% 0.171718 68.607 / 0.2)","--lab-border-warning-soft":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-label-success-primary":"oklch(13.01439% 0.039836 145.937)","--lab-label-success-secondary":"oklch(20.10741% 0.059351 147.255)","--lab-label-success-tertiary":"oklch(32.71649% 0.095649 147.616)","--lab-label-success-quaternary":"oklch(47.52181% 0.139141 147.559)","--lab-fill-success-primary":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-fill-success-secondary":"oklch(73.03242% 0.194381 147.444 / 0.078)","--lab-fill-success-tertiary":"oklch(73.03242% 0.194381 147.444 / 0.039)","--lab-fill-success-quaternary":"oklch(73.03242% 0.194381 147.444 / 0.02)","--lab-border-success-strong":"oklch(88.67895% 0.198079 147.416 / 1)","--lab-border-success-base":"oklch(73.03242% 0.194381 147.444 / 0.2)","--lab-border-success-soft":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-label-info-primary":"oklch(13.83602% 0.055501 259.684)","--lab-label-info-secondary":"oklch(21.26627% 0.087439 260.146)","--lab-label-info-tertiary":"oklch(34.39884% 0.140838 260.069)","--lab-label-info-quaternary":"oklch(50.11242% 0.202893 259.861)","--lab-fill-info-primary":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fill-info-secondary":"oklch(64.04613% 0.193058 259.892 / 0.078)","--lab-fill-info-tertiary":"oklch(64.04613% 0.193058 259.892 / 0.039)","--lab-fill-info-quaternary":"oklch(64.04613% 0.193058 259.892 / 0.02)","--lab-border-info-base":"oklch(64.04613% 0.193058 259.892 / 0.2)","--lab-border-info-soft":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fx-focus-ring-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fx-focus-ring-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-fx-focus-ring-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-fx-focus-ring-neutral":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-fx-glow-brand":"oklch(60.27647% 0.217712 257.424)","--lab-fx-glow-brand-core":"oklch(78.76611% 0.108476 257.256)","--lab-fx-glow-brand-alpha":"0.11057506131405687","--lab-fx-glow-danger":"oklch(65.42146% 0.232135 28.659)","--lab-fx-glow-danger-core":"oklch(81.77103% 0.102764 29.025)","--lab-fx-glow-danger-alpha":"0.11023622047244083","--lab-fx-glow-warning":"oklch(78.56796% 0.171718 68.607)","--lab-fx-glow-warning-core":"oklch(88.88181% 0.082838 68.777)","--lab-fx-glow-warning-alpha":"0.07169755954418727","--lab-fx-glow-neutral":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-core":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-alpha":"0.04724409448818886","--lab-fx-glow-inverted":"oklch(75.99692% 0.012723 286.093 / 0.522)","--lab-fx-skeleton-highlight":"oklch(57.53363% 0.012136 286.012 / 0.039)","--lab-fill-neutral":"oklch(57.53363% 0.012136 286.012 / 1)","--lab-fill-accent-tinted":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-danger-tinted":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-label-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-accent":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-danger":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-focus":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fill-neutral-tinted":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-border-neutral":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fx-skeleton-base":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-icon":"oklch(33.70211% 0.013945 291.371)","--lab-border-brand-strong":"oklch(34.17043% 0.122830 257.309 / 1)","--lab-border-danger-strong":"oklch(35.54757% 0.145582 28.627 / 1)","--lab-border-info-strong":"oklch(34.39884% 0.140838 260.069 / 1)"}},{"theme":"light","bg":"#3A3A3C","vars":{"--lab-bg-tone-2":"oklch(36.65084% 0.011912 285.797)","--lab-bg-tone-3":"oklch(36.65084% 0.011912 285.797)","--lab-label-primary":"oklch(98.92134% 0.005280 286.303)","--lab-label-secondary":"oklch(82.53513% 0.012530 281.039)","--lab-label-tertiary":"oklch(72.82755% 0.014297 286.039)","--lab-label-quaternary":"oklch(61.25907% 0.011943 286.042)","--lab-border-strong":"oklch(98.92134% 0.005280 286.303)","--lab-border-base":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-border-soft":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-primary":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-fill-secondary":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fill-tertiary":"oklch(57.53363% 0.012136 286.012 / 0.122)","--lab-fill-quaternary":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fx-shadow-minor":"oklch(17.39406% 0.004094 285.967 / 0.012)","--lab-fx-shadow-ambient":"oklch(17.39406% 0.004094 285.967 / 0.02)","--lab-fx-shadow-penumbra":"oklch(17.39406% 0.004094 285.967 / 0.039)","--lab-fx-shadow-major":"oklch(17.39406% 0.004094 285.967 / 0.122)","--lab-label-brand-primary":"oklch(98.77036% 0.005722 264.533)","--lab-label-brand-secondary":"oklch(82.44675% 0.088464 257.008)","--lab-label-brand-tertiary":"oklch(73.08383% 0.140495 257.442)","--lab-label-brand-quaternary":"oklch(61.77245% 0.208353 257.413)","--lab-fill-brand-primary":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-brand-secondary":"oklch(60.27647% 0.217712 257.424 / 0.078)","--lab-fill-brand-tertiary":"oklch(60.27647% 0.217712 257.424 / 0.039)","--lab-fill-brand-quaternary":"oklch(60.27647% 0.217712 257.424 / 0.02)","--lab-border-brand-strong":"oklch(61.75781% 0.208491 257.338 / 1)","--lab-border-brand-base":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-brand-soft":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-label-danger-primary":"oklch(98.86605% 0.005516 31.054)","--lab-label-danger-secondary":"oklch(83.38591% 0.092088 28.223)","--lab-label-danger-tertiary":"oklch(74.63167% 0.153784 28.842)","--lab-label-danger-quaternary":"oklch(64.10554% 0.244701 28.706)","--lab-fill-danger-primary":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-fill-danger-secondary":"oklch(65.42146% 0.232135 28.659 / 0.078)","--lab-fill-danger-tertiary":"oklch(65.42146% 0.232135 28.659 / 0.039)","--lab-fill-danger-quaternary":"oklch(65.42146% 0.232135 28.659 / 0.02)","--lab-border-danger-strong":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-danger-base":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-danger-soft":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-warning-primary":"oklch(98.75683% 0.008520 67.727)","--lab-label-warning-secondary":"oklch(83.08426% 0.131366 68.787)","--lab-label-warning-tertiary":"oklch(73.86147% 0.161425 68.614)","--lab-label-warning-quaternary":"oklch(61.96370% 0.135496 68.532)","--lab-fill-warning-primary":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-fill-warning-secondary":"oklch(78.56796% 0.171718 68.607 / 0.078)","--lab-fill-warning-tertiary":"oklch(78.56796% 0.171718 68.607 / 0.039)","--lab-fill-warning-quaternary":"oklch(78.56796% 0.171718 68.607 / 0.02)","--lab-border-warning-strong":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-border-warning-base":"oklch(78.56796% 0.171718 68.607 / 0.2)","--lab-border-warning-soft":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-label-success-primary":"oklch(98.51065% 0.024195 147.300)","--lab-label-success-secondary":"oklch(80.08887% 0.235054 147.469)","--lab-label-success-tertiary":"oklch(70.67977% 0.207519 147.455)","--lab-label-success-quaternary":"oklch(59.24524% 0.173588 147.533)","--lab-fill-success-primary":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-fill-success-secondary":"oklch(73.03242% 0.194381 147.444 / 0.078)","--lab-fill-success-tertiary":"oklch(73.03242% 0.194381 147.444 / 0.039)","--lab-fill-success-quaternary":"oklch(73.03242% 0.194381 147.444 / 0.02)","--lab-border-success-strong":"oklch(73.03242% 0.194381 147.444 / 1)","--lab-border-success-base":"oklch(73.03242% 0.194381 147.444 / 0.2)","--lab-border-success-soft":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-label-info-primary":"oklch(98.77036% 0.005722 264.533)","--lab-label-info-secondary":"oklch(82.42821% 0.087905 259.666)","--lab-label-info-tertiary":"oklch(73.17409% 0.139071 259.898)","--lab-label-info-quaternary":"oklch(61.97090% 0.205878 259.740)","--lab-fill-info-primary":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fill-info-secondary":"oklch(64.04613% 0.193058 259.892 / 0.078)","--lab-fill-info-tertiary":"oklch(64.04613% 0.193058 259.892 / 0.039)","--lab-fill-info-quaternary":"oklch(64.04613% 0.193058 259.892 / 0.02)","--lab-border-info-strong":"oklch(64.04613% 0.193058 259.892 / 1)","--lab-border-info-base":"oklch(64.04613% 0.193058 259.892 / 0.2)","--lab-border-info-soft":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fx-focus-ring-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fx-focus-ring-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-fx-focus-ring-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-fx-focus-ring-neutral":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-fx-glow-brand":"oklch(60.27647% 0.217712 257.424)","--lab-fx-glow-brand-core":"oklch(78.76611% 0.108476 257.256)","--lab-fx-glow-brand-alpha":"0.06909778454881221","--lab-fx-glow-danger":"oklch(65.42146% 0.232135 28.659)","--lab-fx-glow-danger-core":"oklch(81.77103% 0.102764 29.025)","--lab-fx-glow-danger-alpha":"0.060913705583756306","--lab-fx-glow-warning":"oklch(78.56796% 0.171718 68.607)","--lab-fx-glow-warning-core":"oklch(88.88181% 0.082838 68.777)","--lab-fx-glow-warning-alpha":"0.040609137055837526","--lab-fx-glow-neutral":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-core":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-alpha":"0.028061954965508236","--lab-fx-glow-inverted":"oklch(75.99692% 0.012723 286.093 / 0.522)","--lab-fx-skeleton-highlight":"oklch(57.53363% 0.012136 286.012 / 0.039)","--lab-fill-neutral":"oklch(57.53363% 0.012136 286.012 / 1)","--lab-fill-accent-tinted":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-danger-tinted":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-label-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-accent":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-danger":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-focus":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fill-neutral-tinted":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-border-neutral":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fx-skeleton-base":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-icon":"oklch(72.82755% 0.014297 286.039)"}},{"theme":"light","bg":"#007AFF","vars":{"--lab-bg-tone-2":"oklch(57.87475% 0.012117 286.015)","--lab-bg-tone-3":"oklch(57.87475% 0.012117 286.015)","--lab-label-primary":"oklch(13.41536% 0.016099 291.497)","--lab-label-secondary":"oklch(20.01464% 0.013939 291.638)","--lab-label-tertiary":"oklch(33.31157% 0.013986 291.359)","--lab-label-quaternary":"oklch(48.82850% 0.012656 285.922)","--lab-border-strong":"oklch(13.41536% 0.016099 291.497)","--lab-border-base":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-border-soft":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-primary":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-fill-secondary":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fill-tertiary":"oklch(57.53363% 0.012136 286.012 / 0.122)","--lab-fill-quaternary":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fx-shadow-minor":"oklch(17.39406% 0.004094 285.967 / 0.012)","--lab-fx-shadow-ambient":"oklch(17.39406% 0.004094 285.967 / 0.02)","--lab-fx-shadow-penumbra":"oklch(17.39406% 0.004094 285.967 / 0.039)","--lab-fx-shadow-major":"oklch(17.39406% 0.004094 285.967 / 0.122)","--lab-label-brand-primary":"oklch(13.49058% 0.050205 258.119)","--lab-label-brand-secondary":"oklch(20.23588% 0.071518 256.892)","--lab-label-brand-tertiary":"oklch(33.52307% 0.122654 257.729)","--lab-label-brand-quaternary":"oklch(49.33014% 0.177503 257.333)","--lab-fill-brand-primary":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-brand-secondary":"oklch(60.27647% 0.217712 257.424 / 0.078)","--lab-fill-brand-tertiary":"oklch(60.27647% 0.217712 257.424 / 0.039)","--lab-fill-brand-quaternary":"oklch(60.27647% 0.217712 257.424 / 0.02)","--lab-border-brand-base":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-brand-soft":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-label-danger-primary":"oklch(14.22004% 0.058352 29.234)","--lab-label-danger-secondary":"oklch(20.90400% 0.085780 29.234)","--lab-label-danger-tertiary":"oklch(34.90075% 0.142917 28.593)","--lab-label-danger-quaternary":"oklch(51.33845% 0.210254 28.629)","--lab-fill-danger-primary":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-fill-danger-secondary":"oklch(65.42146% 0.232135 28.659 / 0.078)","--lab-fill-danger-tertiary":"oklch(65.42146% 0.232135 28.659 / 0.039)","--lab-fill-danger-quaternary":"oklch(65.42146% 0.232135 28.659 / 0.02)","--lab-border-danger-base":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-danger-soft":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-warning-primary":"oklch(13.56514% 0.029751 68.088)","--lab-label-warning-secondary":"oklch(20.32691% 0.044866 67.175)","--lab-label-warning-tertiary":"oklch(33.70499% 0.073308 69.356)","--lab-label-warning-quaternary":"oklch(49.32505% 0.107532 68.994)","--lab-fill-warning-primary":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-fill-warning-secondary":"oklch(78.56796% 0.171718 68.607 / 0.078)","--lab-fill-warning-tertiary":"oklch(78.56796% 0.171718 68.607 / 0.039)","--lab-fill-warning-quaternary":"oklch(78.56796% 0.171718 68.607 / 0.02)","--lab-border-warning-strong":"oklch(90.50991% 0.070001 68.839 / 1)","--lab-border-warning-base":"oklch(78.56796% 0.171718 68.607 / 0.2)","--lab-border-warning-soft":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-label-success-primary":"oklch(13.01439% 0.039836 145.937)","--lab-label-success-secondary":"oklch(19.36939% 0.056254 147.869)","--lab-label-success-tertiary":"oklch(32.05045% 0.093836 147.561)","--lab-label-success-quaternary":"oklch(47.21271% 0.138736 147.423)","--lab-fill-success-primary":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-fill-success-secondary":"oklch(73.03242% 0.194381 147.444 / 0.078)","--lab-fill-success-tertiary":"oklch(73.03242% 0.194381 147.444 / 0.039)","--lab-fill-success-quaternary":"oklch(73.03242% 0.194381 147.444 / 0.02)","--lab-border-success-strong":"oklch(87.96697% 0.214930 147.350 / 1)","--lab-border-success-base":"oklch(73.03242% 0.194381 147.444 / 0.2)","--lab-border-success-soft":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-label-info-primary":"oklch(13.83602% 0.055501 259.684)","--lab-label-info-secondary":"oklch(20.33878% 0.084139 260.257)","--lab-label-info-tertiary":"oklch(33.98919% 0.139378 260.097)","--lab-label-info-quaternary":"oklch(49.73960% 0.201561 259.877)","--lab-fill-info-primary":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fill-info-secondary":"oklch(64.04613% 0.193058 259.892 / 0.078)","--lab-fill-info-tertiary":"oklch(64.04613% 0.193058 259.892 / 0.039)","--lab-fill-info-quaternary":"oklch(64.04613% 0.193058 259.892 / 0.02)","--lab-border-info-base":"oklch(64.04613% 0.193058 259.892 / 0.2)","--lab-border-info-soft":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fx-focus-ring-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fx-focus-ring-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-fx-focus-ring-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-fx-focus-ring-neutral":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-fx-glow-brand":"oklch(60.27647% 0.217712 257.424)","--lab-fx-glow-brand-core":"oklch(78.76611% 0.108476 257.256)","--lab-fx-glow-brand-alpha":"0.1414396647356093","--lab-fx-glow-danger":"oklch(65.42146% 0.232135 28.659)","--lab-fx-glow-danger-core":"oklch(81.77103% 0.102764 29.025)","--lab-fx-glow-danger-alpha":"0.1805418331471749","--lab-fx-glow-warning":"oklch(78.56796% 0.171718 68.607)","--lab-fx-glow-warning-core":"oklch(88.88181% 0.082838 68.777)","--lab-fx-glow-warning-alpha":"0.09073588245366788","--lab-fx-glow-neutral":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-core":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-alpha":"0.06430782839451565","--lab-fx-glow-inverted":"oklch(75.99692% 0.012723 286.093 / 0.522)","--lab-fx-skeleton-highlight":"oklch(57.53363% 0.012136 286.012 / 0.039)","--lab-fill-neutral":"oklch(57.53363% 0.012136 286.012 / 1)","--lab-fill-accent-tinted":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-danger-tinted":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-label-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-accent":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-danger":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-focus":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fill-neutral-tinted":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-border-neutral":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fx-skeleton-base":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-icon":"oklch(33.31157% 0.013986 291.359)","--lab-border-brand-strong":"oklch(33.52307% 0.122654 257.729 / 1)","--lab-border-danger-strong":"oklch(34.90075% 0.142917 28.593 / 1)","--lab-border-info-strong":"oklch(33.98919% 0.139378 260.097 / 1)"}},{"theme":"light","bg":"#FF3B30","vars":{"--lab-bg-tone-2":"oklch(63.60048% 0.011829 286.059)","--lab-bg-tone-3":"oklch(63.60048% 0.011829 286.059)","--lab-label-primary":"oklch(13.83495% 0.015576 284.180)","--lab-label-secondary":"oklch(25.12484% 0.015017 285.269)","--lab-label-tertiary":"oklch(36.40556% 0.013673 291.446)","--lab-label-quaternary":"oklch(51.30243% 0.012497 285.951)","--lab-border-strong":"oklch(13.83495% 0.015576 284.180)","--lab-border-base":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-border-soft":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fill-primary":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-fill-secondary":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fill-tertiary":"oklch(57.53363% 0.012136 286.012 / 0.122)","--lab-fill-quaternary":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-fx-shadow-minor":"oklch(17.39406% 0.004094 285.967 / 0.012)","--lab-fx-shadow-ambient":"oklch(17.39406% 0.004094 285.967 / 0.02)","--lab-fx-shadow-penumbra":"oklch(17.39406% 0.004094 285.967 / 0.039)","--lab-fx-shadow-major":"oklch(17.39406% 0.004094 285.967 / 0.122)","--lab-label-brand-primary":"oklch(14.05977% 0.051776 257.879)","--lab-label-brand-secondary":"oklch(25.33060% 0.091490 257.423)","--lab-label-brand-tertiary":"oklch(36.69778% 0.133197 257.540)","--lab-label-brand-quaternary":"oklch(51.93456% 0.187074 257.359)","--lab-fill-brand-primary":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-brand-secondary":"oklch(60.27647% 0.217712 257.424 / 0.078)","--lab-fill-brand-tertiary":"oklch(60.27647% 0.217712 257.424 / 0.039)","--lab-fill-brand-quaternary":"oklch(60.27647% 0.217712 257.424 / 0.02)","--lab-border-brand-base":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-brand-soft":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-label-danger-primary":"oklch(14.49010% 0.059461 29.234)","--lab-label-danger-secondary":"oklch(26.17120% 0.107130 28.474)","--lab-label-danger-tertiary":"oklch(38.31782% 0.156989 28.748)","--lab-label-danger-quaternary":"oklch(53.87750% 0.220648 28.624)","--lab-fill-danger-primary":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-fill-danger-secondary":"oklch(65.42146% 0.232135 28.659 / 0.078)","--lab-fill-danger-tertiary":"oklch(65.42146% 0.232135 28.659 / 0.039)","--lab-fill-danger-quaternary":"oklch(65.42146% 0.232135 28.659 / 0.02)","--lab-border-danger-base":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-danger-soft":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-warning-primary":"oklch(14.09840% 0.030367 70.955)","--lab-label-warning-secondary":"oklch(25.16306% 0.055216 68.014)","--lab-label-warning-tertiary":"oklch(36.95716% 0.080989 68.209)","--lab-label-warning-quaternary":"oklch(51.83499% 0.113346 68.533)","--lab-fill-warning-primary":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-fill-warning-secondary":"oklch(78.56796% 0.171718 68.607 / 0.078)","--lab-fill-warning-tertiary":"oklch(78.56796% 0.171718 68.607 / 0.039)","--lab-fill-warning-quaternary":"oklch(78.56796% 0.171718 68.607 / 0.02)","--lab-border-warning-strong":"oklch(94.55166% 0.039779 69.746 / 1)","--lab-border-warning-base":"oklch(78.56796% 0.171718 68.607 / 0.2)","--lab-border-warning-soft":"oklch(78.56796% 0.171718 68.607 / 0.122)","--lab-label-success-primary":"oklch(13.47470% 0.038038 148.984)","--lab-label-success-secondary":"oklch(24.13011% 0.070822 147.468)","--lab-label-success-tertiary":"oklch(35.33425% 0.103721 147.463)","--lab-label-success-quaternary":"oklch(49.62199% 0.145290 147.559)","--lab-fill-success-primary":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-fill-success-secondary":"oklch(73.03242% 0.194381 147.444 / 0.078)","--lab-fill-success-tertiary":"oklch(73.03242% 0.194381 147.444 / 0.039)","--lab-fill-success-quaternary":"oklch(73.03242% 0.194381 147.444 / 0.02)","--lab-border-success-strong":"oklch(93.08890% 0.111383 147.112 / 1)","--lab-border-success-base":"oklch(73.03242% 0.194381 147.444 / 0.2)","--lab-border-success-soft":"oklch(73.03242% 0.194381 147.444 / 0.122)","--lab-label-info-primary":"oklch(14.07469% 0.058967 260.480)","--lab-label-info-secondary":"oklch(25.53067% 0.104920 260.137)","--lab-label-info-tertiary":"oklch(37.14168% 0.149591 259.762)","--lab-label-info-quaternary":"oklch(52.13660% 0.212021 259.943)","--lab-fill-info-primary":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fill-info-secondary":"oklch(64.04613% 0.193058 259.892 / 0.078)","--lab-fill-info-tertiary":"oklch(64.04613% 0.193058 259.892 / 0.039)","--lab-fill-info-quaternary":"oklch(64.04613% 0.193058 259.892 / 0.02)","--lab-border-info-base":"oklch(64.04613% 0.193058 259.892 / 0.2)","--lab-border-info-soft":"oklch(64.04613% 0.193058 259.892 / 0.122)","--lab-fx-focus-ring-brand":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fx-focus-ring-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-fx-focus-ring-warning":"oklch(78.56796% 0.171718 68.607 / 1)","--lab-fx-focus-ring-neutral":"oklch(17.39406% 0.004094 285.967 / 1)","--lab-fx-glow-brand":"oklch(60.27647% 0.217712 257.424)","--lab-fx-glow-brand-core":"oklch(78.76611% 0.108476 257.256)","--lab-fx-glow-brand-alpha":"0.1835748792270531","--lab-fx-glow-danger":"oklch(65.42146% 0.232135 28.659)","--lab-fx-glow-danger-core":"oklch(81.77103% 0.102764 29.025)","--lab-fx-glow-danger-alpha":"0.4395613333149519","--lab-fx-glow-warning":"oklch(78.56796% 0.171718 68.607)","--lab-fx-glow-warning-core":"oklch(88.88181% 0.082838 68.777)","--lab-fx-glow-warning-alpha":"0.1777791862086449","--lab-fx-glow-neutral":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-core":"oklch(100.00000% 0.000000 89.876)","--lab-fx-glow-neutral-alpha":"0.1016772651089421","--lab-fx-glow-inverted":"oklch(75.99692% 0.012723 286.093 / 0.522)","--lab-fx-skeleton-highlight":"oklch(57.53363% 0.012136 286.012 / 0.039)","--lab-fill-neutral":"oklch(57.53363% 0.012136 286.012 / 1)","--lab-fill-accent-tinted":"oklch(60.27647% 0.217712 257.424 / 0.122)","--lab-fill-danger-tinted":"oklch(65.42146% 0.232135 28.659 / 0.122)","--lab-label-accent":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-label-danger":"oklch(65.42146% 0.232135 28.659 / 1)","--lab-border-accent":"oklch(60.27647% 0.217712 257.424 / 0.2)","--lab-border-danger":"oklch(65.42146% 0.232135 28.659 / 0.2)","--lab-border-focus":"oklch(60.27647% 0.217712 257.424 / 1)","--lab-fill-neutral-tinted":"oklch(57.53363% 0.012136 286.012 / 0.2)","--lab-border-neutral":"oklch(57.53363% 0.012136 286.012 / 0.161)","--lab-fx-skeleton-base":"oklch(57.53363% 0.012136 286.012 / 0.078)","--lab-icon":"oklch(36.40556% 0.013673 291.446)","--lab-border-brand-strong":"oklch(36.69778% 0.133197 257.540 / 1)","--lab-border-danger-strong":"oklch(38.31782% 0.156989 28.748 / 1)","--lab-border-info-strong":"oklch(37.14168% 0.149591 259.762 / 1)"}}]} diff --git a/scripts/check-wasm-size-budget.mjs b/scripts/check-wasm-size-budget.mjs index 8ceb346b..33b83e7c 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 = - "c51c0bd3d62bf3b1d57ea5f9b65da48f0b50621472071eaa37ff8ba145ab1bc7"; + "224d92604e5ee861da0c09e219da91f0807c4d5cb3c8733f23dff72066ecdb08"; const SCHEMA_VERSION = 1; const CANONICAL_ARTIFACT = "packages/colors/pkg/labcolors_bg.wasm"; diff --git a/scripts/verify_point_support_surplus.py b/scripts/verify_point_support_surplus.py index 47db06cd..cd60585d 100644 --- 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 = ( - "1845ff9cb4584f752d576e87a456c758f280b447410f5c0666e8c35d3f0f199b" + "6a541c168d9a5c266367050caec0114ea137a8f20f11f9868a6e46c821ba2ec2" ) EXPECTED_Q55_PROOF_SHA256 = ( "ac59cf89503170c789223b91d775213a19d4e571ef930f2ea609fcd51b14defd" From f832a534e246b56cab27add1cc3b1543d24f0c7a Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Thu, 23 Jul 2026 00:25:28 +0300 Subject: [PATCH 31/58] Compile typed finite targets into Program sessions --- ...rt-reference-surplus-q55-bps-proof-v1.json | 2 +- crates/labcolors-core/src/appearance.rs | 34 + crates/labcolors-core/src/constraints/mod.rs | 179 ++ crates/labcolors-core/src/joint.rs | 145 ++ crates/labcolors-core/src/lib.rs | 3 + .../src/program_joint_integration_tests.rs | 1199 ++++++++++++++ .../src/program_lcs_integration_tests.rs | 60 +- crates/labcolors-core/src/program_session.rs | 1475 +++++++++++++++-- .../src/program_session_tests.rs | 66 +- scripts/verify_point_support_surplus.py | 2 +- 10 files changed, 2970 insertions(+), 195 deletions(-) create mode 100644 crates/labcolors-core/src/program_joint_integration_tests.rs 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 1b99b1db..0e1b54c4 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":"535001a983b26b2ba9a157417e4d42de3517c0bf784c9d1863f0e4943981e959","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":"6a541c168d9a5c266367050caec0114ea137a8f20f11f9868a6e46c821ba2ec2","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":"57e6feeab15182322c258829d10214c31bb3c93fa58d8d47e37127db204cdc28"},{"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":"15b6437ed59c92d82b928d9d913d25c14b430ba8c75db54a02f7da18abcde684"},{"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":"a58acbae5b06a8cd9a45adf93ddfbdfb569c5916b60140bd4046182aab2a9518"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"c1adca0472a97272fbeb1eec9e110aec93768a3b898c4d34a8afdd54cafcc535"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"abed15a542b5e8031f0ebfe36138232920c7898c9eae1a2037c6455033ecd91e"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"945755f2eb91eb21aeee9b7a7b0e67b093d7e766af35a121480003669d88a053"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"0a1fdf10c9ecae497cfcd447a6af937f2b07d43b81b2cd28a50d998e11197fe9"},{"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":"2e192f355564c93d83a8f43ae648e06b34cbcb20d347137a998608657c98c434"} +{"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":"16f1efb88a9224b4507264f2a560eaa6b174a24020a208266c07a2f47309312a","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":"6b81edc2d56bdca78a4d7c579c79e2791dc59e9bf2626fd22ddc7aa164dfa5c8","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":"e361cf144e0630a4f2ff52261e6515c9394188fc1351c97b98ea9c0ec87ec39d"},{"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":"a58acbae5b06a8cd9a45adf93ddfbdfb569c5916b60140bd4046182aab2a9518"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"1c75a1bc5f33c497aff148fbe5d943716b5d88f680623e8cbf028545c08958c7"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"abed15a542b5e8031f0ebfe36138232920c7898c9eae1a2037c6455033ecd91e"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"945755f2eb91eb21aeee9b7a7b0e67b093d7e766af35a121480003669d88a053"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"0a1fdf10c9ecae497cfcd447a6af937f2b07d43b81b2cd28a50d998e11197fe9"},{"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":"e70ce1fadbb64b044772d9f18f7c16ee76cf6aa33b1d1c0e642e0437fd5ed4a3"} diff --git a/crates/labcolors-core/src/appearance.rs b/crates/labcolors-core/src/appearance.rs index f6e02fb1..95377930 100644 --- a/crates/labcolors-core/src/appearance.rs +++ b/crates/labcolors-core/src/appearance.rs @@ -770,6 +770,16 @@ pub(crate) struct CompiledPaintSlotV1 { id: PaintId, } +/// Cold-bound canonical colour-input position used by finite Program targets. +/// The nominal ID is retained and rechecked on every overwrite. This rejects +/// stale or mismatched index/ID pairs, but does not claim graph-instance +/// identity when two graphs have the same canonical input at that position. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct CompiledColorInputSlotV1 { + index: usize, + id: ColorInputId, +} + /// Cold-bound canonical Occurrence position for allocation-free repeated /// lookup. As with [`CompiledPaintSlotV1`], construction remains sealed inside /// the compiled appearance graph and every use revalidates the exact ID. @@ -1106,6 +1116,23 @@ impl AdmittedAppearanceBindings { Ok(()) } + /// Overwrite one prebound finite-target input without lookup or allocation. + /// The canonical index and nominal ID must both match before mutation. + pub(crate) fn overwrite_color_at( + &mut self, + slot: CompiledColorInputSlotV1, + value: Srgb8, + ) -> Result<(), BindingError> { + let Some((bound, destination)) = self.colors.get_mut(slot.index) else { + return Err(BindingError::IncompatibleAdmittedBindings); + }; + if *bound != slot.id { + return Err(BindingError::IncompatibleAdmittedBindings); + } + *destination = value; + Ok(()) + } + /// Borrow the admitted Surface-input slice in its exact canonical order. pub(crate) fn surface_inputs_canonical( &self, @@ -1574,6 +1601,13 @@ impl CompiledAppearanceGraph { self.program() == program } + /// Bind one colour input to its canonical compiled ordinal. Runtime target + /// selection consumes the sealed slot instead of repeating an ID search. + pub(crate) fn bind_color_input(&self, id: ColorInputId) -> Option { + let index = self.color_inputs.binary_search(&id).ok()?; + Some(CompiledColorInputSlotV1 { index, id }) + } + /// Bind one Paint identity to its canonical compiled ordinal. /// /// This is the only cold lookup. Repeated evaluations consume the sealed diff --git a/crates/labcolors-core/src/constraints/mod.rs b/crates/labcolors-core/src/constraints/mod.rs index ec44ffe9..fc56ab48 100644 --- a/crates/labcolors-core/src/constraints/mod.rs +++ b/crates/labcolors-core/src/constraints/mod.rs @@ -28,6 +28,56 @@ pub(crate) use wcag22::{ Wcag22ViolationV1, }; +/// Test-only probe around the production Program WCAG evaluator. It records +/// each physical visible signal without changing measurement or +/// classification, allowing execution-count assertions at the Program +/// certification boundary. +#[cfg(test)] +#[derive(Debug, Clone, Default)] +pub(crate) struct CountingProgramWcag22Srgb8V1 { + calls: std::rc::Rc>>, +} + +#[cfg(test)] +impl CountingProgramWcag22Srgb8V1 { + pub(crate) fn calls(&self) -> Vec { + self.calls.borrow().clone() + } +} + +#[cfg(test)] +#[derive(Debug, Default)] +struct FinalRecheckMutantControlV1 { + armed: std::cell::Cell, + calls_after_arm: std::cell::Cell, + force_current_violation: std::cell::Cell, +} + +/// Test-only exact evaluator which can be armed to pass the next search call +/// and fail the immediately following final-recheck call. +#[cfg(test)] +#[derive(Debug, Clone, Default)] +pub(crate) struct FinalRecheckMutantProgramEvaluatorV1 { + control: std::rc::Rc, +} + +#[cfg(test)] +impl FinalRecheckMutantProgramEvaluatorV1 { + pub(crate) fn arm(&self) { + self.control.calls_after_arm.set(0); + self.control.force_current_violation.set(false); + self.control.armed.set(true); + } +} + +#[cfg(test)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct MutantExactPassV1; + +#[cfg(test)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct MutantExactViolationV1; + /// Seals недоступны внешним crate-ам: новые evaluator/classifier families /// добавляются только вместе с code-owned physical adapter-ом. mod private { @@ -35,6 +85,18 @@ mod private { pub trait HardClassifierSealed {} } +#[cfg(test)] +impl private::EvaluatorSealed for CountingProgramWcag22Srgb8V1 {} + +#[cfg(test)] +impl private::HardClassifierSealed for CountingProgramWcag22Srgb8V1 {} + +#[cfg(test)] +impl private::EvaluatorSealed for FinalRecheckMutantProgramEvaluatorV1 {} + +#[cfg(test)] +impl private::HardClassifierSealed for FinalRecheckMutantProgramEvaluatorV1 {} + pub(crate) trait Evaluator: private::EvaluatorSealed { type Invocation; type Identity; @@ -214,6 +276,123 @@ impl ProgramPointEvaluatorV1 for Evaluation where { } +#[cfg(test)] +impl Evaluator for CountingProgramWcag22Srgb8V1 { + type Invocation = >::Invocation; + type Identity = >::Identity; + type Release = >::Release; + type Capability = >::Capability; + type Measurement = >::Measurement; + type Error = >::Error; + + fn identity(&self) -> Self::Identity { + >::identity(&Wcag22Srgb8V1) + } + + fn release(&self) -> Self::Release { + >::release(&Wcag22Srgb8V1) + } + + fn capability(&self) -> Self::Capability { + >::capability(&Wcag22Srgb8V1) + } + + fn evaluate( + &self, + target: &ProgramPointTargetV1, + invocation: &Self::Invocation, + ) -> Result { + self.calls + .borrow_mut() + .push(Srgb8::new(target.encoded().visible())); + >::evaluate( + &Wcag22Srgb8V1, + target, + invocation, + ) + } +} + +#[cfg(test)] +impl + HardClassifier< + >::Invocation, + >::Measurement, + > for CountingProgramWcag22Srgb8V1 +{ + type Pass = >::Invocation, + >::Measurement, + >>::Pass; + type Violation = >::Invocation, + >::Measurement, + >>::Violation; + + fn classify( + &self, + invocation: &>::Invocation, + measurement: &>::Measurement, + ) -> HardDecision { + >::classify(&Wcag22Srgb8V1, invocation, measurement) + } +} + +#[cfg(test)] +impl Evaluator for FinalRecheckMutantProgramEvaluatorV1 { + type Invocation = Srgb8; + type Identity = (); + type Release = (); + type Capability = (); + type Measurement = Srgb8; + type Error = core::convert::Infallible; + + fn identity(&self) -> Self::Identity {} + + fn release(&self) -> Self::Release {} + + fn capability(&self) -> Self::Capability {} + + fn evaluate( + &self, + target: &ProgramPointTargetV1, + _invocation: &Self::Invocation, + ) -> Result { + let force_violation = if self.control.armed.get() { + let call = self.control.calls_after_arm.get(); + self.control.calls_after_arm.set(call + 1); + if call == 1 { + self.control.armed.set(false); + true + } else { + false + } + } else { + false + }; + self.control.force_current_violation.set(force_violation); + Ok(Srgb8::new(target.encoded().visible())) + } +} + +#[cfg(test)] +impl HardClassifier for FinalRecheckMutantProgramEvaluatorV1 { + type Pass = MutantExactPassV1; + type Violation = MutantExactViolationV1; + + fn classify( + &self, + invocation: &Srgb8, + measurement: &Srgb8, + ) -> HardDecision { + if self.control.force_current_violation.replace(false) || invocation != measurement { + HardDecision::Violation(MutantExactViolationV1) + } else { + HardDecision::Pass(MutantExactPassV1) + } + } +} + /// Program evidence binds the physical source-over certificate and the exact /// modeled LCS provenance/context used by the evaluator in one non-forgeable /// value. diff --git a/crates/labcolors-core/src/joint.rs b/crates/labcolors-core/src/joint.rs index 5188ac9d..922cee76 100644 --- a/crates/labcolors-core/src/joint.rs +++ b/crates/labcolors-core/src/joint.rs @@ -19,6 +19,151 @@ use crate::constraints::{ assess_visible_point_hard, }; use crate::observation::{RevisionBoundObservationV1, ScenarioId}; + +/// One canonical candidate ordinal inside a finite target domain. +/// +/// The ordinal is assigned only after the owning Program has sorted the +/// target's opaque candidate IDs. It is therefore an internal compiled index, +/// never client identity or declaration-order policy. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct FiniteDomainOrdinalV1(usize); + +impl FiniteDomainOrdinalV1 { + pub(crate) const fn new(index: usize) -> Self { + Self(index) + } + + pub(crate) const fn index(self) -> usize { + self.0 + } +} + +/// A fully admitted total order over the product of finite target +/// domains. Each tuple is stored in canonical target order. The tuple order +/// is authored policy; no target ID, candidate value, or declaration position +/// becomes an implicit tie-break. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct AdmittedFiniteJointOrderV1 { + tuples: Box<[Box<[FiniteDomainOrdinalV1]>]>, +} + +impl AdmittedFiniteJointOrderV1 { + pub(crate) fn tuples(&self) -> impl ExactSizeIterator + '_ { + self.tuples.iter().map(Box::as_ref) + } +} + +/// Failure to admit a declared finite joint selection order. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum FiniteJointOrderErrorV1 { + EmptyDomain { + dimension: usize, + }, + CardinalityOverflow, + EmptyOrder, + TupleArity { + tuple: usize, + expected: usize, + actual: usize, + }, + OrdinalOutOfDomain { + tuple: usize, + dimension: usize, + ordinal: usize, + domain_len: usize, + }, + DuplicateTuple { + first: usize, + duplicate: usize, + }, + IncompleteOrder { + expected: usize, + actual: usize, + }, + ResourceExhausted, +} + +/// Check and seal an explicit total order over a finite product domain. +/// +/// This function deliberately does not synthesize lexicographic policy. A +/// caller must enumerate every tuple exactly once. Checked multiplication and +/// fallible allocation happen before the result can reach runtime. +pub(crate) fn admit_finite_joint_order_v1( + domain_lengths: &[usize], + authored: Vec>, +) -> Result { + let mut expected = 1usize; + for (dimension, &domain_len) in domain_lengths.iter().enumerate() { + if domain_len == 0 { + return Err(FiniteJointOrderErrorV1::EmptyDomain { dimension }); + } + expected = expected + .checked_mul(domain_len) + .ok_or(FiniteJointOrderErrorV1::CardinalityOverflow)?; + } + if authored.is_empty() { + return Err(FiniteJointOrderErrorV1::EmptyOrder); + } + if authored.len() != expected { + return Err(FiniteJointOrderErrorV1::IncompleteOrder { + expected, + actual: authored.len(), + }); + } + + // The mixed-radix ordinal is a bijection over the admitted product. A + // fallibly allocated first-seen table makes duplicate admission O(states × + // dimensions), rather than comparing every tuple with every earlier tuple. + let mut first_seen = Vec::new(); + first_seen + .try_reserve_exact(expected) + .map_err(|_| FiniteJointOrderErrorV1::ResourceExhausted)?; + first_seen.resize(expected, usize::MAX); + + let mut tuples = Vec::new(); + tuples + .try_reserve_exact(authored.len()) + .map_err(|_| FiniteJointOrderErrorV1::ResourceExhausted)?; + for (tuple_index, tuple) in authored.into_iter().enumerate() { + if tuple.len() != domain_lengths.len() { + return Err(FiniteJointOrderErrorV1::TupleArity { + tuple: tuple_index, + expected: domain_lengths.len(), + actual: tuple.len(), + }); + } + let mut mixed_radix_index = 0usize; + for (dimension, (ordinal, &domain_len)) in tuple.iter().zip(domain_lengths).enumerate() { + if ordinal.index() >= domain_len { + return Err(FiniteJointOrderErrorV1::OrdinalOutOfDomain { + tuple: tuple_index, + dimension, + ordinal: ordinal.index(), + domain_len, + }); + } + mixed_radix_index = mixed_radix_index + .checked_mul(domain_len) + .and_then(|index| index.checked_add(ordinal.index())) + .ok_or(FiniteJointOrderErrorV1::CardinalityOverflow)?; + } + let first = first_seen + .get_mut(mixed_radix_index) + .ok_or(FiniteJointOrderErrorV1::CardinalityOverflow)?; + if *first != usize::MAX { + return Err(FiniteJointOrderErrorV1::DuplicateTuple { + first: *first, + duplicate: tuple_index, + }); + } + *first = tuple_index; + tuples.push(tuple.into_boxed_slice()); + } + + Ok(AdmittedFiniteJointOrderV1 { + tuples: tuples.into_boxed_slice(), + }) +} use crate::session::SessionObservationBindingPermitV1; /// Sealed evaluator family, которую joint-program вызывает одинаково для diff --git a/crates/labcolors-core/src/lib.rs b/crates/labcolors-core/src/lib.rs index ec828672..09e418be 100644 --- a/crates/labcolors-core/src/lib.rs +++ b/crates/labcolors-core/src/lib.rs @@ -94,6 +94,9 @@ mod program_session_tests; #[cfg(test)] mod program_lcs_integration_tests; +#[cfg(test)] +mod program_joint_integration_tests; + #[cfg(test)] mod release_registry_tests; diff --git a/crates/labcolors-core/src/program_joint_integration_tests.rs b/crates/labcolors-core/src/program_joint_integration_tests.rs new file mode 100644 index 00000000..a04d8b3c --- /dev/null +++ b/crates/labcolors-core/src/program_joint_integration_tests.rs @@ -0,0 +1,1199 @@ +use crate::Srgb8; +use crate::appearance::{OccurrenceId, PaintId, SurfaceId, SurfaceInputPortId}; +use crate::constraints::{ + CountingProgramWcag22Srgb8V1, FinalRecheckMutantProgramEvaluatorV1, Wcag22Srgb8V1, +}; +use crate::joint::FiniteJointOrderErrorV1; +use crate::lcs_occurrence::{ + AdaptingLuminanceCdM2, AppearanceContextId, AppearanceContextSchemaReleaseId, + BackgroundLuminanceRatio, ColorSignal, IEC_SRGB_D65_XYZ_FRAME_V1, SurroundProfileId, +}; +use crate::observation::{ + ObservationGroupId, ObservationPayloadInput, ObservationStreamId, ObservationUpdateInput, + ObservedScenarioSetInput, Revision, ScenarioId, ScenarioInput, SurfaceInputBinding, +}; +use crate::program_session::{ + CompositionProfile, ConstraintId, ConstraintInvocation, ConstraintSet, + DeclaredJointSelectionV1, JointCandidateStateV1, ObservationGroup, Occurrence, OutputBinding, + OutputSlotId, Paint, Program, ProgramCompileError, ProgramSessionEvaluationError, Source, + SourceId, Surface, Target, TargetCandidateChoiceV1, TargetCandidateId, TargetCandidateV1, + TargetDomainV1, TargetId, checked_program_evaluation_cell_counts_for_test, + fail_program_preflight_reservation_for_test, +}; +use crate::session::{SessionState, SessionUpdateError}; +use crate::wcag22::Wcag22CriterionV1; + +const SOURCE: SourceId = SourceId::new(1); +const SURFACE_PORT: SurfaceInputPortId = SurfaceInputPortId::new(2); +const PAINT: PaintId = PaintId::new(10); +const BACKDROP: SurfaceId = SurfaceId::new(20); +const OCCURRENCE: OccurrenceId = OccurrenceId::new(30); +const OUTPUT: OutputSlotId = OutputSlotId::new(40); +const TARGET: TargetId = TargetId::new(50); +const FIRST: TargetCandidateId = TargetCandidateId::new(60); +const SECOND: TargetCandidateId = TargetCandidateId::new(61); +const GROUP: ObservationGroupId = ObservationGroupId::new(70); +const STREAM: ObservationStreamId = ObservationStreamId::new(80); +const UPPER_SOURCE: SourceId = SourceId::new(81); +const UPPER_PAINT: PaintId = PaintId::new(82); +const DERIVED_SURFACE: SurfaceId = SurfaceId::new(83); +const UPPER_OCCURRENCE: OccurrenceId = OccurrenceId::new(84); +const UPPER_OUTPUT: OutputSlotId = OutputSlotId::new(85); +const UPPER_TARGET: TargetId = TargetId::new(86); +const UPPER_FIRST: TargetCandidateId = TargetCandidateId::new(87); +const UPPER_SECOND: TargetCandidateId = TargetCandidateId::new(88); + +fn appearance_context() -> AppearanceContextId { + AppearanceContextId::from_inputs( + AppearanceContextSchemaReleaseId::Ciecam16ViewingInputsV1, + IEC_SRGB_D65_XYZ_FRAME_V1, + AdaptingLuminanceCdM2::try_new(64.0).unwrap(), + BackgroundLuminanceRatio::try_new(0.2).unwrap(), + SurroundProfileId::AverageV1, + ) +} + +fn signal(value: u8) -> ColorSignal { + ColorSignal::from_srgb8(Srgb8::new([value; 3])) +} + +fn candidate(id: TargetCandidateId, value: u8) -> TargetCandidateV1 { + TargetCandidateV1::new(id, signal(value)) +} + +fn state(candidate: TargetCandidateId) -> JointCandidateStateV1 { + JointCandidateStateV1::new(vec![TargetCandidateChoiceV1::new(TARGET, candidate)]) +} + +fn target(candidates: Vec) -> Target { + Target::finite(TARGET, SOURCE, candidates) +} + +fn program( + hard: Vec>, + report_only: Vec>, + candidates: Vec, + order: Vec, +) -> Program { + Program::new( + vec![Source::new(SOURCE, signal(0))], + vec![target(candidates)], + ObservationGroup::new(GROUP, vec![SURFACE_PORT]), + vec![], + vec![Paint::Solid { + id: PAINT, + target: TARGET, + }], + vec![Surface::Input { + id: BACKDROP, + input: SURFACE_PORT, + }], + vec![Occurrence::new( + OCCURRENCE, + PAINT, + BACKDROP, + CompositionProfile::EncodedSrgb8SourceOverV1, + appearance_context(), + )], + ConstraintSet::new(hard, report_only), + vec![OutputBinding::new(OUTPUT, PAINT)], + Wcag22Srgb8V1, + ) + .with_joint_selection(DeclaredJointSelectionV1::new(order)) +} + +fn update(revision: u64, backdrop: u8) -> ObservationUpdateInput { + ObservationUpdateInput { + stream: STREAM, + revision: Revision::new(revision), + payload: ObservationPayloadInput::Scenarios(ObservedScenarioSetInput { + scenarios: vec![ScenarioInput { + id: ScenarioId::new(1), + bindings: vec![SurfaceInputBinding::new(SURFACE_PORT, signal(backdrop))], + }], + }), + } +} + +fn paired_state( + lower: TargetCandidateId, + upper: TargetCandidateId, + reverse_choices: bool, +) -> JointCandidateStateV1 { + let mut choices = vec![ + TargetCandidateChoiceV1::new(TARGET, lower), + TargetCandidateChoiceV1::new(UPPER_TARGET, upper), + ]; + if reverse_choices { + choices.reverse(); + } + JointCandidateStateV1::new(choices) +} + +fn nested_two_target_program( + reverse_targets: bool, + reverse_choices: bool, +) -> Program { + let lower = Target::finite( + TARGET, + SOURCE, + vec![candidate(FIRST, 0x00), candidate(SECOND, 0xFF)], + ); + let upper = Target::finite( + UPPER_TARGET, + UPPER_SOURCE, + vec![candidate(UPPER_FIRST, 0x55), candidate(UPPER_SECOND, 0xFF)], + ); + let mut targets = vec![lower, upper]; + if reverse_targets { + targets.reverse(); + } + + Program::new( + vec![ + Source::new(SOURCE, signal(0)), + Source::new(UPPER_SOURCE, signal(0)), + ], + targets, + ObservationGroup::new(GROUP, vec![SURFACE_PORT]), + vec![], + vec![ + Paint::Solid { + id: PAINT, + target: TARGET, + }, + Paint::Solid { + id: UPPER_PAINT, + target: UPPER_TARGET, + }, + ], + vec![ + Surface::Input { + id: BACKDROP, + input: SURFACE_PORT, + }, + Surface::FromOccurrence { + id: DERIVED_SURFACE, + occurrence: OCCURRENCE, + }, + ], + vec![ + Occurrence::new( + OCCURRENCE, + PAINT, + BACKDROP, + CompositionProfile::EncodedSrgb8SourceOverV1, + appearance_context(), + ), + Occurrence::new( + UPPER_OCCURRENCE, + UPPER_PAINT, + DERIVED_SURFACE, + CompositionProfile::EncodedSrgb8SourceOverV1, + appearance_context(), + ), + ], + ConstraintSet::new( + vec![ConstraintInvocation::hard( + ConstraintId::new(1), + UPPER_OCCURRENCE, + Wcag22CriterionV1::Sc143TextLargeScale, + )], + vec![], + ), + vec![ + OutputBinding::new(OUTPUT, PAINT), + OutputBinding::new(UPPER_OUTPUT, UPPER_PAINT), + ], + Wcag22Srgb8V1, + ) + .with_joint_selection(DeclaredJointSelectionV1::new(vec![ + paired_state(FIRST, UPPER_FIRST, reverse_choices), + paired_state(SECOND, UPPER_FIRST, reverse_choices), + paired_state(FIRST, UPPER_SECOND, reverse_choices), + paired_state(SECOND, UPPER_SECOND, reverse_choices), + ])) +} + +#[derive(Clone, Copy)] +struct AlphaRenamedJointIds { + lower_source: SourceId, + upper_source: SourceId, + lower_target: TargetId, + upper_target: TargetId, + lower_first: TargetCandidateId, + lower_second: TargetCandidateId, + upper_first: TargetCandidateId, + upper_second: TargetCandidateId, +} + +fn alpha_renamed_nested_program( + ids: AlphaRenamedJointIds, + permute_declarations: bool, +) -> Program { + let mut sources = vec![ + Source::new(ids.lower_source, signal(0)), + Source::new(ids.upper_source, signal(0)), + ]; + let mut lower_candidates = vec![ + candidate(ids.lower_first, 0x00), + candidate(ids.lower_second, 0xFF), + ]; + let mut upper_candidates = vec![ + candidate(ids.upper_first, 0x55), + candidate(ids.upper_second, 0xFF), + ]; + if permute_declarations { + sources.reverse(); + lower_candidates.reverse(); + 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), + ]; + if permute_declarations { + targets.reverse(); + } + + let state = |lower, upper| { + let mut choices = vec![ + TargetCandidateChoiceV1::new(ids.lower_target, lower), + TargetCandidateChoiceV1::new(ids.upper_target, upper), + ]; + if permute_declarations { + choices.reverse(); + } + JointCandidateStateV1::new(choices) + }; + + Program::new( + sources, + targets, + ObservationGroup::new(GROUP, vec![SURFACE_PORT]), + vec![], + vec![ + Paint::Solid { + id: PAINT, + target: ids.lower_target, + }, + Paint::Solid { + id: UPPER_PAINT, + target: ids.upper_target, + }, + ], + vec![ + Surface::Input { + id: BACKDROP, + input: SURFACE_PORT, + }, + Surface::FromOccurrence { + id: DERIVED_SURFACE, + occurrence: OCCURRENCE, + }, + ], + vec![ + Occurrence::new( + OCCURRENCE, + PAINT, + BACKDROP, + CompositionProfile::EncodedSrgb8SourceOverV1, + appearance_context(), + ), + Occurrence::new( + UPPER_OCCURRENCE, + UPPER_PAINT, + DERIVED_SURFACE, + CompositionProfile::EncodedSrgb8SourceOverV1, + appearance_context(), + ), + ], + ConstraintSet::new( + vec![ConstraintInvocation::hard( + ConstraintId::new(1), + UPPER_OCCURRENCE, + Wcag22CriterionV1::Sc143TextLargeScale, + )], + vec![], + ), + vec![ + OutputBinding::new(OUTPUT, PAINT), + OutputBinding::new(UPPER_OUTPUT, UPPER_PAINT), + ], + Wcag22Srgb8V1, + ) + .with_joint_selection(DeclaredJointSelectionV1::new(vec![ + state(ids.lower_first, ids.upper_first), + state(ids.lower_second, ids.upper_first), + state(ids.lower_first, ids.upper_second), + state(ids.lower_second, ids.upper_second), + ])) +} + +#[test] +fn authored_finite_target_values_keep_only_opaque_identity_and_explicit_policy() { + let first = candidate(FIRST, 0x66); + assert_eq!(TARGET.value(), 50); + assert_eq!(FIRST.value(), 60); + assert_eq!(first.id(), FIRST); + assert_eq!(first.signal(), signal(0x66)); + + let target = target(vec![first]); + assert_eq!(target.id(), TARGET); + assert_eq!(target.source(), SOURCE); + let TargetDomainV1::Finite(candidates) = target.domain() else { + panic!("target must retain its explicit finite domain"); + }; + assert_eq!(candidates, &[first]); + + let choice = TargetCandidateChoiceV1::new(TARGET, FIRST); + assert_eq!(choice.target(), TARGET); + assert_eq!(choice.candidate(), FIRST); + let state = JointCandidateStateV1::new(vec![choice]); + assert_eq!(state.choices(), &[choice]); + let order = DeclaredJointSelectionV1::new(vec![state.clone()]); + assert_eq!(order.states(), &[state]); +} + +#[test] +fn terminal_safety_rejects_an_output_outside_every_assessment_cone() { + let error = match Program::new( + vec![ + Source::new(SOURCE, signal(0)), + Source::new(UPPER_SOURCE, signal(0xFF)), + ], + vec![ + Target::fixed(TARGET, SOURCE), + Target::fixed(UPPER_TARGET, UPPER_SOURCE), + ], + ObservationGroup::new(GROUP, vec![SURFACE_PORT]), + vec![], + vec![ + Paint::Solid { + id: PAINT, + target: TARGET, + }, + Paint::Solid { + id: UPPER_PAINT, + target: UPPER_TARGET, + }, + ], + vec![Surface::Input { + id: BACKDROP, + input: SURFACE_PORT, + }], + vec![Occurrence::new( + OCCURRENCE, + PAINT, + BACKDROP, + CompositionProfile::EncodedSrgb8SourceOverV1, + appearance_context(), + )], + ConstraintSet::new( + vec![ConstraintInvocation::hard( + ConstraintId::new(1), + OCCURRENCE, + Wcag22CriterionV1::Sc143TextLargeScale, + )], + vec![], + ), + vec![OutputBinding::new(UPPER_OUTPUT, UPPER_PAINT)], + Wcag22Srgb8V1, + ) + .compile() + { + Ok(_) => panic!("unassessed output must not compile"), + Err(error) => error, + }; + assert_eq!( + error, + ProgramCompileError::UnassessedOutput { + output: UPPER_OUTPUT, + paint: UPPER_PAINT, + } + ); +} + +#[test] +fn terminal_safety_rejects_an_unconstrained_finite_target() { + let error = match Program::new( + vec![ + Source::new(SOURCE, signal(0)), + Source::new(UPPER_SOURCE, signal(0xFF)), + ], + vec![ + target(vec![candidate(FIRST, 0), candidate(SECOND, 0xFF)]), + Target::fixed(UPPER_TARGET, UPPER_SOURCE), + ], + ObservationGroup::new(GROUP, vec![SURFACE_PORT]), + vec![], + vec![Paint::Solid { + id: UPPER_PAINT, + target: UPPER_TARGET, + }], + vec![Surface::Input { + id: BACKDROP, + input: SURFACE_PORT, + }], + vec![Occurrence::new( + UPPER_OCCURRENCE, + UPPER_PAINT, + BACKDROP, + CompositionProfile::EncodedSrgb8SourceOverV1, + appearance_context(), + )], + ConstraintSet::new( + vec![ConstraintInvocation::hard( + ConstraintId::new(1), + UPPER_OCCURRENCE, + Wcag22CriterionV1::Sc143TextLargeScale, + )], + vec![], + ), + vec![OutputBinding::new(UPPER_OUTPUT, UPPER_PAINT)], + Wcag22Srgb8V1, + ) + .with_joint_selection(DeclaredJointSelectionV1::new(vec![ + state(FIRST), + state(SECOND), + ])) + .compile() + { + Ok(_) => panic!("unconstrained finite target must not compile"), + Err(error) => error, + }; + assert_eq!( + error, + ProgramCompileError::UnconstrainedTarget { target: TARGET } + ); +} + +#[test] +fn independent_finite_target_components_are_rejected_before_global_product_search() { + let error = match Program::new( + vec![ + Source::new(SOURCE, signal(0)), + Source::new(UPPER_SOURCE, signal(0)), + ], + vec![ + target(vec![candidate(FIRST, 0), candidate(SECOND, 0xFF)]), + Target::finite( + UPPER_TARGET, + UPPER_SOURCE, + vec![candidate(UPPER_FIRST, 0x55), candidate(UPPER_SECOND, 0xFF)], + ), + ], + ObservationGroup::new(GROUP, vec![SURFACE_PORT]), + vec![], + vec![ + Paint::Solid { + id: PAINT, + target: TARGET, + }, + Paint::Solid { + id: UPPER_PAINT, + target: UPPER_TARGET, + }, + ], + vec![Surface::Input { + id: BACKDROP, + input: SURFACE_PORT, + }], + vec![ + Occurrence::new( + OCCURRENCE, + PAINT, + BACKDROP, + CompositionProfile::EncodedSrgb8SourceOverV1, + appearance_context(), + ), + Occurrence::new( + UPPER_OCCURRENCE, + UPPER_PAINT, + BACKDROP, + CompositionProfile::EncodedSrgb8SourceOverV1, + appearance_context(), + ), + ], + ConstraintSet::new( + vec![ + ConstraintInvocation::hard( + ConstraintId::new(1), + OCCURRENCE, + Wcag22CriterionV1::Sc143TextLargeScale, + ), + ConstraintInvocation::hard( + ConstraintId::new(2), + UPPER_OCCURRENCE, + Wcag22CriterionV1::Sc143TextLargeScale, + ), + ], + vec![], + ), + vec![ + OutputBinding::new(OUTPUT, PAINT), + OutputBinding::new(UPPER_OUTPUT, UPPER_PAINT), + ], + Wcag22Srgb8V1, + ) + .with_joint_selection(DeclaredJointSelectionV1::new(vec![ + paired_state(FIRST, UPPER_FIRST, false), + paired_state(SECOND, UPPER_FIRST, false), + paired_state(FIRST, UPPER_SECOND, false), + paired_state(SECOND, UPPER_SECOND, false), + ])) + .compile() + { + Ok(_) => panic!("independent components must not enter one global product search"), + Err(error) => error, + }; + assert_eq!(error, ProgramCompileError::DisconnectedFiniteTargets); +} + +#[test] +fn candidate_passing_one_hard_cell_and_failing_another_is_never_certified() { + let compiled = program( + vec![ + ConstraintInvocation::hard( + ConstraintId::new(1), + OCCURRENCE, + Wcag22CriterionV1::Sc143TextLargeScale, + ), + ConstraintInvocation::hard( + ConstraintId::new(2), + OCCURRENCE, + Wcag22CriterionV1::Sc143TextDefault, + ), + ], + vec![], + vec![candidate(FIRST, 0x66), candidate(SECOND, 0xFF)], + vec![state(FIRST), state(SECOND)], + ) + .compile() + .unwrap(); + let mut session = compiled.instantiate(STREAM).unwrap(); + + let SessionState::Ready { current } = session.update(update(1, 0x00)).unwrap() else { + panic!("the later all-hard-feasible candidate must be selected"); + }; + assert_eq!(current.selected_state_index(), Some(1)); + assert_eq!(current.outputs().len(), 1); + assert_eq!(current.outputs()[0].source_signal(), signal(0xFF)); + assert_eq!(current.report().cells().len(), 2); + assert!( + current + .report() + .cells() + .iter() + .all(|cell| cell.candidate_state_index() == 1 && !cell.result().is_violation()) + ); +} + +#[test] +fn report_only_violation_is_retained_but_does_not_select() { + let compiled = program( + vec![ConstraintInvocation::hard( + ConstraintId::new(1), + OCCURRENCE, + Wcag22CriterionV1::Sc143TextLargeScale, + )], + vec![ConstraintInvocation::report_only( + ConstraintId::new(2), + OCCURRENCE, + Wcag22CriterionV1::Sc143TextDefault, + )], + vec![candidate(FIRST, 0x66), candidate(SECOND, 0xFF)], + vec![state(FIRST), state(SECOND)], + ) + .compile() + .unwrap(); + let mut session = compiled.instantiate(STREAM).unwrap(); + + let SessionState::Ready { current } = session.update(update(1, 0x00)).unwrap() else { + panic!("report-only failure must not reject the first hard-feasible state"); + }; + assert_eq!(current.selected_state_index(), Some(0)); + assert_eq!(current.outputs()[0].source_signal(), signal(0x66)); + let cells = current.report().cells(); + assert_eq!(cells.len(), 2); + assert!(!cells[0].result().is_violation()); + assert!(cells[0].is_hard()); + assert!(cells[1].result().is_violation()); + assert!(!cells[1].is_hard()); +} + +#[test] +fn report_only_assessment_admits_output_but_cannot_override_explicit_selection_order() { + let compiled = program( + vec![], + vec![ConstraintInvocation::report_only( + ConstraintId::new(1), + OCCURRENCE, + Wcag22CriterionV1::Sc143TextDefault, + )], + vec![candidate(FIRST, 0x66), candidate(SECOND, 0xFF)], + vec![state(FIRST), state(SECOND)], + ) + .compile() + .unwrap(); + let mut session = compiled.instantiate(STREAM).unwrap(); + + let SessionState::Ready { current } = session.update(update(1, 0x00)).unwrap() else { + panic!("report-only assessment must not create hard infeasibility"); + }; + assert_eq!(current.selected_state_index(), Some(0)); + assert_eq!(current.outputs()[0].source_signal(), signal(0x66)); + assert!(current.report().cells()[0].result().is_violation()); + assert!(!current.report().cells()[0].is_hard()); +} + +#[test] +fn no_feasible_joint_state_commits_no_output_and_retains_previous_certificate() { + let compiled = program( + vec![ConstraintInvocation::hard( + ConstraintId::new(1), + OCCURRENCE, + Wcag22CriterionV1::Sc143TextLargeScale, + )], + vec![], + vec![candidate(FIRST, 0xAA), candidate(SECOND, 0xFF)], + vec![state(FIRST), state(SECOND)], + ) + .compile() + .unwrap(); + let mut session = compiled.instantiate(STREAM).unwrap(); + + let SessionState::Ready { current } = session.update(update(1, 0x00)).unwrap() else { + panic!("control update must certify the first state"); + }; + assert_eq!(current.outputs()[0].source_signal(), signal(0xAA)); + + let SessionState::Failed { cause, previous } = session.update(update(2, 0xFF)).unwrap() else { + panic!("all-hard-infeasible domain must become Conflict/Failed"); + }; + assert_eq!(cause.considered_state_count(), 2); + let previous = previous + .as_ref() + .expect("last certificate must be retained"); + assert_eq!(previous.outputs()[0].source_signal(), signal(0xAA)); + assert_eq!(cause.report().cells().len(), 2); + assert!( + cause + .report() + .cells() + .iter() + .all(|cell| { cell.is_hard() && cell.result().is_violation() }) + ); +} + +#[test] +fn candidate_declaration_permutation_preserves_explicit_physical_order() { + let make = |candidates| { + program( + vec![ConstraintInvocation::hard( + ConstraintId::new(1), + OCCURRENCE, + Wcag22CriterionV1::Sc143TextLargeScale, + )], + vec![], + candidates, + vec![state(FIRST), state(SECOND)], + ) + .compile() + .unwrap() + }; + let first = make(vec![candidate(FIRST, 0x66), candidate(SECOND, 0xFF)]); + let reversed = make(vec![candidate(SECOND, 0xFF), candidate(FIRST, 0x66)]); + let mut first_session = first.instantiate(STREAM).unwrap(); + let mut reversed_session = reversed.instantiate(STREAM).unwrap(); + + let SessionState::Ready { current: first } = first_session.update(update(1, 0x00)).unwrap() + else { + panic!("first program must certify"); + }; + let SessionState::Ready { current: reversed } = + reversed_session.update(update(1, 0x00)).unwrap() + else { + panic!("permuted program must certify"); + }; + assert_eq!( + first.selected_state_index(), + reversed.selected_state_index() + ); + assert_eq!(first.outputs(), reversed.outputs()); +} + +#[test] +fn nested_two_target_selection_ignores_target_and_choice_declaration_order() { + let run = |reverse_targets, reverse_choices| { + let compiled = nested_two_target_program(reverse_targets, reverse_choices) + .compile() + .unwrap(); + let mut session = compiled.instantiate(STREAM).unwrap(); + let SessionState::Ready { current } = session.update(update(1, 0x00)).unwrap() else { + panic!("the second explicitly ordered joint tuple must certify"); + }; + ( + current.selected_state_index(), + current + .outputs() + .iter() + .map(|output| (output.output(), output.source_signal())) + .collect::>(), + ) + }; + + let canonical = run(false, false); + let permuted = run(true, true); + assert_eq!(canonical, permuted); + assert_eq!(canonical.0, Some(1)); + assert_eq!( + canonical.1, + vec![(OUTPUT, signal(0xFF)), (UPPER_OUTPUT, signal(0x55))] + ); +} + +#[test] +fn bijective_source_target_and_candidate_renaming_preserves_joint_evidence() { + let canonical_ids = AlphaRenamedJointIds { + lower_source: SourceId::new(100), + upper_source: SourceId::new(200), + lower_target: TargetId::new(300), + upper_target: TargetId::new(400), + lower_first: TargetCandidateId::new(500), + lower_second: TargetCandidateId::new(600), + upper_first: TargetCandidateId::new(700), + upper_second: TargetCandidateId::new(800), + }; + // Every authored namespace is alpha-renamed. Source and Target order is + // reversed across logical dimensions, while both candidate domains also + // reverse numeric order. Declaration and per-state choice order are then + // independently permuted; only the explicit logical state order remains. + let renamed_ids = AlphaRenamedJointIds { + lower_source: SourceId::new(920), + upper_source: SourceId::new(110), + lower_target: TargetId::new(840), + upper_target: TargetId::new(230), + lower_first: TargetCandidateId::new(760), + lower_second: TargetCandidateId::new(650), + upper_first: TargetCandidateId::new(540), + upper_second: TargetCandidateId::new(430), + }; + + let run = |program: Program| { + let compiled = program.compile().unwrap(); + let mut session = compiled.instantiate(STREAM).unwrap(); + let SessionState::Ready { current } = session.update(update(1, 0x00)).unwrap() else { + panic!("the second logical state must certify after the first is rejected"); + }; + assert_eq!(current.selected_state_index(), Some(1)); + + let outputs = current + .outputs() + .iter() + .map(|output| (output.output(), output.paint(), output.source_signal())) + .collect::>(); + let cells = current + .report() + .cells() + .iter() + .map(|cell| { + let modeled = cell.modeled_lcs_occurrence(); + ( + cell.candidate_state_index(), + cell.case_index(), + cell.constraint(), + cell.target(), + cell.is_hard(), + cell.result().is_violation(), + modeled, + modeled.signal(), + ) + }) + .collect::>(); + let observation = current.report().observation(); + let physical_cases = (0..observation.physical_case_count()) + .map(|case_index| { + ( + observation.physical_values(case_index).unwrap().to_vec(), + observation.provenance(case_index).unwrap().to_vec(), + ) + }) + .collect::>(); + + ( + current.selected_state_index(), + outputs, + cells, + observation.stream(), + observation.revision(), + physical_cases, + ) + }; + + let canonical = run(alpha_renamed_nested_program(canonical_ids, false)); + let renamed = run(alpha_renamed_nested_program(renamed_ids, true)); + assert_eq!(canonical, renamed); + assert_eq!( + canonical.1, + vec![ + (OUTPUT, canonical.1[0].1, signal(0xFF),), + (UPPER_OUTPUT, canonical.1[1].1, signal(0x55),), + ] + ); +} + +#[test] +fn rejected_state_runs_once_and_selected_state_runs_fresh_recheck_twice() { + let evaluator = CountingProgramWcag22Srgb8V1::default(); + let calls = evaluator.clone(); + let compiled = Program::new( + vec![Source::new(SOURCE, signal(0))], + vec![target(vec![ + candidate(FIRST, 0x55), + candidate(SECOND, 0xFF), + ])], + ObservationGroup::new(GROUP, vec![SURFACE_PORT]), + vec![], + vec![Paint::Solid { + id: PAINT, + target: TARGET, + }], + vec![Surface::Input { + id: BACKDROP, + input: SURFACE_PORT, + }], + vec![Occurrence::new( + OCCURRENCE, + PAINT, + BACKDROP, + CompositionProfile::EncodedSrgb8SourceOverV1, + appearance_context(), + )], + ConstraintSet::new( + vec![ConstraintInvocation::hard( + ConstraintId::new(1), + OCCURRENCE, + Wcag22CriterionV1::Sc143TextLargeScale, + )], + vec![], + ), + vec![OutputBinding::new(OUTPUT, PAINT)], + evaluator, + ) + .with_joint_selection(DeclaredJointSelectionV1::new(vec![ + state(FIRST), + state(SECOND), + ])) + .compile() + .unwrap(); + let mut session = compiled.instantiate(STREAM).unwrap(); + + let SessionState::Ready { current } = session.update(update(1, 0x00)).unwrap() else { + panic!("the second candidate must certify after a fresh full recheck"); + }; + assert_eq!(current.selected_state_index(), Some(1)); + assert_eq!( + calls.calls(), + vec![ + Srgb8::new([0x55; 3]), + Srgb8::new([0xFF; 3]), + Srgb8::new([0xFF; 3]), + ] + ); +} + +#[test] +fn successful_search_allocations_do_not_scale_with_rejected_states() { + let compile = |candidates, order| { + program( + vec![ConstraintInvocation::hard( + ConstraintId::new(1), + OCCURRENCE, + Wcag22CriterionV1::Sc143TextLargeScale, + )], + vec![], + candidates, + order, + ) + .compile() + .unwrap() + }; + let direct = compile(vec![candidate(SECOND, 0xFF)], vec![state(SECOND)]); + let after_rejection = compile( + vec![candidate(FIRST, 0x55), candidate(SECOND, 0xFF)], + vec![state(FIRST), state(SECOND)], + ); + let mut direct_session = direct.instantiate(STREAM).unwrap(); + let mut rejected_session = after_rejection.instantiate(STREAM).unwrap(); + + let (_, direct_allocations) = crate::test_support::measured_allocations(|| { + let SessionState::Ready { .. } = direct_session.update(update(1, 0x00)).unwrap() else { + panic!("direct candidate must certify"); + }; + }); + let (_, rejected_allocations) = crate::test_support::measured_allocations(|| { + let SessionState::Ready { current } = rejected_session.update(update(1, 0x00)).unwrap() + else { + panic!("later candidate must certify"); + }; + assert_eq!(current.selected_state_index(), Some(1)); + }); + assert_eq!(rejected_allocations, direct_allocations); +} + +#[test] +fn evaluation_cell_cardinality_checks_both_products_without_a_numeric_cap() { + assert_eq!( + checked_program_evaluation_cell_counts_for_test(3, 2, 4), + Some((6, 24)) + ); + assert_eq!( + checked_program_evaluation_cell_counts_for_test(usize::MAX, 2, 1), + None + ); + assert_eq!( + checked_program_evaluation_cell_counts_for_test(usize::MAX, 1, 2), + None + ); +} + +#[test] +fn every_fallible_joint_preflight_reservation_precedes_evaluator_work() { + for reservation_index in 0..3 { + let evaluator = CountingProgramWcag22Srgb8V1::default(); + let calls = evaluator.clone(); + let compiled = Program::new( + vec![Source::new(SOURCE, signal(0))], + vec![target(vec![ + candidate(FIRST, 0x55), + candidate(SECOND, 0xFF), + ])], + ObservationGroup::new(GROUP, vec![SURFACE_PORT]), + vec![], + vec![Paint::Solid { + id: PAINT, + target: TARGET, + }], + vec![Surface::Input { + id: BACKDROP, + input: SURFACE_PORT, + }], + vec![Occurrence::new( + OCCURRENCE, + PAINT, + BACKDROP, + CompositionProfile::EncodedSrgb8SourceOverV1, + appearance_context(), + )], + ConstraintSet::new( + vec![ConstraintInvocation::hard( + ConstraintId::new(1), + OCCURRENCE, + Wcag22CriterionV1::Sc143TextLargeScale, + )], + vec![], + ), + vec![OutputBinding::new(OUTPUT, PAINT)], + evaluator, + ) + .with_joint_selection(DeclaredJointSelectionV1::new(vec![ + state(FIRST), + state(SECOND), + ])) + .compile() + .unwrap(); + let mut session = compiled.instantiate(STREAM).unwrap(); + + let error = { + let _failure = fail_program_preflight_reservation_for_test(reservation_index); + match session.update(update(1, 0x00)) { + Ok(_) => panic!("injected preflight failure must abort the update"), + Err(error) => error, + } + }; + assert_eq!( + error, + SessionUpdateError::Plan(ProgramSessionEvaluationError::ResourceExhausted) + ); + assert!(calls.calls().is_empty()); + assert!(matches!(session.state(), SessionState::Waiting)); + } +} + +#[test] +fn every_fallible_fixed_preflight_reservation_precedes_evaluator_work() { + for reservation_index in 0..2 { + let evaluator = CountingProgramWcag22Srgb8V1::default(); + let calls = evaluator.clone(); + let compiled = Program::new( + vec![Source::new(SOURCE, signal(0xFF))], + vec![Target::fixed(TARGET, SOURCE)], + ObservationGroup::new(GROUP, vec![SURFACE_PORT]), + vec![], + vec![Paint::Solid { + id: PAINT, + target: TARGET, + }], + vec![Surface::Input { + id: BACKDROP, + input: SURFACE_PORT, + }], + vec![Occurrence::new( + OCCURRENCE, + PAINT, + BACKDROP, + CompositionProfile::EncodedSrgb8SourceOverV1, + appearance_context(), + )], + ConstraintSet::new( + vec![ConstraintInvocation::hard( + ConstraintId::new(1), + OCCURRENCE, + Wcag22CriterionV1::Sc143TextLargeScale, + )], + vec![], + ), + vec![OutputBinding::new(OUTPUT, PAINT)], + evaluator, + ) + .compile() + .unwrap(); + let mut session = compiled.instantiate(STREAM).unwrap(); + + let error = { + let _failure = fail_program_preflight_reservation_for_test(reservation_index); + match session.update(update(1, 0x00)) { + Ok(_) => panic!("injected fixed preflight failure must abort the update"), + Err(error) => error, + } + }; + assert_eq!( + error, + SessionUpdateError::Plan(ProgramSessionEvaluationError::ResourceExhausted) + ); + assert!(calls.calls().is_empty()); + assert!(matches!(session.state(), SessionState::Waiting)); + } +} + +#[test] +fn final_recheck_violation_is_typed_and_retains_the_previous_certificate() { + let evaluator = FinalRecheckMutantProgramEvaluatorV1::default(); + let control = evaluator.clone(); + let compiled = Program::new( + vec![Source::new(SOURCE, signal(0))], + vec![target(vec![candidate(FIRST, 0xFF)])], + ObservationGroup::new(GROUP, vec![SURFACE_PORT]), + vec![], + vec![Paint::Solid { + id: PAINT, + target: TARGET, + }], + vec![Surface::Input { + id: BACKDROP, + input: SURFACE_PORT, + }], + vec![Occurrence::new( + OCCURRENCE, + PAINT, + BACKDROP, + CompositionProfile::EncodedSrgb8SourceOverV1, + appearance_context(), + )], + ConstraintSet::new( + vec![ConstraintInvocation::hard( + ConstraintId::new(1), + OCCURRENCE, + Srgb8::new([0xFF; 3]), + )], + vec![], + ), + vec![OutputBinding::new(OUTPUT, PAINT)], + evaluator, + ) + .with_joint_selection(DeclaredJointSelectionV1::new(vec![state(FIRST)])) + .compile() + .unwrap(); + let mut session = compiled.instantiate(STREAM).unwrap(); + + let SessionState::Ready { current } = session.update(update(1, 0x00)).unwrap() else { + panic!("control revision must certify before the mutant is armed"); + }; + assert_eq!(current.outputs()[0].source_signal(), signal(0xFF)); + + control.arm(); + let error = match session.update(update(2, 0x00)) { + Ok(_) => panic!("a failing final recheck must not commit"), + Err(error) => error, + }; + assert_eq!( + error, + SessionUpdateError::Plan(ProgramSessionEvaluationError::FinalRecheckViolation { + state_index: 0, + case_index: 0, + constraint: ConstraintId::new(1), + target: OCCURRENCE, + hard_violation_count: 1, + }) + ); + let SessionState::Ready { current } = session.state() else { + panic!("the previous certificate must remain the sole committed state"); + }; + assert_eq!(current.outputs()[0].source_signal(), signal(0xFF)); +} + +#[test] +fn duplicate_physical_candidate_signal_is_typed_and_declaration_order_invariant() { + let compile = |candidates| match program( + vec![ConstraintInvocation::hard( + ConstraintId::new(1), + OCCURRENCE, + Wcag22CriterionV1::Sc143TextLargeScale, + )], + vec![], + candidates, + vec![state(FIRST), state(SECOND)], + ) + .compile() + { + Ok(_) => panic!("duplicate physical candidate signals must not compile"), + Err(error) => error, + }; + let canonical = compile(vec![candidate(FIRST, 0x66), candidate(SECOND, 0x66)]); + let permuted = compile(vec![candidate(SECOND, 0x66), candidate(FIRST, 0x66)]); + let expected = ProgramCompileError::DuplicateTargetCandidateSignal { + target: TARGET, + first: FIRST, + duplicate: SECOND, + signal: signal(0x66), + }; + assert_eq!(canonical, expected); + assert_eq!(permuted, expected); +} + +#[test] +fn duplicate_joint_tuple_is_rejected_before_runtime() { + let error = match program( + vec![ConstraintInvocation::hard( + ConstraintId::new(1), + OCCURRENCE, + Wcag22CriterionV1::Sc143TextLargeScale, + )], + vec![], + vec![candidate(FIRST, 0x66), candidate(SECOND, 0xFF)], + vec![state(FIRST), state(FIRST)], + ) + .compile() + { + Ok(_) => panic!("duplicate tuple must not compile"), + Err(error) => error, + }; + assert_eq!( + error, + ProgramCompileError::InvalidJointOrder(FiniteJointOrderErrorV1::DuplicateTuple { + first: 0, + duplicate: 1, + }) + ); +} diff --git a/crates/labcolors-core/src/program_lcs_integration_tests.rs b/crates/labcolors-core/src/program_lcs_integration_tests.rs index 3ac51463..cb09ebee 100644 --- a/crates/labcolors-core/src/program_lcs_integration_tests.rs +++ b/crates/labcolors-core/src/program_lcs_integration_tests.rs @@ -1,7 +1,6 @@ use crate::Srgb8; use crate::appearance::{ - ColorInputId, OccurrenceId, OpacityInputId, PaintId, PointOpacityOverSurfaceV1, SurfaceId, - SurfaceInputPortId, + OccurrenceId, OpacityInputId, PaintId, PointOpacityOverSurfaceV1, SurfaceId, SurfaceInputPortId, }; use crate::constraints::{ ExactSrgb8IdentityV1, ProgramPointAssessmentErrorV1, ProgramVisiblePointBindingV1, @@ -17,16 +16,18 @@ use crate::observation::{ ObservedScenarioSetInput, Revision, ScenarioId, ScenarioInput, SurfaceInputBinding, }; use crate::program_session::{ - ColorInput, CompiledProgram, CompositionProfile, ConstraintId, ConstraintInvocation, - ConstraintSet, ObservationGroup, Occurrence, OpacityInput, OutputBinding, OutputSlotId, Paint, - Program, ProgramConstraintResultV1, Surface, + CompiledProgram, CompositionProfile, ConstraintId, ConstraintInvocation, ConstraintSet, + ObservationGroup, Occurrence, OpacityInput, OutputBinding, OutputSlotId, Paint, Program, + ProgramConstraintResultV1, Source, SourceId, Surface, Target, TargetId, }; use crate::session::SessionState; use crate::spaces::cam16::FORWARD_CALLS; use crate::wcag22::{Wcag22ApplicableDecisionV1, Wcag22CriterionV1}; -const BLACK_INPUT: ColorInputId = ColorInputId::new(1); -const WHITE_INPUT: ColorInputId = ColorInputId::new(2); +const BLACK_SOURCE: SourceId = SourceId::new(1); +const WHITE_SOURCE: SourceId = SourceId::new(2); +const BLACK_TARGET: TargetId = TargetId::new(1); +const WHITE_TARGET: TargetId = TargetId::new(2); const SURFACE_PORT: SurfaceInputPortId = SurfaceInputPortId::new(3); const HALF: OpacityInputId = OpacityInputId::new(4); @@ -41,7 +42,6 @@ const AVERAGE_CONSTRAINT: ConstraintId = ConstraintId::new(40); const DIM_CONSTRAINT: ConstraintId = ConstraintId::new(41); const BLACK_OUTPUT: OutputSlotId = OutputSlotId::new(50); -const WHITE_OUTPUT: OutputSlotId = OutputSlotId::new(51); const GROUP: ObservationGroupId = ObservationGroupId::new(60); const STREAM_A: ObservationStreamId = ObservationStreamId::new(70); const STREAM_B: ObservationStreamId = ObservationStreamId::new(71); @@ -98,14 +98,18 @@ fn compiled_program( opacity: f64, permuted: bool, ) -> CompiledProgram { - let mut colors = vec![ - ColorInput::new(BLACK_INPUT, signal([0; 3])), - ColorInput::new(WHITE_INPUT, signal([0xFF; 3])), + let mut sources = vec![ + Source::new(BLACK_SOURCE, signal([0; 3])), + Source::new(WHITE_SOURCE, signal([0xFF; 3])), + ]; + let mut targets = vec![ + Target::fixed(BLACK_TARGET, BLACK_SOURCE), + Target::fixed(WHITE_TARGET, WHITE_SOURCE), ]; let mut paints = vec![ Paint::Solid { id: BLACK_SOLID, - color: BLACK_INPUT, + target: BLACK_TARGET, }, Paint::Opacity { id: TRANSLUCENT_BLACK, @@ -114,7 +118,7 @@ fn compiled_program( }, Paint::Solid { id: WHITE_SOLID, - color: WHITE_INPUT, + target: WHITE_TARGET, }, ]; let mut occurrences = declarations @@ -135,13 +139,11 @@ fn compiled_program( ConstraintInvocation::hard(*constraint, *occurrence, expected_visible) }) .collect::>(); - let mut outputs = vec![ - OutputBinding::new(BLACK_OUTPUT, TRANSLUCENT_BLACK), - OutputBinding::new(WHITE_OUTPUT, WHITE_SOLID), - ]; + let mut outputs = vec![OutputBinding::new(BLACK_OUTPUT, TRANSLUCENT_BLACK)]; if permuted { - colors.reverse(); + sources.reverse(); + targets.reverse(); paints.reverse(); occurrences.reverse(); hard.reverse(); @@ -149,7 +151,8 @@ fn compiled_program( } Program::new( - colors, + sources, + targets, ObservationGroup::new(GROUP, vec![SURFACE_PORT]), vec![OpacityInput::new(HALF, opacity)], paints, @@ -169,15 +172,19 @@ fn compiled_program( fn compiled_wcag_program(opacity: f64) -> CompiledProgram { Program::new( vec![ - ColorInput::new(BLACK_INPUT, signal([0; 3])), - ColorInput::new(WHITE_INPUT, signal([0xFF; 3])), + Source::new(BLACK_SOURCE, signal([0; 3])), + Source::new(WHITE_SOURCE, signal([0xFF; 3])), + ], + vec![ + Target::fixed(BLACK_TARGET, BLACK_SOURCE), + Target::fixed(WHITE_TARGET, WHITE_SOURCE), ], ObservationGroup::new(GROUP, vec![SURFACE_PORT]), vec![OpacityInput::new(HALF, opacity)], vec![ Paint::Solid { id: BLACK_SOLID, - color: BLACK_INPUT, + target: BLACK_TARGET, }, Paint::Opacity { id: TRANSLUCENT_BLACK, @@ -186,7 +193,7 @@ fn compiled_wcag_program(opacity: f64) -> CompiledProgram { }, Paint::Solid { id: WHITE_SOLID, - color: WHITE_INPUT, + target: WHITE_TARGET, }, ], vec![Surface::Input { @@ -217,13 +224,14 @@ fn compiled_wcag_program(opacity: f64) -> CompiledProgram { fn compiled_duplicate_constraint_program() -> CompiledProgram { Program::new( - vec![ColorInput::new(BLACK_INPUT, signal([0; 3]))], + vec![Source::new(BLACK_SOURCE, signal([0; 3]))], + vec![Target::fixed(BLACK_TARGET, BLACK_SOURCE)], ObservationGroup::new(GROUP, vec![SURFACE_PORT]), vec![OpacityInput::new(HALF, 0.5)], vec![ Paint::Solid { id: BLACK_SOLID, - color: BLACK_INPUT, + target: BLACK_TARGET, }, Paint::Opacity { id: TRANSLUCENT_BLACK, @@ -411,7 +419,7 @@ fn hard_violation_retains_modeled_lcs_and_commits_no_current_outputs() { assert_binding_matches_modeled(*evidence.binding(), modeled); assert_eq!(*evidence.measurement().value(), Srgb8::new([0x80; 3])); assert_eq!(*evidence.invocation(), Srgb8::new([0x7F; 3])); - // `ProgramViolationV1` intentionally exposes only `report()`: current + // `ProgramConflictV1` intentionally exposes only `report()`: current // outputs exist exclusively on the `ProgramVerifiedV1` Ready branch. } diff --git a/crates/labcolors-core/src/program_session.rs b/crates/labcolors-core/src/program_session.rs index 5441e440..1f713c9b 100644 --- a/crates/labcolors-core/src/program_session.rs +++ b/crates/labcolors-core/src/program_session.rs @@ -16,9 +16,9 @@ use std::rc::Rc; use crate::Srgb8; use crate::appearance::{ AdmittedAppearanceBindings, AppearanceBindings, AppearanceGraphSpec, AppearanceWorkspace, - BindingError, ColorInputId, CompileError, CompiledAppearanceGraph, CompiledOccurrenceSlotV1, - CompiledPaintSlotV1, EncodedPointPaintV1, OccurrenceId, OccurrenceSpec, OpacityInputId, - PaintId, PaintSpec, SurfaceId, SurfaceInputPortId, SurfaceSpec, + BindingError, ColorInputId, CompileError, CompiledAppearanceGraph, CompiledColorInputSlotV1, + CompiledOccurrenceSlotV1, CompiledPaintSlotV1, EncodedPointPaintV1, OccurrenceId, + OccurrenceSpec, OpacityInputId, PaintId, PaintSpec, SurfaceId, SurfaceInputPortId, SurfaceSpec, }; use crate::composition::CompositionProfileV1; use crate::constraints::{ @@ -26,6 +26,10 @@ use crate::constraints::{ ProgramPointTargetV1, ProgramVisiblePointBindingV1, ProgramVisiblePointPassEvidence, ProgramVisiblePointViolationEvidence, assess_program_point_hard, }; +use crate::joint::{ + AdmittedFiniteJointOrderV1, FiniteDomainOrdinalV1, FiniteJointOrderErrorV1, + admit_finite_joint_order_v1, +}; use crate::lcs_occurrence::{ AppearanceContextId, ColorSignal, ModeledLcsOccurrenceFormationErrorV1, ModeledLcsOccurrenceV1, }; @@ -39,24 +43,189 @@ use crate::session::{ private as session_private, }; -/// One immutable encoded colour binding owned by a [`Program`]. +/// Opaque identity of one immutable authored colour source. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct SourceId(u32); + +impl SourceId { + pub const fn new(value: u32) -> Self { + Self(value) + } + + pub const fn value(self) -> u32 { + self.0 + } +} + +/// One immutable encoded source owned by a [`Program`]. Sources carry data, +/// never solver freedom and never appear directly in Paint topology. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct ColorInput { - id: ColorInputId, - value: ColorSignal, +pub struct Source { + id: SourceId, + signal: ColorSignal, } -impl ColorInput { - pub const fn new(id: ColorInputId, value: ColorSignal) -> Self { - Self { id, value } +impl Source { + pub const fn new(id: SourceId, signal: ColorSignal) -> Self { + Self { id, signal } } - pub const fn id(self) -> ColorInputId { + pub const fn id(self) -> SourceId { self.id } - pub const fn value(self) -> ColorSignal { - self.value + pub const fn signal(self) -> ColorSignal { + self.signal + } +} + +/// Opaque identity of one jointly selected finite target. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct TargetId(u32); + +impl TargetId { + pub const fn new(value: u32) -> Self { + Self(value) + } + + pub const fn value(self) -> u32 { + self.0 + } +} + +/// Opaque identity of one candidate inside a finite target domain. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct TargetCandidateId(u32); + +impl TargetCandidateId { + pub const fn new(value: u32) -> Self { + Self(value) + } + + pub const fn value(self) -> u32 { + self.0 + } +} + +/// One candidate signal in a finite target domain. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct TargetCandidateV1 { + id: TargetCandidateId, + signal: ColorSignal, +} + +impl TargetCandidateV1 { + pub const fn new(id: TargetCandidateId, signal: ColorSignal) -> Self { + Self { id, signal } + } + + pub const fn id(self) -> TargetCandidateId { + self.id + } + + pub const fn signal(self) -> ColorSignal { + self.signal + } +} + +/// Closed authored freedom of one Target. Fixed targets use their Source +/// signal exactly; finite targets can select only from the explicit domain. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TargetDomainV1 { + Fixed, + Finite(Vec), +} + +/// A Paint-addressable target distinct from both source data and appearance +/// storage. Only finite targets participate in declared joint selection. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Target { + id: TargetId, + source: SourceId, + domain: TargetDomainV1, +} + +impl Target { + pub const fn new(id: TargetId, source: SourceId, domain: TargetDomainV1) -> Self { + Self { id, source, domain } + } + + pub const fn fixed(id: TargetId, source: SourceId) -> Self { + Self::new(id, source, TargetDomainV1::Fixed) + } + + pub const fn finite( + id: TargetId, + source: SourceId, + candidates: Vec, + ) -> Self { + Self::new(id, source, TargetDomainV1::Finite(candidates)) + } + + pub const fn id(&self) -> TargetId { + self.id + } + + pub const fn source(&self) -> SourceId { + self.source + } + + pub const fn domain(&self) -> &TargetDomainV1 { + &self.domain + } +} + +/// One typed target/candidate assignment inside a declared joint state. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct TargetCandidateChoiceV1 { + target: TargetId, + candidate: TargetCandidateId, +} + +impl TargetCandidateChoiceV1 { + pub const fn new(target: TargetId, candidate: TargetCandidateId) -> Self { + Self { target, candidate } + } + + pub const fn target(self) -> TargetId { + self.target + } + + pub const fn candidate(self) -> TargetCandidateId { + self.candidate + } +} + +/// One complete joint candidate state. Choices are keyed, so authored choice +/// order has no physical meaning. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct JointCandidateStateV1 { + choices: Vec, +} + +impl JointCandidateStateV1 { + pub const fn new(choices: Vec) -> Self { + Self { choices } + } + + pub fn choices(&self) -> &[TargetCandidateChoiceV1] { + &self.choices + } +} + +/// Explicit total order over every state in the finite product domain. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DeclaredJointSelectionV1 { + states: Vec, +} + +impl DeclaredJointSelectionV1 { + pub const fn new(states: Vec) -> Self { + Self { states } + } + + pub fn states(&self) -> &[JointCandidateStateV1] { + &self.states } } @@ -86,7 +255,7 @@ impl OpacityInput { pub enum Paint { Solid { id: PaintId, - color: ColorInputId, + target: TargetId, }, Opacity { id: PaintId, @@ -330,7 +499,9 @@ where Evaluation: ProgramPointEvaluatorV1, ProgramPointInvocation: Copy, { - colors: Vec, + sources: Vec, + targets: Vec, + joint_selection: Option, observation_group: ObservationGroup, opacities: Vec, paints: Vec, @@ -348,7 +519,8 @@ where { #[allow(clippy::too_many_arguments)] pub fn new( - colors: Vec, + sources: Vec, + targets: Vec, observation_group: ObservationGroup, opacities: Vec, paints: Vec, @@ -359,7 +531,9 @@ where evaluator: Evaluation, ) -> Self { Self { - colors, + sources, + targets, + joint_selection: None, observation_group, opacities, paints, @@ -371,6 +545,14 @@ where } } + /// Attach the complete explicit order for all finite Target domains. + /// No order is synthesized from target IDs, candidate bytes, or + /// declaration position. + pub fn with_joint_selection(mut self, selection: DeclaredJointSelectionV1) -> Self { + self.joint_selection = Some(selection); + self + } + pub fn compile(self) -> Result, ProgramCompileError> { prepare_program(self).map(|epoch| CompiledProgram { epoch: Rc::new(epoch), @@ -381,8 +563,15 @@ where /// Atomic compile failure. No executable partial graph escapes. #[derive(Debug, PartialEq, Eq)] pub enum ProgramCompileError { - DuplicateColorInput { - input: ColorInputId, + DuplicateSource { + source: SourceId, + }, + DuplicateTarget { + target: TargetId, + }, + MissingTargetSource { + target: TargetId, + source: SourceId, }, DuplicateOpacityInput { input: OpacityInputId, @@ -399,9 +588,9 @@ pub enum ProgramCompileError { DuplicateOccurrence { occurrence: OccurrenceId, }, - MissingPaintColorInput { + MissingPaintTarget { paint: PaintId, - input: ColorInputId, + target: TargetId, }, MissingPaintSource { paint: PaintId, @@ -437,6 +626,47 @@ pub enum ProgramCompileError { OpacityOutOfDomain { input: OpacityInputId, }, + EmptyTargetDomain { + target: TargetId, + }, + DuplicateTargetCandidate { + target: TargetId, + candidate: TargetCandidateId, + }, + DuplicateTargetCandidateSignal { + target: TargetId, + first: TargetCandidateId, + duplicate: TargetCandidateId, + signal: ColorSignal, + }, + UnconstrainedTarget { + target: TargetId, + }, + DisconnectedFiniteTargets, + UnassessedOutput { + output: OutputSlotId, + paint: PaintId, + }, + MissingJointSelection, + JointSelectionWithoutTargets, + JointStateDuplicateTarget { + state: usize, + target: TargetId, + }, + JointStateMissingTarget { + state: usize, + target: TargetId, + }, + JointStateUnknownTarget { + state: usize, + target: TargetId, + }, + JointStateUnknownCandidate { + state: usize, + target: TargetId, + candidate: TargetCandidateId, + }, + InvalidJointOrder(FiniteJointOrderErrorV1), EmptyObservationGroup { group: ObservationGroupId, }, @@ -495,6 +725,15 @@ struct CompiledObservationGroupV1 { schema: CanonicalObservationSchemaV1, } +struct CompiledFiniteTargetV1 { + binding: CompiledColorInputSlotV1, + candidates: Box<[ColorSignal]>, +} + +struct CompiledJointSelectionV1 { + order: AdmittedFiniteJointOrderV1, +} + struct ProgramEpochV1 where Evaluation: ProgramPointEvaluatorV1, @@ -507,6 +746,8 @@ where occurrence_contexts: Box<[CompiledOccurrenceContextV1]>, constraints: Box<[CompiledPointConstraint>]>, outputs: Box<[CompiledOutputBinding]>, + finite_targets: Box<[CompiledFiniteTargetV1]>, + joint_selection: Option, } /// Fully validated immutable Program, not yet attached to runtime. @@ -627,6 +868,7 @@ pub struct ProgramConstraintCellV1 where Evaluation: ProgramPointEvaluatorV1, { + candidate_state_index: usize, case_index: usize, constraint: ConstraintId, target: OccurrenceId, @@ -638,6 +880,10 @@ impl ProgramConstraintCellV1 where Evaluation: ProgramPointEvaluatorV1, { + pub const fn candidate_state_index(&self) -> usize { + self.candidate_state_index + } + pub const fn case_index(&self) -> usize { self.case_index } @@ -713,6 +959,7 @@ where { report: ProgramReportV1, outputs: Vec, + selected_state_index: Option, } impl session_private::EvidenceSealed for ProgramVerifiedV1 where @@ -740,23 +987,30 @@ where pub fn outputs(&self) -> &[ProgramOutputV1] { &self.outputs } + + /// Index inside the authored total order. `None` means this Program has no + /// finite targets and therefore performed validation only. + pub const fn selected_state_index(&self) -> Option { + self.selected_state_index + } } -/// Complete report containing at least one hard violation. Outputs are absent -/// by construction and therefore cannot be mistaken for committed Paints. -pub struct ProgramViolationV1 +/// Exhaustive hard-infeasibility report. Outputs are absent by construction +/// and therefore cannot be mistaken for committed Paints. +pub struct ProgramConflictV1 where Evaluation: ProgramPointEvaluatorV1, { report: ProgramReportV1, + considered_state_count: usize, } -impl session_private::EvidenceSealed for ProgramViolationV1 where +impl session_private::EvidenceSealed for ProgramConflictV1 where Evaluation: ProgramPointEvaluatorV1 { } -impl SessionEvidenceV1 for ProgramViolationV1 +impl SessionEvidenceV1 for ProgramConflictV1 where Evaluation: ProgramPointEvaluatorV1, { @@ -765,13 +1019,17 @@ where } } -impl ProgramViolationV1 +impl ProgramConflictV1 where Evaluation: ProgramPointEvaluatorV1, { pub const fn report(&self) -> &ProgramReportV1 { &self.report } + + pub const fn considered_state_count(&self) -> usize { + self.considered_state_count + } } /// Program execution failure before Session commit. @@ -800,6 +1058,13 @@ pub enum ProgramSessionEvaluationError { first_case: usize, actual_case: usize, }, + FinalRecheckViolation { + state_index: usize, + case_index: usize, + constraint: ConstraintId, + target: OccurrenceId, + hard_violation_count: usize, + }, InternalInvariant, } @@ -807,10 +1072,157 @@ type ProgramEvaluatorError = >::Error; type ProgramSessionEvaluationResult = Result< - SessionDecision, ProgramViolationV1>, + SessionDecision, ProgramConflictV1>, ProgramSessionEvaluationError>, >; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct ProgramEvaluationCellCountsV1 { + selected: usize, + exhaustive_conflict: usize, +} + +fn checked_program_evaluation_cell_counts( + physical_case_count: usize, + constraint_count: usize, + state_count: usize, +) -> Option { + let selected = physical_case_count.checked_mul(constraint_count)?; + let exhaustive_conflict = selected.checked_mul(state_count)?; + Some(ProgramEvaluationCellCountsV1 { + selected, + exhaustive_conflict, + }) +} + +#[cfg(test)] +pub(crate) fn checked_program_evaluation_cell_counts_for_test( + physical_case_count: usize, + constraint_count: usize, + state_count: usize, +) -> Option<(usize, usize)> { + checked_program_evaluation_cell_counts(physical_case_count, constraint_count, state_count) + .map(|counts| (counts.selected, counts.exhaustive_conflict)) +} + +#[cfg(test)] +std::thread_local! { + static PROGRAM_PREFLIGHT_FAILURE_AT: std::cell::Cell> = const { + std::cell::Cell::new(None) + }; + static PROGRAM_PREFLIGHT_FAILURE_ACTIVE: std::cell::Cell = const { + std::cell::Cell::new(false) + }; +} + +#[cfg(test)] +pub(crate) struct ProgramPreflightFailureGuardV1 { + _not_send: PhantomData>, +} + +#[cfg(test)] +impl Drop for ProgramPreflightFailureGuardV1 { + fn drop(&mut self) { + PROGRAM_PREFLIGHT_FAILURE_AT.with(|failure| failure.set(None)); + PROGRAM_PREFLIGHT_FAILURE_ACTIVE.with(|active| active.set(false)); + } +} + +#[cfg(test)] +pub(crate) fn fail_program_preflight_reservation_for_test( + reservation_index: usize, +) -> ProgramPreflightFailureGuardV1 { + PROGRAM_PREFLIGHT_FAILURE_ACTIVE.with(|active| { + assert!( + !active.replace(true), + "a preflight failure is already armed" + ); + }); + PROGRAM_PREFLIGHT_FAILURE_AT.with(|failure| failure.set(Some(reservation_index))); + ProgramPreflightFailureGuardV1 { + _not_send: PhantomData, + } +} + +#[cfg(test)] +fn injected_program_preflight_failure() -> bool { + PROGRAM_PREFLIGHT_FAILURE_AT.with(|failure| match failure.get() { + Some(0) => { + failure.set(None); + true + } + Some(remaining) => { + failure.set(Some(remaining - 1)); + false + } + None => false, + }) +} + +fn try_reserve_program_evaluation_buffer( + buffer: &mut Vec, + capacity: usize, +) -> Result<(), ()> { + #[cfg(test)] + if injected_program_preflight_failure() { + return Err(()); + } + buffer.try_reserve_exact(capacity).map_err(|_| ()) +} + +struct PreparedProgramEvaluationBuffersV1 +where + Evaluation: ProgramPointEvaluatorV1, +{ + selected_cells: Vec>, + conflict_cells: Vec>, + outputs: Vec, + counts: ProgramEvaluationCellCountsV1, +} + +fn prepare_program_evaluation_buffers( + epoch: &ProgramEpochV1, + observation: &RevisionBoundObservationV1, + joint_state_count: Option, +) -> Result< + PreparedProgramEvaluationBuffersV1, + ProgramSessionEvaluationError>, +> +where + Evaluation: ProgramPointEvaluatorV1, + ProgramPointInvocation: Copy, +{ + let state_count = joint_state_count.unwrap_or(1); + if state_count == 0 { + return Err(ProgramSessionEvaluationError::InternalInvariant); + } + let counts = checked_program_evaluation_cell_counts( + observation.physical_case_count(), + epoch.constraints.len(), + state_count, + ) + .ok_or(ProgramSessionEvaluationError::ResourceExhausted)?; + + let mut selected_cells = Vec::new(); + try_reserve_program_evaluation_buffer(&mut selected_cells, counts.selected) + .map_err(|()| ProgramSessionEvaluationError::ResourceExhausted)?; + let mut conflict_cells = Vec::new(); + if joint_state_count.is_some() { + try_reserve_program_evaluation_buffer(&mut conflict_cells, counts.exhaustive_conflict) + .map_err(|()| ProgramSessionEvaluationError::ResourceExhausted)?; + } + let mut outputs = Vec::new(); + try_reserve_program_evaluation_buffer(&mut outputs, epoch.outputs.len()) + .map_err(|()| ProgramSessionEvaluationError::ResourceExhausted)?; + + Ok(PreparedProgramEvaluationBuffersV1 { + selected_cells, + conflict_cells, + outputs, + counts, + }) +} + /// Per-Session mutable execution state backed by one strong immutable epoch. pub struct ProgramSessionPlan where @@ -836,7 +1248,7 @@ where ProgramPointInvocation: Copy, { type Verified = ProgramVerifiedV1; - type Violation = ProgramViolationV1; + type Violation = ProgramConflictV1; type Error = ProgramSessionEvaluationError>; fn observation_schema(&self) -> &CanonicalObservationSchemaV1 { @@ -856,6 +1268,171 @@ fn evaluate_program_session( plan: &mut ProgramSessionPlan, observation: RevisionBoundObservationV1, ) -> ProgramSessionEvaluationResult +where + Evaluation: ProgramPointEvaluatorV1, + ProgramPointInvocation: Copy, +{ + let epoch = Rc::clone(&plan.epoch); + let Some(selection) = &epoch.joint_selection else { + let mut buffers = prepare_program_evaluation_buffers(&epoch, &observation, None)?; + return collect_program_candidate_into( + plan, + observation, + None, + 1, + std::mem::take(&mut buffers.selected_cells), + std::mem::take(&mut buffers.outputs), + buffers.counts.selected, + ); + }; + + let state_count = selection.order.tuples().len(); + let mut buffers = prepare_program_evaluation_buffers(&epoch, &observation, Some(state_count))?; + for (state_index, tuple) in selection.order.tuples().enumerate() { + apply_joint_candidate(plan, &epoch.finite_targets, tuple)?; + if !scan_program_candidate(plan, &observation, state_index, None, None)? { + // A selected tuple is never certified from its allocation-free + // search pass. Re-apply and collect fresh terminal evidence. + apply_joint_candidate(plan, &epoch.finite_targets, tuple)?; + match collect_program_candidate_into( + plan, + observation.clone(), + Some(state_index), + state_index + 1, + std::mem::take(&mut buffers.selected_cells), + std::mem::take(&mut buffers.outputs), + buffers.counts.selected, + )? { + SessionDecision::Verified(verified) => { + return Ok(SessionDecision::Verified(verified)); + } + SessionDecision::Violation(conflict) => { + let first = conflict + .report + .cells + .iter() + .find(|cell| cell.is_hard() && cell.result().is_violation()) + .ok_or(ProgramSessionEvaluationError::InternalInvariant)?; + let hard_violation_count = conflict + .report + .cells + .iter() + .filter(|cell| cell.is_hard() && cell.result().is_violation()) + .count(); + return Err(ProgramSessionEvaluationError::FinalRecheckViolation { + state_index, + case_index: first.case_index, + constraint: first.constraint, + target: first.target, + hard_violation_count, + }); + } + } + } + } + + for (state_index, tuple) in selection.order.tuples().enumerate() { + apply_joint_candidate(plan, &epoch.finite_targets, tuple)?; + if !scan_program_candidate( + plan, + &observation, + state_index, + Some(&mut buffers.conflict_cells), + None, + )? { + return Err(ProgramSessionEvaluationError::InternalInvariant); + } + } + if buffers.conflict_cells.len() != buffers.counts.exhaustive_conflict { + return Err(ProgramSessionEvaluationError::InternalInvariant); + } + + Ok(SessionDecision::Violation(ProgramConflictV1 { + report: ProgramReportV1 { + observation, + cells: buffers.conflict_cells, + }, + considered_state_count: state_count, + })) +} + +fn apply_joint_candidate( + plan: &mut ProgramSessionPlan, + targets: &[CompiledFiniteTargetV1], + tuple: &[FiniteDomainOrdinalV1], +) -> Result<(), ProgramSessionEvaluationError>> +where + Evaluation: ProgramPointEvaluatorV1, + ProgramPointInvocation: Copy, +{ + if targets.len() != tuple.len() { + return Err(ProgramSessionEvaluationError::InternalInvariant); + } + for (target, ordinal) in targets.iter().zip(tuple) { + let candidate = target + .candidates + .get(ordinal.index()) + .ok_or(ProgramSessionEvaluationError::InternalInvariant)?; + plan.bindings + .overwrite_color_at(target.binding, candidate.srgb8()) + .map_err(map_program_execution_binding_error)?; + } + Ok(()) +} + +fn collect_program_candidate_into( + plan: &mut ProgramSessionPlan, + observation: RevisionBoundObservationV1, + selected_state_index: Option, + considered_state_count: usize, + mut cells: Vec>, + mut outputs: Vec, + expected_cell_count: usize, +) -> ProgramSessionEvaluationResult +where + Evaluation: ProgramPointEvaluatorV1, + ProgramPointInvocation: Copy, +{ + if !cells.is_empty() + || cells.capacity() < expected_cell_count + || !outputs.is_empty() + || outputs.capacity() < plan.epoch.outputs.len() + { + return Err(ProgramSessionEvaluationError::InternalInvariant); + } + let candidate_state_index = selected_state_index.unwrap_or(0); + let has_hard_violation = scan_program_candidate( + plan, + &observation, + candidate_state_index, + Some(&mut cells), + Some(&mut outputs), + )?; + if cells.len() != expected_cell_count { + return Err(ProgramSessionEvaluationError::InternalInvariant); + } + let report = ProgramReportV1 { observation, cells }; + if has_hard_violation { + Ok(SessionDecision::Violation(ProgramConflictV1 { + report, + considered_state_count, + })) + } else { + Ok(SessionDecision::Verified(ProgramVerifiedV1 { + report, + outputs, + selected_state_index, + })) + } +} + +fn scan_program_candidate( + plan: &mut ProgramSessionPlan, + observation: &RevisionBoundObservationV1, + candidate_state_index: usize, + mut cells: Option<&mut Vec>>, + mut outputs: Option<&mut Vec>, +) -> Result>> where Evaluation: ProgramPointEvaluatorV1, ProgramPointInvocation: Copy, @@ -870,18 +1447,6 @@ where } let case_count = observation.physical_case_count(); - let cell_count = case_count - .checked_mul(epoch.constraints.len()) - .ok_or(ProgramSessionEvaluationError::ResourceExhausted)?; - let mut cells = Vec::new(); - cells - .try_reserve_exact(cell_count) - .map_err(|_| ProgramSessionEvaluationError::ResourceExhausted)?; - let mut outputs = Vec::new(); - outputs - .try_reserve_exact(epoch.outputs.len()) - .map_err(|_| ProgramSessionEvaluationError::ResourceExhausted)?; - let mut has_hard_violation = false; let mut output_mismatch = None; for case_index in 0..case_count { @@ -988,51 +1553,51 @@ where }; debug_assert_eq!(result.binding().physical(), source.visible_point_binding()); debug_assert_eq!(result.binding().modeled_lcs(), modeled_lcs_occurrence); - cells.push(ProgramConstraintCellV1 { - case_index, - constraint: constraint.id, - target: constraint.target_id, - mode: constraint.mode, - result, - }); + if let Some(cells) = cells.as_deref_mut() { + cells.push(ProgramConstraintCellV1 { + candidate_state_index, + case_index, + constraint: constraint.id, + target: constraint.target_id, + mode: constraint.mode, + result, + }); + } } - for (output_index, output) in epoch.outputs.iter().enumerate() { - let paint = evaluation - .paint_at(output.paint) - .copied() - .ok_or(ProgramSessionEvaluationError::InternalInvariant)?; - if paint.id() != output.paint_id { - return Err(ProgramSessionEvaluationError::InternalInvariant); - } - let routed = ProgramOutputV1 { - output: output.output, - paint, - }; - if case_index == 0 { - outputs.push(routed); - } else if outputs.get(output_index).copied() != Some(routed) - && output_mismatch.is_none() - { - output_mismatch = Some(ProgramSessionEvaluationError::OutputVariesAcrossCases { + if let Some(outputs) = outputs.as_deref_mut() { + for (output_index, output) in epoch.outputs.iter().enumerate() { + let paint = evaluation + .paint_at(output.paint) + .copied() + .ok_or(ProgramSessionEvaluationError::InternalInvariant)?; + if paint.id() != output.paint_id { + return Err(ProgramSessionEvaluationError::InternalInvariant); + } + let routed = ProgramOutputV1 { output: output.output, - first_case: 0, - actual_case: case_index, - }); + paint, + }; + if case_index == 0 { + outputs.push(routed); + } else if outputs.get(output_index).copied() != Some(routed) + && output_mismatch.is_none() + { + output_mismatch = + Some(ProgramSessionEvaluationError::OutputVariesAcrossCases { + output: output.output, + first_case: 0, + actual_case: case_index, + }); + } } } } - let report = ProgramReportV1 { observation, cells }; if let Some(error) = output_mismatch { Err(error) - } else if has_hard_violation { - Ok(SessionDecision::Violation(ProgramViolationV1 { report })) } else { - Ok(SessionDecision::Verified(ProgramVerifiedV1 { - report, - outputs, - })) + Ok(has_hard_violation) } } @@ -1046,7 +1611,7 @@ fn map_program_execution_binding_error( } fn prepare_program( - program: Program, + mut program: Program, ) -> Result, ProgramCompileError> where Evaluation: ProgramPointEvaluatorV1, @@ -1071,10 +1636,13 @@ where .constraints .checked_len() .ok_or(ProgramCompileError::ResourceExhausted)?; + canonicalize_sources_and_targets(&mut program)?; - let graph = lower_graph(&program).compile().map_err(map_compile_error)?; + let graph = lower_graph(&program)? + .compile() + .map_err(map_compile_error)?; let binding_template = graph - .admit_bindings(&lower_bindings(&program)) + .admit_bindings(&lower_bindings(&program)?) .map_err(map_binding_compile_error)?; let mut surface_input_ports = Vec::new(); @@ -1092,6 +1660,9 @@ where let observation_schema = canonicalize_observation_schema(surface_input_ports) .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 all_occurrence_contexts = compile_occurrence_contexts(&graph, &program.occurrences)?; let mut constraints = compile_constraints::(&graph, &all_occurrence_contexts, program.constraints)?; @@ -1109,9 +1680,424 @@ where occurrence_contexts, constraints, outputs, + finite_targets, + joint_selection, }) } +fn canonicalize_sources_and_targets( + program: &mut Program, +) -> Result<(), ProgramCompileError> +where + Evaluation: ProgramPointEvaluatorV1, + ProgramPointInvocation: Copy, +{ + program.sources.sort_unstable_by_key(|source| source.id); + if let Some(source) = program + .sources + .windows(2) + .find(|pair| pair[0].id == pair[1].id) + .map(|pair| pair[0].id) + { + return Err(ProgramCompileError::DuplicateSource { source }); + } + + program.targets.sort_unstable_by_key(|target| target.id); + if let Some(target) = program + .targets + .windows(2) + .find(|pair| pair[0].id == pair[1].id) + .map(|pair| pair[0].id) + { + return Err(ProgramCompileError::DuplicateTarget { target }); + } + for target in &program.targets { + if program + .sources + .binary_search_by_key(&target.source, |source| source.id) + .is_err() + { + return Err(ProgramCompileError::MissingTargetSource { + target: target.id, + source: target.source, + }); + } + } + for paint in &program.paints { + if let Paint::Solid { id, target } = *paint { + if program + .targets + .binary_search_by_key(&target, |candidate| candidate.id) + .is_err() + { + return Err(ProgramCompileError::MissingPaintTarget { paint: id, target }); + } + } + } + Ok(()) +} + +#[derive(Debug, Clone, Copy)] +enum IndexedPaintDependencyV1 { + Target(usize), + Paint(usize), +} + +#[derive(Debug, Clone, Copy)] +enum IndexedDependencyNodeV1 { + Paint(usize), + Surface(usize), + Occurrence(usize), +} + +struct IndexedProgramDependenciesV1 { + paint_ids: Vec<(PaintId, usize)>, + occurrence_ids: Vec<(OccurrenceId, usize)>, + paint_dependencies: Vec, + surface_occurrences: Vec>, + occurrence_paints: Vec, + occurrence_surfaces: Vec, +} + +impl IndexedProgramDependenciesV1 { + fn paint(&self, id: PaintId) -> Option { + self.paint_ids + .binary_search_by_key(&id, |(candidate, _)| *candidate) + .ok() + .map(|index| self.paint_ids[index].1) + } + + fn occurrence(&self, id: OccurrenceId) -> Option { + self.occurrence_ids + .binary_search_by_key(&id, |(candidate, _)| *candidate) + .ok() + .map(|index| self.occurrence_ids[index].1) + } +} + +fn index_program_dependencies( + program: &Program, +) -> Result +where + Evaluation: ProgramPointEvaluatorV1, + ProgramPointInvocation: Copy, +{ + let mut paint_ids = Vec::new(); + paint_ids + .try_reserve_exact(program.paints.len()) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + paint_ids.extend(program.paints.iter().enumerate().map(|(index, paint)| { + let id = match *paint { + Paint::Solid { id, .. } | Paint::Opacity { id, .. } => id, + }; + (id, index) + })); + paint_ids.sort_unstable_by_key(|(id, _)| *id); + + let mut surface_ids = Vec::new(); + surface_ids + .try_reserve_exact(program.surfaces.len()) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + surface_ids.extend(program.surfaces.iter().enumerate().map(|(index, surface)| { + let id = match *surface { + Surface::Input { id, .. } | Surface::FromOccurrence { id, .. } => id, + }; + (id, index) + })); + surface_ids.sort_unstable_by_key(|(id, _)| *id); + + let mut occurrence_ids = Vec::new(); + occurrence_ids + .try_reserve_exact(program.occurrences.len()) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + occurrence_ids.extend( + program + .occurrences + .iter() + .enumerate() + .map(|(index, occurrence)| (occurrence.id, index)), + ); + occurrence_ids.sort_unstable_by_key(|(id, _)| *id); + + let paint_ordinal = |id: PaintId| { + paint_ids + .binary_search_by_key(&id, |(candidate, _)| *candidate) + .ok() + .map(|index| paint_ids[index].1) + .ok_or(ProgramCompileError::InternalInvariant) + }; + let surface_ordinal = |id: SurfaceId| { + surface_ids + .binary_search_by_key(&id, |(candidate, _)| *candidate) + .ok() + .map(|index| surface_ids[index].1) + .ok_or(ProgramCompileError::InternalInvariant) + }; + let occurrence_ordinal = |id: OccurrenceId| { + occurrence_ids + .binary_search_by_key(&id, |(candidate, _)| *candidate) + .ok() + .map(|index| occurrence_ids[index].1) + .ok_or(ProgramCompileError::InternalInvariant) + }; + + let mut paint_dependencies = Vec::new(); + paint_dependencies + .try_reserve_exact(program.paints.len()) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + for paint in &program.paints { + paint_dependencies.push(match *paint { + Paint::Solid { target, .. } => IndexedPaintDependencyV1::Target( + program + .targets + .binary_search_by_key(&target, |candidate| candidate.id) + .map_err(|_| ProgramCompileError::InternalInvariant)?, + ), + Paint::Opacity { source, .. } => { + IndexedPaintDependencyV1::Paint(paint_ordinal(source)?) + } + }); + } + let mut surface_occurrences = Vec::new(); + surface_occurrences + .try_reserve_exact(program.surfaces.len()) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + for surface in &program.surfaces { + surface_occurrences.push(match *surface { + Surface::Input { .. } => None, + Surface::FromOccurrence { occurrence, .. } => Some(occurrence_ordinal(occurrence)?), + }); + } + let mut occurrence_paints = Vec::new(); + let mut occurrence_surfaces = Vec::new(); + occurrence_paints + .try_reserve_exact(program.occurrences.len()) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + occurrence_surfaces + .try_reserve_exact(program.occurrences.len()) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + for occurrence in &program.occurrences { + occurrence_paints.push(paint_ordinal(occurrence.subject)?); + occurrence_surfaces.push(surface_ordinal(occurrence.against)?); + } + Ok(IndexedProgramDependenciesV1 { + paint_ids, + occurrence_ids, + paint_dependencies, + surface_occurrences, + occurrence_paints, + occurrence_surfaces, + }) +} + +struct ProgramDependencyScratchV1 { + targets: Vec, + paints: Vec, + surfaces: Vec, + occurrences: Vec, + queue: Vec, +} + +impl ProgramDependencyScratchV1 { + fn new(program: &Program) -> Result + where + Evaluation: ProgramPointEvaluatorV1, + ProgramPointInvocation: Copy, + { + let node_count = program + .paints + .len() + .checked_add(program.surfaces.len()) + .and_then(|count| count.checked_add(program.occurrences.len())) + .ok_or(ProgramCompileError::ResourceExhausted)?; + let mut queue = Vec::new(); + queue + .try_reserve_exact(node_count) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + Ok(Self { + targets: false_slots(program.targets.len())?, + paints: false_slots(program.paints.len())?, + surfaces: false_slots(program.surfaces.len())?, + occurrences: false_slots(program.occurrences.len())?, + queue, + }) + } + + fn scan( + &mut self, + index: &IndexedProgramDependenciesV1, + roots: impl IntoIterator, + ) -> Result<(), ProgramCompileError> { + self.targets.fill(false); + self.paints.fill(false); + self.surfaces.fill(false); + self.occurrences.fill(false); + self.queue.clear(); + for root in roots { + let occurrence = index + .occurrence(root) + .ok_or(ProgramCompileError::InternalInvariant)?; + if !self.occurrences[occurrence] { + self.occurrences[occurrence] = true; + self.queue + .push(IndexedDependencyNodeV1::Occurrence(occurrence)); + } + } + + let mut cursor = 0usize; + while let Some(node) = self.queue.get(cursor).copied() { + cursor += 1; + match node { + IndexedDependencyNodeV1::Occurrence(occurrence) => { + let paint = index.occurrence_paints[occurrence]; + if !self.paints[paint] { + self.paints[paint] = true; + self.queue.push(IndexedDependencyNodeV1::Paint(paint)); + } + let surface = index.occurrence_surfaces[occurrence]; + if !self.surfaces[surface] { + self.surfaces[surface] = true; + self.queue.push(IndexedDependencyNodeV1::Surface(surface)); + } + } + IndexedDependencyNodeV1::Surface(surface) => { + if let Some(occurrence) = index.surface_occurrences[surface] { + if !self.occurrences[occurrence] { + self.occurrences[occurrence] = true; + self.queue + .push(IndexedDependencyNodeV1::Occurrence(occurrence)); + } + } + } + IndexedDependencyNodeV1::Paint(paint) => match index.paint_dependencies[paint] { + IndexedPaintDependencyV1::Target(target) => self.targets[target] = true, + IndexedPaintDependencyV1::Paint(source) => { + if !self.paints[source] { + self.paints[source] = true; + self.queue.push(IndexedDependencyNodeV1::Paint(source)); + } + } + }, + } + } + Ok(()) + } +} + +fn false_slots(len: usize) -> Result, ProgramCompileError> { + let mut slots = Vec::new(); + slots + .try_reserve_exact(len) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + slots.resize(len, false); + Ok(slots) +} + +fn validate_terminal_dependency_cone( + program: &Program, +) -> Result<(), ProgramCompileError> +where + Evaluation: ProgramPointEvaluatorV1, + ProgramPointInvocation: Copy, +{ + // Preserve the canonical missing-reference diagnostics owned by constraint + // and output compilation before applying the stronger terminal-safety law. + if program + .constraints + .hard + .iter() + .map(|constraint| constraint.target) + .chain( + program + .constraints + .report_only + .iter() + .map(|constraint| constraint.target), + ) + .any(|target| { + !program + .occurrences + .iter() + .any(|occurrence| occurrence.id == target) + }) + || program.outputs.iter().any(|output| { + !program.paints.iter().any(|paint| match *paint { + Paint::Solid { id, .. } | Paint::Opacity { id, .. } => id == output.paint, + }) + }) + { + return Ok(()); + } + + let index = index_program_dependencies(program)?; + let mut scratch = ProgramDependencyScratchV1::new(program)?; + scratch.scan( + &index, + program + .constraints + .hard + .iter() + .map(|constraint| constraint.target) + .chain( + program + .constraints + .report_only + .iter() + .map(|constraint| constraint.target), + ), + )?; + 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 }); + } + } + for output in &program.outputs { + let paint_index = index + .paint(output.paint) + .ok_or(ProgramCompileError::InternalInvariant)?; + if !scratch.paints[paint_index] { + return Err(ProgramCompileError::UnassessedOutput { + output: output.output, + paint: output.paint, + }); + } + } + + let finite_count = program + .targets + .iter() + .filter(|target| matches!(&target.domain, TargetDomainV1::Finite(_))) + .count(); + if finite_count > 1 { + let mut has_common_assessment = false; + for target in program + .constraints + .hard + .iter() + .map(|constraint| constraint.target) + .chain( + program + .constraints + .report_only + .iter() + .map(|constraint| constraint.target), + ) + { + scratch.scan(&index, [target])?; + if program.targets.iter().enumerate().all(|(index, target)| { + !matches!(&target.domain, TargetDomainV1::Finite(_)) || scratch.targets[index] + }) { + has_common_assessment = true; + break; + } + } + if !has_common_assessment { + return Err(ProgramCompileError::DisconnectedFiniteTargets); + } + } + Ok(()) +} + fn map_observation_schema_compile_error(error: ObservationError) -> ProgramCompileError { match error { ObservationError::ResourceExhausted => ProgramCompileError::ResourceExhausted, @@ -1126,6 +2112,165 @@ struct LoweredConstraint { invocation: Invocation, } +fn compile_targets( + graph: &CompiledAppearanceGraph, + authored_targets: Vec, + authored_selection: Option, +) -> Result< + ( + Box<[CompiledFiniteTargetV1]>, + Option, + ), + ProgramCompileError, +> { + struct CanonicalFiniteTargetV1 { + id: TargetId, + binding: CompiledColorInputSlotV1, + candidates: Vec, + } + + let mut compiled = Vec::new(); + compiled + .try_reserve_exact(authored_targets.len()) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + for target in authored_targets { + let TargetDomainV1::Finite(mut candidates) = target.domain else { + continue; + }; + if candidates.is_empty() { + return Err(ProgramCompileError::EmptyTargetDomain { target: target.id }); + } + let binding = graph + .bind_color_input(target_color_input_id(target.id)) + .ok_or(ProgramCompileError::InternalInvariant)?; + candidates.sort_unstable_by_key(|candidate| candidate.id); + if let Some(candidate) = candidates + .windows(2) + .find(|pair| pair[0].id == pair[1].id) + .map(|pair| pair[0].id) + { + return Err(ProgramCompileError::DuplicateTargetCandidate { + target: target.id, + candidate, + }); + } + let mut physical = Vec::new(); + physical + .try_reserve_exact(candidates.len()) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + physical.extend( + candidates + .iter() + .map(|candidate| (candidate.signal, candidate.id)), + ); + physical.sort_unstable(); + if let Some(pair) = physical.windows(2).find(|pair| pair[0].0 == pair[1].0) { + return Err(ProgramCompileError::DuplicateTargetCandidateSignal { + target: target.id, + first: pair[0].1, + duplicate: pair[1].1, + signal: pair[0].0, + }); + } + compiled.push(CanonicalFiniteTargetV1 { + id: target.id, + binding, + candidates, + }); + } + + if compiled.is_empty() { + return match authored_selection { + None => Ok((Box::new([]), None)), + Some(_) => Err(ProgramCompileError::JointSelectionWithoutTargets), + }; + } + let Some(authored_selection) = authored_selection else { + return Err(ProgramCompileError::MissingJointSelection); + }; + + let mut authored_tuples = Vec::new(); + 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 + .choices + .windows(2) + .find(|pair| pair[0].target == pair[1].target) + .map(|pair| pair[0].target) + { + return Err(ProgramCompileError::JointStateDuplicateTarget { + state: state_index, + target, + }); + } + if let Some(choice) = state.choices.iter().find(|choice| { + compiled + .binary_search_by_key(&choice.target, |target| target.id) + .is_err() + }) { + return Err(ProgramCompileError::JointStateUnknownTarget { + state: state_index, + target: choice.target, + }); + } + + let mut tuple = Vec::new(); + tuple + .try_reserve_exact(compiled.len()) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + for target in &compiled { + let choice_index = 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 candidate_index = target + .candidates + .binary_search_by_key(&choice.candidate, |candidate| candidate.id) + .map_err(|_| ProgramCompileError::JointStateUnknownCandidate { + state: state_index, + target: target.id, + candidate: choice.candidate, + })?; + tuple.push(FiniteDomainOrdinalV1::new(candidate_index)); + } + authored_tuples.push(tuple); + } + + let mut domain_lengths = Vec::new(); + domain_lengths + .try_reserve_exact(compiled.len()) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + domain_lengths.extend(compiled.iter().map(|target| target.candidates.len())); + let order = admit_finite_joint_order_v1(&domain_lengths, authored_tuples) + .map_err(ProgramCompileError::InvalidJointOrder)?; + let mut runtime_targets = Vec::new(); + runtime_targets + .try_reserve_exact(compiled.len()) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + for target in compiled { + let mut candidates = Vec::new(); + candidates + .try_reserve_exact(target.candidates.len()) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + candidates.extend(target.candidates.into_iter().map(TargetCandidateV1::signal)); + runtime_targets.push(CompiledFiniteTargetV1 { + binding: target.binding, + candidates: candidates.into_boxed_slice(), + }); + } + Ok(( + runtime_targets.into_boxed_slice(), + Some(CompiledJointSelectionV1 { order }), + )) +} + fn compile_occurrence_contexts( graph: &CompiledAppearanceGraph, authored: &[Occurrence], @@ -1341,88 +2486,140 @@ pub(crate) fn canonical_surface_input_port_sequence_matches( actual.into_iter().eq(expected.iter().copied()) } -fn lower_graph(program: &Program) -> AppearanceGraphSpec +const fn target_color_input_id(target: TargetId) -> ColorInputId { + ColorInputId::new(target.value()) +} + +fn try_collect_program( + exact_len: usize, + values: impl IntoIterator, +) -> Result, ProgramCompileError> { + let mut collected = Vec::new(); + collected + .try_reserve_exact(exact_len) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + collected.extend(values); + Ok(collected) +} + +fn lower_graph( + program: &Program, +) -> Result where Evaluation: ProgramPointEvaluatorV1, ProgramPointInvocation: Copy, { - AppearanceGraphSpec::new( - program.colors.iter().map(|input| input.id).collect(), - program.observation_group.surface_input_ports.clone(), - program.opacities.iter().map(|input| input.id).collect(), + let colors = try_collect_program( + program.targets.len(), program - .paints + .targets .iter() - .map(|paint| match *paint { - Paint::Solid { id, color } => PaintSpec::Solid { id, color }, - Paint::Opacity { - id, - source, - opacity, - } => PaintSpec::Opacity { - id, - source, - opacity, - }, - }) - .collect(), + .map(|target| target_color_input_id(target.id)), + )?; + let surface_inputs = try_collect_program( + program.observation_group.surface_input_ports.len(), program - .surfaces + .observation_group + .surface_input_ports .iter() - .map(|surface| match *surface { - Surface::Input { id, input } => SurfaceSpec::Input { id, port: input }, - Surface::FromOccurrence { id, occurrence } => { - SurfaceSpec::FromOccurrence { id, occurrence } + .copied(), + )?; + let opacities = try_collect_program( + program.opacities.len(), + program.opacities.iter().map(|input| input.id), + )?; + let paints = try_collect_program( + program.paints.len(), + program.paints.iter().map(|paint| match *paint { + Paint::Solid { id, target } => PaintSpec::Solid { + id, + color: target_color_input_id(target), + }, + Paint::Opacity { + id, + source, + opacity, + } => PaintSpec::Opacity { + id, + source, + opacity, + }, + }), + )?; + let surfaces = try_collect_program( + program.surfaces.len(), + program.surfaces.iter().map(|surface| match *surface { + Surface::Input { id, input } => SurfaceSpec::Input { id, port: input }, + Surface::FromOccurrence { id, occurrence } => { + SurfaceSpec::FromOccurrence { id, occurrence } + } + }), + )?; + let occurrences = try_collect_program( + program.occurrences.len(), + program.occurrences.iter().map(|occurrence| OccurrenceSpec { + id: occurrence.id, + subject: occurrence.subject, + against: occurrence.against, + profile: match occurrence.composition { + CompositionProfile::EncodedSrgb8SourceOverV1 => { + CompositionProfileV1::EncodedSrgb8SourceOverV1 } - }) - .collect(), - program - .occurrences - .iter() - .map(|occurrence| OccurrenceSpec { - id: occurrence.id, - subject: occurrence.subject, - against: occurrence.against, - profile: match occurrence.composition { - CompositionProfile::EncodedSrgb8SourceOverV1 => { - CompositionProfileV1::EncodedSrgb8SourceOverV1 - } - }, - }) - .collect(), - ) -} - -fn lower_bindings(program: &Program) -> AppearanceBindings + }, + }), + )?; + Ok(AppearanceGraphSpec::new( + colors, + surface_inputs, + opacities, + paints, + surfaces, + occurrences, + )) +} + +fn lower_bindings( + program: &Program, +) -> Result where Evaluation: ProgramPointEvaluatorV1, ProgramPointInvocation: Copy, { - AppearanceBindings::new( - program - .colors - .iter() - .map(|input| (input.id, input.value.srgb8())) - .collect(), + let mut colors = Vec::new(); + colors + .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)?; + colors.push(( + target_color_input_id(target.id), + program.sources[source_index].signal.srgb8(), + )); + } + let surfaces = try_collect_program( + program.observation_group.surface_input_ports.len(), program .observation_group .surface_input_ports .iter() - .map(|input| (*input, Srgb8::new([0; 3]))) - .collect(), + .map(|input| (*input, Srgb8::new([0; 3]))), + )?; + let opacities = try_collect_program( + program.opacities.len(), program .opacities .iter() - .map(|input| (input.id, input.value)) - .collect(), - ) + .map(|input| (input.id, input.value)), + )?; + Ok(AppearanceBindings::new(colors, surfaces, opacities)) } fn map_compile_error(error: CompileError) -> ProgramCompileError { match error { - CompileError::DuplicateColorInput { input } => { - ProgramCompileError::DuplicateColorInput { input } - } + CompileError::DuplicateColorInput { .. } => ProgramCompileError::InternalInvariant, CompileError::DuplicateOpacityInput { input } => { ProgramCompileError::DuplicateOpacityInput { input } } @@ -1436,9 +2633,7 @@ fn map_compile_error(error: CompileError) -> ProgramCompileError { CompileError::DuplicateOccurrence { occurrence } => { ProgramCompileError::DuplicateOccurrence { occurrence } } - CompileError::MissingPaintColorInput { paint, input } => { - ProgramCompileError::MissingPaintColorInput { paint, input } - } + CompileError::MissingPaintColorInput { .. } => ProgramCompileError::InternalInvariant, CompileError::MissingPaintSource { paint, source } => { ProgramCompileError::MissingPaintSource { paint, source } } diff --git a/crates/labcolors-core/src/program_session_tests.rs b/crates/labcolors-core/src/program_session_tests.rs index 73418806..7618e9ae 100644 --- a/crates/labcolors-core/src/program_session_tests.rs +++ b/crates/labcolors-core/src/program_session_tests.rs @@ -1,7 +1,5 @@ use crate::Srgb8; -use crate::appearance::{ - ColorInputId, OccurrenceId, OpacityInputId, PaintId, SurfaceId, SurfaceInputPortId, -}; +use crate::appearance::{OccurrenceId, OpacityInputId, PaintId, SurfaceId, SurfaceInputPortId}; use crate::constraints::{ExactSrgb8IdentityV1, ProgramPointEvaluatorV1, ProgramPointInvocation}; use crate::lcs_occurrence::{ AdaptingLuminanceCdM2, AppearanceContextId, AppearanceContextSchemaReleaseId, @@ -13,14 +11,15 @@ use crate::observation::{ SurfaceInputBinding, }; use crate::program_session::{ - ColorInput, CompositionProfile, ConstraintId, ConstraintInvocation, ConstraintSet, - ObservationGroup, Occurrence, OpacityInput, OutputBinding, OutputSlotId, Paint, Program, - ProgramCompileError, Surface, canonical_surface_input_port_sequence_matches, + CompositionProfile, ConstraintId, ConstraintInvocation, ConstraintSet, ObservationGroup, + Occurrence, OpacityInput, OutputBinding, OutputSlotId, Paint, Program, ProgramCompileError, + Source, SourceId, Surface, Target, TargetId, canonical_surface_input_port_sequence_matches, check_render_node_count, }; use crate::session::SessionState; -const COLOR: ColorInputId = ColorInputId::new(1); +const SOURCE: SourceId = SourceId::new(1); +const TARGET: TargetId = TargetId::new(1); const SURFACE_PORT: SurfaceInputPortId = SurfaceInputPortId::new(2); const OPACITY: OpacityInputId = OpacityInputId::new(3); const SOLID: PaintId = PaintId::new(10); @@ -75,16 +74,17 @@ where ProgramPointInvocation: Copy, { Program::new( - vec![ColorInput::new( - COLOR, + vec![Source::new( + SOURCE, ColorSignal::from_srgb8(Srgb8::new([0; 3])), )], + vec![Target::fixed(TARGET, SOURCE)], ObservationGroup::new(group, vec![SURFACE_PORT]), vec![OpacityInput::new(OPACITY, opacity)], vec![ Paint::Solid { id: SOLID, - color: COLOR, + target: TARGET, }, Paint::Opacity { id: TRANSLUCENT, @@ -180,12 +180,16 @@ fn authored_modes_are_marker_typed_and_values_preserve_exact_ids() { assert_eq!(ConstraintId::new(7).value(), 7); assert_eq!(OutputSlotId::new(8).value(), 8); - let color = ColorInput::new(COLOR, ColorSignal::from_srgb8(Srgb8::new([1, 2, 3]))); - assert_eq!(color.id(), COLOR); + let source = Source::new(SOURCE, ColorSignal::from_srgb8(Srgb8::new([1, 2, 3]))); + assert_eq!(SOURCE.value(), 1); + assert_eq!(source.id(), SOURCE); assert_eq!( - color.value(), + source.signal(), ColorSignal::from_srgb8(Srgb8::new([1, 2, 3])) ); + let target = Target::fixed(TARGET, SOURCE); + assert_eq!(target.id(), TARGET); + assert_eq!(target.source(), SOURCE); let opacity = OpacityInput::new(OPACITY, 0.375); assert_eq!(opacity.id(), OPACITY); assert_eq!(opacity.value(), 0.375); @@ -213,15 +217,16 @@ fn authored_modes_are_marker_typed_and_values_preserve_exact_ids() { #[test] fn empty_domains_have_stable_precedence() { let empty_surface = Program::new( - vec![ColorInput::new( - COLOR, + vec![Source::new( + SOURCE, ColorSignal::from_srgb8(Srgb8::new([0; 3])), )], + vec![Target::fixed(TARGET, SOURCE)], observation_group(vec![]), vec![], vec![Paint::Solid { id: SOLID, - color: COLOR, + target: TARGET, }], vec![Surface::Input { id: BACKDROP, @@ -251,15 +256,16 @@ fn empty_domains_have_stable_precedence() { ); let empty_occurrence = Program::new( - vec![ColorInput::new( - COLOR, + vec![Source::new( + SOURCE, ColorSignal::from_srgb8(Srgb8::new([0; 3])), )], + vec![Target::fixed(TARGET, SOURCE)], observation_group(vec![SURFACE_PORT]), vec![], vec![Paint::Solid { id: SOLID, - color: COLOR, + target: TARGET, }], vec![Surface::Input { id: BACKDROP, @@ -592,7 +598,7 @@ fn multi_case_hard_failure_retains_the_full_matrix_without_outputs() { .count(), 2, ); - // `ProgramViolationV1` owns only the complete report; no output accessor or + // `ProgramConflictV1` owns only the complete report; no output accessor or // output storage exists on the failure type. } @@ -647,7 +653,7 @@ fn mixed_modes_retain_the_full_canonical_matrix_without_outputs_on_hard_failure( vec![true, false, false, true], ); assert!(cells.iter().all(|cell| cell.target() == OCCURRENCE)); - // `cause` is `ProgramViolationV1`: the failure surface exposes only this + // `cause` is `ProgramConflictV1`: the failure surface exposes only this // complete report, while Paint outputs exist only on `ProgramVerifiedV1`. } @@ -692,8 +698,10 @@ fn report_only_violations_do_not_block_program_scope_paint_outputs() { #[test] fn nested_surface_uses_the_lower_occurrence_before_assessing_the_upper() { - const LOWER_COLOR: ColorInputId = ColorInputId::new(101); - const UPPER_COLOR: ColorInputId = ColorInputId::new(102); + const LOWER_SOURCE: SourceId = SourceId::new(101); + const UPPER_SOURCE: SourceId = SourceId::new(102); + const LOWER_TARGET: TargetId = TargetId::new(101); + const UPPER_TARGET: TargetId = TargetId::new(102); const HALF: OpacityInputId = OpacityInputId::new(103); const LOWER_PAINT: PaintId = PaintId::new(110); const UPPER_SOLID: PaintId = PaintId::new(111); @@ -706,19 +714,23 @@ fn nested_surface_uses_the_lower_occurrence_before_assessing_the_upper() { let program = Program::new( vec![ - ColorInput::new(LOWER_COLOR, ColorSignal::from_srgb8(Srgb8::new([0x80; 3]))), - ColorInput::new(UPPER_COLOR, ColorSignal::from_srgb8(Srgb8::new([0xFF; 3]))), + Source::new(LOWER_SOURCE, ColorSignal::from_srgb8(Srgb8::new([0x80; 3]))), + Source::new(UPPER_SOURCE, ColorSignal::from_srgb8(Srgb8::new([0xFF; 3]))), + ], + vec![ + Target::fixed(LOWER_TARGET, LOWER_SOURCE), + Target::fixed(UPPER_TARGET, UPPER_SOURCE), ], observation_group(vec![SURFACE_PORT]), vec![OpacityInput::new(HALF, 0.5)], vec![ Paint::Solid { id: LOWER_PAINT, - color: LOWER_COLOR, + target: LOWER_TARGET, }, Paint::Solid { id: UPPER_SOLID, - color: UPPER_COLOR, + target: UPPER_TARGET, }, Paint::Opacity { id: UPPER_PAINT, diff --git a/scripts/verify_point_support_surplus.py b/scripts/verify_point_support_surplus.py index cd60585d..69816a19 100644 --- 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 = ( - "6a541c168d9a5c266367050caec0114ea137a8f20f11f9868a6e46c821ba2ec2" + "6b81edc2d56bdca78a4d7c579c79e2791dc59e9bf2626fd22ddc7aa164dfa5c8" ) EXPECTED_Q55_PROOF_SHA256 = ( "ac59cf89503170c789223b91d775213a19d4e571ef930f2ea609fcd51b14defd" From 8391d227595f73c04c6988b913a3ab227b43a035 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Thu, 23 Jul 2026 01:54:55 +0300 Subject: [PATCH 32/58] quarantine unsupported Display P3 promises --- .github/workflows/publish.yml | 1 - bindings/swift/README.md | 2 +- .../ConformanceTests.swift | 1 - conformance/README.md | 2 +- crates/labcolors-conformance/src/lib.rs | 3 +- ...rt-reference-surplus-q55-bps-proof-v1.json | 2 +- .../solve-characterization-v1-linux-x64.json | 1 - ...lve-characterization-v1-macos-aarch64.json | 1 - crates/labcolors-core/src/lib.rs | 1 - crates/labcolors-core/src/scale.rs | 14 +- crates/labcolors-core/src/semantic.rs | 63 +-- crates/labcolors-core/src/solve.rs | 76 +--- crates/labcolors-core/src/spaces/mod.rs | 1 + crates/labcolors-core/src/spaces/oklch.rs | 4 +- crates/labcolors-core/src/spaces/p3.rs | 410 +----------------- .../tests/property_invariants.rs | 7 +- .../tests/solve_characterization.rs | 37 +- crates/labcolors-ffi/src/lib.rs | 10 +- crates/labcolors-wasm/src/lib.rs | 2 +- packages/colors/bench/wasm.json | 8 +- packages/colors/smoke.consumer.ts | 9 - .../colors/test/release-contract.test.mjs | 4 +- scripts/check-wasm-size-budget.mjs | 2 +- scripts/verify-package-release.mjs | 2 - scripts/verify_point_support_surplus.py | 2 +- 25 files changed, 91 insertions(+), 574 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 2acc34b9..9a1c5264 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -790,7 +790,6 @@ jobs: "stable-cam16-glow-target-or-maximum-selection", "renderer-or-output-pipeline-equivalence", "spatial-glow-field", - "display-p3", ]; if ( JSON.stringify(manifest.supported) !== JSON.stringify(expectedSupported) || diff --git a/bindings/swift/README.md b/bindings/swift/README.md index 59d9279f..f240d726 100644 --- a/bindings/swift/README.md +++ b/bindings/swift/README.md @@ -53,7 +53,7 @@ Swift/UniFFI evidence сейчас ограничено описанным вы runtime. Solve-hex — квантование трансцендентного резолва, ±1 LSB на канал. Неуспешный solve возвращает `ColorError.Failure(category, code)`: category — закрытый enum `FailureCategory`, а не произвольная строка. Он -отделяет доказанную `unreachable` от `unresolved`, `rejected` и `unsupported`, +отделяет доказанную `unreachable` от `unresolved` и `rejected`, а code задаёт конкретную машинную причину. Оба поля приходят из одного core-owned descriptor и проверяются conformance-паком. diff --git a/bindings/swift/Tests/LabColorsConformanceTests/ConformanceTests.swift b/bindings/swift/Tests/LabColorsConformanceTests/ConformanceTests.swift index e812e566..10ba110b 100644 --- a/bindings/swift/Tests/LabColorsConformanceTests/ConformanceTests.swift +++ b/bindings/swift/Tests/LabColorsConformanceTests/ConformanceTests.swift @@ -53,7 +53,6 @@ final class ConformanceTests: XCTestCase { case "unreachable": return .unreachable case "unresolved": return .unresolved case "rejected": return .rejected - case "unsupported": return .unsupported default: fatalError("неизвестная failure category в pack: \(key)") } } diff --git a/conformance/README.md b/conformance/README.md index 5fba329a..74a50287 100644 --- a/conformance/README.md +++ b/conformance/README.md @@ -76,7 +76,7 @@ adjacent bytes или нормативного отношения пересчи - `(category, code)` — атомарная core-owned классификация, общая для всех биндингов: `unreachable/exceeds_range`, `unreachable/floor_unreachable`, `unresolved/bounded_search_exhausted`, `rejected/invalid_input`, - `unreachable/below_contrast_floor` и `unsupported/gamut_unsupported`. Только + `unreachable/below_contrast_floor`. Только `unreachable` доказывает отсутствие решения в объявленном полном domain; `unresolved` не делает утверждения о непроверенных кандидатах. diff --git a/crates/labcolors-conformance/src/lib.rs b/crates/labcolors-conformance/src/lib.rs index 19690e9f..fe3be626 100644 --- a/crates/labcolors-conformance/src/lib.rs +++ b/crates/labcolors-conformance/src/lib.rs @@ -296,7 +296,7 @@ pub enum SolveOutcome { floor_override: bool, }, /// Resolver не вернул цвет; category отделяет доказанную недостижимость от - /// unresolved, rejected и unsupported исходов. + /// unresolved и rejected исходов. Failure { /// Стабильная семантическая категория core failure. category: String, @@ -883,7 +883,6 @@ mod tests { "unreachable", "floor_unreachable", ), - (F::GamutUnsupported, "unsupported", "gamut_unsupported"), ( F::InvalidInput("fixture".into()), "rejected", 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 0e1b54c4..1e529242 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":"16f1efb88a9224b4507264f2a560eaa6b174a24020a208266c07a2f47309312a","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":"6b81edc2d56bdca78a4d7c579c79e2791dc59e9bf2626fd22ddc7aa164dfa5c8","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":"e361cf144e0630a4f2ff52261e6515c9394188fc1351c97b98ea9c0ec87ec39d"},{"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":"a58acbae5b06a8cd9a45adf93ddfbdfb569c5916b60140bd4046182aab2a9518"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"1c75a1bc5f33c497aff148fbe5d943716b5d88f680623e8cbf028545c08958c7"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"abed15a542b5e8031f0ebfe36138232920c7898c9eae1a2037c6455033ecd91e"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"945755f2eb91eb21aeee9b7a7b0e67b093d7e766af35a121480003669d88a053"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"0a1fdf10c9ecae497cfcd447a6af937f2b07d43b81b2cd28a50d998e11197fe9"},{"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":"e70ce1fadbb64b044772d9f18f7c16ee76cf6aa33b1d1c0e642e0437fd5ed4a3"} +{"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":"fd8cc51bc850523f600cc850b10af56afb450dccaeb928c795d2c45c421df8e4","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":"2e63424882231ec2f00ae79911bf74cfb935bb96f11368bce53240468f7f2dd4","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":"e361cf144e0630a4f2ff52261e6515c9394188fc1351c97b98ea9c0ec87ec39d"},{"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":"a58acbae5b06a8cd9a45adf93ddfbdfb569c5916b60140bd4046182aab2a9518"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"0f0e0ed726aba4defc750b793a76435a3f3825e91002c2cd90d982be7260f180"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"abed15a542b5e8031f0ebfe36138232920c7898c9eae1a2037c6455033ecd91e"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"945755f2eb91eb21aeee9b7a7b0e67b093d7e766af35a121480003669d88a053"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"0a1fdf10c9ecae497cfcd447a6af937f2b07d43b81b2cd28a50d998e11197fe9"},{"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":"a8f1b4f65a45bf18ad8c2ae21cc88e5cd270a5ee438d625153b66d26660a7e90"} diff --git a/crates/labcolors-core/contracts/solve-characterization-v1-linux-x64.json b/crates/labcolors-core/contracts/solve-characterization-v1-linux-x64.json index f778ca88..b9947c14 100644 --- a/crates/labcolors-core/contracts/solve-characterization-v1-linux-x64.json +++ b/crates/labcolors-core/contracts/solve-characterization-v1-linux-x64.json @@ -62,7 +62,6 @@ "bg=#FFFFFF contract=text(150) floor=default hue=264 chroma=neutral": "err exceeds_range target_bits=4062c00000000000 max_achievable_bits=405a829a490ad002", "bg=#FFFFFF contract=text(3) floor=none hue=0 chroma=neutral": "err below_contrast_floor target_bits=4008000000000000", "bg=#FFFFFF contract=text(30) floor=default hue=264 chroma=neutral": "ok hex=#767676 lc_bits=40510fca61d5b9ee wcag_ratio_bits=40122b3d05125164 floor_override=true jp_bits=404a3678a8f0c874 h_ok_bits=0000000000000000 s_bits=3f92a6b801503a78", - "bg=#FFFFFF contract=text(60) floor=default hue=0 chroma=neutral gamut=display-p3": "err gamut_unsupported", "bg=#FFFFFF contract=text(60) floor=default hue=264 chroma=neutral": "ok hex=#767676 lc_bits=40510fca61d5b9ee wcag_ratio_bits=40122b3d05125164 floor_override=true jp_bits=404a3678a8f0c874 h_ok_bits=0000000000000000 s_bits=3f92a6b801503a78", "bg=#FFFFFF contract=text(7.3) floor=none hue=0 chroma=neutral": "ok hex=#EDEDED lc_bits=401e6aa8625b8742 wcag_ratio_bits=3ff2bb539763a677 floor_override=false jp_bits=4057afba8603b8da h_ok_bits=0000000000000000 s_bits=3f8f614c4436cac4", "bg=#FFFFFF contract=text(7.35) floor=none hue=0 chroma=neutral": "ok hex=#EDEDED lc_bits=401e6aa8625b8742 wcag_ratio_bits=3ff2bb539763a677 floor_override=false jp_bits=4057afba8603b8da h_ok_bits=0000000000000000 s_bits=3f8f614c4436cac4", diff --git a/crates/labcolors-core/contracts/solve-characterization-v1-macos-aarch64.json b/crates/labcolors-core/contracts/solve-characterization-v1-macos-aarch64.json index e3d55f3f..115971b1 100644 --- a/crates/labcolors-core/contracts/solve-characterization-v1-macos-aarch64.json +++ b/crates/labcolors-core/contracts/solve-characterization-v1-macos-aarch64.json @@ -62,7 +62,6 @@ "bg=#FFFFFF contract=text(150) floor=default hue=264 chroma=neutral": "err exceeds_range target_bits=4062c00000000000 max_achievable_bits=405a829a490ad002", "bg=#FFFFFF contract=text(3) floor=none hue=0 chroma=neutral": "err below_contrast_floor target_bits=4008000000000000", "bg=#FFFFFF contract=text(30) floor=default hue=264 chroma=neutral": "ok hex=#767676 lc_bits=40510fca61d5b9ee wcag_ratio_bits=40122b3d05125164 floor_override=true jp_bits=404a3678a8f0c874 h_ok_bits=0000000000000000 s_bits=3f92a6b801503a78", - "bg=#FFFFFF contract=text(60) floor=default hue=0 chroma=neutral gamut=display-p3": "err gamut_unsupported", "bg=#FFFFFF contract=text(60) floor=default hue=264 chroma=neutral": "ok hex=#767676 lc_bits=40510fca61d5b9ee wcag_ratio_bits=40122b3d05125164 floor_override=true jp_bits=404a3678a8f0c874 h_ok_bits=0000000000000000 s_bits=3f92a6b801503a78", "bg=#FFFFFF contract=text(7.3) floor=none hue=0 chroma=neutral": "ok hex=#EDEDED lc_bits=401e6aa8625b8742 wcag_ratio_bits=3ff2bb539763a677 floor_override=false jp_bits=4057afba8603b8da h_ok_bits=0000000000000000 s_bits=3f8f614c4436cac4", "bg=#FFFFFF contract=text(7.35) floor=none hue=0 chroma=neutral": "ok hex=#EDEDED lc_bits=401e6aa8625b8742 wcag_ratio_bits=3ff2bb539763a677 floor_override=false jp_bits=4057afba8603b8da h_ok_bits=0000000000000000 s_bits=3f8f614c4436cac4", diff --git a/crates/labcolors-core/src/lib.rs b/crates/labcolors-core/src/lib.rs index 09e418be..3734dc54 100644 --- a/crates/labcolors-core/src/lib.rs +++ b/crates/labcolors-core/src/lib.rs @@ -237,7 +237,6 @@ pub use solve::{ SolveFailureCategory, SolveJob, Solved, solve, solve_many, }; pub use spaces::oklch::{css_alpha_value, oklch_css_from_hex, oklch_from_hex}; -pub use spaces::p3::{p3_css_from_hex, p3_from_hex}; pub use spaces::srgb::srgb_encoded_from_hex; pub use spaces::vc::ViewingConditions; diff --git a/crates/labcolors-core/src/scale.rs b/crates/labcolors-core/src/scale.rs index 6e2b3fb6..db7f042c 100644 --- a/crates/labcolors-core/src/scale.rs +++ b/crates/labcolors-core/src/scale.rs @@ -752,16 +752,10 @@ fn quadratic_roots(d: f64, c: f64, b: f64) -> ([f64; 3], usize) { (roots, 2) } -/// Стена гамута **Display P3** при `(L, h)` — та же бисекция, что -/// [`max_chroma_bisect`], но валидность кандидата проверяется в ЛИНЕЙНОМ P3 -/// (Oklab → линейный sRGB → XYZ → линейный P3; первые два шага — линейная -/// алгебра, корректная и за пределами sRGB-куба). -/// -/// Этап 1 gamut-aware солвера (2026-07-03): геометрия стен и решётка эмиссии; -/// перевод `Solved`/эмиссии на P3-кандидаты — этап 2. Чистая гамут-геометрия -/// CSS Color 4 матриц — нуля подгонки (класс M-13 инвентаря). -// Прод-потребитель — этап 2 (P3-кандидаты солвера); до него читается тестами. -#[cfg_attr(not(test), allow(dead_code))] +/// Test-only physical Display P3 gamut boundary. This is geometry, not a +/// released output capability: no production selector, encoder, or verifier +/// consumes it yet. +#[cfg(test)] pub(crate) fn max_chroma_p3_bisect(l_ok: f64, h_ok_deg: f64) -> f64 { let h_ok = h_ok_deg.to_radians(); let cos_h = h_ok.cos(); diff --git a/crates/labcolors-core/src/semantic.rs b/crates/labcolors-core/src/semantic.rs index 033f1768..d899d933 100644 --- a/crates/labcolors-core/src/semantic.rs +++ b/crates/labcolors-core/src/semantic.rs @@ -8,8 +8,8 @@ //! текущего фона и viewing conditions; сериализация принадлежит биндингу. //! //! Граница набора атомарна. Доказанная недостижимость и незавершённый bounded -//! search остаются типизированными исходами отдельных ролей; rejected, -//! unsupported и internal закрывают вызов через [`ResolveSetError`] без +//! search остаются типизированными исходами отдельных ролей; rejected и +//! internal закрывают вызов через [`ResolveSetError`] без //! частичного успешного вектора. Нулевое значение представлено явно через //! [`Resolved::None`]. //! @@ -1335,8 +1335,6 @@ impl std::error::Error for RoleFailure { pub enum ResolveSetErrorKind { /// Значение запроса вышло за объявленный домен. Rejected, - /// Запрос требует capability, которую этот резолвер не реализует. - Unsupported, /// Состояние, произведённое ядром, нарушило внутренний постинвариант. Internal, } @@ -1346,7 +1344,6 @@ impl ResolveSetErrorKind { pub const fn as_str(self) -> &'static str { match self { Self::Rejected => SolveFailureCategory::Rejected.as_str(), - Self::Unsupported => SolveFailureCategory::Unsupported.as_str(), Self::Internal => "internal", } } @@ -1355,16 +1352,15 @@ impl ResolveSetErrorKind { #[derive(Debug, Clone, PartialEq)] enum ResolveSetErrorState { Rejected(BoundaryFailure), - Unsupported(BoundaryFailure), Internal(SolveFailure), } /// Отказ всего набора из [`resolve_named_set`]. /// -/// Rejected-запросы, неподдержанные capability и внутренний дрейф закрывают -/// весь вызов. Конструкторы приватны; допуск делит [`SolveFailure::boundary`] -/// с [`RoleFailure`], а [`Self::kind`] и [`Self::code`] остаются -/// авторитетной whole-call-классификацией. +/// Rejected-запросы и внутренний дрейф закрывают весь вызов. Конструкторы +/// приватны; допуск делит [`SolveFailure::boundary`] с [`RoleFailure`], а +/// [`Self::kind`] и [`Self::code`] остаются авторитетной whole-call- +/// классификацией. #[derive(Debug, Clone, PartialEq)] pub struct ResolveSetError { state: ResolveSetErrorState, @@ -1375,17 +1371,15 @@ impl ResolveSetError { pub const fn kind(&self) -> ResolveSetErrorKind { match &self.state { ResolveSetErrorState::Rejected(_) => ResolveSetErrorKind::Rejected, - ResolveSetErrorState::Unsupported(_) => ResolveSetErrorKind::Unsupported, ResolveSetErrorState::Internal(_) => ResolveSetErrorKind::Internal, } } - /// Стабильный машинный код ядра для rejected/unsupported-отказов. + /// Стабильный машинный код ядра для rejected-отказов. /// У внутреннего дрейфа намеренно нет публичного solver-кода. pub const fn code(&self) -> Option<&'static str> { match &self.state { - ResolveSetErrorState::Rejected(evidence) - | ResolveSetErrorState::Unsupported(evidence) => Some(evidence.boundary.code()), + ResolveSetErrorState::Rejected(evidence) => Some(evidence.boundary.code()), ResolveSetErrorState::Internal(_) => None, } } @@ -1393,8 +1387,7 @@ impl ResolveSetError { /// Структурированный исходный отказ — диагностика и точные evidence-поля. pub const fn reason(&self) -> &SolveFailure { match &self.state { - ResolveSetErrorState::Rejected(evidence) - | ResolveSetErrorState::Unsupported(evidence) => &evidence.reason, + ResolveSetErrorState::Rejected(evidence) => &evidence.reason, ResolveSetErrorState::Internal(reason) => reason, } } @@ -1418,10 +1411,9 @@ type PendingResolution = Result; /// Исход резолва одной допущенной роли: решённый цвет, честный ноль, /// типизированная численная неопределённость или локальный отказ роли. /// -/// Доказанная недостижимость и незавершённый bounded search отдаются -/// пер-ролью и не маскируются. Rejected/unsupported/internal провенанс в этом -/// типе жить не может: он закрывает [`resolve_named_set`] через -/// [`ResolveSetError`]. +/// Доказанная недостижимость и незавершённый bounded search отдаются пер-ролью +/// и не маскируются. Rejected/internal провенанс в этом типе жить не может: он +/// закрывает [`resolve_named_set`] через [`ResolveSetError`]. #[derive(Debug, Clone, PartialEq)] #[non_exhaustive] pub enum Resolved { @@ -1479,9 +1471,6 @@ fn classify_role_failure(reason: SolveFailure) -> Result Err(ResolveSetError { state: ResolveSetErrorState::Rejected(BoundaryFailure { reason, boundary }), }), - SolveFailureCategory::Unsupported => Err(ResolveSetError { - state: ResolveSetErrorState::Unsupported(BoundaryFailure { reason, boundary }), - }), }, None => Err(ResolveSetError { state: ResolveSetErrorState::Internal(reason), @@ -3766,11 +3755,9 @@ fn demotion_outcome( Err(failure) => match failure.boundary().map(|boundary| boundary.category()) { Some(SolveFailureCategory::Unreachable) => Ok(None), Some(SolveFailureCategory::Unresolved) | None => Err(failure), - Some(SolveFailureCategory::Rejected | SolveFailureCategory::Unsupported) => { - Err(SolveFailure::InternalInvariant(format!( - "validated sRGB hierarchy demotion produced {failure}" - ))) - } + Some(SolveFailureCategory::Rejected) => Err(SolveFailure::InternalInvariant(format!( + "validated sRGB hierarchy demotion produced {failure}" + ))), }, } } @@ -3990,11 +3977,6 @@ mod tests { ResolveSetErrorKind::Rejected, Some("invalid_input"), ), - ( - SolveFailure::GamutUnsupported, - ResolveSetErrorKind::Unsupported, - Some("gamut_unsupported"), - ), ( SolveFailure::InternalInvariant("injected drift".into()), ResolveSetErrorKind::Internal, @@ -4258,7 +4240,6 @@ mod tests { floor: 4.5, max_ratio: 3.0, }, - SolveFailure::GamutUnsupported, SolveFailure::InvalidInput("generated probe".into()), ] { assert!( @@ -4319,15 +4300,11 @@ mod tests { ] { assert_eq!(demotion_outcome(Err(failure), 20.0), Ok(None)); } - for failure in [ - SolveFailure::GamutUnsupported, - SolveFailure::InvalidInput("generated request".into()), - ] { - assert!(matches!( - demotion_outcome(Err(failure), 20.0), - Err(SolveFailure::InternalInvariant(_)) - )); - } + let failure = SolveFailure::InvalidInput("generated request".into()); + assert!(matches!( + demotion_outcome(Err(failure), 20.0), + Err(SolveFailure::InternalInvariant(_)) + )); } #[test] diff --git a/crates/labcolors-core/src/solve.rs b/crates/labcolors-core/src/solve.rs index ceaf0b2f..66472972 100644 --- a/crates/labcolors-core/src/solve.rs +++ b/crates/labcolors-core/src/solve.rs @@ -73,16 +73,29 @@ pub enum ChromaPolicy { Relative(f64), } -/// Output colour gamut. The solver produces colours inside this gamut. +/// Output colour gamut. The public surface lists only executable output paths. +/// +/// A reserved Display P3 selector is intentionally not a public capability: +/// +/// ```compile_fail +/// let _ = labcolors_core::Gamut::DisplayP3; +/// ``` +/// +/// Re-encoding an already sRGB-bounded hex value is not a P3 output path, so +/// those former helpers are intentionally absent too: +/// +/// ```compile_fail +/// use labcolors_core::p3_from_hex; +/// ``` +/// +/// ```compile_fail +/// use labcolors_core::p3_css_from_hex; +/// ``` #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[non_exhaustive] pub enum Gamut { /// Standard sRGB. Srgb, - /// Display P3. Reserved: the wider-gamut chroma boundary lands in a later - /// chapter, so v1 returns [`SolveFailure::GamutUnsupported`] rather than - /// silently solving in sRGB. - DisplayP3, } /// The WCAG 2.1 AA legal contrast floor a contract must clear. @@ -339,8 +352,8 @@ impl Solved { /// Why a solve did not return a colour. The variant and its /// [`SolveFailureCategory`] distinguish proof of unreachability from an -/// exhausted algorithm, a rejected request, an unsupported capability, and an -/// internal invariant. Bindings must fail closed on [`Self::InternalInvariant`]. +/// exhausted algorithm, a rejected request, and an internal invariant. Bindings +/// must fail closed on [`Self::InternalInvariant`]. #[derive(Debug, Clone, PartialEq)] #[non_exhaustive] pub enum SolveFailure { @@ -363,8 +376,6 @@ pub enum SolveFailure { /// light-on-dark). `max_ratio` is the most contrast this background can /// supply in that polarity; `floor` is the ratio the contract required. FloorUnreachable { floor: f64, max_ratio: f64 }, - /// The requested gamut is not supported yet (Display P3 arrives later). - GamutUnsupported, /// Malformed input, such as an invalid hex colour or a non-finite target. InvalidInput(String), /// A value produced and validated by the core later violated an internal @@ -386,8 +397,6 @@ pub enum SolveFailureCategory { Unresolved, /// The request is malformed or inconsistent with the declared domain. Rejected, - /// The request is valid, but the requested capability is not implemented. - Unsupported, } impl SolveFailureCategory { @@ -397,7 +406,6 @@ impl SolveFailureCategory { Self::Unreachable => "unreachable", Self::Unresolved => "unresolved", Self::Rejected => "rejected", - Self::Unsupported => "unsupported", } } } @@ -439,7 +447,6 @@ impl SolveFailure { Self::FloorUnreachable { .. } => { (SolveFailureCategory::Unreachable, "floor_unreachable") } - Self::GamutUnsupported => (SolveFailureCategory::Unsupported, "gamut_unsupported"), Self::InvalidInput(_) => (SolveFailureCategory::Rejected, "invalid_input"), Self::InternalInvariant(_) => return None, }; @@ -472,12 +479,6 @@ impl core::fmt::Display for SolveFailure { f, "WCAG floor {floor:.1}:1 is unreachable on this background (max {max_ratio:.2}:1)" ), - Self::GamutUnsupported => { - write!( - f, - "requested gamut is not supported yet (Display P3 is future work)" - ) - } Self::InvalidInput(msg) => write!(f, "invalid input: {msg}"), Self::InternalInvariant(msg) => write!(f, "internal invariant failure: {msg}"), } @@ -507,10 +508,7 @@ pub fn solve( vc: &ViewingConditions, gamut: Gamut, ) -> Result { - // The Display P3 chroma boundary is future work (chapter 5); fail loudly. - if gamut != Gamut::Srgb { - return Err(SolveFailure::GamutUnsupported); - } + let Gamut::Srgb = gamut; validate_job(contract, hue, chroma_policy)?; // Compute the background's quantised display-luminance interval once and // hand it to [`solve_in`]; batch/set entry points reuse the same value for @@ -539,17 +537,14 @@ pub struct SolveJob { /// quantised display-luminance interval is computed once for the whole slice. /// The returned vector is positional: entry `i` is the result for `jobs[i]`, /// each carrying its own `Result` so one failed request never fails the batch. -/// A whole-batch failure (unsupported gamut, or a background that cannot be -/// reduced) is the outer `Err`. +/// A background that cannot be reduced is the outer `Err`. pub fn solve_many( bg: BgInput, jobs: &[SolveJob], vc: &ViewingConditions, gamut: Gamut, ) -> Result>, SolveFailure> { - if gamut != Gamut::Srgb { - return Err(SolveFailure::GamutUnsupported); - } + let Gamut::Srgb = gamut; // Background side: one forward for the whole batch (see [`solve`]). let interval = bg.luma_interval(vc)?; Ok(jobs @@ -1423,11 +1418,6 @@ mod tests { SolveFailureCategory::Unreachable, "floor_unreachable", ), - ( - SolveFailure::GamutUnsupported, - SolveFailureCategory::Unsupported, - "gamut_unsupported", - ), ( SolveFailure::InvalidInput("x".into()), SolveFailureCategory::Rejected, @@ -1468,7 +1458,6 @@ mod tests { floor: 4.5, max_ratio: 2.0, }, - SolveFailure::GamutUnsupported, SolveFailure::InvalidInput("x".to_string()), SolveFailure::InternalInvariant("x".to_string()), ]; @@ -1481,7 +1470,6 @@ mod tests { | SolveFailure::ExceedsRange { .. } | SolveFailure::BoundedSearchExhausted { .. } | SolveFailure::FloorUnreachable { .. } - | SolveFailure::GamutUnsupported | SolveFailure::InvalidInput(_) | SolveFailure::InternalInvariant(_) => {} } @@ -2040,24 +2028,6 @@ mod tests { ); } - #[test] - fn display_p3_gamut_is_reserved_not_implemented() { - // SEAM (c): the P3 variant exists in the type but returns a real error, - // never a panic and never a silent sRGB fallback. - let vc = ViewingConditions::srgb(); - let bg = BgInput::solid("#FFFFFF").unwrap(); - let err = solve( - bg, - Contract::text(60.0), - Hue::deg(0.0), - ChromaPolicy::Neutral, - &vc, - Gamut::DisplayP3, - ) - .unwrap_err(); - assert_eq!(err, SolveFailure::GamutUnsupported); - } - #[test] fn degenerate_range_matches_explicit_target() { // SEAM (b): a degenerate range [t, t] solves identically to text(t). diff --git a/crates/labcolors-core/src/spaces/mod.rs b/crates/labcolors-core/src/spaces/mod.rs index f3c2e20d..2445d3cd 100644 --- a/crates/labcolors-core/src/spaces/mod.rs +++ b/crates/labcolors-core/src/spaces/mod.rs @@ -2,6 +2,7 @@ pub(crate) mod cam16; pub(crate) mod cat16; pub(crate) mod oklab; pub(crate) mod oklch; +#[cfg(test)] pub(crate) mod p3; pub(crate) mod srgb; pub(crate) mod vc; diff --git a/crates/labcolors-core/src/spaces/oklch.rs b/crates/labcolors-core/src/spaces/oklch.rs index 83874159..e7e7fe29 100644 --- a/crates/labcolors-core/src/spaces/oklch.rs +++ b/crates/labcolors-core/src/spaces/oklch.rs @@ -4,7 +4,7 @@ //! …в идеале бы выводить окончательно oklch». Солид и полупрозрачная роль //! отличаются ФИЗИКОЙ (α), но не синтаксисом: `oklch(L% C H)` и //! `oklch(L% C H / A)` — один парсер, одна форма, явно именованные -//! компоненты, готовность к широкому гамуту (P3 — этап gamut-aware солвера). +//! компоненты. //! //! Значения остаются решёнными в sRGB-гамуте: oklch здесь — система координат //! записи, не расширение гамута. Точность цифр подобрана под БАЙТ-ТОЧНЫЙ @@ -55,7 +55,7 @@ pub fn oklch_css_from_hex(hex: &str, alpha: Option) -> Result, значения -//! эталонной реализации colorjs.io). Передаточная функция Display P3 идентична -//! sRGB (IEC 61966-2-1 § 6.4) — переиспользуются [`super::srgb::srgb_gamma`] / -//! [`super::srgb::srgb_gamma_inv`], вторых копий кривой нет. -//! -//! Точность цифр подобрана под БАЙТ-ТОЧНЫЙ round-trip: `p3_css_from_hex` → -//! парс → XYZ → sRGB даёт исходные 8-битные байты на решётке с шагом 5 по -//! каждому каналу (около 140 тысяч цветов, включая края; тест -//! `round_trip_is_byte_exact_on_lattice`). Это не полный перебор куба. - -use super::srgb::{srgb_from_hex, srgb_gamma, srgb_to_xyz}; +//! This module is test-only until a complete output-profile release supplies +//! its own candidate domain, encoder, and final encoded verifier. The sole +//! retained operation is the physical XYZ(D65) to linear Display P3 transform +//! used by the private gamut-boundary regression. It is not an output +//! capability and exposes no public selector or CSS serializer. -// ------------------------------------------------------------------ -// linear Display P3 → XYZ(D65) -// ------------------------------------------------------------------ -#[rustfmt::skip] -const P3_TO_XYZ_D65: [[f64; 3]; 3] = [ - [ 0.486_570_948_648_216_15, 0.265_667_693_169_093_06, 0.198_217_285_234_362_5 ], - [ 0.228_974_564_069_748_78, 0.691_738_521_836_506_4, 0.079_286_914_093_745 ], - [ 0.0, 0.045_113_381_858_902_64, 1.043_944_368_900_976 ], -]; - -// ------------------------------------------------------------------ -// XYZ(D65) → linear Display P3 -// ------------------------------------------------------------------ #[rustfmt::skip] const XYZ_D65_TO_P3: [[f64; 3]; 3] = [ [ 2.493_496_911_941_425, -0.931_383_617_919_123_9, -0.402_710_784_450_716_84 ], @@ -54,367 +13,36 @@ const XYZ_D65_TO_P3: [[f64; 3]; 3] = [ [ 0.035_845_830_243_784_47, -0.076_172_389_268_041_82, 0.956_884_524_007_687_2 ], ]; -fn mat_vec_mul(m: [[f64; 3]; 3], v: [f64; 3]) -> [f64; 3] { - [ - m[0][0] * v[0] + m[0][1] * v[1] + m[0][2] * v[2], - m[1][0] * v[0] + m[1][1] * v[1] + m[1][2] * v[2], - m[2][0] * v[0] + m[2][1] * v[1] + m[2][2] * v[2], - ] -} - -/// XYZ(D65, Y∈[0,1]) → линейный Display P3. +/// XYZ(D65, Y in `[0, 1]`) to linear Display P3. pub(crate) fn xyz_to_p3_linear(xyz: [f64; 3]) -> [f64; 3] { - mat_vec_mul(XYZ_D65_TO_P3, xyz) -} - -/// Линейный Display P3 → XYZ(D65, Y∈[0,1]). -#[cfg_attr(not(test), allow(dead_code))] -pub(crate) fn p3_linear_to_xyz(rgb: [f64; 3]) -> [f64; 3] { - mat_vec_mul(P3_TO_XYZ_D65, rgb) -} - -/// Гашение вычислительного шума у краёв [0, 1]. -/// -/// Для sRGB-входа компоненты P3 математически лежат в [0, 1] (sRGB ⊂ P3); -/// за края может выйти только f64-шум цепочки матриц (≲1e-12, у белой точки — -/// две независимые деривации D65). Шум гасится, реальный выход за гамут — -/// честная ошибка вызывающего (сюда такие значения не приходят, пока солвер -/// работает в sRGB-гамуте; гард — на будущий gamut-aware этап). -const GAMUT_NOISE: f64 = 1e-9; - -fn clamp_gamut_noise(v: f64) -> Result { - if !(-GAMUT_NOISE..=1.0 + GAMUT_NOISE).contains(&v) { - return Err(format!("компонента P3 вне гамута: {v}")); - } - Ok(v.clamp(0.0, 1.0)) -} - -/// Гамма-кодированные компоненты Display P3 `[r, g, b]` (каждая в [0, 1]) -/// из sRGB-hex-солида. -/// -/// Путь: hex → линейный sRGB → XYZ(D65) → линейный P3 → передаточная кривая -/// (общая с sRGB, IEC 61966-2-1 § 6.4). -/// -/// # Errors -/// -/// `Err` — невалидный hex (пробрасывается из парсера) либо компонента вне -/// гамута сверх шумового эпсилона (недостижимо для валидного sRGB-входа). -pub fn p3_from_hex(hex: &str) -> Result<[f64; 3], String> { - let xyz = srgb_to_xyz(srgb_from_hex(hex)?); - let lin = xyz_to_p3_linear(xyz); - Ok([ - srgb_gamma(clamp_gamut_noise(lin[0])?), - srgb_gamma(clamp_gamut_noise(lin[1])?), - srgb_gamma(clamp_gamut_noise(lin[2])?), - ]) -} - -/// CSS-строка `color(display-p3 R G B)` / `color(display-p3 R G B / A)` из -/// sRGB-hex-солида и опциональной альфы. -/// -/// Точность: 6 знаков на компоненту — ошибка квантования печати ≤ 5·10⁻⁷ при -/// полушаге 8-битного канала ≈ 2·10⁻³, запас > 3 порядков; байт-точность -/// round-trip проверена тестом на решётке с шагом 5. Политика альфы — единая -/// (`super::oklch::css_alpha_suffix`): та же, что у oklch-эмиссии. -/// -/// # Errors -/// -/// `Err` — невалидный hex, неконечная альфа либо альфа вне `[0, 1]`. -pub fn p3_css_from_hex(hex: &str, alpha: Option) -> Result { - let [r, g, b] = p3_from_hex(hex)?; - let suffix = super::oklch::css_alpha_suffix(alpha)?; - Ok(format!("color(display-p3 {r:.6} {g:.6} {b:.6}{suffix})")) -} - -// ------------------------------------------------------------------ -// 8-битная решётка эмиссии P3 (этап 1 gamut-aware солвера, 2026-07-03). -// МЁРТВАЯ В ПРОД — только тесты до подключения этапом 2 (см. модульный -// раздел «Потребление»); каждая функция несёт allow(dead_code) для non-test. -// ------------------------------------------------------------------ - -/// 8-битное квантование линейного P3: передаточная кривая (общая с sRGB) → -/// байты. Решётка кандидатов будущего P3-солвера — зеркало sRGB-пути -/// (quantise + измерение на отданном значении). -/// -/// # Errors -/// -/// `Err` — компонента вне гамута P3 сверх шумового эпсилона: квантовать -/// такое значение как цвет нельзя. -#[cfg_attr(not(test), allow(dead_code))] // прод-потребитель — этап 2 -pub(crate) fn p3_bytes_from_linear(lin: [f64; 3]) -> Result<[u8; 3], String> { - let mut out = [0_u8; 3]; - for (i, &v) in lin.iter().enumerate() { - let encoded = srgb_gamma(clamp_gamut_noise(v)?); - out[i] = (encoded * 255.0).round() as u8; - } - Ok(out) -} - -/// Линейный P3 из 8-битных байтов решётки (обратный путь квантования). -#[cfg_attr(not(test), allow(dead_code))] // прод-потребитель — этап 2 -pub(crate) fn p3_linear_from_bytes(bytes: [u8; 3]) -> [f64; 3] { [ - super::srgb::srgb_gamma_inv(f64::from(bytes[0]) / 255.0), - super::srgb::srgb_gamma_inv(f64::from(bytes[1]) / 255.0), - super::srgb::srgb_gamma_inv(f64::from(bytes[2]) / 255.0), + XYZ_D65_TO_P3[0][0] * xyz[0] + XYZ_D65_TO_P3[0][1] * xyz[1] + XYZ_D65_TO_P3[0][2] * xyz[2], + XYZ_D65_TO_P3[1][0] * xyz[0] + XYZ_D65_TO_P3[1][1] * xyz[1] + XYZ_D65_TO_P3[1][2] * xyz[2], + XYZ_D65_TO_P3[2][0] * xyz[0] + XYZ_D65_TO_P3[2][1] * xyz[1] + XYZ_D65_TO_P3[2][2] * xyz[2], ] } -/// CSS-строка `color(display-p3 R G B [/ A])` из 8-битных байтов решётки. -/// Точность печати и политика альфы — те же, что у [`p3_css_from_hex`] -/// (6 знаков: полушаг канала ≈ 2·10⁻³, запас > 3 порядков; байт-точность -/// round-trip доказана тестом на решётке). -#[cfg_attr(not(test), allow(dead_code))] // прод-потребитель — этап 2 -pub(crate) fn p3_css_from_bytes(bytes: [u8; 3], alpha: Option) -> Result { - let r = f64::from(bytes[0]) / 255.0; - let g = f64::from(bytes[1]) / 255.0; - let b = f64::from(bytes[2]) / 255.0; - let suffix = super::oklch::css_alpha_suffix(alpha)?; - Ok(format!("color(display-p3 {r:.6} {g:.6} {b:.6}{suffix})")) -} - #[cfg(test)] mod tests { - use super::*; - use crate::spaces::srgb::{hex_from_srgb, srgb_gamma_inv, xyz_to_srgb}; - - /// Парсер эмитированной строки — эталонная реконструкция потребителя: - /// браузер декодирует компоненты той же передаточной кривой и тем же - /// матричным путём P3 → XYZ → sRGB. - fn parse_emitted(css: &str) -> (String, Option) { - let inner = css - .strip_prefix("color(display-p3 ") - .and_then(|s| s.strip_suffix(')')) - .expect("форма color(display-p3 ...)"); - let (rgb_str, alpha) = match inner.split_once(" / ") { - Some((rgb, a)) => (rgb, Some(a.parse::().expect("альфа — число"))), - None => (inner, None), - }; - let parts: Vec = rgb_str - .split_whitespace() - .map(|p| { - p.parse::() - .unwrap_or_else(|_| panic!("компонента не число: {p} в {css}")) - }) - .collect(); - assert_eq!(parts.len(), 3, "ровно R G B: {css}"); - let lin_p3 = [ - srgb_gamma_inv(parts[0]), - srgb_gamma_inv(parts[1]), - srgb_gamma_inv(parts[2]), - ]; - let xyz = p3_linear_to_xyz(lin_p3); - (hex_from_srgb(xyz_to_srgb(xyz)), alpha) - } - - /// Матрицы — взаимные обратные: P3 → XYZ → P3 тождественно до f64-шума. - #[test] - fn matrices_are_mutual_inverses() { - for rgb in [ - [1.0, 0.0, 0.0], - [0.0, 1.0, 0.0], - [0.0, 0.0, 1.0], - [1.0, 1.0, 1.0], - [0.25, 0.5, 0.75], - ] { - let back = xyz_to_p3_linear(p3_linear_to_xyz(rgb)); - for i in 0..3 { - assert!( - (back[i] - rgb[i]).abs() < 1e-12, - "P3 roundtrip канал {i}: {} vs {}", - back[i], - rgb[i] - ); - } - } - } - - /// Белые точки согласованы: sRGB-белый → P3 (1, 1, 1) до шума двух - /// независимых D65-дериваций (обе цепочки CSS Color 4). #[test] - fn srgb_white_maps_to_p3_white() { - let [r, g, b] = p3_from_hex("#FFFFFF").unwrap(); - for (ch, v) in [("r", r), ("g", g), ("b", b)] { - assert!((v - 1.0).abs() < 1e-6, "белый канал {ch}: {v}"); - } - } - - /// sRGB ⊂ P3: каждая точка решётки куба конвертируется без выхода за - /// гамут (кламп только шумового эпсилона — иначе p3_from_hex вернул бы Err). - /// Шаг 17 взаимно прост с 255 — решётка не выровнена по «удобным» байтам. - #[test] - fn srgb_cube_is_inside_p3_gamut() { - let steps: Vec = (0u16..=255).step_by(17).map(|v| v as u8).collect(); - for &r in &steps { - for &g in &steps { - for &b in &steps { - let hex = format!("#{r:02X}{g:02X}{b:02X}"); - p3_from_hex(&hex).unwrap_or_else(|e| panic!("{hex} вне P3: {e}")); - } - } - } - } - - /// Байт-точность round-trip на решётке 8-битного куба с шагом 5 (включая - /// края 0 и 255): формат → парс → P3 → XYZ → sRGB → те же байты. - #[test] - fn round_trip_is_byte_exact_on_lattice() { - let steps: Vec = (0u16..=255).step_by(5).map(|v| v as u8).collect(); - assert!(steps.contains(&0) && steps.contains(&255)); - for &r in &steps { - for &g in &steps { - for &b in &steps { - let hex = format!("#{r:02X}{g:02X}{b:02X}"); - let css = p3_css_from_hex(&hex, None).unwrap(); - let (back, alpha) = parse_emitted(&css); - assert_eq!(back, hex, "round-trip разошёлся: {css}"); - assert_eq!(alpha, None); - } - } - } - } - - /// Серые с альфой: полный грей-рамп байт-точен, альфа проходит как данные. - #[test] - fn round_trip_is_byte_exact_on_greys_with_alpha() { - for v in 0u16..=255 { - let v = v as u8; - let hex = format!("#{v:02X}{v:02X}{v:02X}"); - let css = p3_css_from_hex(&hex, Some(0.361)).unwrap(); - let (back, alpha) = parse_emitted(&css); - assert_eq!(back, hex, "grey round-trip разошёлся: {css}"); - assert_eq!(alpha, Some(0.361)); - } - } - - /// Политика alpha едина с oklch-эмиссией: любое недоменное значение — - /// ошибка. Даже малый clamp изменил бы публичное число и мог бы выдать - /// соседний конечный композит за сертифицированный. - #[test] - fn alpha_guard_shared_with_oklch() { - assert!(p3_css_from_hex("#101012", Some(f64::NAN)).is_err()); - assert!(p3_css_from_hex("#101012", Some(-10.0)).is_err()); - assert!(p3_css_from_hex("#101012", Some(2.0)).is_err()); - assert!(p3_css_from_hex("#101012", Some(-1e-7)).is_err()); - assert!(p3_css_from_hex("#101012", Some(1.0 + 1e-9)).is_err()); - assert!( - p3_css_from_hex("#101012", Some(0.0)) - .unwrap() - .ends_with(" / 0)") - ); - assert!( - p3_css_from_hex("#101012", Some(1.0)) - .unwrap() - .ends_with(" / 1)") - ); - } - - /// Форма строки — контракт потребителя: `color(display-p3 R G B [/ A])`, - /// компоненты в [0, 1], без знакового нуля. - #[test] - fn css_shape_is_the_contract() { - let solid = p3_css_from_hex("#3E87FF", None).unwrap(); - assert!(solid.starts_with("color(display-p3 ") && solid.ends_with(')')); - assert!(!solid.contains('/')); - assert!(!solid.contains("-0."), "signed zero запрещён: {solid}"); - let translucent = p3_css_from_hex("#101012", Some(0.122)).unwrap(); - assert!(translucent.contains(" / 0.122)")); - // Чистый sRGB-красный внутри P3 — менее насыщен, чем P3-красный: - // r < 1, g/b > 0 (иначе матрицы перепутаны). - let [r, g, b] = p3_from_hex("#FF0000").unwrap(); - assert!(r > 0.9 && r < 1.0, "P3 r красного: {r}"); - assert!(g > 0.0 && b > 0.0, "P3 g/b красного: {g}/{b}"); - } - - /// Этап 1 gamut-aware: стена P3 не уже sRGB-стены НИГДЕ (sRGB ⊂ P3) и - /// СТРОГО шире на насыщенных срединных светлотах (зелёная зона P3 — - /// самое сильное расширение). Сетка L × h покрывает обе ветки бисекции. - #[test] - fn p3_wall_dominates_srgb_wall() { + fn physical_p3_wall_contains_the_srgb_wall() { let mut strictly_wider_somewhere = false; for l10 in 2..=9 { - let l = f64::from(l10) / 10.0; - for h in (0..360).step_by(15) { - let h = f64::from(h); - let srgb = crate::scale::max_chroma_bisect(l, h); - let p3 = crate::scale::max_chroma_p3_bisect(l, h); + let lightness = f64::from(l10) / 10.0; + for hue in (0..360).step_by(15) { + let hue = f64::from(hue); + let srgb = crate::scale::max_chroma_bisect(lightness, hue); + let p3 = crate::scale::max_chroma_p3_bisect(lightness, hue); assert!( p3 >= srgb - 1e-9, - "P3-стена уже sRGB при L={l}, h={h}: {p3} < {srgb}" + "P3 wall is narrower than sRGB at L={lightness}, h={hue}: {p3} < {srgb}" ); - if p3 > srgb * 1.05 { - strictly_wider_somewhere = true; - } + strictly_wider_somewhere |= p3 > srgb * 1.05; } } assert!( strictly_wider_somewhere, - "P3 обязан быть строго шире sRGB хоть где-то (иначе матрицы выродились)" - ); - } - - /// Достижимость за sRGB-стеной: цвет с хромой между стенами (вне sRGB, - /// внутри P3) представим на 8-битной P3-решётке И ПЕРЕЖИВАЕТ квантование — - /// перечитанный с решётки цвет остаётся за sRGB-стеной. Это ровно то, - /// что этап 2 отдаст наружу. - #[test] - fn beyond_srgb_chroma_survives_the_p3_lattice() { - use crate::spaces::oklab::{oklab_to_srgb_linear, srgb_linear_to_oklab}; - // Зелёная срединная зона — максимальный разрыв стен. - let (l, h) = (0.75, 145.0); - let srgb_wall = crate::scale::max_chroma_bisect(l, h); - let p3_wall = crate::scale::max_chroma_p3_bisect(l, h); - assert!( - p3_wall > srgb_wall * 1.1, - "в зелёной зоне разрыв стен обязан быть ощутимым: {p3_wall} vs {srgb_wall}" + "the P3 matrix must produce a genuinely wider gamut somewhere" ); - let c = (srgb_wall + p3_wall) / 2.0; - let h_rad = h.to_radians(); - let lab = [l, c * h_rad.cos(), c * h_rad.sin()]; - let lin_p3 = xyz_to_p3_linear(srgb_to_xyz(oklab_to_srgb_linear(lab))); - let bytes = p3_bytes_from_linear(lin_p3).expect("между стенами — внутри P3"); - // Перечитываем с решётки и меряем хрому честно (на отданном значении). - let back = p3_linear_to_xyz(p3_linear_from_bytes(bytes)); - let back_lab = srgb_linear_to_oklab(crate::spaces::srgb::xyz_to_srgb(back)); - let back_c = (back_lab[1] * back_lab[1] + back_lab[2] * back_lab[2]).sqrt(); - assert!( - back_c > srgb_wall, - "квантование не должно ронять хрому обратно в sRGB: {back_c} <= {srgb_wall}" - ); - } - - /// Байт-точный round-trip решётки: байты → css-строка → парс компонент → - /// байты. Шаг 7 взаимно прост с 255 — решётка пробегает все классы вычетов. - #[test] - fn p3_lattice_css_round_trip_is_byte_exact() { - for r in (0..=255).step_by(7) { - for g in (0..=255).step_by(51) { - for b in (0..=255).step_by(51) { - let bytes = [r as u8, g as u8, b as u8]; - let css = p3_css_from_bytes(bytes, None).expect("байты валидны"); - let inner = css - .strip_prefix("color(display-p3 ") - .and_then(|s| s.strip_suffix(')')) - .expect("форма color(display-p3 ...)"); - let parts: Vec = inner - .split_whitespace() - .map(|p| p.parse::().expect("компонента — число")) - .collect(); - let parsed = [ - (parts[0] * 255.0).round() as u8, - (parts[1] * 255.0).round() as u8, - (parts[2] * 255.0).round() as u8, - ]; - assert_eq!(parsed, bytes, "byte round-trip сломан: {css}"); - } - } - } - } - - /// Гард решётки: не-цвет (за гамутом P3) не квантуется молча. - #[test] - fn out_of_gamut_linear_is_an_error_not_a_clamp() { - assert!(p3_bytes_from_linear([1.2, 0.5, 0.5]).is_err()); - assert!(p3_bytes_from_linear([-0.2, 0.5, 0.5]).is_err()); } } diff --git a/crates/labcolors-core/tests/property_invariants.rs b/crates/labcolors-core/tests/property_invariants.rs index 9a7b13e7..8b19c7f8 100644 --- a/crates/labcolors-core/tests/property_invariants.rs +++ b/crates/labcolors-core/tests/property_invariants.rs @@ -23,7 +23,7 @@ use labcolors_core::{ BgInput, Brand, Floor, GlowDecisionProfileV1, LadderPosition, LadderSource, NeutralAnchors, NeutralConfig, NeutralPick, NeutralTint, PaletteFamily, Resolved, RoleFailure, RoleRecipe, ThemeAnchors, ThemeConfig, ThemesConfig, VcPreset, ViewingConditions, oklch_from_hex, - p3_from_hex, resolve_named_set, srgb_encoded_from_hex, + resolve_named_set, srgb_encoded_from_hex, }; use proptest::prelude::*; use proptest::test_runner::{Config, RngAlgorithm, TestRng, TestRunner}; @@ -593,7 +593,7 @@ fn hued_brand_label_stays_in_family_coordinate_band_when_chromatic() { // СВОЙСТВО 6 — fuzz-устойчивость парсеров цвета (ноль паник на мусоре) // // КЛАСС: «парсер hex падает/паникует на враждебном/битом входе». Любая строка на -// входе `oklch_from_hex` / `srgb_encoded_from_hex` / `p3_from_hex` / `BgInput::solid` +// входе `oklch_from_hex` / `srgb_encoded_from_hex` / `BgInput::solid` // → аккуратный `Result` (Ok или Err), НИКОГДА не паника (паника развернула бы стек // и уронила бы тест). Класс-закрыватель для парсеров, читающих внешний ввод. // БЬЁТ НА МУТАЦИИ: замена валидации на `unwrap`/индексацию без границ → паника → RED. @@ -615,10 +615,9 @@ fn hex_like() -> impl Strategy { #[test] fn color_parsers_never_panic_on_arbitrary_input() { check(2000, hex_like(), |s| { - // Все четыре парсера обязаны вернуть Result, не паниковать. + // Все три парсера обязаны вернуть Result, не паниковать. let _ = oklch_from_hex(&s); let _ = srgb_encoded_from_hex(&s); - let _ = p3_from_hex(&s); let _ = BgInput::solid(&s); Ok(()) }); diff --git a/crates/labcolors-core/tests/solve_characterization.rs b/crates/labcolors-core/tests/solve_characterization.rs index ba2a2f34..ef7e8d30 100644 --- a/crates/labcolors-core/tests/solve_characterization.rs +++ b/crates/labcolors-core/tests/solve_characterization.rs @@ -262,7 +262,6 @@ fn outcome_line(result: &Result) -> String { bits(*floor), bits(*max_ratio) ), - Err(SolveFailure::GamutUnsupported) => "err gamut_unsupported".to_string(), Err(SolveFailure::InvalidInput(message)) => { format!("err invalid_input message={message:?}") } @@ -287,20 +286,6 @@ fn run_case(case: &CaseSpec) -> Result { fn observed_map() -> BTreeMap { let mut observed = BTreeMap::new(); - // Одна GamutUnsupported-строка поверх матрицы (у solve это внешний гейт). - let gamut_case = solve( - BgInput::solid("#FFFFFF").expect("literal background"), - Contract::text(60.0), - Hue::deg(0.0), - ChromaPolicy::Neutral, - &ViewingConditions::srgb(), - Gamut::DisplayP3, - ); - observed.insert( - "bg=#FFFFFF contract=text(60) floor=default hue=0 chroma=neutral gamut=display-p3" - .to_string(), - outcome_line(&gamut_case), - ); for case in matrix() { let previous = observed.insert(case_key(&case), outcome_line(&run_case(&case))); assert!( @@ -410,7 +395,6 @@ fn characterization_counters_are_non_vacuous() { "exceeds_range" => "exceeds_range", "bounded_search_exhausted" => "bounded_search_exhausted", "floor_unreachable" => "floor_unreachable", - "gamut_unsupported" => "gamut_unsupported", "invalid_input" => "invalid_input", "internal_invariant" => "internal_invariant", other => panic!("unknown error class {other}"), @@ -426,7 +410,6 @@ fn characterization_counters_are_non_vacuous() { "below_contrast_floor", "exceeds_range", "floor_unreachable", - "gamut_unsupported", "invalid_input", ] { assert!( @@ -441,10 +424,9 @@ fn characterization_counters_are_non_vacuous() { // квантования; walk в 2 distinct-шага пересекает всё остальное (фикс #44). // Эмпирически: сканы публичного API на миллионы вызовов (solid-фоны обеих // полярностей, серые и хроматические, hue-сетка, Neutral/Relative вплоть до - // 1.0, Floor::None/AaText/AaUi, |Lc| 7.3..112, srgb и dim surround; wide - // gamut не участвует — DisplayP3 умирает на внешнем гейте) не производят - // ни одного. Правда самого варианта (`closest_examined` локален, не глобален) - // запинена на его собственном шве: + // 1.0, Floor::None/AaText/AaUi, |Lc| 7.3..112, srgb и dim surround) не + // производят ни одного. Правда самого варианта (`closest_examined` локален, + // не глобален) запинена на его собственном шве: // `solve::tests::bounded_search_exhausted_is_local_not_global_counterexample`. // Появление исхода из этой матрицы = изменение поведения поиска, не «новый кейс». assert_eq!( @@ -464,7 +446,7 @@ fn characterization_counters_are_non_vacuous() { /// `solve_many(bg, jobs) == jobs.map(solve)` позиционно: успехи, каждый класс /// per-job ошибки, пустой вход, дубликаты и смешанные валидные/невалидные -/// задания; внешняя gamut-ошибка остаётся внешней и не сдвигает позиции. +/// задания. #[test] fn solve_many_is_positionally_identical_to_sequential_solve() { let vc = ViewingConditions::srgb(); @@ -600,13 +582,6 @@ fn solve_many_is_positionally_identical_to_sequential_solve() { .expect("empty batch") .is_empty() ); - - // Внешняя ошибка гамута — внешняя: Err всей партии, позиций нет. - let bg = BgInput::solid("#FFFFFF").expect("literal background"); - assert!(matches!( - solve_many(bg, &jobs, &vc, Gamut::DisplayP3), - Err(SolveFailure::GamutUnsupported) - )); } /// Позитивная характеризация JND-полосы против `recheck_against` — публичного @@ -701,7 +676,7 @@ fn jnd_band_resolves_within_budget_with_tolerant_acceptance() { /// Linux-x64 расходится РОВНО хвост ulp одного поля — Oklab-hue коррелята /// `h_ok` — в двух хроматических кейсах матрицы. Всё остальное (hex-байты, `lc`, /// `wcag_ratio`, `floor_override`, `jp`, `s`, все payload'ы ошибок) — -/// бит-идентично на всех 77 кейсах. Оба кейса — libm-разница +/// бит-идентично на всех 76 кейсах. Оба кейса — libm-разница /// (atan2/cbrt, 5 ulp на хроматике). У точного sRGB-серого `h_ok = 0` по /// определению, поэтому его прежний atan2-шум больше не является частью /// платформенного контракта. Рост этого множества — изменение численного @@ -728,7 +703,7 @@ fn platform_fixtures_agree_except_documented_hue_ulp_drift() { }; let mac = load(FIXTURE_MACOS_AARCH64); let linux = load(FIXTURE_LINUX_X64); - assert_eq!(mac.len(), 77, "macOS fixture cardinality"); + assert_eq!(mac.len(), 76, "macOS fixture cardinality"); assert_eq!( mac.keys().collect::>(), linux.keys().collect::>(), diff --git a/crates/labcolors-ffi/src/lib.rs b/crates/labcolors-ffi/src/lib.rs index 76a2c33d..ae5136f1 100644 --- a/crates/labcolors-ffi/src/lib.rs +++ b/crates/labcolors-ffi/src/lib.rs @@ -352,8 +352,6 @@ pub enum FailureCategory { Unresolved, /// Запрос не принадлежит объявленному domain. Rejected, - /// Запрос валиден, но capability не реализована. - Unsupported, } impl FailureCategory { @@ -362,7 +360,6 @@ impl FailureCategory { labcolors_core::SolveFailureCategory::Unreachable => Self::Unreachable, labcolors_core::SolveFailureCategory::Unresolved => Self::Unresolved, labcolors_core::SolveFailureCategory::Rejected => Self::Rejected, - labcolors_core::SolveFailureCategory::Unsupported => Self::Unsupported, } } } @@ -389,7 +386,7 @@ pub enum ColorError { key: String, }, /// Resolver не вернул цвет. Категория отделяет доказанную недостижимость - /// от unresolved, rejected и unsupported исходов. + /// от unresolved и rejected исходов. #[error("solve failure {category:?}/{code}")] Failure { /// Стабильная семантическая категория core failure. @@ -997,11 +994,6 @@ mod tests { FailureCategory::Unreachable, "floor_unreachable", ), - ( - U::GamutUnsupported, - FailureCategory::Unsupported, - "gamut_unsupported", - ), ( U::InvalidInput("fixture".into()), FailureCategory::Rejected, diff --git a/crates/labcolors-wasm/src/lib.rs b/crates/labcolors-wasm/src/lib.rs index 10722928..38b0a313 100644 --- a/crates/labcolors-wasm/src/lib.rs +++ b/crates/labcolors-wasm/src/lib.rs @@ -657,7 +657,7 @@ impl LabColors { /// /// Возвращает полный `ResolvedTheme`. Локальный `unresolved` остаётся /// типизированным исходом роли; ordinary `unreachable` отклоняет весь вызов - /// как `OutputConflictError`. Rejected/unsupported/internal также + /// как `OutputConflictError`. Rejected/internal также /// отклоняются атомарно: частичной темы или CSS не бывает. /// Ошибки границы — структурная форма `": "`, Rust-паника /// в JavaScript не разматывается. diff --git a/packages/colors/bench/wasm.json b/packages/colors/bench/wasm.json index 212f1cba..528207e5 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-29955098154", + "source": "github-actions-run-29962821215", "platform": "linux-x64", - "rawBytes": 376977 + "rawBytes": 376707 }, "policy": { - "maxRawBytes": 376977, - "basis": "pair-taxonomy-hard-delete", + "maxRawBytes": 376707, + "basis": "p3-promise-hard-delete", "gzip": "diagnostic-only" } } diff --git a/packages/colors/smoke.consumer.ts b/packages/colors/smoke.consumer.ts index 64f6c003..59e591f7 100644 --- a/packages/colors/smoke.consumer.ts +++ b/packages/colors/smoke.consumer.ts @@ -66,15 +66,6 @@ requireFailure({ code: "invalid_input", message: "x", }); -requireFailure({ - kind: "failure", - cssVar: "--lab-example", - // @ts-expect-error unsupported closes the whole resolve and cannot be role data. - category: "unsupported", - code: "gamut_unsupported", - message: "x", -}); - const admittedFailureCategory: FailureCategory = "unresolved"; void admittedFailureCategory; diff --git a/packages/colors/test/release-contract.test.mjs b/packages/colors/test/release-contract.test.mjs index b2ff182b..b8c22103 100644 --- a/packages/colors/test/release-contract.test.mjs +++ b/packages/colors/test/release-contract.test.mjs @@ -1012,7 +1012,6 @@ test("publish artifact validator executes and rejects identity or byte drift", ( "stable-cam16-glow-target-or-maximum-selection", "renderer-or-output-pipeline-equivalence", "spatial-glow-field", - "display-p3", ], artifacts: { tarball: { @@ -1228,10 +1227,9 @@ test("release checker rejects solve failure wire drift", () => { ["unreachable", "exceeds_range"], ["unresolved", "bounded_search_exhausted"], ["unreachable", "floor_unreachable"], - ["unsupported", "gamut_unsupported"], ["rejected", "invalid_input"], ]; - assert.equal(boundaryRows.length, 6, "public core failure dictionary changed"); + assert.equal(boundaryRows.length, 5, "public core failure dictionary changed"); for (const [category, code] of boundaryRows) { assert.doesNotThrow(() => validateSolveFailurePair(category, code)); const wrongCategory = category === "unreachable" ? "rejected" : "unreachable"; diff --git a/scripts/check-wasm-size-budget.mjs b/scripts/check-wasm-size-budget.mjs index 33b83e7c..a24ddff4 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 = - "224d92604e5ee861da0c09e219da91f0807c4d5cb3c8733f23dff72066ecdb08"; + "035cece04afa7ea37e819c2432c2238b1e902399c31db01c39fbd67e1e299018"; const SCHEMA_VERSION = 1; const CANONICAL_ARTIFACT = "packages/colors/pkg/labcolors_bg.wasm"; diff --git a/scripts/verify-package-release.mjs b/scripts/verify-package-release.mjs index 5b64dc1b..3824ae6f 100644 --- a/scripts/verify-package-release.mjs +++ b/scripts/verify-package-release.mjs @@ -700,7 +700,6 @@ const SOLVE_FAILURE_CATEGORY_BY_CODE = new Map([ ["exceeds_range", "unreachable"], ["bounded_search_exhausted", "unresolved"], ["floor_unreachable", "unreachable"], - ["gamut_unsupported", "unsupported"], ["invalid_input", "rejected"], ]); @@ -1722,7 +1721,6 @@ export async function verifyPackageRelease() { "stable-cam16-glow-target-or-maximum-selection", "renderer-or-output-pipeline-equivalence", "spatial-glow-field", - "display-p3", ], artifacts: { tarball, diff --git a/scripts/verify_point_support_surplus.py b/scripts/verify_point_support_surplus.py index 69816a19..feb34d3f 100644 --- 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 = ( - "6b81edc2d56bdca78a4d7c579c79e2791dc59e9bf2626fd22ddc7aa164dfa5c8" + "2e63424882231ec2f00ae79911bf74cfb935bb96f11368bce53240468f7f2dd4" ) EXPECTED_Q55_PROOF_SHA256 = ( "ac59cf89503170c789223b91d775213a19d4e571ef930f2ea609fcd51b14defd" From 9ff2504726d1631b466d673a3dd9b8bae1e72c39 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Thu, 23 Jul 2026 02:16:22 +0300 Subject: [PATCH 33/58] core: make compiled programs the sole session-generation owner --- ...rt-reference-surplus-q55-bps-proof-v1.json | 2 +- .../src/generic_boundary_tests.rs | 53 +++++++- crates/labcolors-core/src/point_support.rs | 6 + .../src/program_joint_integration_tests.rs | 108 +++++++++++++++- crates/labcolors-core/src/program_session.rs | 115 ++++++++++++------ .../src/program_session_tests.rs | 33 +++-- crates/labcolors-core/src/session.rs | 24 +++- crates/labcolors-core/src/session_tests.rs | 87 ++++++++++++- scripts/verify_point_support_surplus.py | 4 +- 9 files changed, 367 insertions(+), 65 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 1e529242..5c918468 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":"fd8cc51bc850523f600cc850b10af56afb450dccaeb928c795d2c45c421df8e4","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":"2e63424882231ec2f00ae79911bf74cfb935bb96f11368bce53240468f7f2dd4","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":"e361cf144e0630a4f2ff52261e6515c9394188fc1351c97b98ea9c0ec87ec39d"},{"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":"a58acbae5b06a8cd9a45adf93ddfbdfb569c5916b60140bd4046182aab2a9518"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"0f0e0ed726aba4defc750b793a76435a3f3825e91002c2cd90d982be7260f180"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"abed15a542b5e8031f0ebfe36138232920c7898c9eae1a2037c6455033ecd91e"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"945755f2eb91eb21aeee9b7a7b0e67b093d7e766af35a121480003669d88a053"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"0a1fdf10c9ecae497cfcd447a6af937f2b07d43b81b2cd28a50d998e11197fe9"},{"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":"a8f1b4f65a45bf18ad8c2ae21cc88e5cd270a5ee438d625153b66d26660a7e90"} +{"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":"d731e6b3d068906ee02ebd90611b0d101dda6db4b0c98ff9dc4bdc79058618a8","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":"f5e813b21df3bad8c93fa3525190715110b4965792f5b7a2a0bfde7a3b4297d3","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":"e361cf144e0630a4f2ff52261e6515c9394188fc1351c97b98ea9c0ec87ec39d"},{"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":"a58acbae5b06a8cd9a45adf93ddfbdfb569c5916b60140bd4046182aab2a9518"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"0f0e0ed726aba4defc750b793a76435a3f3825e91002c2cd90d982be7260f180"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"abed15a542b5e8031f0ebfe36138232920c7898c9eae1a2037c6455033ecd91e"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"0755210e3e591d7049f293a0f0b7647681631f32feee5ad7d3b3309cffca8f9d"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"ab36d9e9339cf1b1030963d78eba42e7f898b4d3757712c7908e1d696bc620c9"},{"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":"a201ca9d971d9b7b9928ccef498752c837ce17d6feb1b2ee8d2d7187c3cebb5e"} diff --git a/crates/labcolors-core/src/generic_boundary_tests.rs b/crates/labcolors-core/src/generic_boundary_tests.rs index 38e750e4..33956b55 100644 --- a/crates/labcolors-core/src/generic_boundary_tests.rs +++ b/crates/labcolors-core/src/generic_boundary_tests.rs @@ -207,8 +207,11 @@ fn shared_observation_ssot_has_one_backing_without_lifecycle_or_adapter_facades( "production must have exactly one generic revision-bound Session owner", ); for required in [ + "type OwnerLease;", "type Verified: SessionEvidenceV1;", "type Violation: SessionEvidenceV1;", + "fn try_acquire_owner(&self) -> Option;", + "SessionUpdateError::OwnerExpired", ".is_same_binding_as(expected_observation)", "SessionUpdateError::EvidenceBindingInvariant", ] { @@ -239,7 +242,6 @@ fn shared_observation_ssot_has_one_backing_without_lifecycle_or_adapter_facades( "into_session_recheck", "ObservationStreamBinding", "ProgramExpired", - "Weak<", ] { assert!( !source.contains(forbidden), @@ -247,6 +249,31 @@ fn shared_observation_ssot_has_one_backing_without_lifecycle_or_adapter_facades( ); } } + for (path, source) in [ + ("session.rs", SESSION_SOURCE), + ("point_support.rs", POINT_SUPPORT_SOURCE), + ] { + assert!( + !source.contains("Weak<"), + "{path} must not create another weak ownership boundary", + ); + } + + let update = normalized_source_scope( + SESSION_SOURCE, + "pub(crate) fn update(", + "/// Move exactly one retained verified witness", + ); + let owner_preflight = update + .find(".try_acquire_owner()") + .expect("Session update must acquire the exact owner generation"); + let admission = update + .find("prepare_observation(") + .expect("Session update must perform canonical admission"); + assert!( + owner_preflight < admission, + "owner expiry must precede raw admission and physical execution", + ); let consuming_entry = source_scope( POINT_SUPPORT_SOURCE, @@ -419,9 +446,31 @@ fn program_session_owns_context_bound_lcs_evidence_and_one_session_scratch_cache let plan = source_scope( PROGRAM_SESSION_SOURCE, - "pub struct ProgramSessionPlan", + "pub(crate) struct ProgramSessionPlan", "impl session_private::PlanSealed for ProgramSessionPlan", ); + assert_eq!( + plan.matches("owner_generation: Weak>,") + .count(), + 1, + "a Program Session must hold exactly one weak compiled-generation binding", + ); + assert!( + !plan.contains("epoch: Rc>,"), + "a Program Session must not prolong its CompiledProgram owner", + ); + let compiled = source_scope( + PROGRAM_SESSION_SOURCE, + "pub struct CompiledProgram", + "impl CompiledProgram", + ); + assert_eq!( + compiled + .matches("owner_generation: Rc>,") + .count(), + 1, + "CompiledProgram must be the one strong owner of its generation", + ); assert_eq!( plan.matches("modeled_occurrences: Vec>,") .count(), diff --git a/crates/labcolors-core/src/point_support.rs b/crates/labcolors-core/src/point_support.rs index bb997588..94a01b54 100644 --- a/crates/labcolors-core/src/point_support.rs +++ b/crates/labcolors-core/src/point_support.rs @@ -322,16 +322,22 @@ impl CompiledPointSupportRecheckV1 { impl session_private::PlanSealed for CompiledPointSupportRecheckV1 {} impl SessionPlanV1 for CompiledPointSupportRecheckV1 { + type OwnerLease = (); type Verified = VerifiedPointSupportV1; type Violation = PointSupportViolationV1; type Error = PointSupportEvaluationErrorV1; + fn try_acquire_owner(&self) -> Option { + Some(()) + } + fn observation_schema(&self) -> &CanonicalObservationSchemaV1 { &self.surface_schema } fn evaluate( &mut self, + _owner: &Self::OwnerLease, observation: RevisionBoundObservationV1, _permit: SessionObservationBindingPermitV1, ) -> Result, Self::Error> { diff --git a/crates/labcolors-core/src/program_joint_integration_tests.rs b/crates/labcolors-core/src/program_joint_integration_tests.rs index a04d8b3c..db1ce5d2 100644 --- a/crates/labcolors-core/src/program_joint_integration_tests.rs +++ b/crates/labcolors-core/src/program_joint_integration_tests.rs @@ -9,8 +9,9 @@ use crate::lcs_occurrence::{ BackgroundLuminanceRatio, ColorSignal, IEC_SRGB_D65_XYZ_FRAME_V1, SurroundProfileId, }; use crate::observation::{ - ObservationGroupId, ObservationPayloadInput, ObservationStreamId, ObservationUpdateInput, - ObservedScenarioSetInput, Revision, ScenarioId, ScenarioInput, SurfaceInputBinding, + ObservationGroupId, ObservationHeadViewV1, ObservationPayloadInput, ObservationStreamId, + ObservationUpdateInput, ObservedScenarioSetInput, Revision, ScenarioId, ScenarioInput, + SurfaceInputBinding, }; use crate::program_session::{ CompositionProfile, ConstraintId, ConstraintInvocation, ConstraintSet, @@ -1077,6 +1078,109 @@ fn every_fallible_fixed_preflight_reservation_precedes_evaluator_work() { } } +fn counting_fixed_program( + evaluator: CountingProgramWcag22Srgb8V1, +) -> crate::program_session::CompiledProgram { + Program::new( + vec![Source::new(SOURCE, signal(0xFF))], + vec![Target::fixed(TARGET, SOURCE)], + ObservationGroup::new(GROUP, vec![SURFACE_PORT]), + vec![], + vec![Paint::Solid { + id: PAINT, + target: TARGET, + }], + vec![Surface::Input { + id: BACKDROP, + input: SURFACE_PORT, + }], + vec![Occurrence::new( + OCCURRENCE, + PAINT, + BACKDROP, + CompositionProfile::EncodedSrgb8SourceOverV1, + appearance_context(), + )], + ConstraintSet::new( + vec![ConstraintInvocation::hard( + ConstraintId::new(1), + OCCURRENCE, + Wcag22CriterionV1::Sc143TextLargeScale, + )], + vec![], + ), + vec![OutputBinding::new(OUTPUT, PAINT)], + evaluator, + ) + .compile() + .unwrap() +} + +#[test] +fn expired_program_generation_precedes_composition_and_evaluation_without_allocation() { + let evaluator = CountingProgramWcag22Srgb8V1::default(); + let calls = evaluator.clone(); + let compiled = counting_fixed_program(evaluator); + let mut session = compiled.instantiate(STREAM).unwrap(); + + crate::composition::reset_source_over_evaluation_count(); + let SessionState::Ready { current } = session.update(update(1, 0x00)).unwrap() else { + panic!("control generation must certify"); + }; + assert_eq!(current.report().observation().revision(), Revision::new(1)); + assert_eq!(calls.calls().len(), 1); + assert_eq!(crate::composition::source_over_evaluation_count(), 1); + + drop(compiled); + let expired_update = update(2, 0x00); + let (error, allocations) = crate::test_support::measured_allocations(|| { + session.update(expired_update).map(|_| ()).unwrap_err() + }); + assert_eq!(error, SessionUpdateError::OwnerExpired); + assert_eq!(allocations, 0); + assert_eq!(calls.calls().len(), 1); + assert_eq!(crate::composition::source_over_evaluation_count(), 1); + assert_eq!(session.raw_head().revision(), Some(Revision::new(1))); + let SessionState::Ready { current } = session.state() else { + panic!("expiry must retain the previous committed state"); + }; + assert_eq!(current.report().observation().revision(), Revision::new(1)); +} + +#[test] +fn equivalent_recompiled_owner_is_a_new_generation_and_cannot_revive_old_sessions() { + let first_evaluator = CountingProgramWcag22Srgb8V1::default(); + let first_calls = first_evaluator.clone(); + let mut compiled = counting_fixed_program(first_evaluator); + let mut old_session = compiled.instantiate(STREAM).unwrap(); + assert!(matches!( + old_session.update(update(1, 0x00)).unwrap(), + SessionState::Ready { .. } + )); + + let replacement_evaluator = CountingProgramWcag22Srgb8V1::default(); + let replacement_calls = replacement_evaluator.clone(); + compiled = counting_fixed_program(replacement_evaluator); + assert!(matches!( + old_session.update(update(2, 0x00)), + Err(SessionUpdateError::OwnerExpired), + )); + assert_eq!(first_calls.calls().len(), 1); + assert!(replacement_calls.calls().is_empty()); + assert_eq!(old_session.raw_head().revision(), Some(Revision::new(1))); + + let mut replacement_session = compiled.instantiate(STREAM).unwrap(); + assert!(matches!( + replacement_session.update(update(1, 0x00)).unwrap(), + SessionState::Ready { .. } + )); + assert_eq!(replacement_calls.calls().len(), 1); + assert!(matches!( + replacement_session.raw_head(), + ObservationHeadViewV1::Observed(_) + )); +} + #[test] fn final_recheck_violation_is_typed_and_retains_the_previous_certificate() { let evaluator = FinalRecheckMutantProgramEvaluatorV1::default(); diff --git a/crates/labcolors-core/src/program_session.rs b/crates/labcolors-core/src/program_session.rs index 1f713c9b..97e668b4 100644 --- a/crates/labcolors-core/src/program_session.rs +++ b/crates/labcolors-core/src/program_session.rs @@ -11,7 +11,7 @@ //! is renderer observation or human-subject evidence. use std::marker::PhantomData; -use std::rc::Rc; +use std::rc::{Rc, Weak}; use crate::Srgb8; use crate::appearance::{ @@ -555,7 +555,7 @@ where pub fn compile(self) -> Result, ProgramCompileError> { prepare_program(self).map(|epoch| CompiledProgram { - epoch: Rc::new(epoch), + owner_generation: Rc::new(epoch), }) } } @@ -750,13 +750,21 @@ where joint_selection: Option, } +/// Transaction-local strong pin for one exact compiled Program generation. +/// Construction is possible only by upgrading a Session plan's weak binding; +/// the contained epoch never becomes an independently shareable API. +pub(crate) struct ProgramOwnerLeaseV1(Rc>) +where + Evaluation: ProgramPointEvaluatorV1, + ProgramPointInvocation: Copy; + /// Fully validated immutable Program, not yet attached to runtime. pub struct CompiledProgram where Evaluation: ProgramPointEvaluatorV1, ProgramPointInvocation: Copy, { - epoch: Rc>, + owner_generation: Rc>, } impl CompiledProgram @@ -765,53 +773,55 @@ where ProgramPointInvocation: Copy, { pub fn observation_group_id(&self) -> ObservationGroupId { - self.epoch.observation_group.id + self.owner_generation.observation_group.id } pub fn surface_input_ports(&self) -> &[SurfaceInputPortId] { - self.epoch.observation_group.schema.as_slice() + self.owner_generation.observation_group.schema.as_slice() } pub fn constraint_ids(&self) -> impl ExactSizeIterator + '_ { - self.epoch + self.owner_generation .constraints .iter() .map(|constraint| constraint.id) } pub fn outputs(&self) -> impl ExactSizeIterator + '_ { - self.epoch + self.owner_generation .outputs .iter() .map(|output| (output.output, output.paint_id)) } - /// Create one independent stream-affine Session from the immutable - /// compiled epoch. The graph/evaluator/schema stay shared by strong - /// ownership; mutable bindings and workspace belong only to this Session. + /// Create one independent stream-affine Session for this exact compiled + /// owner generation. Mutable bindings and workspace belong to the Session, + /// while executable graph/evaluator state is reached only through a weak + /// generation binding and expires when this owner is dropped or replaced. pub(crate) fn instantiate( &self, stream: ObservationStreamId, ) -> Result>, ProgramSessionInstantiateError> { let bindings = self - .epoch + .owner_generation .binding_template .try_clone_v1() .map_err(map_session_instantiate_error)?; let workspace = self - .epoch + .owner_generation .graph .new_workspace() .map_err(map_session_instantiate_error)?; let mut modeled_occurrences = Vec::new(); modeled_occurrences - .try_reserve_exact(self.epoch.occurrence_contexts.len()) + .try_reserve_exact(self.owner_generation.occurrence_contexts.len()) .map_err(|_| ProgramSessionInstantiateError::ResourceExhausted)?; - modeled_occurrences.resize(self.epoch.occurrence_contexts.len(), None); + modeled_occurrences.resize(self.owner_generation.occurrence_contexts.len(), None); Ok(Session::new( stream, ProgramSessionPlan { - epoch: Rc::clone(&self.epoch), + owner_generation: Rc::downgrade(&self.owner_generation), + schema: self.owner_generation.observation_group.schema.clone(), bindings, workspace, modeled_occurrences, @@ -1180,6 +1190,28 @@ where counts: ProgramEvaluationCellCountsV1, } +struct SelectedProgramEvaluationBuffersV1 +where + Evaluation: ProgramPointEvaluatorV1, +{ + cells: Vec>, + outputs: Vec, + expected_cell_count: usize, +} + +impl PreparedProgramEvaluationBuffersV1 +where + Evaluation: ProgramPointEvaluatorV1, +{ + fn take_selected(&mut self) -> SelectedProgramEvaluationBuffersV1 { + SelectedProgramEvaluationBuffersV1 { + cells: std::mem::take(&mut self.selected_cells), + outputs: std::mem::take(&mut self.outputs), + expected_cell_count: self.counts.selected, + } + } +} + fn prepare_program_evaluation_buffers( epoch: &ProgramEpochV1, observation: &RevisionBoundObservationV1, @@ -1223,13 +1255,16 @@ where }) } -/// Per-Session mutable execution state backed by one strong immutable epoch. -pub struct ProgramSessionPlan +/// Per-Session mutable execution state bound weakly to one immutable compiled +/// owner generation. A transaction pins the generation before raw admission; +/// the Session itself cannot prolong the owner lifetime. +pub(crate) struct ProgramSessionPlan where Evaluation: ProgramPointEvaluatorV1, ProgramPointInvocation: Copy, { - epoch: Rc>, + owner_generation: Weak>, + schema: CanonicalObservationSchemaV1, bindings: AdmittedAppearanceBindings, workspace: AppearanceWorkspace, modeled_occurrences: Vec>, @@ -1247,61 +1282,65 @@ where Evaluation: ProgramPointEvaluatorV1, ProgramPointInvocation: Copy, { + type OwnerLease = ProgramOwnerLeaseV1; type Verified = ProgramVerifiedV1; type Violation = ProgramConflictV1; type Error = ProgramSessionEvaluationError>; + fn try_acquire_owner(&self) -> Option { + self.owner_generation.upgrade().map(ProgramOwnerLeaseV1) + } + fn observation_schema(&self) -> &CanonicalObservationSchemaV1 { - &self.epoch.observation_group.schema + &self.schema } fn evaluate( &mut self, + owner: &Self::OwnerLease, observation: RevisionBoundObservationV1, _permit: SessionObservationBindingPermitV1, ) -> Result, Self::Error> { - evaluate_program_session(self, observation) + evaluate_program_session(self, &owner.0, observation) } } fn evaluate_program_session( plan: &mut ProgramSessionPlan, + epoch: &ProgramEpochV1, observation: RevisionBoundObservationV1, ) -> ProgramSessionEvaluationResult where Evaluation: ProgramPointEvaluatorV1, ProgramPointInvocation: Copy, { - let epoch = Rc::clone(&plan.epoch); let Some(selection) = &epoch.joint_selection else { - let mut buffers = prepare_program_evaluation_buffers(&epoch, &observation, None)?; + let mut buffers = prepare_program_evaluation_buffers(epoch, &observation, None)?; return collect_program_candidate_into( plan, + epoch, observation, None, 1, - std::mem::take(&mut buffers.selected_cells), - std::mem::take(&mut buffers.outputs), - buffers.counts.selected, + buffers.take_selected(), ); }; let state_count = selection.order.tuples().len(); - let mut buffers = prepare_program_evaluation_buffers(&epoch, &observation, Some(state_count))?; + let mut buffers = prepare_program_evaluation_buffers(epoch, &observation, Some(state_count))?; for (state_index, tuple) in selection.order.tuples().enumerate() { apply_joint_candidate(plan, &epoch.finite_targets, tuple)?; - if !scan_program_candidate(plan, &observation, state_index, None, None)? { + if !scan_program_candidate(plan, epoch, &observation, state_index, None, None)? { // A selected tuple is never certified from its allocation-free // search pass. Re-apply and collect fresh terminal evidence. apply_joint_candidate(plan, &epoch.finite_targets, tuple)?; match collect_program_candidate_into( plan, + epoch, observation.clone(), Some(state_index), state_index + 1, - std::mem::take(&mut buffers.selected_cells), - std::mem::take(&mut buffers.outputs), - buffers.counts.selected, + buffers.take_selected(), )? { SessionDecision::Verified(verified) => { return Ok(SessionDecision::Verified(verified)); @@ -1335,6 +1374,7 @@ where apply_joint_candidate(plan, &epoch.finite_targets, tuple)?; if !scan_program_candidate( plan, + epoch, &observation, state_index, Some(&mut buffers.conflict_cells), @@ -1382,27 +1422,32 @@ where fn collect_program_candidate_into( plan: &mut ProgramSessionPlan, + epoch: &ProgramEpochV1, observation: RevisionBoundObservationV1, selected_state_index: Option, considered_state_count: usize, - mut cells: Vec>, - mut outputs: Vec, - expected_cell_count: usize, + buffers: SelectedProgramEvaluationBuffersV1, ) -> ProgramSessionEvaluationResult where Evaluation: ProgramPointEvaluatorV1, ProgramPointInvocation: Copy, { + let SelectedProgramEvaluationBuffersV1 { + mut cells, + mut outputs, + expected_cell_count, + } = buffers; if !cells.is_empty() || cells.capacity() < expected_cell_count || !outputs.is_empty() - || outputs.capacity() < plan.epoch.outputs.len() + || outputs.capacity() < epoch.outputs.len() { return Err(ProgramSessionEvaluationError::InternalInvariant); } let candidate_state_index = selected_state_index.unwrap_or(0); let has_hard_violation = scan_program_candidate( plan, + epoch, &observation, candidate_state_index, Some(&mut cells), @@ -1428,6 +1473,7 @@ where fn scan_program_candidate( plan: &mut ProgramSessionPlan, + epoch: &ProgramEpochV1, observation: &RevisionBoundObservationV1, candidate_state_index: usize, mut cells: Option<&mut Vec>>, @@ -1437,7 +1483,6 @@ where Evaluation: ProgramPointEvaluatorV1, ProgramPointInvocation: Copy, { - let epoch = &plan.epoch; let schema = &epoch.observation_group.schema; if !observation.shares_schema_backing_with(schema) { observation diff --git a/crates/labcolors-core/src/program_session_tests.rs b/crates/labcolors-core/src/program_session_tests.rs index 7618e9ae..50059864 100644 --- a/crates/labcolors-core/src/program_session_tests.rs +++ b/crates/labcolors-core/src/program_session_tests.rs @@ -16,7 +16,7 @@ use crate::program_session::{ Source, SourceId, Surface, Target, TargetId, canonical_surface_input_port_sequence_matches, check_render_node_count, }; -use crate::session::SessionState; +use crate::session::{SessionState, SessionUpdateError}; const SOURCE: SourceId = SourceId::new(1); const TARGET: TargetId = TargetId::new(1); @@ -521,7 +521,7 @@ fn canonical_helpers_and_checked_cardinality_fail_closed() { } #[test] -fn independently_instantiated_streams_survive_the_compiled_handle() { +fn independently_instantiated_streams_expire_with_their_compiled_owner_generation() { let compiled = exact_compiled(ConstraintSet::new( vec![ConstraintInvocation::hard( REQUIRED, @@ -534,23 +534,18 @@ fn independently_instantiated_streams_survive_the_compiled_handle() { let mut second = compiled.instantiate(STREAM_B).unwrap(); drop(compiled); - let first_state = first - .update(observed_update(STREAM_A, 1, &[(1, [0xFF; 3])])) - .unwrap(); - let SessionState::Ready { current: first } = first_state else { - panic!("first independent stream must verify"); - }; - assert_eq!(first.outputs().len(), 1); - - let second_state = second - .update(observed_update(STREAM_B, 9, &[(2, [0xFF; 3])])) - .unwrap(); - let SessionState::Ready { current: second } = second_state else { - panic!("second independent stream must verify after compiled handle drop"); - }; - assert_eq!(second.outputs().len(), 1); - assert_eq!(first.report().observation().revision(), Revision::new(1)); - assert_eq!(second.report().observation().revision(), Revision::new(9)); + assert!(matches!( + first.update(observed_update(STREAM_A, 1, &[(1, [0xFF; 3])])), + Err(SessionUpdateError::OwnerExpired), + )); + assert!(matches!( + second.update(observed_update(STREAM_B, 9, &[(2, [0xFF; 3])])), + Err(SessionUpdateError::OwnerExpired), + )); + assert!(matches!(first.state(), SessionState::Waiting)); + assert!(matches!(second.state(), SessionState::Waiting)); + assert_eq!(first.raw_head(), ObservationHeadViewV1::Empty); + assert_eq!(second.raw_head(), ObservationHeadViewV1::Empty); } #[test] diff --git a/crates/labcolors-core/src/session.rs b/crates/labcolors-core/src/session.rs index 2497219b..3e8643d3 100644 --- a/crates/labcolors-core/src/session.rs +++ b/crates/labcolors-core/src/session.rs @@ -66,14 +66,21 @@ where /// A compiled, statically dispatched evaluator used by the sole [`Session`] /// lifecycle. Implementations own their per-Session scratch directly. pub(crate) trait SessionPlanV1: private::PlanSealed { + /// One owned lease over the exact compiled owner generation used by an + /// update. Acquiring it is the first operation in the transaction, so an + /// expired plan cannot admit raw state or reach physical execution. + type OwnerLease; type Verified: SessionEvidenceV1; type Violation: SessionEvidenceV1; type Error; + fn try_acquire_owner(&self) -> Option; + fn observation_schema(&self) -> &CanonicalObservationSchemaV1; fn evaluate( &mut self, + owner: &Self::OwnerLease, observation: RevisionBoundObservationV1, permit: SessionObservationBindingPermitV1, ) -> Result, Self::Error>; @@ -128,6 +135,7 @@ impl ObservationOwnerV1 for SessionObservationHeadV1 { /// An update failed before either raw-head or lifecycle commit. #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) enum SessionUpdateError { + OwnerExpired, Observation(ObservationError), Plan(PlanError), EvidenceBindingInvariant, @@ -139,8 +147,10 @@ type SessionUpdateResult<'session, Plan> = Result< >; /// The only production owner of revision admission and evaluator lifecycle. -/// `Plan` is monomorphized; there is no plan enum, dynamic dispatch, adapter, -/// weak owner or expiration branch. +/// `Plan` is monomorphized; there is no plan enum, dynamic dispatch or adapter. +/// A plan may keep only a weak reference to its compiled owner generation; +/// every update pins that exact generation before admission and releases it +/// after commit or rollback. #[derive(Debug)] pub(crate) struct Session { stream: ObservationStreamId, @@ -179,6 +189,10 @@ impl Session { &mut self, update: ObservationUpdateInput, ) -> SessionUpdateResult<'_, Plan> { + let owner = self + .plan + .try_acquire_owner() + .ok_or(SessionUpdateError::OwnerExpired)?; let prepared = prepare_observation(&mut self.raw_head, self.stream, &self.schema, update) .map_err(SessionUpdateError::Observation)?; @@ -205,7 +219,11 @@ impl Session { let next_raw_head = SessionObservationHeadV1::Observed(observation.clone()); let decision = self .plan - .evaluate(observation, SessionObservationBindingPermitV1::mint()) + .evaluate( + &owner, + observation, + SessionObservationBindingPermitV1::mint(), + ) .map_err(SessionUpdateError::Plan)?; let SessionObservationHeadV1::Observed(expected_observation) = &next_raw_head else { diff --git a/crates/labcolors-core/src/session_tests.rs b/crates/labcolors-core/src/session_tests.rs index 454090b9..2da91b7f 100644 --- a/crates/labcolors-core/src/session_tests.rs +++ b/crates/labcolors-core/src/session_tests.rs @@ -82,16 +82,22 @@ struct SentinelPlan { impl session_private::PlanSealed for SentinelPlan {} impl SessionPlanV1 for SentinelPlan { + type OwnerLease = (); type Verified = SentinelVerified; type Violation = SentinelViolation; type Error = SentinelError; + fn try_acquire_owner(&self) -> Option { + Some(()) + } + fn observation_schema(&self) -> &CanonicalObservationSchemaV1 { &self.schema } fn evaluate( &mut self, + _owner: &Self::OwnerLease, observation: RevisionBoundObservationV1, _permit: SessionObservationBindingPermitV1, ) -> Result, Self::Error> { @@ -126,6 +132,52 @@ impl SessionPlanV1 for SentinelPlan { } } +#[derive(Debug)] +struct ReplacingOwnerPlan { + schema: CanonicalObservationSchemaV1, + generation: std::rc::Weak<()>, + owner_slot: Rc>>>, + evaluations: Rc>, +} + +impl session_private::PlanSealed for ReplacingOwnerPlan {} + +impl SessionPlanV1 for ReplacingOwnerPlan { + type OwnerLease = Rc<()>; + type Verified = SentinelVerified; + type Violation = SentinelViolation; + type Error = SentinelError; + + fn try_acquire_owner(&self) -> Option { + self.generation.upgrade() + } + + fn observation_schema(&self) -> &CanonicalObservationSchemaV1 { + &self.schema + } + + fn evaluate( + &mut self, + owner: &Self::OwnerLease, + observation: RevisionBoundObservationV1, + _permit: SessionObservationBindingPermitV1, + ) -> Result, Self::Error> { + self.evaluations.set(self.evaluations.get() + 1); + let old_generation = self + .owner_slot + .borrow_mut() + .replace(Rc::new(())) + .expect("the first owner generation must still be installed"); + assert!(Rc::ptr_eq(owner, &old_generation)); + drop(old_generation); + assert!( + self.generation.upgrade().is_some(), + "the transaction lease must pin its starting owner generation" + ); + Ok(SessionDecision::Verified(SentinelVerified { observation })) + } +} + fn session() -> ( Session, SentinelControl, @@ -413,6 +465,40 @@ fn detached_plan_evidence_is_rejected_before_raw_or_lifecycle_commit() { assert_shared_observation(raw_observed(&session), ¤t_observation); } +#[test] +fn reentrant_owner_replacement_finishes_on_its_pinned_generation_then_expires() { + let schema = canonicalize_observation_schema(vec![SURFACE]).unwrap(); + let first_generation = Rc::new(()); + let owner_slot = Rc::new(RefCell::new(Some(Rc::clone(&first_generation)))); + let evaluations = Rc::new(Cell::new(0)); + let mut session = Session::new( + STREAM, + ReplacingOwnerPlan { + schema, + generation: Rc::downgrade(&first_generation), + owner_slot: Rc::clone(&owner_slot), + evaluations: Rc::clone(&evaluations), + }, + ); + drop(first_generation); + + let SessionState::Ready { current } = session.update(observed_update(1, [255; 3])).unwrap() + else { + panic!("the transaction pinned before replacement must commit"); + }; + assert_eq!(current.observation.revision(), Revision::new(1)); + assert_eq!(evaluations.get(), 1); + assert!(owner_slot.borrow().is_some()); + + assert_eq!( + session.update(observed_update(2, [0; 3])), + Err(SessionUpdateError::OwnerExpired), + ); + assert_eq!(evaluations.get(), 1); + assert_eq!(session.raw_head().revision(), Some(Revision::new(1))); + assert_eq!(verified_revision(session.state()), Some(Revision::new(1))); +} + #[test] fn session_source_contains_one_generic_update_owner_and_no_legacy_runtime() { let source = include_str!("session.rs"); @@ -421,7 +507,6 @@ fn session_source_contains_one_generic_update_owner_and_no_legacy_runtime() { "PointSupportSessionV1", "PointSupportSessionStateV1", "PointSupportSessionUpdateErrorV1", - "Weak<", "ProgramExpired", "ObservationStreamBinding", "SurfaceUpdate", diff --git a/scripts/verify_point_support_surplus.py b/scripts/verify_point_support_surplus.py index feb34d3f..8dbf5b53 100644 --- 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 = ( - "2e63424882231ec2f00ae79911bf74cfb935bb96f11368bce53240468f7f2dd4" + "f5e813b21df3bad8c93fa3525190715110b4965792f5b7a2a0bfde7a3b4297d3" ) EXPECTED_Q55_PROOF_SHA256 = ( "ac59cf89503170c789223b91d775213a19d4e571ef930f2ea609fcd51b14defd" @@ -211,7 +211,7 @@ def verify_source_binding() -> tuple[str, int]: (SESSION_SOURCE, b" let next_raw_head = SessionObservationHeadV1::Observed(observation.clone());\n", b" let next_raw_head = SessionObservationHeadV1::Empty;\n"), (SESSION_SOURCE, b" *raw_head = next_raw_head;\n", b" *raw_head = SessionObservationHeadV1::Empty;\n"), (SESSION_SOURCE, b" Some(previous) => SessionState::Stale { previous },", b" Some(_) => SessionState::Waiting,"), - (SESSION_SOURCE, b" .evaluate(observation, SessionObservationBindingPermitV1::mint())", b" .evaluate(observation, SessionObservationBindingPermitV1::for_test())"), + (SESSION_SOURCE, b" SessionObservationBindingPermitV1::mint(),", b" SessionObservationBindingPermitV1::for_test(),"), (SESSION_SOURCE, b" SessionDecision::Verified(current) => SessionState::Ready { current },", b" SessionDecision::Verified(current) => SessionState::Stale { previous: current },"), (SESSION_SOURCE, b" SessionDecision::Violation(cause) => SessionState::Failed { cause, previous },", b" SessionDecision::Violation(_) => SessionState::Waiting,"), (SESSION_SOURCE, b" return Err(SessionUpdateError::EvidenceBindingInvariant);\n", b" unreachable!();\n"), From 3e8f240c50b6423a5ca4e62d9fa3acfd2b6f36b3 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Thu, 23 Jul 2026 02:55:02 +0300 Subject: [PATCH 34/58] core: close the evaluator union and package session bridge --- ...rt-reference-surplus-q55-bps-proof-v1.json | 2 +- crates/labcolors-core/src/lib.rs | 5 + crates/labcolors-core/src/observation.rs | 292 ++++++++ crates/labcolors-core/src/package_bridge.rs | 621 ++++++++++++++++++ .../src/program_mixed_evaluator_tests.rs | 522 +++++++++++++++ crates/labcolors-core/src/program_session.rs | 329 +++++++--- crates/labcolors-core/src/session.rs | 134 ++-- .../tests/package_bridge_red.rs | 94 +++ packages/colors/bench/wasm.json | 8 +- scripts/check-wasm-size-budget.mjs | 2 +- scripts/verify_point_support_surplus.py | 16 +- 11 files changed, 1884 insertions(+), 141 deletions(-) create mode 100644 crates/labcolors-core/src/package_bridge.rs create mode 100644 crates/labcolors-core/src/program_mixed_evaluator_tests.rs create mode 100644 crates/labcolors-core/tests/package_bridge_red.rs mode change 100644 => 100755 scripts/check-wasm-size-budget.mjs mode change 100644 => 100755 scripts/verify_point_support_surplus.py 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 5c918468..9344cbe1 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":"d731e6b3d068906ee02ebd90611b0d101dda6db4b0c98ff9dc4bdc79058618a8","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":"f5e813b21df3bad8c93fa3525190715110b4965792f5b7a2a0bfde7a3b4297d3","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":"e361cf144e0630a4f2ff52261e6515c9394188fc1351c97b98ea9c0ec87ec39d"},{"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":"a58acbae5b06a8cd9a45adf93ddfbdfb569c5916b60140bd4046182aab2a9518"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"0f0e0ed726aba4defc750b793a76435a3f3825e91002c2cd90d982be7260f180"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"abed15a542b5e8031f0ebfe36138232920c7898c9eae1a2037c6455033ecd91e"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"0755210e3e591d7049f293a0f0b7647681631f32feee5ad7d3b3309cffca8f9d"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"ab36d9e9339cf1b1030963d78eba42e7f898b4d3757712c7908e1d696bc620c9"},{"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":"a201ca9d971d9b7b9928ccef498752c837ce17d6feb1b2ee8d2d7187c3cebb5e"} +{"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":"6f24c596c8c29e4116bc1f19ba70390f90f2a606ae617eb04c42dd4a1da15343","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":"2cf6589a15d2669aca9f1f5a287841805c0fe7293074530d70a1cb803d235c7d","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":"e361cf144e0630a4f2ff52261e6515c9394188fc1351c97b98ea9c0ec87ec39d"},{"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":"a58acbae5b06a8cd9a45adf93ddfbdfb569c5916b60140bd4046182aab2a9518"},{"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":"5a5badfdd164d88aceaee64c4fe519a5151661242f4c4b7982bce816a8b85516"} diff --git a/crates/labcolors-core/src/lib.rs b/crates/labcolors-core/src/lib.rs index 3734dc54..ec0b63ea 100644 --- a/crates/labcolors-core/src/lib.rs +++ b/crates/labcolors-core/src/lib.rs @@ -36,6 +36,8 @@ pub mod numerical_plan; reason = "private F0 output-projection release firewall precedes the atomic public hard cut" )] pub(crate) mod output_projection; +#[doc(hidden)] +pub mod package_bridge; #[cfg_attr( not(test), expect( @@ -97,6 +99,9 @@ mod program_lcs_integration_tests; #[cfg(test)] mod program_joint_integration_tests; +#[cfg(test)] +mod program_mixed_evaluator_tests; + #[cfg(test)] mod release_registry_tests; diff --git a/crates/labcolors-core/src/observation.rs b/crates/labcolors-core/src/observation.rs index eca73378..8881c4c8 100644 --- a/crates/labcolors-core/src/observation.rs +++ b/crates/labcolors-core/src/observation.rs @@ -4,9 +4,11 @@ //! The F2 Session is the sole production owner of the current payload and the //! only code allowed to bind an admitted observation to evaluator evidence. +use core::cmp::Ordering; use core::ops::Range; use std::rc::Rc; +use crate::Srgb8; use crate::appearance::SurfaceInputPortId; use crate::lcs_occurrence::ColorSignal; @@ -38,6 +40,10 @@ impl Revision { pub(crate) const fn new(raw: u64) -> Self { Self(raw) } + + pub(crate) const fn value(self) -> u64 { + self.0 + } } /// Opaque provenance of one simultaneously observed tuple. @@ -101,6 +107,18 @@ pub(crate) struct ObservationUpdateInput { pub(crate) payload: ObservationPayloadInput, } +/// Borrowed schema-ordered point-sRGB8 source for the package hot path. +/// +/// The trait is crate-private and statically dispatched. Callers provide one +/// value per compiled schema ordinal; no port IDs, transport words, or +/// intermediate keyed binding collections enter Core admission. +pub(crate) trait SchemaOrderedScenarioSourceV1 { + fn scenario_count(&self) -> usize; + fn scenario_id(&self, scenario_index: usize) -> ScenarioId; + fn value_count(&self, scenario_index: usize) -> usize; + fn value(&self, scenario_index: usize, binding_index: usize) -> Srgb8; +} + /// One unique physical tuple inside the shared canonical backing. /// /// Values are stored once in schema order. The schema remains attached to the @@ -259,6 +277,21 @@ impl RevisionBoundObservationV1 { &self.backing.schema == schema && canonical_input_matches_set(&self.backing.set, scenarios) } + fn has_schema_ordered_input( + &self, + schema: &CanonicalObservationSchemaV1, + source: &Source, + order: &[usize], + ) -> bool { + &self.backing.schema == schema + && schema_ordered_input_matches_set( + &self.backing.set, + source, + order, + schema.as_slice().len(), + ) + } + #[cfg(test)] pub(crate) fn backing_ptr_for_test(&self) -> *const () { Rc::as_ptr(&self.backing).cast() @@ -331,6 +364,11 @@ pub(crate) enum ObservationError { DuplicateScenarioId { scenario: ScenarioId, }, + SchemaOrderedValueCountMismatch { + scenario: ScenarioId, + expected: usize, + actual: usize, + }, DuplicateSurfaceInputBinding { scenario: ScenarioId, input: SurfaceInputPortId, @@ -534,6 +572,134 @@ pub(crate) fn prepare_observation<'owner, Owner: ObservationOwnerV1>( } } +/// Prepare one borrowed schema-ordered observation without constructing keyed +/// port bindings. One caller-owned scratch vector is reused first as an +/// open-addressed scenario-ID set and then as the canonical scenario order. +/// Consequently admission needs one sort, performs no per-scenario +/// allocation, and exact replay can be allocation-free after scratch growth. +/// A higher revision materializes the canonical backing exactly once; exact +/// replay compares against the existing backing without rebuilding it. +pub(crate) fn prepare_schema_ordered_observation< + 'owner, + Owner: ObservationOwnerV1, + Source: SchemaOrderedScenarioSourceV1, +>( + owner: &'owner mut Owner, + stream: ObservationStreamId, + schema: &CanonicalObservationSchemaV1, + revision: Revision, + source: &Source, + order_scratch: &mut Vec, +) -> Result, ObservationError> { + let scenario_count = source.scenario_count(); + if scenario_count == 0 { + return Err(ObservationError::EmptyScenarioSet); + } + + let id_table_len = scenario_count + .checked_mul(2) + .and_then(usize::checked_next_power_of_two) + .ok_or(ObservationError::ResourceExhausted)?; + order_scratch.clear(); + order_scratch + .try_reserve_exact(id_table_len) + .map_err(|_| ObservationError::ResourceExhausted)?; + order_scratch.resize(id_table_len, usize::MAX); + + for scenario_index in 0..scenario_count { + let actual = source.value_count(scenario_index); + if actual != schema.as_slice().len() { + return Err(ObservationError::SchemaOrderedValueCountMismatch { + scenario: source.scenario_id(scenario_index), + expected: schema.as_slice().len(), + actual, + }); + } + + let scenario_id = source.scenario_id(scenario_index); + let mut table_index = + (scenario_id.0 as usize).wrapping_mul(0x9e37_79b1) & (id_table_len - 1); + loop { + let existing_index = order_scratch[table_index]; + if existing_index == usize::MAX { + order_scratch[table_index] = scenario_index; + break; + } + if source.scenario_id(existing_index) == scenario_id { + return Err(ObservationError::DuplicateScenarioId { + scenario: scenario_id, + }); + } + table_index = (table_index + 1) & (id_table_len - 1); + } + } + + order_scratch.clear(); + order_scratch.extend(0..scenario_count); + order_scratch.sort_unstable_by(|&left, &right| { + compare_schema_ordered_scenarios(source, left, right, schema.as_slice().len()) + }); + + let current_revision = owner.observation_head().revision(); + if let Some(current) = current_revision { + if revision < current { + return Err(ObservationError::RevisionOutOfOrder { + current, + incoming: revision, + }); + } + } + if current_revision == Some(revision) + && !matches!(owner.observation_head(), ObservationHeadViewV1::Observed(_)) + { + return Err(ObservationError::RevisionConflict { revision }); + } + if current_revision == Some(revision) { + let exact = matches!( + owner.observation_head(), + ObservationHeadViewV1::Observed(current) + if current.has_schema_ordered_input(schema, source, order_scratch) + ); + return if exact { + Ok(PreparedObservationUpdateV1::Idempotent( + PreparedIdempotentV1 { owner }, + )) + } else { + Err(ObservationError::RevisionConflict { revision }) + }; + } + + let set = materialize_schema_ordered_scenarios(schema.as_slice(), source, order_scratch)?; + Ok(PreparedObservationUpdateV1::Observed(PreparedObservedV1 { + owner, + observation: RevisionBoundObservationV1 { + stream, + revision, + backing: Rc::new(ObservationBackingV1 { + schema: schema.clone(), + set, + }), + }, + })) +} + +fn compare_schema_ordered_scenarios( + source: &Source, + left: usize, + right: usize, + binding_count: usize, +) -> Ordering { + for binding_index in 0..binding_count { + let ordering = source + .value(left, binding_index) + .cmp(&source.value(right, binding_index)); + if ordering != Ordering::Equal { + return ordering; + } + } + source.scenario_id(left).cmp(&source.scenario_id(right)) +} + fn canonicalize_scenarios_input( schema: &[SurfaceInputPortId], raw: ObservedScenarioSetInput, @@ -642,6 +808,132 @@ fn canonical_input_matches_set(set: &ObservedScenarioSet, scenarios: &[ScenarioI case_index == set.cases.len() } +fn schema_ordered_input_matches_set( + set: &ObservedScenarioSet, + source: &Source, + order: &[usize], + binding_count: usize, +) -> bool { + let mut case_index = 0; + let mut scenario_ordinal = 0; + while scenario_ordinal < order.len() { + let scenario_index = order[scenario_ordinal]; + let Some(values) = set.values(case_index) else { + return false; + }; + if values.len() != binding_count + || values.iter().enumerate().any(|(binding_index, value)| { + *value != ColorSignal::from_srgb8(source.value(scenario_index, binding_index)) + }) + { + return false; + } + + let first = scenario_ordinal; + scenario_ordinal += 1; + while scenario_ordinal < order.len() + && schema_ordered_scenarios_equal( + source, + order[first], + order[scenario_ordinal], + binding_count, + ) + { + scenario_ordinal += 1; + } + let Some(provenance) = set.provenance(case_index) else { + return false; + }; + if provenance.len() != scenario_ordinal - first + || order[first..scenario_ordinal] + .iter() + .zip(provenance) + .any(|(&source_index, expected)| source.scenario_id(source_index) != *expected) + { + return false; + } + case_index += 1; + } + case_index == set.cases.len() +} + +fn schema_ordered_scenarios_equal( + source: &Source, + left: usize, + right: usize, + binding_count: usize, +) -> bool { + (0..binding_count).all(|binding_index| { + source.value(left, binding_index) == source.value(right, binding_index) + }) +} + +fn materialize_schema_ordered_scenarios( + schema: &[SurfaceInputPortId], + source: &Source, + order: &[usize], +) -> Result { + debug_assert!(!order.is_empty()); + + let unique_case_count = 1 + order + .windows(2) + .filter(|pair| !schema_ordered_scenarios_equal(source, pair[0], pair[1], schema.len())) + .count(); + let value_count = unique_case_count + .checked_mul(schema.len()) + .ok_or(ObservationError::ResourceExhausted)?; + + let mut cases = Vec::new(); + cases + .try_reserve_exact(unique_case_count) + .map_err(|_| ObservationError::ResourceExhausted)?; + let mut values = Vec::new(); + values + .try_reserve_exact(value_count) + .map_err(|_| ObservationError::ResourceExhausted)?; + let mut provenance = Vec::new(); + provenance + .try_reserve_exact(order.len()) + .map_err(|_| ObservationError::ResourceExhausted)?; + + let mut scenario_ordinal = 0; + while scenario_ordinal < order.len() { + let first_source_index = order[scenario_ordinal]; + let values_start = values.len(); + values.extend((0..schema.len()).map(|binding_index| { + ColorSignal::from_srgb8(source.value(first_source_index, binding_index)) + })); + let values_end = values.len(); + let provenance_start = provenance.len(); + provenance.push(source.scenario_id(first_source_index)); + scenario_ordinal += 1; + while scenario_ordinal < order.len() + && schema_ordered_scenarios_equal( + source, + first_source_index, + order[scenario_ordinal], + schema.len(), + ) + { + provenance.push(source.scenario_id(order[scenario_ordinal])); + scenario_ordinal += 1; + } + let provenance_end = provenance.len(); + cases.push(PhysicalScenario { + values: values_start..values_end, + provenance: provenance_start..provenance_end, + }); + } + + debug_assert_eq!(cases.len(), unique_case_count); + debug_assert_eq!(values.len(), value_count); + Ok(ObservedScenarioSet { + cases: cases.into_boxed_slice(), + values: values.into_boxed_slice(), + provenance: provenance.into_boxed_slice(), + }) +} + fn materialize_scenarios( schema: &[SurfaceInputPortId], scenarios: Vec, diff --git a/crates/labcolors-core/src/package_bridge.rs b/crates/labcolors-core/src/package_bridge.rs new file mode 100644 index 00000000..e291fd3d --- /dev/null +++ b/crates/labcolors-core/src/package_bridge.rs @@ -0,0 +1,621 @@ +//! Sole concrete package projection for a compiled Core Program Session. +//! +//! This hidden module is deliberately narrower than the authored Program IR: +//! it exposes no evaluator trait, generic Session plan, threshold, candidate +//! domain, client vocabulary, transport word, or lifecycle generation. The +//! package adapter supplies schema-ordered physical scenarios and receives a +//! borrowed, allocation-free projection of Core-owned state and evidence. + +use core::iter::FusedIterator; +use core::slice; + +use crate::Srgb8; +use crate::observation::{ + ObservationError, ObservationPayloadInput, ObservationStreamId, ObservationUpdateInput, + Revision, ScenarioId, SchemaOrderedScenarioSourceV1, UnknownReasonId, +}; +use crate::program_session::{ + CompiledCoreProgramV1, CoreProgramEvaluatorErrorV1, CoreProgramEvaluatorsV1, ProgramConflictV1, + ProgramOutputV1, ProgramSessionEvaluationError, ProgramSessionInstantiateError, + ProgramSessionPlan, ProgramVerifiedV1, +}; +use crate::session::{Session, SessionState, SessionUpdateError}; + +type CoreVerifiedV1 = ProgramVerifiedV1; +type CoreConflictV1 = ProgramConflictV1; +type CoreProgramPlanV1 = ProgramSessionPlan; +type CoreProgramSessionV1 = Session; +type CoreProgramStateV1 = SessionState; +type CoreProgramPlanErrorV1 = ProgramSessionEvaluationError; + +/// Opaque strong owner of one exact compiled Core Program. +/// +/// Sessions instantiated from this owner are independently mutable. In the +/// terminal stacked build they retain only the canonical weak owner binding; +/// dropping this value therefore expires every such Session before its next +/// admission. +pub struct PackageProgramOwnerV1 { + compiled: CompiledCoreProgramV1, +} + +impl PackageProgramOwnerV1 { + /// Internal handoff from the canonical Core lowerer. Keeping this + /// constructor crate-private prevents an adapter-authored Program dialect. + #[cfg_attr( + not(test), + expect( + dead_code, + reason = "the canonical package lowerer is linked in the following stacked slice" + ) + )] + pub(crate) const fn from_compiled(compiled: CompiledCoreProgramV1) -> Self { + Self { compiled } + } + + /// Number of schema-ordered surface values required in every scenario. + pub fn surface_input_count(&self) -> usize { + self.compiled.surface_input_ports().len() + } + + /// Canonically ordered opaque output slots owned by this Program. + pub fn output_slots(&self) -> impl ExactSizeIterator + '_ { + self.compiled.outputs().map(|(slot, _paint)| slot.value()) + } + + /// Instantiate one stream-affine Session without exposing a generation. + pub fn instantiate( + &self, + stream_id: u32, + ) -> Result { + let surface_input_count = self.compiled.surface_input_ports().len(); + let output_slots = try_copy_output_slots(&self.compiled)?; + let stream = ObservationStreamId::new(stream_id); + let session = self + .compiled + .instantiate(stream) + .map_err(PackageProgramInstantiateErrorV1::from_core)?; + Ok(PackageProgramSessionV1 { + stream, + surface_input_count, + output_slots, + scenario_order_scratch: Vec::new(), + session, + }) + } +} + +/// One borrowed physical scenario in the compiled schema order. +/// +/// A scenario ID is opaque provenance. `values` contains exactly one encoded +/// sRGB8 value per compiled surface input; ports are intentionally absent from +/// the hot package boundary. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PackageProgramScenarioV1<'a> { + scenario_id: u32, + values: &'a [Srgb8], +} + +impl<'a> PackageProgramScenarioV1<'a> { + /// Construct one simultaneous physical tuple in compiled schema order. + pub const fn new(scenario_id: u32, values: &'a [Srgb8]) -> Self { + Self { + scenario_id, + values, + } + } +} + +/// One revision-bound package update. Stream ownership stays in the Session. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PackageProgramUpdateV1<'a> { + /// Correlated, schema-ordered physical scenarios. + Observed { + revision: u64, + scenarios: &'a [PackageProgramScenarioV1<'a>], + }, + /// Explicitly unavailable observation; no background is invented. + Unknown { revision: u64, reason_id: u32 }, +} + +/// Concrete opaque owner of one mutable Core Program Session. +pub struct PackageProgramSessionV1 { + stream: ObservationStreamId, + surface_input_count: usize, + output_slots: Box<[u32]>, + scenario_order_scratch: Vec, + session: CoreProgramSessionV1, +} + +impl PackageProgramSessionV1 { + /// Number of schema-ordered values required in every observed scenario. + pub fn surface_input_count(&self) -> usize { + self.surface_input_count + } + + /// Canonically ordered opaque output slots for one-time host binding. + pub fn output_slots(&self) -> impl ExactSizeIterator + '_ { + self.output_slots.iter().copied() + } + + /// Allocation-free view of the current Core-owned lifecycle state. + pub fn state(&self) -> PackageProgramStateViewV1<'_> { + let revision = self.session.raw_head().revision().map(Revision::value); + PackageProgramStateViewV1 { + state: self.session.state(), + revision, + output_slots: &self.output_slots, + } + } + + /// Admit, evaluate and atomically commit one revision before projecting it. + pub fn update( + &mut self, + update: PackageProgramUpdateV1<'_>, + ) -> Result, PackageProgramUpdateErrorV1> { + match update { + PackageProgramUpdateV1::Observed { + revision, + scenarios, + } => { + let source = PackageProgramScenarioSourceV1(scenarios); + self.session + .update_schema_ordered( + Revision::new(revision), + &source, + &mut self.scenario_order_scratch, + ) + .map_err(map_session_update_error)?; + } + PackageProgramUpdateV1::Unknown { + revision, + reason_id, + } => { + self.session + .update(ObservationUpdateInput { + stream: self.stream, + revision: Revision::new(revision), + payload: ObservationPayloadInput::Unknown(UnknownReasonId::new(reason_id)), + }) + .map_err(map_session_update_error)?; + } + } + Ok(self.state()) + } +} + +struct PackageProgramScenarioSourceV1<'a>(&'a [PackageProgramScenarioV1<'a>]); + +impl SchemaOrderedScenarioSourceV1 for PackageProgramScenarioSourceV1<'_> { + fn scenario_count(&self) -> usize { + self.0.len() + } + + fn scenario_id(&self, scenario_index: usize) -> ScenarioId { + ScenarioId::new(self.0[scenario_index].scenario_id) + } + + fn value_count(&self, scenario_index: usize) -> usize { + self.0[scenario_index].values.len() + } + + fn value(&self, scenario_index: usize, binding_index: usize) -> Srgb8 { + self.0[scenario_index].values[binding_index] + } +} + +/// Closed package-visible lifecycle classification. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PackageProgramStateKindV1 { + Waiting, + Ready, + Stale, + Failed, +} + +/// Borrowed projection of one complete Core-owned lifecycle state. +#[derive(Clone, Copy)] +pub struct PackageProgramStateViewV1<'a> { + state: &'a CoreProgramStateV1, + revision: Option, + output_slots: &'a [u32], +} + +impl<'a> PackageProgramStateViewV1<'a> { + pub const fn kind(self) -> PackageProgramStateKindV1 { + match self.state { + SessionState::Waiting => PackageProgramStateKindV1::Waiting, + SessionState::Ready { .. } => PackageProgramStateKindV1::Ready, + SessionState::Stale { .. } => PackageProgramStateKindV1::Stale, + SessionState::Failed { .. } => PackageProgramStateKindV1::Failed, + } + } + + /// Current raw-head revision; only the initial Waiting state has none. + pub const fn revision(self) -> Option { + self.revision + } + + /// Failed-state cause ordinal inside [`Self::certificates`]. + pub const fn cause_certificate_index(self) -> Option { + match self.state { + SessionState::Failed { .. } => Some(0), + SessionState::Waiting | SessionState::Ready { .. } | SessionState::Stale { .. } => None, + } + } + + /// Core-owned certificates in canonical same-call ordinal order. + pub fn certificates( + self, + ) -> impl ExactSizeIterator> + 'a { + let (first, second) = match self.state { + SessionState::Waiting => (None, None), + SessionState::Ready { current } | SessionState::Stale { previous: current } => { + (Some(PackageProgramCertificateV1::verified(current)), None) + } + SessionState::Failed { cause, previous } => ( + Some(PackageProgramCertificateV1::conflict(cause)), + previous.as_ref().map(PackageProgramCertificateV1::verified), + ), + }; + PackageProgramCertificatesV1::new(first, second) + } + + /// Total canonical output projection for this lifecycle state. + pub fn operations(self) -> impl ExactSizeIterator + 'a { + let inner = match self.state { + SessionState::Waiting => PackageProgramOperationSourceV1::Empty, + SessionState::Ready { current } => { + debug_assert_eq!(current.outputs().len(), self.output_slots.len()); + debug_assert!( + current + .outputs() + .iter() + .zip(self.output_slots) + .all(|(output, slot)| output.output().value() == *slot) + ); + PackageProgramOperationSourceV1::Set(current.outputs().iter()) + } + SessionState::Stale { .. } => PackageProgramOperationSourceV1::Hold { + slots: self.output_slots.iter(), + certificate_index: 0, + }, + SessionState::Failed { + previous: Some(_), .. + } => PackageProgramOperationSourceV1::Hold { + slots: self.output_slots.iter(), + certificate_index: 1, + }, + SessionState::Failed { previous: None, .. } => { + PackageProgramOperationSourceV1::Remove(self.output_slots.iter()) + } + }; + PackageProgramOperationsV1 { inner } + } +} + +/// Opaque certificate family; evaluator-specific evidence never escapes. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PackageProgramCertificateKindV1 { + Verified, + Conflict, +} + +#[derive(Clone, Copy)] +enum PackageProgramCertificateRefV1<'a> { + Verified(&'a CoreVerifiedV1), + Conflict(&'a CoreConflictV1), +} + +/// Borrowed opaque handle to one Core-owned certificate. +#[derive(Clone, Copy)] +pub struct PackageProgramCertificateV1<'a> { + inner: PackageProgramCertificateRefV1<'a>, +} + +impl<'a> PackageProgramCertificateV1<'a> { + const fn verified(value: &'a CoreVerifiedV1) -> Self { + Self { + inner: PackageProgramCertificateRefV1::Verified(value), + } + } + + const fn conflict(value: &'a CoreConflictV1) -> Self { + Self { + inner: PackageProgramCertificateRefV1::Conflict(value), + } + } + + pub const fn kind(self) -> PackageProgramCertificateKindV1 { + match self.inner { + PackageProgramCertificateRefV1::Verified(_) => { + PackageProgramCertificateKindV1::Verified + } + PackageProgramCertificateRefV1::Conflict(_) => { + PackageProgramCertificateKindV1::Conflict + } + } + } + + /// Revision bound into this exact evidence object. + pub const fn revision(self) -> u64 { + let revision = match self.inner { + PackageProgramCertificateRefV1::Verified(value) => { + value.report().observation().revision() + } + PackageProgramCertificateRefV1::Conflict(value) => { + value.report().observation().revision() + } + }; + revision.value() + } + + #[cfg(test)] + pub(crate) fn observation_backing_ptr_for_test(self) -> *const () { + match self.inner { + PackageProgramCertificateRefV1::Verified(value) => { + value.report().observation().backing_ptr_for_test() + } + PackageProgramCertificateRefV1::Conflict(value) => { + value.report().observation().backing_ptr_for_test() + } + } + } +} + +/// Closed total operation union over opaque output slots. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum PackageProgramOperationV1 { + Set { + output_slot: u32, + source: Srgb8, + opacity: f64, + certificate_index: usize, + }, + Remove { + output_slot: u32, + }, + Hold { + output_slot: u32, + certificate_index: usize, + }, +} + +struct PackageProgramCertificatesV1<'a> { + values: [Option>; 2], + index: usize, + len: usize, +} + +impl<'a> PackageProgramCertificatesV1<'a> { + fn new( + first: Option>, + second: Option>, + ) -> Self { + let len = usize::from(first.is_some()) + usize::from(second.is_some()); + debug_assert!(first.is_some() || second.is_none()); + Self { + values: [first, second], + index: 0, + len, + } + } +} + +impl<'a> Iterator for PackageProgramCertificatesV1<'a> { + type Item = PackageProgramCertificateV1<'a>; + + fn next(&mut self) -> Option { + if self.index == self.len { + return None; + } + let value = self.values[self.index]; + self.index += 1; + value + } + + fn size_hint(&self) -> (usize, Option) { + let remaining = self.len - self.index; + (remaining, Some(remaining)) + } +} + +impl ExactSizeIterator for PackageProgramCertificatesV1<'_> {} +impl FusedIterator for PackageProgramCertificatesV1<'_> {} + +enum PackageProgramOperationSourceV1<'a> { + Empty, + Set(slice::Iter<'a, ProgramOutputV1>), + Hold { + slots: slice::Iter<'a, u32>, + certificate_index: usize, + }, + Remove(slice::Iter<'a, u32>), +} + +struct PackageProgramOperationsV1<'a> { + inner: PackageProgramOperationSourceV1<'a>, +} + +impl Iterator for PackageProgramOperationsV1<'_> { + type Item = PackageProgramOperationV1; + + fn next(&mut self) -> Option { + match &mut self.inner { + PackageProgramOperationSourceV1::Empty => None, + PackageProgramOperationSourceV1::Set(outputs) => { + let output = *outputs.next()?; + let paint = output.paint(); + Some(PackageProgramOperationV1::Set { + output_slot: output.output().value(), + source: paint.source(), + opacity: paint.opacity().value(), + certificate_index: 0, + }) + } + PackageProgramOperationSourceV1::Hold { + slots, + certificate_index, + } => Some(PackageProgramOperationV1::Hold { + output_slot: *slots.next()?, + certificate_index: *certificate_index, + }), + PackageProgramOperationSourceV1::Remove(slots) => { + Some(PackageProgramOperationV1::Remove { + output_slot: *slots.next()?, + }) + } + } + } + + fn size_hint(&self) -> (usize, Option) { + let remaining = match &self.inner { + PackageProgramOperationSourceV1::Empty => 0, + PackageProgramOperationSourceV1::Set(outputs) => outputs.len(), + PackageProgramOperationSourceV1::Hold { slots, .. } + | PackageProgramOperationSourceV1::Remove(slots) => slots.len(), + }; + (remaining, Some(remaining)) + } +} + +impl ExactSizeIterator for PackageProgramOperationsV1<'_> {} +impl FusedIterator for PackageProgramOperationsV1<'_> {} + +/// Closed package error classifications for Session construction. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PackageProgramInstantiateErrorKindV1 { + ResourceExhausted, + InternalInvariant, +} + +/// Opaque Session construction failure. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PackageProgramInstantiateErrorV1 { + kind: PackageProgramInstantiateErrorKindV1, +} + +impl PackageProgramInstantiateErrorV1 { + const fn new(kind: PackageProgramInstantiateErrorKindV1) -> Self { + Self { kind } + } + + fn from_core(error: ProgramSessionInstantiateError) -> Self { + let kind = match error { + ProgramSessionInstantiateError::ResourceExhausted => { + PackageProgramInstantiateErrorKindV1::ResourceExhausted + } + ProgramSessionInstantiateError::InternalInvariant => { + PackageProgramInstantiateErrorKindV1::InternalInvariant + } + }; + Self::new(kind) + } + + pub const fn kind(self) -> PackageProgramInstantiateErrorKindV1 { + self.kind + } +} + +impl From for PackageProgramInstantiateErrorV1 { + fn from(kind: PackageProgramInstantiateErrorKindV1) -> Self { + Self::new(kind) + } +} + +/// Closed package error classifications for one atomic update. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PackageProgramUpdateErrorKindV1 { + OwnerExpired, + InvalidObservation, + RevisionOutOfOrder, + RevisionConflict, + ResourceExhausted, + EvaluationFailed, + InternalInvariant, +} + +/// Opaque update failure. Core state is unchanged for every returned error. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PackageProgramUpdateErrorV1 { + kind: PackageProgramUpdateErrorKindV1, +} + +impl PackageProgramUpdateErrorV1 { + const fn new(kind: PackageProgramUpdateErrorKindV1) -> Self { + Self { kind } + } + + pub const fn kind(self) -> PackageProgramUpdateErrorKindV1 { + self.kind + } +} + +fn try_copy_output_slots( + compiled: &CompiledCoreProgramV1, +) -> Result, PackageProgramInstantiateErrorV1> { + let outputs = compiled.outputs(); + let mut copied = Vec::new(); + copied + .try_reserve_exact(outputs.len()) + .map_err(|_| PackageProgramInstantiateErrorKindV1::ResourceExhausted)?; + copied.extend(outputs.map(|(slot, _paint)| slot.value())); + Ok(copied.into_boxed_slice()) +} + +fn map_session_update_error( + error: SessionUpdateError, +) -> PackageProgramUpdateErrorV1 { + let kind = match error { + SessionUpdateError::OwnerExpired => PackageProgramUpdateErrorKindV1::OwnerExpired, + SessionUpdateError::Observation(error) => map_observation_error(error), + SessionUpdateError::Plan(error) => map_plan_error(error), + SessionUpdateError::EvidenceBindingInvariant => { + PackageProgramUpdateErrorKindV1::InternalInvariant + } + }; + PackageProgramUpdateErrorV1::new(kind) +} + +fn map_observation_error(error: ObservationError) -> PackageProgramUpdateErrorKindV1 { + match error { + ObservationError::EmptyScenarioSet + | ObservationError::DuplicateScenarioId { .. } + | ObservationError::SchemaOrderedValueCountMismatch { .. } => { + PackageProgramUpdateErrorKindV1::InvalidObservation + } + ObservationError::RevisionOutOfOrder { .. } => { + PackageProgramUpdateErrorKindV1::RevisionOutOfOrder + } + ObservationError::RevisionConflict { .. } => { + PackageProgramUpdateErrorKindV1::RevisionConflict + } + ObservationError::ResourceExhausted => PackageProgramUpdateErrorKindV1::ResourceExhausted, + ObservationError::EmptyCompiledSurfaceInputSchema + | ObservationError::DuplicateCompiledSurfaceInputPort { .. } + | ObservationError::StreamMismatch { .. } + | ObservationError::DuplicateSurfaceInputBinding { .. } + | ObservationError::MissingSurfaceInputBinding { .. } + | ObservationError::UnexpectedSurfaceInputBinding { .. } => { + PackageProgramUpdateErrorKindV1::InternalInvariant + } + } +} + +fn map_plan_error(error: CoreProgramPlanErrorV1) -> PackageProgramUpdateErrorKindV1 { + match error { + ProgramSessionEvaluationError::ResourceExhausted => { + PackageProgramUpdateErrorKindV1::ResourceExhausted + } + ProgramSessionEvaluationError::Evaluator { .. } => { + PackageProgramUpdateErrorKindV1::EvaluationFailed + } + ProgramSessionEvaluationError::ObservationSchemaMismatch(_) + | ProgramSessionEvaluationError::ProgramTargetBinding { .. } + | ProgramSessionEvaluationError::ModeledOccurrence { .. } + | ProgramSessionEvaluationError::OutputVariesAcrossCases { .. } + | ProgramSessionEvaluationError::FinalRecheckViolation { .. } + | ProgramSessionEvaluationError::InternalInvariant => { + PackageProgramUpdateErrorKindV1::InternalInvariant + } + } +} diff --git a/crates/labcolors-core/src/program_mixed_evaluator_tests.rs b/crates/labcolors-core/src/program_mixed_evaluator_tests.rs new file mode 100644 index 00000000..3b566a47 --- /dev/null +++ b/crates/labcolors-core/src/program_mixed_evaluator_tests.rs @@ -0,0 +1,522 @@ +use crate::Srgb8; +use crate::appearance::{OccurrenceId, PaintId, SurfaceId, SurfaceInputPortId}; +use crate::constraints::{ + ExactConstraintIdentityV1, ExactIdentityCapabilityV1, ExactIdentityReleaseV1, +}; +use crate::lcs_occurrence::{ + AdaptingLuminanceCdM2, AppearanceContextId, AppearanceContextSchemaReleaseId, + BackgroundLuminanceRatio, ColorSignal, IEC_SRGB_D65_XYZ_FRAME_V1, SurroundProfileId, +}; +use crate::observation::{ + ObservationGroupId, ObservationPayloadInput, ObservationStreamId, ObservationUpdateInput, + ObservedScenarioSetInput, Revision, ScenarioId, ScenarioInput, SurfaceInputBinding, +}; +use crate::package_bridge::{ + PackageProgramCertificateKindV1, PackageProgramOperationV1, PackageProgramOwnerV1, + PackageProgramScenarioV1, PackageProgramStateKindV1, PackageProgramUpdateErrorKindV1, + PackageProgramUpdateV1, +}; +use crate::program_session::{ + CompiledCoreProgramV1, CompositionProfile, ConstraintId, ConstraintInvocation, ConstraintSet, + CoreProgramConstraintInvocationV1, CoreProgramEvaluatorsV1, CoreProgramPassEvidenceV1, + CoreProgramV1, CoreProgramViolationEvidenceV1, DeclaredJointSelectionV1, JointCandidateStateV1, + ObservationGroup, Occurrence, OutputBinding, OutputSlotId, Paint, Program, + ProgramConstraintResultV1, Source, SourceId, Surface, Target, TargetCandidateChoiceV1, + TargetCandidateId, TargetCandidateV1, TargetId, +}; +use crate::session::SessionState; +use crate::wcag22::{Wcag22CriterionV1, wcag22_profile_v1}; + +const SOURCE: SourceId = SourceId::new(1); +const TARGET: TargetId = TargetId::new(2); +const PAINT: PaintId = PaintId::new(3); +const SURFACE: SurfaceId = SurfaceId::new(4); +const SURFACE_PORT: SurfaceInputPortId = SurfaceInputPortId::new(5); +const OCCURRENCE: OccurrenceId = OccurrenceId::new(6); +const EXACT_CONSTRAINT: ConstraintId = ConstraintId::new(7); +const WCAG_CONSTRAINT: ConstraintId = ConstraintId::new(8); +const OUTPUT: OutputSlotId = OutputSlotId::new(9); +const GROUP: ObservationGroupId = ObservationGroupId::new(10); +const STREAM: ObservationStreamId = ObservationStreamId::new(11); + +fn signal(bytes: [u8; 3]) -> ColorSignal { + ColorSignal::from_srgb8(Srgb8::new(bytes)) +} + +fn context() -> AppearanceContextId { + AppearanceContextId::from_inputs( + AppearanceContextSchemaReleaseId::Ciecam16ViewingInputsV1, + IEC_SRGB_D65_XYZ_FRAME_V1, + AdaptingLuminanceCdM2::try_new(64.0).unwrap(), + BackgroundLuminanceRatio::try_new(0.2).unwrap(), + SurroundProfileId::AverageV1, + ) +} + +fn observed_white() -> ObservationUpdateInput { + ObservationUpdateInput { + stream: STREAM, + revision: Revision::new(1), + payload: ObservationPayloadInput::Scenarios(ObservedScenarioSetInput { + scenarios: vec![ScenarioInput { + id: ScenarioId::new(1), + bindings: vec![SurfaceInputBinding::new(SURFACE_PORT, signal([0xFF; 3]))], + }], + }), + } +} + +fn observed_backdrops(backdrops: &[[u8; 3]]) -> ObservationUpdateInput { + ObservationUpdateInput { + stream: STREAM, + revision: Revision::new(1), + payload: ObservationPayloadInput::Scenarios(ObservedScenarioSetInput { + scenarios: backdrops + .iter() + .enumerate() + .map(|(index, backdrop)| ScenarioInput { + id: ScenarioId::new(index as u32 + 1), + bindings: vec![SurfaceInputBinding::new(SURFACE_PORT, signal(*backdrop))], + }) + .collect(), + }), + } +} + +fn finite_program(candidate_signals: [[u8; 3]; 2]) -> CompiledCoreProgramV1 { + const FIRST: TargetCandidateId = TargetCandidateId::new(1); + const SECOND: TargetCandidateId = TargetCandidateId::new(2); + let program: CoreProgramV1 = Program::new( + vec![Source::new(SOURCE, signal(candidate_signals[0]))], + vec![Target::finite( + TARGET, + SOURCE, + vec![ + TargetCandidateV1::new(FIRST, signal(candidate_signals[0])), + TargetCandidateV1::new(SECOND, signal(candidate_signals[1])), + ], + )], + ObservationGroup::new(GROUP, vec![SURFACE_PORT]), + vec![], + vec![Paint::Solid { + id: PAINT, + target: TARGET, + }], + vec![Surface::Input { + id: SURFACE, + input: SURFACE_PORT, + }], + vec![Occurrence::new( + OCCURRENCE, + PAINT, + SURFACE, + CompositionProfile::EncodedSrgb8SourceOverV1, + context(), + )], + ConstraintSet::new( + vec![ConstraintInvocation::hard( + WCAG_CONSTRAINT, + OCCURRENCE, + CoreProgramConstraintInvocationV1::Wcag22Srgb8(Wcag22CriterionV1::Sc143TextDefault), + )], + vec![ConstraintInvocation::report_only( + EXACT_CONSTRAINT, + OCCURRENCE, + CoreProgramConstraintInvocationV1::ExactSrgb8(Srgb8::new([0; 3])), + )], + ), + vec![OutputBinding::new(OUTPUT, PAINT)], + CoreProgramEvaluatorsV1, + ); + program + .with_joint_selection(DeclaredJointSelectionV1::new(vec![ + JointCandidateStateV1::new(vec![TargetCandidateChoiceV1::new(TARGET, FIRST)]), + JointCandidateStateV1::new(vec![TargetCandidateChoiceV1::new(TARGET, SECOND)]), + ])) + .compile() + .unwrap() +} + +#[test] +fn one_program_retains_typed_exact_and_wcag22_outcomes() { + let program: CoreProgramV1 = Program::new( + vec![Source::new(SOURCE, signal([0; 3]))], + vec![Target::fixed(TARGET, SOURCE)], + ObservationGroup::new(GROUP, vec![SURFACE_PORT]), + vec![], + vec![Paint::Solid { + id: PAINT, + target: TARGET, + }], + vec![Surface::Input { + id: SURFACE, + input: SURFACE_PORT, + }], + vec![Occurrence::new( + OCCURRENCE, + PAINT, + SURFACE, + CompositionProfile::EncodedSrgb8SourceOverV1, + context(), + )], + ConstraintSet::new( + vec![ + ConstraintInvocation::hard( + EXACT_CONSTRAINT, + OCCURRENCE, + CoreProgramConstraintInvocationV1::ExactSrgb8(Srgb8::new([0; 3])), + ), + ConstraintInvocation::hard( + WCAG_CONSTRAINT, + OCCURRENCE, + CoreProgramConstraintInvocationV1::Wcag22Srgb8( + Wcag22CriterionV1::Sc143TextDefault, + ), + ), + ], + vec![], + ), + vec![OutputBinding::new(OUTPUT, PAINT)], + CoreProgramEvaluatorsV1, + ); + let compiled = program.compile().unwrap(); + + let mut session = compiled.instantiate(STREAM).unwrap(); + let SessionState::Ready { current } = session.update(observed_white()).unwrap() else { + panic!("opaque black on white must satisfy both authored hard constraints"); + }; + let [exact, wcag] = current.report().cells() else { + panic!("one case times two heterogeneous constraints must produce two cells"); + }; + + let ProgramConstraintResultV1::Pass(CoreProgramPassEvidenceV1::ExactSrgb8(evidence)) = + exact.result() + else { + panic!("the first cell must retain Exact-specific pass evidence"); + }; + assert_eq!( + evidence.identity(), + &ExactConstraintIdentityV1::FinalSrgb8IdentityV1, + ); + assert_eq!(evidence.release(), &ExactIdentityReleaseV1::V1); + assert_eq!( + evidence.capability(), + &ExactIdentityCapabilityV1::FinalOccurrenceSrgb8IdentityV1, + ); + assert_eq!( + evidence.binding().modeled_lcs(), + exact.modeled_lcs_occurrence(), + ); + + let ProgramConstraintResultV1::Pass(CoreProgramPassEvidenceV1::Wcag22Srgb8(evidence)) = + wcag.result() + else { + panic!("the second cell must retain WCAG22-specific pass evidence"); + }; + assert_eq!(evidence.release(), &wcag22_profile_v1().profile_id); + assert_eq!( + evidence.binding().modeled_lcs(), + wcag.modeled_lcs_occurrence(), + ); + assert_ne!( + core::any::type_name_of_val(evidence.identity()), + core::any::type_name_of_val(exact.result()), + ); +} + +#[test] +fn mixed_families_select_only_a_state_that_passes_every_case_then_recheck_it() { + let compiled = finite_program([[0x80; 3], [0; 3]]); + let mut session = compiled.instantiate(STREAM).unwrap(); + let SessionState::Ready { current } = session + .update(observed_backdrops(&[[0xFF; 3], [0x80; 3]])) + .unwrap() + else { + panic!("the later black state must pass WCAG22 over both physical cases"); + }; + + assert_eq!(current.selected_state_index(), Some(1)); + let cells = current.report().cells(); + assert_eq!(cells.len(), 4); + assert_eq!( + cells + .iter() + .map(|cell| (cell.case_index(), cell.constraint(), cell.is_hard())) + .collect::>(), + vec![ + (0, EXACT_CONSTRAINT, false), + (0, WCAG_CONSTRAINT, true), + (1, EXACT_CONSTRAINT, false), + (1, WCAG_CONSTRAINT, true), + ], + ); + for cell in [cells[0].result(), cells[2].result()] { + assert!(matches!( + cell, + ProgramConstraintResultV1::Pass(CoreProgramPassEvidenceV1::ExactSrgb8(_)) + )); + } + for cell in [cells[1].result(), cells[3].result()] { + assert!(matches!( + cell, + ProgramConstraintResultV1::Pass(CoreProgramPassEvidenceV1::Wcag22Srgb8(_)) + )); + } +} + +#[test] +fn mixed_family_conflict_is_exhaustive_and_keeps_report_only_non_gating() { + let compiled = finite_program([[0x80; 3], [0xFF; 3]]); + let mut session = compiled.instantiate(STREAM).unwrap(); + let SessionState::Failed { cause, previous } = session.update(observed_white()).unwrap() else { + panic!("neither gray nor white satisfies default text contrast on white"); + }; + assert!(previous.is_none()); + assert_eq!(cause.considered_state_count(), 2); + let cells = cause.report().cells(); + assert_eq!(cells.len(), 4); + assert_eq!( + cells + .iter() + .map(|cell| ( + cell.candidate_state_index(), + cell.constraint(), + cell.is_hard() + )) + .collect::>(), + vec![ + (0, EXACT_CONSTRAINT, false), + (0, WCAG_CONSTRAINT, true), + (1, EXACT_CONSTRAINT, false), + (1, WCAG_CONSTRAINT, true), + ], + ); + assert!(cells.iter().all(|cell| cell.result().is_violation())); + assert!(matches!( + cells[0].result(), + ProgramConstraintResultV1::Violation(CoreProgramViolationEvidenceV1::ExactSrgb8(_)) + )); + assert!(matches!( + cells[1].result(), + ProgramConstraintResultV1::Violation(CoreProgramViolationEvidenceV1::Wcag22Srgb8(_)) + )); +} + +#[test] +fn concrete_package_bridge_projects_total_ready_and_stale_operations() { + let owner = PackageProgramOwnerV1::from_compiled(finite_program([[0x80; 3], [0; 3]])); + assert_eq!(owner.surface_input_count(), 1); + assert_eq!(owner.output_slots().collect::>(), [OUTPUT.value()]); + + let mut session = owner.instantiate(11).unwrap(); + let initial = session.state(); + assert_eq!(initial.kind(), PackageProgramStateKindV1::Waiting); + assert_eq!(initial.revision(), None); + assert_eq!(initial.certificates().len(), 0); + assert_eq!(initial.operations().len(), 0); + + let white = [Srgb8::new([0xFF; 3])]; + let gray = [Srgb8::new([0x80; 3])]; + let scenarios = [ + PackageProgramScenarioV1::new(2, &gray), + PackageProgramScenarioV1::new(1, &white), + ]; + let ready = session + .update(PackageProgramUpdateV1::Observed { + revision: 1, + scenarios: &scenarios, + }) + .unwrap(); + assert_eq!(ready.kind(), PackageProgramStateKindV1::Ready); + assert_eq!(ready.revision(), Some(1)); + assert_eq!(ready.cause_certificate_index(), None); + let certificates = ready.certificates().collect::>(); + assert_eq!(certificates.len(), 1); + assert_eq!( + certificates[0].kind(), + PackageProgramCertificateKindV1::Verified + ); + assert_eq!(certificates[0].revision(), 1); + let ready_backing = certificates[0].observation_backing_ptr_for_test(); + assert_eq!( + ready.operations().collect::>(), + [PackageProgramOperationV1::Set { + output_slot: OUTPUT.value(), + source: Srgb8::new([0; 3]), + opacity: 1.0, + certificate_index: 0, + }] + ); + + let reordered = [ + PackageProgramScenarioV1::new(1, &white), + PackageProgramScenarioV1::new(2, &gray), + ]; + let replay = session + .update(PackageProgramUpdateV1::Observed { + revision: 1, + scenarios: &reordered, + }) + .unwrap(); + let replay_certificate = replay.certificates().next().unwrap(); + assert_eq!( + replay_certificate.observation_backing_ptr_for_test(), + ready_backing, + "scenario permutation at the same revision must be exact idempotence" + ); + + let changed_same_revision = [PackageProgramScenarioV1::new(1, &white)]; + let error = match session.update(PackageProgramUpdateV1::Observed { + revision: 1, + scenarios: &changed_same_revision, + }) { + Ok(_) => panic!("changed payload at the same revision must be rejected"), + Err(error) => error, + }; + assert_eq!( + error.kind(), + PackageProgramUpdateErrorKindV1::RevisionConflict + ); + assert_eq!(session.state().kind(), PackageProgramStateKindV1::Ready); + assert_eq!(session.state().revision(), Some(1)); + + let stale = session + .update(PackageProgramUpdateV1::Unknown { + revision: 2, + reason_id: 7, + }) + .unwrap(); + assert_eq!(stale.kind(), PackageProgramStateKindV1::Stale); + assert_eq!(stale.revision(), Some(2)); + let certificates = stale.certificates().collect::>(); + assert_eq!(certificates.len(), 1); + assert_eq!( + certificates[0].kind(), + PackageProgramCertificateKindV1::Verified + ); + assert_eq!(certificates[0].revision(), 1); + assert_eq!( + stale.operations().collect::>(), + [PackageProgramOperationV1::Hold { + output_slot: OUTPUT.value(), + certificate_index: 0, + }] + ); +} + +#[test] +fn concrete_package_bridge_distinguishes_failed_remove_from_failed_hold() { + let white = [Srgb8::new([0xFF; 3])]; + let black = [Srgb8::new([0; 3])]; + let white_only = [PackageProgramScenarioV1::new(1, &white)]; + + let owner = PackageProgramOwnerV1::from_compiled(finite_program([[0x80; 3], [0xFF; 3]])); + let mut session = owner.instantiate(11).unwrap(); + let failed = session + .update(PackageProgramUpdateV1::Observed { + revision: 1, + scenarios: &white_only, + }) + .unwrap(); + assert_eq!(failed.kind(), PackageProgramStateKindV1::Failed); + assert_eq!(failed.cause_certificate_index(), Some(0)); + let certificates = failed.certificates().collect::>(); + assert_eq!(certificates.len(), 1); + assert_eq!( + certificates[0].kind(), + PackageProgramCertificateKindV1::Conflict + ); + assert_eq!(certificates[0].revision(), 1); + assert_eq!( + failed.operations().collect::>(), + [PackageProgramOperationV1::Remove { + output_slot: OUTPUT.value(), + }] + ); + + let owner = PackageProgramOwnerV1::from_compiled(finite_program([[0; 3], [0xFF; 3]])); + let mut session = owner.instantiate(12).unwrap(); + session + .update(PackageProgramUpdateV1::Observed { + revision: 1, + scenarios: &white_only, + }) + .unwrap(); + let both = [ + PackageProgramScenarioV1::new(1, &white), + PackageProgramScenarioV1::new(2, &black), + ]; + let failed = session + .update(PackageProgramUpdateV1::Observed { + revision: 2, + scenarios: &both, + }) + .unwrap(); + assert_eq!(failed.kind(), PackageProgramStateKindV1::Failed); + assert_eq!(failed.cause_certificate_index(), Some(0)); + let certificates = failed.certificates().collect::>(); + assert_eq!( + certificates + .iter() + .map(|certificate| (certificate.kind(), certificate.revision())) + .collect::>(), + [ + (PackageProgramCertificateKindV1::Conflict, 2), + (PackageProgramCertificateKindV1::Verified, 1), + ] + ); + assert_eq!( + failed.operations().collect::>(), + [PackageProgramOperationV1::Hold { + output_slot: OUTPUT.value(), + certificate_index: 1, + }] + ); +} + +#[test] +fn concrete_package_bridge_rejects_transport_shape_before_core_admission() { + let owner = PackageProgramOwnerV1::from_compiled(finite_program([[0x80; 3], [0; 3]])); + let mut session = owner.instantiate(11).unwrap(); + let empty_values = []; + let malformed = [PackageProgramScenarioV1::new(1, &empty_values)]; + let error = match session.update(PackageProgramUpdateV1::Observed { + revision: 1, + scenarios: &malformed, + }) { + Ok(_) => panic!("schema-short package input must fail before Core admission"), + Err(error) => error, + }; + assert_eq!( + error.kind(), + PackageProgramUpdateErrorKindV1::InvalidObservation + ); + assert_eq!(session.state().kind(), PackageProgramStateKindV1::Waiting); + assert_eq!(session.state().revision(), None); + + let white = [Srgb8::new([0xFF; 3])]; + let valid = [PackageProgramScenarioV1::new(1, &white)]; + session + .update(PackageProgramUpdateV1::Observed { + revision: 2, + scenarios: &valid, + }) + .unwrap(); + let duplicate = [ + PackageProgramScenarioV1::new(7, &white), + PackageProgramScenarioV1::new(7, &white), + ]; + let error = match session.update(PackageProgramUpdateV1::Observed { + revision: 1, + scenarios: &duplicate, + }) { + Ok(_) => panic!("duplicate scenario IDs must precede revision admission"), + Err(error) => error, + }; + assert_eq!( + error.kind(), + PackageProgramUpdateErrorKindV1::InvalidObservation + ); + assert_eq!(session.state().kind(), PackageProgramStateKindV1::Ready); + assert_eq!(session.state().revision(), Some(2)); +} diff --git a/crates/labcolors-core/src/program_session.rs b/crates/labcolors-core/src/program_session.rs index 97e668b4..f9c01e64 100644 --- a/crates/labcolors-core/src/program_session.rs +++ b/crates/labcolors-core/src/program_session.rs @@ -22,9 +22,10 @@ use crate::appearance::{ }; use crate::composition::CompositionProfileV1; use crate::constraints::{ - HardDecision, ProgramPointAssessmentErrorV1, ProgramPointEvaluatorV1, ProgramPointInvocation, - ProgramPointTargetV1, ProgramVisiblePointBindingV1, ProgramVisiblePointPassEvidence, - ProgramVisiblePointViolationEvidence, assess_program_point_hard, + Evaluator, ExactSrgb8IdentityV1, HardDecision, ProgramPointAssessmentErrorV1, + ProgramPointEvaluatorV1, ProgramPointInvocation, ProgramPointTargetV1, + ProgramVisiblePointBindingV1, ProgramVisiblePointPassEvidence, + ProgramVisiblePointViolationEvidence, Wcag22Srgb8V1, assess_program_point_hard, }; use crate::joint::{ AdmittedFiniteJointOrderV1, FiniteDomainOrdinalV1, FiniteJointOrderErrorV1, @@ -42,6 +43,7 @@ use crate::session::{ Session, SessionDecision, SessionEvidenceV1, SessionObservationBindingPermitV1, SessionPlanV1, private as session_private, }; +use crate::wcag22::Wcag22CriterionV1; /// Opaque identity of one immutable authored colour source. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -448,6 +450,170 @@ impl ConstraintSet { } } +/// Static dispatch contract used by one Program epoch. The invocation, +/// evaluator error, and both evidence branches are one closed type family; +/// no trait object or client-provided callback reaches the evaluation loop. +type ProgramConstraintAssessmentResultV1 = Result< + HardDecision< + ::PassEvidence, + ::ViolationEvidence, + >, + ProgramPointAssessmentErrorV1<::Error>, +>; + +pub(crate) trait ProgramConstraintEvaluatorSetV1: Sized { + type Invocation: Copy; + type PassEvidence; + type ViolationEvidence; + type Error; + + fn assess( + &self, + source: &crate::appearance::ResolvedOccurrence, + modeled_lcs: ModeledLcsOccurrenceV1, + invocation: Self::Invocation, + ) -> ProgramConstraintAssessmentResultV1; + + fn pass_binding(evidence: &Self::PassEvidence) -> ProgramVisiblePointBindingV1; + + fn violation_binding(evidence: &Self::ViolationEvidence) -> ProgramVisiblePointBindingV1; +} + +impl ProgramConstraintEvaluatorSetV1 for Evaluation +where + Evaluation: ProgramPointEvaluatorV1, + ProgramPointInvocation: Copy, +{ + type Invocation = ProgramPointInvocation; + type PassEvidence = ProgramVisiblePointPassEvidence; + type ViolationEvidence = ProgramVisiblePointViolationEvidence; + type Error = >::Error; + + fn assess( + &self, + source: &crate::appearance::ResolvedOccurrence, + modeled_lcs: ModeledLcsOccurrenceV1, + invocation: Self::Invocation, + ) -> ProgramConstraintAssessmentResultV1 { + assess_program_point_hard(source, modeled_lcs, self, invocation) + } + + fn pass_binding(evidence: &Self::PassEvidence) -> ProgramVisiblePointBindingV1 { + *evidence.binding() + } + + fn violation_binding(evidence: &Self::ViolationEvidence) -> ProgramVisiblePointBindingV1 { + *evidence.binding() + } +} + +/// Generates the code-owned heterogeneous evaluator set as parallel closed +/// unions. Each evidence variant retains the concrete evaluator's physical + +/// LCS binding, identity, release, capability, invocation, measurement, and +/// classifier payload. Adding a family therefore requires a Core code change +/// in this single declaration, not a client-extensible semantic registry. +macro_rules! define_core_program_evaluators_v1 { + ($( + $variant:ident { + evaluator: $evaluator:ty = $evaluator_value:expr, + invocation: $invocation:ty + } + ),+ $(,)?) => { + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub(crate) enum CoreProgramConstraintInvocationV1 { + $($variant($invocation)),+ + } + + pub(crate) enum CoreProgramPassEvidenceV1 { + $($variant(ProgramVisiblePointPassEvidence<$evaluator>)),+ + } + + pub(crate) enum CoreProgramViolationEvidenceV1 { + $($variant(ProgramVisiblePointViolationEvidence<$evaluator>)),+ + } + + #[derive(Debug, PartialEq)] + pub(crate) enum CoreProgramEvaluatorErrorV1 { + $($variant(<$evaluator as Evaluator>::Error)),+ + } + + /// The sole production evaluator set for this Program schema version. + /// Dispatch compiles to a direct match over the generated invocation + /// tag; it performs neither virtual dispatch nor lookup allocation. + #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] + pub(crate) struct CoreProgramEvaluatorsV1; + + impl ProgramConstraintEvaluatorSetV1 for CoreProgramEvaluatorsV1 { + type Invocation = CoreProgramConstraintInvocationV1; + type PassEvidence = CoreProgramPassEvidenceV1; + type ViolationEvidence = CoreProgramViolationEvidenceV1; + type Error = CoreProgramEvaluatorErrorV1; + + fn assess( + &self, + source: &crate::appearance::ResolvedOccurrence, + modeled_lcs: ModeledLcsOccurrenceV1, + invocation: Self::Invocation, + ) -> ProgramConstraintAssessmentResultV1 { + match invocation { + $(CoreProgramConstraintInvocationV1::$variant(invocation) => { + let evaluator: $evaluator = $evaluator_value; + match assess_program_point_hard( + source, + modeled_lcs, + &evaluator, + invocation, + ) { + Ok(HardDecision::Pass(evidence)) => Ok(HardDecision::Pass( + CoreProgramPassEvidenceV1::$variant(evidence), + )), + Ok(HardDecision::Violation(evidence)) => Ok(HardDecision::Violation( + CoreProgramViolationEvidenceV1::$variant(evidence), + )), + Err(ProgramPointAssessmentErrorV1::Binding(source)) => { + Err(ProgramPointAssessmentErrorV1::Binding(source)) + } + Err(ProgramPointAssessmentErrorV1::Evaluator(source)) => Err( + ProgramPointAssessmentErrorV1::Evaluator( + CoreProgramEvaluatorErrorV1::$variant(source), + ), + ), + } + }),+ + } + } + + fn pass_binding(evidence: &Self::PassEvidence) -> ProgramVisiblePointBindingV1 { + match evidence { + $(CoreProgramPassEvidenceV1::$variant(evidence) => *evidence.binding()),+ + } + } + + fn violation_binding( + evidence: &Self::ViolationEvidence, + ) -> ProgramVisiblePointBindingV1 { + match evidence { + $(CoreProgramViolationEvidenceV1::$variant(evidence) => *evidence.binding()),+ + } + } + } + }; +} + +define_core_program_evaluators_v1! { + ExactSrgb8 { + evaluator: ExactSrgb8IdentityV1 = ExactSrgb8IdentityV1, + invocation: Srgb8 + }, + Wcag22Srgb8 { + evaluator: Wcag22Srgb8V1 = Wcag22Srgb8V1, + invocation: Wcag22CriterionV1 + }, +} + +type ProgramConstraintInvocationOf = + ::Invocation; + /// Compile-time binding from one terminal slot to one Paint. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct OutputBinding { @@ -496,8 +662,8 @@ impl ObservationGroup { /// Immutable generic point Program. pub struct Program where - Evaluation: ProgramPointEvaluatorV1, - ProgramPointInvocation: Copy, + Evaluation: ProgramConstraintEvaluatorSetV1, + ProgramConstraintInvocationOf: Copy, { sources: Vec, targets: Vec, @@ -507,15 +673,15 @@ where paints: Vec, surfaces: Vec, occurrences: Vec, - constraints: ConstraintSet>, + constraints: ConstraintSet>, outputs: Vec, evaluator: Evaluation, } impl Program where - Evaluation: ProgramPointEvaluatorV1, - ProgramPointInvocation: Copy, + Evaluation: ProgramConstraintEvaluatorSetV1, + ProgramConstraintInvocationOf: Copy, { #[allow(clippy::too_many_arguments)] pub fn new( @@ -526,7 +692,7 @@ where paints: Vec, surfaces: Vec, occurrences: Vec, - constraints: ConstraintSet>, + constraints: ConstraintSet>, outputs: Vec, evaluator: Evaluation, ) -> Self { @@ -560,6 +726,11 @@ where } } +/// Concrete monomorphized Program boundary for package/WASM lowering. The +/// generic form remains an internal test seam; package code binds only this +/// code-owned evaluator union. +pub(crate) type CoreProgramV1 = Program; + /// Atomic compile failure. No executable partial graph escapes. #[derive(Debug, PartialEq, Eq)] pub enum ProgramCompileError { @@ -736,15 +907,15 @@ struct CompiledJointSelectionV1 { struct ProgramEpochV1 where - Evaluation: ProgramPointEvaluatorV1, - ProgramPointInvocation: Copy, + Evaluation: ProgramConstraintEvaluatorSetV1, + ProgramConstraintInvocationOf: Copy, { evaluator: Evaluation, graph: CompiledAppearanceGraph, binding_template: AdmittedAppearanceBindings, observation_group: CompiledObservationGroupV1, occurrence_contexts: Box<[CompiledOccurrenceContextV1]>, - constraints: Box<[CompiledPointConstraint>]>, + constraints: Box<[CompiledPointConstraint>]>, outputs: Box<[CompiledOutputBinding]>, finite_targets: Box<[CompiledFiniteTargetV1]>, joint_selection: Option, @@ -755,22 +926,24 @@ where /// the contained epoch never becomes an independently shareable API. pub(crate) struct ProgramOwnerLeaseV1(Rc>) where - Evaluation: ProgramPointEvaluatorV1, - ProgramPointInvocation: Copy; + Evaluation: ProgramConstraintEvaluatorSetV1, + ProgramConstraintInvocationOf: Copy; /// Fully validated immutable Program, not yet attached to runtime. pub struct CompiledProgram where - Evaluation: ProgramPointEvaluatorV1, - ProgramPointInvocation: Copy, + Evaluation: ProgramConstraintEvaluatorSetV1, + ProgramConstraintInvocationOf: Copy, { owner_generation: Rc>, } +pub(crate) type CompiledCoreProgramV1 = CompiledProgram; + impl CompiledProgram where - Evaluation: ProgramPointEvaluatorV1, - ProgramPointInvocation: Copy, + Evaluation: ProgramConstraintEvaluatorSetV1, + ProgramConstraintInvocationOf: Copy, { pub fn observation_group_id(&self) -> ObservationGroupId { self.owner_generation.observation_group.id @@ -847,15 +1020,15 @@ fn map_session_instantiate_error(error: BindingError) -> ProgramSessionInstantia /// One evaluator classification retained in the complete Program report. pub enum ProgramConstraintResultV1 where - Evaluation: ProgramPointEvaluatorV1, + Evaluation: ProgramConstraintEvaluatorSetV1, { - Pass(ProgramVisiblePointPassEvidence), - Violation(ProgramVisiblePointViolationEvidence), + Pass(Evaluation::PassEvidence), + Violation(Evaluation::ViolationEvidence), } impl ProgramConstraintResultV1 where - Evaluation: ProgramPointEvaluatorV1, + Evaluation: ProgramConstraintEvaluatorSetV1, { pub const fn is_violation(&self) -> bool { matches!(self, Self::Violation(_)) @@ -863,8 +1036,8 @@ where fn binding(&self) -> ProgramVisiblePointBindingV1 { match self { - Self::Pass(evidence) => *evidence.binding(), - Self::Violation(evidence) => *evidence.binding(), + Self::Pass(evidence) => Evaluation::pass_binding(evidence), + Self::Violation(evidence) => Evaluation::violation_binding(evidence), } } @@ -876,7 +1049,7 @@ where /// One canonical `physical case × constraint` report cell. pub struct ProgramConstraintCellV1 where - Evaluation: ProgramPointEvaluatorV1, + Evaluation: ProgramConstraintEvaluatorSetV1, { candidate_state_index: usize, case_index: usize, @@ -888,7 +1061,7 @@ where impl ProgramConstraintCellV1 where - Evaluation: ProgramPointEvaluatorV1, + Evaluation: ProgramConstraintEvaluatorSetV1, { pub const fn candidate_state_index(&self) -> usize { self.candidate_state_index @@ -922,7 +1095,7 @@ where /// Complete revision-bound assessment in case-major, constraint-ID order. pub struct ProgramReportV1 where - Evaluation: ProgramPointEvaluatorV1, + Evaluation: ProgramConstraintEvaluatorSetV1, { observation: RevisionBoundObservationV1, cells: Vec>, @@ -930,7 +1103,7 @@ where impl ProgramReportV1 where - Evaluation: ProgramPointEvaluatorV1, + Evaluation: ProgramConstraintEvaluatorSetV1, { pub const fn observation(&self) -> &RevisionBoundObservationV1 { &self.observation @@ -965,7 +1138,7 @@ impl ProgramOutputV1 { /// All hard cells passed over the complete admitted physical support. pub struct ProgramVerifiedV1 where - Evaluation: ProgramPointEvaluatorV1, + Evaluation: ProgramConstraintEvaluatorSetV1, { report: ProgramReportV1, outputs: Vec, @@ -973,13 +1146,13 @@ where } impl session_private::EvidenceSealed for ProgramVerifiedV1 where - Evaluation: ProgramPointEvaluatorV1 + Evaluation: ProgramConstraintEvaluatorSetV1 { } impl SessionEvidenceV1 for ProgramVerifiedV1 where - Evaluation: ProgramPointEvaluatorV1, + Evaluation: ProgramConstraintEvaluatorSetV1, { fn observation(&self) -> &RevisionBoundObservationV1 { self.report().observation() @@ -988,7 +1161,7 @@ where impl ProgramVerifiedV1 where - Evaluation: ProgramPointEvaluatorV1, + Evaluation: ProgramConstraintEvaluatorSetV1, { pub const fn report(&self) -> &ProgramReportV1 { &self.report @@ -1009,20 +1182,20 @@ where /// and therefore cannot be mistaken for committed Paints. pub struct ProgramConflictV1 where - Evaluation: ProgramPointEvaluatorV1, + Evaluation: ProgramConstraintEvaluatorSetV1, { report: ProgramReportV1, considered_state_count: usize, } impl session_private::EvidenceSealed for ProgramConflictV1 where - Evaluation: ProgramPointEvaluatorV1 + Evaluation: ProgramConstraintEvaluatorSetV1 { } impl SessionEvidenceV1 for ProgramConflictV1 where - Evaluation: ProgramPointEvaluatorV1, + Evaluation: ProgramConstraintEvaluatorSetV1, { fn observation(&self) -> &RevisionBoundObservationV1 { self.report().observation() @@ -1031,7 +1204,7 @@ where impl ProgramConflictV1 where - Evaluation: ProgramPointEvaluatorV1, + Evaluation: ProgramConstraintEvaluatorSetV1, { pub const fn report(&self) -> &ProgramReportV1 { &self.report @@ -1078,8 +1251,7 @@ pub enum ProgramSessionEvaluationError { InternalInvariant, } -type ProgramEvaluatorError = - >::Error; +type ProgramEvaluatorError = ::Error; type ProgramSessionEvaluationResult = Result< SessionDecision, ProgramConflictV1>, @@ -1182,7 +1354,7 @@ fn try_reserve_program_evaluation_buffer( struct PreparedProgramEvaluationBuffersV1 where - Evaluation: ProgramPointEvaluatorV1, + Evaluation: ProgramConstraintEvaluatorSetV1, { selected_cells: Vec>, conflict_cells: Vec>, @@ -1192,7 +1364,7 @@ where struct SelectedProgramEvaluationBuffersV1 where - Evaluation: ProgramPointEvaluatorV1, + Evaluation: ProgramConstraintEvaluatorSetV1, { cells: Vec>, outputs: Vec, @@ -1201,7 +1373,7 @@ where impl PreparedProgramEvaluationBuffersV1 where - Evaluation: ProgramPointEvaluatorV1, + Evaluation: ProgramConstraintEvaluatorSetV1, { fn take_selected(&mut self) -> SelectedProgramEvaluationBuffersV1 { SelectedProgramEvaluationBuffersV1 { @@ -1221,8 +1393,8 @@ fn prepare_program_evaluation_buffers( ProgramSessionEvaluationError>, > where - Evaluation: ProgramPointEvaluatorV1, - ProgramPointInvocation: Copy, + Evaluation: ProgramConstraintEvaluatorSetV1, + ProgramConstraintInvocationOf: Copy, { let state_count = joint_state_count.unwrap_or(1); if state_count == 0 { @@ -1260,8 +1432,8 @@ where /// the Session itself cannot prolong the owner lifetime. pub(crate) struct ProgramSessionPlan where - Evaluation: ProgramPointEvaluatorV1, - ProgramPointInvocation: Copy, + Evaluation: ProgramConstraintEvaluatorSetV1, + ProgramConstraintInvocationOf: Copy, { owner_generation: Weak>, schema: CanonicalObservationSchemaV1, @@ -1272,15 +1444,15 @@ where impl session_private::PlanSealed for ProgramSessionPlan where - Evaluation: ProgramPointEvaluatorV1, - ProgramPointInvocation: Copy, + Evaluation: ProgramConstraintEvaluatorSetV1, + ProgramConstraintInvocationOf: Copy, { } impl SessionPlanV1 for ProgramSessionPlan where - Evaluation: ProgramPointEvaluatorV1, - ProgramPointInvocation: Copy, + Evaluation: ProgramConstraintEvaluatorSetV1, + ProgramConstraintInvocationOf: Copy, { type OwnerLease = ProgramOwnerLeaseV1; type Verified = ProgramVerifiedV1; @@ -1311,8 +1483,8 @@ fn evaluate_program_session( observation: RevisionBoundObservationV1, ) -> ProgramSessionEvaluationResult where - Evaluation: ProgramPointEvaluatorV1, - ProgramPointInvocation: Copy, + Evaluation: ProgramConstraintEvaluatorSetV1, + ProgramConstraintInvocationOf: Copy, { let Some(selection) = &epoch.joint_selection else { let mut buffers = prepare_program_evaluation_buffers(epoch, &observation, None)?; @@ -1402,8 +1574,8 @@ fn apply_joint_candidate( tuple: &[FiniteDomainOrdinalV1], ) -> Result<(), ProgramSessionEvaluationError>> where - Evaluation: ProgramPointEvaluatorV1, - ProgramPointInvocation: Copy, + Evaluation: ProgramConstraintEvaluatorSetV1, + ProgramConstraintInvocationOf: Copy, { if targets.len() != tuple.len() { return Err(ProgramSessionEvaluationError::InternalInvariant); @@ -1429,8 +1601,8 @@ fn collect_program_candidate_into( buffers: SelectedProgramEvaluationBuffersV1, ) -> ProgramSessionEvaluationResult where - Evaluation: ProgramPointEvaluatorV1, - ProgramPointInvocation: Copy, + Evaluation: ProgramConstraintEvaluatorSetV1, + ProgramConstraintInvocationOf: Copy, { let SelectedProgramEvaluationBuffersV1 { mut cells, @@ -1480,8 +1652,8 @@ fn scan_program_candidate( mut outputs: Option<&mut Vec>, ) -> Result>> where - Evaluation: ProgramPointEvaluatorV1, - ProgramPointInvocation: Copy, + Evaluation: ProgramConstraintEvaluatorSetV1, + ProgramConstraintInvocationOf: Copy, { let schema = &epoch.observation_group.schema; if !observation.shares_schema_backing_with(schema) { @@ -1563,10 +1735,10 @@ where modeled } }; - let decision = assess_program_point_hard( + let decision = Evaluation::assess( + &epoch.evaluator, source, modeled_lcs_occurrence, - &epoch.evaluator, constraint.invocation, ) .map_err(|error| match error { @@ -1659,8 +1831,8 @@ fn prepare_program( mut program: Program, ) -> Result, ProgramCompileError> where - Evaluation: ProgramPointEvaluatorV1, - ProgramPointInvocation: Copy, + Evaluation: ProgramConstraintEvaluatorSetV1, + ProgramConstraintInvocationOf: Copy, { if program.observation_group.surface_input_ports.is_empty() { return Err(ProgramCompileError::EmptyObservationGroup { @@ -1734,8 +1906,8 @@ fn canonicalize_sources_and_targets( program: &mut Program, ) -> Result<(), ProgramCompileError> where - Evaluation: ProgramPointEvaluatorV1, - ProgramPointInvocation: Copy, + Evaluation: ProgramConstraintEvaluatorSetV1, + ProgramConstraintInvocationOf: Copy, { program.sources.sort_unstable_by_key(|source| source.id); if let Some(source) = program @@ -1824,8 +1996,8 @@ fn index_program_dependencies( program: &Program, ) -> Result where - Evaluation: ProgramPointEvaluatorV1, - ProgramPointInvocation: Copy, + Evaluation: ProgramConstraintEvaluatorSetV1, + ProgramConstraintInvocationOf: Copy, { let mut paint_ids = Vec::new(); paint_ids @@ -1946,8 +2118,8 @@ struct ProgramDependencyScratchV1 { impl ProgramDependencyScratchV1 { fn new(program: &Program) -> Result where - Evaluation: ProgramPointEvaluatorV1, - ProgramPointInvocation: Copy, + Evaluation: ProgramConstraintEvaluatorSetV1, + ProgramConstraintInvocationOf: Copy, { let node_count = program .paints @@ -2042,8 +2214,8 @@ fn validate_terminal_dependency_cone( program: &Program, ) -> Result<(), ProgramCompileError> where - Evaluation: ProgramPointEvaluatorV1, - ProgramPointInvocation: Copy, + Evaluation: ProgramConstraintEvaluatorSetV1, + ProgramConstraintInvocationOf: Copy, { // Preserve the canonical missing-reference diagnostics owned by constraint // and output compilation before applying the stronger terminal-safety law. @@ -2357,11 +2529,14 @@ fn compile_occurrence_contexts( fn compile_constraints( graph: &CompiledAppearanceGraph, occurrence_contexts: &[CompiledOccurrenceContextV1], - authored: ConstraintSet>, -) -> Result>]>, ProgramCompileError> + authored: ConstraintSet>, +) -> Result< + Box<[CompiledPointConstraint>]>, + ProgramCompileError, +> where - Evaluation: ProgramPointEvaluatorV1, - ProgramPointInvocation: Copy, + Evaluation: ProgramConstraintEvaluatorSetV1, + ProgramConstraintInvocationOf: Copy, { let total = authored .hard @@ -2551,8 +2726,8 @@ fn lower_graph( program: &Program, ) -> Result where - Evaluation: ProgramPointEvaluatorV1, - ProgramPointInvocation: Copy, + Evaluation: ProgramConstraintEvaluatorSetV1, + ProgramConstraintInvocationOf: Copy, { let colors = try_collect_program( program.targets.len(), @@ -2627,8 +2802,8 @@ fn lower_bindings( program: &Program, ) -> Result where - Evaluation: ProgramPointEvaluatorV1, - ProgramPointInvocation: Copy, + Evaluation: ProgramConstraintEvaluatorSetV1, + ProgramConstraintInvocationOf: Copy, { let mut colors = Vec::new(); colors diff --git a/crates/labcolors-core/src/session.rs b/crates/labcolors-core/src/session.rs index 3e8643d3..e0657895 100644 --- a/crates/labcolors-core/src/session.rs +++ b/crates/labcolors-core/src/session.rs @@ -11,8 +11,9 @@ use std::mem; use crate::observation::{ CanonicalObservationSchemaV1, ObservationError, ObservationHeadViewV1, ObservationOwnerV1, - ObservationStreamId, ObservationUpdateInput, PreparedObservationUpdateV1, - RevisionBoundObservationV1, RevisionBoundUnknownV1, prepare_observation, + ObservationStreamId, ObservationUpdateInput, PreparedObservationUpdateV1, Revision, + RevisionBoundObservationV1, RevisionBoundUnknownV1, SchemaOrderedScenarioSourceV1, + prepare_observation, prepare_schema_ordered_observation, }; /// Crate-private sealing prevents an additional runtime owner from being @@ -196,56 +197,89 @@ impl Session { let prepared = prepare_observation(&mut self.raw_head, self.stream, &self.schema, update) .map_err(SessionUpdateError::Observation)?; - match prepared { - PreparedObservationUpdateV1::Idempotent(prepared) => { - let _raw_head = prepared.into_owner(); - Ok(&self.state) - } - PreparedObservationUpdateV1::Unknown(prepared) => { - let (raw_head, unknown) = prepared.into_parts(); - let next_state = match take_last_verified(&mut self.state) { - Some(previous) => SessionState::Stale { previous }, - None => SessionState::Waiting, - }; - *raw_head = SessionObservationHeadV1::Unknown(unknown); - self.state = next_state; - Ok(&self.state) - } - PreparedObservationUpdateV1::Observed(prepared) => { - // Clone only the small Rc-backed observation handle. Both the - // committed raw head and returned evidence then share the exact - // immutable observation backing. - let (raw_head, observation) = prepared.into_parts(); - let next_raw_head = SessionObservationHeadV1::Observed(observation.clone()); - let decision = self - .plan - .evaluate( - &owner, - observation, - SessionObservationBindingPermitV1::mint(), - ) - .map_err(SessionUpdateError::Plan)?; - let SessionObservationHeadV1::Observed(expected_observation) = &next_raw_head - else { - unreachable!("the pending raw head was constructed as Observed") - }; - if !decision - .observation() - .is_same_binding_as(expected_observation) - { - return Err(SessionUpdateError::EvidenceBindingInvariant); - } + apply_prepared_update(&mut self.plan, &mut self.state, &owner, prepared) + } + + /// Package hot path for already schema-ordered point-sRGB8 scenarios. + /// It shares the exact lifecycle transaction below without constructing + /// keyed surface bindings or a second raw observation owner. + pub(crate) fn update_schema_ordered( + &mut self, + revision: Revision, + source: &Source, + order_scratch: &mut Vec, + ) -> SessionUpdateResult<'_, Plan> { + let owner = self + .plan + .try_acquire_owner() + .ok_or(SessionUpdateError::OwnerExpired)?; + let prepared = prepare_schema_ordered_observation( + &mut self.raw_head, + self.stream, + &self.schema, + revision, + source, + order_scratch, + ) + .map_err(SessionUpdateError::Observation)?; + + apply_prepared_update(&mut self.plan, &mut self.state, &owner, prepared) + } +} - // All fallible work is complete. Commit with moves only. - let previous = take_last_verified(&mut self.state); - let next_state = match decision { - SessionDecision::Verified(current) => SessionState::Ready { current }, - SessionDecision::Violation(cause) => SessionState::Failed { cause, previous }, - }; - *raw_head = next_raw_head; - self.state = next_state; - Ok(&self.state) +fn apply_prepared_update<'session, Plan: SessionPlanV1>( + plan: &mut Plan, + state: &'session mut SessionState, + owner: &Plan::OwnerLease, + prepared: PreparedObservationUpdateV1<'_, SessionObservationHeadV1>, +) -> SessionUpdateResult<'session, Plan> { + match prepared { + PreparedObservationUpdateV1::Idempotent(prepared) => { + let _raw_head = prepared.into_owner(); + Ok(state) + } + PreparedObservationUpdateV1::Unknown(prepared) => { + let (raw_head, unknown) = prepared.into_parts(); + let next_state = match take_last_verified(state) { + Some(previous) => SessionState::Stale { previous }, + None => SessionState::Waiting, + }; + *raw_head = SessionObservationHeadV1::Unknown(unknown); + *state = next_state; + Ok(state) + } + PreparedObservationUpdateV1::Observed(prepared) => { + // Clone only the small Rc-backed observation handle. Both the + // committed raw head and returned evidence then share the exact + // immutable observation backing. + let (raw_head, observation) = prepared.into_parts(); + let next_raw_head = SessionObservationHeadV1::Observed(observation.clone()); + let decision = plan + .evaluate( + owner, + observation, + SessionObservationBindingPermitV1::mint(), + ) + .map_err(SessionUpdateError::Plan)?; + let SessionObservationHeadV1::Observed(expected_observation) = &next_raw_head else { + unreachable!("the pending raw head was constructed as Observed") + }; + if !decision + .observation() + .is_same_binding_as(expected_observation) + { + return Err(SessionUpdateError::EvidenceBindingInvariant); } + + // All fallible work is complete. Commit with moves only. + let previous = take_last_verified(state); + let next_state = match decision { + SessionDecision::Verified(current) => SessionState::Ready { current }, + SessionDecision::Violation(cause) => SessionState::Failed { cause, previous }, + }; + *raw_head = next_raw_head; + *state = next_state; + Ok(state) } } } diff --git a/crates/labcolors-core/tests/package_bridge_red.rs b/crates/labcolors-core/tests/package_bridge_red.rs new file mode 100644 index 00000000..4ac581d0 --- /dev/null +++ b/crates/labcolors-core/tests/package_bridge_red.rs @@ -0,0 +1,94 @@ +//! RED contract for the sole concrete Core package seam. +//! +//! This integration crate deliberately has no access to Core-private generic +//! evaluator/session machinery. It must compile using only one hidden, +//! concrete package module once that seam is linked after the P3 + weak-owner +//! rebase. + +use labcolors_core::Srgb8; +use labcolors_core::package_bridge::{ + PackageProgramCertificateV1, PackageProgramInstantiateErrorV1, PackageProgramOperationV1, + PackageProgramOwnerV1, PackageProgramScenarioV1, PackageProgramSessionV1, + PackageProgramStateKindV1, PackageProgramStateViewV1, PackageProgramUpdateErrorKindV1, + PackageProgramUpdateV1, +}; + +fn exact_size(iterator: I) -> I { + iterator +} + +#[allow(dead_code)] +fn wasm_can_use_only_the_concrete_owner_and_session( + owner: &PackageProgramOwnerV1, + session: &mut PackageProgramSessionV1, + scenarios: &[PackageProgramScenarioV1<'_>], +) -> Result<(), PackageProgramInstantiateErrorV1> { + let _independent_session = owner.instantiate(0xA11CE)?; + let update = PackageProgramUpdateV1::Observed { + revision: 1, + scenarios, + }; + let view = session.update(update).expect("well-formed update"); + assert_projection_is_linear(view); + Ok(()) +} + +fn assert_projection_is_linear(view: PackageProgramStateViewV1<'_>) { + let _kind: PackageProgramStateKindV1 = view.kind(); + let _revision: Option = view.revision(); + let certificates = exact_size(view.certificates()); + let certificate_count = certificates.len(); + for certificate in certificates { + let _: PackageProgramCertificateV1<'_> = certificate; + } + for operation in exact_size(view.operations()) { + match operation { + PackageProgramOperationV1::Set { + output_slot, + source, + opacity, + certificate_index, + } => { + let _: u32 = output_slot; + let _: Srgb8 = source; + assert!(opacity.is_finite() && (0.0..=1.0).contains(&opacity)); + assert!(certificate_index < certificate_count); + } + PackageProgramOperationV1::Remove { output_slot } => { + let _: u32 = output_slot; + } + PackageProgramOperationV1::Hold { + output_slot, + certificate_index, + } => { + let _: u32 = output_slot; + assert!(certificate_index < certificate_count); + } + } + } +} + +#[allow(dead_code)] +fn unknown_is_revision_bound_without_a_stream_or_generation_field( + session: &mut PackageProgramSessionV1, +) { + let update = PackageProgramUpdateV1::Unknown { + revision: 2, + reason_id: 7, + }; + let _ = session.update(update); +} + +#[allow(dead_code)] +fn owner_expiry_is_a_closed_package_error( + error: labcolors_core::package_bridge::PackageProgramUpdateErrorV1, +) { + assert_eq!(error.kind(), PackageProgramUpdateErrorKindV1::OwnerExpired); +} + +#[test] +fn red_contract_is_linked_by_the_concrete_package_module() { + // Reaching this test means the external crate compiled without importing + // Program, evaluator traits, Session, or numeric generations. + assert_eq!(core::mem::size_of::(), 3); +} diff --git a/packages/colors/bench/wasm.json b/packages/colors/bench/wasm.json index 528207e5..ff761b3b 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-29962821215", + "source": "github-actions-run-29966828219", "platform": "linux-x64", - "rawBytes": 376707 + "rawBytes": 376832 }, "policy": { - "maxRawBytes": 376707, - "basis": "p3-promise-hard-delete", + "maxRawBytes": 376832, + "basis": "mixed-evaluator-package-bridge", "gzip": "diagnostic-only" } } diff --git a/scripts/check-wasm-size-budget.mjs b/scripts/check-wasm-size-budget.mjs old mode 100644 new mode 100755 index a24ddff4..d5008214 --- 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 = - "035cece04afa7ea37e819c2432c2238b1e902399c31db01c39fbd67e1e299018"; + "2a296bcdcef65e2a5d1e49ad088ee8c6e7f6fa2d2550a8d26e945e0d98dd0d61"; const SCHEMA_VERSION = 1; const CANONICAL_ARTIFACT = "packages/colors/pkg/labcolors_bg.wasm"; diff --git a/scripts/verify_point_support_surplus.py b/scripts/verify_point_support_surplus.py old mode 100644 new mode 100755 index 8dbf5b53..1529800f --- 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 = ( - "f5e813b21df3bad8c93fa3525190715110b4965792f5b7a2a0bfde7a3b4297d3" + "2cf6589a15d2669aca9f1f5a287841805c0fe7293074530d70a1cb803d235c7d" ) EXPECTED_Q55_PROOF_SHA256 = ( "ac59cf89503170c789223b91d775213a19d4e571ef930f2ea609fcd51b14defd" @@ -208,13 +208,13 @@ def verify_source_binding() -> tuple[str, int]: (OBSERVATION_SOURCE, b" && Rc::ptr_eq(&self.backing, &other.backing)\n", b" && self.backing == other.backing\n"), (LCS_OCCURRENCE_SOURCE, b" pub(crate) const fn srgb8(self) -> Srgb8 {\n self.srgb8\n }", b" pub(crate) const fn srgb8(self) -> Srgb8 {\n Srgb8::new([0, 0, 0])\n }"), (SESSION_SOURCE, b" Self::Observed(observation) => ObservationHeadViewV1::Observed(observation),\n", b" Self::Observed(_) => ObservationHeadViewV1::Empty,\n"), - (SESSION_SOURCE, b" let next_raw_head = SessionObservationHeadV1::Observed(observation.clone());\n", b" let next_raw_head = SessionObservationHeadV1::Empty;\n"), - (SESSION_SOURCE, b" *raw_head = next_raw_head;\n", b" *raw_head = SessionObservationHeadV1::Empty;\n"), - (SESSION_SOURCE, b" Some(previous) => SessionState::Stale { previous },", b" Some(_) => SessionState::Waiting,"), - (SESSION_SOURCE, b" SessionObservationBindingPermitV1::mint(),", b" SessionObservationBindingPermitV1::for_test(),"), - (SESSION_SOURCE, b" SessionDecision::Verified(current) => SessionState::Ready { current },", b" SessionDecision::Verified(current) => SessionState::Stale { previous: current },"), - (SESSION_SOURCE, b" SessionDecision::Violation(cause) => SessionState::Failed { cause, previous },", b" SessionDecision::Violation(_) => SessionState::Waiting,"), - (SESSION_SOURCE, b" return Err(SessionUpdateError::EvidenceBindingInvariant);\n", b" unreachable!();\n"), + (SESSION_SOURCE, b" let next_raw_head = SessionObservationHeadV1::Observed(observation.clone());\n", b" let next_raw_head = SessionObservationHeadV1::Empty;\n"), + (SESSION_SOURCE, b" *raw_head = next_raw_head;\n", b" *raw_head = SessionObservationHeadV1::Empty;\n"), + (SESSION_SOURCE, b" Some(previous) => SessionState::Stale { previous },", b" Some(_) => SessionState::Waiting,"), + (SESSION_SOURCE, b" SessionObservationBindingPermitV1::mint(),", b" SessionObservationBindingPermitV1::for_test(),"), + (SESSION_SOURCE, b" SessionDecision::Verified(current) => SessionState::Ready { current },", b" SessionDecision::Verified(current) => SessionState::Stale { previous: current },"), + (SESSION_SOURCE, b" SessionDecision::Violation(cause) => SessionState::Failed { cause, previous },", b" SessionDecision::Violation(_) => SessionState::Waiting,"), + (SESSION_SOURCE, b" return Err(SessionUpdateError::EvidenceBindingInvariant);\n", b" unreachable!();\n"), (NUMERICS_SOURCE, b"proof_ids: [NumericalProofIdV2::PointSupportReferenceSurplusIntegerV1],\n bound_status: Available", b"proof_ids: [NumericalProofIdV2::PointSupportReferenceSurplusIntegerV1],\n bound_status: Unavailable"), (COMPOSITION_SOURCE, b"f64::from(backdrop) + alpha * (f64::from(tint) - f64::from(backdrop))", b"f64::from(tint)"), (APPEARANCE_SOURCE, b"self.opacity\n", b"crate::composition::AdmittedOpacityV1::OPAQUE\n"), From e45fcc2471ddbb76a2aeb12f3b3ea5bb635f513a Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Thu, 23 Jul 2026 03:02:23 +0300 Subject: [PATCH 35/58] core: derive a typed CAM16-UCS occurrence view --- ...rt-reference-surplus-q55-bps-proof-v1.json | 2 +- crates/labcolors-core/src/lcs_occurrence.rs | 106 +++++++++++++- .../src/lcs_occurrence_tests.rs | 131 +++++++++++++++++- crates/labcolors-core/src/release_registry.rs | 58 ++++++-- .../src/release_registry_tests.rs | 53 ++++++- scripts/verify_point_support_surplus.py | 2 +- 6 files changed, 329 insertions(+), 23 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 9344cbe1..6f16630c 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":"6f24c596c8c29e4116bc1f19ba70390f90f2a606ae617eb04c42dd4a1da15343","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":"2cf6589a15d2669aca9f1f5a287841805c0fe7293074530d70a1cb803d235c7d","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":"e361cf144e0630a4f2ff52261e6515c9394188fc1351c97b98ea9c0ec87ec39d"},{"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":"a58acbae5b06a8cd9a45adf93ddfbdfb569c5916b60140bd4046182aab2a9518"},{"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":"5a5badfdd164d88aceaee64c4fe519a5151661242f4c4b7982bce816a8b85516"} +{"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":"5835c6225252df5184803b548ba151654a05db53ae9bc19b2a9be5999a868311","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":"4a762a7fd0a658286288ef651cbaf53940e4676f47c63c970799736650a62e21","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":"e361cf144e0630a4f2ff52261e6515c9394188fc1351c97b98ea9c0ec87ec39d"},{"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":"f546925d6dedf81bcb78ac9f7342ef8ab3ac93e61cfda5016783c3f733c0f9dd"} diff --git a/crates/labcolors-core/src/lcs_occurrence.rs b/crates/labcolors-core/src/lcs_occurrence.rs index 32eb9147..ea6a9295 100644 --- a/crates/labcolors-core/src/lcs_occurrence.rs +++ b/crates/labcolors-core/src/lcs_occurrence.rs @@ -10,7 +10,7 @@ //! arbitrary second context. use crate::Srgb8; -use crate::spaces::cam16::forward_correlates_v1; +use crate::spaces::cam16::{forward_correlates_v1, ucs_j, ucs_m}; use crate::spaces::oklab::xyz_d65_to_oklab_v1; use crate::spaces::srgb::xyz_d65_from_srgb8_v1; use crate::spaces::vc::{Cam16SurroundV1, ViewingConditions}; @@ -702,17 +702,26 @@ pub enum Cam16ViewReleaseId { LiEtAl2017Cie248ForwardV1, } +/// Formula and operation-order release of the rectangular CAM16-UCS view. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Cam16UcsViewReleaseId { + LiEtAl2017Cam16UcsV1, +} + /// Typed release discriminator used only to qualify derivation errors. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum AppearanceViewReleaseIdV1 { Oklab(OklabViewReleaseId), Cam16(Cam16ViewReleaseId), + Cam16Ucs(Cam16UcsViewReleaseId), } pub(crate) const OKLAB_VIEW_RELEASE_V1: OklabViewReleaseId = OklabViewReleaseId::Ottosson20210125XyzD65V1; pub(crate) const CAM16_VIEW_RELEASE_V1: Cam16ViewReleaseId = Cam16ViewReleaseId::LiEtAl2017Cie248ForwardV1; +pub(crate) const CAM16_UCS_VIEW_RELEASE_V1: Cam16UcsViewReleaseId = + Cam16UcsViewReleaseId::LiEtAl2017Cam16UcsV1; /// Finite binary64 coordinate with canonical positive zero. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -746,6 +755,10 @@ pub enum AppearanceViewFieldV1 { Cam16M, Cam16S, Cam16Hue, + Cam16UcsJPrime, + Cam16UcsMPrimeIntermediate, + Cam16UcsAPrime, + Cam16UcsBPrime, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -758,6 +771,11 @@ pub enum AppearanceStateDerivationErrorV1 { field: AppearanceViewFieldV1, reason: NumericDomainError, }, + InconsistentDerivedHueState { + release: Cam16UcsViewReleaseId, + hue: HueState, + m_prime_bits: u64, + }, } /// Rectangular Oklab geometry of one admitted XYZ(D65) stimulus. @@ -834,6 +852,38 @@ impl Cam16ViewV1 { } } +/// Rectangular CAM16-UCS coordinates derived from one admitted CAM16 view. +/// +/// This is a coordinate view of one occurrence, not a pairwise difference +/// calibration, universal perceptual scale, editable colour or inverse route. +/// Its polar magnitude is an intermediate only; the stored coordinates are the +/// published rectangular `(J', a', b')` form. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Cam16UcsViewV1 { + release: Cam16UcsViewReleaseId, + j_prime: FiniteNonNegative, + a_prime: FiniteCoordinate, + b_prime: FiniteCoordinate, +} + +impl Cam16UcsViewV1 { + pub(crate) const fn release(self) -> Cam16UcsViewReleaseId { + self.release + } + + pub(crate) fn j_prime(self) -> f64 { + self.j_prime.get() + } + + pub(crate) fn a_prime(self) -> f64 { + self.a_prime.get() + } + + pub(crate) fn b_prime(self) -> f64 { + self.b_prime.get() + } +} + /// One-way, derived appearance snapshot of exactly one occurrence. /// /// Canonical LCS identity remains [`LcsOccurrence`] (`sample × context`). This @@ -876,6 +926,15 @@ impl AppearanceState { pub(crate) const fn cam16(self) -> Cam16ViewV1 { self.cam16 } + + /// Derive the optional-cost rectangular CAM16-UCS view from this state's + /// already admitted CAM16 coordinates. + /// + /// Keeping the rescale lazy avoids an `ln` plus polar-to-rectangular + /// trigonometry for evaluators which request only Oklab or CAM16. + pub(crate) fn cam16_ucs(self) -> Result { + derive_cam16_ucs_view_v1(self.cam16) + } } fn view_numeric_error( @@ -951,3 +1010,48 @@ fn derive_cam16_view_v1( hue, }) } + +fn derive_cam16_ucs_view_v1( + cam16: Cam16ViewV1, +) -> Result { + let release = AppearanceViewReleaseIdV1::Cam16Ucs(CAM16_UCS_VIEW_RELEASE_V1); + let j_prime = FiniteNonNegative::new(ucs_j(cam16.j())).map_err(|reason| { + view_numeric_error(release, AppearanceViewFieldV1::Cam16UcsJPrime, reason) + })?; + let m_prime = FiniteNonNegative::new(ucs_m(cam16.m())).map_err(|reason| { + view_numeric_error( + release, + AppearanceViewFieldV1::Cam16UcsMPrimeIntermediate, + reason, + ) + })?; + + let [a_prime, b_prime] = if m_prime.get() == 0.0 { + // Rectangular zero has no angular dependency. This branch also keeps + // both coordinates canonical positive zero for exact black. + [0.0, 0.0] + } else { + let HueState::Defined(hue) = cam16.hue() else { + return Err( + AppearanceStateDerivationErrorV1::InconsistentDerivedHueState { + release: CAM16_UCS_VIEW_RELEASE_V1, + hue: cam16.hue(), + m_prime_bits: m_prime.get().to_bits(), + }, + ); + }; + let radians = hue.degrees().to_radians(); + [m_prime.get() * radians.cos(), m_prime.get() * radians.sin()] + }; + + Ok(Cam16UcsViewV1 { + release: CAM16_UCS_VIEW_RELEASE_V1, + j_prime, + a_prime: FiniteCoordinate::new(a_prime).map_err(|reason| { + view_numeric_error(release, AppearanceViewFieldV1::Cam16UcsAPrime, reason) + })?, + b_prime: FiniteCoordinate::new(b_prime).map_err(|reason| { + view_numeric_error(release, AppearanceViewFieldV1::Cam16UcsBPrime, reason) + })?, + }) +} diff --git a/crates/labcolors-core/src/lcs_occurrence_tests.rs b/crates/labcolors-core/src/lcs_occurrence_tests.rs index 7f9659d9..d51ab4e4 100644 --- a/crates/labcolors-core/src/lcs_occurrence_tests.rs +++ b/crates/labcolors-core/src/lcs_occurrence_tests.rs @@ -5,14 +5,14 @@ use crate::lcs_occurrence::{ ADMITTED_SRGB8_TRISTIMULUS_BINDING_V1, AdaptingLuminanceCdM2, AppearanceContextFieldV1, AppearanceContextId, AppearanceContextSchemaReleaseId, AppearanceState, AppearanceStateDerivationErrorV1, AppearanceViewFieldV1, AppearanceViewReleaseIdV1, - BackgroundLuminanceRatio, CAM16_VIEW_RELEASE_V1, ColorSignal, ColorimetricFrameId, - ColorimetricFrameReleaseId, ColorimetricTransformReleaseId, HueAngle, HueState, - IEC_SRGB_D65_XYZ_FRAME_V1, LcsOccurrence, MUTATION_SENTINEL_XYZ_FRAME_V1, + BackgroundLuminanceRatio, CAM16_UCS_VIEW_RELEASE_V1, CAM16_VIEW_RELEASE_V1, ColorSignal, + ColorimetricFrameId, ColorimetricFrameReleaseId, ColorimetricTransformReleaseId, HueAngle, + HueState, IEC_SRGB_D65_XYZ_FRAME_V1, LcsOccurrence, MUTATION_SENTINEL_XYZ_FRAME_V1, ModeledTristimulusDerivationV1, NumericDomainError, OKLAB_VIEW_RELEASE_V1, ObserverProfileId, OccurrenceFormationError, ReferenceWhiteId, SurroundProfileId, TristimulusComponentV1, TristimulusDomainErrorV1, TristimulusSample, TristimulusScale, derive_modeled_tristimulus_v1, }; -use crate::spaces::cam16::{ForwardCacheGuard, forward, forward_correlates_v1}; +use crate::spaces::cam16::{ForwardCacheGuard, forward, forward_correlates_v1, ucs_j, ucs_m}; use crate::spaces::oklab::{srgb_linear_to_oklab, xyz_d65_to_oklab_v1}; use crate::spaces::srgb::{D65_WHITE, srgb_linear_from_srgb8, srgb_to_xyz}; use crate::spaces::vc::ViewingConditions; @@ -385,6 +385,15 @@ fn appearance_state_is_one_way_views_of_the_same_occurrence_with_separate_releas assert_eq!(state.occurrence(), occurrence); assert_eq!(state.oklab().release(), OKLAB_VIEW_RELEASE_V1); assert_eq!(state.cam16().release(), CAM16_VIEW_RELEASE_V1); + assert_eq!( + state.cam16_ucs().unwrap().release(), + CAM16_UCS_VIEW_RELEASE_V1, + ); + assert_eq!( + state.occurrence().sample().frame().observer(), + ObserverProfileId::Cie1931TwoDegreeV1, + ); + assert_eq!(state.occurrence().context(), occurrence.context()); let direct = xyz_d65_to_oklab_v1(occurrence.sample().xyz()); assert_eq!( @@ -447,6 +456,11 @@ fn context_changes_only_the_contextual_cam16_view() { assert_eq!(average.oklab(), dim.oklab()); assert_ne!(average.cam16().j().to_bits(), dim.cam16().j().to_bits()); assert_ne!(average.cam16().m().to_bits(), dim.cam16().m().to_bits()); + assert_ne!( + average.cam16_ucs().unwrap().j_prime().to_bits(), + dim.cam16_ucs().unwrap().j_prime().to_bits(), + ); + assert_ne!(average.cam16_ucs().unwrap(), dim.cam16_ucs().unwrap()); assert_ne!(average.occurrence(), dim.occurrence()); } @@ -475,6 +489,29 @@ fn every_registered_surround_maps_to_its_exact_cam16_kernel_tuple() { panic!("chromatic fixture must have hue under {surround:?}"); }; assert_eq!(actual_hue.degrees().to_bits(), expected.h.to_bits()); + + let actual_ucs = AppearanceState::derive_v1(occurrence) + .unwrap() + .cam16_ucs() + .unwrap(); + let expected_j_prime = ucs_j(expected.j); + let expected_m_prime = ucs_m(expected.m); + let expected_radians = expected.h.to_radians(); + assert_eq!( + [ + actual_ucs.j_prime(), + actual_ucs.a_prime(), + actual_ucs.b_prime(), + ] + .map(f64::to_bits), + [ + expected_j_prime, + expected_m_prime * expected_radians.cos(), + expected_m_prime * expected_radians.sin(), + ] + .map(f64::to_bits), + "CAM16-UCS must be assembled from the same CAM16 view under {surround:?}", + ); } } @@ -496,6 +533,11 @@ fn exact_zero_coordinate_has_no_invented_hue() { assert_eq!(state.cam16().m().to_bits(), 0); assert_eq!(state.cam16().s().to_bits(), 0); assert_eq!(state.cam16().hue(), HueState::UndefinedExact); + let ucs = state.cam16_ucs().unwrap(); + assert_eq!( + [ucs.j_prime(), ucs.a_prime(), ucs.b_prime()].map(f64::to_bits), + [0; 3], + ); } #[test] @@ -581,6 +623,20 @@ fn cam16_release_pins_a_full_external_correlate_vector() { "CAM16 {name} drifted: actual={actual} expected={expected}", ); } + + // The same independent reference vector projected through Li et al. 2017 + // CAM16-UCS. These are coordinates of this occurrence, not a distance claim. + let ucs = state.cam16_ucs().unwrap(); + for (name, actual, expected, tolerance) in [ + ("J'", ucs.j_prime(), 36.503_620_495_334_07, 0.02), + ("a'", ucs.a_prime(), 10.042_750_480_031_993, 0.1), + ("b'", ucs.b_prime(), -43.950_878_225_240_46, 0.1), + ] { + assert!( + (actual - expected).abs() < tolerance, + "CAM16-UCS {name} drifted: actual={actual} expected={expected}", + ); + } } #[test] @@ -591,7 +647,10 @@ fn full_appearance_state_derivation_is_allocation_free() { crate::test_support::measured_allocations(|| AppearanceState::derive_v1(occurrence)); assert_eq!(allocations, 0); - assert!(derived.is_ok()); + let derived = derived.unwrap(); + let (ucs, ucs_allocations) = crate::test_support::measured_allocations(|| derived.cam16_ucs()); + assert_eq!(ucs_allocations, 0); + assert!(ucs.is_ok()); } #[test] @@ -664,4 +723,66 @@ fn appearance_state_bypasses_active_xyz_only_cache_for_each_context_without_allo }; assert_eq!(dim_hue.degrees().to_bits(), expected_dim.h.to_bits()); assert_eq!(dark_hue.degrees().to_bits(), expected_dark.h.to_bits()); + + let expected_ucs = |expected: crate::spaces::cam16::Cam16CorrelatesV1| { + let m_prime = ucs_m(expected.m); + let radians = expected.h.to_radians(); + [ + ucs_j(expected.j), + m_prime * radians.cos(), + m_prime * radians.sin(), + ] + .map(f64::to_bits) + }; + let actual_ucs = |view: crate::lcs_occurrence::Cam16UcsViewV1| { + [view.j_prime(), view.a_prime(), view.b_prime()].map(f64::to_bits) + }; + assert_eq!( + actual_ucs( + AppearanceState::derive_v1(dim_occurrence) + .unwrap() + .cam16_ucs() + .unwrap(), + ), + expected_ucs(expected_dim), + ); + assert_eq!( + actual_ucs( + AppearanceState::derive_v1(dark_occurrence) + .unwrap() + .cam16_ucs() + .unwrap(), + ), + expected_ucs(expected_dark), + ); +} + +#[test] +fn occurrence_views_have_no_ambient_preset_or_client_semantic_route() { + let source = include_str!("lcs_occurrence.rs"); + for forbidden in [ + "ViewingConditions::srgb()", + "ViewingConditions::dim_surround()", + "ForwardCacheGuard", + "PairFill", + "Glow", + "LabUI", + "Compatibility", + "Legacy", + ] { + assert!( + !source.contains(forbidden), + "occurrence/view foundation must not contain `{forbidden}`", + ); + } + for required in [ + "ViewingConditions::from_semantic_inputs_v1", + "forward_correlates_v1(occurrence.sample().xyz(), &vc)", + "derive_cam16_ucs_view_v1(self.cam16)", + ] { + assert!( + source.contains(required), + "explicit derived-view route must retain `{required}`", + ); + } } diff --git a/crates/labcolors-core/src/release_registry.rs b/crates/labcolors-core/src/release_registry.rs index e101b737..9c3b4a24 100644 --- a/crates/labcolors-core/src/release_registry.rs +++ b/crates/labcolors-core/src/release_registry.rs @@ -9,8 +9,9 @@ use crate::lcs_occurrence::{ ADMITTED_SRGB8_TRISTIMULUS_BINDING_V1, AdmittedSrgb8TristimulusBindingV1, - AppearanceContextSchemaReleaseId, CAM16_VIEW_RELEASE_V1, Cam16ViewReleaseId, - ColorimetricFrameId, IEC_SRGB_D65_XYZ_FRAME_V1, OKLAB_VIEW_RELEASE_V1, OklabViewReleaseId, + AppearanceContextSchemaReleaseId, CAM16_UCS_VIEW_RELEASE_V1, CAM16_VIEW_RELEASE_V1, + Cam16UcsViewReleaseId, Cam16ViewReleaseId, ColorimetricFrameId, IEC_SRGB_D65_XYZ_FRAME_V1, + OKLAB_VIEW_RELEASE_V1, OklabViewReleaseId, }; use crate::output_projection::{ CSS_COLOR_4_OKLCH_D65_FROM_MODELED_SRGB8_SOLID_V1, CssOklchHueSerializationReleaseIdV1, @@ -24,9 +25,11 @@ pub(crate) const RELEASE_REGISTRY_SCHEMA_VERSION_V1: u16 = 1; // key. The byte order is documented by `RegisteredReleaseDescriptorV1` below. const RELEASE_REGISTRY_CANONICAL_BYTES_V1: &[u8] = concat!( "labcolors.release-registry.canonical-binary.v1\0", - "\0\x01\0\x05", + "\0\x01\0\x06", "\x01\x01\0\x26cam16-li-et-al-2017-cie-248-forward-v1", "\x01\x02\x01\x01\x01\x02\x02\x02\0\0\0\0\0\0\0", + "\x01\x01\0\x26cam16-ucs-li-et-al-2017-rectangular-v1", + "\x01\x02\x01\x01\x04\x05\x05\x05\x03\x01\0\0\0\0\0", "\x01\x01\0\x24oklab-ottosson-2021-01-25-xyz-d65-v1", "\x01\x01\0\x01\x01\x01\x01\x01\0\0\0\0\0\0\0", "\x01\x01\0\x27polar-from-ottosson-2021-01-25-oklab-v1", @@ -37,7 +40,7 @@ const RELEASE_REGISTRY_CANONICAL_BYTES_V1: &[u8] = concat!( ) .as_bytes(); -const RELEASE_REGISTRY_FNV1A32_V1: u32 = 1_293_630_307; +const RELEASE_REGISTRY_FNV1A32_V1: u32 = 540_606_852; /// The disjoint kinds whose availability is reported by this registry. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -76,6 +79,7 @@ impl ReleaseRegistryAvailabilityV1 { #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub(crate) enum RegisteredColorReleaseIdV1 { Cam16View(Cam16ViewReleaseId), + Cam16UcsView(Cam16UcsViewReleaseId), OklabView(OklabViewReleaseId), OklchView(OklchViewReleaseId), CssColor4OklchD65FromModeledSrgb8Solid(OutputProjectionReleaseIdV1), @@ -84,9 +88,10 @@ pub(crate) enum RegisteredColorReleaseIdV1 { impl RegisteredColorReleaseIdV1 { pub(crate) const fn class(self) -> ReleaseRegistryClassV1 { match self { - Self::Cam16View(_) | Self::OklabView(_) | Self::OklchView(_) => { - ReleaseRegistryClassV1::AppearanceView - } + Self::Cam16View(_) + | Self::Cam16UcsView(_) + | Self::OklabView(_) + | Self::OklchView(_) => ReleaseRegistryClassV1::AppearanceView, Self::CssColor4OklchD65FromModeledSrgb8Solid(_) => { ReleaseRegistryClassV1::OutputProjection } @@ -99,6 +104,9 @@ impl RegisteredColorReleaseIdV1 { Self::Cam16View(Cam16ViewReleaseId::LiEtAl2017Cie248ForwardV1) => { "cam16-li-et-al-2017-cie-248-forward-v1" } + Self::Cam16UcsView(Cam16UcsViewReleaseId::LiEtAl2017Cam16UcsV1) => { + "cam16-ucs-li-et-al-2017-rectangular-v1" + } Self::OklabView(OklabViewReleaseId::Ottosson20210125XyzD65V1) => { "oklab-ottosson-2021-01-25-xyz-d65-v1" } @@ -171,6 +179,7 @@ pub(crate) enum RegistryAdmittedDomainV1 { FiniteNonNegativeXyzStimulusV1, FiniteOklabRectangularViewV1, ModeledIec61966Srgb8OccurrenceV1, + FiniteCam16ViewV1, } impl RegistryAdmittedDomainV1 { @@ -179,6 +188,7 @@ impl RegistryAdmittedDomainV1 { Self::FiniteNonNegativeXyzStimulusV1 => 1, Self::FiniteOklabRectangularViewV1 => 2, Self::ModeledIec61966Srgb8OccurrenceV1 => 3, + Self::FiniteCam16ViewV1 => 4, } } } @@ -190,6 +200,7 @@ pub(crate) enum RegistryCoordinateUnitsV1 { Cam16CorrelatesUnitlessHueDegreesV1, OklchCoordinatesUnitlessHueDegreesV1, CssColor4OklchPercentLightnessNumericChromaHueDegreesV1, + Cam16UcsJPrimeAPrimeBPrimeUnitlessV1, } impl RegistryCoordinateUnitsV1 { @@ -199,6 +210,7 @@ impl RegistryCoordinateUnitsV1 { Self::Cam16CorrelatesUnitlessHueDegreesV1 => 2, Self::OklchCoordinatesUnitlessHueDegreesV1 => 3, Self::CssColor4OklchPercentLightnessNumericChromaHueDegreesV1 => 4, + Self::Cam16UcsJPrimeAPrimeBPrimeUnitlessV1 => 5, } } } @@ -210,6 +222,7 @@ pub(crate) enum RegistryAchromaticLawV1 { HueUndefinedExactlyWhenCam16MIsZeroV1, HueUndefinedExactlyWhenOklabAAndBAreZeroV1, ExactSourceGreyOrRectangularOriginSerializesHueZeroV1, + RectangularChromaOriginExactlyWhenCam16MIsZeroV1, } impl RegistryAchromaticLawV1 { @@ -219,6 +232,7 @@ impl RegistryAchromaticLawV1 { Self::HueUndefinedExactlyWhenCam16MIsZeroV1 => 2, Self::HueUndefinedExactlyWhenOklabAAndBAreZeroV1 => 3, Self::ExactSourceGreyOrRectangularOriginSerializesHueZeroV1 => 4, + Self::RectangularChromaOriginExactlyWhenCam16MIsZeroV1 => 5, } } } @@ -230,6 +244,7 @@ pub(crate) enum RegistryReferenceIdentityV1 { LiEtAl2017Cie248Cam16ForwardV1, Ottosson20210125OklabPolarV1, CssColor4OklchD65V1, + LiEtAl2017Cam16UcsCoordinatesV1, } impl RegistryReferenceIdentityV1 { @@ -239,6 +254,7 @@ impl RegistryReferenceIdentityV1 { Self::LiEtAl2017Cie248Cam16ForwardV1 => 2, Self::Ottosson20210125OklabPolarV1 => 3, Self::CssColor4OklchD65V1 => 4, + Self::LiEtAl2017Cam16UcsCoordinatesV1 => 5, } } } @@ -297,18 +313,26 @@ impl OutputProjectionDependencyGraphV1 { pub(crate) enum ReleaseDependencyGraphV1 { DirectV1, OklchPolarFromOklabV1(OklabViewReleaseId), + Cam16UcsRectangularFromCam16V1(Cam16ViewReleaseId), CssColor4OklchD65V1(OutputProjectionDependencyGraphV1), } impl ReleaseDependencyGraphV1 { - /// Fixed canonical fields: - /// `[graph, source-binding, Oklab, Oklch, number, hue-policy, gamut-policy]`. + /// Fixed seven-byte tagged union. The graph tag determines its payload: + /// + /// - direct: six zero bytes; + /// - Oklch: `[0, Oklab, 0, 0, 0, 0]`; + /// - CSS output: `[source-binding, Oklab, Oklch, number, hue, gamut]`; + /// - CAM16-UCS: `[CAM16, 0, 0, 0, 0, 0]`. const fn canonical_fields(self) -> [u8; 7] { match self { Self::DirectV1 => [0; 7], Self::OklchPolarFromOklabV1(OklabViewReleaseId::Ottosson20210125XyzD65V1) => { [1, 0, 1, 0, 0, 0, 0] } + Self::Cam16UcsRectangularFromCam16V1( + Cam16ViewReleaseId::LiEtAl2017Cie248ForwardV1, + ) => [3, 1, 0, 0, 0, 0, 0], Self::CssColor4OklchD65V1(graph) => [ 2, match graph.modeled_source_binding { @@ -420,6 +444,19 @@ const CAM16_VIEW_DESCRIPTOR_V1: RegisteredReleaseDescriptorV1 = RegisteredReleas dependencies: ReleaseDependencyGraphV1::DirectV1, }; +const CAM16_UCS_VIEW_DESCRIPTOR_V1: RegisteredReleaseDescriptorV1 = RegisteredReleaseDescriptorV1 { + release: RegisteredColorReleaseIdV1::Cam16UcsView(CAM16_UCS_VIEW_RELEASE_V1), + context_requirement: ReleaseContextRequirementV1::ConsumesAppearanceContextV1( + AppearanceContextSchemaReleaseId::Ciecam16ViewingInputsV1, + ), + admitted_frame: RegistryAdmittedFrameV1::Cie1931TwoDegreeXyzIecD65RelativeY1V1, + admitted_domain: RegistryAdmittedDomainV1::FiniteCam16ViewV1, + coordinate_units: RegistryCoordinateUnitsV1::Cam16UcsJPrimeAPrimeBPrimeUnitlessV1, + achromatic_law: RegistryAchromaticLawV1::RectangularChromaOriginExactlyWhenCam16MIsZeroV1, + reference_identity: RegistryReferenceIdentityV1::LiEtAl2017Cam16UcsCoordinatesV1, + dependencies: ReleaseDependencyGraphV1::Cam16UcsRectangularFromCam16V1(CAM16_VIEW_RELEASE_V1), +}; + const OKLAB_VIEW_DESCRIPTOR_V1: RegisteredReleaseDescriptorV1 = RegisteredReleaseDescriptorV1 { release: RegisteredColorReleaseIdV1::OklabView(OKLAB_VIEW_RELEASE_V1), context_requirement: ReleaseContextRequirementV1::NoAppearanceContextConsumptionV1, @@ -503,8 +540,9 @@ impl ReleaseRegistryRecordV1 { // Canonical order is class tag, then release-key bytes. An unavailable class // has exactly one keyless and descriptor-less row. -const RELEASE_REGISTRY_RECORDS_V1: [ReleaseRegistryRecordV1; 5] = [ +const RELEASE_REGISTRY_RECORDS_V1: [ReleaseRegistryRecordV1; 6] = [ ReleaseRegistryRecordV1::Registered(CAM16_VIEW_DESCRIPTOR_V1), + ReleaseRegistryRecordV1::Registered(CAM16_UCS_VIEW_DESCRIPTOR_V1), ReleaseRegistryRecordV1::Registered(OKLAB_VIEW_DESCRIPTOR_V1), ReleaseRegistryRecordV1::Registered(OKLCH_VIEW_DESCRIPTOR_V1), ReleaseRegistryRecordV1::DifferenceCalibrationUnavailable, diff --git a/crates/labcolors-core/src/release_registry_tests.rs b/crates/labcolors-core/src/release_registry_tests.rs index 1f1c40d0..a89329d6 100644 --- a/crates/labcolors-core/src/release_registry_tests.rs +++ b/crates/labcolors-core/src/release_registry_tests.rs @@ -1,6 +1,7 @@ use crate::lcs_occurrence::{ - ADMITTED_SRGB8_TRISTIMULUS_BINDING_V1, AppearanceContextSchemaReleaseId, CAM16_VIEW_RELEASE_V1, - IEC_SRGB_D65_XYZ_FRAME_V1, OKLAB_VIEW_RELEASE_V1, + ADMITTED_SRGB8_TRISTIMULUS_BINDING_V1, AppearanceContextSchemaReleaseId, + CAM16_UCS_VIEW_RELEASE_V1, CAM16_VIEW_RELEASE_V1, IEC_SRGB_D65_XYZ_FRAME_V1, + OKLAB_VIEW_RELEASE_V1, }; use crate::output_projection::{ CSS_COLOR_4_OKLCH_D65_FROM_MODELED_SRGB8_SOLID_V1, CssOklchHueSerializationReleaseIdV1, @@ -26,7 +27,7 @@ fn descriptor(release: RegisteredColorReleaseIdV1) -> RegisteredReleaseDescripto } #[test] -fn registry_contains_only_the_four_implemented_releases() { +fn registry_contains_only_the_five_implemented_releases() { let releases: Vec<_> = release_registry_records_v1() .iter() .filter_map(|record| record.release()) @@ -35,6 +36,7 @@ fn registry_contains_only_the_four_implemented_releases() { releases, [ RegisteredColorReleaseIdV1::Cam16View(CAM16_VIEW_RELEASE_V1), + RegisteredColorReleaseIdV1::Cam16UcsView(CAM16_UCS_VIEW_RELEASE_V1), RegisteredColorReleaseIdV1::OklabView(OKLAB_VIEW_RELEASE_V1), RegisteredColorReleaseIdV1::OklchView(OKLCH_VIEW_RELEASE_V1), RegisteredColorReleaseIdV1::CssColor4OklchD65FromModeledSrgb8Solid( @@ -101,6 +103,44 @@ fn appearance_descriptors_pin_only_code_owned_domain_units_hue_and_reference_fac ); assert_eq!(cam16.dependencies(), ReleaseDependencyGraphV1::DirectV1); + let cam16_ucs = descriptor(RegisteredColorReleaseIdV1::Cam16UcsView( + CAM16_UCS_VIEW_RELEASE_V1, + )); + assert_eq!( + cam16_ucs.context_requirement(), + ReleaseContextRequirementV1::ConsumesAppearanceContextV1( + AppearanceContextSchemaReleaseId::Ciecam16ViewingInputsV1, + ), + ); + assert_eq!( + cam16_ucs.context_requirement().schema_release(), + Some(AppearanceContextSchemaReleaseId::Ciecam16ViewingInputsV1), + ); + assert_eq!( + cam16_ucs.admitted_frame().frame(), + IEC_SRGB_D65_XYZ_FRAME_V1, + ); + assert_eq!( + cam16_ucs.admitted_domain(), + RegistryAdmittedDomainV1::FiniteCam16ViewV1, + ); + assert_eq!( + cam16_ucs.coordinate_units(), + RegistryCoordinateUnitsV1::Cam16UcsJPrimeAPrimeBPrimeUnitlessV1, + ); + assert_eq!( + cam16_ucs.achromatic_law(), + RegistryAchromaticLawV1::RectangularChromaOriginExactlyWhenCam16MIsZeroV1, + ); + assert_eq!( + cam16_ucs.reference_identity(), + RegistryReferenceIdentityV1::LiEtAl2017Cam16UcsCoordinatesV1, + ); + assert_eq!( + cam16_ucs.dependencies(), + ReleaseDependencyGraphV1::Cam16UcsRectangularFromCam16V1(CAM16_VIEW_RELEASE_V1), + ); + let oklab = descriptor(RegisteredColorReleaseIdV1::OklabView(OKLAB_VIEW_RELEASE_V1)); assert_eq!( oklab.context_requirement(), @@ -217,6 +257,7 @@ fn registered_rows_have_stable_exact_release_keys() { keys, [ "cam16-li-et-al-2017-cie-248-forward-v1", + "cam16-ucs-li-et-al-2017-rectangular-v1", "oklab-ottosson-2021-01-25-xyz-d65-v1", "polar-from-ottosson-2021-01-25-oklab-v1", "css-color-4-oklch-d65-from-modeled-iec61966-srgb8-solid-v1", @@ -231,9 +272,11 @@ fn canonical_bytes_pin_schema_rows_descriptors_dependencies_and_order() { release_registry_canonical_bytes_v1(), concat!( "labcolors.release-registry.canonical-binary.v1\0", - "\0\x01\0\x05", + "\0\x01\0\x06", "\x01\x01\0\x26cam16-li-et-al-2017-cie-248-forward-v1", "\x01\x02\x01\x01\x01\x02\x02\x02\0\0\0\0\0\0\0", + "\x01\x01\0\x26cam16-ucs-li-et-al-2017-rectangular-v1", + "\x01\x02\x01\x01\x04\x05\x05\x05\x03\x01\0\0\0\0\0", "\x01\x01\0\x24oklab-ottosson-2021-01-25-xyz-d65-v1", "\x01\x01\0\x01\x01\x01\x01\x01\0\0\0\0\0\0\0", "\x01\x01\0\x27polar-from-ottosson-2021-01-25-oklab-v1", @@ -301,5 +344,5 @@ fn digest_names_its_non_cryptographic_algorithm_and_covers_descriptor_bytes() { digest.value(), crate::fnv1a_32(release_registry_canonical_bytes_v1()), ); - assert_eq!(digest.value(), 1_293_630_307); + assert_eq!(digest.value(), 540_606_852); } diff --git a/scripts/verify_point_support_surplus.py b/scripts/verify_point_support_surplus.py index 1529800f..9e0702ff 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 = ( - "2cf6589a15d2669aca9f1f5a287841805c0fe7293074530d70a1cb803d235c7d" + "4a762a7fd0a658286288ef651cbaf53940e4676f47c63c970799736650a62e21" ) EXPECTED_Q55_PROOF_SHA256 = ( "ac59cf89503170c789223b91d775213a19d4e571ef930f2ea609fcd51b14defd" From 45f7de4fcb28269cbbadb0c3c10f4088311ea286 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Thu, 23 Jul 2026 04:27:23 +0300 Subject: [PATCH 36/58] core: lower authored physical declarations into the canonical Program --- ...rt-reference-surplus-q55-bps-proof-v1.json | 2 +- crates/labcolors-core/src/appearance.rs | 14 +- .../src/generic_boundary_tests.rs | 50 + crates/labcolors-core/src/package_bridge.rs | 1303 ++++++++++++++++- .../src/program_mixed_evaluator_tests.rs | 21 +- crates/labcolors-core/src/program_session.rs | 158 ++ .../tests/package_bridge_red.rs | 512 ++++++- packages/colors/bench/wasm.json | 8 +- scripts/check-wasm-size-budget.mjs | 2 +- scripts/verify_point_support_surplus.py | 2 +- 10 files changed, 2009 insertions(+), 63 deletions(-) mode change 100755 => 100644 scripts/check-wasm-size-budget.mjs 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 6f16630c..9445d79b 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":"5835c6225252df5184803b548ba151654a05db53ae9bc19b2a9be5999a868311","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":"4a762a7fd0a658286288ef651cbaf53940e4676f47c63c970799736650a62e21","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":"e361cf144e0630a4f2ff52261e6515c9394188fc1351c97b98ea9c0ec87ec39d"},{"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":"f546925d6dedf81bcb78ac9f7342ef8ab3ac93e61cfda5016783c3f733c0f9dd"} +{"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"} diff --git a/crates/labcolors-core/src/appearance.rs b/crates/labcolors-core/src/appearance.rs index 95377930..8c635e94 100644 --- a/crates/labcolors-core/src/appearance.rs +++ b/crates/labcolors-core/src/appearance.rs @@ -54,7 +54,6 @@ impl SurfaceInputPortId { } /// Exact transport value. It has identity semantics only. - #[cfg(test)] pub(crate) const fn value(self) -> u32 { self.0 } @@ -68,6 +67,10 @@ impl OpacityInputId { pub(crate) const fn new(raw: u32) -> Self { Self(raw) } + + pub(crate) const fn value(self) -> u32 { + self.0 + } } /// Непрозрачный handle Paint-программы. @@ -78,6 +81,10 @@ impl PaintId { pub(crate) const fn new(raw: u32) -> Self { Self(raw) } + + pub(crate) const fn value(self) -> u32 { + self.0 + } } /// Непрозрачный handle наблюдаемой поверхности. @@ -88,6 +95,10 @@ impl SurfaceId { pub(crate) const fn new(raw: u32) -> Self { Self(raw) } + + pub(crate) const fn value(self) -> u32 { + self.0 + } } /// Непрозрачный handle применения Paint к Surface. @@ -101,7 +112,6 @@ impl OccurrenceId { } /// Exact transport value. It has identity semantics only. - #[cfg(test)] pub(crate) const fn value(self) -> u32 { self.0 } diff --git a/crates/labcolors-core/src/generic_boundary_tests.rs b/crates/labcolors-core/src/generic_boundary_tests.rs index 33956b55..53457a93 100644 --- a/crates/labcolors-core/src/generic_boundary_tests.rs +++ b/crates/labcolors-core/src/generic_boundary_tests.rs @@ -6,6 +6,7 @@ const LIB_SOURCE: &str = include_str!("lib.rs"); const LCS_OCCURRENCE_SOURCE: &str = include_str!("lcs_occurrence.rs"); 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_SESSION_SOURCE: &str = include_str!("program_session.rs"); const SESSION_SOURCE: &str = include_str!("session.rs"); @@ -75,6 +76,55 @@ fn generic_physical_and_transport_modules_contain_no_client_or_legacy_vocabulary } } +#[test] +fn package_authoring_is_one_thin_concrete_core_draft_without_a_second_graph() { + assert_eq!( + normalized_source_scope( + PACKAGE_BRIDGE_SOURCE, + "pub struct PackageProgramDraftV1 {", + "/// Draft mutation rejected", + ), + "pub struct PackageProgramDraftV1 { inner: CoreProgramDraftV1, }", + "the package seam must forward actual IR nodes into the sole Core draft", + ); + assert_eq!( + normalized_source_scope( + PROGRAM_SESSION_SOURCE, + "pub(crate) struct CoreProgramDraftV1 {", + "#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub(crate) enum CoreProgramDraftErrorV1", + ), + "pub(crate) struct CoreProgramDraftV1 { program: CoreProgramV1, }", + "the mutable Core seam must own the actual concrete Program, not mirror its fields", + ); + for forbidden in [ + "PairFill", + "PairLabel", + "Glow", + "Material", + "Ladder", + "AlphaAnalog", + "ThemeConfig", + "RoleRecipe", + "serde", + "serde_json", + "String", + "HashMap", + "dyn Program", + "OutputProfileId", + "PackageProgramObservationGroupIdV1", + "PackageProgramOpacityIdV1", + "PackageProgramSurfaceInputIdV1", + "pub fn push_opacity(", + "pub fn push_surface_input(", + "pub fn surface_input_slots(", + ] { + assert!( + !PACKAGE_BRIDGE_SOURCE.contains(forbidden), + "the concrete package lowerer must not acquire `{forbidden}`", + ); + } +} + #[test] fn shared_observation_ssot_has_one_backing_without_lifecycle_or_adapter_facades() { assert_eq!( diff --git a/crates/labcolors-core/src/package_bridge.rs b/crates/labcolors-core/src/package_bridge.rs index e291fd3d..16666bba 100644 --- a/crates/labcolors-core/src/package_bridge.rs +++ b/crates/labcolors-core/src/package_bridge.rs @@ -1,25 +1,38 @@ -//! Sole concrete package projection for a compiled Core Program Session. +//! Sole concrete package authoring and runtime seam for a Core Program. //! -//! This hidden module is deliberately narrower than the authored Program IR: -//! it exposes no evaluator trait, generic Session plan, threshold, candidate -//! domain, client vocabulary, transport word, or lifecycle generation. The -//! package adapter supplies schema-ordered physical scenarios and receives a -//! borrowed, allocation-free projection of Core-owned state and evidence. +//! The cold path appends typed physical declarations directly to the real Core +//! Program IR and compiles it with the code-owned evaluator union. The hot path +//! supplies schema-ordered physical scenarios and receives a borrowed, +//! allocation-free projection of Core-owned state and evidence. Neither path +//! exposes evaluator traits, generic Session plans, client vocabulary, +//! transport words, strings, or lifecycle generations. use core::iter::FusedIterator; use core::slice; use crate::Srgb8; +use crate::appearance::{OccurrenceId, OpacityInputId, PaintId, SurfaceId, SurfaceInputPortId}; +use crate::joint::FiniteJointOrderErrorV1; +use crate::lcs_occurrence::{ + AdaptingLuminanceCdM2, AppearanceContextDomainErrorV1, AppearanceContextFieldV1, + AppearanceContextId, AppearanceContextSchemaReleaseId, BackgroundLuminanceRatio, ColorSignal, + IEC_SRGB_D65_XYZ_FRAME_V1, NumericDomainError, SurroundProfileId, +}; use crate::observation::{ ObservationError, ObservationPayloadInput, ObservationStreamId, ObservationUpdateInput, Revision, ScenarioId, SchemaOrderedScenarioSourceV1, UnknownReasonId, }; use crate::program_session::{ - CompiledCoreProgramV1, CoreProgramEvaluatorErrorV1, CoreProgramEvaluatorsV1, ProgramConflictV1, - ProgramOutputV1, ProgramSessionEvaluationError, ProgramSessionInstantiateError, - ProgramSessionPlan, ProgramVerifiedV1, + CompiledCoreProgramV1, CompositionProfile, ConstraintId, ConstraintInvocation, + CoreProgramConstraintInvocationV1, CoreProgramDraftErrorV1, CoreProgramDraftV1, + CoreProgramEvaluatorErrorV1, CoreProgramEvaluatorsV1, DeclaredJointSelectionV1, + JointCandidateStateV1, Occurrence, OpacityInput, OutputBinding, OutputSlotId, Paint, + ProgramCompileError, ProgramConflictV1, ProgramOutputV1, ProgramSessionEvaluationError, + ProgramSessionInstantiateError, ProgramSessionPlan, ProgramVerifiedV1, Source, SourceId, + Surface, Target, TargetCandidateChoiceV1, TargetCandidateId, TargetCandidateV1, TargetId, }; use crate::session::{Session, SessionState, SessionUpdateError}; +use crate::wcag22::Wcag22CriterionV1; type CoreVerifiedV1 = ProgramVerifiedV1; type CoreConflictV1 = ProgramConflictV1; @@ -28,6 +41,903 @@ type CoreProgramSessionV1 = Session; type CoreProgramStateV1 = SessionState; type CoreProgramPlanErrorV1 = ProgramSessionEvaluationError; +macro_rules! package_program_id { + ($name:ident, $core:ty) => { + #[repr(transparent)] + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] + #[must_use] + pub struct $name($core); + + impl $name { + pub const fn new(value: u32) -> Self { + Self(<$core>::new(value)) + } + + pub const fn value(self) -> u32 { + self.0.value() + } + + const fn from_core(value: $core) -> Self { + Self(value) + } + + const fn into_core(self) -> $core { + self.0 + } + } + + impl core::hash::Hash for $name { + fn hash(&self, state: &mut H) { + core::hash::Hash::hash(&self.value(), state); + } + } + }; +} + +package_program_id!(PackageProgramSourceIdV1, SourceId); +package_program_id!(PackageProgramTargetIdV1, TargetId); +package_program_id!(PackageProgramTargetCandidateIdV1, TargetCandidateId); +package_program_id!(PackageProgramOpacityInputIdV1, OpacityInputId); +package_program_id!(PackageProgramPaintIdV1, PaintId); +package_program_id!(PackageProgramSurfaceInputPortIdV1, SurfaceInputPortId); +package_program_id!(PackageProgramSurfaceIdV1, SurfaceId); +package_program_id!(PackageProgramOccurrenceIdV1, OccurrenceId); +package_program_id!(PackageProgramConstraintIdV1, ConstraintId); +package_program_id!(PackageProgramOutputSlotIdV1, OutputSlotId); + +/// One finite candidate, stored as the actual Core target-candidate IR node. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PackageProgramTargetCandidateV1(TargetCandidateV1); + +impl PackageProgramTargetCandidateV1 { + pub const fn new(id: PackageProgramTargetCandidateIdV1, source: Srgb8) -> Self { + Self(TargetCandidateV1::from_srgb8(id.into_core(), source)) + } +} + +/// One typed target/candidate choice stored as the actual Core joint IR node. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PackageProgramJointChoiceV1(TargetCandidateChoiceV1); + +impl PackageProgramJointChoiceV1 { + pub const fn new( + target: PackageProgramTargetIdV1, + candidate: PackageProgramTargetCandidateIdV1, + ) -> Self { + Self(TargetCandidateChoiceV1::new( + target.into_core(), + candidate.into_core(), + )) + } +} + +/// One complete explicit state in the finite joint order. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PackageProgramJointStateV1(JointCandidateStateV1); + +impl PackageProgramJointStateV1 { + pub fn new(choices: Vec) -> Self { + Self(JointCandidateStateV1::new( + choices.into_iter().map(|choice| choice.0).collect(), + )) + } +} + +/// Registered surround input for the current CIECAM16 context release. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PackageProgramSurroundV1 { + Average, + Dim, + Dark, +} + +impl PackageProgramSurroundV1 { + const fn into_core(self) -> SurroundProfileId { + match self { + Self::Average => SurroundProfileId::AverageV1, + Self::Dim => SurroundProfileId::DimV1, + Self::Dark => SurroundProfileId::DarkV1, + } + } +} + +/// Exact semantic input field rejected while forming an appearance context. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PackageProgramAppearanceContextFieldV1 { + AdaptingLuminanceCdM2, + BackgroundLuminanceRatioYbYw, +} + +/// Exact numeric reason rejected while forming an appearance context. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PackageProgramNumericDomainErrorV1 { + NonFinite, + Negative, + NotPositive, + AboveOne, +} + +/// Closed appearance-context admission failure. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PackageProgramAppearanceContextErrorKindV1 { + Domain, + InternalInvariant, +} + +/// Closed appearance-context admission failure. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PackageProgramAppearanceContextErrorV1 { + kind: PackageProgramAppearanceContextErrorKindV1, + field: Option, + reason: Option, +} + +impl PackageProgramAppearanceContextErrorV1 { + pub const fn kind(self) -> PackageProgramAppearanceContextErrorKindV1 { + self.kind + } + + pub const fn field(self) -> Option { + self.field + } + + pub const fn reason(self) -> Option { + self.reason + } + + fn from_core(error: AppearanceContextDomainErrorV1) -> Self { + let field = match error.field() { + AppearanceContextFieldV1::AdaptingLuminanceCdM2 => { + PackageProgramAppearanceContextFieldV1::AdaptingLuminanceCdM2 + } + AppearanceContextFieldV1::BackgroundLuminanceRatio => { + PackageProgramAppearanceContextFieldV1::BackgroundLuminanceRatioYbYw + } + }; + let reason = match error.reason() { + NumericDomainError::NonFinite => Some(PackageProgramNumericDomainErrorV1::NonFinite), + NumericDomainError::Negative => Some(PackageProgramNumericDomainErrorV1::Negative), + NumericDomainError::NotPositive => { + Some(PackageProgramNumericDomainErrorV1::NotPositive) + } + NumericDomainError::AboveOne => Some(PackageProgramNumericDomainErrorV1::AboveOne), + NumericDomainError::HueOutOfRange => None, + }; + match reason { + Some(reason) => Self { + kind: PackageProgramAppearanceContextErrorKindV1::Domain, + field: Some(field), + reason: Some(reason), + }, + None => Self { + kind: PackageProgramAppearanceContextErrorKindV1::InternalInvariant, + field: None, + reason: None, + }, + } + } +} + +/// Immutable admitted appearance context stored as the actual Core value. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct PackageProgramAppearanceContextV1(AppearanceContextId); + +impl PackageProgramAppearanceContextV1 { + /// Admit the explicit CIECAM16 viewing inputs for encoded sRGB8/D65. + /// `background_luminance_ratio_yb_yw` is the dimensionless ratio `Y_b/Y_w` + /// and must be finite in `(0, 1]`; it is not an absolute luminance. + pub fn try_new( + adapting_luminance_cd_m2: f64, + background_luminance_ratio_yb_yw: f64, + surround: PackageProgramSurroundV1, + ) -> Result { + let adapting_luminance_cd_m2 = AdaptingLuminanceCdM2::try_new(adapting_luminance_cd_m2) + .map_err(PackageProgramAppearanceContextErrorV1::from_core)?; + let background_luminance_ratio = + BackgroundLuminanceRatio::try_new(background_luminance_ratio_yb_yw) + .map_err(PackageProgramAppearanceContextErrorV1::from_core)?; + Ok(Self(AppearanceContextId::from_inputs( + AppearanceContextSchemaReleaseId::Ciecam16ViewingInputsV1, + IEC_SRGB_D65_XYZ_FRAME_V1, + adapting_luminance_cd_m2, + background_luminance_ratio, + surround.into_core(), + ))) + } +} + +/// Closed compile classification; the generic Core error never escapes. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PackageProgramCompileErrorKindV1 { + DuplicateSource, + DuplicateTarget, + DuplicateTargetCandidate, + DuplicateTargetCandidateSignal, + DuplicateOpacityInput, + DuplicateSurfaceInputPort, + UnusedSurfaceInputPort, + DuplicateSurfaceInputBinding, + DuplicatePaint, + DuplicateSurface, + DuplicateOccurrence, + DuplicateConstraint, + DuplicateOutputSlot, + MissingTargetSource, + MissingPaintTarget, + MissingPaintSource, + MissingPaintOpacityInput, + MissingSurfaceInputPort, + MissingSurfaceOccurrence, + MissingOccurrencePaint, + MissingOccurrenceBackdrop, + MissingConstraintOccurrence, + MissingOutputPaint, + PaintCycle, + RenderCycle, + OpacityOutOfDomain, + EmptyTargetDomain, + UnconstrainedTarget, + DisconnectedFiniteTargets, + UnassessedOutput, + MissingJointSelection, + JointSelectionWithoutTargets, + JointStateDuplicateTarget, + JointStateMissingTarget, + JointStateUnknownTarget, + JointStateUnknownCandidate, + InvalidJointOrder, + EmptySurfaceInputPortSet, + EmptyOccurrenceSet, + EmptyConstraintSet, + EmptyOutputSet, + ResourceExhausted, + InternalInvariant, +} + +/// Typed offending identity when one error has a single attributable handle. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PackageProgramCompileErrorHandleV1 { + Source(PackageProgramSourceIdV1), + Target(PackageProgramTargetIdV1), + TargetCandidate(PackageProgramTargetCandidateIdV1), + OpacityInput(PackageProgramOpacityInputIdV1), + Paint(PackageProgramPaintIdV1), + SurfaceInputPort(PackageProgramSurfaceInputPortIdV1), + Surface(PackageProgramSurfaceIdV1), + Occurrence(PackageProgramOccurrenceIdV1), + Constraint(PackageProgramConstraintIdV1), + OutputSlot(PackageProgramOutputSlotIdV1), +} + +impl PackageProgramCompileErrorHandleV1 { + pub const fn value(self) -> u32 { + match self { + Self::Source(value) => value.value(), + Self::Target(value) => value.value(), + Self::TargetCandidate(value) => value.value(), + Self::OpacityInput(value) => value.value(), + Self::Paint(value) => value.value(), + Self::SurfaceInputPort(value) => value.value(), + Self::Surface(value) => value.value(), + Self::Occurrence(value) => value.value(), + Self::Constraint(value) => value.value(), + Self::OutputSlot(value) => value.value(), + } + } +} + +/// Exact owned members of one paint dependency cycle. +/// +/// The wrapper takes ownership of Core's existing allocation. Projecting a +/// compile failure therefore cannot introduce a second infallible allocation. +#[derive(Debug, PartialEq, Eq)] +pub struct PackageProgramPaintCycleV1 { + paints: Vec, +} + +impl PackageProgramPaintCycleV1 { + pub fn paints(&self) -> impl ExactSizeIterator + '_ { + self.paints + .iter() + .copied() + .map(PackageProgramPaintIdV1::from_core) + } +} + +/// Exact owned members of one render dependency cycle. +/// +/// Surface and occurrence identities remain separate physical namespaces. +#[derive(Debug, PartialEq, Eq)] +pub struct PackageProgramRenderCycleV1 { + surfaces: Vec, + occurrences: Vec, +} + +impl PackageProgramRenderCycleV1 { + pub fn surfaces(&self) -> impl ExactSizeIterator + '_ { + self.surfaces + .iter() + .copied() + .map(PackageProgramSurfaceIdV1::from_core) + } + + pub fn occurrences(&self) -> impl ExactSizeIterator + '_ { + self.occurrences + .iter() + .copied() + .map(PackageProgramOccurrenceIdV1::from_core) + } +} + +/// Exact closed reason why an explicit finite joint order was rejected. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PackageProgramJointOrderErrorV1 { + EmptyDomain { + dimension: usize, + }, + CardinalityOverflow, + EmptyOrder, + TupleArity { + state: usize, + expected: usize, + actual: usize, + }, + OrdinalOutOfDomain { + state: usize, + dimension: usize, + ordinal: usize, + domain_len: usize, + }, + DuplicateTuple { + first_state: usize, + duplicate_state: usize, + }, + IncompleteOrder { + expected: usize, + actual: usize, + }, + ResourceExhausted, +} + +/// Atomic, lossless authored-program compile failure. +/// +/// This public enum is authoritative. `kind`, `primary_handle`, and +/// `related_handle` are convenience projections only; they never replace the +/// complete typed payload carried by the matching variant. +#[derive(Debug, PartialEq, Eq)] +pub enum PackageProgramCompileErrorV1 { + DuplicateSource { + source: PackageProgramSourceIdV1, + }, + DuplicateTarget { + target: PackageProgramTargetIdV1, + }, + MissingTargetSource { + target: PackageProgramTargetIdV1, + source: PackageProgramSourceIdV1, + }, + DuplicateOpacityInput { + input: PackageProgramOpacityInputIdV1, + }, + DuplicateSurfaceInputPort { + input: PackageProgramSurfaceInputPortIdV1, + }, + UnusedSurfaceInputPort { + input: PackageProgramSurfaceInputPortIdV1, + }, + DuplicateSurfaceInputBinding { + input: PackageProgramSurfaceInputPortIdV1, + first: PackageProgramSurfaceIdV1, + duplicate: PackageProgramSurfaceIdV1, + }, + DuplicatePaint { + paint: PackageProgramPaintIdV1, + }, + DuplicateSurface { + surface: PackageProgramSurfaceIdV1, + }, + DuplicateOccurrence { + occurrence: PackageProgramOccurrenceIdV1, + }, + MissingPaintTarget { + paint: PackageProgramPaintIdV1, + target: PackageProgramTargetIdV1, + }, + MissingPaintSource { + paint: PackageProgramPaintIdV1, + source: PackageProgramPaintIdV1, + }, + MissingPaintOpacityInput { + paint: PackageProgramPaintIdV1, + input: PackageProgramOpacityInputIdV1, + }, + MissingSurfaceInputPort { + surface: PackageProgramSurfaceIdV1, + input: PackageProgramSurfaceInputPortIdV1, + }, + MissingSurfaceOccurrence { + surface: PackageProgramSurfaceIdV1, + occurrence: PackageProgramOccurrenceIdV1, + }, + MissingOccurrencePaint { + occurrence: PackageProgramOccurrenceIdV1, + paint: PackageProgramPaintIdV1, + }, + MissingOccurrenceBackdrop { + occurrence: PackageProgramOccurrenceIdV1, + surface: PackageProgramSurfaceIdV1, + }, + PaintCycle(PackageProgramPaintCycleV1), + RenderCycle(PackageProgramRenderCycleV1), + OpacityOutOfDomain { + input: PackageProgramOpacityInputIdV1, + }, + EmptyTargetDomain { + target: PackageProgramTargetIdV1, + }, + DuplicateTargetCandidate { + target: PackageProgramTargetIdV1, + candidate: PackageProgramTargetCandidateIdV1, + }, + DuplicateTargetCandidateSignal { + target: PackageProgramTargetIdV1, + first: PackageProgramTargetCandidateIdV1, + duplicate: PackageProgramTargetCandidateIdV1, + encoded_srgb8: Srgb8, + }, + UnconstrainedTarget { + target: PackageProgramTargetIdV1, + }, + DisconnectedFiniteTargets, + UnassessedOutput { + output: PackageProgramOutputSlotIdV1, + paint: PackageProgramPaintIdV1, + }, + MissingJointSelection, + JointSelectionWithoutTargets, + JointStateDuplicateTarget { + state: usize, + target: PackageProgramTargetIdV1, + }, + JointStateMissingTarget { + state: usize, + target: PackageProgramTargetIdV1, + }, + JointStateUnknownTarget { + state: usize, + target: PackageProgramTargetIdV1, + }, + JointStateUnknownCandidate { + state: usize, + target: PackageProgramTargetIdV1, + candidate: PackageProgramTargetCandidateIdV1, + }, + InvalidJointOrder(PackageProgramJointOrderErrorV1), + /// The package contract has one code-owned atomic observation group; + /// authored input ports are its complete, canonical membership. + EmptySurfaceInputPortSet, + EmptyOccurrenceSet, + EmptyConstraintSet, + EmptyOutputSet, + DuplicateConstraint { + constraint: PackageProgramConstraintIdV1, + }, + MissingConstraintOccurrence { + constraint: PackageProgramConstraintIdV1, + occurrence: PackageProgramOccurrenceIdV1, + }, + DuplicateOutputSlot { + output: PackageProgramOutputSlotIdV1, + }, + MissingOutputPaint { + output: PackageProgramOutputSlotIdV1, + paint: PackageProgramPaintIdV1, + }, + ResourceExhausted, + InternalInvariant, +} + +impl PackageProgramCompileErrorV1 { + pub const fn kind(&self) -> PackageProgramCompileErrorKindV1 { + use PackageProgramCompileErrorKindV1 as Kind; + + match self { + Self::DuplicateSource { .. } => Kind::DuplicateSource, + Self::DuplicateTarget { .. } => Kind::DuplicateTarget, + Self::MissingTargetSource { .. } => Kind::MissingTargetSource, + Self::DuplicateOpacityInput { .. } => Kind::DuplicateOpacityInput, + Self::DuplicateSurfaceInputPort { .. } => Kind::DuplicateSurfaceInputPort, + Self::UnusedSurfaceInputPort { .. } => Kind::UnusedSurfaceInputPort, + Self::DuplicateSurfaceInputBinding { .. } => Kind::DuplicateSurfaceInputBinding, + Self::DuplicatePaint { .. } => Kind::DuplicatePaint, + Self::DuplicateSurface { .. } => Kind::DuplicateSurface, + Self::DuplicateOccurrence { .. } => Kind::DuplicateOccurrence, + Self::MissingPaintTarget { .. } => Kind::MissingPaintTarget, + Self::MissingPaintSource { .. } => Kind::MissingPaintSource, + Self::MissingPaintOpacityInput { .. } => Kind::MissingPaintOpacityInput, + Self::MissingSurfaceInputPort { .. } => Kind::MissingSurfaceInputPort, + Self::MissingSurfaceOccurrence { .. } => Kind::MissingSurfaceOccurrence, + Self::MissingOccurrencePaint { .. } => Kind::MissingOccurrencePaint, + Self::MissingOccurrenceBackdrop { .. } => Kind::MissingOccurrenceBackdrop, + Self::PaintCycle(_) => Kind::PaintCycle, + Self::RenderCycle(_) => Kind::RenderCycle, + Self::OpacityOutOfDomain { .. } => Kind::OpacityOutOfDomain, + Self::EmptyTargetDomain { .. } => Kind::EmptyTargetDomain, + Self::DuplicateTargetCandidate { .. } => Kind::DuplicateTargetCandidate, + Self::DuplicateTargetCandidateSignal { .. } => Kind::DuplicateTargetCandidateSignal, + Self::UnconstrainedTarget { .. } => Kind::UnconstrainedTarget, + Self::DisconnectedFiniteTargets => Kind::DisconnectedFiniteTargets, + Self::UnassessedOutput { .. } => Kind::UnassessedOutput, + Self::MissingJointSelection => Kind::MissingJointSelection, + Self::JointSelectionWithoutTargets => Kind::JointSelectionWithoutTargets, + Self::JointStateDuplicateTarget { .. } => Kind::JointStateDuplicateTarget, + Self::JointStateMissingTarget { .. } => Kind::JointStateMissingTarget, + Self::JointStateUnknownTarget { .. } => Kind::JointStateUnknownTarget, + Self::JointStateUnknownCandidate { .. } => Kind::JointStateUnknownCandidate, + Self::InvalidJointOrder(_) => Kind::InvalidJointOrder, + Self::EmptySurfaceInputPortSet => Kind::EmptySurfaceInputPortSet, + Self::EmptyOccurrenceSet => Kind::EmptyOccurrenceSet, + Self::EmptyConstraintSet => Kind::EmptyConstraintSet, + Self::EmptyOutputSet => Kind::EmptyOutputSet, + Self::DuplicateConstraint { .. } => Kind::DuplicateConstraint, + Self::MissingConstraintOccurrence { .. } => Kind::MissingConstraintOccurrence, + Self::DuplicateOutputSlot { .. } => Kind::DuplicateOutputSlot, + Self::MissingOutputPaint { .. } => Kind::MissingOutputPaint, + Self::ResourceExhausted => Kind::ResourceExhausted, + Self::InternalInvariant => Kind::InternalInvariant, + } + } + + pub const fn primary_handle(&self) -> Option { + use PackageProgramCompileErrorHandleV1 as Handle; + + match self { + Self::DuplicateSource { source } => Some(Handle::Source(*source)), + Self::DuplicateTarget { target } + | Self::EmptyTargetDomain { target } + | Self::UnconstrainedTarget { target } + | Self::JointStateDuplicateTarget { target, .. } + | Self::JointStateMissingTarget { target, .. } + | Self::JointStateUnknownTarget { target, .. } + | Self::JointStateUnknownCandidate { target, .. } => Some(Handle::Target(*target)), + Self::MissingTargetSource { target, .. } + | Self::DuplicateTargetCandidate { target, .. } + | Self::DuplicateTargetCandidateSignal { target, .. } => Some(Handle::Target(*target)), + Self::DuplicateOpacityInput { input } | Self::OpacityOutOfDomain { input } => { + Some(Handle::OpacityInput(*input)) + } + Self::DuplicateSurfaceInputPort { input } + | Self::UnusedSurfaceInputPort { input } + | Self::DuplicateSurfaceInputBinding { input, .. } => { + Some(Handle::SurfaceInputPort(*input)) + } + Self::DuplicatePaint { paint } + | Self::MissingPaintTarget { paint, .. } + | Self::MissingPaintSource { paint, .. } + | Self::MissingPaintOpacityInput { paint, .. } => Some(Handle::Paint(*paint)), + Self::DuplicateSurface { surface } + | Self::MissingSurfaceInputPort { surface, .. } + | Self::MissingSurfaceOccurrence { surface, .. } => Some(Handle::Surface(*surface)), + Self::DuplicateOccurrence { occurrence } + | Self::MissingOccurrencePaint { occurrence, .. } + | Self::MissingOccurrenceBackdrop { occurrence, .. } => { + Some(Handle::Occurrence(*occurrence)) + } + Self::UnassessedOutput { output, .. } + | Self::DuplicateOutputSlot { output } + | Self::MissingOutputPaint { output, .. } => Some(Handle::OutputSlot(*output)), + Self::DuplicateConstraint { constraint } + | Self::MissingConstraintOccurrence { constraint, .. } => { + Some(Handle::Constraint(*constraint)) + } + Self::PaintCycle(_) + | Self::RenderCycle(_) + | Self::DisconnectedFiniteTargets + | Self::MissingJointSelection + | Self::JointSelectionWithoutTargets + | Self::InvalidJointOrder(_) + | Self::EmptySurfaceInputPortSet + | Self::EmptyOccurrenceSet + | Self::EmptyConstraintSet + | Self::EmptyOutputSet + | Self::ResourceExhausted + | Self::InternalInvariant => None, + } + } + + pub const fn related_handle(&self) -> Option { + use PackageProgramCompileErrorHandleV1 as Handle; + + match self { + Self::MissingTargetSource { source, .. } => Some(Handle::Source(*source)), + Self::DuplicateSurfaceInputBinding { duplicate, .. } => { + Some(Handle::Surface(*duplicate)) + } + Self::MissingPaintTarget { target, .. } => Some(Handle::Target(*target)), + Self::MissingPaintSource { source, .. } => Some(Handle::Paint(*source)), + Self::MissingPaintOpacityInput { input, .. } => Some(Handle::OpacityInput(*input)), + Self::MissingSurfaceInputPort { input, .. } => Some(Handle::SurfaceInputPort(*input)), + Self::MissingSurfaceOccurrence { occurrence, .. } + | Self::MissingConstraintOccurrence { occurrence, .. } => { + Some(Handle::Occurrence(*occurrence)) + } + Self::MissingOccurrencePaint { paint, .. } + | Self::UnassessedOutput { paint, .. } + | Self::MissingOutputPaint { paint, .. } => Some(Handle::Paint(*paint)), + Self::MissingOccurrenceBackdrop { surface, .. } => Some(Handle::Surface(*surface)), + Self::DuplicateTargetCandidate { candidate, .. } + | Self::DuplicateTargetCandidateSignal { + duplicate: candidate, + .. + } + | Self::JointStateUnknownCandidate { candidate, .. } => { + Some(Handle::TargetCandidate(*candidate)) + } + Self::DuplicateSource { .. } + | Self::DuplicateTarget { .. } + | Self::DuplicateOpacityInput { .. } + | Self::DuplicateSurfaceInputPort { .. } + | Self::UnusedSurfaceInputPort { .. } + | Self::DuplicatePaint { .. } + | Self::DuplicateSurface { .. } + | Self::DuplicateOccurrence { .. } + | Self::PaintCycle(_) + | Self::RenderCycle(_) + | Self::OpacityOutOfDomain { .. } + | Self::EmptyTargetDomain { .. } + | Self::UnconstrainedTarget { .. } + | Self::DisconnectedFiniteTargets + | Self::MissingJointSelection + | Self::JointSelectionWithoutTargets + | Self::JointStateDuplicateTarget { .. } + | Self::JointStateMissingTarget { .. } + | Self::JointStateUnknownTarget { .. } + | Self::InvalidJointOrder(_) + | Self::EmptySurfaceInputPortSet + | Self::EmptyOccurrenceSet + | Self::EmptyConstraintSet + | Self::EmptyOutputSet + | Self::DuplicateConstraint { .. } + | Self::DuplicateOutputSlot { .. } + | Self::ResourceExhausted + | Self::InternalInvariant => None, + } + } +} + +/// Concrete cold-path builder over the actual Core Program IR. +#[must_use] +pub struct PackageProgramDraftV1 { + inner: CoreProgramDraftV1, +} + +/// Draft mutation rejected before Core compilation. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PackageProgramDraftErrorV1 { + JointSelectionAlreadyDeclared, +} + +impl PackageProgramDraftV1 { + pub fn new() -> Self { + Self { + inner: CoreProgramDraftV1::new(), + } + } + + pub fn push_source(&mut self, id: PackageProgramSourceIdV1, source: Srgb8) -> &mut Self { + self.inner + .push_source(Source::new(id.into_core(), ColorSignal::from_srgb8(source))); + self + } + + pub fn push_fixed_target( + &mut self, + id: PackageProgramTargetIdV1, + source: PackageProgramSourceIdV1, + ) -> &mut Self { + self.inner + .push_target(Target::fixed(id.into_core(), source.into_core())); + self + } + + pub fn push_finite_target( + &mut self, + id: PackageProgramTargetIdV1, + source: PackageProgramSourceIdV1, + candidates: Vec, + ) -> &mut Self { + self.inner.push_target(Target::finite( + id.into_core(), + source.into_core(), + candidates + .into_iter() + .map(|candidate| candidate.0) + .collect(), + )); + self + } + + pub fn set_joint_selection( + &mut self, + states: Vec, + ) -> Result<&mut Self, PackageProgramDraftErrorV1> { + self.inner + .set_joint_selection(DeclaredJointSelectionV1::new( + states.into_iter().map(|state| state.0).collect(), + )) + .map_err(|error| match error { + CoreProgramDraftErrorV1::JointSelectionAlreadyDeclared => { + PackageProgramDraftErrorV1::JointSelectionAlreadyDeclared + } + })?; + Ok(self) + } + + pub fn push_surface_input_port( + &mut self, + input: PackageProgramSurfaceInputPortIdV1, + ) -> &mut Self { + self.inner.push_surface_input_port(input.into_core()); + self + } + + pub fn push_opacity_input( + &mut self, + id: PackageProgramOpacityInputIdV1, + value: f64, + ) -> &mut Self { + self.inner + .push_opacity_input(OpacityInput::new(id.into_core(), value)); + self + } + + pub fn push_solid_paint( + &mut self, + id: PackageProgramPaintIdV1, + target: PackageProgramTargetIdV1, + ) -> &mut Self { + self.inner.push_paint(Paint::Solid { + id: id.into_core(), + target: target.into_core(), + }); + self + } + + pub fn push_opacity_paint( + &mut self, + id: PackageProgramPaintIdV1, + source: PackageProgramPaintIdV1, + opacity: PackageProgramOpacityInputIdV1, + ) -> &mut Self { + self.inner.push_paint(Paint::Opacity { + id: id.into_core(), + source: source.into_core(), + opacity: opacity.into_core(), + }); + self + } + + pub fn push_input_surface( + &mut self, + id: PackageProgramSurfaceIdV1, + input: PackageProgramSurfaceInputPortIdV1, + ) -> &mut Self { + self.inner.push_surface(Surface::Input { + id: id.into_core(), + input: input.into_core(), + }); + self + } + + pub fn push_occurrence_surface( + &mut self, + id: PackageProgramSurfaceIdV1, + occurrence: PackageProgramOccurrenceIdV1, + ) -> &mut Self { + self.inner.push_surface(Surface::FromOccurrence { + id: id.into_core(), + occurrence: occurrence.into_core(), + }); + self + } + + pub fn push_source_over_occurrence( + &mut self, + id: PackageProgramOccurrenceIdV1, + subject: PackageProgramPaintIdV1, + against: PackageProgramSurfaceIdV1, + context: PackageProgramAppearanceContextV1, + ) -> &mut Self { + self.inner.push_occurrence(Occurrence::new( + id.into_core(), + subject.into_core(), + against.into_core(), + CompositionProfile::EncodedSrgb8SourceOverV1, + context.0, + )); + self + } + + pub fn push_exact_hard( + &mut self, + id: PackageProgramConstraintIdV1, + occurrence: PackageProgramOccurrenceIdV1, + expected: Srgb8, + ) -> &mut Self { + self.inner.push_hard_constraint(ConstraintInvocation::hard( + id.into_core(), + occurrence.into_core(), + CoreProgramConstraintInvocationV1::ExactSrgb8(expected), + )); + self + } + + pub fn push_exact_report_only( + &mut self, + id: PackageProgramConstraintIdV1, + occurrence: PackageProgramOccurrenceIdV1, + expected: Srgb8, + ) -> &mut Self { + self.inner + .push_report_constraint(ConstraintInvocation::report_only( + id.into_core(), + occurrence.into_core(), + CoreProgramConstraintInvocationV1::ExactSrgb8(expected), + )); + self + } + + pub fn push_wcag22_hard( + &mut self, + id: PackageProgramConstraintIdV1, + occurrence: PackageProgramOccurrenceIdV1, + criterion: Wcag22CriterionV1, + ) -> &mut Self { + self.inner.push_hard_constraint(ConstraintInvocation::hard( + id.into_core(), + occurrence.into_core(), + CoreProgramConstraintInvocationV1::Wcag22Srgb8(criterion), + )); + self + } + + pub fn push_wcag22_report_only( + &mut self, + id: PackageProgramConstraintIdV1, + occurrence: PackageProgramOccurrenceIdV1, + criterion: Wcag22CriterionV1, + ) -> &mut Self { + self.inner + .push_report_constraint(ConstraintInvocation::report_only( + id.into_core(), + occurrence.into_core(), + CoreProgramConstraintInvocationV1::Wcag22Srgb8(criterion), + )); + self + } + + pub fn push_output( + &mut self, + output: PackageProgramOutputSlotIdV1, + paint: PackageProgramPaintIdV1, + ) -> &mut Self { + self.inner + .push_output(OutputBinding::new(output.into_core(), paint.into_core())); + self + } + + pub fn compile(self) -> Result { + let compiled = self.inner.compile().map_err(map_program_compile_error)?; + Ok(PackageProgramOwnerV1::from_compiled(compiled)) + } +} + +impl Default for PackageProgramDraftV1 { + fn default() -> Self { + Self::new() + } +} + /// Opaque strong owner of one exact compiled Core Program. /// /// Sessions instantiated from this owner are independently mutable. In the @@ -39,27 +949,32 @@ pub struct PackageProgramOwnerV1 { } impl PackageProgramOwnerV1 { - /// Internal handoff from the canonical Core lowerer. Keeping this - /// constructor crate-private prevents an adapter-authored Program dialect. - #[cfg_attr( - not(test), - expect( - dead_code, - reason = "the canonical package lowerer is linked in the following stacked slice" - ) - )] + /// Internal handoff from the canonical concrete Core draft compiler. pub(crate) const fn from_compiled(compiled: CompiledCoreProgramV1) -> Self { Self { compiled } } /// Number of schema-ordered surface values required in every scenario. - pub fn surface_input_count(&self) -> usize { + pub fn surface_input_port_count(&self) -> usize { self.compiled.surface_input_ports().len() } + /// Canonically ordered authored input handles for one-time host binding. + pub fn surface_input_ports( + &self, + ) -> impl ExactSizeIterator + '_ { + self.compiled + .surface_input_ports() + .iter() + .copied() + .map(PackageProgramSurfaceInputPortIdV1::from_core) + } + /// Canonically ordered opaque output slots owned by this Program. - pub fn output_slots(&self) -> impl ExactSizeIterator + '_ { - self.compiled.outputs().map(|(slot, _paint)| slot.value()) + pub fn output_slots(&self) -> impl ExactSizeIterator + '_ { + self.compiled + .outputs() + .map(|(slot, _paint)| PackageProgramOutputSlotIdV1::from_core(slot)) } /// Instantiate one stream-affine Session without exposing a generation. @@ -67,7 +982,7 @@ impl PackageProgramOwnerV1 { &self, stream_id: u32, ) -> Result { - let surface_input_count = self.compiled.surface_input_ports().len(); + let surface_input_ports = try_copy_surface_input_ports(&self.compiled)?; let output_slots = try_copy_output_slots(&self.compiled)?; let stream = ObservationStreamId::new(stream_id); let session = self @@ -76,7 +991,7 @@ impl PackageProgramOwnerV1 { .map_err(PackageProgramInstantiateErrorV1::from_core)?; Ok(PackageProgramSessionV1 { stream, - surface_input_count, + surface_input_ports, output_slots, scenario_order_scratch: Vec::new(), session, @@ -120,20 +1035,27 @@ pub enum PackageProgramUpdateV1<'a> { /// Concrete opaque owner of one mutable Core Program Session. pub struct PackageProgramSessionV1 { stream: ObservationStreamId, - surface_input_count: usize, - output_slots: Box<[u32]>, + surface_input_ports: Box<[PackageProgramSurfaceInputPortIdV1]>, + output_slots: Box<[PackageProgramOutputSlotIdV1]>, scenario_order_scratch: Vec, session: CoreProgramSessionV1, } impl PackageProgramSessionV1 { /// Number of schema-ordered values required in every observed scenario. - pub fn surface_input_count(&self) -> usize { - self.surface_input_count + pub fn surface_input_port_count(&self) -> usize { + self.surface_input_ports.len() + } + + /// Canonically ordered authored input handles for one-time host binding. + pub fn surface_input_ports( + &self, + ) -> impl ExactSizeIterator + '_ { + self.surface_input_ports.iter().copied() } /// Canonically ordered opaque output slots for one-time host binding. - pub fn output_slots(&self) -> impl ExactSizeIterator + '_ { + pub fn output_slots(&self) -> impl ExactSizeIterator + '_ { self.output_slots.iter().copied() } @@ -217,7 +1139,7 @@ pub enum PackageProgramStateKindV1 { pub struct PackageProgramStateViewV1<'a> { state: &'a CoreProgramStateV1, revision: Option, - output_slots: &'a [u32], + output_slots: &'a [PackageProgramOutputSlotIdV1], } impl<'a> PackageProgramStateViewV1<'a> { @@ -271,7 +1193,7 @@ impl<'a> PackageProgramStateViewV1<'a> { .outputs() .iter() .zip(self.output_slots) - .all(|(output, slot)| output.output().value() == *slot) + .all(|(output, slot)| output.output().value() == slot.value()) ); PackageProgramOperationSourceV1::Set(current.outputs().iter()) } @@ -366,16 +1288,16 @@ impl<'a> PackageProgramCertificateV1<'a> { #[derive(Debug, Clone, Copy, PartialEq)] pub enum PackageProgramOperationV1 { Set { - output_slot: u32, + output_slot: PackageProgramOutputSlotIdV1, source: Srgb8, opacity: f64, certificate_index: usize, }, Remove { - output_slot: u32, + output_slot: PackageProgramOutputSlotIdV1, }, Hold { - output_slot: u32, + output_slot: PackageProgramOutputSlotIdV1, certificate_index: usize, }, } @@ -426,10 +1348,10 @@ enum PackageProgramOperationSourceV1<'a> { Empty, Set(slice::Iter<'a, ProgramOutputV1>), Hold { - slots: slice::Iter<'a, u32>, + slots: slice::Iter<'a, PackageProgramOutputSlotIdV1>, certificate_index: usize, }, - Remove(slice::Iter<'a, u32>), + Remove(slice::Iter<'a, PackageProgramOutputSlotIdV1>), } struct PackageProgramOperationsV1<'a> { @@ -446,7 +1368,7 @@ impl Iterator for PackageProgramOperationsV1<'_> { let output = *outputs.next()?; let paint = output.paint(); Some(PackageProgramOperationV1::Set { - output_slot: output.output().value(), + output_slot: PackageProgramOutputSlotIdV1::from_core(output.output()), source: paint.source(), opacity: paint.opacity().value(), certificate_index: 0, @@ -550,15 +1472,297 @@ impl PackageProgramUpdateErrorV1 { } } +fn map_joint_order_error(error: FiniteJointOrderErrorV1) -> PackageProgramJointOrderErrorV1 { + match error { + FiniteJointOrderErrorV1::EmptyDomain { dimension } => { + PackageProgramJointOrderErrorV1::EmptyDomain { dimension } + } + FiniteJointOrderErrorV1::CardinalityOverflow => { + PackageProgramJointOrderErrorV1::CardinalityOverflow + } + FiniteJointOrderErrorV1::EmptyOrder => PackageProgramJointOrderErrorV1::EmptyOrder, + FiniteJointOrderErrorV1::TupleArity { + tuple, + expected, + actual, + } => PackageProgramJointOrderErrorV1::TupleArity { + state: tuple, + expected, + actual, + }, + FiniteJointOrderErrorV1::OrdinalOutOfDomain { + tuple, + dimension, + ordinal, + domain_len, + } => PackageProgramJointOrderErrorV1::OrdinalOutOfDomain { + state: tuple, + dimension, + ordinal, + domain_len, + }, + FiniteJointOrderErrorV1::DuplicateTuple { first, duplicate } => { + PackageProgramJointOrderErrorV1::DuplicateTuple { + first_state: first, + duplicate_state: duplicate, + } + } + FiniteJointOrderErrorV1::IncompleteOrder { expected, actual } => { + PackageProgramJointOrderErrorV1::IncompleteOrder { expected, actual } + } + FiniteJointOrderErrorV1::ResourceExhausted => { + PackageProgramJointOrderErrorV1::ResourceExhausted + } + } +} + +fn map_program_compile_error(error: ProgramCompileError) -> PackageProgramCompileErrorV1 { + match error { + ProgramCompileError::DuplicateSource { source } => { + PackageProgramCompileErrorV1::DuplicateSource { + source: PackageProgramSourceIdV1::from_core(source), + } + } + ProgramCompileError::DuplicateTarget { target } => { + PackageProgramCompileErrorV1::DuplicateTarget { + target: PackageProgramTargetIdV1::from_core(target), + } + } + ProgramCompileError::MissingTargetSource { target, source } => { + PackageProgramCompileErrorV1::MissingTargetSource { + target: PackageProgramTargetIdV1::from_core(target), + source: PackageProgramSourceIdV1::from_core(source), + } + } + ProgramCompileError::DuplicateOpacityInput { input } => { + PackageProgramCompileErrorV1::DuplicateOpacityInput { + input: PackageProgramOpacityInputIdV1::from_core(input), + } + } + ProgramCompileError::DuplicateSurfaceInputPort { input } => { + PackageProgramCompileErrorV1::DuplicateSurfaceInputPort { + input: PackageProgramSurfaceInputPortIdV1::from_core(input), + } + } + ProgramCompileError::UnusedSurfaceInputPort { input } => { + PackageProgramCompileErrorV1::UnusedSurfaceInputPort { + input: PackageProgramSurfaceInputPortIdV1::from_core(input), + } + } + ProgramCompileError::DuplicateSurfaceInputBinding { + input, + first, + duplicate, + } => PackageProgramCompileErrorV1::DuplicateSurfaceInputBinding { + input: PackageProgramSurfaceInputPortIdV1::from_core(input), + first: PackageProgramSurfaceIdV1::from_core(first), + duplicate: PackageProgramSurfaceIdV1::from_core(duplicate), + }, + ProgramCompileError::DuplicatePaint { paint } => { + PackageProgramCompileErrorV1::DuplicatePaint { + paint: PackageProgramPaintIdV1::from_core(paint), + } + } + ProgramCompileError::DuplicateSurface { surface } => { + PackageProgramCompileErrorV1::DuplicateSurface { + surface: PackageProgramSurfaceIdV1::from_core(surface), + } + } + ProgramCompileError::DuplicateOccurrence { occurrence } => { + PackageProgramCompileErrorV1::DuplicateOccurrence { + occurrence: PackageProgramOccurrenceIdV1::from_core(occurrence), + } + } + ProgramCompileError::MissingPaintTarget { paint, target } => { + PackageProgramCompileErrorV1::MissingPaintTarget { + paint: PackageProgramPaintIdV1::from_core(paint), + target: PackageProgramTargetIdV1::from_core(target), + } + } + ProgramCompileError::MissingPaintSource { paint, source } => { + PackageProgramCompileErrorV1::MissingPaintSource { + paint: PackageProgramPaintIdV1::from_core(paint), + source: PackageProgramPaintIdV1::from_core(source), + } + } + ProgramCompileError::MissingPaintOpacityInput { paint, input } => { + PackageProgramCompileErrorV1::MissingPaintOpacityInput { + paint: PackageProgramPaintIdV1::from_core(paint), + input: PackageProgramOpacityInputIdV1::from_core(input), + } + } + ProgramCompileError::MissingSurfaceInputPort { surface, input } => { + PackageProgramCompileErrorV1::MissingSurfaceInputPort { + surface: PackageProgramSurfaceIdV1::from_core(surface), + input: PackageProgramSurfaceInputPortIdV1::from_core(input), + } + } + ProgramCompileError::MissingSurfaceOccurrence { + surface, + occurrence, + } => PackageProgramCompileErrorV1::MissingSurfaceOccurrence { + surface: PackageProgramSurfaceIdV1::from_core(surface), + occurrence: PackageProgramOccurrenceIdV1::from_core(occurrence), + }, + ProgramCompileError::MissingOccurrencePaint { occurrence, paint } => { + PackageProgramCompileErrorV1::MissingOccurrencePaint { + occurrence: PackageProgramOccurrenceIdV1::from_core(occurrence), + paint: PackageProgramPaintIdV1::from_core(paint), + } + } + ProgramCompileError::MissingOccurrenceBackdrop { + occurrence, + surface, + } => PackageProgramCompileErrorV1::MissingOccurrenceBackdrop { + occurrence: PackageProgramOccurrenceIdV1::from_core(occurrence), + surface: PackageProgramSurfaceIdV1::from_core(surface), + }, + ProgramCompileError::PaintCycle { paints } => { + PackageProgramCompileErrorV1::PaintCycle(PackageProgramPaintCycleV1 { paints }) + } + ProgramCompileError::RenderCycle { + surfaces, + occurrences, + } => PackageProgramCompileErrorV1::RenderCycle(PackageProgramRenderCycleV1 { + surfaces, + occurrences, + }), + ProgramCompileError::OpacityOutOfDomain { input } => { + PackageProgramCompileErrorV1::OpacityOutOfDomain { + input: PackageProgramOpacityInputIdV1::from_core(input), + } + } + ProgramCompileError::EmptyTargetDomain { target } => { + PackageProgramCompileErrorV1::EmptyTargetDomain { + target: PackageProgramTargetIdV1::from_core(target), + } + } + ProgramCompileError::DuplicateTargetCandidate { target, candidate } => { + PackageProgramCompileErrorV1::DuplicateTargetCandidate { + target: PackageProgramTargetIdV1::from_core(target), + candidate: PackageProgramTargetCandidateIdV1::from_core(candidate), + } + } + ProgramCompileError::DuplicateTargetCandidateSignal { + target, + first, + duplicate, + signal, + } => PackageProgramCompileErrorV1::DuplicateTargetCandidateSignal { + target: PackageProgramTargetIdV1::from_core(target), + first: PackageProgramTargetCandidateIdV1::from_core(first), + duplicate: PackageProgramTargetCandidateIdV1::from_core(duplicate), + encoded_srgb8: signal.srgb8(), + }, + ProgramCompileError::UnconstrainedTarget { target } => { + PackageProgramCompileErrorV1::UnconstrainedTarget { + target: PackageProgramTargetIdV1::from_core(target), + } + } + ProgramCompileError::DisconnectedFiniteTargets => { + PackageProgramCompileErrorV1::DisconnectedFiniteTargets + } + ProgramCompileError::UnassessedOutput { output, paint } => { + PackageProgramCompileErrorV1::UnassessedOutput { + output: PackageProgramOutputSlotIdV1::from_core(output), + paint: PackageProgramPaintIdV1::from_core(paint), + } + } + ProgramCompileError::MissingJointSelection => { + PackageProgramCompileErrorV1::MissingJointSelection + } + ProgramCompileError::JointSelectionWithoutTargets => { + PackageProgramCompileErrorV1::JointSelectionWithoutTargets + } + ProgramCompileError::JointStateDuplicateTarget { state, target } => { + PackageProgramCompileErrorV1::JointStateDuplicateTarget { + state, + target: PackageProgramTargetIdV1::from_core(target), + } + } + ProgramCompileError::JointStateMissingTarget { state, target } => { + PackageProgramCompileErrorV1::JointStateMissingTarget { + state, + target: PackageProgramTargetIdV1::from_core(target), + } + } + ProgramCompileError::JointStateUnknownTarget { state, target } => { + PackageProgramCompileErrorV1::JointStateUnknownTarget { + state, + target: PackageProgramTargetIdV1::from_core(target), + } + } + ProgramCompileError::JointStateUnknownCandidate { + state, + target, + candidate, + } => PackageProgramCompileErrorV1::JointStateUnknownCandidate { + state, + target: PackageProgramTargetIdV1::from_core(target), + candidate: PackageProgramTargetCandidateIdV1::from_core(candidate), + }, + ProgramCompileError::InvalidJointOrder(error) => { + PackageProgramCompileErrorV1::InvalidJointOrder(map_joint_order_error(error)) + } + ProgramCompileError::EmptyObservationGroup { .. } => { + PackageProgramCompileErrorV1::EmptySurfaceInputPortSet + } + ProgramCompileError::EmptyOccurrenceSet => PackageProgramCompileErrorV1::EmptyOccurrenceSet, + ProgramCompileError::EmptyConstraintSet => PackageProgramCompileErrorV1::EmptyConstraintSet, + ProgramCompileError::EmptyOutputSet => PackageProgramCompileErrorV1::EmptyOutputSet, + ProgramCompileError::DuplicateConstraint { constraint } => { + PackageProgramCompileErrorV1::DuplicateConstraint { + constraint: PackageProgramConstraintIdV1::from_core(constraint), + } + } + ProgramCompileError::MissingConstraintOccurrence { + constraint, + occurrence, + } => PackageProgramCompileErrorV1::MissingConstraintOccurrence { + constraint: PackageProgramConstraintIdV1::from_core(constraint), + occurrence: PackageProgramOccurrenceIdV1::from_core(occurrence), + }, + ProgramCompileError::DuplicateOutputSlot { output } => { + PackageProgramCompileErrorV1::DuplicateOutputSlot { + output: PackageProgramOutputSlotIdV1::from_core(output), + } + } + ProgramCompileError::MissingOutputPaint { output, paint } => { + PackageProgramCompileErrorV1::MissingOutputPaint { + output: PackageProgramOutputSlotIdV1::from_core(output), + paint: PackageProgramPaintIdV1::from_core(paint), + } + } + ProgramCompileError::ResourceExhausted => PackageProgramCompileErrorV1::ResourceExhausted, + ProgramCompileError::InternalInvariant => PackageProgramCompileErrorV1::InternalInvariant, + } +} +fn try_copy_surface_input_ports( + compiled: &CompiledCoreProgramV1, +) -> Result, PackageProgramInstantiateErrorV1> { + let inputs = compiled.surface_input_ports(); + let mut copied = Vec::new(); + copied + .try_reserve_exact(inputs.len()) + .map_err(|_| PackageProgramInstantiateErrorKindV1::ResourceExhausted)?; + copied.extend( + inputs + .iter() + .copied() + .map(PackageProgramSurfaceInputPortIdV1::from_core), + ); + Ok(copied.into_boxed_slice()) +} + fn try_copy_output_slots( compiled: &CompiledCoreProgramV1, -) -> Result, PackageProgramInstantiateErrorV1> { +) -> Result, PackageProgramInstantiateErrorV1> { let outputs = compiled.outputs(); let mut copied = Vec::new(); copied .try_reserve_exact(outputs.len()) .map_err(|_| PackageProgramInstantiateErrorKindV1::ResourceExhausted)?; - copied.extend(outputs.map(|(slot, _paint)| slot.value())); + copied.extend(outputs.map(|(slot, _paint)| PackageProgramOutputSlotIdV1::from_core(slot))); Ok(copied.into_boxed_slice()) } @@ -619,3 +1823,26 @@ fn map_plan_error(error: CoreProgramPlanErrorV1) -> PackageProgramUpdateErrorKin } } } + +#[cfg(test)] +mod compile_error_projection_tests { + use super::*; + + #[test] + fn nested_joint_resource_exhaustion_keeps_its_exact_reason_and_site_kind() { + let error = map_program_compile_error(ProgramCompileError::InvalidJointOrder( + FiniteJointOrderErrorV1::ResourceExhausted, + )); + + assert_eq!( + error.kind(), + PackageProgramCompileErrorKindV1::InvalidJointOrder + ); + assert_eq!( + error, + PackageProgramCompileErrorV1::InvalidJointOrder( + PackageProgramJointOrderErrorV1::ResourceExhausted + ) + ); + } +} diff --git a/crates/labcolors-core/src/program_mixed_evaluator_tests.rs b/crates/labcolors-core/src/program_mixed_evaluator_tests.rs index 3b566a47..7b30e193 100644 --- a/crates/labcolors-core/src/program_mixed_evaluator_tests.rs +++ b/crates/labcolors-core/src/program_mixed_evaluator_tests.rs @@ -12,9 +12,9 @@ use crate::observation::{ ObservedScenarioSetInput, Revision, ScenarioId, ScenarioInput, SurfaceInputBinding, }; use crate::package_bridge::{ - PackageProgramCertificateKindV1, PackageProgramOperationV1, PackageProgramOwnerV1, - PackageProgramScenarioV1, PackageProgramStateKindV1, PackageProgramUpdateErrorKindV1, - PackageProgramUpdateV1, + PackageProgramCertificateKindV1, PackageProgramOperationV1, PackageProgramOutputSlotIdV1, + PackageProgramOwnerV1, PackageProgramScenarioV1, PackageProgramStateKindV1, + PackageProgramUpdateErrorKindV1, PackageProgramUpdateV1, }; use crate::program_session::{ CompiledCoreProgramV1, CompositionProfile, ConstraintId, ConstraintInvocation, ConstraintSet, @@ -305,8 +305,11 @@ fn mixed_family_conflict_is_exhaustive_and_keeps_report_only_non_gating() { #[test] fn concrete_package_bridge_projects_total_ready_and_stale_operations() { let owner = PackageProgramOwnerV1::from_compiled(finite_program([[0x80; 3], [0; 3]])); - assert_eq!(owner.surface_input_count(), 1); - assert_eq!(owner.output_slots().collect::>(), [OUTPUT.value()]); + assert_eq!(owner.surface_input_port_count(), 1); + assert_eq!( + owner.output_slots().collect::>(), + [PackageProgramOutputSlotIdV1::new(OUTPUT.value())] + ); let mut session = owner.instantiate(11).unwrap(); let initial = session.state(); @@ -341,7 +344,7 @@ fn concrete_package_bridge_projects_total_ready_and_stale_operations() { assert_eq!( ready.operations().collect::>(), [PackageProgramOperationV1::Set { - output_slot: OUTPUT.value(), + output_slot: PackageProgramOutputSlotIdV1::new(OUTPUT.value()), source: Srgb8::new([0; 3]), opacity: 1.0, certificate_index: 0, @@ -398,7 +401,7 @@ fn concrete_package_bridge_projects_total_ready_and_stale_operations() { assert_eq!( stale.operations().collect::>(), [PackageProgramOperationV1::Hold { - output_slot: OUTPUT.value(), + output_slot: PackageProgramOutputSlotIdV1::new(OUTPUT.value()), certificate_index: 0, }] ); @@ -430,7 +433,7 @@ fn concrete_package_bridge_distinguishes_failed_remove_from_failed_hold() { assert_eq!( failed.operations().collect::>(), [PackageProgramOperationV1::Remove { - output_slot: OUTPUT.value(), + output_slot: PackageProgramOutputSlotIdV1::new(OUTPUT.value()), }] ); @@ -468,7 +471,7 @@ fn concrete_package_bridge_distinguishes_failed_remove_from_failed_hold() { assert_eq!( failed.operations().collect::>(), [PackageProgramOperationV1::Hold { - output_slot: OUTPUT.value(), + output_slot: PackageProgramOutputSlotIdV1::new(OUTPUT.value()), certificate_index: 1, }] ); diff --git a/crates/labcolors-core/src/program_session.rs b/crates/labcolors-core/src/program_session.rs index f9c01e64..30c4b217 100644 --- a/crates/labcolors-core/src/program_session.rs +++ b/crates/labcolors-core/src/program_session.rs @@ -121,6 +121,12 @@ impl TargetCandidateV1 { Self { id, signal } } + /// Bind one encoded sRGB8 candidate through the sole admitted signal + /// profile. Package authoring never carries a free-form profile tag. + pub const fn from_srgb8(id: TargetCandidateId, value: Srgb8) -> Self { + Self::new(id, ColorSignal::from_srgb8(value)) + } + pub const fn id(self) -> TargetCandidateId { self.id } @@ -731,6 +737,104 @@ where /// code-owned evaluator union. pub(crate) type CoreProgramV1 = Program; +/// Mutable cold-edge builder for the one concrete Core Program IR. +/// +/// Every pushed value is already an actual Program declaration type. This +/// builder owns no transport tags, names, client taxonomy, or second graph; +/// compilation moves the concrete Program directly into the existing atomic +/// compiler. +pub(crate) struct CoreProgramDraftV1 { + program: CoreProgramV1, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum CoreProgramDraftErrorV1 { + JointSelectionAlreadyDeclared, +} + +impl CoreProgramDraftV1 { + pub(crate) fn new() -> Self { + Self { + program: Program::new( + Vec::new(), + Vec::new(), + ObservationGroup::new(ObservationGroupId::new(0), Vec::new()), + Vec::new(), + Vec::new(), + Vec::new(), + Vec::new(), + ConstraintSet::new(Vec::new(), Vec::new()), + Vec::new(), + CoreProgramEvaluatorsV1, + ), + } + } + + pub(crate) fn push_source(&mut self, source: Source) { + self.program.sources.push(source); + } + + pub(crate) fn push_target(&mut self, target: Target) { + self.program.targets.push(target); + } + + pub(crate) fn set_joint_selection( + &mut self, + selection: DeclaredJointSelectionV1, + ) -> Result<(), CoreProgramDraftErrorV1> { + if self.program.joint_selection.is_some() { + return Err(CoreProgramDraftErrorV1::JointSelectionAlreadyDeclared); + } + self.program.joint_selection = Some(selection); + Ok(()) + } + + pub(crate) fn push_surface_input_port(&mut self, input: SurfaceInputPortId) { + self.program + .observation_group + .surface_input_ports + .push(input); + } + + pub(crate) fn push_opacity_input(&mut self, opacity: OpacityInput) { + self.program.opacities.push(opacity); + } + + pub(crate) fn push_paint(&mut self, paint: Paint) { + self.program.paints.push(paint); + } + + pub(crate) fn push_surface(&mut self, surface: Surface) { + self.program.surfaces.push(surface); + } + + pub(crate) fn push_occurrence(&mut self, occurrence: Occurrence) { + self.program.occurrences.push(occurrence); + } + + pub(crate) fn push_hard_constraint( + &mut self, + constraint: ConstraintInvocation, + ) { + self.program.constraints.hard.push(constraint); + } + + pub(crate) fn push_report_constraint( + &mut self, + constraint: ConstraintInvocation, + ) { + self.program.constraints.report_only.push(constraint); + } + + pub(crate) fn push_output(&mut self, output: OutputBinding) { + self.program.outputs.push(output); + } + + pub(crate) fn compile(self) -> Result { + self.program.compile() + } +} + /// Atomic compile failure. No executable partial graph escapes. #[derive(Debug, PartialEq, Eq)] pub enum ProgramCompileError { @@ -750,6 +854,14 @@ pub enum ProgramCompileError { DuplicateSurfaceInputPort { input: SurfaceInputPortId, }, + UnusedSurfaceInputPort { + input: SurfaceInputPortId, + }, + DuplicateSurfaceInputBinding { + input: SurfaceInputPortId, + first: SurfaceId, + duplicate: SurfaceId, + }, DuplicatePaint { paint: PaintId, }, @@ -1827,6 +1939,51 @@ fn map_program_execution_binding_error( } } +fn validate_surface_input_bijection( + program: &Program, +) -> Result<(), ProgramCompileError> +where + Evaluation: ProgramConstraintEvaluatorSetV1, + ProgramConstraintInvocationOf: Copy, +{ + let mut bindings = Vec::new(); + bindings + .try_reserve_exact(program.surfaces.len()) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + for surface in &program.surfaces { + if let Surface::Input { id, input } = *surface { + bindings.push((input, id)); + } + } + bindings.sort_unstable(); + + for pair in bindings.windows(2) { + if let [ + (first_input, first_surface), + (duplicate_input, duplicate_surface), + ] = pair + { + if first_input == duplicate_input { + return Err(ProgramCompileError::DuplicateSurfaceInputBinding { + input: *first_input, + first: *first_surface, + duplicate: *duplicate_surface, + }); + } + } + } + + for input in &program.observation_group.surface_input_ports { + if bindings + .binary_search_by_key(input, |(bound, _surface)| *bound) + .is_err() + { + return Err(ProgramCompileError::UnusedSurfaceInputPort { input: *input }); + } + } + Ok(()) +} + fn prepare_program( mut program: Program, ) -> Result, ProgramCompileError> @@ -1858,6 +2015,7 @@ where let graph = lower_graph(&program)? .compile() .map_err(map_compile_error)?; + validate_surface_input_bijection(&program)?; let binding_template = graph .admit_bindings(&lower_bindings(&program)?) .map_err(map_binding_compile_error)?; diff --git a/crates/labcolors-core/tests/package_bridge_red.rs b/crates/labcolors-core/tests/package_bridge_red.rs index 4ac581d0..0e7b6469 100644 --- a/crates/labcolors-core/tests/package_bridge_red.rs +++ b/crates/labcolors-core/tests/package_bridge_red.rs @@ -7,16 +7,33 @@ use labcolors_core::Srgb8; use labcolors_core::package_bridge::{ - PackageProgramCertificateV1, PackageProgramInstantiateErrorV1, PackageProgramOperationV1, - PackageProgramOwnerV1, PackageProgramScenarioV1, PackageProgramSessionV1, - PackageProgramStateKindV1, PackageProgramStateViewV1, PackageProgramUpdateErrorKindV1, - PackageProgramUpdateV1, + PackageProgramAppearanceContextErrorKindV1, PackageProgramAppearanceContextFieldV1, + PackageProgramAppearanceContextV1, PackageProgramCertificateV1, + PackageProgramCompileErrorHandleV1, PackageProgramCompileErrorKindV1, + PackageProgramCompileErrorV1, PackageProgramConstraintIdV1, PackageProgramDraftErrorV1, + PackageProgramDraftV1, PackageProgramInstantiateErrorV1, PackageProgramJointChoiceV1, + PackageProgramJointOrderErrorV1, PackageProgramJointStateV1, + PackageProgramNumericDomainErrorV1, PackageProgramOccurrenceIdV1, + PackageProgramOpacityInputIdV1, PackageProgramOperationV1, PackageProgramOutputSlotIdV1, + PackageProgramOwnerV1, PackageProgramPaintIdV1, PackageProgramScenarioV1, + PackageProgramSessionV1, PackageProgramSourceIdV1, PackageProgramStateKindV1, + PackageProgramStateViewV1, PackageProgramSurfaceIdV1, PackageProgramSurfaceInputPortIdV1, + PackageProgramSurroundV1, PackageProgramTargetCandidateIdV1, PackageProgramTargetCandidateV1, + PackageProgramTargetIdV1, PackageProgramUpdateErrorKindV1, PackageProgramUpdateV1, }; +use labcolors_core::wcag22::Wcag22CriterionV1; fn exact_size(iterator: I) -> I { iterator } +fn compile_error(draft: PackageProgramDraftV1) -> PackageProgramCompileErrorV1 { + match draft.compile() { + Ok(_) => panic!("the invalid authored program must not compile"), + Err(error) => error, + } +} + #[allow(dead_code)] fn wasm_can_use_only_the_concrete_owner_and_session( owner: &PackageProgramOwnerV1, @@ -49,19 +66,19 @@ fn assert_projection_is_linear(view: PackageProgramStateViewV1<'_>) { opacity, certificate_index, } => { - let _: u32 = output_slot; + let _: PackageProgramOutputSlotIdV1 = output_slot; let _: Srgb8 = source; assert!(opacity.is_finite() && (0.0..=1.0).contains(&opacity)); assert!(certificate_index < certificate_count); } PackageProgramOperationV1::Remove { output_slot } => { - let _: u32 = output_slot; + let _: PackageProgramOutputSlotIdV1 = output_slot; } PackageProgramOperationV1::Hold { output_slot, certificate_index, } => { - let _: u32 = output_slot; + let _: PackageProgramOutputSlotIdV1 = output_slot; assert!(certificate_index < certificate_count); } } @@ -92,3 +109,484 @@ fn red_contract_is_linked_by_the_concrete_package_module() { // Program, evaluator traits, Session, or numeric generations. assert_eq!(core::mem::size_of::(), 3); } + +fn fixed_nested_draft( + opacity_value: f64, + target_source: PackageProgramSourceIdV1, + declared_input: PackageProgramSurfaceInputPortIdV1, + used_input: PackageProgramSurfaceInputPortIdV1, +) -> PackageProgramDraftV1 { + let source = PackageProgramSourceIdV1::new(1); + let target = PackageProgramTargetIdV1::new(2); + let opacity = PackageProgramOpacityInputIdV1::new(3); + let solid = PackageProgramPaintIdV1::new(4); + let translucent = PackageProgramPaintIdV1::new(5); + let input_surface = PackageProgramSurfaceIdV1::new(6); + let nested_surface = PackageProgramSurfaceIdV1::new(7); + let first_occurrence = PackageProgramOccurrenceIdV1::new(8); + let second_occurrence = PackageProgramOccurrenceIdV1::new(9); + let exact = PackageProgramConstraintIdV1::new(10); + let wcag = PackageProgramConstraintIdV1::new(11); + let output = PackageProgramOutputSlotIdV1::new(12); + let context = + PackageProgramAppearanceContextV1::try_new(64.0, 0.2, PackageProgramSurroundV1::Dim) + .unwrap(); + + let mut draft = PackageProgramDraftV1::new(); + draft.push_source(source, Srgb8::new([0; 3])); + draft.push_fixed_target(target, target_source); + draft.push_surface_input_port(declared_input); + draft.push_opacity_input(opacity, opacity_value); + draft.push_solid_paint(solid, target); + draft.push_opacity_paint(translucent, solid, opacity); + draft.push_input_surface(input_surface, used_input); + draft.push_source_over_occurrence(first_occurrence, translucent, input_surface, context); + draft.push_occurrence_surface(nested_surface, first_occurrence); + draft.push_source_over_occurrence(second_occurrence, solid, nested_surface, context); + draft.push_exact_hard(exact, first_occurrence, Srgb8::new([0; 3])); + draft.push_wcag22_report_only(wcag, second_occurrence, Wcag22CriterionV1::Sc143TextDefault); + draft.push_output(output, translucent); + draft +} + +fn attach_target_assessment(draft: &mut PackageProgramDraftV1, target: PackageProgramTargetIdV1) { + let paint = PackageProgramPaintIdV1::new(770); + let occurrence = PackageProgramOccurrenceIdV1::new(771); + let constraint = PackageProgramConstraintIdV1::new(772); + let context = + PackageProgramAppearanceContextV1::try_new(64.0, 0.2, PackageProgramSurroundV1::Average) + .unwrap(); + draft.push_solid_paint(paint, target); + draft.push_source_over_occurrence( + occurrence, + paint, + PackageProgramSurfaceIdV1::new(6), + context, + ); + draft.push_exact_report_only(constraint, occurrence, Srgb8::new([0; 3])); +} + +#[test] +fn external_authoring_lowers_the_actual_closed_program_and_returns_canonical_input_ports() { + let source = PackageProgramSourceIdV1::new(91); + let target = PackageProgramTargetIdV1::new(72); + let gray = PackageProgramTargetCandidateIdV1::new(8); + let black = PackageProgramTargetCandidateIdV1::new(3); + let paint = PackageProgramPaintIdV1::new(54); + let high_input = PackageProgramSurfaceInputPortIdV1::new(900); + let low_input = PackageProgramSurfaceInputPortIdV1::new(2); + let high_surface = PackageProgramSurfaceIdV1::new(401); + let low_surface = PackageProgramSurfaceIdV1::new(400); + let high_occurrence = PackageProgramOccurrenceIdV1::new(301); + let low_occurrence = PackageProgramOccurrenceIdV1::new(300); + let exact = PackageProgramConstraintIdV1::new(201); + let high_wcag = PackageProgramConstraintIdV1::new(203); + let low_wcag = PackageProgramConstraintIdV1::new(202); + let output = PackageProgramOutputSlotIdV1::new(101); + let context = + PackageProgramAppearanceContextV1::try_new(64.0, 0.2, PackageProgramSurroundV1::Average) + .unwrap(); + + let mut draft = PackageProgramDraftV1::new(); + draft.push_source(source, Srgb8::new([0x80; 3])); + draft.push_finite_target( + target, + source, + vec![ + PackageProgramTargetCandidateV1::new(gray, Srgb8::new([0x80; 3])), + PackageProgramTargetCandidateV1::new(black, Srgb8::new([0; 3])), + ], + ); + draft + .set_joint_selection(vec![ + PackageProgramJointStateV1::new(vec![PackageProgramJointChoiceV1::new(target, gray)]), + PackageProgramJointStateV1::new(vec![PackageProgramJointChoiceV1::new(target, black)]), + ]) + .unwrap(); + // Deliberately reverse numeric order. Core, not the caller, owns the hot + // scenario schema order returned below. + draft.push_surface_input_port(high_input); + draft.push_surface_input_port(low_input); + draft.push_solid_paint(paint, target); + draft.push_input_surface(high_surface, high_input); + draft.push_input_surface(low_surface, low_input); + draft.push_source_over_occurrence(high_occurrence, paint, high_surface, context); + draft.push_source_over_occurrence(low_occurrence, paint, low_surface, context); + draft.push_exact_report_only(exact, high_occurrence, Srgb8::new([0; 3])); + draft.push_wcag22_hard( + high_wcag, + high_occurrence, + Wcag22CriterionV1::Sc143TextDefault, + ); + draft.push_wcag22_hard( + low_wcag, + low_occurrence, + Wcag22CriterionV1::Sc143TextDefault, + ); + draft.push_output(output, paint); + + let owner = draft.compile().unwrap(); + assert_eq!( + owner.surface_input_ports().collect::>(), + [low_input, high_input] + ); + let mut session = owner.instantiate(44).unwrap(); + assert_eq!( + session.surface_input_ports().collect::>(), + [low_input, high_input] + ); + let white = [Srgb8::new([0xFF; 3]), Srgb8::new([0xFF; 3])]; + let scenarios = [PackageProgramScenarioV1::new(7, &white)]; + let ready = session + .update(PackageProgramUpdateV1::Observed { + revision: 1, + scenarios: &scenarios, + }) + .unwrap(); + assert_eq!(ready.kind(), PackageProgramStateKindV1::Ready); + assert_eq!( + ready.operations().collect::>(), + [PackageProgramOperationV1::Set { + output_slot: output, + source: Srgb8::new([0; 3]), + opacity: 1.0, + certificate_index: 0, + }] + ); +} + +#[test] +fn authored_compile_is_atomic_and_projects_a_closed_error() { + let source = PackageProgramSourceIdV1::new(1); + let input = PackageProgramSurfaceInputPortIdV1::new(50); + let mut draft = fixed_nested_draft(1.0, source, input, input); + draft.push_source(source, Srgb8::new([0xFF; 3])); + + let error = match draft.compile() { + Ok(_) => panic!("duplicate declaration must not compile"), + Err(error) => error, + }; + assert_eq!( + error.kind(), + PackageProgramCompileErrorKindV1::DuplicateSource + ); + assert_eq!( + error.primary_handle(), + Some(PackageProgramCompileErrorHandleV1::Source(source)) + ); + assert_eq!(error.related_handle(), None); +} + +#[test] +fn every_physical_constructor_and_both_remaining_constraint_modes_execute() { + let input = PackageProgramSurfaceInputPortIdV1::new(50); + let draft = fixed_nested_draft(1.0, PackageProgramSourceIdV1::new(1), input, input); + let owner = draft.compile().unwrap(); + assert_eq!(owner.surface_input_ports().collect::>(), [input]); + + let mut session = owner.instantiate(13).unwrap(); + let white = [Srgb8::new([0xFF; 3])]; + let scenarios = [PackageProgramScenarioV1::new(1, &white)]; + let state = session + .update(PackageProgramUpdateV1::Observed { + revision: 1, + scenarios: &scenarios, + }) + .unwrap(); + assert_eq!(state.kind(), PackageProgramStateKindV1::Ready); + assert_eq!( + state.operations().collect::>(), + [PackageProgramOperationV1::Set { + output_slot: PackageProgramOutputSlotIdV1::new(12), + source: Srgb8::new([0; 3]), + opacity: 1.0, + certificate_index: 0, + }] + ); +} + +#[test] +fn invalid_context_and_opacity_are_typed_and_fail_closed() { + let context_error = + PackageProgramAppearanceContextV1::try_new(64.0, 1.01, PackageProgramSurroundV1::Dark) + .unwrap_err(); + assert_eq!( + context_error.kind(), + PackageProgramAppearanceContextErrorKindV1::Domain + ); + assert_eq!( + context_error.field(), + Some(PackageProgramAppearanceContextFieldV1::BackgroundLuminanceRatioYbYw) + ); + assert_eq!( + context_error.reason(), + Some(PackageProgramNumericDomainErrorV1::AboveOne) + ); + + let input = PackageProgramSurfaceInputPortIdV1::new(50); + let error = match fixed_nested_draft(f64::NAN, PackageProgramSourceIdV1::new(1), input, input) + .compile() + { + Ok(_) => panic!("non-finite opacity must not compile"), + Err(error) => error, + }; + assert_eq!( + error.kind(), + PackageProgramCompileErrorKindV1::OpacityOutOfDomain + ); + assert_eq!( + error.primary_handle(), + Some(PackageProgramCompileErrorHandleV1::OpacityInput( + PackageProgramOpacityInputIdV1::new(3) + )) + ); + assert_eq!(error.related_handle(), None); +} + +#[test] +fn relational_compile_errors_keep_both_typed_handles() { + let input = PackageProgramSurfaceInputPortIdV1::new(50); + let missing_source = PackageProgramSourceIdV1::new(99); + let error = match fixed_nested_draft(1.0, missing_source, input, input).compile() { + Ok(_) => panic!("a target cannot reference an undeclared source"), + Err(error) => error, + }; + assert_eq!( + error.kind(), + PackageProgramCompileErrorKindV1::MissingTargetSource + ); + assert_eq!( + error.primary_handle(), + Some(PackageProgramCompileErrorHandleV1::Target( + PackageProgramTargetIdV1::new(2) + )) + ); + assert_eq!( + error.related_handle(), + Some(PackageProgramCompileErrorHandleV1::Source(missing_source)) + ); +} + +#[test] +fn declared_and_referenced_surface_inputs_cannot_drift() { + let declared = PackageProgramSurfaceInputPortIdV1::new(50); + let missing = PackageProgramSurfaceInputPortIdV1::new(51); + let error = match fixed_nested_draft(1.0, PackageProgramSourceIdV1::new(1), declared, missing) + .compile() + { + Ok(_) => panic!("an undeclared physical input must not compile"), + Err(error) => error, + }; + assert_eq!( + error.kind(), + PackageProgramCompileErrorKindV1::MissingSurfaceInputPort + ); + assert_eq!( + error.primary_handle(), + Some(PackageProgramCompileErrorHandleV1::Surface( + PackageProgramSurfaceIdV1::new(6) + )) + ); + assert_eq!( + error.related_handle(), + Some(PackageProgramCompileErrorHandleV1::SurfaceInputPort( + missing + )) + ); +} + +#[test] +fn declared_input_ports_form_an_exact_bijection_with_input_surfaces() { + let input = PackageProgramSurfaceInputPortIdV1::new(50); + let extra = PackageProgramSurfaceInputPortIdV1::new(51); + let mut unused = fixed_nested_draft(1.0, PackageProgramSourceIdV1::new(1), input, input); + unused.push_surface_input_port(extra); + let error = match unused.compile() { + Ok(_) => panic!("an unused declared input must not compile"), + Err(error) => error, + }; + assert_eq!( + error.kind(), + PackageProgramCompileErrorKindV1::UnusedSurfaceInputPort + ); + assert_eq!( + error.primary_handle(), + Some(PackageProgramCompileErrorHandleV1::SurfaceInputPort(extra)) + ); + + let duplicate_surface = PackageProgramSurfaceIdV1::new(60); + let mut duplicate = fixed_nested_draft(1.0, PackageProgramSourceIdV1::new(1), input, input); + duplicate.push_input_surface(duplicate_surface, input); + let error = match duplicate.compile() { + Ok(_) => panic!("two input surfaces must not bind one declared port"), + Err(error) => error, + }; + assert_eq!( + error.kind(), + PackageProgramCompileErrorKindV1::DuplicateSurfaceInputBinding + ); + assert_eq!( + error.primary_handle(), + Some(PackageProgramCompileErrorHandleV1::SurfaceInputPort(input)) + ); + assert_eq!( + error.related_handle(), + Some(PackageProgramCompileErrorHandleV1::Surface( + duplicate_surface + )) + ); + assert_eq!( + error, + PackageProgramCompileErrorV1::DuplicateSurfaceInputBinding { + input, + first: PackageProgramSurfaceIdV1::new(6), + duplicate: duplicate_surface, + } + ); +} + +#[test] +fn duplicate_candidate_signal_preserves_both_candidates_and_exact_stimulus() { + let input = PackageProgramSurfaceInputPortIdV1::new(50); + let target = PackageProgramTargetIdV1::new(70); + let first = PackageProgramTargetCandidateIdV1::new(701); + let duplicate = PackageProgramTargetCandidateIdV1::new(702); + let encoded_srgb8 = Srgb8::new([17, 33, 65]); + let mut draft = fixed_nested_draft(1.0, PackageProgramSourceIdV1::new(1), input, input); + draft.push_finite_target( + target, + PackageProgramSourceIdV1::new(1), + vec![ + PackageProgramTargetCandidateV1::new(first, encoded_srgb8), + PackageProgramTargetCandidateV1::new(duplicate, encoded_srgb8), + ], + ); + attach_target_assessment(&mut draft, target); + + assert_eq!( + compile_error(draft), + PackageProgramCompileErrorV1::DuplicateTargetCandidateSignal { + target, + first, + duplicate, + encoded_srgb8, + } + ); +} + +#[test] +fn joint_diagnostics_preserve_state_and_total_order_details() { + let input = PackageProgramSurfaceInputPortIdV1::new(50); + let target = PackageProgramTargetIdV1::new(70); + let first = PackageProgramTargetCandidateIdV1::new(701); + let second = PackageProgramTargetCandidateIdV1::new(702); + let candidates = vec![ + PackageProgramTargetCandidateV1::new(first, Srgb8::new([0; 3])), + PackageProgramTargetCandidateV1::new(second, Srgb8::new([255; 3])), + ]; + + let mut duplicate_target = + fixed_nested_draft(1.0, PackageProgramSourceIdV1::new(1), input, input); + duplicate_target.push_finite_target( + target, + PackageProgramSourceIdV1::new(1), + candidates.clone(), + ); + attach_target_assessment(&mut duplicate_target, target); + duplicate_target + .set_joint_selection(vec![PackageProgramJointStateV1::new(vec![ + PackageProgramJointChoiceV1::new(target, first), + PackageProgramJointChoiceV1::new(target, second), + ])]) + .unwrap(); + assert_eq!( + compile_error(duplicate_target), + PackageProgramCompileErrorV1::JointStateDuplicateTarget { state: 0, target } + ); + + let mut incomplete = fixed_nested_draft(1.0, PackageProgramSourceIdV1::new(1), input, input); + incomplete.push_finite_target(target, PackageProgramSourceIdV1::new(1), candidates); + attach_target_assessment(&mut incomplete, target); + incomplete + .set_joint_selection(vec![PackageProgramJointStateV1::new(vec![ + PackageProgramJointChoiceV1::new(target, first), + ])]) + .unwrap(); + assert_eq!( + compile_error(incomplete), + PackageProgramCompileErrorV1::InvalidJointOrder( + PackageProgramJointOrderErrorV1::IncompleteOrder { + expected: 2, + actual: 1, + } + ) + ); +} + +#[test] +fn dependency_cycles_retain_all_typed_core_members_without_reallocation() { + let input = PackageProgramSurfaceInputPortIdV1::new(50); + let first_paint = PackageProgramPaintIdV1::new(70); + let second_paint = PackageProgramPaintIdV1::new(71); + let mut paint_cycle = fixed_nested_draft(1.0, PackageProgramSourceIdV1::new(1), input, input); + paint_cycle.push_opacity_paint( + first_paint, + second_paint, + PackageProgramOpacityInputIdV1::new(3), + ); + paint_cycle.push_opacity_paint( + second_paint, + first_paint, + PackageProgramOpacityInputIdV1::new(3), + ); + let error = compile_error(paint_cycle); + let PackageProgramCompileErrorV1::PaintCycle(cycle) = error else { + panic!("expected an exact paint cycle") + }; + assert_eq!( + cycle.paints().collect::>(), + [first_paint, second_paint] + ); + + let cyclic_surface = PackageProgramSurfaceIdV1::new(80); + let cyclic_occurrence = PackageProgramOccurrenceIdV1::new(81); + let context = + PackageProgramAppearanceContextV1::try_new(64.0, 0.2, PackageProgramSurroundV1::Average) + .unwrap(); + let mut render_cycle = fixed_nested_draft(1.0, PackageProgramSourceIdV1::new(1), input, input); + render_cycle.push_occurrence_surface(cyclic_surface, cyclic_occurrence); + render_cycle.push_source_over_occurrence( + cyclic_occurrence, + PackageProgramPaintIdV1::new(4), + cyclic_surface, + context, + ); + let error = compile_error(render_cycle); + let PackageProgramCompileErrorV1::RenderCycle(cycle) = error else { + panic!("expected an exact render cycle") + }; + assert_eq!(cycle.surfaces().collect::>(), [cyclic_surface]); + assert_eq!(cycle.occurrences().collect::>(), [cyclic_occurrence]); +} + +#[test] +fn the_code_owned_observation_group_reports_package_authored_port_semantics() { + assert_eq!( + compile_error(PackageProgramDraftV1::new()), + PackageProgramCompileErrorV1::EmptySurfaceInputPortSet + ); +} + +#[test] +fn singleton_joint_order_cannot_be_silently_replaced() { + let mut draft = PackageProgramDraftV1::new(); + draft.set_joint_selection(vec![]).unwrap(); + let error = match draft.set_joint_selection(vec![]) { + Ok(_) => panic!("a singleton declaration must not be replaced"), + Err(error) => error, + }; + assert_eq!( + error, + PackageProgramDraftErrorV1::JointSelectionAlreadyDeclared + ); +} diff --git a/packages/colors/bench/wasm.json b/packages/colors/bench/wasm.json index ff761b3b..f88f6209 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-29966828219", + "source": "github-actions-run-29971399220", "platform": "linux-x64", - "rawBytes": 376832 + "rawBytes": 376830 }, "policy": { - "maxRawBytes": 376832, - "basis": "mixed-evaluator-package-bridge", + "maxRawBytes": 376830, + "basis": "canonical-authored-program-lowerer", "gzip": "diagnostic-only" } } diff --git a/scripts/check-wasm-size-budget.mjs b/scripts/check-wasm-size-budget.mjs old mode 100755 new mode 100644 index d5008214..a5776086 --- 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 = - "2a296bcdcef65e2a5d1e49ad088ee8c6e7f6fa2d2550a8d26e945e0d98dd0d61"; + "84a8d2e1f0517f871256fdc64bdf9323135c497e3a374b751d8c0bb710f60084"; const SCHEMA_VERSION = 1; const CANONICAL_ARTIFACT = "packages/colors/pkg/labcolors_bg.wasm"; diff --git a/scripts/verify_point_support_surplus.py b/scripts/verify_point_support_surplus.py index 9e0702ff..e39328fa 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 = ( - "4a762a7fd0a658286288ef651cbaf53940e4676f47c63c970799736650a62e21" + "4310a239e732f0710fd6201c2517fd98ef4afd0e3cd8e27c248dee69d6c54cb3" ) EXPECTED_Q55_PROOF_SHA256 = ( "ac59cf89503170c789223b91d775213a19d4e571ef930f2ea609fcd51b14defd" 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 37/58] 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 38/58] 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 39/58] 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 40/58] 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" From 0f6ddab8636226b3029e3164177e0fe77d02f2bf Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Fri, 24 Jul 2026 22:42:00 +0300 Subject: [PATCH 41/58] core: project complete program certificate evidence --- crates/labcolors-core/src/appearance.rs | 9 +- crates/labcolors-core/src/constraints/mod.rs | 1 - .../labcolors-core/src/constraints/wcag22.rs | 2 +- crates/labcolors-core/src/lcs_occurrence.rs | 23 +- crates/labcolors-core/src/observation.rs | 8 + crates/labcolors-core/src/package_bridge.rs | 815 ++++++++++-- .../src/program_mixed_evaluator_tests.rs | 1148 ++++++++++++++++- crates/labcolors-core/src/program_session.rs | 11 + ...kage_bridge_red.rs => program_boundary.rs} | 233 +++- 9 files changed, 2048 insertions(+), 202 deletions(-) rename crates/labcolors-core/tests/{package_bridge_red.rs => program_boundary.rs} (69%) diff --git a/crates/labcolors-core/src/appearance.rs b/crates/labcolors-core/src/appearance.rs index 8c635e94..321a3709 100644 --- a/crates/labcolors-core/src/appearance.rs +++ b/crates/labcolors-core/src/appearance.rs @@ -134,17 +134,14 @@ pub(crate) struct ProgramOccurrenceBindingV1 { } impl ProgramOccurrenceBindingV1 { - #[cfg(test)] pub(crate) const fn occurrence(self) -> OccurrenceId { self.occurrence } - #[cfg(test)] pub(crate) const fn subject(self) -> PaintId { self.subject } - #[cfg(test)] pub(crate) const fn backdrop_surface(self) -> SurfaceId { self.backdrop_surface } @@ -1217,7 +1214,6 @@ impl EncodedPointPaintV1 { self.opacity } - #[cfg(test)] pub(crate) const fn opacity_bits(self) -> u64 { self.opacity.bits() } @@ -1242,7 +1238,6 @@ impl SourceOverCertificateV1 { .composite(self.subject_rgb, self.subject_opacity, self.backdrop_rgb) } - #[cfg(test)] pub(crate) const fn profile(&self) -> CompositionProfileV1 { self.profile } @@ -1357,11 +1352,11 @@ pub(crate) struct VisiblePointBindingV1 { } impl VisiblePointBindingV1 { - pub(crate) fn program_occurrence(self) -> ProgramOccurrenceBindingV1 { + pub(crate) const fn program_occurrence(self) -> ProgramOccurrenceBindingV1 { self.program_occurrence } - pub(crate) fn occurrence(self) -> SourceOverCertificateV1 { + pub(crate) const fn occurrence(self) -> SourceOverCertificateV1 { self.occurrence } diff --git a/crates/labcolors-core/src/constraints/mod.rs b/crates/labcolors-core/src/constraints/mod.rs index 4aae4880..fb887337 100644 --- a/crates/labcolors-core/src/constraints/mod.rs +++ b/crates/labcolors-core/src/constraints/mod.rs @@ -155,7 +155,6 @@ impl &self.invocation } - #[cfg(test)] pub(crate) fn measurement(&self) -> &Measurement { &self.measurement } diff --git a/crates/labcolors-core/src/constraints/wcag22.rs b/crates/labcolors-core/src/constraints/wcag22.rs index c4b28a87..cc9f3a78 100644 --- a/crates/labcolors-core/src/constraints/wcag22.rs +++ b/crates/labcolors-core/src/constraints/wcag22.rs @@ -30,7 +30,6 @@ pub(crate) struct ApplicableWcag22MeasurementV1 { evidence: NumericalDecisionEvidenceV1, } -#[cfg(test)] impl ApplicableWcag22MeasurementV1 { pub(crate) const fn profile_id(&self) -> Wcag22ProfileIdV1 { self.profile_id @@ -44,6 +43,7 @@ impl ApplicableWcag22MeasurementV1 { &self.measurement } + #[cfg(test)] pub(crate) const fn decision(&self) -> Wcag22ApplicableDecisionV1 { self.decision } diff --git a/crates/labcolors-core/src/lcs_occurrence.rs b/crates/labcolors-core/src/lcs_occurrence.rs index ea6a9295..fb7f08c5 100644 --- a/crates/labcolors-core/src/lcs_occurrence.rs +++ b/crates/labcolors-core/src/lcs_occurrence.rs @@ -28,6 +28,13 @@ pub struct ColorSignal { output_profile: OutputProfileId, } +/// Exhaustive internal decomposition for boundaries that must preserve the +/// signal profile instead of treating encoded bytes as self-describing. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ColorSignalViewV1 { + Iec61966Srgb8D65(Srgb8), +} + impl ColorSignal { /// Form the only admitted encoded signal without accepting a free-form /// channel/profile pairing. @@ -45,6 +52,12 @@ impl ColorSignal { pub(crate) const fn output_profile(self) -> OutputProfileId { self.output_profile } + + pub(crate) const fn view(self) -> ColorSignalViewV1 { + match self.output_profile { + OutputProfileId::Iec61966Srgb8D65V1 => ColorSignalViewV1::Iec61966Srgb8D65(self.srgb8), + } + } } /// Exact code release for one colorimetric signal-to-tristimulus transform. @@ -321,6 +334,8 @@ fn derive_sample_with_binding( signal: ColorSignal, binding: AdmittedSrgb8TristimulusBindingV1, ) -> Result { + #[cfg(test)] + MODELED_TRISTIMULUS_DERIVATION_CALLS.with(|calls| calls.set(calls.get() + 1)); let xyz = match ( signal.output_profile(), binding.signal_output_profile(), @@ -337,9 +352,9 @@ fn derive_sample_with_binding( #[cfg(test)] thread_local! { - /// Per-thread count of modeled signal-to-tristimulus derivations. Program - /// regression tests use this deterministic metric to pin one derivation - /// per unique target occurrence and physical case without timing noise. + /// Per-thread count of modeled signal-to-tristimulus kernel executions. + /// Counting below both initial derivation and replay keeps a projection + /// from hiding recomputation behind the replay API. pub(crate) static MODELED_TRISTIMULUS_DERIVATION_CALLS: std::cell::Cell = const { std::cell::Cell::new(0) }; } @@ -352,8 +367,6 @@ thread_local! { pub(crate) fn derive_modeled_tristimulus_v1( signal: ColorSignal, ) -> Result { - #[cfg(test)] - MODELED_TRISTIMULUS_DERIVATION_CALLS.with(|calls| calls.set(calls.get() + 1)); let binding = admitted_binding(signal.output_profile()); let sample = derive_sample_with_binding(signal, binding)?; Ok(ModeledTristimulusDerivationV1 { diff --git a/crates/labcolors-core/src/observation.rs b/crates/labcolors-core/src/observation.rs index 8881c4c8..d2549f3c 100644 --- a/crates/labcolors-core/src/observation.rs +++ b/crates/labcolors-core/src/observation.rs @@ -30,6 +30,10 @@ impl ObservationStreamId { pub(crate) const fn new(raw: u32) -> Self { Self(raw) } + + pub(crate) const fn value(self) -> u32 { + self.0 + } } /// Monotonic revision inside one [`ObservationStreamId`]. @@ -54,6 +58,10 @@ impl ScenarioId { pub(crate) const fn new(raw: u32) -> Self { Self(raw) } + + pub(crate) const fn value(self) -> u32 { + self.0 + } } /// Opaque reason why the current observation is unavailable. diff --git a/crates/labcolors-core/src/package_bridge.rs b/crates/labcolors-core/src/package_bridge.rs index 16666bba..7e594b19 100644 --- a/crates/labcolors-core/src/package_bridge.rs +++ b/crates/labcolors-core/src/package_bridge.rs @@ -12,12 +12,19 @@ use core::slice; use crate::Srgb8; use crate::appearance::{OccurrenceId, OpacityInputId, PaintId, SurfaceId, SurfaceInputPortId}; +use crate::composition::CompositionProfileV1; +use crate::constraints::{ + ExactSrgb8IdentityV1, ProgramVisiblePointBindingV1, ProgramVisiblePointPassEvidence, + ProgramVisiblePointViolationEvidence, Wcag22Srgb8V1, +}; use crate::joint::FiniteJointOrderErrorV1; use crate::lcs_occurrence::{ - AdaptingLuminanceCdM2, AppearanceContextDomainErrorV1, AppearanceContextFieldV1, - AppearanceContextId, AppearanceContextSchemaReleaseId, BackgroundLuminanceRatio, ColorSignal, - IEC_SRGB_D65_XYZ_FRAME_V1, NumericDomainError, SurroundProfileId, + AdaptingLuminanceCdM2, AdmittedSrgb8TristimulusBindingV1, AppearanceContextDomainErrorV1, + AppearanceContextFieldV1, AppearanceContextId, AppearanceContextSchemaReleaseId, + BackgroundLuminanceRatio, ColorSignal, ColorSignalViewV1, IEC_SRGB_D65_XYZ_FRAME_V1, + NumericDomainError, SurroundProfileId, }; +use crate::numerics::NumericalDecisionEvidenceV1; use crate::observation::{ ObservationError, ObservationPayloadInput, ObservationStreamId, ObservationUpdateInput, Revision, ScenarioId, SchemaOrderedScenarioSourceV1, UnknownReasonId, @@ -25,14 +32,16 @@ use crate::observation::{ use crate::program_session::{ CompiledCoreProgramV1, CompositionProfile, ConstraintId, ConstraintInvocation, CoreProgramConstraintInvocationV1, CoreProgramDraftErrorV1, CoreProgramDraftV1, - CoreProgramEvaluatorErrorV1, CoreProgramEvaluatorsV1, DeclaredJointSelectionV1, - JointCandidateStateV1, Occurrence, OpacityInput, OutputBinding, OutputSlotId, Paint, - ProgramCompileError, ProgramConflictV1, ProgramOutputV1, ProgramSessionEvaluationError, - ProgramSessionInstantiateError, ProgramSessionPlan, ProgramVerifiedV1, Source, SourceId, - Surface, Target, TargetCandidateChoiceV1, TargetCandidateId, TargetCandidateV1, TargetId, + CoreProgramEvaluatorErrorV1, CoreProgramEvaluatorsV1, CoreProgramPassEvidenceV1, + CoreProgramViolationEvidenceV1, DeclaredJointSelectionV1, JointCandidateStateV1, Occurrence, + OpacityInput, OutputBinding, OutputSlotId, Paint, ProgramCompileError, ProgramConflictV1, + ProgramConstraintCellV1, ProgramConstraintResultV1, ProgramContentIdentityV1, ProgramOutputV1, + ProgramSessionEvaluationError, ProgramSessionInstantiateError, ProgramSessionPlan, + ProgramVerifiedV1, Source, SourceId, Surface, Target, TargetCandidateChoiceV1, + TargetCandidateId, TargetCandidateV1, TargetId, }; use crate::session::{Session, SessionState, SessionUpdateError}; -use crate::wcag22::Wcag22CriterionV1; +use crate::wcag22::{Wcag22CriterionV1, Wcag22LuminanceBoundsQ55V1, Wcag22ProfileIdV1}; type CoreVerifiedV1 = ProgramVerifiedV1; type CoreConflictV1 = ProgramConflictV1; @@ -40,6 +49,11 @@ type CoreProgramPlanV1 = ProgramSessionPlan; type CoreProgramSessionV1 = Session; type CoreProgramStateV1 = SessionState; type CoreProgramPlanErrorV1 = ProgramSessionEvaluationError; +type CoreProgramConstraintCellV1 = ProgramConstraintCellV1; +type CoreExactPassEvidenceV1 = ProgramVisiblePointPassEvidence; +type CoreExactViolationEvidenceV1 = ProgramVisiblePointViolationEvidence; +type CoreWcag22PassEvidenceV1 = ProgramVisiblePointPassEvidence; +type CoreWcag22ViolationEvidenceV1 = ProgramVisiblePointViolationEvidence; macro_rules! package_program_id { ($name:ident, $core:ty) => { @@ -74,6 +88,31 @@ macro_rules! package_program_id { }; } +macro_rules! package_program_projected_id { + ($name:ident, $core:ty) => { + #[repr(transparent)] + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] + #[must_use] + pub struct $name($core); + + impl $name { + const fn from_core(value: $core) -> Self { + Self(value) + } + + pub const fn value(self) -> u32 { + self.0.value() + } + } + + impl core::hash::Hash for $name { + fn hash(&self, state: &mut H) { + core::hash::Hash::hash(&self.value(), state); + } + } + }; +} + package_program_id!(PackageProgramSourceIdV1, SourceId); package_program_id!(PackageProgramTargetIdV1, TargetId); package_program_id!(PackageProgramTargetCandidateIdV1, TargetCandidateId); @@ -84,6 +123,8 @@ package_program_id!(PackageProgramSurfaceIdV1, SurfaceId); package_program_id!(PackageProgramOccurrenceIdV1, OccurrenceId); package_program_id!(PackageProgramConstraintIdV1, ConstraintId); package_program_id!(PackageProgramOutputSlotIdV1, OutputSlotId); +package_program_projected_id!(PackageProgramStreamIdV1, ObservationStreamId); +package_program_projected_id!(PackageProgramScenarioIdV1, ScenarioId); /// One finite candidate, stored as the actual Core target-candidate IR node. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -244,6 +285,22 @@ impl PackageProgramAppearanceContextV1 { surround.into_core(), ))) } + + pub fn adapting_luminance_cd_m2(self) -> f64 { + self.0.adapting_luminance_cd_m2() + } + + pub fn background_luminance_ratio_yb_yw(self) -> f64 { + self.0.background_luminance_ratio() + } + + pub const fn surround(self) -> PackageProgramSurroundV1 { + match self.0.surround_profile() { + SurroundProfileId::AverageV1 => PackageProgramSurroundV1::Average, + SurroundProfileId::DimV1 => PackageProgramSurroundV1::Dim, + SurroundProfileId::DarkV1 => PackageProgramSurroundV1::Dark, + } + } } /// Closed compile classification; the generic Core error never escapes. @@ -1168,7 +1225,7 @@ impl<'a> PackageProgramStateViewV1<'a> { /// Core-owned certificates in canonical same-call ordinal order. pub fn certificates( self, - ) -> impl ExactSizeIterator> + 'a { + ) -> impl ExactSizeIterator> + FusedIterator + 'a { let (first, second) = match self.state { SessionState::Waiting => (None, None), SessionState::Ready { current } | SessionState::Stale { previous: current } => { @@ -1183,7 +1240,9 @@ impl<'a> PackageProgramStateViewV1<'a> { } /// Total canonical output projection for this lifecycle state. - pub fn operations(self) -> impl ExactSizeIterator + 'a { + pub fn operations( + self, + ) -> impl ExactSizeIterator> + FusedIterator + 'a { let inner = match self.state { SessionState::Waiting => PackageProgramOperationSourceV1::Empty, SessionState::Ready { current } => { @@ -1195,17 +1254,21 @@ impl<'a> PackageProgramStateViewV1<'a> { .zip(self.output_slots) .all(|(output, slot)| output.output().value() == slot.value()) ); - PackageProgramOperationSourceV1::Set(current.outputs().iter()) + PackageProgramOperationSourceV1::Set { + outputs: current.outputs().iter(), + certificate: PackageProgramVerifiedCertificateV1 { inner: current }, + } } - SessionState::Stale { .. } => PackageProgramOperationSourceV1::Hold { + SessionState::Stale { previous } => PackageProgramOperationSourceV1::Hold { slots: self.output_slots.iter(), - certificate_index: 0, + certificate: PackageProgramVerifiedCertificateV1 { inner: previous }, }, SessionState::Failed { - previous: Some(_), .. + previous: Some(previous), + .. } => PackageProgramOperationSourceV1::Hold { slots: self.output_slots.iter(), - certificate_index: 1, + certificate: PackageProgramVerifiedCertificateV1 { inner: previous }, }, SessionState::Failed { previous: None, .. } => { PackageProgramOperationSourceV1::Remove(self.output_slots.iter()) @@ -1215,91 +1278,661 @@ impl<'a> PackageProgramStateViewV1<'a> { } } -/// Opaque certificate family; evaluator-specific evidence never escapes. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum PackageProgramCertificateKindV1 { - Verified, - Conflict, +/// Collision-resistant address of the canonical physical Program content. +/// It deliberately does not identify an owner epoch or runtime authority. +#[repr(transparent)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct PackageProgramContentIdentityV1([u8; 32]); + +impl PackageProgramContentIdentityV1 { + const fn from_core(value: ProgramContentIdentityV1) -> Self { + Self(*value.as_bytes()) + } + + pub const fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } } +/// All hard cells passed over the complete admitted physical support. #[derive(Clone, Copy)] -enum PackageProgramCertificateRefV1<'a> { - Verified(&'a CoreVerifiedV1), - Conflict(&'a CoreConflictV1), +pub struct PackageProgramVerifiedCertificateV1<'a> { + inner: &'a CoreVerifiedV1, } -/// Borrowed opaque handle to one Core-owned certificate. +impl<'a> PackageProgramVerifiedCertificateV1<'a> { + pub const fn content_identity(self) -> PackageProgramContentIdentityV1 { + PackageProgramContentIdentityV1::from_core(self.inner.report().content_identity()) + } + + pub const fn observation(self) -> PackageProgramObservationV1<'a> { + PackageProgramObservationV1 { + inner: self.inner.report().observation(), + } + } + + pub const fn selected_state_index(self) -> Option { + self.inner.selected_state_index() + } + + pub fn cells( + self, + ) -> impl ExactSizeIterator> + FusedIterator + 'a { + self.inner + .report() + .cells() + .iter() + .map(PackageProgramVerifiedCellV1::from_core) + } + + pub fn outputs( + self, + ) -> impl ExactSizeIterator> + FusedIterator + 'a + { + self.inner + .outputs() + .iter() + .map(PackageProgramCertifiedOutputV1::from_core) + } +} + +/// Exhaustive proof that every declared candidate state violates a hard cell. #[derive(Clone, Copy)] -pub struct PackageProgramCertificateV1<'a> { - inner: PackageProgramCertificateRefV1<'a>, +pub struct PackageProgramConflictCertificateV1<'a> { + inner: &'a CoreConflictV1, +} + +impl<'a> PackageProgramConflictCertificateV1<'a> { + pub const fn content_identity(self) -> PackageProgramContentIdentityV1 { + PackageProgramContentIdentityV1::from_core(self.inner.report().content_identity()) + } + + pub const fn observation(self) -> PackageProgramObservationV1<'a> { + PackageProgramObservationV1 { + inner: self.inner.report().observation(), + } + } + + pub const fn considered_state_count(self) -> usize { + self.inner.considered_state_count() + } + + pub fn cells( + self, + ) -> impl ExactSizeIterator> + FusedIterator + 'a { + self.inner + .report() + .cells() + .iter() + .map(PackageProgramConflictCellV1::from_core) + } +} + +/// Closed borrowed projection of one exact Core-owned certificate. +#[derive(Clone, Copy)] +pub enum PackageProgramCertificateV1<'a> { + Verified(PackageProgramVerifiedCertificateV1<'a>), + Conflict(PackageProgramConflictCertificateV1<'a>), } impl<'a> PackageProgramCertificateV1<'a> { const fn verified(value: &'a CoreVerifiedV1) -> Self { - Self { - inner: PackageProgramCertificateRefV1::Verified(value), - } + Self::Verified(PackageProgramVerifiedCertificateV1 { inner: value }) } const fn conflict(value: &'a CoreConflictV1) -> Self { - Self { - inner: PackageProgramCertificateRefV1::Conflict(value), + Self::Conflict(PackageProgramConflictCertificateV1 { inner: value }) + } + + pub const fn content_identity(self) -> PackageProgramContentIdentityV1 { + match self { + Self::Verified(value) => value.content_identity(), + Self::Conflict(value) => value.content_identity(), + } + } + + pub const fn observation(self) -> PackageProgramObservationV1<'a> { + match self { + Self::Verified(value) => value.observation(), + Self::Conflict(value) => value.observation(), + } + } + + #[cfg(test)] + pub(crate) fn observation_backing_ptr_for_test(self) -> *const () { + self.observation().inner.backing_ptr_for_test() + } +} + +/// The exact revision-bound observation retained by a certificate. +#[derive(Clone, Copy)] +pub struct PackageProgramObservationV1<'a> { + inner: &'a crate::observation::RevisionBoundObservationV1, +} + +impl<'a> PackageProgramObservationV1<'a> { + pub const fn stream(self) -> PackageProgramStreamIdV1 { + PackageProgramStreamIdV1::from_core(self.inner.stream()) + } + + pub const fn revision(self) -> u64 { + self.inner.revision().value() + } + + /// Canonical schema shared by every physical case. Position `i` is the + /// identity of position `i` in each [`PackageProgramPhysicalCaseV1::values`] + /// iterator; the two exact-size iterators always have equal length. + pub fn surface_input_ports( + self, + ) -> impl ExactSizeIterator + FusedIterator + 'a + { + self.inner + .schema() + .iter() + .copied() + .map(PackageProgramSurfaceInputPortIdV1::from_core) + } + + /// Canonical unique physical value vectors. Each case is schema-ordered by + /// [`Self::surface_input_ports`]; scenario IDs remain in `provenance()`. + pub fn physical_cases( + self, + ) -> impl ExactSizeIterator> + FusedIterator + 'a { + (0..self.inner.physical_case_count()).map(move |index| PackageProgramPhysicalCaseV1 { + observation: self.inner, + index, + }) + } +} + +/// Closed encoded signal family retained in a physical observation case. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PackageProgramSignalV1 { + Iec61966Srgb8D65(Srgb8), +} + +/// One canonical physical observation case and its complete provenance set. +#[derive(Clone, Copy)] +pub struct PackageProgramPhysicalCaseV1<'a> { + observation: &'a crate::observation::RevisionBoundObservationV1, + index: usize, +} + +impl<'a> PackageProgramPhysicalCaseV1<'a> { + pub fn values( + self, + ) -> impl ExactSizeIterator + FusedIterator + 'a { + self.observation + .physical_values(self.index) + .expect("package case originates from the same observation") + .iter() + .copied() + .map(|signal| match signal.view() { + ColorSignalViewV1::Iec61966Srgb8D65(value) => { + PackageProgramSignalV1::Iec61966Srgb8D65(value) + } + }) + } + + pub fn provenance( + self, + ) -> impl ExactSizeIterator + FusedIterator + 'a { + self.observation + .provenance(self.index) + .expect("package case originates from the same observation") + .iter() + .copied() + .map(PackageProgramScenarioIdV1::from_core) + } +} + +/// Whether one constraint cell gates selection or is retained for reporting. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PackageProgramConstraintModeV1 { + Hard, + ReportOnly, +} + +/// One selected/fixed case × constraint cell; state is owned by its certificate. +#[derive(Clone, Copy)] +pub struct PackageProgramVerifiedCellV1<'a> { + inner: &'a CoreProgramConstraintCellV1, +} + +impl<'a> PackageProgramVerifiedCellV1<'a> { + const fn from_core(inner: &'a CoreProgramConstraintCellV1) -> Self { + Self { inner } + } + + pub const fn case_index(self) -> usize { + self.inner.case_index() + } + + pub const fn constraint(self) -> PackageProgramConstraintIdV1 { + PackageProgramConstraintIdV1::from_core(self.inner.constraint()) + } + + pub const fn occurrence(self) -> PackageProgramOccurrenceIdV1 { + PackageProgramOccurrenceIdV1::from_core(self.inner.target()) + } + + pub const fn mode(self) -> PackageProgramConstraintModeV1 { + project_constraint_mode(self.inner) + } + + pub fn assessment(self) -> PackageProgramAssessmentV1<'a> { + project_assessment(self.inner) + } +} + +/// One exhaustive candidate-state × case × constraint conflict cell. +#[derive(Clone, Copy)] +pub struct PackageProgramConflictCellV1<'a> { + inner: &'a CoreProgramConstraintCellV1, +} + +impl<'a> PackageProgramConflictCellV1<'a> { + const fn from_core(inner: &'a CoreProgramConstraintCellV1) -> Self { + Self { inner } + } + + pub const fn state_index(self) -> usize { + self.inner.candidate_state_index() + } + + pub const fn case_index(self) -> usize { + self.inner.case_index() + } + + pub const fn constraint(self) -> PackageProgramConstraintIdV1 { + PackageProgramConstraintIdV1::from_core(self.inner.constraint()) + } + + pub const fn occurrence(self) -> PackageProgramOccurrenceIdV1 { + PackageProgramOccurrenceIdV1::from_core(self.inner.target()) + } + + pub const fn mode(self) -> PackageProgramConstraintModeV1 { + project_constraint_mode(self.inner) + } + + pub fn assessment(self) -> PackageProgramAssessmentV1<'a> { + project_assessment(self.inner) + } +} + +const fn project_constraint_mode( + cell: &CoreProgramConstraintCellV1, +) -> PackageProgramConstraintModeV1 { + if cell.is_hard() { + PackageProgramConstraintModeV1::Hard + } else { + PackageProgramConstraintModeV1::ReportOnly + } +} + +fn project_assessment(cell: &CoreProgramConstraintCellV1) -> PackageProgramAssessmentV1<'_> { + match cell.result() { + ProgramConstraintResultV1::Pass(CoreProgramPassEvidenceV1::ExactSrgb8(evidence)) => { + PackageProgramAssessmentV1::ExactSrgb8(PackageProgramExactSrgb8EvidenceV1 { + inner: PackageProgramExactSrgb8EvidenceRefV1::Pass(evidence), + }) + } + ProgramConstraintResultV1::Violation(CoreProgramViolationEvidenceV1::ExactSrgb8( + evidence, + )) => PackageProgramAssessmentV1::ExactSrgb8(PackageProgramExactSrgb8EvidenceV1 { + inner: PackageProgramExactSrgb8EvidenceRefV1::Violation(evidence), + }), + ProgramConstraintResultV1::Pass(CoreProgramPassEvidenceV1::Wcag22Srgb8(evidence)) => { + PackageProgramAssessmentV1::Wcag22Srgb8(PackageProgramWcag22Srgb8EvidenceV1 { + inner: PackageProgramWcag22Srgb8EvidenceRefV1::Pass(evidence), + }) + } + ProgramConstraintResultV1::Violation(CoreProgramViolationEvidenceV1::Wcag22Srgb8( + evidence, + )) => PackageProgramAssessmentV1::Wcag22Srgb8(PackageProgramWcag22Srgb8EvidenceV1 { + inner: PackageProgramWcag22Srgb8EvidenceRefV1::Violation(evidence), + }), + } +} + +/// Stored evaluator family. Its sealed witness retains the incompatible verdict. +#[derive(Clone, Copy)] +pub enum PackageProgramAssessmentV1<'a> { + ExactSrgb8(PackageProgramExactSrgb8EvidenceV1<'a>), + Wcag22Srgb8(PackageProgramWcag22Srgb8EvidenceV1<'a>), +} + +impl<'a> PackageProgramAssessmentV1<'a> { + pub const fn verdict(self) -> PackageProgramVerdictV1 { + match self { + Self::ExactSrgb8(value) => value.verdict(), + Self::Wcag22Srgb8(value) => value.verdict(), + } + } + + pub fn binding(self) -> PackageProgramPointBindingV1<'a> { + match self { + Self::ExactSrgb8(value) => value.binding(), + Self::Wcag22Srgb8(value) => value.binding(), } } +} + +/// Incompatible stored classifier outcomes. Clients cannot construct evidence. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PackageProgramVerdictV1 { + Pass, + Violation, +} - pub const fn kind(self) -> PackageProgramCertificateKindV1 { +#[derive(Clone, Copy)] +enum PackageProgramExactSrgb8EvidenceRefV1<'a> { + Pass(&'a CoreExactPassEvidenceV1), + Violation(&'a CoreExactViolationEvidenceV1), +} + +/// Exact-sRGB8 expected value plus retained physical composition and modeled +/// tristimulus/context. +#[derive(Clone, Copy)] +pub struct PackageProgramExactSrgb8EvidenceV1<'a> { + inner: PackageProgramExactSrgb8EvidenceRefV1<'a>, +} + +impl<'a> PackageProgramExactSrgb8EvidenceV1<'a> { + pub const fn verdict(self) -> PackageProgramVerdictV1 { match self.inner { - PackageProgramCertificateRefV1::Verified(_) => { - PackageProgramCertificateKindV1::Verified + PackageProgramExactSrgb8EvidenceRefV1::Pass(_) => PackageProgramVerdictV1::Pass, + PackageProgramExactSrgb8EvidenceRefV1::Violation(_) => { + PackageProgramVerdictV1::Violation } - PackageProgramCertificateRefV1::Conflict(_) => { - PackageProgramCertificateKindV1::Conflict + } + } + + pub fn expected(self) -> Srgb8 { + match self.inner { + PackageProgramExactSrgb8EvidenceRefV1::Pass(value) => value.target(), + PackageProgramExactSrgb8EvidenceRefV1::Violation(value) => value.target(), + } + } + + pub fn binding(self) -> PackageProgramPointBindingV1<'a> { + let value = match self.inner { + PackageProgramExactSrgb8EvidenceRefV1::Pass(value) => value.binding(), + PackageProgramExactSrgb8EvidenceRefV1::Violation(value) => value.binding(), + }; + PackageProgramPointBindingV1 { inner: value } + } +} + +#[derive(Clone, Copy)] +enum PackageProgramWcag22Srgb8EvidenceRefV1<'a> { + Pass(&'a CoreWcag22PassEvidenceV1), + Violation(&'a CoreWcag22ViolationEvidenceV1), +} + +/// WCAG 2.2 profile, criterion, luminance evidence, physical composition and +/// modeled tristimulus/context retained by the Core report. +#[derive(Clone, Copy)] +pub struct PackageProgramWcag22Srgb8EvidenceV1<'a> { + inner: PackageProgramWcag22Srgb8EvidenceRefV1<'a>, +} + +impl<'a> PackageProgramWcag22Srgb8EvidenceV1<'a> { + pub const fn verdict(self) -> PackageProgramVerdictV1 { + match self.inner { + PackageProgramWcag22Srgb8EvidenceRefV1::Pass(_) => PackageProgramVerdictV1::Pass, + PackageProgramWcag22Srgb8EvidenceRefV1::Violation(_) => { + PackageProgramVerdictV1::Violation } } } - /// Revision bound into this exact evidence object. - pub const fn revision(self) -> u64 { - let revision = match self.inner { - PackageProgramCertificateRefV1::Verified(value) => { - value.report().observation().revision() + pub fn profile_id(self) -> Wcag22ProfileIdV1 { + match self.inner { + PackageProgramWcag22Srgb8EvidenceRefV1::Pass(value) => { + value.measurement().value().profile_id() } - PackageProgramCertificateRefV1::Conflict(value) => { - value.report().observation().revision() + PackageProgramWcag22Srgb8EvidenceRefV1::Violation(value) => { + value.measurement().value().profile_id() + } + } + } + + pub fn criterion(self) -> Wcag22CriterionV1 { + match self.inner { + PackageProgramWcag22Srgb8EvidenceRefV1::Pass(value) => { + value.measurement().value().criterion() + } + PackageProgramWcag22Srgb8EvidenceRefV1::Violation(value) => { + value.measurement().value().criterion() + } + } + } + + pub fn foreground_luminance(self) -> Wcag22LuminanceBoundsQ55V1 { + let measurement = match self.inner { + PackageProgramWcag22Srgb8EvidenceRefV1::Pass(value) => { + value.measurement().value().measurement() + } + PackageProgramWcag22Srgb8EvidenceRefV1::Violation(value) => { + value.measurement().value().measurement() } }; - revision.value() + measurement.foreground_luminance } - #[cfg(test)] - pub(crate) fn observation_backing_ptr_for_test(self) -> *const () { + pub fn background_luminance(self) -> Wcag22LuminanceBoundsQ55V1 { + let measurement = match self.inner { + PackageProgramWcag22Srgb8EvidenceRefV1::Pass(value) => { + value.measurement().value().measurement() + } + PackageProgramWcag22Srgb8EvidenceRefV1::Violation(value) => { + value.measurement().value().measurement() + } + }; + measurement.background_luminance + } + + pub fn numerical_evidence(self) -> &'a NumericalDecisionEvidenceV1 { match self.inner { - PackageProgramCertificateRefV1::Verified(value) => { - value.report().observation().backing_ptr_for_test() + PackageProgramWcag22Srgb8EvidenceRefV1::Pass(value) => { + value.measurement().value().evidence() + } + PackageProgramWcag22Srgb8EvidenceRefV1::Violation(value) => { + value.measurement().value().evidence() + } + } + } + + pub fn binding(self) -> PackageProgramPointBindingV1<'a> { + let value = match self.inner { + PackageProgramWcag22Srgb8EvidenceRefV1::Pass(value) => value.binding(), + PackageProgramWcag22Srgb8EvidenceRefV1::Violation(value) => value.binding(), + }; + PackageProgramPointBindingV1 { inner: value } + } +} + +/// Retained physical composition and modeled tristimulus/context shared by an +/// evaluator witness. +#[derive(Clone, Copy)] +pub struct PackageProgramPointBindingV1<'a> { + inner: &'a ProgramVisiblePointBindingV1, +} + +impl<'a> PackageProgramPointBindingV1<'a> { + pub const fn physical(self) -> PackageProgramPhysicalPointV1<'a> { + match self.inner.physical().occurrence().profile() { + CompositionProfileV1::EncodedSrgb8SourceOverV1 => { + PackageProgramPhysicalPointV1::EncodedSrgb8SourceOver( + PackageProgramEncodedSrgb8SourceOverV1 { inner: self.inner }, + ) } - PackageProgramCertificateRefV1::Conflict(value) => { - value.report().observation().backing_ptr_for_test() + } + } + + pub const fn modeled(self) -> PackageProgramModeledPointV1<'a> { + match self.inner.modeled_lcs().provenance().binding() { + AdmittedSrgb8TristimulusBindingV1::Iec61966Srgb8ToCie1931TwoDegreeXyzD65RelativeY1V1 => { + PackageProgramModeledPointV1::Iec61966Srgb8ToCie1931TwoDegreeXyzD65RelativeY1( + PackageProgramModeledTristimulusV1 { inner: self.inner }, + ) } } } } +/// Closed exact physical-composition family. +#[derive(Clone, Copy)] +pub enum PackageProgramPhysicalPointV1<'a> { + EncodedSrgb8SourceOver(PackageProgramEncodedSrgb8SourceOverV1<'a>), +} + +#[derive(Clone, Copy)] +pub struct PackageProgramEncodedSrgb8SourceOverV1<'a> { + inner: &'a ProgramVisiblePointBindingV1, +} + +impl PackageProgramEncodedSrgb8SourceOverV1<'_> { + pub const fn subject_paint(self) -> PackageProgramPaintIdV1 { + PackageProgramPaintIdV1::from_core(self.inner.physical().program_occurrence().subject()) + } + + pub const fn backdrop_surface(self) -> PackageProgramSurfaceIdV1 { + PackageProgramSurfaceIdV1::from_core( + self.inner + .physical() + .program_occurrence() + .backdrop_surface(), + ) + } + + pub const fn subject(self) -> Srgb8 { + Srgb8::new(self.inner.physical().occurrence().subject_rgb()) + } + + pub const fn opacity(self) -> f64 { + f64::from_bits(self.inner.physical().occurrence().subject_opacity_bits()) + } + + pub const fn backdrop(self) -> Srgb8 { + Srgb8::new(self.inner.physical().occurrence().backdrop_rgb()) + } + + pub const fn visible(self) -> Srgb8 { + Srgb8::new(self.inner.physical().occurrence().output_rgb()) + } +} + +/// Closed modeled-tristimulus provenance family. +#[derive(Clone, Copy)] +pub enum PackageProgramModeledPointV1<'a> { + Iec61966Srgb8ToCie1931TwoDegreeXyzD65RelativeY1(PackageProgramModeledTristimulusV1<'a>), +} + +#[derive(Clone, Copy)] +pub struct PackageProgramModeledTristimulusV1<'a> { + inner: &'a ProgramVisiblePointBindingV1, +} + +impl PackageProgramModeledTristimulusV1<'_> { + pub fn xyz(self) -> [f64; 3] { + self.inner.modeled_lcs().derivation().sample().xyz() + } + + pub const fn appearance_context(self) -> PackageProgramAppearanceContextV1 { + PackageProgramAppearanceContextV1(self.inner.modeled_lcs().occurrence().context()) + } +} + +/// One Core-certified output Paint. No output exists for Conflict. +#[derive(Clone, Copy)] +pub struct PackageProgramCertifiedOutputV1<'a> { + inner: &'a ProgramOutputV1, +} + +impl<'a> PackageProgramCertifiedOutputV1<'a> { + const fn from_core(inner: &'a ProgramOutputV1) -> Self { + Self { inner } + } + + pub const fn output_slot(self) -> PackageProgramOutputSlotIdV1 { + PackageProgramOutputSlotIdV1::from_core((*self.inner).output()) + } + + pub const fn paint(self) -> PackageProgramPaintIdV1 { + PackageProgramPaintIdV1::from_core((*self.inner).paint().id()) + } + + pub const fn source(self) -> Srgb8 { + (*self.inner).paint().source() + } + + pub const fn opacity(self) -> f64 { + (*self.inner).paint().opacity().value() + } +} + +/// A Set operation is structurally tied to the exact Verified certificate. +#[derive(Clone, Copy)] +pub struct PackageProgramSetV1<'a> { + output: &'a ProgramOutputV1, + certificate: PackageProgramVerifiedCertificateV1<'a>, +} + +impl<'a> PackageProgramSetV1<'a> { + pub const fn output_slot(self) -> PackageProgramOutputSlotIdV1 { + PackageProgramOutputSlotIdV1::from_core((*self.output).output()) + } + + pub const fn source(self) -> Srgb8 { + (*self.output).paint().source() + } + + pub const fn opacity(self) -> f64 { + (*self.output).paint().opacity().value() + } + + pub const fn certificate(self) -> PackageProgramVerifiedCertificateV1<'a> { + self.certificate + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PackageProgramRemoveV1 { + output_slot: PackageProgramOutputSlotIdV1, +} + +impl PackageProgramRemoveV1 { + pub const fn output_slot(self) -> PackageProgramOutputSlotIdV1 { + self.output_slot + } +} + +/// A Hold operation is structurally tied to the retained Verified certificate. +#[derive(Clone, Copy)] +pub struct PackageProgramHoldV1<'a> { + output_slot: PackageProgramOutputSlotIdV1, + certificate: PackageProgramVerifiedCertificateV1<'a>, +} + +impl<'a> PackageProgramHoldV1<'a> { + pub const fn output_slot(self) -> PackageProgramOutputSlotIdV1 { + self.output_slot + } + + pub const fn certificate(self) -> PackageProgramVerifiedCertificateV1<'a> { + self.certificate + } +} + /// Closed total operation union over opaque output slots. -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum PackageProgramOperationV1 { - Set { - output_slot: PackageProgramOutputSlotIdV1, - source: Srgb8, - opacity: f64, - certificate_index: usize, - }, - Remove { - output_slot: PackageProgramOutputSlotIdV1, - }, - Hold { - output_slot: PackageProgramOutputSlotIdV1, - certificate_index: usize, - }, +#[derive(Clone, Copy)] +pub enum PackageProgramOperationV1<'a> { + Set(PackageProgramSetV1<'a>), + Remove(PackageProgramRemoveV1), + Hold(PackageProgramHoldV1<'a>), } struct PackageProgramCertificatesV1<'a> { @@ -1346,10 +1979,13 @@ impl FusedIterator for PackageProgramCertificatesV1<'_> {} enum PackageProgramOperationSourceV1<'a> { Empty, - Set(slice::Iter<'a, ProgramOutputV1>), + Set { + outputs: slice::Iter<'a, ProgramOutputV1>, + certificate: PackageProgramVerifiedCertificateV1<'a>, + }, Hold { slots: slice::Iter<'a, PackageProgramOutputSlotIdV1>, - certificate_index: usize, + certificate: PackageProgramVerifiedCertificateV1<'a>, }, Remove(slice::Iter<'a, PackageProgramOutputSlotIdV1>), } @@ -1358,33 +1994,32 @@ struct PackageProgramOperationsV1<'a> { inner: PackageProgramOperationSourceV1<'a>, } -impl Iterator for PackageProgramOperationsV1<'_> { - type Item = PackageProgramOperationV1; +impl<'a> Iterator for PackageProgramOperationsV1<'a> { + type Item = PackageProgramOperationV1<'a>; fn next(&mut self) -> Option { match &mut self.inner { PackageProgramOperationSourceV1::Empty => None, - PackageProgramOperationSourceV1::Set(outputs) => { - let output = *outputs.next()?; - let paint = output.paint(); - Some(PackageProgramOperationV1::Set { - output_slot: PackageProgramOutputSlotIdV1::from_core(output.output()), - source: paint.source(), - opacity: paint.opacity().value(), - certificate_index: 0, - }) - } - PackageProgramOperationSourceV1::Hold { - slots, - certificate_index, - } => Some(PackageProgramOperationV1::Hold { - output_slot: *slots.next()?, - certificate_index: *certificate_index, - }), + PackageProgramOperationSourceV1::Set { + outputs, + certificate, + } => { + let output = outputs.next()?; + Some(PackageProgramOperationV1::Set(PackageProgramSetV1 { + output, + certificate: *certificate, + })) + } + PackageProgramOperationSourceV1::Hold { slots, certificate } => { + Some(PackageProgramOperationV1::Hold(PackageProgramHoldV1 { + output_slot: *slots.next()?, + certificate: *certificate, + })) + } PackageProgramOperationSourceV1::Remove(slots) => { - Some(PackageProgramOperationV1::Remove { + Some(PackageProgramOperationV1::Remove(PackageProgramRemoveV1 { output_slot: *slots.next()?, - }) + })) } } } @@ -1392,7 +2027,7 @@ impl Iterator for PackageProgramOperationsV1<'_> { fn size_hint(&self) -> (usize, Option) { let remaining = match &self.inner { PackageProgramOperationSourceV1::Empty => 0, - PackageProgramOperationSourceV1::Set(outputs) => outputs.len(), + PackageProgramOperationSourceV1::Set { outputs, .. } => outputs.len(), PackageProgramOperationSourceV1::Hold { slots, .. } | PackageProgramOperationSourceV1::Remove(slots) => slots.len(), }; diff --git a/crates/labcolors-core/src/program_mixed_evaluator_tests.rs b/crates/labcolors-core/src/program_mixed_evaluator_tests.rs index 7b30e193..7a3a91a7 100644 --- a/crates/labcolors-core/src/program_mixed_evaluator_tests.rs +++ b/crates/labcolors-core/src/program_mixed_evaluator_tests.rs @@ -1,28 +1,36 @@ +use core::iter::FusedIterator; + use crate::Srgb8; -use crate::appearance::{OccurrenceId, PaintId, SurfaceId, SurfaceInputPortId}; +use crate::appearance::{OccurrenceId, OpacityInputId, PaintId, SurfaceId, SurfaceInputPortId}; use crate::constraints::{ ExactConstraintIdentityV1, ExactIdentityCapabilityV1, ExactIdentityReleaseV1, + ProgramVisiblePointBindingV1, }; use crate::lcs_occurrence::{ AdaptingLuminanceCdM2, AppearanceContextId, AppearanceContextSchemaReleaseId, - BackgroundLuminanceRatio, ColorSignal, IEC_SRGB_D65_XYZ_FRAME_V1, SurroundProfileId, + BackgroundLuminanceRatio, ColorSignal, IEC_SRGB_D65_XYZ_FRAME_V1, + MODELED_TRISTIMULUS_DERIVATION_CALLS, SurroundProfileId, }; use crate::observation::{ ObservationGroupId, ObservationPayloadInput, ObservationStreamId, ObservationUpdateInput, ObservedScenarioSetInput, Revision, ScenarioId, ScenarioInput, SurfaceInputBinding, }; use crate::package_bridge::{ - PackageProgramCertificateKindV1, PackageProgramOperationV1, PackageProgramOutputSlotIdV1, - PackageProgramOwnerV1, PackageProgramScenarioV1, PackageProgramStateKindV1, - PackageProgramUpdateErrorKindV1, PackageProgramUpdateV1, + PackageProgramAssessmentV1, PackageProgramCertificateV1, PackageProgramConflictCellV1, + PackageProgramModeledPointV1, PackageProgramObservationV1, PackageProgramOperationV1, + PackageProgramOutputSlotIdV1, PackageProgramOwnerV1, PackageProgramPhysicalPointV1, + PackageProgramScenarioV1, PackageProgramSignalV1, PackageProgramStateKindV1, + PackageProgramStateViewV1, PackageProgramSurroundV1, PackageProgramUpdateErrorKindV1, + PackageProgramUpdateV1, PackageProgramVerdictV1, PackageProgramVerifiedCellV1, }; use crate::program_session::{ - CompiledCoreProgramV1, CompositionProfile, ConstraintId, ConstraintInvocation, ConstraintSet, - CoreProgramConstraintInvocationV1, CoreProgramEvaluatorsV1, CoreProgramPassEvidenceV1, - CoreProgramV1, CoreProgramViolationEvidenceV1, DeclaredJointSelectionV1, JointCandidateStateV1, - ObservationGroup, Occurrence, OutputBinding, OutputSlotId, Paint, Program, - ProgramConstraintResultV1, Source, SourceId, Surface, Target, TargetCandidateChoiceV1, - TargetCandidateId, TargetCandidateV1, TargetId, + CORE_PROGRAM_ASSESSMENT_CALLS, CompiledCoreProgramV1, CompositionProfile, ConstraintId, + ConstraintInvocation, ConstraintSet, CoreProgramConstraintInvocationV1, + CoreProgramEvaluatorsV1, CoreProgramPassEvidenceV1, CoreProgramV1, + CoreProgramViolationEvidenceV1, DeclaredJointSelectionV1, JointCandidateStateV1, + ObservationGroup, Occurrence, OpacityInput, OutputBinding, OutputSlotId, Paint, Program, + ProgramConstraintCellV1, ProgramConstraintResultV1, Source, SourceId, Surface, Target, + TargetCandidateChoiceV1, TargetCandidateId, TargetCandidateV1, TargetId, }; use crate::session::SessionState; use crate::wcag22::{Wcag22CriterionV1, wcag22_profile_v1}; @@ -137,6 +145,523 @@ fn finite_program(candidate_signals: [[u8; 3]; 2]) -> CompiledCoreProgramV1 { .unwrap() } +fn fixed_translucent_program() -> CompiledCoreProgramV1 { + const OPACITY: OpacityInputId = OpacityInputId::new(12); + const TRANSLUCENT_PAINT: PaintId = PaintId::new(13); + Program::new( + vec![Source::new(SOURCE, signal([0; 3]))], + vec![Target::fixed(TARGET, SOURCE)], + ObservationGroup::new(GROUP, vec![SURFACE_PORT]), + vec![OpacityInput::new(OPACITY, 0.5)], + vec![ + Paint::Solid { + id: PAINT, + target: TARGET, + }, + Paint::Opacity { + id: TRANSLUCENT_PAINT, + source: PAINT, + opacity: OPACITY, + }, + ], + vec![Surface::Input { + id: SURFACE, + input: SURFACE_PORT, + }], + vec![Occurrence::new( + OCCURRENCE, + TRANSLUCENT_PAINT, + SURFACE, + CompositionProfile::EncodedSrgb8SourceOverV1, + context(), + )], + ConstraintSet::new( + vec![ConstraintInvocation::hard( + EXACT_CONSTRAINT, + OCCURRENCE, + CoreProgramConstraintInvocationV1::ExactSrgb8(Srgb8::new([0x80; 3])), + )], + vec![], + ), + vec![OutputBinding::new(OUTPUT, TRANSLUCENT_PAINT)], + CoreProgramEvaluatorsV1, + ) + .compile() + .unwrap() +} + +fn assert_package_observation_matches_core( + package: PackageProgramObservationV1<'_>, + core: &crate::observation::RevisionBoundObservationV1, +) { + assert_eq!(package.stream().value(), core.stream().value()); + assert_eq!(package.revision(), core.revision().value()); + assert_eq!( + package + .surface_input_ports() + .map(|port| port.value()) + .collect::>(), + core.schema() + .iter() + .map(|port| port.value()) + .collect::>(), + ); + + let package_cases = package + .physical_cases() + .map(|case| { + let values = case + .values() + .map(|value| match value { + PackageProgramSignalV1::Iec61966Srgb8D65(value) => value, + }) + .collect::>(); + let provenance = case + .provenance() + .map(|scenario| scenario.value()) + .collect::>(); + (values, provenance) + }) + .collect::>(); + let core_cases = (0..core.physical_case_count()) + .map(|case_index| { + let values = core + .physical_values(case_index) + .unwrap() + .iter() + .map(|signal| signal.srgb8()) + .collect::>(); + let provenance = core + .provenance(case_index) + .unwrap() + .iter() + .map(|scenario| scenario.value()) + .collect::>(); + (values, provenance) + }) + .collect::>(); + assert_eq!(package_cases, core_cases); +} + +fn assert_package_binding_matches_core( + package: PackageProgramAssessmentV1<'_>, + core: &ProgramVisiblePointBindingV1, + expected_occurrence: OccurrenceId, +) -> (Srgb8, Srgb8) { + let core_physical = core.physical(); + let core_occurrence = core_physical.occurrence(); + let core_program_occurrence = core_physical.program_occurrence(); + assert_eq!(core_program_occurrence.occurrence(), expected_occurrence); + let PackageProgramPhysicalPointV1::EncodedSrgb8SourceOver(package_physical) = + package.binding().physical(); + assert_eq!( + package_physical.subject_paint().value(), + core_program_occurrence.subject().value() + ); + assert_eq!( + package_physical.backdrop_surface().value(), + core_program_occurrence.backdrop_surface().value() + ); + assert_eq!( + package_physical.subject(), + Srgb8::new(core_occurrence.subject_rgb()) + ); + assert_eq!( + package_physical.opacity().to_bits(), + core_occurrence.subject_opacity_bits() + ); + assert_eq!( + package_physical.backdrop(), + Srgb8::new(core_occurrence.backdrop_rgb()) + ); + assert_eq!( + package_physical.visible(), + Srgb8::new(core_occurrence.output_rgb()) + ); + + let PackageProgramModeledPointV1::Iec61966Srgb8ToCie1931TwoDegreeXyzD65RelativeY1( + package_modeled, + ) = package.binding().modeled(); + assert_eq!( + package_modeled.xyz().map(f64::to_bits), + core.modeled_lcs() + .derivation() + .sample() + .xyz() + .map(f64::to_bits) + ); + let package_context = package_modeled.appearance_context(); + let core_context = core.modeled_lcs().occurrence().context(); + assert_eq!( + package_context.adapting_luminance_cd_m2().to_bits(), + core_context.adapting_luminance_cd_m2().to_bits() + ); + assert_eq!( + package_context.background_luminance_ratio_yb_yw().to_bits(), + core_context.background_luminance_ratio().to_bits() + ); + let core_surround = match core_context.surround_profile() { + SurroundProfileId::AverageV1 => PackageProgramSurroundV1::Average, + SurroundProfileId::DimV1 => PackageProgramSurroundV1::Dim, + SurroundProfileId::DarkV1 => PackageProgramSurroundV1::Dark, + }; + assert_eq!(package_context.surround(), core_surround); + + (package_physical.visible(), package_physical.backdrop()) +} + +fn assert_package_assessment_matches_core( + package: PackageProgramAssessmentV1<'_>, + core: &ProgramConstraintResultV1, + expected_occurrence: OccurrenceId, +) { + match (package, core) { + ( + PackageProgramAssessmentV1::ExactSrgb8(package), + ProgramConstraintResultV1::Pass(CoreProgramPassEvidenceV1::ExactSrgb8(core)), + ) => { + assert_eq!(package.verdict(), PackageProgramVerdictV1::Pass); + assert_eq!(package.expected(), core.target()); + let (visible, _) = assert_package_binding_matches_core( + PackageProgramAssessmentV1::ExactSrgb8(package), + core.binding(), + expected_occurrence, + ); + assert_eq!(visible, core.actual()); + } + ( + PackageProgramAssessmentV1::ExactSrgb8(package), + ProgramConstraintResultV1::Violation(CoreProgramViolationEvidenceV1::ExactSrgb8(core)), + ) => { + assert_eq!(package.verdict(), PackageProgramVerdictV1::Violation); + assert_eq!(package.expected(), core.target()); + let (visible, _) = assert_package_binding_matches_core( + PackageProgramAssessmentV1::ExactSrgb8(package), + core.binding(), + expected_occurrence, + ); + assert_eq!(visible, core.actual()); + } + ( + PackageProgramAssessmentV1::Wcag22Srgb8(package), + ProgramConstraintResultV1::Pass(CoreProgramPassEvidenceV1::Wcag22Srgb8(core)), + ) => { + assert_eq!(package.verdict(), PackageProgramVerdictV1::Pass); + let measurement = core.measurement().value(); + assert_eq!(package.profile_id(), measurement.profile_id()); + assert_eq!(package.criterion(), measurement.criterion()); + assert_eq!( + package.foreground_luminance(), + measurement.measurement().foreground_luminance + ); + assert_eq!( + package.background_luminance(), + measurement.measurement().background_luminance + ); + assert_eq!(package.numerical_evidence(), measurement.evidence()); + let (visible, backdrop) = assert_package_binding_matches_core( + PackageProgramAssessmentV1::Wcag22Srgb8(package), + core.binding(), + expected_occurrence, + ); + assert_eq!(visible, Srgb8::new(measurement.measurement().foreground)); + assert_eq!(backdrop, Srgb8::new(measurement.measurement().background)); + } + ( + PackageProgramAssessmentV1::Wcag22Srgb8(package), + ProgramConstraintResultV1::Violation(CoreProgramViolationEvidenceV1::Wcag22Srgb8(core)), + ) => { + assert_eq!(package.verdict(), PackageProgramVerdictV1::Violation); + let measurement = core.measurement().value(); + assert_eq!(package.profile_id(), measurement.profile_id()); + assert_eq!(package.criterion(), measurement.criterion()); + assert_eq!( + package.foreground_luminance(), + measurement.measurement().foreground_luminance + ); + assert_eq!( + package.background_luminance(), + measurement.measurement().background_luminance + ); + assert_eq!(package.numerical_evidence(), measurement.evidence()); + let (visible, backdrop) = assert_package_binding_matches_core( + PackageProgramAssessmentV1::Wcag22Srgb8(package), + core.binding(), + expected_occurrence, + ); + assert_eq!(visible, Srgb8::new(measurement.measurement().foreground)); + assert_eq!(backdrop, Srgb8::new(measurement.measurement().background)); + } + _ => panic!("package assessment family or verdict drifted from Core"), + } +} + +fn assert_verified_cell_matches_core( + package: PackageProgramVerifiedCellV1<'_>, + core: &ProgramConstraintCellV1, + selected_state_index: usize, +) { + assert_eq!(core.candidate_state_index(), selected_state_index); + assert_eq!(package.case_index(), core.case_index()); + assert_eq!(package.constraint().value(), core.constraint().value()); + assert_eq!(package.occurrence().value(), core.target().value()); + assert_eq!( + matches!( + package.mode(), + crate::package_bridge::PackageProgramConstraintModeV1::Hard + ), + core.is_hard() + ); + assert_package_assessment_matches_core(package.assessment(), core.result(), core.target()); +} + +fn assert_conflict_cell_matches_core( + package: PackageProgramConflictCellV1<'_>, + core: &ProgramConstraintCellV1, +) { + assert_eq!(package.state_index(), core.candidate_state_index()); + assert_eq!(package.case_index(), core.case_index()); + assert_eq!(package.constraint().value(), core.constraint().value()); + assert_eq!(package.occurrence().value(), core.target().value()); + assert_eq!( + matches!( + package.mode(), + crate::package_bridge::PackageProgramConstraintModeV1::Hard + ), + core.is_hard() + ); + assert_package_assessment_matches_core(package.assessment(), core.result(), core.target()); +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct ProjectionProbe { + iterators: usize, + certificates: usize, + cases: usize, + values: usize, + provenance: usize, + cells: usize, + outputs: usize, + operations: usize, + exact_assessments: usize, + wcag_assessments: usize, + iterator_laws_hold: bool, + checksum: u64, +} + +impl ProjectionProbe { + const fn new() -> Self { + Self { + iterators: 0, + certificates: 0, + cases: 0, + values: 0, + provenance: 0, + cells: 0, + outputs: 0, + operations: 0, + exact_assessments: 0, + wcag_assessments: 0, + iterator_laws_hold: true, + checksum: 0, + } + } + + fn mix(&mut self, value: u64) { + self.checksum = self.checksum.rotate_left(1) ^ value; + } + + fn mix_bytes(&mut self, bytes: &[u8]) { + for byte in bytes { + self.mix(u64::from(*byte)); + } + } + + fn mix_srgb8(&mut self, value: Srgb8) { + self.mix_bytes(&value.bytes()); + } +} + +fn consume_exact_fused( + mut iterator: I, + probe: &mut ProjectionProbe, + mut consume: impl FnMut(I::Item, &mut ProjectionProbe), +) where + I: ExactSizeIterator + FusedIterator, +{ + probe.iterators += 1; + let mut remaining = iterator.len(); + let initial_len = remaining; + probe.iterator_laws_hold &= iterator.size_hint() == (remaining, Some(remaining)); + for _ in 0..initial_len { + let Some(value) = iterator.next() else { + probe.iterator_laws_hold = false; + break; + }; + remaining -= 1; + probe.iterator_laws_hold &= iterator.len() == remaining; + probe.iterator_laws_hold &= iterator.size_hint() == (remaining, Some(remaining)); + consume(value, probe); + } + probe.iterator_laws_hold &= remaining == 0; + probe.iterator_laws_hold &= iterator.len() == 0; + probe.iterator_laws_hold &= iterator.next().is_none(); + probe.iterator_laws_hold &= iterator.next().is_none(); +} + +fn consume_package_assessment( + assessment: PackageProgramAssessmentV1<'_>, + probe: &mut ProjectionProbe, +) { + probe.mix(match assessment.verdict() { + PackageProgramVerdictV1::Pass => 1, + PackageProgramVerdictV1::Violation => 2, + }); + match assessment { + PackageProgramAssessmentV1::ExactSrgb8(evidence) => { + probe.exact_assessments += 1; + probe.mix_srgb8(evidence.expected()); + } + PackageProgramAssessmentV1::Wcag22Srgb8(evidence) => { + probe.wcag_assessments += 1; + probe.mix_bytes(evidence.profile_id().key().as_bytes()); + probe.mix_bytes(evidence.criterion().key().as_bytes()); + probe.mix(evidence.foreground_luminance().lower()); + probe.mix(evidence.foreground_luminance().upper()); + probe.mix(evidence.background_luminance().lower()); + probe.mix(evidence.background_luminance().upper()); + probe.mix_bytes(evidence.numerical_evidence().class_key().as_bytes()); + } + } + + let PackageProgramPhysicalPointV1::EncodedSrgb8SourceOver(physical) = + assessment.binding().physical(); + probe.mix(u64::from(physical.subject_paint().value())); + probe.mix(u64::from(physical.backdrop_surface().value())); + probe.mix_srgb8(physical.subject()); + probe.mix(physical.opacity().to_bits()); + probe.mix_srgb8(physical.backdrop()); + probe.mix_srgb8(physical.visible()); + + let PackageProgramModeledPointV1::Iec61966Srgb8ToCie1931TwoDegreeXyzD65RelativeY1(modeled) = + assessment.binding().modeled(); + for coordinate in modeled.xyz() { + probe.mix(coordinate.to_bits()); + } + let context = modeled.appearance_context(); + probe.mix(context.adapting_luminance_cd_m2().to_bits()); + probe.mix(context.background_luminance_ratio_yb_yw().to_bits()); + probe.mix(match context.surround() { + PackageProgramSurroundV1::Average => 1, + PackageProgramSurroundV1::Dim => 2, + PackageProgramSurroundV1::Dark => 3, + }); +} + +fn consume_package_projection(view: PackageProgramStateViewV1<'_>) -> ProjectionProbe { + let mut probe = ProjectionProbe::new(); + probe.mix(match view.kind() { + PackageProgramStateKindV1::Waiting => 1, + PackageProgramStateKindV1::Ready => 2, + PackageProgramStateKindV1::Failed => 3, + PackageProgramStateKindV1::Stale => 4, + }); + probe.mix(view.revision().unwrap_or_default()); + probe.mix( + view.cause_certificate_index() + .map_or(0, |index| index as u64 + 1), + ); + + consume_exact_fused(view.certificates(), &mut probe, |certificate, probe| { + probe.certificates += 1; + probe.mix_bytes(certificate.content_identity().as_bytes()); + let observation = certificate.observation(); + probe.mix(u64::from(observation.stream().value())); + probe.mix(observation.revision()); + consume_exact_fused(observation.surface_input_ports(), probe, |port, probe| { + probe.mix(u64::from(port.value())); + }); + consume_exact_fused(observation.physical_cases(), probe, |case, probe| { + probe.cases += 1; + consume_exact_fused(case.values(), probe, |value, probe| { + probe.values += 1; + let PackageProgramSignalV1::Iec61966Srgb8D65(value) = value; + probe.mix_srgb8(value); + }); + consume_exact_fused(case.provenance(), probe, |scenario, probe| { + probe.provenance += 1; + probe.mix(u64::from(scenario.value())); + }); + }); + match certificate { + PackageProgramCertificateV1::Verified(verified) => { + probe.mix( + verified + .selected_state_index() + .map_or(0, |index| index as u64 + 1), + ); + consume_exact_fused(verified.cells(), probe, |cell, probe| { + probe.cells += 1; + probe.mix(cell.case_index() as u64); + probe.mix(u64::from(cell.constraint().value())); + probe.mix(u64::from(cell.occurrence().value())); + probe.mix(match cell.mode() { + crate::package_bridge::PackageProgramConstraintModeV1::Hard => 1, + crate::package_bridge::PackageProgramConstraintModeV1::ReportOnly => 2, + }); + consume_package_assessment(cell.assessment(), probe); + }); + consume_exact_fused(verified.outputs(), probe, |output, probe| { + probe.outputs += 1; + probe.mix(u64::from(output.output_slot().value())); + probe.mix(u64::from(output.paint().value())); + probe.mix_srgb8(output.source()); + probe.mix(output.opacity().to_bits()); + }); + } + PackageProgramCertificateV1::Conflict(conflict) => { + probe.mix(conflict.considered_state_count() as u64); + consume_exact_fused(conflict.cells(), probe, |cell, probe| { + probe.cells += 1; + probe.mix(cell.state_index() as u64); + probe.mix(cell.case_index() as u64); + probe.mix(u64::from(cell.constraint().value())); + probe.mix(u64::from(cell.occurrence().value())); + probe.mix(match cell.mode() { + crate::package_bridge::PackageProgramConstraintModeV1::Hard => 1, + crate::package_bridge::PackageProgramConstraintModeV1::ReportOnly => 2, + }); + consume_package_assessment(cell.assessment(), probe); + }); + } + } + }); + consume_exact_fused(view.operations(), &mut probe, |operation, probe| { + probe.operations += 1; + match operation { + PackageProgramOperationV1::Set(set) => { + probe.mix(1); + probe.mix(u64::from(set.output_slot().value())); + probe.mix_srgb8(set.source()); + probe.mix(set.opacity().to_bits()); + probe.mix_bytes(set.certificate().content_identity().as_bytes()); + probe.mix(set.certificate().observation().revision()); + } + PackageProgramOperationV1::Remove(remove) => { + probe.mix(2); + probe.mix(u64::from(remove.output_slot().value())); + } + PackageProgramOperationV1::Hold(hold) => { + probe.mix(3); + probe.mix(u64::from(hold.output_slot().value())); + probe.mix_bytes(hold.certificate().content_identity().as_bytes()); + probe.mix(hold.certificate().observation().revision()); + } + } + }); + std::hint::black_box(probe) +} + #[test] fn one_program_retains_typed_exact_and_wcag22_outcomes() { let program: CoreProgramV1 = Program::new( @@ -224,6 +749,42 @@ fn one_program_retains_typed_exact_and_wcag22_outcomes() { ); } +#[test] +fn fixed_package_certificate_retains_none_selection_and_nonunit_output_opacity() { + let owner = PackageProgramOwnerV1::from_compiled(fixed_translucent_program()); + let mut session = owner.instantiate(STREAM.value()).unwrap(); + let white = [Srgb8::new([0xFF; 3])]; + let scenarios = [PackageProgramScenarioV1::new(1, &white)]; + let state = session + .update(PackageProgramUpdateV1::Observed { + revision: 1, + scenarios: &scenarios, + }) + .unwrap(); + let Some(PackageProgramCertificateV1::Verified(certificate)) = state.certificates().next() + else { + panic!("the exact translucent midpoint must be verified"); + }; + assert_eq!(certificate.selected_state_index(), None); + let PackageProgramAssessmentV1::ExactSrgb8(assessment) = + certificate.cells().next().unwrap().assessment() + else { + panic!("the fixed Program has one Exact certificate cell"); + }; + let PackageProgramPhysicalPointV1::EncodedSrgb8SourceOver(physical) = + assessment.binding().physical(); + assert_eq!(physical.opacity().to_bits(), 0.5_f64.to_bits()); + assert_eq!(physical.visible(), Srgb8::new([0x80; 3])); + assert_eq!( + certificate.outputs().next().unwrap().opacity().to_bits(), + physical.opacity().to_bits() + ); + let Some(PackageProgramOperationV1::Set(set)) = state.operations().next() else { + panic!("Verified must emit one Set"); + }; + assert_eq!(set.opacity().to_bits(), physical.opacity().to_bits()); +} + #[test] fn mixed_families_select_only_a_state_that_passes_every_case_then_recheck_it() { let compiled = finite_program([[0x80; 3], [0; 3]]); @@ -302,6 +863,438 @@ fn mixed_family_conflict_is_exhaustive_and_keeps_report_only_non_gating() { )); } +#[test] +fn package_projection_preserves_every_exposed_ready_and_conflict_field_against_core() { + let ready_core_owner = finite_program([[0x80; 3], [0; 3]]); + let ready_identity = ready_core_owner.content_identity(); + let ready_package_owner = + PackageProgramOwnerV1::from_compiled(finite_program([[0x80; 3], [0; 3]])); + let mut ready_core_session = ready_core_owner.instantiate(STREAM).unwrap(); + let mut ready_package_session = ready_package_owner.instantiate(STREAM.value()).unwrap(); + let ready_backdrops = [[0xFF; 3], [0xFF; 3], [0x80; 3]]; + let SessionState::Ready { + current: core_verified, + } = ready_core_session + .update(observed_backdrops(&ready_backdrops)) + .unwrap() + else { + panic!("black is the first state that passes both canonical physical cases"); + }; + let ready_white = [Srgb8::new([0xFF; 3])]; + let ready_gray = [Srgb8::new([0x80; 3])]; + let ready_scenarios = [ + PackageProgramScenarioV1::new(1, &ready_white), + PackageProgramScenarioV1::new(2, &ready_white), + PackageProgramScenarioV1::new(3, &ready_gray), + ]; + let package_ready = ready_package_session + .update(PackageProgramUpdateV1::Observed { + revision: 1, + scenarios: &ready_scenarios, + }) + .unwrap(); + let mut package_certificates = package_ready.certificates(); + assert_eq!(package_certificates.len(), 1); + let Some(PackageProgramCertificateV1::Verified(package_verified)) = package_certificates.next() + else { + panic!("Ready must retain exactly one Verified certificate"); + }; + assert!(package_certificates.next().is_none()); + assert!(package_certificates.next().is_none()); + assert_eq!( + package_verified.content_identity().as_bytes(), + ready_identity.as_bytes() + ); + assert_package_observation_matches_core( + package_verified.observation(), + core_verified.report().observation(), + ); + assert_eq!( + package_verified.selected_state_index(), + core_verified.selected_state_index() + ); + let selected_state_index = core_verified.selected_state_index().unwrap(); + let mut package_cells = package_verified.cells(); + let mut core_cells = core_verified.report().cells().iter(); + assert_eq!(package_cells.len(), core_cells.len()); + while let (Some(package), Some(core)) = (package_cells.next(), core_cells.next()) { + assert_verified_cell_matches_core(package, core, selected_state_index); + assert_eq!(package_cells.len(), core_cells.len()); + } + assert!(package_cells.next().is_none()); + assert!(package_cells.next().is_none()); + assert!(core_cells.next().is_none()); + + let mut package_outputs = package_verified.outputs(); + let mut core_outputs = core_verified.outputs().iter(); + assert_eq!(package_outputs.len(), core_outputs.len()); + while let (Some(package), Some(core)) = (package_outputs.next(), core_outputs.next()) { + assert_eq!(package.output_slot().value(), core.output().value()); + assert_eq!(package.paint().value(), core.paint().id().value()); + assert_eq!(package.source(), core.paint().source()); + assert_eq!( + package.opacity().to_bits(), + core.paint().opacity().value().to_bits() + ); + assert_eq!(package_outputs.len(), core_outputs.len()); + } + assert!(package_outputs.next().is_none()); + assert!(package_outputs.next().is_none()); + assert!(core_outputs.next().is_none()); + let mut ready_operations = package_ready.operations(); + assert_eq!(ready_operations.len(), core_verified.outputs().len()); + for core_output in core_verified.outputs() { + let Some(PackageProgramOperationV1::Set(set)) = ready_operations.next() else { + panic!("every certified output must become exactly one Set"); + }; + assert_eq!(set.output_slot().value(), core_output.output().value()); + assert_eq!(set.source(), core_output.paint().source()); + assert_eq!( + set.opacity().to_bits(), + core_output.paint().opacity().value().to_bits() + ); + assert_eq!( + set.certificate().content_identity(), + package_verified.content_identity() + ); + assert_eq!( + set.certificate().observation().revision(), + package_verified.observation().revision() + ); + } + assert!(ready_operations.next().is_none()); + assert!(ready_operations.next().is_none()); + + let conflict_core_owner = finite_program([[0; 3], [0xFF; 3]]); + let conflict_identity = conflict_core_owner.content_identity(); + let conflict_package_owner = + PackageProgramOwnerV1::from_compiled(finite_program([[0; 3], [0xFF; 3]])); + let mut conflict_core_session = conflict_core_owner.instantiate(STREAM).unwrap(); + let mut conflict_package_session = conflict_package_owner.instantiate(STREAM.value()).unwrap(); + let conflict_backdrops = [[0xFF; 3], [0; 3]]; + let SessionState::Failed { + cause: core_conflict, + previous: None, + } = conflict_core_session + .update(observed_backdrops(&conflict_backdrops)) + .unwrap() + else { + panic!("neither black nor white passes both opposing physical cases"); + }; + let conflict_white = [Srgb8::new([0xFF; 3])]; + let conflict_black = [Srgb8::new([0; 3])]; + let conflict_scenarios = [ + PackageProgramScenarioV1::new(1, &conflict_white), + PackageProgramScenarioV1::new(2, &conflict_black), + ]; + let package_failed = conflict_package_session + .update(PackageProgramUpdateV1::Observed { + revision: 1, + scenarios: &conflict_scenarios, + }) + .unwrap(); + let mut package_certificates = package_failed.certificates(); + assert_eq!(package_certificates.len(), 1); + let Some(PackageProgramCertificateV1::Conflict(package_conflict)) = package_certificates.next() + else { + panic!("Failed without previous state must retain one Conflict certificate"); + }; + assert!(package_certificates.next().is_none()); + assert!(package_certificates.next().is_none()); + assert_eq!( + package_conflict.content_identity().as_bytes(), + conflict_identity.as_bytes() + ); + assert_package_observation_matches_core( + package_conflict.observation(), + core_conflict.report().observation(), + ); + assert_eq!( + package_conflict.considered_state_count(), + core_conflict.considered_state_count() + ); + let core_passes = core_conflict + .report() + .cells() + .iter() + .filter(|cell| !cell.result().is_violation()) + .count(); + assert!(core_passes > 0); + assert!(core_passes < core_conflict.report().cells().len()); + let mut package_cells = package_conflict.cells(); + let mut core_cells = core_conflict.report().cells().iter(); + assert_eq!(package_cells.len(), core_cells.len()); + while let (Some(package), Some(core)) = (package_cells.next(), core_cells.next()) { + assert_conflict_cell_matches_core(package, core); + assert_eq!(package_cells.len(), core_cells.len()); + } + assert!(package_cells.next().is_none()); + assert!(package_cells.next().is_none()); + assert!(core_cells.next().is_none()); + let mut failed_operations = package_failed.operations(); + assert_eq!(failed_operations.len(), 1); + assert!(matches!( + failed_operations.next(), + Some(PackageProgramOperationV1::Remove(_)) + )); + assert!(failed_operations.next().is_none()); + assert!(failed_operations.next().is_none()); +} + +#[test] +fn committed_projection_is_zero_alloc_and_repeats_no_composite_transform_or_evaluator_dispatch() { + crate::composition::reset_source_over_evaluation_count(); + MODELED_TRISTIMULUS_DERIVATION_CALLS.with(|calls| calls.set(0)); + CORE_PROGRAM_ASSESSMENT_CALLS.with(|calls| calls.set(0)); + + let owner = PackageProgramOwnerV1::from_compiled(finite_program([[0; 3], [0xFF; 3]])); + let mut session = owner.instantiate(STREAM.value()).unwrap(); + let white = [Srgb8::new([0xFF; 3])]; + let white_only = [PackageProgramScenarioV1::new(1, &white)]; + session + .update(PackageProgramUpdateV1::Observed { + revision: 1, + scenarios: &white_only, + }) + .unwrap(); + let Some(PackageProgramCertificateV1::Verified(ready_certificate)) = + session.state().certificates().next() + else { + panic!("black must be selected for the white-only physical support"); + }; + assert_eq!(ready_certificate.selected_state_index(), Some(0)); + + let ready_compositions = crate::composition::source_over_evaluation_count(); + let ready_derivations = MODELED_TRISTIMULUS_DERIVATION_CALLS.with(core::cell::Cell::get); + let ready_assessments = CORE_PROGRAM_ASSESSMENT_CALLS.with(core::cell::Cell::get); + assert!(ready_compositions > 0); + assert!(ready_derivations > 0); + assert!(ready_assessments > 0); + let (ready_probe, ready_allocations) = crate::test_support::measured_allocations(|| { + consume_package_projection(std::hint::black_box(session.state())) + }); + assert_eq!(ready_allocations, 0); + assert!(ready_probe.iterator_laws_hold); + assert_eq!(ready_probe.certificates, 1); + assert_eq!(ready_probe.cases, 1); + assert_eq!(ready_probe.values, 1); + assert_eq!(ready_probe.provenance, 1); + assert_eq!(ready_probe.cells, 2); + assert_eq!(ready_probe.outputs, 1); + assert_eq!(ready_probe.operations, 1); + assert_eq!(ready_probe.exact_assessments, 1); + assert_eq!(ready_probe.wcag_assessments, 1); + assert_ne!(ready_probe.checksum, 0); + assert_eq!( + crate::composition::source_over_evaluation_count(), + ready_compositions + ); + assert_eq!( + MODELED_TRISTIMULUS_DERIVATION_CALLS.with(core::cell::Cell::get), + ready_derivations + ); + assert_eq!( + CORE_PROGRAM_ASSESSMENT_CALLS.with(core::cell::Cell::get), + ready_assessments + ); + + session + .update(PackageProgramUpdateV1::Unknown { + revision: 2, + reason_id: 7, + }) + .unwrap(); + let stale_compositions = crate::composition::source_over_evaluation_count(); + let stale_derivations = MODELED_TRISTIMULUS_DERIVATION_CALLS.with(core::cell::Cell::get); + let stale_assessments = CORE_PROGRAM_ASSESSMENT_CALLS.with(core::cell::Cell::get); + let (stale_probe, stale_allocations) = crate::test_support::measured_allocations(|| { + consume_package_projection(std::hint::black_box(session.state())) + }); + assert_eq!(stale_allocations, 0); + assert!(stale_probe.iterator_laws_hold); + assert_eq!(stale_probe.certificates, 1); + assert_eq!(stale_probe.cells, 2); + assert_eq!(stale_probe.outputs, 1); + assert_eq!(stale_probe.operations, 1); + assert_ne!(stale_probe.checksum, ready_probe.checksum); + assert_eq!( + crate::composition::source_over_evaluation_count(), + stale_compositions + ); + assert_eq!( + MODELED_TRISTIMULUS_DERIVATION_CALLS.with(core::cell::Cell::get), + stale_derivations + ); + assert_eq!( + CORE_PROGRAM_ASSESSMENT_CALLS.with(core::cell::Cell::get), + stale_assessments + ); + + let black = [Srgb8::new([0; 3])]; + let opposing_backdrops = [ + PackageProgramScenarioV1::new(1, &white), + PackageProgramScenarioV1::new(2, &black), + ]; + session + .update(PackageProgramUpdateV1::Observed { + revision: 3, + scenarios: &opposing_backdrops, + }) + .unwrap(); + let failed_compositions = crate::composition::source_over_evaluation_count(); + let failed_derivations = MODELED_TRISTIMULUS_DERIVATION_CALLS.with(core::cell::Cell::get); + let failed_assessments = CORE_PROGRAM_ASSESSMENT_CALLS.with(core::cell::Cell::get); + let (failed_probe, failed_allocations) = crate::test_support::measured_allocations(|| { + consume_package_projection(std::hint::black_box(session.state())) + }); + assert_eq!(failed_allocations, 0); + assert!(failed_probe.iterator_laws_hold); + assert_eq!(failed_probe.certificates, 2); + assert_eq!(failed_probe.cases, 3); + assert_eq!(failed_probe.values, 3); + assert_eq!(failed_probe.provenance, 3); + assert_eq!(failed_probe.cells, 10); + assert_eq!(failed_probe.outputs, 1); + assert_eq!(failed_probe.operations, 1); + assert_eq!(failed_probe.exact_assessments, 5); + assert_eq!(failed_probe.wcag_assessments, 5); + assert_ne!(failed_probe.checksum, stale_probe.checksum); + assert_eq!( + crate::composition::source_over_evaluation_count(), + failed_compositions + ); + assert_eq!( + MODELED_TRISTIMULUS_DERIVATION_CALLS.with(core::cell::Cell::get), + failed_derivations + ); + assert_eq!( + CORE_PROGRAM_ASSESSMENT_CALLS.with(core::cell::Cell::get), + failed_assessments + ); +} + +#[test] +fn observation_projection_is_invariant_under_every_scenario_permutation_and_keeps_provenance() { + const PERMUTATIONS: [[usize; 3]; 6] = [ + [0, 1, 2], + [0, 2, 1], + [1, 0, 2], + [1, 2, 0], + [2, 0, 1], + [2, 1, 0], + ]; + const IDS: [u32; 3] = [9, 4, 3]; + const BACKDROPS: [[u8; 3]; 3] = [[0xFF; 3], [0x80; 3], [0xFF; 3]]; + + crate::composition::reset_source_over_evaluation_count(); + MODELED_TRISTIMULUS_DERIVATION_CALLS.with(|calls| calls.set(0)); + CORE_PROGRAM_ASSESSMENT_CALLS.with(|calls| calls.set(0)); + + let core_owner = finite_program([[0x80; 3], [0; 3]]); + let content_identity = core_owner.content_identity(); + let package_owner = PackageProgramOwnerV1::from_compiled(finite_program([[0x80; 3], [0; 3]])); + let mut core_session = core_owner.instantiate(STREAM).unwrap(); + let mut package_session = package_owner.instantiate(STREAM.value()).unwrap(); + let package_values = BACKDROPS.map(|value| [Srgb8::new(value)]); + let mut first_package_backing = None; + let mut evaluation_counts_after_first = None; + + for permutation in PERMUTATIONS { + let core_update = ObservationUpdateInput { + stream: STREAM, + revision: Revision::new(1), + payload: ObservationPayloadInput::Scenarios(ObservedScenarioSetInput { + scenarios: permutation + .iter() + .map(|index| ScenarioInput { + id: ScenarioId::new(IDS[*index]), + bindings: vec![SurfaceInputBinding::new( + SURFACE_PORT, + signal(BACKDROPS[*index]), + )], + }) + .collect(), + }), + }; + let SessionState::Ready { + current: core_verified, + } = core_session.update(core_update).unwrap() + else { + panic!("black must pass both deduplicated physical cases"); + }; + let package_scenarios = permutation + .map(|index| PackageProgramScenarioV1::new(IDS[index], &package_values[index])); + let package_state = package_session + .update(PackageProgramUpdateV1::Observed { + revision: 1, + scenarios: &package_scenarios, + }) + .unwrap(); + let Some(PackageProgramCertificateV1::Verified(package_verified)) = + package_state.certificates().next() + else { + panic!("the canonical observation must keep one Verified certificate"); + }; + assert_eq!( + package_verified.content_identity().as_bytes(), + content_identity.as_bytes() + ); + assert_package_observation_matches_core( + package_verified.observation(), + core_verified.report().observation(), + ); + + let projected_cases = package_verified + .observation() + .physical_cases() + .map(|case| { + let values = case + .values() + .map(|value| match value { + PackageProgramSignalV1::Iec61966Srgb8D65(value) => value, + }) + .collect::>(); + let provenance = case + .provenance() + .map(|scenario| scenario.value()) + .collect::>(); + (values, provenance) + }) + .collect::>(); + assert_eq!( + projected_cases, + [ + (vec![Srgb8::new([0x80; 3])], vec![4]), + (vec![Srgb8::new([0xFF; 3])], vec![3, 9]), + ] + ); + + let backing = PackageProgramCertificateV1::Verified(package_verified) + .observation_backing_ptr_for_test(); + match first_package_backing { + None => { + first_package_backing = Some(backing); + evaluation_counts_after_first = Some(( + crate::composition::source_over_evaluation_count(), + MODELED_TRISTIMULUS_DERIVATION_CALLS.with(core::cell::Cell::get), + CORE_PROGRAM_ASSESSMENT_CALLS.with(core::cell::Cell::get), + )); + } + Some(first) => { + assert_eq!(backing, first); + assert_eq!( + evaluation_counts_after_first, + Some(( + crate::composition::source_over_evaluation_count(), + MODELED_TRISTIMULUS_DERIVATION_CALLS.with(core::cell::Cell::get), + CORE_PROGRAM_ASSESSMENT_CALLS.with(core::cell::Cell::get), + )) + ); + } + } + } +} + #[test] fn concrete_package_bridge_projects_total_ready_and_stale_operations() { let owner = PackageProgramOwnerV1::from_compiled(finite_program([[0x80; 3], [0; 3]])); @@ -335,21 +1328,37 @@ fn concrete_package_bridge_projects_total_ready_and_stale_operations() { assert_eq!(ready.cause_certificate_index(), None); let certificates = ready.certificates().collect::>(); assert_eq!(certificates.len(), 1); + assert!(matches!( + certificates[0], + PackageProgramCertificateV1::Verified(_) + )); + assert_eq!(certificates[0].observation().revision(), 1); + let ready_backing = certificates[0].observation_backing_ptr_for_test(); + let mut operations = ready.operations(); + let Some(PackageProgramOperationV1::Set(set)) = operations.next() else { + panic!("Ready must emit one Set operation"); + }; assert_eq!( - certificates[0].kind(), - PackageProgramCertificateKindV1::Verified + set.output_slot(), + PackageProgramOutputSlotIdV1::new(OUTPUT.value()) ); - assert_eq!(certificates[0].revision(), 1); - let ready_backing = certificates[0].observation_backing_ptr_for_test(); + assert_eq!(set.source(), Srgb8::new([0; 3])); + assert_eq!(set.opacity(), 1.0); assert_eq!( - ready.operations().collect::>(), - [PackageProgramOperationV1::Set { - output_slot: PackageProgramOutputSlotIdV1::new(OUTPUT.value()), - source: Srgb8::new([0; 3]), - opacity: 1.0, - certificate_index: 0, - }] + set.certificate().observation().revision(), + certificates[0].observation().revision() ); + assert_eq!( + set.certificate().content_identity(), + certificates[0].content_identity() + ); + assert_eq!( + PackageProgramCertificateV1::Verified(set.certificate()).observation_backing_ptr_for_test(), + ready_backing + ); + assert!(operations.next().is_none()); + drop(operations); + drop(certificates); let reordered = [ PackageProgramScenarioV1::new(1, &white), @@ -393,18 +1402,33 @@ fn concrete_package_bridge_projects_total_ready_and_stale_operations() { assert_eq!(stale.revision(), Some(2)); let certificates = stale.certificates().collect::>(); assert_eq!(certificates.len(), 1); + assert!(matches!( + certificates[0], + PackageProgramCertificateV1::Verified(_) + )); + assert_eq!(certificates[0].observation().revision(), 1); + let mut operations = stale.operations(); + let Some(PackageProgramOperationV1::Hold(hold)) = operations.next() else { + panic!("Stale must emit one Hold operation"); + }; + assert_eq!( + hold.output_slot(), + PackageProgramOutputSlotIdV1::new(OUTPUT.value()) + ); + assert_eq!( + hold.certificate().observation().revision(), + certificates[0].observation().revision() + ); assert_eq!( - certificates[0].kind(), - PackageProgramCertificateKindV1::Verified + hold.certificate().content_identity(), + certificates[0].content_identity() ); - assert_eq!(certificates[0].revision(), 1); assert_eq!( - stale.operations().collect::>(), - [PackageProgramOperationV1::Hold { - output_slot: PackageProgramOutputSlotIdV1::new(OUTPUT.value()), - certificate_index: 0, - }] + PackageProgramCertificateV1::Verified(hold.certificate()) + .observation_backing_ptr_for_test(), + ready_backing ); + assert!(operations.next().is_none()); } #[test] @@ -425,26 +1449,34 @@ fn concrete_package_bridge_distinguishes_failed_remove_from_failed_hold() { assert_eq!(failed.cause_certificate_index(), Some(0)); let certificates = failed.certificates().collect::>(); assert_eq!(certificates.len(), 1); + assert!(matches!( + certificates[0], + PackageProgramCertificateV1::Conflict(_) + )); + assert_eq!(certificates[0].observation().revision(), 1); + let mut operations = failed.operations(); + let Some(PackageProgramOperationV1::Remove(remove)) = operations.next() else { + panic!("Failed without previous evidence must emit one Remove operation"); + }; assert_eq!( - certificates[0].kind(), - PackageProgramCertificateKindV1::Conflict - ); - assert_eq!(certificates[0].revision(), 1); - assert_eq!( - failed.operations().collect::>(), - [PackageProgramOperationV1::Remove { - output_slot: PackageProgramOutputSlotIdV1::new(OUTPUT.value()), - }] + remove.output_slot(), + PackageProgramOutputSlotIdV1::new(OUTPUT.value()) ); + assert!(operations.next().is_none()); let owner = PackageProgramOwnerV1::from_compiled(finite_program([[0; 3], [0xFF; 3]])); let mut session = owner.instantiate(12).unwrap(); - session + let previous = session .update(PackageProgramUpdateV1::Observed { revision: 1, scenarios: &white_only, }) .unwrap(); + let previous_backing = previous + .certificates() + .next() + .unwrap() + .observation_backing_ptr_for_test(); let both = [ PackageProgramScenarioV1::new(1, &white), PackageProgramScenarioV1::new(2, &black), @@ -461,20 +1493,36 @@ fn concrete_package_bridge_distinguishes_failed_remove_from_failed_hold() { assert_eq!( certificates .iter() - .map(|certificate| (certificate.kind(), certificate.revision())) + .map(|certificate| match certificate { + PackageProgramCertificateV1::Verified(value) => { + ("verified", value.observation().revision()) + } + PackageProgramCertificateV1::Conflict(value) => { + ("conflict", value.observation().revision()) + } + }) .collect::>(), - [ - (PackageProgramCertificateKindV1::Conflict, 2), - (PackageProgramCertificateKindV1::Verified, 1), - ] + [("conflict", 2), ("verified", 1)] + ); + let mut operations = failed.operations(); + let Some(PackageProgramOperationV1::Hold(hold)) = operations.next() else { + panic!("Failed with previous evidence must emit one Hold operation"); + }; + assert_eq!( + hold.output_slot(), + PackageProgramOutputSlotIdV1::new(OUTPUT.value()) + ); + assert_eq!(hold.certificate().observation().revision(), 1); + assert_eq!( + hold.certificate().content_identity(), + certificates[1].content_identity() ); assert_eq!( - failed.operations().collect::>(), - [PackageProgramOperationV1::Hold { - output_slot: PackageProgramOutputSlotIdV1::new(OUTPUT.value()), - certificate_index: 1, - }] + PackageProgramCertificateV1::Verified(hold.certificate()) + .observation_backing_ptr_for_test(), + previous_backing ); + assert!(operations.next().is_none()); } #[test] diff --git a/crates/labcolors-core/src/program_session.rs b/crates/labcolors-core/src/program_session.rs index a755d754..a017e6b9 100644 --- a/crates/labcolors-core/src/program_session.rs +++ b/crates/labcolors-core/src/program_session.rs @@ -524,6 +524,15 @@ where } } +#[cfg(test)] +thread_local! { + /// Counts the concrete production evaluator dispatch itself, so certificate + /// projection cannot accidentally recompute a verdict while still reusing + /// the stored physical and modeled witnesses. + pub(crate) static CORE_PROGRAM_ASSESSMENT_CALLS: core::cell::Cell = + const { core::cell::Cell::new(0) }; +} + /// Generates the code-owned heterogeneous evaluator set as parallel closed /// unions. Each evidence variant retains the concrete evaluator's physical + /// LCS binding, identity, release, capability, invocation, measurement, and @@ -572,6 +581,8 @@ macro_rules! define_core_program_evaluators_v1 { modeled_lcs: ModeledLcsOccurrenceV1, invocation: Self::Invocation, ) -> ProgramConstraintAssessmentResultV1 { + #[cfg(test)] + CORE_PROGRAM_ASSESSMENT_CALLS.with(|calls| calls.set(calls.get() + 1)); match invocation { $(CoreProgramConstraintInvocationV1::$variant(invocation) => { let evaluator: $evaluator = $evaluator_value; diff --git a/crates/labcolors-core/tests/package_bridge_red.rs b/crates/labcolors-core/tests/program_boundary.rs similarity index 69% rename from crates/labcolors-core/tests/package_bridge_red.rs rename to crates/labcolors-core/tests/program_boundary.rs index 0e7b6469..a804b279 100644 --- a/crates/labcolors-core/tests/package_bridge_red.rs +++ b/crates/labcolors-core/tests/program_boundary.rs @@ -1,29 +1,31 @@ -//! RED contract for the sole concrete Core package seam. +//! External compile-and-runtime contract for the sole concrete Core Program seam. //! //! This integration crate deliberately has no access to Core-private generic -//! evaluator/session machinery. It must compile using only one hidden, -//! concrete package module once that seam is linked after the P3 + weak-owner -//! rebase. +//! evaluator/session machinery. Every reachable path uses only the closed +//! concrete boundary types. + +use core::iter::FusedIterator; use labcolors_core::Srgb8; use labcolors_core::package_bridge::{ PackageProgramAppearanceContextErrorKindV1, PackageProgramAppearanceContextFieldV1, - PackageProgramAppearanceContextV1, PackageProgramCertificateV1, + PackageProgramAppearanceContextV1, PackageProgramAssessmentV1, PackageProgramCertificateV1, PackageProgramCompileErrorHandleV1, PackageProgramCompileErrorKindV1, PackageProgramCompileErrorV1, PackageProgramConstraintIdV1, PackageProgramDraftErrorV1, PackageProgramDraftV1, PackageProgramInstantiateErrorV1, PackageProgramJointChoiceV1, - PackageProgramJointOrderErrorV1, PackageProgramJointStateV1, + PackageProgramJointOrderErrorV1, PackageProgramJointStateV1, PackageProgramModeledPointV1, PackageProgramNumericDomainErrorV1, PackageProgramOccurrenceIdV1, PackageProgramOpacityInputIdV1, PackageProgramOperationV1, PackageProgramOutputSlotIdV1, - PackageProgramOwnerV1, PackageProgramPaintIdV1, PackageProgramScenarioV1, - PackageProgramSessionV1, PackageProgramSourceIdV1, PackageProgramStateKindV1, - PackageProgramStateViewV1, PackageProgramSurfaceIdV1, PackageProgramSurfaceInputPortIdV1, - PackageProgramSurroundV1, PackageProgramTargetCandidateIdV1, PackageProgramTargetCandidateV1, - PackageProgramTargetIdV1, PackageProgramUpdateErrorKindV1, PackageProgramUpdateV1, + PackageProgramOwnerV1, PackageProgramPaintIdV1, PackageProgramPhysicalPointV1, + PackageProgramScenarioV1, PackageProgramSessionV1, PackageProgramSignalV1, + PackageProgramSourceIdV1, PackageProgramStateKindV1, PackageProgramStateViewV1, + PackageProgramSurfaceIdV1, PackageProgramSurfaceInputPortIdV1, PackageProgramSurroundV1, + PackageProgramTargetCandidateIdV1, PackageProgramTargetCandidateV1, PackageProgramTargetIdV1, + PackageProgramUpdateErrorKindV1, PackageProgramUpdateV1, PackageProgramVerdictV1, }; use labcolors_core::wcag22::Wcag22CriterionV1; -fn exact_size(iterator: I) -> I { +fn exact_size(iterator: I) -> I { iterator } @@ -56,33 +58,102 @@ fn assert_projection_is_linear(view: PackageProgramStateViewV1<'_>) { let certificates = exact_size(view.certificates()); let certificate_count = certificates.len(); for certificate in certificates { - let _: PackageProgramCertificateV1<'_> = certificate; + let _: &[u8; 32] = certificate.content_identity().as_bytes(); + let observation = certificate.observation(); + let _stream_id = observation.stream().value(); + let _revision = observation.revision(); + for port in exact_size(observation.surface_input_ports()) { + let _: PackageProgramSurfaceInputPortIdV1 = port; + } + for case in exact_size(observation.physical_cases()) { + for value in exact_size(case.values()) { + let PackageProgramSignalV1::Iec61966Srgb8D65(value) = value; + let _: Srgb8 = value; + } + for scenario in exact_size(case.provenance()) { + let _ = scenario.value(); + } + } + macro_rules! inspect_cell { + ($cell:expr) => {{ + let cell = $cell; + let _ = cell.case_index(); + let _ = cell.constraint().value(); + let _ = cell.occurrence().value(); + let _ = cell.mode(); + let assessment = cell.assessment(); + let _: PackageProgramVerdictV1 = assessment.verdict(); + match assessment { + PackageProgramAssessmentV1::ExactSrgb8(evidence) => { + let _: Srgb8 = evidence.expected(); + } + PackageProgramAssessmentV1::Wcag22Srgb8(evidence) => { + let _ = evidence.profile_id(); + let _ = evidence.criterion(); + let _ = evidence.foreground_luminance(); + let _ = evidence.background_luminance(); + let _ = evidence.numerical_evidence(); + } + } + let binding = assessment.binding(); + let PackageProgramPhysicalPointV1::EncodedSrgb8SourceOver(physical) = + binding.physical(); + let _ = physical.subject_paint().value(); + let _ = physical.backdrop_surface().value(); + let _: Srgb8 = physical.subject(); + let _ = physical.opacity(); + let _: Srgb8 = physical.backdrop(); + let _: Srgb8 = physical.visible(); + let PackageProgramModeledPointV1::Iec61966Srgb8ToCie1931TwoDegreeXyzD65RelativeY1( + modeled, + ) = binding.modeled(); + let _: [f64; 3] = modeled.xyz(); + let context = modeled.appearance_context(); + let _ = context.adapting_luminance_cd_m2(); + let _ = context.background_luminance_ratio_yb_yw(); + let _ = context.surround(); + }}; + } + match certificate { + PackageProgramCertificateV1::Verified(verified) => { + let _ = verified.selected_state_index(); + for cell in exact_size(verified.cells()) { + inspect_cell!(cell); + } + for output in exact_size(verified.outputs()) { + let _ = output.output_slot().value(); + let _ = output.paint().value(); + let _: Srgb8 = output.source(); + let _ = output.opacity(); + } + } + PackageProgramCertificateV1::Conflict(conflict) => { + let _ = conflict.considered_state_count(); + for cell in exact_size(conflict.cells()) { + let _ = cell.state_index(); + inspect_cell!(cell); + } + } + } } for operation in exact_size(view.operations()) { match operation { - PackageProgramOperationV1::Set { - output_slot, - source, - opacity, - certificate_index, - } => { - let _: PackageProgramOutputSlotIdV1 = output_slot; - let _: Srgb8 = source; - assert!(opacity.is_finite() && (0.0..=1.0).contains(&opacity)); - assert!(certificate_index < certificate_count); + PackageProgramOperationV1::Set(set) => { + let _: PackageProgramOutputSlotIdV1 = set.output_slot(); + let _: Srgb8 = set.source(); + assert!(set.opacity().is_finite() && (0.0..=1.0).contains(&set.opacity())); + let _ = set.certificate().content_identity(); } - PackageProgramOperationV1::Remove { output_slot } => { - let _: PackageProgramOutputSlotIdV1 = output_slot; + PackageProgramOperationV1::Remove(remove) => { + let _: PackageProgramOutputSlotIdV1 = remove.output_slot(); } - PackageProgramOperationV1::Hold { - output_slot, - certificate_index, - } => { - let _: PackageProgramOutputSlotIdV1 = output_slot; - assert!(certificate_index < certificate_count); + PackageProgramOperationV1::Hold(hold) => { + let _: PackageProgramOutputSlotIdV1 = hold.output_slot(); + let _ = hold.certificate().content_identity(); } } } + let _ = certificate_count; } #[allow(dead_code)] @@ -104,7 +175,7 @@ fn owner_expiry_is_a_closed_package_error( } #[test] -fn red_contract_is_linked_by_the_concrete_package_module() { +fn external_boundary_uses_only_closed_concrete_types() { // Reaching this test means the external crate compiled without importing // Program, evaluator traits, Session, or numeric generations. assert_eq!(core::mem::size_of::(), 3); @@ -244,15 +315,15 @@ fn external_authoring_lowers_the_actual_closed_program_and_returns_canonical_inp }) .unwrap(); assert_eq!(ready.kind(), PackageProgramStateKindV1::Ready); - assert_eq!( - ready.operations().collect::>(), - [PackageProgramOperationV1::Set { - output_slot: output, - source: Srgb8::new([0; 3]), - opacity: 1.0, - certificate_index: 0, - }] - ); + let mut operations = ready.operations(); + let Some(PackageProgramOperationV1::Set(set)) = operations.next() else { + panic!("Ready must emit one Set operation"); + }; + assert_eq!(set.output_slot(), output); + assert_eq!(set.source(), Srgb8::new([0; 3])); + assert_eq!(set.opacity(), 1.0); + assert_eq!(set.certificate().observation().revision(), 1); + assert!(operations.next().is_none()); } #[test] @@ -294,15 +365,81 @@ fn every_physical_constructor_and_both_remaining_constraint_modes_execute() { }) .unwrap(); assert_eq!(state.kind(), PackageProgramStateKindV1::Ready); + let Some(PackageProgramCertificateV1::Verified(certificate)) = state.certificates().next() + else { + panic!("a fixed target must produce one Verified certificate"); + }; + assert_eq!(certificate.selected_state_index(), None); + let mut operations = state.operations(); + let Some(PackageProgramOperationV1::Set(set)) = operations.next() else { + panic!("Ready must emit one Set operation"); + }; + assert_eq!(set.output_slot(), PackageProgramOutputSlotIdV1::new(12)); + assert_eq!(set.source(), Srgb8::new([0; 3])); + assert_eq!(set.opacity(), 1.0); + assert_eq!(set.certificate().observation().revision(), 1); + assert!(operations.next().is_none()); +} + +#[test] +fn certificate_and_set_retain_the_same_nonunit_opacity() { + let source = PackageProgramSourceIdV1::new(1); + let target = PackageProgramTargetIdV1::new(2); + let opacity = PackageProgramOpacityInputIdV1::new(3); + let solid = PackageProgramPaintIdV1::new(4); + let translucent = PackageProgramPaintIdV1::new(5); + let input = PackageProgramSurfaceInputPortIdV1::new(6); + let surface = PackageProgramSurfaceIdV1::new(7); + let occurrence = PackageProgramOccurrenceIdV1::new(8); + let constraint = PackageProgramConstraintIdV1::new(9); + let output = PackageProgramOutputSlotIdV1::new(10); + let context = + PackageProgramAppearanceContextV1::try_new(64.0, 0.2, PackageProgramSurroundV1::Average) + .unwrap(); + let mut draft = PackageProgramDraftV1::new(); + draft.push_source(source, Srgb8::new([0; 3])); + draft.push_fixed_target(target, source); + draft.push_surface_input_port(input); + draft.push_opacity_input(opacity, 0.5); + draft.push_solid_paint(solid, target); + draft.push_opacity_paint(translucent, solid, opacity); + draft.push_input_surface(surface, input); + draft.push_source_over_occurrence(occurrence, translucent, surface, context); + draft.push_exact_hard(constraint, occurrence, Srgb8::new([0x80; 3])); + draft.push_output(output, translucent); + + let owner = draft.compile().unwrap(); + let mut session = owner.instantiate(17).unwrap(); + let white = [Srgb8::new([0xFF; 3])]; + let scenarios = [PackageProgramScenarioV1::new(1, &white)]; + let state = session + .update(PackageProgramUpdateV1::Observed { + revision: 1, + scenarios: &scenarios, + }) + .unwrap(); + let Some(PackageProgramCertificateV1::Verified(certificate)) = state.certificates().next() + else { + panic!("the exact emitted midpoint must be verified"); + }; + let PackageProgramAssessmentV1::ExactSrgb8(assessment) = + certificate.cells().next().unwrap().assessment() + else { + panic!("the authored exact constraint must retain Exact evidence"); + }; + let PackageProgramPhysicalPointV1::EncodedSrgb8SourceOver(physical) = + assessment.binding().physical(); + assert_eq!(physical.opacity().to_bits(), 0.5_f64.to_bits()); + assert_eq!(physical.visible(), Srgb8::new([0x80; 3])); assert_eq!( - state.operations().collect::>(), - [PackageProgramOperationV1::Set { - output_slot: PackageProgramOutputSlotIdV1::new(12), - source: Srgb8::new([0; 3]), - opacity: 1.0, - certificate_index: 0, - }] + certificate.outputs().next().unwrap().opacity().to_bits(), + physical.opacity().to_bits() ); + + let Some(PackageProgramOperationV1::Set(set)) = state.operations().next() else { + panic!("the verified output must emit one Set"); + }; + assert_eq!(set.opacity().to_bits(), physical.opacity().to_bits()); } #[test] From ab463f261e73e971511025f4038d63eee7f4c46f Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Sun, 26 Jul 2026 03:14:32 +0300 Subject: [PATCH 42/58] 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 6cd37887..b4e47788 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":"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"} +{"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":"491f909d707f002a8fe2b6b04337ba6f7d044fd5e2b85585a8c978feb930b36c","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":"b25636d4ae969d4a93a82a1324fb4445ba4861a51f18a85c4a0a062afcfbbfda","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":"09be54900efe29ffdac8705efd0d6d613055c90d634446ca4b228f51a63997d0"},{"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":"e73b9c0b8c3a4112cb53987753d5a6b5f639774b0b872afaea9836e6647a2f7d"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/wcag22.rs","sha256":"856093c91159d8b3faab001f2d6524d33d7b16458a5a4e98ea65f8c62ab2694c"},{"kind":"rust-source","path":"crates/labcolors-core/src/hash.rs","sha256":"f97a0fd7d6ad3162f0f1dfb326fccfb7ed40da9a8fa67a5b8a239a1ae2ae49c3"},{"kind":"rust-source","path":"crates/labcolors-core/src/lcs_occurrence.rs","sha256":"6f202ad7425a235b9d18caba0c817fc33a2b8e042050a34f5ddff3fd09efc53d"},{"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":"0fa29533dab84d51af7993bb04dbef7fb978ca55499ea5e1ade282cbc84e6818"},{"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":"16c7fa1a3b1dee795b4434329ebf329066468eeb309213960e35698255b0dd9c"} diff --git a/scripts/verify_point_support_surplus.py b/scripts/verify_point_support_surplus.py index 608c6cdb..084cfd3b 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 = ( - "246c77dbf4923aca4cdaedb12be910ceb2885c94e4e42f652cdcba9b9417a427" + "b25636d4ae969d4a93a82a1324fb4445ba4861a51f18a85c4a0a062afcfbbfda" ) EXPECTED_Q55_PROOF_SHA256 = ( "ac59cf89503170c789223b91d775213a19d4e571ef930f2ea609fcd51b14defd" From 5cd8d17c6f3df5cb6bc1a0b11dfca2c1f77b9712 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Fri, 24 Jul 2026 23:49:00 +0300 Subject: [PATCH 43/58] core: bind Program operations to exact owner snapshots --- .../src/generic_boundary_tests.rs | 135 +++ crates/labcolors-core/src/observation.rs | 4 + crates/labcolors-core/src/package_bridge.rs | 516 ++++++++--- .../src/program_mixed_evaluator_tests.rs | 830 +++++++++++++++--- crates/labcolors-core/src/program_session.rs | 21 + crates/labcolors-core/src/session.rs | 25 +- .../labcolors-core/tests/program_boundary.rs | 105 ++- 7 files changed, 1333 insertions(+), 303 deletions(-) diff --git a/crates/labcolors-core/src/generic_boundary_tests.rs b/crates/labcolors-core/src/generic_boundary_tests.rs index f563773f..f3b5f8f1 100644 --- a/crates/labcolors-core/src/generic_boundary_tests.rs +++ b/crates/labcolors-core/src/generic_boundary_tests.rs @@ -127,6 +127,141 @@ fn package_authoring_is_one_thin_concrete_core_draft_without_a_second_graph() { } } +#[test] +fn package_session_keeps_evidence_but_owner_alone_grants_updates_and_operations() { + assert_eq!( + normalized_source_scope( + PACKAGE_BRIDGE_SOURCE, + "pub struct PackageProgramSessionV1 {", + "impl PackageProgramSessionV1", + ), + concat!( + "pub struct PackageProgramSessionV1 { ", + "scenario_order_scratch: Vec, ", + "session: CoreProgramSessionV1, ", + "}", + ), + "the package Session must not duplicate owner schema, outputs, stream, or lifecycle state", + ); + + let session_api = source_scope( + PACKAGE_BRIDGE_SOURCE, + "impl PackageProgramSessionV1 {", + "struct PackageProgramScenarioSourceV1", + ); + assert_eq!( + session_api.matches("pub fn evidence(").count(), + 1, + "historical evidence is the Session's sole public projection", + ); + assert_eq!( + session_api.matches("pub ").count(), + 1, + "Session must not expose a second public authority by changing function qualifiers", + ); + for forbidden in [ + "pub fn state(", + "pub fn update(", + "pub fn surface_input_port_count(", + "pub fn surface_input_ports(", + "pub fn output_slots(", + ] { + assert!( + !session_api.contains(forbidden), + "Session must not regain owner authority through `{forbidden}`", + ); + } + + let evidence_api = source_scope( + PACKAGE_BRIDGE_SOURCE, + "impl<'a> PackageProgramEvidenceViewV1<'a> {", + "struct PackageProgramBorrowScopeV1<'owner, 'session>", + ); + for forbidden in [ + "pub fn revision(", + "pub const fn revision(", + "pub fn stream(", + "pub const fn stream(", + ] { + assert!( + !evidence_api.contains(forbidden), + "evidence must not flatten atomic raw-head provenance through `{forbidden}`", + ); + } + + let owner_api = source_scope( + PACKAGE_BRIDGE_SOURCE, + "impl PackageProgramOwnerV1 {", + "pub struct PackageProgramScenarioV1<'a> {", + ); + for required in [ + "pub fn project<'owner, 'session>(", + "pub fn update<'owner, 'session>(", + ".owns_session(&session.session)", + ] { + assert!( + owner_api.contains(required), + "the exact owner must remain the only operation authority; missing `{required}`", + ); + } + let update = source_scope( + owner_api, + "pub fn update<'owner, 'session>(", + "pub fn instantiate(", + ); + assert!( + update + .find(".owns_session(&session.session)") + .expect("owner update must preflight exact membership") + < update + .find("session.apply_update(update)?") + .expect("owner update must delegate one atomic Session update"), + "owner mismatch must be rejected before admission, allocation, or evaluation", + ); + + let public_access_errors = source_scope( + PACKAGE_BRIDGE_SOURCE, + "pub enum PackageProgramAccessErrorV1 {", + "impl PackageProgramOwnerV1", + ); + assert!( + public_access_errors.contains("OwnerMismatch,") + && !public_access_errors.contains("OwnerExpired"), + "operation projection must distinguish foreign ownership, not expose internal expiry", + ); + let public_update_errors = source_scope( + PACKAGE_BRIDGE_SOURCE, + "pub enum PackageProgramUpdateErrorKindV1 {", + "pub struct PackageProgramUpdateErrorV1 {", + ); + assert!( + public_update_errors.contains("OwnerMismatch,") + && !public_update_errors.contains("OwnerExpired"), + "owner expiry is an internal invariant after a matching owner borrow", + ); + + for (payload, end) in [ + ( + "pub struct PackageProgramSetV1<'owner, 'session> {", + "impl<'session> PackageProgramSetV1<'_, 'session>", + ), + ( + "pub struct PackageProgramRemoveV1<'owner, 'session> {", + "impl PackageProgramRemoveV1<'_, '_>", + ), + ( + "pub struct PackageProgramHoldV1<'owner, 'session> {", + "impl<'session> PackageProgramHoldV1<'_, 'session>", + ), + ] { + assert!( + source_scope(PACKAGE_BRIDGE_SOURCE, payload, end) + .contains("_scope: PackageProgramBorrowScopeV1<'owner, 'session>,"), + "{payload} must retain both owner and immutable Session borrows", + ); + } +} + #[test] fn shared_observation_ssot_has_one_backing_without_lifecycle_or_adapter_facades() { assert_eq!( diff --git a/crates/labcolors-core/src/observation.rs b/crates/labcolors-core/src/observation.rs index d2549f3c..1cab558e 100644 --- a/crates/labcolors-core/src/observation.rs +++ b/crates/labcolors-core/src/observation.rs @@ -72,6 +72,10 @@ impl UnknownReasonId { pub(crate) const fn new(raw: u32) -> Self { Self(raw) } + + pub(crate) const fn value(self) -> u32 { + self.0 + } } /// One raw surface-input binding inside a correlated scenario. diff --git a/crates/labcolors-core/src/package_bridge.rs b/crates/labcolors-core/src/package_bridge.rs index 7e594b19..42330136 100644 --- a/crates/labcolors-core/src/package_bridge.rs +++ b/crates/labcolors-core/src/package_bridge.rs @@ -8,6 +8,7 @@ //! transport words, strings, or lifecycle generations. use core::iter::FusedIterator; +use core::marker::PhantomData; use core::slice; use crate::Srgb8; @@ -26,8 +27,8 @@ use crate::lcs_occurrence::{ }; use crate::numerics::NumericalDecisionEvidenceV1; use crate::observation::{ - ObservationError, ObservationPayloadInput, ObservationStreamId, ObservationUpdateInput, - Revision, ScenarioId, SchemaOrderedScenarioSourceV1, UnknownReasonId, + ObservationError, ObservationHeadViewV1, ObservationStreamId, Revision, ScenarioId, + SchemaOrderedScenarioSourceV1, UnknownReasonId, }; use crate::program_session::{ CompiledCoreProgramV1, CompositionProfile, ConstraintId, ConstraintInvocation, @@ -997,14 +998,19 @@ impl Default for PackageProgramDraftV1 { /// Opaque strong owner of one exact compiled Core Program. /// -/// Sessions instantiated from this owner are independently mutable. In the -/// terminal stacked build they retain only the canonical weak owner binding; -/// dropping this value therefore expires every such Session before its next -/// admission. +/// Sessions instantiated from this owner are independently mutable only +/// through this exact allocation. Dropping it revokes updates and operation +/// projections; Session-owned historical evidence remains readable. pub struct PackageProgramOwnerV1 { compiled: CompiledCoreProgramV1, } +/// A Session belongs to a different immutable owner allocation. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PackageProgramAccessErrorV1 { + OwnerMismatch, +} + impl PackageProgramOwnerV1 { /// Internal handoff from the canonical concrete Core draft compiler. pub(crate) const fn from_compiled(compiled: CompiledCoreProgramV1) -> Self { @@ -1034,22 +1040,53 @@ impl PackageProgramOwnerV1 { .map(|(slot, _paint)| PackageProgramOutputSlotIdV1::from_core(slot)) } + /// Borrow one operation projection only for the exact Session generation + /// instantiated by this owner. Equivalent content is not authority. + pub fn project<'owner, 'session>( + &'owner self, + session: &'session PackageProgramSessionV1, + ) -> Result, PackageProgramAccessErrorV1> { + if !self.compiled.owns_session(&session.session) { + return Err(PackageProgramAccessErrorV1::OwnerMismatch); + } + Ok(PackageProgramProjectionV1 { + evidence: session.evidence(), + owner: self, + scope: PackageProgramBorrowScopeV1::new(self, session), + }) + } + + /// Admit and commit one update only when owner and Session are the exact + /// same generation, then borrow the resulting immutable snapshot. + pub fn update<'owner, 'session>( + &'owner self, + session: &'session mut PackageProgramSessionV1, + update: PackageProgramUpdateV1<'_>, + ) -> Result, PackageProgramUpdateErrorV1> { + if !self.compiled.owns_session(&session.session) { + return Err(PackageProgramUpdateErrorV1::new( + PackageProgramUpdateErrorKindV1::OwnerMismatch, + )); + } + session.apply_update(update)?; + Ok(PackageProgramProjectionV1 { + evidence: session.evidence(), + owner: self, + scope: PackageProgramBorrowScopeV1::new(self, session), + }) + } + /// Instantiate one stream-affine Session without exposing a generation. pub fn instantiate( &self, stream_id: u32, ) -> Result { - let surface_input_ports = try_copy_surface_input_ports(&self.compiled)?; - let output_slots = try_copy_output_slots(&self.compiled)?; let stream = ObservationStreamId::new(stream_id); let session = self .compiled .instantiate(stream) .map_err(PackageProgramInstantiateErrorV1::from_core)?; Ok(PackageProgramSessionV1 { - stream, - surface_input_ports, - output_slots, scenario_order_scratch: Vec::new(), session, }) @@ -1091,46 +1128,23 @@ pub enum PackageProgramUpdateV1<'a> { /// Concrete opaque owner of one mutable Core Program Session. pub struct PackageProgramSessionV1 { - stream: ObservationStreamId, - surface_input_ports: Box<[PackageProgramSurfaceInputPortIdV1]>, - output_slots: Box<[PackageProgramOutputSlotIdV1]>, scenario_order_scratch: Vec, session: CoreProgramSessionV1, } impl PackageProgramSessionV1 { - /// Number of schema-ordered values required in every observed scenario. - pub fn surface_input_port_count(&self) -> usize { - self.surface_input_ports.len() - } - - /// Canonically ordered authored input handles for one-time host binding. - pub fn surface_input_ports( - &self, - ) -> impl ExactSizeIterator + '_ { - self.surface_input_ports.iter().copied() - } - - /// Canonically ordered opaque output slots for one-time host binding. - pub fn output_slots(&self) -> impl ExactSizeIterator + '_ { - self.output_slots.iter().copied() - } - - /// Allocation-free view of the current Core-owned lifecycle state. - pub fn state(&self) -> PackageProgramStateViewV1<'_> { - let revision = self.session.raw_head().revision().map(Revision::value); - PackageProgramStateViewV1 { - state: self.session.state(), - revision, - output_slots: &self.output_slots, + /// Historical evidence remains readable after owner expiry. It never + /// grants an operation projection. + pub fn evidence(&self) -> PackageProgramEvidenceViewV1<'_> { + PackageProgramEvidenceViewV1 { + session: &self.session, } } - /// Admit, evaluate and atomically commit one revision before projecting it. - pub fn update( + fn apply_update( &mut self, update: PackageProgramUpdateV1<'_>, - ) -> Result, PackageProgramUpdateErrorV1> { + ) -> Result<(), PackageProgramUpdateErrorV1> { match update { PackageProgramUpdateV1::Observed { revision, @@ -1150,15 +1164,11 @@ impl PackageProgramSessionV1 { reason_id, } => { self.session - .update(ObservationUpdateInput { - stream: self.stream, - revision: Revision::new(revision), - payload: ObservationPayloadInput::Unknown(UnknownReasonId::new(reason_id)), - }) + .update_unknown(Revision::new(revision), UnknownReasonId::new(reason_id)) .map_err(map_session_update_error)?; } } - Ok(self.state()) + Ok(()) } } @@ -1191,17 +1201,37 @@ pub enum PackageProgramStateKindV1 { Failed, } -/// Borrowed projection of one complete Core-owned lifecycle state. +/// Closed raw observation head, independent of evaluator lifecycle. +/// +/// A non-empty head retains stream provenance for detached correlation, never +/// as operation authority. `Empty` carries none because no observation exists. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PackageProgramObservationHeadV1 { + Empty, + Unknown { + stream: PackageProgramStreamIdV1, + revision: u64, + reason_id: u32, + }, + Observed { + stream: PackageProgramStreamIdV1, + revision: u64, + }, +} + +/// Borrowed historical evidence owned solely by one Session. #[derive(Clone, Copy)] -pub struct PackageProgramStateViewV1<'a> { - state: &'a CoreProgramStateV1, - revision: Option, - output_slots: &'a [PackageProgramOutputSlotIdV1], +pub struct PackageProgramEvidenceViewV1<'a> { + session: &'a CoreProgramSessionV1, } -impl<'a> PackageProgramStateViewV1<'a> { +impl<'a> PackageProgramEvidenceViewV1<'a> { + const fn state(self) -> &'a CoreProgramStateV1 { + self.session.state() + } + pub const fn kind(self) -> PackageProgramStateKindV1 { - match self.state { + match self.state() { SessionState::Waiting => PackageProgramStateKindV1::Waiting, SessionState::Ready { .. } => PackageProgramStateKindV1::Ready, SessionState::Stale { .. } => PackageProgramStateKindV1::Stale, @@ -1209,14 +1239,26 @@ impl<'a> PackageProgramStateViewV1<'a> { } } - /// Current raw-head revision; only the initial Waiting state has none. - pub const fn revision(self) -> Option { - self.revision + pub fn observation_head(self) -> PackageProgramObservationHeadV1 { + match self.session.raw_head() { + ObservationHeadViewV1::Empty => PackageProgramObservationHeadV1::Empty, + ObservationHeadViewV1::Unknown(unknown) => PackageProgramObservationHeadV1::Unknown { + stream: PackageProgramStreamIdV1::from_core(unknown.stream()), + revision: unknown.revision().value(), + reason_id: unknown.reason().value(), + }, + ObservationHeadViewV1::Observed(observation) => { + PackageProgramObservationHeadV1::Observed { + stream: PackageProgramStreamIdV1::from_core(observation.stream()), + revision: observation.revision().value(), + } + } + } } /// Failed-state cause ordinal inside [`Self::certificates`]. pub const fn cause_certificate_index(self) -> Option { - match self.state { + match self.state() { SessionState::Failed { .. } => Some(0), SessionState::Waiting | SessionState::Ready { .. } | SessionState::Stale { .. } => None, } @@ -1226,7 +1268,7 @@ impl<'a> PackageProgramStateViewV1<'a> { pub fn certificates( self, ) -> impl ExactSizeIterator> + FusedIterator + 'a { - let (first, second) = match self.state { + let (first, second) = match self.state() { SessionState::Waiting => (None, None), SessionState::Ready { current } | SessionState::Stale { previous: current } => { (Some(PackageProgramCertificateV1::verified(current)), None) @@ -1238,40 +1280,86 @@ impl<'a> PackageProgramStateViewV1<'a> { }; PackageProgramCertificatesV1::new(first, second) } +} + +/// Zero-sized lifetime marker tying an operation projection to one exact live +/// owner and one immutable Session snapshot. +/// +/// This constrains borrowed Rust values; it is not a sink commit capability. +#[derive(Clone, Copy)] +struct PackageProgramBorrowScopeV1<'owner, 'session> { + _scope: PhantomData<( + &'owner PackageProgramOwnerV1, + &'session PackageProgramSessionV1, + )>, +} + +impl<'owner, 'session> PackageProgramBorrowScopeV1<'owner, 'session> { + const fn new( + _owner: &'owner PackageProgramOwnerV1, + _session: &'session PackageProgramSessionV1, + ) -> Self { + Self { + _scope: PhantomData, + } + } +} + +/// Owner-and-snapshot-validated projection. Historical evidence is available +/// separately through [`PackageProgramSessionV1::evidence`]. +#[derive(Clone, Copy)] +pub struct PackageProgramProjectionV1<'owner, 'session> { + evidence: PackageProgramEvidenceViewV1<'session>, + owner: &'owner PackageProgramOwnerV1, + scope: PackageProgramBorrowScopeV1<'owner, 'session>, +} + +impl<'owner, 'session> PackageProgramProjectionV1<'owner, 'session> { + pub const fn evidence(self) -> PackageProgramEvidenceViewV1<'session> { + self.evidence + } /// Total canonical output projection for this lifecycle state. pub fn operations( self, - ) -> impl ExactSizeIterator> + FusedIterator + 'a { - let inner = match self.state { + ) -> impl ExactSizeIterator> + FusedIterator + { + let inner = match self.evidence.state() { SessionState::Waiting => PackageProgramOperationSourceV1::Empty, SessionState::Ready { current } => { - debug_assert_eq!(current.outputs().len(), self.output_slots.len()); + debug_assert_eq!(current.outputs().len(), self.owner.compiled.output_count()); debug_assert!( current .outputs() .iter() - .zip(self.output_slots) - .all(|(output, slot)| output.output().value() == slot.value()) + .enumerate() + .all(|(index, output)| self.owner.compiled.output_slot_at(index) + == Some(output.output())) ); PackageProgramOperationSourceV1::Set { outputs: current.outputs().iter(), certificate: PackageProgramVerifiedCertificateV1 { inner: current }, + scope: self.scope, } } SessionState::Stale { previous } => PackageProgramOperationSourceV1::Hold { - slots: self.output_slots.iter(), + outputs: previous.outputs().iter(), certificate: PackageProgramVerifiedCertificateV1 { inner: previous }, + scope: self.scope, }, SessionState::Failed { previous: Some(previous), .. } => PackageProgramOperationSourceV1::Hold { - slots: self.output_slots.iter(), + outputs: previous.outputs().iter(), certificate: PackageProgramVerifiedCertificateV1 { inner: previous }, + scope: self.scope, }, SessionState::Failed { previous: None, .. } => { - PackageProgramOperationSourceV1::Remove(self.output_slots.iter()) + PackageProgramOperationSourceV1::Remove { + slots: PackageProgramOwnerOutputSlotsV1::new(&self.owner.compiled), + scope: self.scope, + } } }; PackageProgramOperationsV1 { inner } @@ -1369,6 +1457,28 @@ impl<'a> PackageProgramConflictCertificateV1<'a> { } /// Closed borrowed projection of one exact Core-owned certificate. +/// +/// A certificate borrows only Session-owned history, so it can outlive the +/// owner that authorized the projection from which it was read. +/// +/// ```no_run +/// use labcolors_core::package_bridge::{ +/// PackageProgramCertificateV1, PackageProgramOwnerV1, PackageProgramSessionV1, +/// }; +/// +/// fn retain_evidence<'session>( +/// owner: PackageProgramOwnerV1, +/// session: &'session PackageProgramSessionV1, +/// ) -> PackageProgramCertificateV1<'session> { +/// owner +/// .project(session) +/// .unwrap() +/// .evidence() +/// .certificates() +/// .next() +/// .unwrap() +/// } +/// ``` #[derive(Clone, Copy)] pub enum PackageProgramCertificateV1<'a> { Verified(PackageProgramVerifiedCertificateV1<'a>), @@ -1876,12 +1986,13 @@ impl<'a> PackageProgramCertifiedOutputV1<'a> { /// A Set operation is structurally tied to the exact Verified certificate. #[derive(Clone, Copy)] -pub struct PackageProgramSetV1<'a> { - output: &'a ProgramOutputV1, - certificate: PackageProgramVerifiedCertificateV1<'a>, +pub struct PackageProgramSetV1<'owner, 'session> { + output: &'session ProgramOutputV1, + certificate: PackageProgramVerifiedCertificateV1<'session>, + _scope: PackageProgramBorrowScopeV1<'owner, 'session>, } -impl<'a> PackageProgramSetV1<'a> { +impl<'session> PackageProgramSetV1<'_, 'session> { pub const fn output_slot(self) -> PackageProgramOutputSlotIdV1 { PackageProgramOutputSlotIdV1::from_core((*self.output).output()) } @@ -1894,17 +2005,18 @@ impl<'a> PackageProgramSetV1<'a> { (*self.output).paint().opacity().value() } - pub const fn certificate(self) -> PackageProgramVerifiedCertificateV1<'a> { + pub const fn certificate(self) -> PackageProgramVerifiedCertificateV1<'session> { self.certificate } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct PackageProgramRemoveV1 { +#[derive(Clone, Copy)] +pub struct PackageProgramRemoveV1<'owner, 'session> { output_slot: PackageProgramOutputSlotIdV1, + _scope: PackageProgramBorrowScopeV1<'owner, 'session>, } -impl PackageProgramRemoveV1 { +impl PackageProgramRemoveV1<'_, '_> { pub const fn output_slot(self) -> PackageProgramOutputSlotIdV1 { self.output_slot } @@ -1912,27 +2024,126 @@ impl PackageProgramRemoveV1 { /// A Hold operation is structurally tied to the retained Verified certificate. #[derive(Clone, Copy)] -pub struct PackageProgramHoldV1<'a> { - output_slot: PackageProgramOutputSlotIdV1, - certificate: PackageProgramVerifiedCertificateV1<'a>, +pub struct PackageProgramHoldV1<'owner, 'session> { + output: &'session ProgramOutputV1, + certificate: PackageProgramVerifiedCertificateV1<'session>, + _scope: PackageProgramBorrowScopeV1<'owner, 'session>, } -impl<'a> PackageProgramHoldV1<'a> { +impl<'session> PackageProgramHoldV1<'_, 'session> { pub const fn output_slot(self) -> PackageProgramOutputSlotIdV1 { - self.output_slot + PackageProgramOutputSlotIdV1::from_core((*self.output).output()) } - pub const fn certificate(self) -> PackageProgramVerifiedCertificateV1<'a> { + pub const fn certificate(self) -> PackageProgramVerifiedCertificateV1<'session> { self.certificate } } /// Closed total operation union over opaque output slots. +/// +/// Every payload borrows both the exact owner and immutable Session snapshot; +/// destructuring a `Remove` cannot erase that scope. +/// Copied slot/source/opacity values are data only: a runtime adapter must +/// recheck its live owner, Session and revision immediately before one atomic +/// sink commit. +/// +/// ```compile_fail,E0515 +/// use labcolors_core::package_bridge::{ +/// PackageProgramOperationV1, PackageProgramOwnerV1, PackageProgramRemoveV1, +/// PackageProgramSessionV1, +/// }; +/// +/// fn escape_remove<'session>( +/// owner: PackageProgramOwnerV1, +/// session: &'session PackageProgramSessionV1, +/// ) -> PackageProgramRemoveV1<'session, 'session> { +/// match owner.project(session).unwrap().operations().next().unwrap() { +/// PackageProgramOperationV1::Remove(remove) => remove, +/// _ => panic!("fixture supplies Remove"), +/// } +/// } +/// ``` +/// +/// ```compile_fail,E0515 +/// use labcolors_core::package_bridge::{ +/// PackageProgramOperationV1, PackageProgramOwnerV1, PackageProgramRemoveV1, +/// }; +/// +/// fn escape_local_session<'owner>( +/// owner: &'owner PackageProgramOwnerV1, +/// ) -> PackageProgramRemoveV1<'owner, 'owner> { +/// let session = owner.instantiate(1).unwrap(); +/// match owner.project(&session).unwrap().operations().next().unwrap() { +/// PackageProgramOperationV1::Remove(remove) => remove, +/// _ => panic!("fixture supplies Remove"), +/// } +/// } +/// ``` +/// +/// ```compile_fail,E0515 +/// use labcolors_core::package_bridge::{ +/// PackageProgramOperationV1, PackageProgramOwnerV1, PackageProgramSessionV1, +/// PackageProgramSetV1, +/// }; +/// +/// fn escape_set<'session>( +/// owner: PackageProgramOwnerV1, +/// session: &'session PackageProgramSessionV1, +/// ) -> PackageProgramSetV1<'session, 'session> { +/// match owner.project(session).unwrap().operations().next().unwrap() { +/// PackageProgramOperationV1::Set(set) => set, +/// _ => panic!("fixture supplies Set"), +/// } +/// } +/// ``` +/// +/// ```compile_fail,E0515 +/// use labcolors_core::package_bridge::{ +/// PackageProgramHoldV1, PackageProgramOperationV1, PackageProgramOwnerV1, +/// PackageProgramSessionV1, +/// }; +/// +/// fn escape_hold<'session>( +/// owner: PackageProgramOwnerV1, +/// session: &'session PackageProgramSessionV1, +/// ) -> PackageProgramHoldV1<'session, 'session> { +/// match owner.project(session).unwrap().operations().next().unwrap() { +/// PackageProgramOperationV1::Hold(hold) => hold, +/// _ => panic!("fixture supplies Hold"), +/// } +/// } +/// ``` +/// +/// ```compile_fail,E0502 +/// use labcolors_core::package_bridge::{ +/// PackageProgramOperationV1, PackageProgramOwnerV1, PackageProgramSessionV1, +/// PackageProgramUpdateV1, +/// }; +/// +/// fn remove_blocks_session_mutation( +/// owner: &PackageProgramOwnerV1, +/// session: &mut PackageProgramSessionV1, +/// ) { +/// let remove = match owner.project(session).unwrap().operations().next().unwrap() { +/// PackageProgramOperationV1::Remove(remove) => remove, +/// _ => return, +/// }; +/// let _second = owner.update( +/// session, +/// PackageProgramUpdateV1::Unknown { +/// revision: 2, +/// reason_id: 7, +/// }, +/// ); +/// let _slot = remove.output_slot(); +/// } +/// ``` #[derive(Clone, Copy)] -pub enum PackageProgramOperationV1<'a> { - Set(PackageProgramSetV1<'a>), - Remove(PackageProgramRemoveV1), - Hold(PackageProgramHoldV1<'a>), +pub enum PackageProgramOperationV1<'owner, 'session> { + Set(PackageProgramSetV1<'owner, 'session>), + Remove(PackageProgramRemoveV1<'owner, 'session>), + Hold(PackageProgramHoldV1<'owner, 'session>), } struct PackageProgramCertificatesV1<'a> { @@ -1977,25 +2188,67 @@ impl<'a> Iterator for PackageProgramCertificatesV1<'a> { impl ExactSizeIterator for PackageProgramCertificatesV1<'_> {} impl FusedIterator for PackageProgramCertificatesV1<'_> {} -enum PackageProgramOperationSourceV1<'a> { +struct PackageProgramOwnerOutputSlotsV1<'owner> { + compiled: &'owner CompiledCoreProgramV1, + index: usize, + len: usize, +} + +impl<'owner> PackageProgramOwnerOutputSlotsV1<'owner> { + fn new(compiled: &'owner CompiledCoreProgramV1) -> Self { + Self { + compiled, + index: 0, + len: compiled.output_count(), + } + } +} + +impl Iterator for PackageProgramOwnerOutputSlotsV1<'_> { + type Item = PackageProgramOutputSlotIdV1; + + fn next(&mut self) -> Option { + if self.index == self.len { + return None; + } + let output = self.compiled.output_slot_at(self.index)?; + self.index += 1; + Some(PackageProgramOutputSlotIdV1::from_core(output)) + } + + fn size_hint(&self) -> (usize, Option) { + let remaining = self.len - self.index; + (remaining, Some(remaining)) + } +} + +impl ExactSizeIterator for PackageProgramOwnerOutputSlotsV1<'_> {} +impl FusedIterator for PackageProgramOwnerOutputSlotsV1<'_> {} + +enum PackageProgramOperationSourceV1<'owner, 'session> { Empty, Set { - outputs: slice::Iter<'a, ProgramOutputV1>, - certificate: PackageProgramVerifiedCertificateV1<'a>, + outputs: slice::Iter<'session, ProgramOutputV1>, + certificate: PackageProgramVerifiedCertificateV1<'session>, + scope: PackageProgramBorrowScopeV1<'owner, 'session>, }, Hold { - slots: slice::Iter<'a, PackageProgramOutputSlotIdV1>, - certificate: PackageProgramVerifiedCertificateV1<'a>, + outputs: slice::Iter<'session, ProgramOutputV1>, + certificate: PackageProgramVerifiedCertificateV1<'session>, + scope: PackageProgramBorrowScopeV1<'owner, 'session>, + }, + Remove { + slots: PackageProgramOwnerOutputSlotsV1<'owner>, + scope: PackageProgramBorrowScopeV1<'owner, 'session>, }, - Remove(slice::Iter<'a, PackageProgramOutputSlotIdV1>), } -struct PackageProgramOperationsV1<'a> { - inner: PackageProgramOperationSourceV1<'a>, +struct PackageProgramOperationsV1<'owner, 'session> { + inner: PackageProgramOperationSourceV1<'owner, 'session>, } -impl<'a> Iterator for PackageProgramOperationsV1<'a> { - type Item = PackageProgramOperationV1<'a>; +impl<'owner, 'session> Iterator for PackageProgramOperationsV1<'owner, 'session> { + type Item = PackageProgramOperationV1<'owner, 'session>; fn next(&mut self) -> Option { match &mut self.inner { @@ -2003,22 +2256,28 @@ impl<'a> Iterator for PackageProgramOperationsV1<'a> { PackageProgramOperationSourceV1::Set { outputs, certificate, + scope, } => { let output = outputs.next()?; Some(PackageProgramOperationV1::Set(PackageProgramSetV1 { output, certificate: *certificate, + _scope: *scope, })) } - PackageProgramOperationSourceV1::Hold { slots, certificate } => { - Some(PackageProgramOperationV1::Hold(PackageProgramHoldV1 { - output_slot: *slots.next()?, - certificate: *certificate, - })) - } - PackageProgramOperationSourceV1::Remove(slots) => { + PackageProgramOperationSourceV1::Hold { + outputs, + certificate, + scope, + } => Some(PackageProgramOperationV1::Hold(PackageProgramHoldV1 { + output: outputs.next()?, + certificate: *certificate, + _scope: *scope, + })), + PackageProgramOperationSourceV1::Remove { slots, scope } => { Some(PackageProgramOperationV1::Remove(PackageProgramRemoveV1 { - output_slot: *slots.next()?, + output_slot: slots.next()?, + _scope: *scope, })) } } @@ -2028,15 +2287,15 @@ impl<'a> Iterator for PackageProgramOperationsV1<'a> { let remaining = match &self.inner { PackageProgramOperationSourceV1::Empty => 0, PackageProgramOperationSourceV1::Set { outputs, .. } => outputs.len(), - PackageProgramOperationSourceV1::Hold { slots, .. } - | PackageProgramOperationSourceV1::Remove(slots) => slots.len(), + PackageProgramOperationSourceV1::Hold { outputs, .. } => outputs.len(), + PackageProgramOperationSourceV1::Remove { slots, .. } => slots.len(), }; (remaining, Some(remaining)) } } -impl ExactSizeIterator for PackageProgramOperationsV1<'_> {} -impl FusedIterator for PackageProgramOperationsV1<'_> {} +impl ExactSizeIterator for PackageProgramOperationsV1<'_, '_> {} +impl FusedIterator for PackageProgramOperationsV1<'_, '_> {} /// Closed package error classifications for Session construction. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -2082,7 +2341,7 @@ impl From for PackageProgramInstantiateErr /// Closed package error classifications for one atomic update. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PackageProgramUpdateErrorKindV1 { - OwnerExpired, + OwnerMismatch, InvalidObservation, RevisionOutOfOrder, RevisionConflict, @@ -2372,40 +2631,13 @@ fn map_program_compile_error(error: ProgramCompileError) -> PackageProgramCompil ProgramCompileError::InternalInvariant => PackageProgramCompileErrorV1::InternalInvariant, } } -fn try_copy_surface_input_ports( - compiled: &CompiledCoreProgramV1, -) -> Result, PackageProgramInstantiateErrorV1> { - let inputs = compiled.surface_input_ports(); - let mut copied = Vec::new(); - copied - .try_reserve_exact(inputs.len()) - .map_err(|_| PackageProgramInstantiateErrorKindV1::ResourceExhausted)?; - copied.extend( - inputs - .iter() - .copied() - .map(PackageProgramSurfaceInputPortIdV1::from_core), - ); - Ok(copied.into_boxed_slice()) -} - -fn try_copy_output_slots( - compiled: &CompiledCoreProgramV1, -) -> Result, PackageProgramInstantiateErrorV1> { - let outputs = compiled.outputs(); - let mut copied = Vec::new(); - copied - .try_reserve_exact(outputs.len()) - .map_err(|_| PackageProgramInstantiateErrorKindV1::ResourceExhausted)?; - copied.extend(outputs.map(|(slot, _paint)| PackageProgramOutputSlotIdV1::from_core(slot))); - Ok(copied.into_boxed_slice()) -} - fn map_session_update_error( error: SessionUpdateError, ) -> PackageProgramUpdateErrorV1 { let kind = match error { - SessionUpdateError::OwnerExpired => PackageProgramUpdateErrorKindV1::OwnerExpired, + // A borrowed matching owner keeps the exact Rc generation alive for + // the whole transaction; expiry here is therefore an internal breach. + SessionUpdateError::OwnerExpired => PackageProgramUpdateErrorKindV1::InternalInvariant, SessionUpdateError::Observation(error) => map_observation_error(error), SessionUpdateError::Plan(error) => map_plan_error(error), SessionUpdateError::EvidenceBindingInvariant => { @@ -2463,6 +2695,14 @@ fn map_plan_error(error: CoreProgramPlanErrorV1) -> PackageProgramUpdateErrorKin mod compile_error_projection_tests { use super::*; + #[test] + fn operation_scope_is_a_zero_sized_borrow_marker() { + assert_eq!( + core::mem::size_of::>(), + 0 + ); + } + #[test] fn nested_joint_resource_exhaustion_keeps_its_exact_reason_and_site_kind() { let error = map_program_compile_error(ProgramCompileError::InvalidJointOrder( diff --git a/crates/labcolors-core/src/program_mixed_evaluator_tests.rs b/crates/labcolors-core/src/program_mixed_evaluator_tests.rs index 7a3a91a7..cd131e49 100644 --- a/crates/labcolors-core/src/program_mixed_evaluator_tests.rs +++ b/crates/labcolors-core/src/program_mixed_evaluator_tests.rs @@ -16,12 +16,13 @@ use crate::observation::{ ObservedScenarioSetInput, Revision, ScenarioId, ScenarioInput, SurfaceInputBinding, }; use crate::package_bridge::{ - PackageProgramAssessmentV1, PackageProgramCertificateV1, PackageProgramConflictCellV1, - PackageProgramModeledPointV1, PackageProgramObservationV1, PackageProgramOperationV1, - PackageProgramOutputSlotIdV1, PackageProgramOwnerV1, PackageProgramPhysicalPointV1, + PackageProgramAccessErrorV1, PackageProgramAssessmentV1, PackageProgramCertificateV1, + PackageProgramConflictCellV1, PackageProgramModeledPointV1, PackageProgramObservationHeadV1, + PackageProgramObservationV1, PackageProgramOperationV1, PackageProgramOutputSlotIdV1, + PackageProgramOwnerV1, PackageProgramPhysicalPointV1, PackageProgramProjectionV1, PackageProgramScenarioV1, PackageProgramSignalV1, PackageProgramStateKindV1, - PackageProgramStateViewV1, PackageProgramSurroundV1, PackageProgramUpdateErrorKindV1, - PackageProgramUpdateV1, PackageProgramVerdictV1, PackageProgramVerifiedCellV1, + PackageProgramSurroundV1, PackageProgramUpdateErrorKindV1, PackageProgramUpdateV1, + PackageProgramVerdictV1, PackageProgramVerifiedCellV1, }; use crate::program_session::{ CORE_PROGRAM_ASSESSMENT_CALLS, CompiledCoreProgramV1, CompositionProfile, ConstraintId, @@ -44,6 +45,7 @@ const OCCURRENCE: OccurrenceId = OccurrenceId::new(6); const EXACT_CONSTRAINT: ConstraintId = ConstraintId::new(7); const WCAG_CONSTRAINT: ConstraintId = ConstraintId::new(8); const OUTPUT: OutputSlotId = OutputSlotId::new(9); +const SECOND_OUTPUT: OutputSlotId = OutputSlotId::new(19); const GROUP: ObservationGroupId = ObservationGroupId::new(10); const STREAM: ObservationStreamId = ObservationStreamId::new(11); @@ -92,6 +94,13 @@ fn observed_backdrops(backdrops: &[[u8; 3]]) -> ObservationUpdateInput { } fn finite_program(candidate_signals: [[u8; 3]; 2]) -> CompiledCoreProgramV1 { + finite_program_with_outputs(candidate_signals, vec![OutputBinding::new(OUTPUT, PAINT)]) +} + +fn finite_program_with_outputs( + candidate_signals: [[u8; 3]; 2], + outputs: Vec, +) -> CompiledCoreProgramV1 { const FIRST: TargetCandidateId = TargetCandidateId::new(1); const SECOND: TargetCandidateId = TargetCandidateId::new(2); let program: CoreProgramV1 = Program::new( @@ -133,7 +142,7 @@ fn finite_program(candidate_signals: [[u8; 3]; 2]) -> CompiledCoreProgramV1 { CoreProgramConstraintInvocationV1::ExactSrgb8(Srgb8::new([0; 3])), )], ), - vec![OutputBinding::new(OUTPUT, PAINT)], + outputs, CoreProgramEvaluatorsV1, ); program @@ -558,7 +567,8 @@ fn consume_package_assessment( }); } -fn consume_package_projection(view: PackageProgramStateViewV1<'_>) -> ProjectionProbe { +fn consume_package_projection(projection: PackageProgramProjectionV1<'_, '_>) -> ProjectionProbe { + let view = projection.evidence(); let mut probe = ProjectionProbe::new(); probe.mix(match view.kind() { PackageProgramStateKindV1::Waiting => 1, @@ -566,7 +576,24 @@ fn consume_package_projection(view: PackageProgramStateViewV1<'_>) -> Projection PackageProgramStateKindV1::Failed => 3, PackageProgramStateKindV1::Stale => 4, }); - probe.mix(view.revision().unwrap_or_default()); + match view.observation_head() { + PackageProgramObservationHeadV1::Empty => probe.mix(0), + PackageProgramObservationHeadV1::Unknown { + stream, + revision, + reason_id, + } => { + probe.mix(1); + probe.mix(u64::from(stream.value())); + probe.mix(revision); + probe.mix(u64::from(reason_id)); + } + PackageProgramObservationHeadV1::Observed { stream, revision } => { + probe.mix(2); + probe.mix(u64::from(stream.value())); + probe.mix(revision); + } + } probe.mix( view.cause_certificate_index() .map_or(0, |index| index as u64 + 1), @@ -636,7 +663,7 @@ fn consume_package_projection(view: PackageProgramStateViewV1<'_>) -> Projection } } }); - consume_exact_fused(view.operations(), &mut probe, |operation, probe| { + consume_exact_fused(projection.operations(), &mut probe, |operation, probe| { probe.operations += 1; match operation { PackageProgramOperationV1::Set(set) => { @@ -662,6 +689,37 @@ fn consume_package_projection(view: PackageProgramStateViewV1<'_>) -> Projection std::hint::black_box(probe) } +fn assert_observed_head( + head: PackageProgramObservationHeadV1, + expected_stream: u32, + expected_revision: u64, +) { + let PackageProgramObservationHeadV1::Observed { stream, revision } = head else { + panic!("raw evidence must remain a closed Observed payload"); + }; + assert_eq!(stream.value(), expected_stream); + assert_eq!(revision, expected_revision); +} + +fn assert_unknown_head( + head: PackageProgramObservationHeadV1, + expected_stream: u32, + expected_revision: u64, + expected_reason: u32, +) { + let PackageProgramObservationHeadV1::Unknown { + stream, + revision, + reason_id, + } = head + else { + panic!("raw evidence must remain a closed Unknown payload"); + }; + assert_eq!(stream.value(), expected_stream); + assert_eq!(revision, expected_revision); + assert_eq!(reason_id, expected_reason); +} + #[test] fn one_program_retains_typed_exact_and_wcag22_outcomes() { let program: CoreProgramV1 = Program::new( @@ -755,13 +813,17 @@ fn fixed_package_certificate_retains_none_selection_and_nonunit_output_opacity() let mut session = owner.instantiate(STREAM.value()).unwrap(); let white = [Srgb8::new([0xFF; 3])]; let scenarios = [PackageProgramScenarioV1::new(1, &white)]; - let state = session - .update(PackageProgramUpdateV1::Observed { - revision: 1, - scenarios: &scenarios, - }) + let projection = owner + .update( + &mut session, + PackageProgramUpdateV1::Observed { + revision: 1, + scenarios: &scenarios, + }, + ) .unwrap(); - let Some(PackageProgramCertificateV1::Verified(certificate)) = state.certificates().next() + let Some(PackageProgramCertificateV1::Verified(certificate)) = + projection.evidence().certificates().next() else { panic!("the exact translucent midpoint must be verified"); }; @@ -779,7 +841,7 @@ fn fixed_package_certificate_retains_none_selection_and_nonunit_output_opacity() certificate.outputs().next().unwrap().opacity().to_bits(), physical.opacity().to_bits() ); - let Some(PackageProgramOperationV1::Set(set)) = state.operations().next() else { + let Some(PackageProgramOperationV1::Set(set)) = projection.operations().next() else { panic!("Verified must emit one Set"); }; assert_eq!(set.opacity().to_bits(), physical.opacity().to_bits()); @@ -887,13 +949,16 @@ fn package_projection_preserves_every_exposed_ready_and_conflict_field_against_c PackageProgramScenarioV1::new(2, &ready_white), PackageProgramScenarioV1::new(3, &ready_gray), ]; - let package_ready = ready_package_session - .update(PackageProgramUpdateV1::Observed { - revision: 1, - scenarios: &ready_scenarios, - }) + let package_ready = ready_package_owner + .update( + &mut ready_package_session, + PackageProgramUpdateV1::Observed { + revision: 1, + scenarios: &ready_scenarios, + }, + ) .unwrap(); - let mut package_certificates = package_ready.certificates(); + let mut package_certificates = package_ready.evidence().certificates(); assert_eq!(package_certificates.len(), 1); let Some(PackageProgramCertificateV1::Verified(package_verified)) = package_certificates.next() else { @@ -987,13 +1052,16 @@ fn package_projection_preserves_every_exposed_ready_and_conflict_field_against_c PackageProgramScenarioV1::new(1, &conflict_white), PackageProgramScenarioV1::new(2, &conflict_black), ]; - let package_failed = conflict_package_session - .update(PackageProgramUpdateV1::Observed { - revision: 1, - scenarios: &conflict_scenarios, - }) + let package_failed = conflict_package_owner + .update( + &mut conflict_package_session, + PackageProgramUpdateV1::Observed { + revision: 1, + scenarios: &conflict_scenarios, + }, + ) .unwrap(); - let mut package_certificates = package_failed.certificates(); + let mut package_certificates = package_failed.evidence().certificates(); assert_eq!(package_certificates.len(), 1); let Some(PackageProgramCertificateV1::Conflict(package_conflict)) = package_certificates.next() else { @@ -1051,14 +1119,17 @@ fn committed_projection_is_zero_alloc_and_repeats_no_composite_transform_or_eval let mut session = owner.instantiate(STREAM.value()).unwrap(); let white = [Srgb8::new([0xFF; 3])]; let white_only = [PackageProgramScenarioV1::new(1, &white)]; - session - .update(PackageProgramUpdateV1::Observed { - revision: 1, - scenarios: &white_only, - }) + owner + .update( + &mut session, + PackageProgramUpdateV1::Observed { + revision: 1, + scenarios: &white_only, + }, + ) .unwrap(); let Some(PackageProgramCertificateV1::Verified(ready_certificate)) = - session.state().certificates().next() + session.evidence().certificates().next() else { panic!("black must be selected for the white-only physical support"); }; @@ -1071,7 +1142,7 @@ fn committed_projection_is_zero_alloc_and_repeats_no_composite_transform_or_eval assert!(ready_derivations > 0); assert!(ready_assessments > 0); let (ready_probe, ready_allocations) = crate::test_support::measured_allocations(|| { - consume_package_projection(std::hint::black_box(session.state())) + consume_package_projection(std::hint::black_box(owner.project(&session).unwrap())) }); assert_eq!(ready_allocations, 0); assert!(ready_probe.iterator_laws_hold); @@ -1098,17 +1169,20 @@ fn committed_projection_is_zero_alloc_and_repeats_no_composite_transform_or_eval ready_assessments ); - session - .update(PackageProgramUpdateV1::Unknown { - revision: 2, - reason_id: 7, - }) + owner + .update( + &mut session, + PackageProgramUpdateV1::Unknown { + revision: 2, + reason_id: 7, + }, + ) .unwrap(); let stale_compositions = crate::composition::source_over_evaluation_count(); let stale_derivations = MODELED_TRISTIMULUS_DERIVATION_CALLS.with(core::cell::Cell::get); let stale_assessments = CORE_PROGRAM_ASSESSMENT_CALLS.with(core::cell::Cell::get); let (stale_probe, stale_allocations) = crate::test_support::measured_allocations(|| { - consume_package_projection(std::hint::black_box(session.state())) + consume_package_projection(std::hint::black_box(owner.project(&session).unwrap())) }); assert_eq!(stale_allocations, 0); assert!(stale_probe.iterator_laws_hold); @@ -1135,17 +1209,20 @@ fn committed_projection_is_zero_alloc_and_repeats_no_composite_transform_or_eval PackageProgramScenarioV1::new(1, &white), PackageProgramScenarioV1::new(2, &black), ]; - session - .update(PackageProgramUpdateV1::Observed { - revision: 3, - scenarios: &opposing_backdrops, - }) + owner + .update( + &mut session, + PackageProgramUpdateV1::Observed { + revision: 3, + scenarios: &opposing_backdrops, + }, + ) .unwrap(); let failed_compositions = crate::composition::source_over_evaluation_count(); let failed_derivations = MODELED_TRISTIMULUS_DERIVATION_CALLS.with(core::cell::Cell::get); let failed_assessments = CORE_PROGRAM_ASSESSMENT_CALLS.with(core::cell::Cell::get); let (failed_probe, failed_allocations) = crate::test_support::measured_allocations(|| { - consume_package_projection(std::hint::black_box(session.state())) + consume_package_projection(std::hint::black_box(owner.project(&session).unwrap())) }); assert_eq!(failed_allocations, 0); assert!(failed_probe.iterator_laws_hold); @@ -1224,14 +1301,17 @@ fn observation_projection_is_invariant_under_every_scenario_permutation_and_keep }; let package_scenarios = permutation .map(|index| PackageProgramScenarioV1::new(IDS[index], &package_values[index])); - let package_state = package_session - .update(PackageProgramUpdateV1::Observed { - revision: 1, - scenarios: &package_scenarios, - }) + let package_state = package_owner + .update( + &mut package_session, + PackageProgramUpdateV1::Observed { + revision: 1, + scenarios: &package_scenarios, + }, + ) .unwrap(); let Some(PackageProgramCertificateV1::Verified(package_verified)) = - package_state.certificates().next() + package_state.evidence().certificates().next() else { panic!("the canonical observation must keep one Verified certificate"); }; @@ -1305,11 +1385,14 @@ fn concrete_package_bridge_projects_total_ready_and_stale_operations() { ); let mut session = owner.instantiate(11).unwrap(); - let initial = session.state(); + let initial = session.evidence(); assert_eq!(initial.kind(), PackageProgramStateKindV1::Waiting); - assert_eq!(initial.revision(), None); + assert_eq!( + initial.observation_head(), + PackageProgramObservationHeadV1::Empty + ); assert_eq!(initial.certificates().len(), 0); - assert_eq!(initial.operations().len(), 0); + assert_eq!(owner.project(&session).unwrap().operations().len(), 0); let white = [Srgb8::new([0xFF; 3])]; let gray = [Srgb8::new([0x80; 3])]; @@ -1317,16 +1400,20 @@ fn concrete_package_bridge_projects_total_ready_and_stale_operations() { PackageProgramScenarioV1::new(2, &gray), PackageProgramScenarioV1::new(1, &white), ]; - let ready = session - .update(PackageProgramUpdateV1::Observed { - revision: 1, - scenarios: &scenarios, - }) + let ready = owner + .update( + &mut session, + PackageProgramUpdateV1::Observed { + revision: 1, + scenarios: &scenarios, + }, + ) .unwrap(); - assert_eq!(ready.kind(), PackageProgramStateKindV1::Ready); - assert_eq!(ready.revision(), Some(1)); - assert_eq!(ready.cause_certificate_index(), None); - let certificates = ready.certificates().collect::>(); + let ready_evidence = ready.evidence(); + assert_eq!(ready_evidence.kind(), PackageProgramStateKindV1::Ready); + assert_observed_head(ready_evidence.observation_head(), STREAM.value(), 1); + assert_eq!(ready_evidence.cause_certificate_index(), None); + let certificates = ready_evidence.certificates().collect::>(); assert_eq!(certificates.len(), 1); assert!(matches!( certificates[0], @@ -1364,13 +1451,16 @@ fn concrete_package_bridge_projects_total_ready_and_stale_operations() { PackageProgramScenarioV1::new(1, &white), PackageProgramScenarioV1::new(2, &gray), ]; - let replay = session - .update(PackageProgramUpdateV1::Observed { - revision: 1, - scenarios: &reordered, - }) + let replay = owner + .update( + &mut session, + PackageProgramUpdateV1::Observed { + revision: 1, + scenarios: &reordered, + }, + ) .unwrap(); - let replay_certificate = replay.certificates().next().unwrap(); + let replay_certificate = replay.evidence().certificates().next().unwrap(); assert_eq!( replay_certificate.observation_backing_ptr_for_test(), ready_backing, @@ -1378,10 +1468,13 @@ fn concrete_package_bridge_projects_total_ready_and_stale_operations() { ); let changed_same_revision = [PackageProgramScenarioV1::new(1, &white)]; - let error = match session.update(PackageProgramUpdateV1::Observed { - revision: 1, - scenarios: &changed_same_revision, - }) { + let error = match owner.update( + &mut session, + PackageProgramUpdateV1::Observed { + revision: 1, + scenarios: &changed_same_revision, + }, + ) { Ok(_) => panic!("changed payload at the same revision must be rejected"), Err(error) => error, }; @@ -1389,18 +1482,22 @@ fn concrete_package_bridge_projects_total_ready_and_stale_operations() { error.kind(), PackageProgramUpdateErrorKindV1::RevisionConflict ); - assert_eq!(session.state().kind(), PackageProgramStateKindV1::Ready); - assert_eq!(session.state().revision(), Some(1)); + assert_eq!(session.evidence().kind(), PackageProgramStateKindV1::Ready); + assert_observed_head(session.evidence().observation_head(), STREAM.value(), 1); - let stale = session - .update(PackageProgramUpdateV1::Unknown { - revision: 2, - reason_id: 7, - }) + let stale = owner + .update( + &mut session, + PackageProgramUpdateV1::Unknown { + revision: 2, + reason_id: 7, + }, + ) .unwrap(); - assert_eq!(stale.kind(), PackageProgramStateKindV1::Stale); - assert_eq!(stale.revision(), Some(2)); - let certificates = stale.certificates().collect::>(); + let stale_evidence = stale.evidence(); + assert_eq!(stale_evidence.kind(), PackageProgramStateKindV1::Stale); + assert_unknown_head(stale_evidence.observation_head(), STREAM.value(), 2, 7); + let certificates = stale_evidence.certificates().collect::>(); assert_eq!(certificates.len(), 1); assert!(matches!( certificates[0], @@ -1439,15 +1536,19 @@ fn concrete_package_bridge_distinguishes_failed_remove_from_failed_hold() { let owner = PackageProgramOwnerV1::from_compiled(finite_program([[0x80; 3], [0xFF; 3]])); let mut session = owner.instantiate(11).unwrap(); - let failed = session - .update(PackageProgramUpdateV1::Observed { - revision: 1, - scenarios: &white_only, - }) + let failed = owner + .update( + &mut session, + PackageProgramUpdateV1::Observed { + revision: 1, + scenarios: &white_only, + }, + ) .unwrap(); - assert_eq!(failed.kind(), PackageProgramStateKindV1::Failed); - assert_eq!(failed.cause_certificate_index(), Some(0)); - let certificates = failed.certificates().collect::>(); + let failed_evidence = failed.evidence(); + assert_eq!(failed_evidence.kind(), PackageProgramStateKindV1::Failed); + assert_eq!(failed_evidence.cause_certificate_index(), Some(0)); + let certificates = failed_evidence.certificates().collect::>(); assert_eq!(certificates.len(), 1); assert!(matches!( certificates[0], @@ -1466,13 +1567,17 @@ fn concrete_package_bridge_distinguishes_failed_remove_from_failed_hold() { let owner = PackageProgramOwnerV1::from_compiled(finite_program([[0; 3], [0xFF; 3]])); let mut session = owner.instantiate(12).unwrap(); - let previous = session - .update(PackageProgramUpdateV1::Observed { - revision: 1, - scenarios: &white_only, - }) + let previous = owner + .update( + &mut session, + PackageProgramUpdateV1::Observed { + revision: 1, + scenarios: &white_only, + }, + ) .unwrap(); let previous_backing = previous + .evidence() .certificates() .next() .unwrap() @@ -1481,15 +1586,19 @@ fn concrete_package_bridge_distinguishes_failed_remove_from_failed_hold() { PackageProgramScenarioV1::new(1, &white), PackageProgramScenarioV1::new(2, &black), ]; - let failed = session - .update(PackageProgramUpdateV1::Observed { - revision: 2, - scenarios: &both, - }) + let failed = owner + .update( + &mut session, + PackageProgramUpdateV1::Observed { + revision: 2, + scenarios: &both, + }, + ) .unwrap(); - assert_eq!(failed.kind(), PackageProgramStateKindV1::Failed); - assert_eq!(failed.cause_certificate_index(), Some(0)); - let certificates = failed.certificates().collect::>(); + let failed_evidence = failed.evidence(); + assert_eq!(failed_evidence.kind(), PackageProgramStateKindV1::Failed); + assert_eq!(failed_evidence.cause_certificate_index(), Some(0)); + let certificates = failed_evidence.certificates().collect::>(); assert_eq!( certificates .iter() @@ -1531,10 +1640,13 @@ fn concrete_package_bridge_rejects_transport_shape_before_core_admission() { let mut session = owner.instantiate(11).unwrap(); let empty_values = []; let malformed = [PackageProgramScenarioV1::new(1, &empty_values)]; - let error = match session.update(PackageProgramUpdateV1::Observed { - revision: 1, - scenarios: &malformed, - }) { + let error = match owner.update( + &mut session, + PackageProgramUpdateV1::Observed { + revision: 1, + scenarios: &malformed, + }, + ) { Ok(_) => panic!("schema-short package input must fail before Core admission"), Err(error) => error, }; @@ -1542,25 +1654,37 @@ fn concrete_package_bridge_rejects_transport_shape_before_core_admission() { error.kind(), PackageProgramUpdateErrorKindV1::InvalidObservation ); - assert_eq!(session.state().kind(), PackageProgramStateKindV1::Waiting); - assert_eq!(session.state().revision(), None); + assert_eq!( + session.evidence().kind(), + PackageProgramStateKindV1::Waiting + ); + assert_eq!( + session.evidence().observation_head(), + PackageProgramObservationHeadV1::Empty + ); let white = [Srgb8::new([0xFF; 3])]; let valid = [PackageProgramScenarioV1::new(1, &white)]; - session - .update(PackageProgramUpdateV1::Observed { - revision: 2, - scenarios: &valid, - }) + owner + .update( + &mut session, + PackageProgramUpdateV1::Observed { + revision: 2, + scenarios: &valid, + }, + ) .unwrap(); let duplicate = [ PackageProgramScenarioV1::new(7, &white), PackageProgramScenarioV1::new(7, &white), ]; - let error = match session.update(PackageProgramUpdateV1::Observed { - revision: 1, - scenarios: &duplicate, - }) { + let error = match owner.update( + &mut session, + PackageProgramUpdateV1::Observed { + revision: 1, + scenarios: &duplicate, + }, + ) { Ok(_) => panic!("duplicate scenario IDs must precede revision admission"), Err(error) => error, }; @@ -1568,6 +1692,466 @@ fn concrete_package_bridge_rejects_transport_shape_before_core_admission() { error.kind(), PackageProgramUpdateErrorKindV1::InvalidObservation ); - assert_eq!(session.state().kind(), PackageProgramStateKindV1::Ready); - assert_eq!(session.state().revision(), Some(2)); + assert_eq!(session.evidence().kind(), PackageProgramStateKindV1::Ready); + assert_observed_head(session.evidence().observation_head(), 11, 2); +} + +#[test] +fn same_content_foreign_owner_is_rejected_before_admission_without_work() { + crate::composition::reset_source_over_evaluation_count(); + MODELED_TRISTIMULUS_DERIVATION_CALLS.with(|calls| calls.set(0)); + CORE_PROGRAM_ASSESSMENT_CALLS.with(|calls| calls.set(0)); + + let compiled_a = finite_program([[0x80; 3], [0; 3]]); + let compiled_b = finite_program([[0x80; 3], [0; 3]]); + assert_eq!(compiled_a.content_identity(), compiled_b.content_identity()); + let owner_a = PackageProgramOwnerV1::from_compiled(compiled_a); + let owner_b = PackageProgramOwnerV1::from_compiled(compiled_b); + let mut session = owner_a.instantiate(STREAM.value()).unwrap(); + let white = [Srgb8::new([0xFF; 3])]; + let observed = [PackageProgramScenarioV1::new(1, &white)]; + + owner_a + .update( + &mut session, + PackageProgramUpdateV1::Observed { + revision: 1, + scenarios: &observed, + }, + ) + .unwrap(); + let before = session.evidence(); + assert_observed_head(before.observation_head(), STREAM.value(), 1); + let before_backing = before + .certificates() + .next() + .unwrap() + .observation_backing_ptr_for_test(); + let counts = ( + crate::composition::source_over_evaluation_count(), + MODELED_TRISTIMULUS_DERIVATION_CALLS.with(core::cell::Cell::get), + CORE_PROGRAM_ASSESSMENT_CALLS.with(core::cell::Cell::get), + ); + + let (project_mismatch, project_allocations) = crate::test_support::measured_allocations(|| { + matches!( + owner_b.project(std::hint::black_box(&session)), + Err(PackageProgramAccessErrorV1::OwnerMismatch) + ) + }); + assert!(project_mismatch); + assert_eq!(project_allocations, 0); + + let empty = []; + let malformed = [PackageProgramScenarioV1::new(2, &empty)]; + let (update_mismatch, update_allocations) = crate::test_support::measured_allocations(|| { + matches!( + owner_b.update( + std::hint::black_box(&mut session), + PackageProgramUpdateV1::Observed { + revision: 2, + scenarios: &malformed, + }, + ), + Err(error) if error.kind() == PackageProgramUpdateErrorKindV1::OwnerMismatch + ) + }); + assert!(update_mismatch); + assert_eq!(update_allocations, 0); + assert_eq!( + ( + crate::composition::source_over_evaluation_count(), + MODELED_TRISTIMULUS_DERIVATION_CALLS.with(core::cell::Cell::get), + CORE_PROGRAM_ASSESSMENT_CALLS.with(core::cell::Cell::get), + ), + counts + ); + + let after = session.evidence(); + assert_eq!(after.kind(), PackageProgramStateKindV1::Ready); + assert_observed_head(after.observation_head(), STREAM.value(), 1); + assert_eq!( + after + .certificates() + .next() + .unwrap() + .observation_backing_ptr_for_test(), + before_backing + ); + assert!(owner_a.project(&session).is_ok()); +} + +#[test] +fn raw_observation_head_preserves_unknown_reason_independently_of_lifecycle() { + let owner = PackageProgramOwnerV1::from_compiled(finite_program([[0x80; 3], [0; 3]])); + let mut session = owner.instantiate(STREAM.value()).unwrap(); + assert_eq!( + session.evidence().observation_head(), + PackageProgramObservationHeadV1::Empty + ); + + owner + .update( + &mut session, + PackageProgramUpdateV1::Unknown { + revision: 1, + reason_id: u32::MAX, + }, + ) + .unwrap(); + let unknown = session.evidence(); + assert_eq!(unknown.kind(), PackageProgramStateKindV1::Waiting); + let PackageProgramObservationHeadV1::Unknown { + stream, + revision, + reason_id, + } = unknown.observation_head() + else { + panic!("Unknown raw evidence must remain a closed Unknown payload"); + }; + assert_eq!(stream.value(), STREAM.value()); + assert_eq!(revision, 1); + assert_eq!(reason_id, u32::MAX); + + let mut other_session = owner.instantiate(29).unwrap(); + owner + .update( + &mut other_session, + PackageProgramUpdateV1::Unknown { + revision: 1, + reason_id: u32::MAX, + }, + ) + .unwrap(); + assert_ne!( + unknown.observation_head(), + other_session.evidence().observation_head(), + "equal revision and reason on different streams are distinct raw evidence" + ); + + let conflicting = match owner.update( + &mut session, + PackageProgramUpdateV1::Unknown { + revision: 1, + reason_id: 0, + }, + ) { + Ok(_) => panic!("same revision with another reason must conflict"), + Err(error) => error, + }; + assert_eq!( + conflicting.kind(), + PackageProgramUpdateErrorKindV1::RevisionConflict + ); + assert_unknown_head( + session.evidence().observation_head(), + STREAM.value(), + 1, + u32::MAX, + ); +} + +#[test] +fn copied_raw_heads_outlive_owner_and_session_without_losing_provenance() { + let (unknown, observed) = { + let owner = PackageProgramOwnerV1::from_compiled(finite_program([[0x80; 3], [0; 3]])); + let mut session = owner.instantiate(u32::MAX).unwrap(); + owner + .update( + &mut session, + PackageProgramUpdateV1::Unknown { + revision: 1, + reason_id: u32::MAX, + }, + ) + .unwrap(); + let unknown = session.evidence().observation_head(); + + let white = [Srgb8::new([0xFF; 3])]; + let scenarios = [PackageProgramScenarioV1::new(1, &white)]; + owner + .update( + &mut session, + PackageProgramUpdateV1::Observed { + revision: u64::MAX, + scenarios: &scenarios, + }, + ) + .unwrap(); + let observed = session.evidence().observation_head(); + (unknown, observed) + }; + + assert_unknown_head(unknown, u32::MAX, 1, u32::MAX); + assert_observed_head(observed, u32::MAX, u64::MAX); +} + +#[test] +fn expired_owner_preserves_historical_evidence_but_equivalent_owner_has_no_authority() { + let compiled_a = finite_program([[0x80; 3], [0; 3]]); + let compiled_b = finite_program([[0x80; 3], [0; 3]]); + assert_eq!(compiled_a.content_identity(), compiled_b.content_identity()); + let owner_a = PackageProgramOwnerV1::from_compiled(compiled_a); + let owner_b = PackageProgramOwnerV1::from_compiled(compiled_b); + let mut session = owner_a.instantiate(STREAM.value()).unwrap(); + let white = [Srgb8::new([0xFF; 3])]; + let observed = [PackageProgramScenarioV1::new(1, &white)]; + owner_a + .update( + &mut session, + PackageProgramUpdateV1::Observed { + revision: 1, + scenarios: &observed, + }, + ) + .unwrap(); + + let certificate = session.evidence().certificates().next().unwrap(); + let backing = certificate.observation_backing_ptr_for_test(); + drop(owner_a); + assert_eq!(certificate.observation().revision(), 1); + assert_eq!( + certificate.observation_backing_ptr_for_test(), + backing, + "historical evidence is Session-owned rather than owner-authorized" + ); + + assert!(matches!( + owner_b.project(&session), + Err(PackageProgramAccessErrorV1::OwnerMismatch) + )); + let mismatch = match owner_b.update( + &mut session, + PackageProgramUpdateV1::Unknown { + revision: 2, + reason_id: 9, + }, + ) { + Ok(_) => panic!("equivalent recompile must not revive another owner generation"), + Err(error) => error, + }; + assert_eq!( + mismatch.kind(), + PackageProgramUpdateErrorKindV1::OwnerMismatch + ); + assert_eq!( + session + .evidence() + .certificates() + .next() + .unwrap() + .observation_backing_ptr_for_test(), + backing + ); +} + +#[test] +fn raw_head_and_evaluator_lifecycle_form_the_exact_reachable_product() { + let owner = PackageProgramOwnerV1::from_compiled(finite_program([[0x80; 3], [0; 3]])); + let mut session = owner.instantiate(STREAM.value()).unwrap(); + let white = [Srgb8::new([0xFF; 3])]; + let black = [Srgb8::new([0; 3])]; + let white_only = [PackageProgramScenarioV1::new(1, &white)]; + let opposing = [ + PackageProgramScenarioV1::new(1, &white), + PackageProgramScenarioV1::new(2, &black), + ]; + + owner + .update( + &mut session, + PackageProgramUpdateV1::Unknown { + revision: 1, + reason_id: 3, + }, + ) + .unwrap(); + owner + .update( + &mut session, + PackageProgramUpdateV1::Observed { + revision: 2, + scenarios: &white_only, + }, + ) + .unwrap(); + let ready = session.evidence(); + assert_eq!(ready.kind(), PackageProgramStateKindV1::Ready); + assert_observed_head(ready.observation_head(), STREAM.value(), 2); + + owner + .update( + &mut session, + PackageProgramUpdateV1::Observed { + revision: 3, + scenarios: &opposing, + }, + ) + .unwrap(); + let failed = session.evidence(); + assert_eq!(failed.kind(), PackageProgramStateKindV1::Failed); + assert_observed_head(failed.observation_head(), STREAM.value(), 3); + assert_eq!( + failed + .certificates() + .map(|certificate| certificate.observation().revision()) + .collect::>(), + [3, 2] + ); + + owner + .update( + &mut session, + PackageProgramUpdateV1::Unknown { + revision: 4, + reason_id: 17, + }, + ) + .unwrap(); + let stale = session.evidence(); + assert_eq!(stale.kind(), PackageProgramStateKindV1::Stale); + assert_unknown_head(stale.observation_head(), STREAM.value(), 4, 17); + assert_eq!( + stale + .certificates() + .map(|certificate| certificate.observation().revision()) + .collect::>(), + [2] + ); + + let rejecting_owner = + PackageProgramOwnerV1::from_compiled(finite_program([[0x80; 3], [0xFF; 3]])); + let mut rejecting_session = rejecting_owner.instantiate(STREAM.value()).unwrap(); + rejecting_owner + .update( + &mut rejecting_session, + PackageProgramUpdateV1::Observed { + revision: 1, + scenarios: &white_only, + }, + ) + .unwrap(); + assert_eq!( + rejecting_session.evidence().kind(), + PackageProgramStateKindV1::Failed + ); + rejecting_owner + .update( + &mut rejecting_session, + PackageProgramUpdateV1::Unknown { + revision: 2, + reason_id: 23, + }, + ) + .unwrap(); + let waiting = rejecting_session.evidence(); + assert_eq!(waiting.kind(), PackageProgramStateKindV1::Waiting); + assert_unknown_head(waiting.observation_head(), STREAM.value(), 2, 23); + assert_eq!(waiting.certificates().len(), 0); +} + +#[test] +fn failed_without_previous_removes_every_output_in_canonical_exact_order() { + let owner = PackageProgramOwnerV1::from_compiled(finite_program_with_outputs( + [[0x80; 3], [0xFF; 3]], + vec![ + OutputBinding::new(SECOND_OUTPUT, PAINT), + OutputBinding::new(OUTPUT, PAINT), + ], + )); + let mut session = owner.instantiate(STREAM.value()).unwrap(); + let white = [Srgb8::new([0xFF; 3])]; + let scenarios = [PackageProgramScenarioV1::new(1, &white)]; + let failed = owner + .update( + &mut session, + PackageProgramUpdateV1::Observed { + revision: 1, + scenarios: &scenarios, + }, + ) + .unwrap(); + assert_eq!(failed.evidence().kind(), PackageProgramStateKindV1::Failed); + assert_eq!(failed.evidence().certificates().len(), 1); + + let mut operations = failed.operations(); + assert_eq!(operations.len(), 2); + assert_eq!(operations.size_hint(), (2, Some(2))); + let Some(PackageProgramOperationV1::Remove(first)) = operations.next() else { + panic!("Failed without previous evidence must remove the first output"); + }; + assert_eq!( + first.output_slot(), + PackageProgramOutputSlotIdV1::new(OUTPUT.value()) + ); + assert_eq!(operations.len(), 1); + assert_eq!(operations.size_hint(), (1, Some(1))); + + let Some(PackageProgramOperationV1::Remove(second)) = operations.next() else { + panic!("Failed without previous evidence must remove the second output"); + }; + assert_eq!( + second.output_slot(), + PackageProgramOutputSlotIdV1::new(SECOND_OUTPUT.value()) + ); + assert_eq!(operations.len(), 0); + assert_eq!(operations.size_hint(), (0, Some(0))); + assert!(operations.next().is_none()); + assert!(operations.next().is_none()); +} + +#[test] +fn ready_and_stale_project_every_output_in_the_same_canonical_order() { + let owner = PackageProgramOwnerV1::from_compiled(finite_program_with_outputs( + [[0x80; 3], [0; 3]], + vec![ + OutputBinding::new(SECOND_OUTPUT, PAINT), + OutputBinding::new(OUTPUT, PAINT), + ], + )); + let mut session = owner.instantiate(STREAM.value()).unwrap(); + let white = [Srgb8::new([0xFF; 3])]; + let scenarios = [PackageProgramScenarioV1::new(1, &white)]; + + { + let ready = owner + .update( + &mut session, + PackageProgramUpdateV1::Observed { + revision: 1, + scenarios: &scenarios, + }, + ) + .unwrap(); + let mut operations = ready.operations(); + assert_eq!(operations.len(), 2); + for expected in [OUTPUT, SECOND_OUTPUT] { + let Some(PackageProgramOperationV1::Set(set)) = operations.next() else { + panic!("Ready must set every compiled output"); + }; + assert_eq!(set.output_slot().value(), expected.value()); + assert_eq!(set.certificate().observation().revision(), 1); + } + assert!(operations.next().is_none()); + } + + let stale = owner + .update( + &mut session, + PackageProgramUpdateV1::Unknown { + revision: 2, + reason_id: 7, + }, + ) + .unwrap(); + let mut operations = stale.operations(); + assert_eq!(operations.len(), 2); + for expected in [OUTPUT, SECOND_OUTPUT] { + let Some(PackageProgramOperationV1::Hold(hold)) = operations.next() else { + panic!("Stale must hold every previously verified output"); + }; + assert_eq!(hold.output_slot().value(), expected.value()); + assert_eq!(hold.certificate().observation().revision(), 1); + } + assert!(operations.next().is_none()); } diff --git a/crates/labcolors-core/src/program_session.rs b/crates/labcolors-core/src/program_session.rs index a017e6b9..f542b612 100644 --- a/crates/labcolors-core/src/program_session.rs +++ b/crates/labcolors-core/src/program_session.rs @@ -1123,6 +1123,27 @@ where .map(|output| (output.output, output.paint_id)) } + pub(crate) fn output_count(&self) -> usize { + self.owner_generation.outputs.len() + } + + pub(crate) fn output_slot_at(&self, index: usize) -> Option { + self.owner_generation + .outputs + .get(index) + .map(|output| output.output) + } + + /// Membership is the exact live owner allocation, never equivalent + /// compiled content. The Session's `Weak` keeps the old control block + /// address reserved until the Session itself is destroyed. + pub(crate) fn owns_session(&self, session: &Session>) -> bool { + core::ptr::eq( + session.plan().owner_generation.as_ptr(), + Rc::as_ptr(&self.owner_generation), + ) + } + /// Create one independent stream-affine Session for this exact compiled /// owner generation. Mutable bindings and workspace belong to the Session, /// while executable graph/evaluator state is reached only through a weak diff --git a/crates/labcolors-core/src/session.rs b/crates/labcolors-core/src/session.rs index e0657895..b873580b 100644 --- a/crates/labcolors-core/src/session.rs +++ b/crates/labcolors-core/src/session.rs @@ -11,9 +11,10 @@ use std::mem; use crate::observation::{ CanonicalObservationSchemaV1, ObservationError, ObservationHeadViewV1, ObservationOwnerV1, - ObservationStreamId, ObservationUpdateInput, PreparedObservationUpdateV1, Revision, - RevisionBoundObservationV1, RevisionBoundUnknownV1, SchemaOrderedScenarioSourceV1, - prepare_observation, prepare_schema_ordered_observation, + ObservationPayloadInput, ObservationStreamId, ObservationUpdateInput, + PreparedObservationUpdateV1, Revision, RevisionBoundObservationV1, RevisionBoundUnknownV1, + SchemaOrderedScenarioSourceV1, UnknownReasonId, prepare_observation, + prepare_schema_ordered_observation, }; /// Crate-private sealing prevents an additional runtime owner from being @@ -181,6 +182,10 @@ impl Session { self.raw_head.observation_head() } + pub(crate) const fn plan(&self) -> &Plan { + &self.plan + } + /// Prepare, evaluate and commit one update transaction. Admission and plan /// errors leave both the concrete raw head and lifecycle state untouched. /// Plan-local scratch may have been overwritten by a failed evaluation, @@ -200,6 +205,20 @@ impl Session { apply_prepared_update(&mut self.plan, &mut self.state, &owner, prepared) } + /// Stream-affine `Unknown` admission without re-exporting or duplicating + /// the Session-owned stream identity at a package boundary. + pub(crate) fn update_unknown( + &mut self, + revision: Revision, + reason: UnknownReasonId, + ) -> SessionUpdateResult<'_, Plan> { + self.update(ObservationUpdateInput { + stream: self.stream, + revision, + payload: ObservationPayloadInput::Unknown(reason), + }) + } + /// Package hot path for already schema-ordered point-sRGB8 scenarios. /// It shares the exact lifecycle transaction below without constructing /// keyed surface bindings or a second raw observation owner. diff --git a/crates/labcolors-core/tests/program_boundary.rs b/crates/labcolors-core/tests/program_boundary.rs index a804b279..cd0a2fc6 100644 --- a/crates/labcolors-core/tests/program_boundary.rs +++ b/crates/labcolors-core/tests/program_boundary.rs @@ -14,14 +14,15 @@ use labcolors_core::package_bridge::{ PackageProgramCompileErrorV1, PackageProgramConstraintIdV1, PackageProgramDraftErrorV1, PackageProgramDraftV1, PackageProgramInstantiateErrorV1, PackageProgramJointChoiceV1, PackageProgramJointOrderErrorV1, PackageProgramJointStateV1, PackageProgramModeledPointV1, - PackageProgramNumericDomainErrorV1, PackageProgramOccurrenceIdV1, - PackageProgramOpacityInputIdV1, PackageProgramOperationV1, PackageProgramOutputSlotIdV1, - PackageProgramOwnerV1, PackageProgramPaintIdV1, PackageProgramPhysicalPointV1, - PackageProgramScenarioV1, PackageProgramSessionV1, PackageProgramSignalV1, - PackageProgramSourceIdV1, PackageProgramStateKindV1, PackageProgramStateViewV1, - PackageProgramSurfaceIdV1, PackageProgramSurfaceInputPortIdV1, PackageProgramSurroundV1, - PackageProgramTargetCandidateIdV1, PackageProgramTargetCandidateV1, PackageProgramTargetIdV1, - PackageProgramUpdateErrorKindV1, PackageProgramUpdateV1, PackageProgramVerdictV1, + PackageProgramNumericDomainErrorV1, PackageProgramObservationHeadV1, + PackageProgramOccurrenceIdV1, PackageProgramOpacityInputIdV1, PackageProgramOperationV1, + PackageProgramOutputSlotIdV1, PackageProgramOwnerV1, PackageProgramPaintIdV1, + PackageProgramPhysicalPointV1, PackageProgramProjectionV1, PackageProgramScenarioV1, + PackageProgramSessionV1, PackageProgramSignalV1, PackageProgramSourceIdV1, + PackageProgramStateKindV1, PackageProgramSurfaceIdV1, PackageProgramSurfaceInputPortIdV1, + PackageProgramSurroundV1, PackageProgramTargetCandidateIdV1, PackageProgramTargetCandidateV1, + PackageProgramTargetIdV1, PackageProgramUpdateErrorKindV1, PackageProgramUpdateV1, + PackageProgramVerdictV1, }; use labcolors_core::wcag22::Wcag22CriterionV1; @@ -47,14 +48,32 @@ fn wasm_can_use_only_the_concrete_owner_and_session( revision: 1, scenarios, }; - let view = session.update(update).expect("well-formed update"); - assert_projection_is_linear(view); + let view = owner + .update(session, update) + .expect("well-formed owner-bound update"); + assert_projection_is_owner_bound(view); Ok(()) } -fn assert_projection_is_linear(view: PackageProgramStateViewV1<'_>) { +fn assert_projection_is_owner_bound(projection: PackageProgramProjectionV1<'_, '_>) { + let view = projection.evidence(); let _kind: PackageProgramStateKindV1 = view.kind(); - let _revision: Option = view.revision(); + match view.observation_head() { + PackageProgramObservationHeadV1::Empty => {} + PackageProgramObservationHeadV1::Unknown { + stream, + revision, + reason_id, + } => { + let _ = stream.value(); + let _: u64 = revision; + let _: u32 = reason_id; + } + PackageProgramObservationHeadV1::Observed { stream, revision } => { + let _ = stream.value(); + let _: u64 = revision; + } + } let certificates = exact_size(view.certificates()); let certificate_count = certificates.len(); for certificate in certificates { @@ -136,7 +155,7 @@ fn assert_projection_is_linear(view: PackageProgramStateViewV1<'_>) { } } } - for operation in exact_size(view.operations()) { + for operation in exact_size(projection.operations()) { match operation { PackageProgramOperationV1::Set(set) => { let _: PackageProgramOutputSlotIdV1 = set.output_slot(); @@ -158,20 +177,21 @@ fn assert_projection_is_linear(view: PackageProgramStateViewV1<'_>) { #[allow(dead_code)] fn unknown_is_revision_bound_without_a_stream_or_generation_field( + owner: &PackageProgramOwnerV1, session: &mut PackageProgramSessionV1, ) { let update = PackageProgramUpdateV1::Unknown { revision: 2, reason_id: 7, }; - let _ = session.update(update); + let _ = owner.update(session, update); } #[allow(dead_code)] -fn owner_expiry_is_a_closed_package_error( +fn owner_mismatch_is_a_closed_package_error( error: labcolors_core::package_bridge::PackageProgramUpdateErrorV1, ) { - assert_eq!(error.kind(), PackageProgramUpdateErrorKindV1::OwnerExpired); + assert_eq!(error.kind(), PackageProgramUpdateErrorKindV1::OwnerMismatch); } #[test] @@ -302,19 +322,18 @@ fn external_authoring_lowers_the_actual_closed_program_and_returns_canonical_inp [low_input, high_input] ); let mut session = owner.instantiate(44).unwrap(); - assert_eq!( - session.surface_input_ports().collect::>(), - [low_input, high_input] - ); let white = [Srgb8::new([0xFF; 3]), Srgb8::new([0xFF; 3])]; let scenarios = [PackageProgramScenarioV1::new(7, &white)]; - let ready = session - .update(PackageProgramUpdateV1::Observed { - revision: 1, - scenarios: &scenarios, - }) + let ready = owner + .update( + &mut session, + PackageProgramUpdateV1::Observed { + revision: 1, + scenarios: &scenarios, + }, + ) .unwrap(); - assert_eq!(ready.kind(), PackageProgramStateKindV1::Ready); + assert_eq!(ready.evidence().kind(), PackageProgramStateKindV1::Ready); let mut operations = ready.operations(); let Some(PackageProgramOperationV1::Set(set)) = operations.next() else { panic!("Ready must emit one Set operation"); @@ -358,14 +377,18 @@ fn every_physical_constructor_and_both_remaining_constraint_modes_execute() { let mut session = owner.instantiate(13).unwrap(); let white = [Srgb8::new([0xFF; 3])]; let scenarios = [PackageProgramScenarioV1::new(1, &white)]; - let state = session - .update(PackageProgramUpdateV1::Observed { - revision: 1, - scenarios: &scenarios, - }) + let state = owner + .update( + &mut session, + PackageProgramUpdateV1::Observed { + revision: 1, + scenarios: &scenarios, + }, + ) .unwrap(); - assert_eq!(state.kind(), PackageProgramStateKindV1::Ready); - let Some(PackageProgramCertificateV1::Verified(certificate)) = state.certificates().next() + assert_eq!(state.evidence().kind(), PackageProgramStateKindV1::Ready); + let Some(PackageProgramCertificateV1::Verified(certificate)) = + state.evidence().certificates().next() else { panic!("a fixed target must produce one Verified certificate"); }; @@ -412,13 +435,17 @@ fn certificate_and_set_retain_the_same_nonunit_opacity() { let mut session = owner.instantiate(17).unwrap(); let white = [Srgb8::new([0xFF; 3])]; let scenarios = [PackageProgramScenarioV1::new(1, &white)]; - let state = session - .update(PackageProgramUpdateV1::Observed { - revision: 1, - scenarios: &scenarios, - }) + let state = owner + .update( + &mut session, + PackageProgramUpdateV1::Observed { + revision: 1, + scenarios: &scenarios, + }, + ) .unwrap(); - let Some(PackageProgramCertificateV1::Verified(certificate)) = state.certificates().next() + let Some(PackageProgramCertificateV1::Verified(certificate)) = + state.evidence().certificates().next() else { panic!("the exact emitted midpoint must be verified"); }; From 8b5fb469c076297373b9e8a5f62a998f781ae025 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Sun, 26 Jul 2026 03:14:35 +0300 Subject: [PATCH 44/58] 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 b4e47788..eea0972c 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":"491f909d707f002a8fe2b6b04337ba6f7d044fd5e2b85585a8c978feb930b36c","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":"b25636d4ae969d4a93a82a1324fb4445ba4861a51f18a85c4a0a062afcfbbfda","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":"09be54900efe29ffdac8705efd0d6d613055c90d634446ca4b228f51a63997d0"},{"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":"e73b9c0b8c3a4112cb53987753d5a6b5f639774b0b872afaea9836e6647a2f7d"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/wcag22.rs","sha256":"856093c91159d8b3faab001f2d6524d33d7b16458a5a4e98ea65f8c62ab2694c"},{"kind":"rust-source","path":"crates/labcolors-core/src/hash.rs","sha256":"f97a0fd7d6ad3162f0f1dfb326fccfb7ed40da9a8fa67a5b8a239a1ae2ae49c3"},{"kind":"rust-source","path":"crates/labcolors-core/src/lcs_occurrence.rs","sha256":"6f202ad7425a235b9d18caba0c817fc33a2b8e042050a34f5ddff3fd09efc53d"},{"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":"0fa29533dab84d51af7993bb04dbef7fb978ca55499ea5e1ade282cbc84e6818"},{"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":"16c7fa1a3b1dee795b4434329ebf329066468eeb309213960e35698255b0dd9c"} +{"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":"108ce18deb22866e8637024b0984e529f329dfdc6c9940de0abc4217a6c6a774","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":"ac2e23b4d89850df7bc6697a79dfc715192e998bbabcf47fe88c415e8c332df8","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":"09be54900efe29ffdac8705efd0d6d613055c90d634446ca4b228f51a63997d0"},{"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":"e73b9c0b8c3a4112cb53987753d5a6b5f639774b0b872afaea9836e6647a2f7d"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/wcag22.rs","sha256":"856093c91159d8b3faab001f2d6524d33d7b16458a5a4e98ea65f8c62ab2694c"},{"kind":"rust-source","path":"crates/labcolors-core/src/hash.rs","sha256":"f97a0fd7d6ad3162f0f1dfb326fccfb7ed40da9a8fa67a5b8a239a1ae2ae49c3"},{"kind":"rust-source","path":"crates/labcolors-core/src/lcs_occurrence.rs","sha256":"6f202ad7425a235b9d18caba0c817fc33a2b8e042050a34f5ddff3fd09efc53d"},{"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":"4b43f6f0363436d0cb80fe5e4517555198ba41aef28379e5507dae0a59138838"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"0755210e3e591d7049f293a0f0b7647681631f32feee5ad7d3b3309cffca8f9d"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"7aac1eac64c7e24add1eafc0b66be62f16258fadf2519669785724fc582fc37f"},{"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":"defa5abb202fd2f3d9a08208a4888f79e1bceeba2b00b168efa00e4465875c66"} diff --git a/scripts/verify_point_support_surplus.py b/scripts/verify_point_support_surplus.py index 084cfd3b..c7c8aa2f 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 = ( - "b25636d4ae969d4a93a82a1324fb4445ba4861a51f18a85c4a0a062afcfbbfda" + "ac2e23b4d89850df7bc6697a79dfc715192e998bbabcf47fe88c415e8c332df8" ) EXPECTED_Q55_PROOF_SHA256 = ( "ac59cf89503170c789223b91d775213a19d4e571ef930f2ea609fcd51b14defd" From 35472cba2a2eb9901b59cdeb58c67db98e2bb829 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Sat, 25 Jul 2026 00:54:48 +0300 Subject: [PATCH 45/58] core: single-own compiled observation schemas --- ...rt-reference-surplus-q55-bps-proof-v1.json | 2 +- .../src/generic_boundary_tests.rs | 154 +++++++++++++++--- crates/labcolors-core/src/observation.rs | 22 ++- .../labcolors-core/src/observation_tests.rs | 20 ++- crates/labcolors-core/src/point_support.rs | 5 +- .../labcolors-core/src/point_support_tests.rs | 56 ++++++- crates/labcolors-core/src/program_session.rs | 17 +- .../src/program_session_tests.rs | 53 +++++- crates/labcolors-core/src/session.rs | 17 +- crates/labcolors-core/src/session_tests.rs | 113 +++++++++++-- scripts/verify_point_support_surplus.py | 9 +- 11 files changed, 403 insertions(+), 65 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 eea0972c..30b34af0 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":"108ce18deb22866e8637024b0984e529f329dfdc6c9940de0abc4217a6c6a774","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":"ac2e23b4d89850df7bc6697a79dfc715192e998bbabcf47fe88c415e8c332df8","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":"09be54900efe29ffdac8705efd0d6d613055c90d634446ca4b228f51a63997d0"},{"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":"e73b9c0b8c3a4112cb53987753d5a6b5f639774b0b872afaea9836e6647a2f7d"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/wcag22.rs","sha256":"856093c91159d8b3faab001f2d6524d33d7b16458a5a4e98ea65f8c62ab2694c"},{"kind":"rust-source","path":"crates/labcolors-core/src/hash.rs","sha256":"f97a0fd7d6ad3162f0f1dfb326fccfb7ed40da9a8fa67a5b8a239a1ae2ae49c3"},{"kind":"rust-source","path":"crates/labcolors-core/src/lcs_occurrence.rs","sha256":"6f202ad7425a235b9d18caba0c817fc33a2b8e042050a34f5ddff3fd09efc53d"},{"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":"4b43f6f0363436d0cb80fe5e4517555198ba41aef28379e5507dae0a59138838"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"0755210e3e591d7049f293a0f0b7647681631f32feee5ad7d3b3309cffca8f9d"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"7aac1eac64c7e24add1eafc0b66be62f16258fadf2519669785724fc582fc37f"},{"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":"defa5abb202fd2f3d9a08208a4888f79e1bceeba2b00b168efa00e4465875c66"} +{"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":"20af49448f088da3b30a0e31b8e5b9bb2e23b36a470bcac351c2629e3f345a48","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":"669326bce56a2901f7fbbd8b4c23f26f8b33daceb1471b81c98763940b41d3e4","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":"09be54900efe29ffdac8705efd0d6d613055c90d634446ca4b228f51a63997d0"},{"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":"e73b9c0b8c3a4112cb53987753d5a6b5f639774b0b872afaea9836e6647a2f7d"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/wcag22.rs","sha256":"856093c91159d8b3faab001f2d6524d33d7b16458a5a4e98ea65f8c62ab2694c"},{"kind":"rust-source","path":"crates/labcolors-core/src/hash.rs","sha256":"f97a0fd7d6ad3162f0f1dfb326fccfb7ed40da9a8fa67a5b8a239a1ae2ae49c3"},{"kind":"rust-source","path":"crates/labcolors-core/src/lcs_occurrence.rs","sha256":"6f202ad7425a235b9d18caba0c817fc33a2b8e042050a34f5ddff3fd09efc53d"},{"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":"887f139e6750cc99cd751b3c7e47276971293f74091130028a1746fa29ebb104"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"6f6a376ff036d3d65960c004e6566e1bca580f19f5bd3cd333a80b0da5b5c242"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"4f77643206077c080e5e9b182e896145bfb69bf3db8aa4c1ac7d4c5360ea8504"},{"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":"1889fd73f85a80d1d9daacd6bb2261e87d3a02df49304d6016c37825252c6af8"} diff --git a/crates/labcolors-core/src/generic_boundary_tests.rs b/crates/labcolors-core/src/generic_boundary_tests.rs index f3b5f8f1..6c717b5a 100644 --- a/crates/labcolors-core/src/generic_boundary_tests.rs +++ b/crates/labcolors-core/src/generic_boundary_tests.rs @@ -1,3 +1,6 @@ +use std::ffi::OsStr; +use std::path::PathBuf; + const APPEARANCE_SOURCE: &str = include_str!("appearance.rs"); const CONSTRAINTS_SOURCE: &str = include_str!("constraints/mod.rs"); const EXACT_CONSTRAINT_SOURCE: &str = include_str!("constraints/exact.rs"); @@ -66,6 +69,40 @@ fn contains_rust_identifier(source: &str, identifier: &str) -> bool { }) } +fn production_rust_sources() -> Vec<(String, String)> { + let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src"); + let mut pending = vec![root.clone()]; + let mut sources = Vec::new(); + while let Some(directory) = pending.pop() { + for entry in std::fs::read_dir(&directory).expect("Core source directory must be readable") + { + let path = entry.expect("Core source entry must be readable").path(); + if path.is_dir() { + pending.push(path); + continue; + } + let is_production_rust = path.extension() == Some(OsStr::new("rs")) + && !path + .file_name() + .and_then(OsStr::to_str) + .is_some_and(|name| name.ends_with("_tests.rs")); + if !is_production_rust { + continue; + } + let relative = path + .strip_prefix(&root) + .expect("Core source must remain below its manifest root") + .to_string_lossy() + .into_owned(); + let source = + std::fs::read_to_string(&path).expect("Core Rust source must be valid UTF-8"); + sources.push((relative, source)); + } + } + sources.sort_unstable_by(|left, right| left.0.cmp(&right.0)); + sources +} + #[test] fn generic_physical_and_transport_modules_contain_no_client_or_legacy_vocabulary() { for (path, source) in GENERIC_SOURCES { @@ -324,6 +361,23 @@ fn shared_observation_ssot_has_one_backing_without_lifecycle_or_adapter_facades( .contains("pub(crate) struct CanonicalObservationSchemaV1(Rc<[SurfaceInputPortId]>);"), "compiled schema and observations must share the same Rc-backed schema", ); + assert!( + OBSERVATION_SOURCE.contains( + "#[derive(Debug, PartialEq, Eq)]\n#[cfg_attr(test, derive(Clone))]\npub(crate) struct CanonicalObservationSchemaV1", + ), + "production schema ownership must not expose a general Clone capability", + ); + assert_eq!( + OBSERVATION_SOURCE + .matches("schema.share_for_observation()") + .count(), + 2, + "only keyed and schema-ordered admission may share a schema handle", + ); + assert!( + !OBSERVATION_SOURCE.contains("schema: schema.clone()"), + "admission must use the private schema-sharing capability", + ); for forbidden in ["std::sync::Arc", "Arc<", "RefCell<", "Mutex<", "RwLock<"] { assert!( !OBSERVATION_SOURCE.contains(forbidden), @@ -368,7 +422,6 @@ fn shared_observation_ssot_has_one_backing_without_lifecycle_or_adapter_facades( "impl Session", ); for required in [ - "schema: CanonicalObservationSchemaV1,", "raw_head: SessionObservationHeadV1,", "state: SessionState,", ] { @@ -378,6 +431,10 @@ fn shared_observation_ssot_has_one_backing_without_lifecycle_or_adapter_facades( "Session must own exactly one `{required}` field", ); } + assert!( + !session_owner.contains("schema: CanonicalObservationSchemaV1,"), + "the concrete plan is the sole Session-local owner of its canonical schema", + ); for forbidden in [ "current_unknown", "observation: RevisionBoundObservationV1", @@ -398,6 +455,7 @@ fn shared_observation_ssot_has_one_backing_without_lifecycle_or_adapter_facades( "type Verified: SessionEvidenceV1;", "type Violation: SessionEvidenceV1;", "fn try_acquire_owner(&self) -> Option;", + "owner: &'a Self::OwnerLease,", "SessionUpdateError::OwnerExpired", ".is_same_binding_as(expected_observation)", "SessionUpdateError::EvidenceBindingInvariant", @@ -407,15 +465,20 @@ fn shared_observation_ssot_has_one_backing_without_lifecycle_or_adapter_facades( "Session must reject detached evaluator evidence; missing `{required}`", ); } + let session_plan_implementors = production_rust_sources() + .into_iter() + .filter_map(|(path, source)| { + let count = source.matches("SessionPlanV1 for").count(); + (count != 0).then_some((path, count)) + }) + .collect::>(); assert_eq!( - POINT_SUPPORT_SOURCE - .matches("impl SessionPlanV1 for CompiledPointSupportRecheckV1") - .count() - + PROGRAM_SESSION_SOURCE - .matches("SessionPlanV1 for ProgramSessionPlan") - .count(), - 2, - "only the point-support and Program compiled plans may inhabit Session", + session_plan_implementors, + vec![ + ("point_support.rs".to_owned(), 1), + ("program_session.rs".to_owned(), 1), + ], + "only the audited point-support and Program plans may inhabit Session", ); for (path, source) in [ ("session.rs", SESSION_SOURCE), @@ -446,21 +509,51 @@ fn shared_observation_ssot_has_one_backing_without_lifecycle_or_adapter_facades( ); } - let update = normalized_source_scope( - SESSION_SOURCE, - "pub(crate) fn update(", - "/// Move exactly one retained verified witness", - ); - let owner_preflight = update - .find(".try_acquire_owner()") - .expect("Session update must acquire the exact owner generation"); - let admission = update - .find("prepare_observation(") - .expect("Session update must perform canonical admission"); - assert!( - owner_preflight < admission, - "owner expiry must precede raw admission and physical execution", - ); + for (name, update, prepare) in [ + ( + "keyed", + source_scope( + SESSION_SOURCE, + "pub(crate) fn update(", + "/// Stream-affine `Unknown` admission", + ), + "prepare_observation(", + ), + ( + "schema-ordered", + source_scope( + SESSION_SOURCE, + "pub(crate) fn update_schema_ordered", + "fn apply_prepared_update", + ), + "prepare_schema_ordered_observation(", + ), + ] { + let owner_preflight = update + .find(".try_acquire_owner()") + .unwrap_or_else(|| panic!("{name} update must acquire the exact owner generation")); + let schema = update + .find("let schema = self.plan.observation_schema(&owner);") + .unwrap_or_else(|| panic!("{name} update must derive schema from that owner")); + let admission = update + .find(prepare) + .unwrap_or_else(|| panic!("{name} update must perform canonical admission")); + assert!( + owner_preflight < schema && schema < admission, + "{name} update must pin owner, derive its schema, then admit", + ); + assert_eq!( + update + .matches("let schema = self.plan.observation_schema(&owner);") + .count(), + 1, + "{name} update must borrow exactly one schema", + ); + assert!( + !update.contains("observation_schema(&owner).clone()"), + "{name} admission must not create a transient schema owner", + ); + } let consuming_entry = source_scope( POINT_SUPPORT_SOURCE, @@ -646,6 +739,19 @@ fn program_session_owns_context_bound_lcs_evidence_and_one_session_scratch_cache !plan.contains("epoch: Rc>,"), "a Program Session must not prolong its CompiledProgram owner", ); + assert!( + !plan.contains("schema: CanonicalObservationSchemaV1,"), + "a Program Session must derive schema from its pinned owner generation", + ); + let instantiate = source_scope( + PROGRAM_SESSION_SOURCE, + "pub(crate) fn instantiate(", + "/// Failure while preparing mutable storage", + ); + assert!( + !instantiate.contains("observation_group.schema.clone()"), + "empty Program Sessions must not add persistent schema handles", + ); let compiled = source_scope( PROGRAM_SESSION_SOURCE, "pub struct CompiledProgram", diff --git a/crates/labcolors-core/src/observation.rs b/crates/labcolors-core/src/observation.rs index 1cab558e..7dc0ae56 100644 --- a/crates/labcolors-core/src/observation.rs +++ b/crates/labcolors-core/src/observation.rs @@ -190,7 +190,8 @@ impl ObservedScenarioSet { /// Canonical immutable schema shared by the compiled recheck and every /// admitted observation backing created for it. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq)] +#[cfg_attr(test, derive(Clone))] pub(crate) struct CanonicalObservationSchemaV1(Rc<[SurfaceInputPortId]>); impl CanonicalObservationSchemaV1 { @@ -202,10 +203,22 @@ impl CanonicalObservationSchemaV1 { Rc::ptr_eq(&self.0, &other.0) } + /// Admission is the sole production boundary allowed to share the compiled + /// schema handle: the immutable observation backing must prove the exact + /// schema against which it was admitted. + fn share_for_observation(&self) -> Self { + Self(Rc::clone(&self.0)) + } + #[cfg(test)] pub(crate) fn backing_ptr_for_test(&self) -> *const SurfaceInputPortId { self.0.as_ptr() } + + #[cfg(test)] + pub(crate) fn strong_count_for_test(&self) -> usize { + Rc::strong_count(&self.0) + } } #[derive(Debug, PartialEq, Eq)] @@ -214,7 +227,8 @@ struct ObservationBackingV1 { set: ObservedScenarioSet, } -/// Sealed observation admitted against the Session-owned compiled schema. +/// Sealed observation admitted against the exact schema owned by its sealed +/// Session plan. #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct RevisionBoundObservationV1 { stream: ObservationStreamId, @@ -575,7 +589,7 @@ pub(crate) fn prepare_observation<'owner, Owner: ObservationOwnerV1>( stream, revision: update.revision, backing: Rc::new(ObservationBackingV1 { - schema: schema.clone(), + schema: schema.share_for_observation(), set, }), }, @@ -688,7 +702,7 @@ pub(crate) fn prepare_schema_ordered_observation< stream, revision, backing: Rc::new(ObservationBackingV1 { - schema: schema.clone(), + schema: schema.share_for_observation(), set, }), }, diff --git a/crates/labcolors-core/src/observation_tests.rs b/crates/labcolors-core/src/observation_tests.rs index 1aea99f0..579af3a2 100644 --- a/crates/labcolors-core/src/observation_tests.rs +++ b/crates/labcolors-core/src/observation_tests.rs @@ -335,11 +335,21 @@ fn independent_equal_admissions_do_not_alias_observation_or_schema_backing() { left.apply(observed_update(STREAM, 1, first)).unwrap(); right.apply(observed_update(STREAM, 1, second)).unwrap(); - let left = revision_bound(&left); - let right = revision_bound(&right); - assert_eq!(left, right); - assert_ne!(left.backing_ptr_for_test(), right.backing_ptr_for_test()); - assert_ne!(left.schema_ptr_for_test(), right.schema_ptr_for_test()); + let left_observation = revision_bound(&left); + let right_observation = revision_bound(&right); + assert_eq!(left_observation, right_observation); + assert!( + !left_observation.shares_schema_backing_with(&right.schema), + "equal schema values from another owner must not inherit authority", + ); + assert_ne!( + left_observation.backing_ptr_for_test(), + right_observation.backing_ptr_for_test() + ); + assert_ne!( + left_observation.schema_ptr_for_test(), + right_observation.schema_ptr_for_test() + ); } #[test] diff --git a/crates/labcolors-core/src/point_support.rs b/crates/labcolors-core/src/point_support.rs index 94a01b54..a61baa55 100644 --- a/crates/labcolors-core/src/point_support.rs +++ b/crates/labcolors-core/src/point_support.rs @@ -331,7 +331,10 @@ impl SessionPlanV1 for CompiledPointSupportRecheckV1 { Some(()) } - fn observation_schema(&self) -> &CanonicalObservationSchemaV1 { + fn observation_schema<'a>( + &'a self, + _owner: &'a Self::OwnerLease, + ) -> &'a CanonicalObservationSchemaV1 { &self.surface_schema } diff --git a/crates/labcolors-core/src/point_support_tests.rs b/crates/labcolors-core/src/point_support_tests.rs index 83b39fda..25565796 100644 --- a/crates/labcolors-core/src/point_support_tests.rs +++ b/crates/labcolors-core/src/point_support_tests.rs @@ -16,7 +16,7 @@ use crate::point_support::{ PointSupportStabilityAnchorV1, PointSupportStabilityAssessmentV1, PointSupportStabilityDecisionV1, PointSupportStabilityPolicyV1, }; -use crate::session::{Session, SessionState}; +use crate::session::{Session, SessionPlanV1, SessionState}; use crate::wcag22::Wcag22CriterionV1; const STREAM: ObservationStreamId = ObservationStreamId::new(31); @@ -60,6 +60,60 @@ fn compiled( .unwrap() } +#[test] +fn point_support_session_owns_exactly_one_canonical_schema_handle() { + let requirements = compiled(vec![occurrence( + OCCURRENCE_A, + SURFACE_A, + paint(PAINT_A, [0; 3], 1.0), + Some([0; 3]), + PointSupportCriterionRequirementV1::NotRequested, + PointSupportStabilityPolicyV1::Disabled, + )]); + + assert_eq!( + requirements.observation_schema(&()).strong_count_for_test(), + 1, + ); + + let schema_ptr = requirements.observation_schema(&()).backing_ptr_for_test(); + let mut session = Session::new(STREAM, requirements); + assert_eq!( + session + .plan() + .observation_schema(&()) + .strong_count_for_test(), + 1, + ); + + let report_schema_ptr = match session + .update(observed_update(1, [(1, vec![(SURFACE_A, [0; 3])])])) + .unwrap() + { + SessionState::Ready { current } => current.report().observation().schema_ptr_for_test(), + _ => panic!("the exact point-support requirement must verify"), + }; + assert_eq!(report_schema_ptr, schema_ptr); + assert_eq!( + session + .plan() + .observation_schema(&()) + .strong_count_for_test(), + 2, + ); + + session + .update(observed_update(1, [(1, vec![(SURFACE_A, [0; 3])])])) + .unwrap(); + assert_eq!( + session + .plan() + .observation_schema(&()) + .strong_count_for_test(), + 2, + ); +} + fn observed_update( revision: u64, scenarios: impl IntoIterator)>, diff --git a/crates/labcolors-core/src/program_session.rs b/crates/labcolors-core/src/program_session.rs index f542b612..5c49e46b 100644 --- a/crates/labcolors-core/src/program_session.rs +++ b/crates/labcolors-core/src/program_session.rs @@ -1134,6 +1134,14 @@ where .map(|output| output.output) } + #[cfg(test)] + pub(crate) fn observation_schema_strong_count_for_test(&self) -> usize { + self.owner_generation + .observation_group + .schema + .strong_count_for_test() + } + /// Membership is the exact live owner allocation, never equivalent /// compiled content. The Session's `Weak` keeps the old control block /// address reserved until the Session itself is destroyed. @@ -1171,7 +1179,6 @@ where stream, ProgramSessionPlan { owner_generation: Rc::downgrade(&self.owner_generation), - schema: self.owner_generation.observation_group.schema.clone(), bindings, workspace, modeled_occurrences, @@ -1622,7 +1629,6 @@ where ProgramConstraintInvocationOf: Copy, { owner_generation: Weak>, - schema: CanonicalObservationSchemaV1, bindings: AdmittedAppearanceBindings, workspace: AppearanceWorkspace, modeled_occurrences: Vec>, @@ -1649,8 +1655,11 @@ where self.owner_generation.upgrade().map(ProgramOwnerLeaseV1) } - fn observation_schema(&self) -> &CanonicalObservationSchemaV1 { - &self.schema + fn observation_schema<'a>( + &'a self, + owner: &'a Self::OwnerLease, + ) -> &'a CanonicalObservationSchemaV1 { + &owner.0.observation_group.schema } fn evaluate( diff --git a/crates/labcolors-core/src/program_session_tests.rs b/crates/labcolors-core/src/program_session_tests.rs index 50059864..bc933725 100644 --- a/crates/labcolors-core/src/program_session_tests.rs +++ b/crates/labcolors-core/src/program_session_tests.rs @@ -16,7 +16,7 @@ use crate::program_session::{ Source, SourceId, Surface, Target, TargetId, canonical_surface_input_port_sequence_matches, check_render_node_count, }; -use crate::session::{SessionState, SessionUpdateError}; +use crate::session::{SessionPlanV1, SessionState, SessionUpdateError}; const SOURCE: SourceId = SourceId::new(1); const TARGET: TargetId = TargetId::new(1); @@ -548,6 +548,57 @@ fn independently_instantiated_streams_expire_with_their_compiled_owner_generatio assert_eq!(second.raw_head(), ObservationHeadViewV1::Empty); } +#[test] +fn program_sessions_reuse_the_owner_canonical_schema_handle() { + let compiled = exact_compiled(ConstraintSet::new( + vec![ConstraintInvocation::hard( + REQUIRED, + OCCURRENCE, + Srgb8::new([0x80; 3]), + )], + vec![], + )); + + assert_eq!(compiled.observation_schema_strong_count_for_test(), 1); + + let mut first = compiled.instantiate(STREAM_A).unwrap(); + assert_eq!(compiled.observation_schema_strong_count_for_test(), 1); + let second = compiled.instantiate(STREAM_B).unwrap(); + assert_eq!(compiled.observation_schema_strong_count_for_test(), 1); + + drop(second); + assert_eq!(compiled.observation_schema_strong_count_for_test(), 1); + + let schema_ptr = { + let owner = first.plan().try_acquire_owner().unwrap(); + first + .plan() + .observation_schema(&owner) + .backing_ptr_for_test() + }; + let report_schema_ptr = match first + .update(observed_update(STREAM_A, 1, &[(1, [0xFF; 3])])) + .unwrap() + { + SessionState::Ready { current } => current.report().observation().schema_ptr_for_test(), + _ => panic!("the exact Program must verify"), + }; + assert_eq!(report_schema_ptr, schema_ptr); + let ObservationHeadViewV1::Observed(raw) = first.raw_head() else { + panic!("the raw head must retain the admitted observation"); + }; + assert_eq!(raw.schema_ptr_for_test(), schema_ptr); + assert_eq!(compiled.observation_schema_strong_count_for_test(), 2); + + first + .update(observed_update(STREAM_A, 1, &[(1, [0xFF; 3])])) + .unwrap(); + assert_eq!(compiled.observation_schema_strong_count_for_test(), 2); + + drop(first); + assert_eq!(compiled.observation_schema_strong_count_for_test(), 1); +} + #[test] fn multi_case_hard_failure_retains_the_full_matrix_without_outputs() { let low = ConstraintId::new(1); diff --git a/crates/labcolors-core/src/session.rs b/crates/labcolors-core/src/session.rs index b873580b..9b8d9be6 100644 --- a/crates/labcolors-core/src/session.rs +++ b/crates/labcolors-core/src/session.rs @@ -78,7 +78,13 @@ pub(crate) trait SessionPlanV1: private::PlanSealed { fn try_acquire_owner(&self) -> Option; - fn observation_schema(&self) -> &CanonicalObservationSchemaV1; + /// Return the canonical schema reached through the same owner lease that + /// will authorize evaluation. Self-owned plans may return their own schema; + /// weakly bound plans must derive it from the pinned generation. + fn observation_schema<'a>( + &'a self, + owner: &'a Self::OwnerLease, + ) -> &'a CanonicalObservationSchemaV1; fn evaluate( &mut self, @@ -156,7 +162,6 @@ type SessionUpdateResult<'session, Plan> = Result< #[derive(Debug)] pub(crate) struct Session { stream: ObservationStreamId, - schema: CanonicalObservationSchemaV1, plan: Plan, raw_head: SessionObservationHeadV1, state: SessionState, @@ -164,10 +169,8 @@ pub(crate) struct Session { impl Session { pub(crate) fn new(stream: ObservationStreamId, plan: Plan) -> Self { - let schema = plan.observation_schema().clone(); Self { stream, - schema, plan, raw_head: SessionObservationHeadV1::Empty, state: SessionState::Waiting, @@ -199,7 +202,8 @@ impl Session { .plan .try_acquire_owner() .ok_or(SessionUpdateError::OwnerExpired)?; - let prepared = prepare_observation(&mut self.raw_head, self.stream, &self.schema, update) + let schema = self.plan.observation_schema(&owner); + let prepared = prepare_observation(&mut self.raw_head, self.stream, schema, update) .map_err(SessionUpdateError::Observation)?; apply_prepared_update(&mut self.plan, &mut self.state, &owner, prepared) @@ -232,10 +236,11 @@ impl Session { .plan .try_acquire_owner() .ok_or(SessionUpdateError::OwnerExpired)?; + let schema = self.plan.observation_schema(&owner); let prepared = prepare_schema_ordered_observation( &mut self.raw_head, self.stream, - &self.schema, + schema, revision, source, order_scratch, diff --git a/crates/labcolors-core/src/session_tests.rs b/crates/labcolors-core/src/session_tests.rs index 2da91b7f..76ad9d8d 100644 --- a/crates/labcolors-core/src/session_tests.rs +++ b/crates/labcolors-core/src/session_tests.rs @@ -7,8 +7,8 @@ use crate::lcs_occurrence::ColorSignal; use crate::observation::{ CanonicalObservationSchemaV1, ObservationError, ObservationHeadViewV1, ObservationPayloadInput, ObservationStreamId, ObservationUpdateInput, ObservedScenarioSetInput, Revision, - RevisionBoundObservationV1, ScenarioId, ScenarioInput, SurfaceInputBinding, UnknownReasonId, - canonicalize_observation_schema, + RevisionBoundObservationV1, ScenarioId, ScenarioInput, SchemaOrderedScenarioSourceV1, + SurfaceInputBinding, UnknownReasonId, canonicalize_observation_schema, }; use crate::session::{ Session, SessionDecision, SessionEvidenceV1, SessionObservationBindingPermitV1, SessionPlanV1, @@ -91,7 +91,10 @@ impl SessionPlanV1 for SentinelPlan { Some(()) } - fn observation_schema(&self) -> &CanonicalObservationSchemaV1 { + fn observation_schema<'a>( + &'a self, + _owner: &'a Self::OwnerLease, + ) -> &'a CanonicalObservationSchemaV1 { &self.schema } @@ -133,17 +136,21 @@ impl SessionPlanV1 for SentinelPlan { } #[derive(Debug)] -struct ReplacingOwnerPlan { +struct ReplacingOwnerGeneration { schema: CanonicalObservationSchemaV1, - generation: std::rc::Weak<()>, - owner_slot: Rc>>>, +} + +#[derive(Debug)] +struct ReplacingOwnerPlan { + generation: std::rc::Weak, + owner_slot: Rc>>>, evaluations: Rc>, } impl session_private::PlanSealed for ReplacingOwnerPlan {} impl SessionPlanV1 for ReplacingOwnerPlan { - type OwnerLease = Rc<()>; + type OwnerLease = Rc; type Verified = SentinelVerified; type Violation = SentinelViolation; type Error = SentinelError; @@ -152,8 +159,11 @@ impl SessionPlanV1 for ReplacingOwnerPlan { self.generation.upgrade() } - fn observation_schema(&self) -> &CanonicalObservationSchemaV1 { - &self.schema + fn observation_schema<'a>( + &'a self, + owner: &'a Self::OwnerLease, + ) -> &'a CanonicalObservationSchemaV1 { + &owner.schema } fn evaluate( @@ -163,10 +173,16 @@ impl SessionPlanV1 for ReplacingOwnerPlan { _permit: SessionObservationBindingPermitV1, ) -> Result, Self::Error> { self.evaluations.set(self.evaluations.get() + 1); + assert!( + observation.shares_schema_backing_with(&owner.schema), + "admission and evaluation must use the same pinned generation" + ); let old_generation = self .owner_slot .borrow_mut() - .replace(Rc::new(())) + .replace(Rc::new(ReplacingOwnerGeneration { + schema: canonicalize_observation_schema(vec![SURFACE]).unwrap(), + })) .expect("the first owner generation must still be installed"); assert!(Rc::ptr_eq(owner, &old_generation)); drop(old_generation); @@ -178,6 +194,32 @@ impl SessionPlanV1 for ReplacingOwnerPlan { } } +struct OneOrderedScenario { + id: ScenarioId, + value: Srgb8, +} + +impl SchemaOrderedScenarioSourceV1 for OneOrderedScenario { + fn scenario_count(&self) -> usize { + 1 + } + + fn scenario_id(&self, scenario_index: usize) -> ScenarioId { + assert_eq!(scenario_index, 0); + self.id + } + + fn value_count(&self, scenario_index: usize) -> usize { + assert_eq!(scenario_index, 0); + 1 + } + + fn value(&self, scenario_index: usize, binding_index: usize) -> Srgb8 { + assert_eq!((scenario_index, binding_index), (0, 0)); + self.value + } +} + fn session() -> ( Session, SentinelControl, @@ -346,6 +388,51 @@ fn exact_replay_is_idempotent_and_never_invokes_the_plan() { assert_eq!(session.raw_head().revision(), Some(Revision::new(2))); } +#[test] +fn schema_ordered_admission_shares_only_the_plan_schema_handle() { + let (mut session, control, schema_ptr) = session(); + assert_eq!( + session + .plan() + .observation_schema(&()) + .strong_count_for_test(), + 1, + ); + let source = OneOrderedScenario { + id: ScenarioId::new(1), + value: Srgb8::new([255; 3]), + }; + let mut order_scratch = Vec::new(); + + let SessionState::Ready { current } = session + .update_schema_ordered(Revision::new(1), &source, &mut order_scratch) + .unwrap() + else { + panic!("white sentinel input must verify"); + }; + assert_eq!(current.observation.schema_ptr_for_test(), schema_ptr); + assert_eq!( + session + .plan() + .observation_schema(&()) + .strong_count_for_test(), + 2, + ); + assert_eq!(control.evaluation_count(), 1); + + session + .update_schema_ordered(Revision::new(1), &source, &mut order_scratch) + .unwrap(); + assert_eq!( + session + .plan() + .observation_schema(&()) + .strong_count_for_test(), + 2, + ); + assert_eq!(control.evaluation_count(), 1); +} + #[test] fn equal_content_at_a_higher_revision_rebinds_fresh_evidence() { let (mut session, control, _) = session(); @@ -467,14 +554,14 @@ fn detached_plan_evidence_is_rejected_before_raw_or_lifecycle_commit() { #[test] fn reentrant_owner_replacement_finishes_on_its_pinned_generation_then_expires() { - let schema = canonicalize_observation_schema(vec![SURFACE]).unwrap(); - let first_generation = Rc::new(()); + let first_generation = Rc::new(ReplacingOwnerGeneration { + schema: canonicalize_observation_schema(vec![SURFACE]).unwrap(), + }); let owner_slot = Rc::new(RefCell::new(Some(Rc::clone(&first_generation)))); let evaluations = Rc::new(Cell::new(0)); let mut session = Session::new( STREAM, ReplacingOwnerPlan { - schema, generation: Rc::downgrade(&first_generation), owner_slot: Rc::clone(&owner_slot), evaluations: Rc::clone(&evaluations), diff --git a/scripts/verify_point_support_surplus.py b/scripts/verify_point_support_surplus.py index c7c8aa2f..2a0b3993 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 = ( - "ac2e23b4d89850df7bc6697a79dfc715192e998bbabcf47fe88c415e8c332df8" + "669326bce56a2901f7fbbd8b4c23f26f8b33daceb1471b81c98763940b41d3e4" ) EXPECTED_Q55_PROOF_SHA256 = ( "ac59cf89503170c789223b91d775213a19d4e571ef930f2ea609fcd51b14defd" @@ -201,7 +201,7 @@ def verify_source_binding() -> tuple[str, int]: (OBSERVATION_SOURCE, b" self.backing.set.values(case_index)\n", b" None\n"), (OBSERVATION_SOURCE, b" values.extend(bindings.iter().map(|binding| binding.value));\n", b" values.extend(bindings.iter().map(|_| Srgb8::new([0, 0, 0])));\n"), (OBSERVATION_SOURCE, b" Rc::ptr_eq(&self.0, &other.0)\n", b" self == other\n"), - (OBSERVATION_SOURCE, b" schema: schema.clone(),\n", b" schema: CanonicalObservationSchemaV1(Rc::from(schema.as_slice())),\n"), + (OBSERVATION_SOURCE, b" Self(Rc::clone(&self.0))\n", b" Self(Rc::from(self.as_slice()))\n"), (OBSERVATION_SOURCE, b"if expected_input != actual_input", b"if expected_input == actual_input"), (OBSERVATION_SOURCE, b"Some(observation.revision)", b"None"), (OBSERVATION_SOURCE, b"(self.owner, self.observation)", b"unreachable!()"), @@ -385,11 +385,10 @@ def multiply( result: dict[tuple[int, ...], int] = {} for left_monomial, left_coefficient in left.items(): for right_monomial, right_coefficient in right.items(): + assert len(left_monomial) == len(right_monomial) monomial = tuple( left_power + right_power - for left_power, right_power in zip( - left_monomial, right_monomial, strict=True - ) + for left_power, right_power in zip(left_monomial, right_monomial) ) result[monomial] = ( result.get(monomial, 0) + left_coefficient * right_coefficient From 43d94980b4c6e94b9ab49fc74de2d246ebac0d74 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Sun, 26 Jul 2026 09:21:45 +0300 Subject: [PATCH 46/58] chore(proof): document Python 3.9 zip invariant --- .../point-support-reference-surplus-q55-bps-proof-v1.json | 2 +- scripts/verify_point_support_surplus.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) 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 30b34af0..13d9b054 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":"20af49448f088da3b30a0e31b8e5b9bb2e23b36a470bcac351c2629e3f345a48","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":"669326bce56a2901f7fbbd8b4c23f26f8b33daceb1471b81c98763940b41d3e4","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":"09be54900efe29ffdac8705efd0d6d613055c90d634446ca4b228f51a63997d0"},{"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":"e73b9c0b8c3a4112cb53987753d5a6b5f639774b0b872afaea9836e6647a2f7d"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/wcag22.rs","sha256":"856093c91159d8b3faab001f2d6524d33d7b16458a5a4e98ea65f8c62ab2694c"},{"kind":"rust-source","path":"crates/labcolors-core/src/hash.rs","sha256":"f97a0fd7d6ad3162f0f1dfb326fccfb7ed40da9a8fa67a5b8a239a1ae2ae49c3"},{"kind":"rust-source","path":"crates/labcolors-core/src/lcs_occurrence.rs","sha256":"6f202ad7425a235b9d18caba0c817fc33a2b8e042050a34f5ddff3fd09efc53d"},{"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":"887f139e6750cc99cd751b3c7e47276971293f74091130028a1746fa29ebb104"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"6f6a376ff036d3d65960c004e6566e1bca580f19f5bd3cd333a80b0da5b5c242"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"4f77643206077c080e5e9b182e896145bfb69bf3db8aa4c1ac7d4c5360ea8504"},{"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":"1889fd73f85a80d1d9daacd6bb2261e87d3a02df49304d6016c37825252c6af8"} +{"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":"2e7c31a9b66fa310e0a7e33291bc167283157034703c47d86d7e22b5323acee6","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":"669326bce56a2901f7fbbd8b4c23f26f8b33daceb1471b81c98763940b41d3e4","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":"09be54900efe29ffdac8705efd0d6d613055c90d634446ca4b228f51a63997d0"},{"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":"e73b9c0b8c3a4112cb53987753d5a6b5f639774b0b872afaea9836e6647a2f7d"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/wcag22.rs","sha256":"856093c91159d8b3faab001f2d6524d33d7b16458a5a4e98ea65f8c62ab2694c"},{"kind":"rust-source","path":"crates/labcolors-core/src/hash.rs","sha256":"f97a0fd7d6ad3162f0f1dfb326fccfb7ed40da9a8fa67a5b8a239a1ae2ae49c3"},{"kind":"rust-source","path":"crates/labcolors-core/src/lcs_occurrence.rs","sha256":"6f202ad7425a235b9d18caba0c817fc33a2b8e042050a34f5ddff3fd09efc53d"},{"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":"887f139e6750cc99cd751b3c7e47276971293f74091130028a1746fa29ebb104"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"6f6a376ff036d3d65960c004e6566e1bca580f19f5bd3cd333a80b0da5b5c242"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"4f77643206077c080e5e9b182e896145bfb69bf3db8aa4c1ac7d4c5360ea8504"},{"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":"e93845aacc62968c9a21a1c7b8ef7222434b64c2100e3a177b3288051d03507d"} diff --git a/scripts/verify_point_support_surplus.py b/scripts/verify_point_support_surplus.py index 2a0b3993..858b86ce 100755 --- a/scripts/verify_point_support_surplus.py +++ b/scripts/verify_point_support_surplus.py @@ -386,6 +386,8 @@ def multiply( for left_monomial, left_coefficient in left.items(): for right_monomial, right_coefficient in right.items(): assert len(left_monomial) == len(right_monomial) + # Python 3.9 lacks zip(..., strict=True); this assertion gives + # the same no-truncation guarantee without raising the floor. monomial = tuple( left_power + right_power for left_power, right_power in zip(left_monomial, right_monomial) From 5389478a6e787ad604e524947d5335f439f8f532 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Sat, 25 Jul 2026 03:03:34 +0300 Subject: [PATCH 47/58] core: define the public Program API --- crates/labcolors-core/src/constraints/mod.rs | 10 +- .../src/generic_boundary_tests.rs | 167 +- crates/labcolors-core/src/lib.rs | 89 +- crates/labcolors-core/src/observation.rs | 16 + crates/labcolors-core/src/package_bridge.rs | 2723 ----------- crates/labcolors-core/src/program.rs | 4078 +++++++++++++++++ .../src/program_mixed_evaluator_tests.rs | 750 ++- crates/labcolors-core/src/program_session.rs | 14 +- .../labcolors-core/tests/program_boundary.rs | 630 +-- .../tests/program_public_api.rs | 58 + 10 files changed, 5036 insertions(+), 3499 deletions(-) delete mode 100644 crates/labcolors-core/src/package_bridge.rs create mode 100644 crates/labcolors-core/src/program.rs create mode 100644 crates/labcolors-core/tests/program_public_api.rs diff --git a/crates/labcolors-core/src/constraints/mod.rs b/crates/labcolors-core/src/constraints/mod.rs index fb887337..6ae5a1c3 100644 --- a/crates/labcolors-core/src/constraints/mod.rs +++ b/crates/labcolors-core/src/constraints/mod.rs @@ -21,14 +21,14 @@ pub(crate) use exact::ExactIdentityPassV1; mod wcag22; -pub(crate) use wcag22::{Wcag22Srgb8CapabilityV1, Wcag22Srgb8EvaluatorIdentityV1, Wcag22Srgb8V1}; - -#[cfg(test)] pub(crate) use wcag22::{ - ApplicableWcag22EvaluationErrorV1, ApplicableWcag22MeasurementV1, Wcag22PassV1, - Wcag22ViolationV1, + ApplicableWcag22EvaluationErrorV1, Wcag22Srgb8CapabilityV1, Wcag22Srgb8EvaluatorIdentityV1, + Wcag22Srgb8V1, }; +#[cfg(test)] +pub(crate) use wcag22::{ApplicableWcag22MeasurementV1, Wcag22PassV1, Wcag22ViolationV1}; + /// Test-only probe around the production Program WCAG evaluator. It records /// each physical visible signal without changing measurement or /// classification, allowing execution-count assertions at the Program diff --git a/crates/labcolors-core/src/generic_boundary_tests.rs b/crates/labcolors-core/src/generic_boundary_tests.rs index 6c717b5a..3680b13b 100644 --- a/crates/labcolors-core/src/generic_boundary_tests.rs +++ b/crates/labcolors-core/src/generic_boundary_tests.rs @@ -9,7 +9,7 @@ const LIB_SOURCE: &str = include_str!("lib.rs"); const LCS_OCCURRENCE_SOURCE: &str = include_str!("lcs_occurrence.rs"); 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 PROGRAM_SOURCE: &str = include_str!("program.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"); @@ -116,15 +116,90 @@ fn generic_physical_and_transport_modules_contain_no_client_or_legacy_vocabulary } #[test] -fn package_authoring_is_one_thin_concrete_core_draft_without_a_second_graph() { +fn stacked_program_module_is_visible_module_qualified_and_transport_neutral() { + let normalized_lib = LIB_SOURCE.split_whitespace().collect::>().join(" "); + assert_eq!( + LIB_SOURCE + .lines() + .filter(|line| line.trim() == "pub mod program;") + .count(), + 1, + "the crate root must expose exactly one file-backed Program module", + ); + assert!( + normalized_lib.contains("#[deny(missing_docs)] pub mod program;"), + "the public Program reference must stay complete by construction", + ); + assert!( + !normalized_lib.contains("#[doc(hidden)] pub mod program;"), + "the reviewed Program API must remain visible in rustdoc", + ); + + for introducer in ["pub use ", "pub type "] { + let mut remaining = LIB_SOURCE; + while let Some(start) = remaining.find(introducer) { + let statement = &remaining[start + ..start + + remaining[start..] + .find(';') + .expect("root public declaration must terminate") + + 1]; + assert!( + !contains_rust_identifier(statement, "program"), + "Program types must stay module-qualified; found root alias `{statement}`", + ); + remaining = &remaining[start + statement.len()..]; + } + } + + let source_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src"); + assert!( + source_root.join("program.rs").is_file(), + "the public Program implementation must remain file-backed", + ); + assert!( + !source_root.join("package_bridge.rs").exists(), + "the superseded transport-named module must not return", + ); + assert!( + !PROGRAM_SOURCE.contains("PackageProgram") + && !contains_rust_identifier(PROGRAM_SOURCE, "package_bridge") + && !PROGRAM_SOURCE.contains("package "), + "the public Program source must not retain transport-era vocabulary", + ); + + for (path, source) in production_rust_sources() { + if path == "lib.rs" { + continue; + } + assert!( + !source.contains("PackageProgram") + && !contains_rust_identifier(&source, "package_bridge"), + "{path} must not retain the superseded public path or prefix", + ); + } + assert_eq!( + LIB_SOURCE.matches("PackageProgram").count(), + 2, + "the old prefix may appear only in the two negative API sentinels", + ); + assert_eq!( + LIB_SOURCE.matches("package_bridge").count(), + 2, + "the old module may appear only in the two negative API sentinels", + ); +} + +#[test] +fn public_program_draft_wraps_the_single_canonical_core_graph() { assert_eq!( normalized_source_scope( - PACKAGE_BRIDGE_SOURCE, - "pub struct PackageProgramDraftV1 {", - "/// Draft mutation rejected", + PROGRAM_SOURCE, + "pub struct DraftV1 {", + "/// Ошибка изменения Draft до компиляции.", ), - "pub struct PackageProgramDraftV1 { inner: CoreProgramDraftV1, }", - "the package seam must forward actual IR nodes into the sole Core draft", + "pub struct DraftV1 { inner: CoreProgramDraftV1, }", + "the public seam must forward actual IR nodes into the sole Core draft", ); assert_eq!( normalized_source_scope( @@ -150,41 +225,37 @@ fn package_authoring_is_one_thin_concrete_core_draft_without_a_second_graph() { "HashMap", "dyn Program", "OutputProfileId", - "PackageProgramObservationGroupIdV1", - "PackageProgramOpacityIdV1", - "PackageProgramSurfaceInputIdV1", + "ObservationGroupIdV1", + "OpacityIdV1", + "SurfaceInputIdV1", "pub fn push_opacity(", "pub fn push_surface_input(", "pub fn surface_input_slots(", ] { assert!( - !PACKAGE_BRIDGE_SOURCE.contains(forbidden), - "the concrete package lowerer must not acquire `{forbidden}`", + !PROGRAM_SOURCE.contains(forbidden), + "the concrete public lowerer must not acquire `{forbidden}`", ); } } #[test] -fn package_session_keeps_evidence_but_owner_alone_grants_updates_and_operations() { +fn public_session_keeps_evidence_but_owner_alone_grants_updates_and_operations() { assert_eq!( - normalized_source_scope( - PACKAGE_BRIDGE_SOURCE, - "pub struct PackageProgramSessionV1 {", - "impl PackageProgramSessionV1", - ), + normalized_source_scope(PROGRAM_SOURCE, "pub struct SessionV1 {", "impl SessionV1",), concat!( - "pub struct PackageProgramSessionV1 { ", + "pub struct SessionV1 { ", "scenario_order_scratch: Vec, ", "session: CoreProgramSessionV1, ", "}", ), - "the package Session must not duplicate owner schema, outputs, stream, or lifecycle state", + "the public Session must not duplicate owner schema, outputs, stream, or lifecycle state", ); let session_api = source_scope( - PACKAGE_BRIDGE_SOURCE, - "impl PackageProgramSessionV1 {", - "struct PackageProgramScenarioSourceV1", + PROGRAM_SOURCE, + "impl SessionV1 {", + "struct ScenarioSourceV1", ); assert_eq!( session_api.matches("pub fn evidence(").count(), @@ -210,9 +281,9 @@ fn package_session_keeps_evidence_but_owner_alone_grants_updates_and_operations( } let evidence_api = source_scope( - PACKAGE_BRIDGE_SOURCE, - "impl<'a> PackageProgramEvidenceViewV1<'a> {", - "struct PackageProgramBorrowScopeV1<'owner, 'session>", + PROGRAM_SOURCE, + "impl<'a> EvidenceViewV1<'a> {", + "struct BorrowScopeV1<'owner, 'session>", ); for forbidden in [ "pub fn revision(", @@ -227,9 +298,9 @@ fn package_session_keeps_evidence_but_owner_alone_grants_updates_and_operations( } let owner_api = source_scope( - PACKAGE_BRIDGE_SOURCE, - "impl PackageProgramOwnerV1 {", - "pub struct PackageProgramScenarioV1<'a> {", + PROGRAM_SOURCE, + "impl OwnerV1 {", + "pub struct ScenarioV1<'a> {", ); for required in [ "pub fn project<'owner, 'session>(", @@ -256,44 +327,50 @@ fn package_session_keeps_evidence_but_owner_alone_grants_updates_and_operations( "owner mismatch must be rejected before admission, allocation, or evaluation", ); - let public_access_errors = source_scope( - PACKAGE_BRIDGE_SOURCE, - "pub enum PackageProgramAccessErrorV1 {", - "impl PackageProgramOwnerV1", - ); + let public_access_errors = + source_scope(PROGRAM_SOURCE, "pub enum AccessErrorV1 {", "impl OwnerV1"); assert!( public_access_errors.contains("OwnerMismatch,") && !public_access_errors.contains("OwnerExpired"), "operation projection must distinguish foreign ownership, not expose internal expiry", ); let public_update_errors = source_scope( - PACKAGE_BRIDGE_SOURCE, - "pub enum PackageProgramUpdateErrorKindV1 {", - "pub struct PackageProgramUpdateErrorV1 {", + PROGRAM_SOURCE, + "pub enum UpdateErrorKindV1 {", + "pub enum UpdateErrorV1 {", ); assert!( public_update_errors.contains("OwnerMismatch,") && !public_update_errors.contains("OwnerExpired"), "owner expiry is an internal invariant after a matching owner borrow", ); + assert!( + PROGRAM_SOURCE.contains("pub enum UpdateErrorV1 {") + && !PROGRAM_SOURCE.contains("pub struct UpdateErrorV1 {") + && PROGRAM_SOURCE + .contains("fn map_observation_error(error: ObservationError) -> UpdateErrorV1",) + && PROGRAM_SOURCE + .contains("fn map_plan_error(error: CoreProgramPlanErrorV1) -> UpdateErrorV1",), + "update errors must retain payloads in the authoritative enum before kind projection", + ); for (payload, end) in [ ( - "pub struct PackageProgramSetV1<'owner, 'session> {", - "impl<'session> PackageProgramSetV1<'_, 'session>", + "pub struct SetV1<'owner, 'session> {", + "impl<'session> SetV1<'_, 'session>", ), ( - "pub struct PackageProgramRemoveV1<'owner, 'session> {", - "impl PackageProgramRemoveV1<'_, '_>", + "pub struct RemoveV1<'owner, 'session> {", + "impl RemoveV1<'_, '_>", ), ( - "pub struct PackageProgramHoldV1<'owner, 'session> {", - "impl<'session> PackageProgramHoldV1<'_, 'session>", + "pub struct HoldV1<'owner, 'session> {", + "impl<'session> HoldV1<'_, 'session>", ), ] { assert!( - source_scope(PACKAGE_BRIDGE_SOURCE, payload, end) - .contains("_scope: PackageProgramBorrowScopeV1<'owner, 'session>,"), + source_scope(PROGRAM_SOURCE, payload, end) + .contains("_scope: BorrowScopeV1<'owner, 'session>,"), "{payload} must retain both owner and immutable Session borrows", ); } diff --git a/crates/labcolors-core/src/lib.rs b/crates/labcolors-core/src/lib.rs index fa185ec4..c1d72f4b 100644 --- a/crates/labcolors-core/src/lib.rs +++ b/crates/labcolors-core/src/lib.rs @@ -24,7 +24,7 @@ pub mod ladder; pub mod lcs; #[expect( dead_code, - reason = "private F0 colour-identity foundation precedes the terminal public hard cut" + reason = "F0 colour-identity internals are exposed only through typed Program evidence" )] pub(crate) mod lcs_occurrence; pub(crate) mod lpc; @@ -33,24 +33,24 @@ pub mod neutral; pub mod numerical_plan; #[expect( dead_code, - reason = "private F0 output-projection release firewall precedes the atomic public hard cut" + reason = "the output-profile firewall is intentionally internal to registered profiles" )] pub(crate) mod output_projection; -#[doc(hidden)] -pub mod package_bridge; #[cfg_attr( not(test), expect( dead_code, - reason = "private C8d full-support recheck is production-compiled before its package bridge exists" + reason = "the full-support recheck remains a private verified engine" ) )] pub(crate) mod point_support; +#[deny(missing_docs)] +pub mod program; #[cfg_attr( not(test), expect( dead_code, - reason = "private Program compiler/lowering precedes its direct sole-Session bridge" + reason = "generic Program machinery is exposed only through the concrete public module" ) )] pub(crate) mod program_session; @@ -58,7 +58,7 @@ pub(crate) mod program_session; not(test), expect( dead_code, - reason = "private F0 release registry precedes the atomic public hard cut" + reason = "release-registry internals are projected only through typed public evidence" ) )] pub(crate) mod release_registry; @@ -116,7 +116,7 @@ mod generic_boundary_tests; not(test), expect( dead_code, - reason = "private F2 raw admission is production-compiled before its package bridge exists" + reason = "raw observation ownership is exposed only through the public Program session" ) )] pub(crate) mod observation; @@ -131,7 +131,7 @@ mod point_support_tests; not(test), expect( dead_code, - reason = "private F2 Session is production-compiled before C8c package integration exists" + reason = "the generic Session engine is exposed only through the public Program owner" ) )] pub(crate) mod session; @@ -143,7 +143,7 @@ mod session_tests; not(test), expect( dead_code, - reason = "private V2a joint selection is production-compiled before a public Program exists" + reason = "joint-selection internals are exposed only through the public Program contract" ) )] pub(crate) mod joint; @@ -359,8 +359,73 @@ pub struct NoHybridLpcSurfaceMetric; #[cfg(doctest)] pub struct NoPrematureScalarLpcApi; -/// C8d recheck and F2 observation remain one private Session-owned protocol; -/// they do not create a second public authoring root before C7c. +/// Публичный Program API живёт только в одноимённом модуле и не сохраняет +/// транспортный префикс, старый путь или корневые реэкспорты. +/// +/// ``` +/// use labcolors_core::{Srgb8, program}; +/// +/// let source = program::SourceIdV1::new(1); +/// let target = program::TargetIdV1::new(2); +/// let input = program::SurfaceInputPortIdV1::new(3); +/// let paint = program::PaintIdV1::new(4); +/// let surface = program::SurfaceIdV1::new(5); +/// let occurrence = program::OccurrenceIdV1::new(6); +/// let constraint = program::ConstraintIdV1::new(7); +/// let output = program::OutputSlotIdV1::new(8); +/// let context = program::AppearanceContextV1::try_new( +/// 64.0, +/// 0.2, +/// program::SurroundV1::Average, +/// ).unwrap(); +/// let mut draft = program::DraftV1::new(); +/// +/// draft.push_source(source, Srgb8::new([0, 0, 0])); +/// draft.push_fixed_target(target, source); +/// draft.push_surface_input_port(input); +/// draft.push_solid_paint(paint, target); +/// draft.push_input_surface(surface, input); +/// draft.push_source_over_occurrence(occurrence, paint, surface, context); +/// draft.push_exact_hard(constraint, occurrence, Srgb8::new([0, 0, 0])); +/// draft.push_output(output, paint); +/// +/// let owner = draft.compile().unwrap(); +/// let mut session = owner.instantiate(1).unwrap(); +/// let white = [Srgb8::new([255, 255, 255])]; +/// let scenarios = [program::ScenarioV1::new(1, &white)]; +/// let projection = owner.update( +/// &mut session, +/// program::UpdateV1::Observed { +/// revision: 1, +/// scenarios: &scenarios, +/// }, +/// ).unwrap(); +/// assert!(matches!( +/// projection.operations().next(), +/// Some(program::OperationV1::Set(_)), +/// )); +/// ``` +/// +/// ```compile_fail +/// use labcolors_core::package_bridge; +/// ``` +/// +/// ```compile_fail +/// use labcolors_core::package_bridge::PackageProgramDraftV1; +/// ``` +/// +/// ```compile_fail +/// use labcolors_core::program::PackageProgramDraftV1; +/// ``` +/// +/// ```compile_fail +/// use labcolors_core::DraftV1; +/// ``` +#[cfg(doctest)] +pub struct ProgramApiBoundary; + +/// C8d recheck и F2 observation остаются деталями одной приватной Session; +/// они не могут стать дополнительными public authoring/runtime roots. /// /// ```compile_fail /// use labcolors_core::point_support::CompiledPointSupportRecheckV1; diff --git a/crates/labcolors-core/src/observation.rs b/crates/labcolors-core/src/observation.rs index 7dc0ae56..686d4c5f 100644 --- a/crates/labcolors-core/src/observation.rs +++ b/crates/labcolors-core/src/observation.rs @@ -165,6 +165,22 @@ impl ObservationSchemaMismatchV1 { actual, } } + + pub(crate) const fn into_parts( + self, + ) -> ( + usize, + usize, + Option, + Option, + ) { + ( + self.case_index, + self.binding_index, + self.expected, + self.actual, + ) + } } /// Canonical nonempty correlated set. Values and provenance each use one flat diff --git a/crates/labcolors-core/src/package_bridge.rs b/crates/labcolors-core/src/package_bridge.rs deleted file mode 100644 index 42330136..00000000 --- a/crates/labcolors-core/src/package_bridge.rs +++ /dev/null @@ -1,2723 +0,0 @@ -//! Sole concrete package authoring and runtime seam for a Core Program. -//! -//! The cold path appends typed physical declarations directly to the real Core -//! Program IR and compiles it with the code-owned evaluator union. The hot path -//! supplies schema-ordered physical scenarios and receives a borrowed, -//! allocation-free projection of Core-owned state and evidence. Neither path -//! exposes evaluator traits, generic Session plans, client vocabulary, -//! transport words, strings, or lifecycle generations. - -use core::iter::FusedIterator; -use core::marker::PhantomData; -use core::slice; - -use crate::Srgb8; -use crate::appearance::{OccurrenceId, OpacityInputId, PaintId, SurfaceId, SurfaceInputPortId}; -use crate::composition::CompositionProfileV1; -use crate::constraints::{ - ExactSrgb8IdentityV1, ProgramVisiblePointBindingV1, ProgramVisiblePointPassEvidence, - ProgramVisiblePointViolationEvidence, Wcag22Srgb8V1, -}; -use crate::joint::FiniteJointOrderErrorV1; -use crate::lcs_occurrence::{ - AdaptingLuminanceCdM2, AdmittedSrgb8TristimulusBindingV1, AppearanceContextDomainErrorV1, - AppearanceContextFieldV1, AppearanceContextId, AppearanceContextSchemaReleaseId, - BackgroundLuminanceRatio, ColorSignal, ColorSignalViewV1, IEC_SRGB_D65_XYZ_FRAME_V1, - NumericDomainError, SurroundProfileId, -}; -use crate::numerics::NumericalDecisionEvidenceV1; -use crate::observation::{ - ObservationError, ObservationHeadViewV1, ObservationStreamId, Revision, ScenarioId, - SchemaOrderedScenarioSourceV1, UnknownReasonId, -}; -use crate::program_session::{ - CompiledCoreProgramV1, CompositionProfile, ConstraintId, ConstraintInvocation, - CoreProgramConstraintInvocationV1, CoreProgramDraftErrorV1, CoreProgramDraftV1, - CoreProgramEvaluatorErrorV1, CoreProgramEvaluatorsV1, CoreProgramPassEvidenceV1, - CoreProgramViolationEvidenceV1, DeclaredJointSelectionV1, JointCandidateStateV1, Occurrence, - OpacityInput, OutputBinding, OutputSlotId, Paint, ProgramCompileError, ProgramConflictV1, - ProgramConstraintCellV1, ProgramConstraintResultV1, ProgramContentIdentityV1, ProgramOutputV1, - ProgramSessionEvaluationError, ProgramSessionInstantiateError, ProgramSessionPlan, - ProgramVerifiedV1, Source, SourceId, Surface, Target, TargetCandidateChoiceV1, - TargetCandidateId, TargetCandidateV1, TargetId, -}; -use crate::session::{Session, SessionState, SessionUpdateError}; -use crate::wcag22::{Wcag22CriterionV1, Wcag22LuminanceBoundsQ55V1, Wcag22ProfileIdV1}; - -type CoreVerifiedV1 = ProgramVerifiedV1; -type CoreConflictV1 = ProgramConflictV1; -type CoreProgramPlanV1 = ProgramSessionPlan; -type CoreProgramSessionV1 = Session; -type CoreProgramStateV1 = SessionState; -type CoreProgramPlanErrorV1 = ProgramSessionEvaluationError; -type CoreProgramConstraintCellV1 = ProgramConstraintCellV1; -type CoreExactPassEvidenceV1 = ProgramVisiblePointPassEvidence; -type CoreExactViolationEvidenceV1 = ProgramVisiblePointViolationEvidence; -type CoreWcag22PassEvidenceV1 = ProgramVisiblePointPassEvidence; -type CoreWcag22ViolationEvidenceV1 = ProgramVisiblePointViolationEvidence; - -macro_rules! package_program_id { - ($name:ident, $core:ty) => { - #[repr(transparent)] - #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] - #[must_use] - pub struct $name($core); - - impl $name { - pub const fn new(value: u32) -> Self { - Self(<$core>::new(value)) - } - - pub const fn value(self) -> u32 { - self.0.value() - } - - const fn from_core(value: $core) -> Self { - Self(value) - } - - const fn into_core(self) -> $core { - self.0 - } - } - - impl core::hash::Hash for $name { - fn hash(&self, state: &mut H) { - core::hash::Hash::hash(&self.value(), state); - } - } - }; -} - -macro_rules! package_program_projected_id { - ($name:ident, $core:ty) => { - #[repr(transparent)] - #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] - #[must_use] - pub struct $name($core); - - impl $name { - const fn from_core(value: $core) -> Self { - Self(value) - } - - pub const fn value(self) -> u32 { - self.0.value() - } - } - - impl core::hash::Hash for $name { - fn hash(&self, state: &mut H) { - core::hash::Hash::hash(&self.value(), state); - } - } - }; -} - -package_program_id!(PackageProgramSourceIdV1, SourceId); -package_program_id!(PackageProgramTargetIdV1, TargetId); -package_program_id!(PackageProgramTargetCandidateIdV1, TargetCandidateId); -package_program_id!(PackageProgramOpacityInputIdV1, OpacityInputId); -package_program_id!(PackageProgramPaintIdV1, PaintId); -package_program_id!(PackageProgramSurfaceInputPortIdV1, SurfaceInputPortId); -package_program_id!(PackageProgramSurfaceIdV1, SurfaceId); -package_program_id!(PackageProgramOccurrenceIdV1, OccurrenceId); -package_program_id!(PackageProgramConstraintIdV1, ConstraintId); -package_program_id!(PackageProgramOutputSlotIdV1, OutputSlotId); -package_program_projected_id!(PackageProgramStreamIdV1, ObservationStreamId); -package_program_projected_id!(PackageProgramScenarioIdV1, ScenarioId); - -/// One finite candidate, stored as the actual Core target-candidate IR node. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct PackageProgramTargetCandidateV1(TargetCandidateV1); - -impl PackageProgramTargetCandidateV1 { - pub const fn new(id: PackageProgramTargetCandidateIdV1, source: Srgb8) -> Self { - Self(TargetCandidateV1::from_srgb8(id.into_core(), source)) - } -} - -/// One typed target/candidate choice stored as the actual Core joint IR node. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct PackageProgramJointChoiceV1(TargetCandidateChoiceV1); - -impl PackageProgramJointChoiceV1 { - pub const fn new( - target: PackageProgramTargetIdV1, - candidate: PackageProgramTargetCandidateIdV1, - ) -> Self { - Self(TargetCandidateChoiceV1::new( - target.into_core(), - candidate.into_core(), - )) - } -} - -/// One complete explicit state in the finite joint order. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct PackageProgramJointStateV1(JointCandidateStateV1); - -impl PackageProgramJointStateV1 { - pub fn new(choices: Vec) -> Self { - Self(JointCandidateStateV1::new( - choices.into_iter().map(|choice| choice.0).collect(), - )) - } -} - -/// Registered surround input for the current CIECAM16 context release. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum PackageProgramSurroundV1 { - Average, - Dim, - Dark, -} - -impl PackageProgramSurroundV1 { - const fn into_core(self) -> SurroundProfileId { - match self { - Self::Average => SurroundProfileId::AverageV1, - Self::Dim => SurroundProfileId::DimV1, - Self::Dark => SurroundProfileId::DarkV1, - } - } -} - -/// Exact semantic input field rejected while forming an appearance context. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum PackageProgramAppearanceContextFieldV1 { - AdaptingLuminanceCdM2, - BackgroundLuminanceRatioYbYw, -} - -/// Exact numeric reason rejected while forming an appearance context. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum PackageProgramNumericDomainErrorV1 { - NonFinite, - Negative, - NotPositive, - AboveOne, -} - -/// Closed appearance-context admission failure. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum PackageProgramAppearanceContextErrorKindV1 { - Domain, - InternalInvariant, -} - -/// Closed appearance-context admission failure. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct PackageProgramAppearanceContextErrorV1 { - kind: PackageProgramAppearanceContextErrorKindV1, - field: Option, - reason: Option, -} - -impl PackageProgramAppearanceContextErrorV1 { - pub const fn kind(self) -> PackageProgramAppearanceContextErrorKindV1 { - self.kind - } - - pub const fn field(self) -> Option { - self.field - } - - pub const fn reason(self) -> Option { - self.reason - } - - fn from_core(error: AppearanceContextDomainErrorV1) -> Self { - let field = match error.field() { - AppearanceContextFieldV1::AdaptingLuminanceCdM2 => { - PackageProgramAppearanceContextFieldV1::AdaptingLuminanceCdM2 - } - AppearanceContextFieldV1::BackgroundLuminanceRatio => { - PackageProgramAppearanceContextFieldV1::BackgroundLuminanceRatioYbYw - } - }; - let reason = match error.reason() { - NumericDomainError::NonFinite => Some(PackageProgramNumericDomainErrorV1::NonFinite), - NumericDomainError::Negative => Some(PackageProgramNumericDomainErrorV1::Negative), - NumericDomainError::NotPositive => { - Some(PackageProgramNumericDomainErrorV1::NotPositive) - } - NumericDomainError::AboveOne => Some(PackageProgramNumericDomainErrorV1::AboveOne), - NumericDomainError::HueOutOfRange => None, - }; - match reason { - Some(reason) => Self { - kind: PackageProgramAppearanceContextErrorKindV1::Domain, - field: Some(field), - reason: Some(reason), - }, - None => Self { - kind: PackageProgramAppearanceContextErrorKindV1::InternalInvariant, - field: None, - reason: None, - }, - } - } -} - -/// Immutable admitted appearance context stored as the actual Core value. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct PackageProgramAppearanceContextV1(AppearanceContextId); - -impl PackageProgramAppearanceContextV1 { - /// Admit the explicit CIECAM16 viewing inputs for encoded sRGB8/D65. - /// `background_luminance_ratio_yb_yw` is the dimensionless ratio `Y_b/Y_w` - /// and must be finite in `(0, 1]`; it is not an absolute luminance. - pub fn try_new( - adapting_luminance_cd_m2: f64, - background_luminance_ratio_yb_yw: f64, - surround: PackageProgramSurroundV1, - ) -> Result { - let adapting_luminance_cd_m2 = AdaptingLuminanceCdM2::try_new(adapting_luminance_cd_m2) - .map_err(PackageProgramAppearanceContextErrorV1::from_core)?; - let background_luminance_ratio = - BackgroundLuminanceRatio::try_new(background_luminance_ratio_yb_yw) - .map_err(PackageProgramAppearanceContextErrorV1::from_core)?; - Ok(Self(AppearanceContextId::from_inputs( - AppearanceContextSchemaReleaseId::Ciecam16ViewingInputsV1, - IEC_SRGB_D65_XYZ_FRAME_V1, - adapting_luminance_cd_m2, - background_luminance_ratio, - surround.into_core(), - ))) - } - - pub fn adapting_luminance_cd_m2(self) -> f64 { - self.0.adapting_luminance_cd_m2() - } - - pub fn background_luminance_ratio_yb_yw(self) -> f64 { - self.0.background_luminance_ratio() - } - - pub const fn surround(self) -> PackageProgramSurroundV1 { - match self.0.surround_profile() { - SurroundProfileId::AverageV1 => PackageProgramSurroundV1::Average, - SurroundProfileId::DimV1 => PackageProgramSurroundV1::Dim, - SurroundProfileId::DarkV1 => PackageProgramSurroundV1::Dark, - } - } -} - -/// Closed compile classification; the generic Core error never escapes. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum PackageProgramCompileErrorKindV1 { - DuplicateSource, - DuplicateTarget, - DuplicateTargetCandidate, - DuplicateTargetCandidateSignal, - DuplicateOpacityInput, - DuplicateSurfaceInputPort, - UnusedSurfaceInputPort, - DuplicateSurfaceInputBinding, - DuplicatePaint, - DuplicateSurface, - DuplicateOccurrence, - DuplicateConstraint, - DuplicateOutputSlot, - MissingTargetSource, - MissingPaintTarget, - MissingPaintSource, - MissingPaintOpacityInput, - MissingSurfaceInputPort, - MissingSurfaceOccurrence, - MissingOccurrencePaint, - MissingOccurrenceBackdrop, - MissingConstraintOccurrence, - MissingOutputPaint, - PaintCycle, - RenderCycle, - OpacityOutOfDomain, - EmptyTargetDomain, - UnconstrainedTarget, - DisconnectedFiniteTargets, - UnassessedOutput, - MissingJointSelection, - JointSelectionWithoutTargets, - JointStateDuplicateTarget, - JointStateMissingTarget, - JointStateUnknownTarget, - JointStateUnknownCandidate, - InvalidJointOrder, - EmptySurfaceInputPortSet, - EmptyOccurrenceSet, - EmptyConstraintSet, - EmptyOutputSet, - ResourceExhausted, - InternalInvariant, -} - -/// Typed offending identity when one error has a single attributable handle. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum PackageProgramCompileErrorHandleV1 { - Source(PackageProgramSourceIdV1), - Target(PackageProgramTargetIdV1), - TargetCandidate(PackageProgramTargetCandidateIdV1), - OpacityInput(PackageProgramOpacityInputIdV1), - Paint(PackageProgramPaintIdV1), - SurfaceInputPort(PackageProgramSurfaceInputPortIdV1), - Surface(PackageProgramSurfaceIdV1), - Occurrence(PackageProgramOccurrenceIdV1), - Constraint(PackageProgramConstraintIdV1), - OutputSlot(PackageProgramOutputSlotIdV1), -} - -impl PackageProgramCompileErrorHandleV1 { - pub const fn value(self) -> u32 { - match self { - Self::Source(value) => value.value(), - Self::Target(value) => value.value(), - Self::TargetCandidate(value) => value.value(), - Self::OpacityInput(value) => value.value(), - Self::Paint(value) => value.value(), - Self::SurfaceInputPort(value) => value.value(), - Self::Surface(value) => value.value(), - Self::Occurrence(value) => value.value(), - Self::Constraint(value) => value.value(), - Self::OutputSlot(value) => value.value(), - } - } -} - -/// Exact owned members of one paint dependency cycle. -/// -/// The wrapper takes ownership of Core's existing allocation. Projecting a -/// compile failure therefore cannot introduce a second infallible allocation. -#[derive(Debug, PartialEq, Eq)] -pub struct PackageProgramPaintCycleV1 { - paints: Vec, -} - -impl PackageProgramPaintCycleV1 { - pub fn paints(&self) -> impl ExactSizeIterator + '_ { - self.paints - .iter() - .copied() - .map(PackageProgramPaintIdV1::from_core) - } -} - -/// Exact owned members of one render dependency cycle. -/// -/// Surface and occurrence identities remain separate physical namespaces. -#[derive(Debug, PartialEq, Eq)] -pub struct PackageProgramRenderCycleV1 { - surfaces: Vec, - occurrences: Vec, -} - -impl PackageProgramRenderCycleV1 { - pub fn surfaces(&self) -> impl ExactSizeIterator + '_ { - self.surfaces - .iter() - .copied() - .map(PackageProgramSurfaceIdV1::from_core) - } - - pub fn occurrences(&self) -> impl ExactSizeIterator + '_ { - self.occurrences - .iter() - .copied() - .map(PackageProgramOccurrenceIdV1::from_core) - } -} - -/// Exact closed reason why an explicit finite joint order was rejected. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum PackageProgramJointOrderErrorV1 { - EmptyDomain { - dimension: usize, - }, - CardinalityOverflow, - EmptyOrder, - TupleArity { - state: usize, - expected: usize, - actual: usize, - }, - OrdinalOutOfDomain { - state: usize, - dimension: usize, - ordinal: usize, - domain_len: usize, - }, - DuplicateTuple { - first_state: usize, - duplicate_state: usize, - }, - IncompleteOrder { - expected: usize, - actual: usize, - }, - ResourceExhausted, -} - -/// Atomic, lossless authored-program compile failure. -/// -/// This public enum is authoritative. `kind`, `primary_handle`, and -/// `related_handle` are convenience projections only; they never replace the -/// complete typed payload carried by the matching variant. -#[derive(Debug, PartialEq, Eq)] -pub enum PackageProgramCompileErrorV1 { - DuplicateSource { - source: PackageProgramSourceIdV1, - }, - DuplicateTarget { - target: PackageProgramTargetIdV1, - }, - MissingTargetSource { - target: PackageProgramTargetIdV1, - source: PackageProgramSourceIdV1, - }, - DuplicateOpacityInput { - input: PackageProgramOpacityInputIdV1, - }, - DuplicateSurfaceInputPort { - input: PackageProgramSurfaceInputPortIdV1, - }, - UnusedSurfaceInputPort { - input: PackageProgramSurfaceInputPortIdV1, - }, - DuplicateSurfaceInputBinding { - input: PackageProgramSurfaceInputPortIdV1, - first: PackageProgramSurfaceIdV1, - duplicate: PackageProgramSurfaceIdV1, - }, - DuplicatePaint { - paint: PackageProgramPaintIdV1, - }, - DuplicateSurface { - surface: PackageProgramSurfaceIdV1, - }, - DuplicateOccurrence { - occurrence: PackageProgramOccurrenceIdV1, - }, - MissingPaintTarget { - paint: PackageProgramPaintIdV1, - target: PackageProgramTargetIdV1, - }, - MissingPaintSource { - paint: PackageProgramPaintIdV1, - source: PackageProgramPaintIdV1, - }, - MissingPaintOpacityInput { - paint: PackageProgramPaintIdV1, - input: PackageProgramOpacityInputIdV1, - }, - MissingSurfaceInputPort { - surface: PackageProgramSurfaceIdV1, - input: PackageProgramSurfaceInputPortIdV1, - }, - MissingSurfaceOccurrence { - surface: PackageProgramSurfaceIdV1, - occurrence: PackageProgramOccurrenceIdV1, - }, - MissingOccurrencePaint { - occurrence: PackageProgramOccurrenceIdV1, - paint: PackageProgramPaintIdV1, - }, - MissingOccurrenceBackdrop { - occurrence: PackageProgramOccurrenceIdV1, - surface: PackageProgramSurfaceIdV1, - }, - PaintCycle(PackageProgramPaintCycleV1), - RenderCycle(PackageProgramRenderCycleV1), - OpacityOutOfDomain { - input: PackageProgramOpacityInputIdV1, - }, - EmptyTargetDomain { - target: PackageProgramTargetIdV1, - }, - DuplicateTargetCandidate { - target: PackageProgramTargetIdV1, - candidate: PackageProgramTargetCandidateIdV1, - }, - DuplicateTargetCandidateSignal { - target: PackageProgramTargetIdV1, - first: PackageProgramTargetCandidateIdV1, - duplicate: PackageProgramTargetCandidateIdV1, - encoded_srgb8: Srgb8, - }, - UnconstrainedTarget { - target: PackageProgramTargetIdV1, - }, - DisconnectedFiniteTargets, - UnassessedOutput { - output: PackageProgramOutputSlotIdV1, - paint: PackageProgramPaintIdV1, - }, - MissingJointSelection, - JointSelectionWithoutTargets, - JointStateDuplicateTarget { - state: usize, - target: PackageProgramTargetIdV1, - }, - JointStateMissingTarget { - state: usize, - target: PackageProgramTargetIdV1, - }, - JointStateUnknownTarget { - state: usize, - target: PackageProgramTargetIdV1, - }, - JointStateUnknownCandidate { - state: usize, - target: PackageProgramTargetIdV1, - candidate: PackageProgramTargetCandidateIdV1, - }, - InvalidJointOrder(PackageProgramJointOrderErrorV1), - /// The package contract has one code-owned atomic observation group; - /// authored input ports are its complete, canonical membership. - EmptySurfaceInputPortSet, - EmptyOccurrenceSet, - EmptyConstraintSet, - EmptyOutputSet, - DuplicateConstraint { - constraint: PackageProgramConstraintIdV1, - }, - MissingConstraintOccurrence { - constraint: PackageProgramConstraintIdV1, - occurrence: PackageProgramOccurrenceIdV1, - }, - DuplicateOutputSlot { - output: PackageProgramOutputSlotIdV1, - }, - MissingOutputPaint { - output: PackageProgramOutputSlotIdV1, - paint: PackageProgramPaintIdV1, - }, - ResourceExhausted, - InternalInvariant, -} - -impl PackageProgramCompileErrorV1 { - pub const fn kind(&self) -> PackageProgramCompileErrorKindV1 { - use PackageProgramCompileErrorKindV1 as Kind; - - match self { - Self::DuplicateSource { .. } => Kind::DuplicateSource, - Self::DuplicateTarget { .. } => Kind::DuplicateTarget, - Self::MissingTargetSource { .. } => Kind::MissingTargetSource, - Self::DuplicateOpacityInput { .. } => Kind::DuplicateOpacityInput, - Self::DuplicateSurfaceInputPort { .. } => Kind::DuplicateSurfaceInputPort, - Self::UnusedSurfaceInputPort { .. } => Kind::UnusedSurfaceInputPort, - Self::DuplicateSurfaceInputBinding { .. } => Kind::DuplicateSurfaceInputBinding, - Self::DuplicatePaint { .. } => Kind::DuplicatePaint, - Self::DuplicateSurface { .. } => Kind::DuplicateSurface, - Self::DuplicateOccurrence { .. } => Kind::DuplicateOccurrence, - Self::MissingPaintTarget { .. } => Kind::MissingPaintTarget, - Self::MissingPaintSource { .. } => Kind::MissingPaintSource, - Self::MissingPaintOpacityInput { .. } => Kind::MissingPaintOpacityInput, - Self::MissingSurfaceInputPort { .. } => Kind::MissingSurfaceInputPort, - Self::MissingSurfaceOccurrence { .. } => Kind::MissingSurfaceOccurrence, - Self::MissingOccurrencePaint { .. } => Kind::MissingOccurrencePaint, - Self::MissingOccurrenceBackdrop { .. } => Kind::MissingOccurrenceBackdrop, - Self::PaintCycle(_) => Kind::PaintCycle, - Self::RenderCycle(_) => Kind::RenderCycle, - Self::OpacityOutOfDomain { .. } => Kind::OpacityOutOfDomain, - Self::EmptyTargetDomain { .. } => Kind::EmptyTargetDomain, - Self::DuplicateTargetCandidate { .. } => Kind::DuplicateTargetCandidate, - Self::DuplicateTargetCandidateSignal { .. } => Kind::DuplicateTargetCandidateSignal, - Self::UnconstrainedTarget { .. } => Kind::UnconstrainedTarget, - Self::DisconnectedFiniteTargets => Kind::DisconnectedFiniteTargets, - Self::UnassessedOutput { .. } => Kind::UnassessedOutput, - Self::MissingJointSelection => Kind::MissingJointSelection, - Self::JointSelectionWithoutTargets => Kind::JointSelectionWithoutTargets, - Self::JointStateDuplicateTarget { .. } => Kind::JointStateDuplicateTarget, - Self::JointStateMissingTarget { .. } => Kind::JointStateMissingTarget, - Self::JointStateUnknownTarget { .. } => Kind::JointStateUnknownTarget, - Self::JointStateUnknownCandidate { .. } => Kind::JointStateUnknownCandidate, - Self::InvalidJointOrder(_) => Kind::InvalidJointOrder, - Self::EmptySurfaceInputPortSet => Kind::EmptySurfaceInputPortSet, - Self::EmptyOccurrenceSet => Kind::EmptyOccurrenceSet, - Self::EmptyConstraintSet => Kind::EmptyConstraintSet, - Self::EmptyOutputSet => Kind::EmptyOutputSet, - Self::DuplicateConstraint { .. } => Kind::DuplicateConstraint, - Self::MissingConstraintOccurrence { .. } => Kind::MissingConstraintOccurrence, - Self::DuplicateOutputSlot { .. } => Kind::DuplicateOutputSlot, - Self::MissingOutputPaint { .. } => Kind::MissingOutputPaint, - Self::ResourceExhausted => Kind::ResourceExhausted, - Self::InternalInvariant => Kind::InternalInvariant, - } - } - - pub const fn primary_handle(&self) -> Option { - use PackageProgramCompileErrorHandleV1 as Handle; - - match self { - Self::DuplicateSource { source } => Some(Handle::Source(*source)), - Self::DuplicateTarget { target } - | Self::EmptyTargetDomain { target } - | Self::UnconstrainedTarget { target } - | Self::JointStateDuplicateTarget { target, .. } - | Self::JointStateMissingTarget { target, .. } - | Self::JointStateUnknownTarget { target, .. } - | Self::JointStateUnknownCandidate { target, .. } => Some(Handle::Target(*target)), - Self::MissingTargetSource { target, .. } - | Self::DuplicateTargetCandidate { target, .. } - | Self::DuplicateTargetCandidateSignal { target, .. } => Some(Handle::Target(*target)), - Self::DuplicateOpacityInput { input } | Self::OpacityOutOfDomain { input } => { - Some(Handle::OpacityInput(*input)) - } - Self::DuplicateSurfaceInputPort { input } - | Self::UnusedSurfaceInputPort { input } - | Self::DuplicateSurfaceInputBinding { input, .. } => { - Some(Handle::SurfaceInputPort(*input)) - } - Self::DuplicatePaint { paint } - | Self::MissingPaintTarget { paint, .. } - | Self::MissingPaintSource { paint, .. } - | Self::MissingPaintOpacityInput { paint, .. } => Some(Handle::Paint(*paint)), - Self::DuplicateSurface { surface } - | Self::MissingSurfaceInputPort { surface, .. } - | Self::MissingSurfaceOccurrence { surface, .. } => Some(Handle::Surface(*surface)), - Self::DuplicateOccurrence { occurrence } - | Self::MissingOccurrencePaint { occurrence, .. } - | Self::MissingOccurrenceBackdrop { occurrence, .. } => { - Some(Handle::Occurrence(*occurrence)) - } - Self::UnassessedOutput { output, .. } - | Self::DuplicateOutputSlot { output } - | Self::MissingOutputPaint { output, .. } => Some(Handle::OutputSlot(*output)), - Self::DuplicateConstraint { constraint } - | Self::MissingConstraintOccurrence { constraint, .. } => { - Some(Handle::Constraint(*constraint)) - } - Self::PaintCycle(_) - | Self::RenderCycle(_) - | Self::DisconnectedFiniteTargets - | Self::MissingJointSelection - | Self::JointSelectionWithoutTargets - | Self::InvalidJointOrder(_) - | Self::EmptySurfaceInputPortSet - | Self::EmptyOccurrenceSet - | Self::EmptyConstraintSet - | Self::EmptyOutputSet - | Self::ResourceExhausted - | Self::InternalInvariant => None, - } - } - - pub const fn related_handle(&self) -> Option { - use PackageProgramCompileErrorHandleV1 as Handle; - - match self { - Self::MissingTargetSource { source, .. } => Some(Handle::Source(*source)), - Self::DuplicateSurfaceInputBinding { duplicate, .. } => { - Some(Handle::Surface(*duplicate)) - } - Self::MissingPaintTarget { target, .. } => Some(Handle::Target(*target)), - Self::MissingPaintSource { source, .. } => Some(Handle::Paint(*source)), - Self::MissingPaintOpacityInput { input, .. } => Some(Handle::OpacityInput(*input)), - Self::MissingSurfaceInputPort { input, .. } => Some(Handle::SurfaceInputPort(*input)), - Self::MissingSurfaceOccurrence { occurrence, .. } - | Self::MissingConstraintOccurrence { occurrence, .. } => { - Some(Handle::Occurrence(*occurrence)) - } - Self::MissingOccurrencePaint { paint, .. } - | Self::UnassessedOutput { paint, .. } - | Self::MissingOutputPaint { paint, .. } => Some(Handle::Paint(*paint)), - Self::MissingOccurrenceBackdrop { surface, .. } => Some(Handle::Surface(*surface)), - Self::DuplicateTargetCandidate { candidate, .. } - | Self::DuplicateTargetCandidateSignal { - duplicate: candidate, - .. - } - | Self::JointStateUnknownCandidate { candidate, .. } => { - Some(Handle::TargetCandidate(*candidate)) - } - Self::DuplicateSource { .. } - | Self::DuplicateTarget { .. } - | Self::DuplicateOpacityInput { .. } - | Self::DuplicateSurfaceInputPort { .. } - | Self::UnusedSurfaceInputPort { .. } - | Self::DuplicatePaint { .. } - | Self::DuplicateSurface { .. } - | Self::DuplicateOccurrence { .. } - | Self::PaintCycle(_) - | Self::RenderCycle(_) - | Self::OpacityOutOfDomain { .. } - | Self::EmptyTargetDomain { .. } - | Self::UnconstrainedTarget { .. } - | Self::DisconnectedFiniteTargets - | Self::MissingJointSelection - | Self::JointSelectionWithoutTargets - | Self::JointStateDuplicateTarget { .. } - | Self::JointStateMissingTarget { .. } - | Self::JointStateUnknownTarget { .. } - | Self::InvalidJointOrder(_) - | Self::EmptySurfaceInputPortSet - | Self::EmptyOccurrenceSet - | Self::EmptyConstraintSet - | Self::EmptyOutputSet - | Self::DuplicateConstraint { .. } - | Self::DuplicateOutputSlot { .. } - | Self::ResourceExhausted - | Self::InternalInvariant => None, - } - } -} - -/// Concrete cold-path builder over the actual Core Program IR. -#[must_use] -pub struct PackageProgramDraftV1 { - inner: CoreProgramDraftV1, -} - -/// Draft mutation rejected before Core compilation. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum PackageProgramDraftErrorV1 { - JointSelectionAlreadyDeclared, -} - -impl PackageProgramDraftV1 { - pub fn new() -> Self { - Self { - inner: CoreProgramDraftV1::new(), - } - } - - pub fn push_source(&mut self, id: PackageProgramSourceIdV1, source: Srgb8) -> &mut Self { - self.inner - .push_source(Source::new(id.into_core(), ColorSignal::from_srgb8(source))); - self - } - - pub fn push_fixed_target( - &mut self, - id: PackageProgramTargetIdV1, - source: PackageProgramSourceIdV1, - ) -> &mut Self { - self.inner - .push_target(Target::fixed(id.into_core(), source.into_core())); - self - } - - pub fn push_finite_target( - &mut self, - id: PackageProgramTargetIdV1, - source: PackageProgramSourceIdV1, - candidates: Vec, - ) -> &mut Self { - self.inner.push_target(Target::finite( - id.into_core(), - source.into_core(), - candidates - .into_iter() - .map(|candidate| candidate.0) - .collect(), - )); - self - } - - pub fn set_joint_selection( - &mut self, - states: Vec, - ) -> Result<&mut Self, PackageProgramDraftErrorV1> { - self.inner - .set_joint_selection(DeclaredJointSelectionV1::new( - states.into_iter().map(|state| state.0).collect(), - )) - .map_err(|error| match error { - CoreProgramDraftErrorV1::JointSelectionAlreadyDeclared => { - PackageProgramDraftErrorV1::JointSelectionAlreadyDeclared - } - })?; - Ok(self) - } - - pub fn push_surface_input_port( - &mut self, - input: PackageProgramSurfaceInputPortIdV1, - ) -> &mut Self { - self.inner.push_surface_input_port(input.into_core()); - self - } - - pub fn push_opacity_input( - &mut self, - id: PackageProgramOpacityInputIdV1, - value: f64, - ) -> &mut Self { - self.inner - .push_opacity_input(OpacityInput::new(id.into_core(), value)); - self - } - - pub fn push_solid_paint( - &mut self, - id: PackageProgramPaintIdV1, - target: PackageProgramTargetIdV1, - ) -> &mut Self { - self.inner.push_paint(Paint::Solid { - id: id.into_core(), - target: target.into_core(), - }); - self - } - - pub fn push_opacity_paint( - &mut self, - id: PackageProgramPaintIdV1, - source: PackageProgramPaintIdV1, - opacity: PackageProgramOpacityInputIdV1, - ) -> &mut Self { - self.inner.push_paint(Paint::Opacity { - id: id.into_core(), - source: source.into_core(), - opacity: opacity.into_core(), - }); - self - } - - pub fn push_input_surface( - &mut self, - id: PackageProgramSurfaceIdV1, - input: PackageProgramSurfaceInputPortIdV1, - ) -> &mut Self { - self.inner.push_surface(Surface::Input { - id: id.into_core(), - input: input.into_core(), - }); - self - } - - pub fn push_occurrence_surface( - &mut self, - id: PackageProgramSurfaceIdV1, - occurrence: PackageProgramOccurrenceIdV1, - ) -> &mut Self { - self.inner.push_surface(Surface::FromOccurrence { - id: id.into_core(), - occurrence: occurrence.into_core(), - }); - self - } - - pub fn push_source_over_occurrence( - &mut self, - id: PackageProgramOccurrenceIdV1, - subject: PackageProgramPaintIdV1, - against: PackageProgramSurfaceIdV1, - context: PackageProgramAppearanceContextV1, - ) -> &mut Self { - self.inner.push_occurrence(Occurrence::new( - id.into_core(), - subject.into_core(), - against.into_core(), - CompositionProfile::EncodedSrgb8SourceOverV1, - context.0, - )); - self - } - - pub fn push_exact_hard( - &mut self, - id: PackageProgramConstraintIdV1, - occurrence: PackageProgramOccurrenceIdV1, - expected: Srgb8, - ) -> &mut Self { - self.inner.push_hard_constraint(ConstraintInvocation::hard( - id.into_core(), - occurrence.into_core(), - CoreProgramConstraintInvocationV1::ExactSrgb8(expected), - )); - self - } - - pub fn push_exact_report_only( - &mut self, - id: PackageProgramConstraintIdV1, - occurrence: PackageProgramOccurrenceIdV1, - expected: Srgb8, - ) -> &mut Self { - self.inner - .push_report_constraint(ConstraintInvocation::report_only( - id.into_core(), - occurrence.into_core(), - CoreProgramConstraintInvocationV1::ExactSrgb8(expected), - )); - self - } - - pub fn push_wcag22_hard( - &mut self, - id: PackageProgramConstraintIdV1, - occurrence: PackageProgramOccurrenceIdV1, - criterion: Wcag22CriterionV1, - ) -> &mut Self { - self.inner.push_hard_constraint(ConstraintInvocation::hard( - id.into_core(), - occurrence.into_core(), - CoreProgramConstraintInvocationV1::Wcag22Srgb8(criterion), - )); - self - } - - pub fn push_wcag22_report_only( - &mut self, - id: PackageProgramConstraintIdV1, - occurrence: PackageProgramOccurrenceIdV1, - criterion: Wcag22CriterionV1, - ) -> &mut Self { - self.inner - .push_report_constraint(ConstraintInvocation::report_only( - id.into_core(), - occurrence.into_core(), - CoreProgramConstraintInvocationV1::Wcag22Srgb8(criterion), - )); - self - } - - pub fn push_output( - &mut self, - output: PackageProgramOutputSlotIdV1, - paint: PackageProgramPaintIdV1, - ) -> &mut Self { - self.inner - .push_output(OutputBinding::new(output.into_core(), paint.into_core())); - self - } - - pub fn compile(self) -> Result { - let compiled = self.inner.compile().map_err(map_program_compile_error)?; - Ok(PackageProgramOwnerV1::from_compiled(compiled)) - } -} - -impl Default for PackageProgramDraftV1 { - fn default() -> Self { - Self::new() - } -} - -/// Opaque strong owner of one exact compiled Core Program. -/// -/// Sessions instantiated from this owner are independently mutable only -/// through this exact allocation. Dropping it revokes updates and operation -/// projections; Session-owned historical evidence remains readable. -pub struct PackageProgramOwnerV1 { - compiled: CompiledCoreProgramV1, -} - -/// A Session belongs to a different immutable owner allocation. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum PackageProgramAccessErrorV1 { - OwnerMismatch, -} - -impl PackageProgramOwnerV1 { - /// Internal handoff from the canonical concrete Core draft compiler. - pub(crate) const fn from_compiled(compiled: CompiledCoreProgramV1) -> Self { - Self { compiled } - } - - /// Number of schema-ordered surface values required in every scenario. - pub fn surface_input_port_count(&self) -> usize { - self.compiled.surface_input_ports().len() - } - - /// Canonically ordered authored input handles for one-time host binding. - pub fn surface_input_ports( - &self, - ) -> impl ExactSizeIterator + '_ { - self.compiled - .surface_input_ports() - .iter() - .copied() - .map(PackageProgramSurfaceInputPortIdV1::from_core) - } - - /// Canonically ordered opaque output slots owned by this Program. - pub fn output_slots(&self) -> impl ExactSizeIterator + '_ { - self.compiled - .outputs() - .map(|(slot, _paint)| PackageProgramOutputSlotIdV1::from_core(slot)) - } - - /// Borrow one operation projection only for the exact Session generation - /// instantiated by this owner. Equivalent content is not authority. - pub fn project<'owner, 'session>( - &'owner self, - session: &'session PackageProgramSessionV1, - ) -> Result, PackageProgramAccessErrorV1> { - if !self.compiled.owns_session(&session.session) { - return Err(PackageProgramAccessErrorV1::OwnerMismatch); - } - Ok(PackageProgramProjectionV1 { - evidence: session.evidence(), - owner: self, - scope: PackageProgramBorrowScopeV1::new(self, session), - }) - } - - /// Admit and commit one update only when owner and Session are the exact - /// same generation, then borrow the resulting immutable snapshot. - pub fn update<'owner, 'session>( - &'owner self, - session: &'session mut PackageProgramSessionV1, - update: PackageProgramUpdateV1<'_>, - ) -> Result, PackageProgramUpdateErrorV1> { - if !self.compiled.owns_session(&session.session) { - return Err(PackageProgramUpdateErrorV1::new( - PackageProgramUpdateErrorKindV1::OwnerMismatch, - )); - } - session.apply_update(update)?; - Ok(PackageProgramProjectionV1 { - evidence: session.evidence(), - owner: self, - scope: PackageProgramBorrowScopeV1::new(self, session), - }) - } - - /// Instantiate one stream-affine Session without exposing a generation. - pub fn instantiate( - &self, - stream_id: u32, - ) -> Result { - let stream = ObservationStreamId::new(stream_id); - let session = self - .compiled - .instantiate(stream) - .map_err(PackageProgramInstantiateErrorV1::from_core)?; - Ok(PackageProgramSessionV1 { - scenario_order_scratch: Vec::new(), - session, - }) - } -} - -/// One borrowed physical scenario in the compiled schema order. -/// -/// A scenario ID is opaque provenance. `values` contains exactly one encoded -/// sRGB8 value per compiled surface input; ports are intentionally absent from -/// the hot package boundary. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct PackageProgramScenarioV1<'a> { - scenario_id: u32, - values: &'a [Srgb8], -} - -impl<'a> PackageProgramScenarioV1<'a> { - /// Construct one simultaneous physical tuple in compiled schema order. - pub const fn new(scenario_id: u32, values: &'a [Srgb8]) -> Self { - Self { - scenario_id, - values, - } - } -} - -/// One revision-bound package update. Stream ownership stays in the Session. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum PackageProgramUpdateV1<'a> { - /// Correlated, schema-ordered physical scenarios. - Observed { - revision: u64, - scenarios: &'a [PackageProgramScenarioV1<'a>], - }, - /// Explicitly unavailable observation; no background is invented. - Unknown { revision: u64, reason_id: u32 }, -} - -/// Concrete opaque owner of one mutable Core Program Session. -pub struct PackageProgramSessionV1 { - scenario_order_scratch: Vec, - session: CoreProgramSessionV1, -} - -impl PackageProgramSessionV1 { - /// Historical evidence remains readable after owner expiry. It never - /// grants an operation projection. - pub fn evidence(&self) -> PackageProgramEvidenceViewV1<'_> { - PackageProgramEvidenceViewV1 { - session: &self.session, - } - } - - fn apply_update( - &mut self, - update: PackageProgramUpdateV1<'_>, - ) -> Result<(), PackageProgramUpdateErrorV1> { - match update { - PackageProgramUpdateV1::Observed { - revision, - scenarios, - } => { - let source = PackageProgramScenarioSourceV1(scenarios); - self.session - .update_schema_ordered( - Revision::new(revision), - &source, - &mut self.scenario_order_scratch, - ) - .map_err(map_session_update_error)?; - } - PackageProgramUpdateV1::Unknown { - revision, - reason_id, - } => { - self.session - .update_unknown(Revision::new(revision), UnknownReasonId::new(reason_id)) - .map_err(map_session_update_error)?; - } - } - Ok(()) - } -} - -struct PackageProgramScenarioSourceV1<'a>(&'a [PackageProgramScenarioV1<'a>]); - -impl SchemaOrderedScenarioSourceV1 for PackageProgramScenarioSourceV1<'_> { - fn scenario_count(&self) -> usize { - self.0.len() - } - - fn scenario_id(&self, scenario_index: usize) -> ScenarioId { - ScenarioId::new(self.0[scenario_index].scenario_id) - } - - fn value_count(&self, scenario_index: usize) -> usize { - self.0[scenario_index].values.len() - } - - fn value(&self, scenario_index: usize, binding_index: usize) -> Srgb8 { - self.0[scenario_index].values[binding_index] - } -} - -/// Closed package-visible lifecycle classification. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum PackageProgramStateKindV1 { - Waiting, - Ready, - Stale, - Failed, -} - -/// Closed raw observation head, independent of evaluator lifecycle. -/// -/// A non-empty head retains stream provenance for detached correlation, never -/// as operation authority. `Empty` carries none because no observation exists. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum PackageProgramObservationHeadV1 { - Empty, - Unknown { - stream: PackageProgramStreamIdV1, - revision: u64, - reason_id: u32, - }, - Observed { - stream: PackageProgramStreamIdV1, - revision: u64, - }, -} - -/// Borrowed historical evidence owned solely by one Session. -#[derive(Clone, Copy)] -pub struct PackageProgramEvidenceViewV1<'a> { - session: &'a CoreProgramSessionV1, -} - -impl<'a> PackageProgramEvidenceViewV1<'a> { - const fn state(self) -> &'a CoreProgramStateV1 { - self.session.state() - } - - pub const fn kind(self) -> PackageProgramStateKindV1 { - match self.state() { - SessionState::Waiting => PackageProgramStateKindV1::Waiting, - SessionState::Ready { .. } => PackageProgramStateKindV1::Ready, - SessionState::Stale { .. } => PackageProgramStateKindV1::Stale, - SessionState::Failed { .. } => PackageProgramStateKindV1::Failed, - } - } - - pub fn observation_head(self) -> PackageProgramObservationHeadV1 { - match self.session.raw_head() { - ObservationHeadViewV1::Empty => PackageProgramObservationHeadV1::Empty, - ObservationHeadViewV1::Unknown(unknown) => PackageProgramObservationHeadV1::Unknown { - stream: PackageProgramStreamIdV1::from_core(unknown.stream()), - revision: unknown.revision().value(), - reason_id: unknown.reason().value(), - }, - ObservationHeadViewV1::Observed(observation) => { - PackageProgramObservationHeadV1::Observed { - stream: PackageProgramStreamIdV1::from_core(observation.stream()), - revision: observation.revision().value(), - } - } - } - } - - /// Failed-state cause ordinal inside [`Self::certificates`]. - pub const fn cause_certificate_index(self) -> Option { - match self.state() { - SessionState::Failed { .. } => Some(0), - SessionState::Waiting | SessionState::Ready { .. } | SessionState::Stale { .. } => None, - } - } - - /// Core-owned certificates in canonical same-call ordinal order. - pub fn certificates( - self, - ) -> impl ExactSizeIterator> + FusedIterator + 'a { - let (first, second) = match self.state() { - SessionState::Waiting => (None, None), - SessionState::Ready { current } | SessionState::Stale { previous: current } => { - (Some(PackageProgramCertificateV1::verified(current)), None) - } - SessionState::Failed { cause, previous } => ( - Some(PackageProgramCertificateV1::conflict(cause)), - previous.as_ref().map(PackageProgramCertificateV1::verified), - ), - }; - PackageProgramCertificatesV1::new(first, second) - } -} - -/// Zero-sized lifetime marker tying an operation projection to one exact live -/// owner and one immutable Session snapshot. -/// -/// This constrains borrowed Rust values; it is not a sink commit capability. -#[derive(Clone, Copy)] -struct PackageProgramBorrowScopeV1<'owner, 'session> { - _scope: PhantomData<( - &'owner PackageProgramOwnerV1, - &'session PackageProgramSessionV1, - )>, -} - -impl<'owner, 'session> PackageProgramBorrowScopeV1<'owner, 'session> { - const fn new( - _owner: &'owner PackageProgramOwnerV1, - _session: &'session PackageProgramSessionV1, - ) -> Self { - Self { - _scope: PhantomData, - } - } -} - -/// Owner-and-snapshot-validated projection. Historical evidence is available -/// separately through [`PackageProgramSessionV1::evidence`]. -#[derive(Clone, Copy)] -pub struct PackageProgramProjectionV1<'owner, 'session> { - evidence: PackageProgramEvidenceViewV1<'session>, - owner: &'owner PackageProgramOwnerV1, - scope: PackageProgramBorrowScopeV1<'owner, 'session>, -} - -impl<'owner, 'session> PackageProgramProjectionV1<'owner, 'session> { - pub const fn evidence(self) -> PackageProgramEvidenceViewV1<'session> { - self.evidence - } - - /// Total canonical output projection for this lifecycle state. - pub fn operations( - self, - ) -> impl ExactSizeIterator> + FusedIterator - { - let inner = match self.evidence.state() { - SessionState::Waiting => PackageProgramOperationSourceV1::Empty, - SessionState::Ready { current } => { - debug_assert_eq!(current.outputs().len(), self.owner.compiled.output_count()); - debug_assert!( - current - .outputs() - .iter() - .enumerate() - .all(|(index, output)| self.owner.compiled.output_slot_at(index) - == Some(output.output())) - ); - PackageProgramOperationSourceV1::Set { - outputs: current.outputs().iter(), - certificate: PackageProgramVerifiedCertificateV1 { inner: current }, - scope: self.scope, - } - } - SessionState::Stale { previous } => PackageProgramOperationSourceV1::Hold { - outputs: previous.outputs().iter(), - certificate: PackageProgramVerifiedCertificateV1 { inner: previous }, - scope: self.scope, - }, - SessionState::Failed { - previous: Some(previous), - .. - } => PackageProgramOperationSourceV1::Hold { - outputs: previous.outputs().iter(), - certificate: PackageProgramVerifiedCertificateV1 { inner: previous }, - scope: self.scope, - }, - SessionState::Failed { previous: None, .. } => { - PackageProgramOperationSourceV1::Remove { - slots: PackageProgramOwnerOutputSlotsV1::new(&self.owner.compiled), - scope: self.scope, - } - } - }; - PackageProgramOperationsV1 { inner } - } -} - -/// Collision-resistant address of the canonical physical Program content. -/// It deliberately does not identify an owner epoch or runtime authority. -#[repr(transparent)] -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct PackageProgramContentIdentityV1([u8; 32]); - -impl PackageProgramContentIdentityV1 { - const fn from_core(value: ProgramContentIdentityV1) -> Self { - Self(*value.as_bytes()) - } - - pub const fn as_bytes(&self) -> &[u8; 32] { - &self.0 - } -} - -/// All hard cells passed over the complete admitted physical support. -#[derive(Clone, Copy)] -pub struct PackageProgramVerifiedCertificateV1<'a> { - inner: &'a CoreVerifiedV1, -} - -impl<'a> PackageProgramVerifiedCertificateV1<'a> { - pub const fn content_identity(self) -> PackageProgramContentIdentityV1 { - PackageProgramContentIdentityV1::from_core(self.inner.report().content_identity()) - } - - pub const fn observation(self) -> PackageProgramObservationV1<'a> { - PackageProgramObservationV1 { - inner: self.inner.report().observation(), - } - } - - pub const fn selected_state_index(self) -> Option { - self.inner.selected_state_index() - } - - pub fn cells( - self, - ) -> impl ExactSizeIterator> + FusedIterator + 'a { - self.inner - .report() - .cells() - .iter() - .map(PackageProgramVerifiedCellV1::from_core) - } - - pub fn outputs( - self, - ) -> impl ExactSizeIterator> + FusedIterator + 'a - { - self.inner - .outputs() - .iter() - .map(PackageProgramCertifiedOutputV1::from_core) - } -} - -/// Exhaustive proof that every declared candidate state violates a hard cell. -#[derive(Clone, Copy)] -pub struct PackageProgramConflictCertificateV1<'a> { - inner: &'a CoreConflictV1, -} - -impl<'a> PackageProgramConflictCertificateV1<'a> { - pub const fn content_identity(self) -> PackageProgramContentIdentityV1 { - PackageProgramContentIdentityV1::from_core(self.inner.report().content_identity()) - } - - pub const fn observation(self) -> PackageProgramObservationV1<'a> { - PackageProgramObservationV1 { - inner: self.inner.report().observation(), - } - } - - pub const fn considered_state_count(self) -> usize { - self.inner.considered_state_count() - } - - pub fn cells( - self, - ) -> impl ExactSizeIterator> + FusedIterator + 'a { - self.inner - .report() - .cells() - .iter() - .map(PackageProgramConflictCellV1::from_core) - } -} - -/// Closed borrowed projection of one exact Core-owned certificate. -/// -/// A certificate borrows only Session-owned history, so it can outlive the -/// owner that authorized the projection from which it was read. -/// -/// ```no_run -/// use labcolors_core::package_bridge::{ -/// PackageProgramCertificateV1, PackageProgramOwnerV1, PackageProgramSessionV1, -/// }; -/// -/// fn retain_evidence<'session>( -/// owner: PackageProgramOwnerV1, -/// session: &'session PackageProgramSessionV1, -/// ) -> PackageProgramCertificateV1<'session> { -/// owner -/// .project(session) -/// .unwrap() -/// .evidence() -/// .certificates() -/// .next() -/// .unwrap() -/// } -/// ``` -#[derive(Clone, Copy)] -pub enum PackageProgramCertificateV1<'a> { - Verified(PackageProgramVerifiedCertificateV1<'a>), - Conflict(PackageProgramConflictCertificateV1<'a>), -} - -impl<'a> PackageProgramCertificateV1<'a> { - const fn verified(value: &'a CoreVerifiedV1) -> Self { - Self::Verified(PackageProgramVerifiedCertificateV1 { inner: value }) - } - - const fn conflict(value: &'a CoreConflictV1) -> Self { - Self::Conflict(PackageProgramConflictCertificateV1 { inner: value }) - } - - pub const fn content_identity(self) -> PackageProgramContentIdentityV1 { - match self { - Self::Verified(value) => value.content_identity(), - Self::Conflict(value) => value.content_identity(), - } - } - - pub const fn observation(self) -> PackageProgramObservationV1<'a> { - match self { - Self::Verified(value) => value.observation(), - Self::Conflict(value) => value.observation(), - } - } - - #[cfg(test)] - pub(crate) fn observation_backing_ptr_for_test(self) -> *const () { - self.observation().inner.backing_ptr_for_test() - } -} - -/// The exact revision-bound observation retained by a certificate. -#[derive(Clone, Copy)] -pub struct PackageProgramObservationV1<'a> { - inner: &'a crate::observation::RevisionBoundObservationV1, -} - -impl<'a> PackageProgramObservationV1<'a> { - pub const fn stream(self) -> PackageProgramStreamIdV1 { - PackageProgramStreamIdV1::from_core(self.inner.stream()) - } - - pub const fn revision(self) -> u64 { - self.inner.revision().value() - } - - /// Canonical schema shared by every physical case. Position `i` is the - /// identity of position `i` in each [`PackageProgramPhysicalCaseV1::values`] - /// iterator; the two exact-size iterators always have equal length. - pub fn surface_input_ports( - self, - ) -> impl ExactSizeIterator + FusedIterator + 'a - { - self.inner - .schema() - .iter() - .copied() - .map(PackageProgramSurfaceInputPortIdV1::from_core) - } - - /// Canonical unique physical value vectors. Each case is schema-ordered by - /// [`Self::surface_input_ports`]; scenario IDs remain in `provenance()`. - pub fn physical_cases( - self, - ) -> impl ExactSizeIterator> + FusedIterator + 'a { - (0..self.inner.physical_case_count()).map(move |index| PackageProgramPhysicalCaseV1 { - observation: self.inner, - index, - }) - } -} - -/// Closed encoded signal family retained in a physical observation case. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum PackageProgramSignalV1 { - Iec61966Srgb8D65(Srgb8), -} - -/// One canonical physical observation case and its complete provenance set. -#[derive(Clone, Copy)] -pub struct PackageProgramPhysicalCaseV1<'a> { - observation: &'a crate::observation::RevisionBoundObservationV1, - index: usize, -} - -impl<'a> PackageProgramPhysicalCaseV1<'a> { - pub fn values( - self, - ) -> impl ExactSizeIterator + FusedIterator + 'a { - self.observation - .physical_values(self.index) - .expect("package case originates from the same observation") - .iter() - .copied() - .map(|signal| match signal.view() { - ColorSignalViewV1::Iec61966Srgb8D65(value) => { - PackageProgramSignalV1::Iec61966Srgb8D65(value) - } - }) - } - - pub fn provenance( - self, - ) -> impl ExactSizeIterator + FusedIterator + 'a { - self.observation - .provenance(self.index) - .expect("package case originates from the same observation") - .iter() - .copied() - .map(PackageProgramScenarioIdV1::from_core) - } -} - -/// Whether one constraint cell gates selection or is retained for reporting. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum PackageProgramConstraintModeV1 { - Hard, - ReportOnly, -} - -/// One selected/fixed case × constraint cell; state is owned by its certificate. -#[derive(Clone, Copy)] -pub struct PackageProgramVerifiedCellV1<'a> { - inner: &'a CoreProgramConstraintCellV1, -} - -impl<'a> PackageProgramVerifiedCellV1<'a> { - const fn from_core(inner: &'a CoreProgramConstraintCellV1) -> Self { - Self { inner } - } - - pub const fn case_index(self) -> usize { - self.inner.case_index() - } - - pub const fn constraint(self) -> PackageProgramConstraintIdV1 { - PackageProgramConstraintIdV1::from_core(self.inner.constraint()) - } - - pub const fn occurrence(self) -> PackageProgramOccurrenceIdV1 { - PackageProgramOccurrenceIdV1::from_core(self.inner.target()) - } - - pub const fn mode(self) -> PackageProgramConstraintModeV1 { - project_constraint_mode(self.inner) - } - - pub fn assessment(self) -> PackageProgramAssessmentV1<'a> { - project_assessment(self.inner) - } -} - -/// One exhaustive candidate-state × case × constraint conflict cell. -#[derive(Clone, Copy)] -pub struct PackageProgramConflictCellV1<'a> { - inner: &'a CoreProgramConstraintCellV1, -} - -impl<'a> PackageProgramConflictCellV1<'a> { - const fn from_core(inner: &'a CoreProgramConstraintCellV1) -> Self { - Self { inner } - } - - pub const fn state_index(self) -> usize { - self.inner.candidate_state_index() - } - - pub const fn case_index(self) -> usize { - self.inner.case_index() - } - - pub const fn constraint(self) -> PackageProgramConstraintIdV1 { - PackageProgramConstraintIdV1::from_core(self.inner.constraint()) - } - - pub const fn occurrence(self) -> PackageProgramOccurrenceIdV1 { - PackageProgramOccurrenceIdV1::from_core(self.inner.target()) - } - - pub const fn mode(self) -> PackageProgramConstraintModeV1 { - project_constraint_mode(self.inner) - } - - pub fn assessment(self) -> PackageProgramAssessmentV1<'a> { - project_assessment(self.inner) - } -} - -const fn project_constraint_mode( - cell: &CoreProgramConstraintCellV1, -) -> PackageProgramConstraintModeV1 { - if cell.is_hard() { - PackageProgramConstraintModeV1::Hard - } else { - PackageProgramConstraintModeV1::ReportOnly - } -} - -fn project_assessment(cell: &CoreProgramConstraintCellV1) -> PackageProgramAssessmentV1<'_> { - match cell.result() { - ProgramConstraintResultV1::Pass(CoreProgramPassEvidenceV1::ExactSrgb8(evidence)) => { - PackageProgramAssessmentV1::ExactSrgb8(PackageProgramExactSrgb8EvidenceV1 { - inner: PackageProgramExactSrgb8EvidenceRefV1::Pass(evidence), - }) - } - ProgramConstraintResultV1::Violation(CoreProgramViolationEvidenceV1::ExactSrgb8( - evidence, - )) => PackageProgramAssessmentV1::ExactSrgb8(PackageProgramExactSrgb8EvidenceV1 { - inner: PackageProgramExactSrgb8EvidenceRefV1::Violation(evidence), - }), - ProgramConstraintResultV1::Pass(CoreProgramPassEvidenceV1::Wcag22Srgb8(evidence)) => { - PackageProgramAssessmentV1::Wcag22Srgb8(PackageProgramWcag22Srgb8EvidenceV1 { - inner: PackageProgramWcag22Srgb8EvidenceRefV1::Pass(evidence), - }) - } - ProgramConstraintResultV1::Violation(CoreProgramViolationEvidenceV1::Wcag22Srgb8( - evidence, - )) => PackageProgramAssessmentV1::Wcag22Srgb8(PackageProgramWcag22Srgb8EvidenceV1 { - inner: PackageProgramWcag22Srgb8EvidenceRefV1::Violation(evidence), - }), - } -} - -/// Stored evaluator family. Its sealed witness retains the incompatible verdict. -#[derive(Clone, Copy)] -pub enum PackageProgramAssessmentV1<'a> { - ExactSrgb8(PackageProgramExactSrgb8EvidenceV1<'a>), - Wcag22Srgb8(PackageProgramWcag22Srgb8EvidenceV1<'a>), -} - -impl<'a> PackageProgramAssessmentV1<'a> { - pub const fn verdict(self) -> PackageProgramVerdictV1 { - match self { - Self::ExactSrgb8(value) => value.verdict(), - Self::Wcag22Srgb8(value) => value.verdict(), - } - } - - pub fn binding(self) -> PackageProgramPointBindingV1<'a> { - match self { - Self::ExactSrgb8(value) => value.binding(), - Self::Wcag22Srgb8(value) => value.binding(), - } - } -} - -/// Incompatible stored classifier outcomes. Clients cannot construct evidence. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum PackageProgramVerdictV1 { - Pass, - Violation, -} - -#[derive(Clone, Copy)] -enum PackageProgramExactSrgb8EvidenceRefV1<'a> { - Pass(&'a CoreExactPassEvidenceV1), - Violation(&'a CoreExactViolationEvidenceV1), -} - -/// Exact-sRGB8 expected value plus retained physical composition and modeled -/// tristimulus/context. -#[derive(Clone, Copy)] -pub struct PackageProgramExactSrgb8EvidenceV1<'a> { - inner: PackageProgramExactSrgb8EvidenceRefV1<'a>, -} - -impl<'a> PackageProgramExactSrgb8EvidenceV1<'a> { - pub const fn verdict(self) -> PackageProgramVerdictV1 { - match self.inner { - PackageProgramExactSrgb8EvidenceRefV1::Pass(_) => PackageProgramVerdictV1::Pass, - PackageProgramExactSrgb8EvidenceRefV1::Violation(_) => { - PackageProgramVerdictV1::Violation - } - } - } - - pub fn expected(self) -> Srgb8 { - match self.inner { - PackageProgramExactSrgb8EvidenceRefV1::Pass(value) => value.target(), - PackageProgramExactSrgb8EvidenceRefV1::Violation(value) => value.target(), - } - } - - pub fn binding(self) -> PackageProgramPointBindingV1<'a> { - let value = match self.inner { - PackageProgramExactSrgb8EvidenceRefV1::Pass(value) => value.binding(), - PackageProgramExactSrgb8EvidenceRefV1::Violation(value) => value.binding(), - }; - PackageProgramPointBindingV1 { inner: value } - } -} - -#[derive(Clone, Copy)] -enum PackageProgramWcag22Srgb8EvidenceRefV1<'a> { - Pass(&'a CoreWcag22PassEvidenceV1), - Violation(&'a CoreWcag22ViolationEvidenceV1), -} - -/// WCAG 2.2 profile, criterion, luminance evidence, physical composition and -/// modeled tristimulus/context retained by the Core report. -#[derive(Clone, Copy)] -pub struct PackageProgramWcag22Srgb8EvidenceV1<'a> { - inner: PackageProgramWcag22Srgb8EvidenceRefV1<'a>, -} - -impl<'a> PackageProgramWcag22Srgb8EvidenceV1<'a> { - pub const fn verdict(self) -> PackageProgramVerdictV1 { - match self.inner { - PackageProgramWcag22Srgb8EvidenceRefV1::Pass(_) => PackageProgramVerdictV1::Pass, - PackageProgramWcag22Srgb8EvidenceRefV1::Violation(_) => { - PackageProgramVerdictV1::Violation - } - } - } - - pub fn profile_id(self) -> Wcag22ProfileIdV1 { - match self.inner { - PackageProgramWcag22Srgb8EvidenceRefV1::Pass(value) => { - value.measurement().value().profile_id() - } - PackageProgramWcag22Srgb8EvidenceRefV1::Violation(value) => { - value.measurement().value().profile_id() - } - } - } - - pub fn criterion(self) -> Wcag22CriterionV1 { - match self.inner { - PackageProgramWcag22Srgb8EvidenceRefV1::Pass(value) => { - value.measurement().value().criterion() - } - PackageProgramWcag22Srgb8EvidenceRefV1::Violation(value) => { - value.measurement().value().criterion() - } - } - } - - pub fn foreground_luminance(self) -> Wcag22LuminanceBoundsQ55V1 { - let measurement = match self.inner { - PackageProgramWcag22Srgb8EvidenceRefV1::Pass(value) => { - value.measurement().value().measurement() - } - PackageProgramWcag22Srgb8EvidenceRefV1::Violation(value) => { - value.measurement().value().measurement() - } - }; - measurement.foreground_luminance - } - - pub fn background_luminance(self) -> Wcag22LuminanceBoundsQ55V1 { - let measurement = match self.inner { - PackageProgramWcag22Srgb8EvidenceRefV1::Pass(value) => { - value.measurement().value().measurement() - } - PackageProgramWcag22Srgb8EvidenceRefV1::Violation(value) => { - value.measurement().value().measurement() - } - }; - measurement.background_luminance - } - - pub fn numerical_evidence(self) -> &'a NumericalDecisionEvidenceV1 { - match self.inner { - PackageProgramWcag22Srgb8EvidenceRefV1::Pass(value) => { - value.measurement().value().evidence() - } - PackageProgramWcag22Srgb8EvidenceRefV1::Violation(value) => { - value.measurement().value().evidence() - } - } - } - - pub fn binding(self) -> PackageProgramPointBindingV1<'a> { - let value = match self.inner { - PackageProgramWcag22Srgb8EvidenceRefV1::Pass(value) => value.binding(), - PackageProgramWcag22Srgb8EvidenceRefV1::Violation(value) => value.binding(), - }; - PackageProgramPointBindingV1 { inner: value } - } -} - -/// Retained physical composition and modeled tristimulus/context shared by an -/// evaluator witness. -#[derive(Clone, Copy)] -pub struct PackageProgramPointBindingV1<'a> { - inner: &'a ProgramVisiblePointBindingV1, -} - -impl<'a> PackageProgramPointBindingV1<'a> { - pub const fn physical(self) -> PackageProgramPhysicalPointV1<'a> { - match self.inner.physical().occurrence().profile() { - CompositionProfileV1::EncodedSrgb8SourceOverV1 => { - PackageProgramPhysicalPointV1::EncodedSrgb8SourceOver( - PackageProgramEncodedSrgb8SourceOverV1 { inner: self.inner }, - ) - } - } - } - - pub const fn modeled(self) -> PackageProgramModeledPointV1<'a> { - match self.inner.modeled_lcs().provenance().binding() { - AdmittedSrgb8TristimulusBindingV1::Iec61966Srgb8ToCie1931TwoDegreeXyzD65RelativeY1V1 => { - PackageProgramModeledPointV1::Iec61966Srgb8ToCie1931TwoDegreeXyzD65RelativeY1( - PackageProgramModeledTristimulusV1 { inner: self.inner }, - ) - } - } - } -} - -/// Closed exact physical-composition family. -#[derive(Clone, Copy)] -pub enum PackageProgramPhysicalPointV1<'a> { - EncodedSrgb8SourceOver(PackageProgramEncodedSrgb8SourceOverV1<'a>), -} - -#[derive(Clone, Copy)] -pub struct PackageProgramEncodedSrgb8SourceOverV1<'a> { - inner: &'a ProgramVisiblePointBindingV1, -} - -impl PackageProgramEncodedSrgb8SourceOverV1<'_> { - pub const fn subject_paint(self) -> PackageProgramPaintIdV1 { - PackageProgramPaintIdV1::from_core(self.inner.physical().program_occurrence().subject()) - } - - pub const fn backdrop_surface(self) -> PackageProgramSurfaceIdV1 { - PackageProgramSurfaceIdV1::from_core( - self.inner - .physical() - .program_occurrence() - .backdrop_surface(), - ) - } - - pub const fn subject(self) -> Srgb8 { - Srgb8::new(self.inner.physical().occurrence().subject_rgb()) - } - - pub const fn opacity(self) -> f64 { - f64::from_bits(self.inner.physical().occurrence().subject_opacity_bits()) - } - - pub const fn backdrop(self) -> Srgb8 { - Srgb8::new(self.inner.physical().occurrence().backdrop_rgb()) - } - - pub const fn visible(self) -> Srgb8 { - Srgb8::new(self.inner.physical().occurrence().output_rgb()) - } -} - -/// Closed modeled-tristimulus provenance family. -#[derive(Clone, Copy)] -pub enum PackageProgramModeledPointV1<'a> { - Iec61966Srgb8ToCie1931TwoDegreeXyzD65RelativeY1(PackageProgramModeledTristimulusV1<'a>), -} - -#[derive(Clone, Copy)] -pub struct PackageProgramModeledTristimulusV1<'a> { - inner: &'a ProgramVisiblePointBindingV1, -} - -impl PackageProgramModeledTristimulusV1<'_> { - pub fn xyz(self) -> [f64; 3] { - self.inner.modeled_lcs().derivation().sample().xyz() - } - - pub const fn appearance_context(self) -> PackageProgramAppearanceContextV1 { - PackageProgramAppearanceContextV1(self.inner.modeled_lcs().occurrence().context()) - } -} - -/// One Core-certified output Paint. No output exists for Conflict. -#[derive(Clone, Copy)] -pub struct PackageProgramCertifiedOutputV1<'a> { - inner: &'a ProgramOutputV1, -} - -impl<'a> PackageProgramCertifiedOutputV1<'a> { - const fn from_core(inner: &'a ProgramOutputV1) -> Self { - Self { inner } - } - - pub const fn output_slot(self) -> PackageProgramOutputSlotIdV1 { - PackageProgramOutputSlotIdV1::from_core((*self.inner).output()) - } - - pub const fn paint(self) -> PackageProgramPaintIdV1 { - PackageProgramPaintIdV1::from_core((*self.inner).paint().id()) - } - - pub const fn source(self) -> Srgb8 { - (*self.inner).paint().source() - } - - pub const fn opacity(self) -> f64 { - (*self.inner).paint().opacity().value() - } -} - -/// A Set operation is structurally tied to the exact Verified certificate. -#[derive(Clone, Copy)] -pub struct PackageProgramSetV1<'owner, 'session> { - output: &'session ProgramOutputV1, - certificate: PackageProgramVerifiedCertificateV1<'session>, - _scope: PackageProgramBorrowScopeV1<'owner, 'session>, -} - -impl<'session> PackageProgramSetV1<'_, 'session> { - pub const fn output_slot(self) -> PackageProgramOutputSlotIdV1 { - PackageProgramOutputSlotIdV1::from_core((*self.output).output()) - } - - pub const fn source(self) -> Srgb8 { - (*self.output).paint().source() - } - - pub const fn opacity(self) -> f64 { - (*self.output).paint().opacity().value() - } - - pub const fn certificate(self) -> PackageProgramVerifiedCertificateV1<'session> { - self.certificate - } -} - -#[derive(Clone, Copy)] -pub struct PackageProgramRemoveV1<'owner, 'session> { - output_slot: PackageProgramOutputSlotIdV1, - _scope: PackageProgramBorrowScopeV1<'owner, 'session>, -} - -impl PackageProgramRemoveV1<'_, '_> { - pub const fn output_slot(self) -> PackageProgramOutputSlotIdV1 { - self.output_slot - } -} - -/// A Hold operation is structurally tied to the retained Verified certificate. -#[derive(Clone, Copy)] -pub struct PackageProgramHoldV1<'owner, 'session> { - output: &'session ProgramOutputV1, - certificate: PackageProgramVerifiedCertificateV1<'session>, - _scope: PackageProgramBorrowScopeV1<'owner, 'session>, -} - -impl<'session> PackageProgramHoldV1<'_, 'session> { - pub const fn output_slot(self) -> PackageProgramOutputSlotIdV1 { - PackageProgramOutputSlotIdV1::from_core((*self.output).output()) - } - - pub const fn certificate(self) -> PackageProgramVerifiedCertificateV1<'session> { - self.certificate - } -} - -/// Closed total operation union over opaque output slots. -/// -/// Every payload borrows both the exact owner and immutable Session snapshot; -/// destructuring a `Remove` cannot erase that scope. -/// Copied slot/source/opacity values are data only: a runtime adapter must -/// recheck its live owner, Session and revision immediately before one atomic -/// sink commit. -/// -/// ```compile_fail,E0515 -/// use labcolors_core::package_bridge::{ -/// PackageProgramOperationV1, PackageProgramOwnerV1, PackageProgramRemoveV1, -/// PackageProgramSessionV1, -/// }; -/// -/// fn escape_remove<'session>( -/// owner: PackageProgramOwnerV1, -/// session: &'session PackageProgramSessionV1, -/// ) -> PackageProgramRemoveV1<'session, 'session> { -/// match owner.project(session).unwrap().operations().next().unwrap() { -/// PackageProgramOperationV1::Remove(remove) => remove, -/// _ => panic!("fixture supplies Remove"), -/// } -/// } -/// ``` -/// -/// ```compile_fail,E0515 -/// use labcolors_core::package_bridge::{ -/// PackageProgramOperationV1, PackageProgramOwnerV1, PackageProgramRemoveV1, -/// }; -/// -/// fn escape_local_session<'owner>( -/// owner: &'owner PackageProgramOwnerV1, -/// ) -> PackageProgramRemoveV1<'owner, 'owner> { -/// let session = owner.instantiate(1).unwrap(); -/// match owner.project(&session).unwrap().operations().next().unwrap() { -/// PackageProgramOperationV1::Remove(remove) => remove, -/// _ => panic!("fixture supplies Remove"), -/// } -/// } -/// ``` -/// -/// ```compile_fail,E0515 -/// use labcolors_core::package_bridge::{ -/// PackageProgramOperationV1, PackageProgramOwnerV1, PackageProgramSessionV1, -/// PackageProgramSetV1, -/// }; -/// -/// fn escape_set<'session>( -/// owner: PackageProgramOwnerV1, -/// session: &'session PackageProgramSessionV1, -/// ) -> PackageProgramSetV1<'session, 'session> { -/// match owner.project(session).unwrap().operations().next().unwrap() { -/// PackageProgramOperationV1::Set(set) => set, -/// _ => panic!("fixture supplies Set"), -/// } -/// } -/// ``` -/// -/// ```compile_fail,E0515 -/// use labcolors_core::package_bridge::{ -/// PackageProgramHoldV1, PackageProgramOperationV1, PackageProgramOwnerV1, -/// PackageProgramSessionV1, -/// }; -/// -/// fn escape_hold<'session>( -/// owner: PackageProgramOwnerV1, -/// session: &'session PackageProgramSessionV1, -/// ) -> PackageProgramHoldV1<'session, 'session> { -/// match owner.project(session).unwrap().operations().next().unwrap() { -/// PackageProgramOperationV1::Hold(hold) => hold, -/// _ => panic!("fixture supplies Hold"), -/// } -/// } -/// ``` -/// -/// ```compile_fail,E0502 -/// use labcolors_core::package_bridge::{ -/// PackageProgramOperationV1, PackageProgramOwnerV1, PackageProgramSessionV1, -/// PackageProgramUpdateV1, -/// }; -/// -/// fn remove_blocks_session_mutation( -/// owner: &PackageProgramOwnerV1, -/// session: &mut PackageProgramSessionV1, -/// ) { -/// let remove = match owner.project(session).unwrap().operations().next().unwrap() { -/// PackageProgramOperationV1::Remove(remove) => remove, -/// _ => return, -/// }; -/// let _second = owner.update( -/// session, -/// PackageProgramUpdateV1::Unknown { -/// revision: 2, -/// reason_id: 7, -/// }, -/// ); -/// let _slot = remove.output_slot(); -/// } -/// ``` -#[derive(Clone, Copy)] -pub enum PackageProgramOperationV1<'owner, 'session> { - Set(PackageProgramSetV1<'owner, 'session>), - Remove(PackageProgramRemoveV1<'owner, 'session>), - Hold(PackageProgramHoldV1<'owner, 'session>), -} - -struct PackageProgramCertificatesV1<'a> { - values: [Option>; 2], - index: usize, - len: usize, -} - -impl<'a> PackageProgramCertificatesV1<'a> { - fn new( - first: Option>, - second: Option>, - ) -> Self { - let len = usize::from(first.is_some()) + usize::from(second.is_some()); - debug_assert!(first.is_some() || second.is_none()); - Self { - values: [first, second], - index: 0, - len, - } - } -} - -impl<'a> Iterator for PackageProgramCertificatesV1<'a> { - type Item = PackageProgramCertificateV1<'a>; - - fn next(&mut self) -> Option { - if self.index == self.len { - return None; - } - let value = self.values[self.index]; - self.index += 1; - value - } - - fn size_hint(&self) -> (usize, Option) { - let remaining = self.len - self.index; - (remaining, Some(remaining)) - } -} - -impl ExactSizeIterator for PackageProgramCertificatesV1<'_> {} -impl FusedIterator for PackageProgramCertificatesV1<'_> {} - -struct PackageProgramOwnerOutputSlotsV1<'owner> { - compiled: &'owner CompiledCoreProgramV1, - index: usize, - len: usize, -} - -impl<'owner> PackageProgramOwnerOutputSlotsV1<'owner> { - fn new(compiled: &'owner CompiledCoreProgramV1) -> Self { - Self { - compiled, - index: 0, - len: compiled.output_count(), - } - } -} - -impl Iterator for PackageProgramOwnerOutputSlotsV1<'_> { - type Item = PackageProgramOutputSlotIdV1; - - fn next(&mut self) -> Option { - if self.index == self.len { - return None; - } - let output = self.compiled.output_slot_at(self.index)?; - self.index += 1; - Some(PackageProgramOutputSlotIdV1::from_core(output)) - } - - fn size_hint(&self) -> (usize, Option) { - let remaining = self.len - self.index; - (remaining, Some(remaining)) - } -} - -impl ExactSizeIterator for PackageProgramOwnerOutputSlotsV1<'_> {} -impl FusedIterator for PackageProgramOwnerOutputSlotsV1<'_> {} - -enum PackageProgramOperationSourceV1<'owner, 'session> { - Empty, - Set { - outputs: slice::Iter<'session, ProgramOutputV1>, - certificate: PackageProgramVerifiedCertificateV1<'session>, - scope: PackageProgramBorrowScopeV1<'owner, 'session>, - }, - Hold { - outputs: slice::Iter<'session, ProgramOutputV1>, - certificate: PackageProgramVerifiedCertificateV1<'session>, - scope: PackageProgramBorrowScopeV1<'owner, 'session>, - }, - Remove { - slots: PackageProgramOwnerOutputSlotsV1<'owner>, - scope: PackageProgramBorrowScopeV1<'owner, 'session>, - }, -} - -struct PackageProgramOperationsV1<'owner, 'session> { - inner: PackageProgramOperationSourceV1<'owner, 'session>, -} - -impl<'owner, 'session> Iterator for PackageProgramOperationsV1<'owner, 'session> { - type Item = PackageProgramOperationV1<'owner, 'session>; - - fn next(&mut self) -> Option { - match &mut self.inner { - PackageProgramOperationSourceV1::Empty => None, - PackageProgramOperationSourceV1::Set { - outputs, - certificate, - scope, - } => { - let output = outputs.next()?; - Some(PackageProgramOperationV1::Set(PackageProgramSetV1 { - output, - certificate: *certificate, - _scope: *scope, - })) - } - PackageProgramOperationSourceV1::Hold { - outputs, - certificate, - scope, - } => Some(PackageProgramOperationV1::Hold(PackageProgramHoldV1 { - output: outputs.next()?, - certificate: *certificate, - _scope: *scope, - })), - PackageProgramOperationSourceV1::Remove { slots, scope } => { - Some(PackageProgramOperationV1::Remove(PackageProgramRemoveV1 { - output_slot: slots.next()?, - _scope: *scope, - })) - } - } - } - - fn size_hint(&self) -> (usize, Option) { - let remaining = match &self.inner { - PackageProgramOperationSourceV1::Empty => 0, - PackageProgramOperationSourceV1::Set { outputs, .. } => outputs.len(), - PackageProgramOperationSourceV1::Hold { outputs, .. } => outputs.len(), - PackageProgramOperationSourceV1::Remove { slots, .. } => slots.len(), - }; - (remaining, Some(remaining)) - } -} - -impl ExactSizeIterator for PackageProgramOperationsV1<'_, '_> {} -impl FusedIterator for PackageProgramOperationsV1<'_, '_> {} - -/// Closed package error classifications for Session construction. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum PackageProgramInstantiateErrorKindV1 { - ResourceExhausted, - InternalInvariant, -} - -/// Opaque Session construction failure. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct PackageProgramInstantiateErrorV1 { - kind: PackageProgramInstantiateErrorKindV1, -} - -impl PackageProgramInstantiateErrorV1 { - const fn new(kind: PackageProgramInstantiateErrorKindV1) -> Self { - Self { kind } - } - - fn from_core(error: ProgramSessionInstantiateError) -> Self { - let kind = match error { - ProgramSessionInstantiateError::ResourceExhausted => { - PackageProgramInstantiateErrorKindV1::ResourceExhausted - } - ProgramSessionInstantiateError::InternalInvariant => { - PackageProgramInstantiateErrorKindV1::InternalInvariant - } - }; - Self::new(kind) - } - - pub const fn kind(self) -> PackageProgramInstantiateErrorKindV1 { - self.kind - } -} - -impl From for PackageProgramInstantiateErrorV1 { - fn from(kind: PackageProgramInstantiateErrorKindV1) -> Self { - Self::new(kind) - } -} - -/// Closed package error classifications for one atomic update. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum PackageProgramUpdateErrorKindV1 { - OwnerMismatch, - InvalidObservation, - RevisionOutOfOrder, - RevisionConflict, - ResourceExhausted, - EvaluationFailed, - InternalInvariant, -} - -/// Opaque update failure. Core state is unchanged for every returned error. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct PackageProgramUpdateErrorV1 { - kind: PackageProgramUpdateErrorKindV1, -} - -impl PackageProgramUpdateErrorV1 { - const fn new(kind: PackageProgramUpdateErrorKindV1) -> Self { - Self { kind } - } - - pub const fn kind(self) -> PackageProgramUpdateErrorKindV1 { - self.kind - } -} - -fn map_joint_order_error(error: FiniteJointOrderErrorV1) -> PackageProgramJointOrderErrorV1 { - match error { - FiniteJointOrderErrorV1::EmptyDomain { dimension } => { - PackageProgramJointOrderErrorV1::EmptyDomain { dimension } - } - FiniteJointOrderErrorV1::CardinalityOverflow => { - PackageProgramJointOrderErrorV1::CardinalityOverflow - } - FiniteJointOrderErrorV1::EmptyOrder => PackageProgramJointOrderErrorV1::EmptyOrder, - FiniteJointOrderErrorV1::TupleArity { - tuple, - expected, - actual, - } => PackageProgramJointOrderErrorV1::TupleArity { - state: tuple, - expected, - actual, - }, - FiniteJointOrderErrorV1::OrdinalOutOfDomain { - tuple, - dimension, - ordinal, - domain_len, - } => PackageProgramJointOrderErrorV1::OrdinalOutOfDomain { - state: tuple, - dimension, - ordinal, - domain_len, - }, - FiniteJointOrderErrorV1::DuplicateTuple { first, duplicate } => { - PackageProgramJointOrderErrorV1::DuplicateTuple { - first_state: first, - duplicate_state: duplicate, - } - } - FiniteJointOrderErrorV1::IncompleteOrder { expected, actual } => { - PackageProgramJointOrderErrorV1::IncompleteOrder { expected, actual } - } - FiniteJointOrderErrorV1::ResourceExhausted => { - PackageProgramJointOrderErrorV1::ResourceExhausted - } - } -} - -fn map_program_compile_error(error: ProgramCompileError) -> PackageProgramCompileErrorV1 { - match error { - ProgramCompileError::DuplicateSource { source } => { - PackageProgramCompileErrorV1::DuplicateSource { - source: PackageProgramSourceIdV1::from_core(source), - } - } - ProgramCompileError::DuplicateTarget { target } => { - PackageProgramCompileErrorV1::DuplicateTarget { - target: PackageProgramTargetIdV1::from_core(target), - } - } - ProgramCompileError::MissingTargetSource { target, source } => { - PackageProgramCompileErrorV1::MissingTargetSource { - target: PackageProgramTargetIdV1::from_core(target), - source: PackageProgramSourceIdV1::from_core(source), - } - } - ProgramCompileError::DuplicateOpacityInput { input } => { - PackageProgramCompileErrorV1::DuplicateOpacityInput { - input: PackageProgramOpacityInputIdV1::from_core(input), - } - } - ProgramCompileError::DuplicateSurfaceInputPort { input } => { - PackageProgramCompileErrorV1::DuplicateSurfaceInputPort { - input: PackageProgramSurfaceInputPortIdV1::from_core(input), - } - } - ProgramCompileError::UnusedSurfaceInputPort { input } => { - PackageProgramCompileErrorV1::UnusedSurfaceInputPort { - input: PackageProgramSurfaceInputPortIdV1::from_core(input), - } - } - ProgramCompileError::DuplicateSurfaceInputBinding { - input, - first, - duplicate, - } => PackageProgramCompileErrorV1::DuplicateSurfaceInputBinding { - input: PackageProgramSurfaceInputPortIdV1::from_core(input), - first: PackageProgramSurfaceIdV1::from_core(first), - duplicate: PackageProgramSurfaceIdV1::from_core(duplicate), - }, - ProgramCompileError::DuplicatePaint { paint } => { - PackageProgramCompileErrorV1::DuplicatePaint { - paint: PackageProgramPaintIdV1::from_core(paint), - } - } - ProgramCompileError::DuplicateSurface { surface } => { - PackageProgramCompileErrorV1::DuplicateSurface { - surface: PackageProgramSurfaceIdV1::from_core(surface), - } - } - ProgramCompileError::DuplicateOccurrence { occurrence } => { - PackageProgramCompileErrorV1::DuplicateOccurrence { - occurrence: PackageProgramOccurrenceIdV1::from_core(occurrence), - } - } - ProgramCompileError::MissingPaintTarget { paint, target } => { - PackageProgramCompileErrorV1::MissingPaintTarget { - paint: PackageProgramPaintIdV1::from_core(paint), - target: PackageProgramTargetIdV1::from_core(target), - } - } - ProgramCompileError::MissingPaintSource { paint, source } => { - PackageProgramCompileErrorV1::MissingPaintSource { - paint: PackageProgramPaintIdV1::from_core(paint), - source: PackageProgramPaintIdV1::from_core(source), - } - } - ProgramCompileError::MissingPaintOpacityInput { paint, input } => { - PackageProgramCompileErrorV1::MissingPaintOpacityInput { - paint: PackageProgramPaintIdV1::from_core(paint), - input: PackageProgramOpacityInputIdV1::from_core(input), - } - } - ProgramCompileError::MissingSurfaceInputPort { surface, input } => { - PackageProgramCompileErrorV1::MissingSurfaceInputPort { - surface: PackageProgramSurfaceIdV1::from_core(surface), - input: PackageProgramSurfaceInputPortIdV1::from_core(input), - } - } - ProgramCompileError::MissingSurfaceOccurrence { - surface, - occurrence, - } => PackageProgramCompileErrorV1::MissingSurfaceOccurrence { - surface: PackageProgramSurfaceIdV1::from_core(surface), - occurrence: PackageProgramOccurrenceIdV1::from_core(occurrence), - }, - ProgramCompileError::MissingOccurrencePaint { occurrence, paint } => { - PackageProgramCompileErrorV1::MissingOccurrencePaint { - occurrence: PackageProgramOccurrenceIdV1::from_core(occurrence), - paint: PackageProgramPaintIdV1::from_core(paint), - } - } - ProgramCompileError::MissingOccurrenceBackdrop { - occurrence, - surface, - } => PackageProgramCompileErrorV1::MissingOccurrenceBackdrop { - occurrence: PackageProgramOccurrenceIdV1::from_core(occurrence), - surface: PackageProgramSurfaceIdV1::from_core(surface), - }, - ProgramCompileError::PaintCycle { paints } => { - PackageProgramCompileErrorV1::PaintCycle(PackageProgramPaintCycleV1 { paints }) - } - ProgramCompileError::RenderCycle { - surfaces, - occurrences, - } => PackageProgramCompileErrorV1::RenderCycle(PackageProgramRenderCycleV1 { - surfaces, - occurrences, - }), - ProgramCompileError::OpacityOutOfDomain { input } => { - PackageProgramCompileErrorV1::OpacityOutOfDomain { - input: PackageProgramOpacityInputIdV1::from_core(input), - } - } - ProgramCompileError::EmptyTargetDomain { target } => { - PackageProgramCompileErrorV1::EmptyTargetDomain { - target: PackageProgramTargetIdV1::from_core(target), - } - } - ProgramCompileError::DuplicateTargetCandidate { target, candidate } => { - PackageProgramCompileErrorV1::DuplicateTargetCandidate { - target: PackageProgramTargetIdV1::from_core(target), - candidate: PackageProgramTargetCandidateIdV1::from_core(candidate), - } - } - ProgramCompileError::DuplicateTargetCandidateSignal { - target, - first, - duplicate, - signal, - } => PackageProgramCompileErrorV1::DuplicateTargetCandidateSignal { - target: PackageProgramTargetIdV1::from_core(target), - first: PackageProgramTargetCandidateIdV1::from_core(first), - duplicate: PackageProgramTargetCandidateIdV1::from_core(duplicate), - encoded_srgb8: signal.srgb8(), - }, - ProgramCompileError::UnconstrainedTarget { target } => { - PackageProgramCompileErrorV1::UnconstrainedTarget { - target: PackageProgramTargetIdV1::from_core(target), - } - } - ProgramCompileError::DisconnectedFiniteTargets => { - PackageProgramCompileErrorV1::DisconnectedFiniteTargets - } - ProgramCompileError::UnassessedOutput { output, paint } => { - PackageProgramCompileErrorV1::UnassessedOutput { - output: PackageProgramOutputSlotIdV1::from_core(output), - paint: PackageProgramPaintIdV1::from_core(paint), - } - } - ProgramCompileError::MissingJointSelection => { - PackageProgramCompileErrorV1::MissingJointSelection - } - ProgramCompileError::JointSelectionWithoutTargets => { - PackageProgramCompileErrorV1::JointSelectionWithoutTargets - } - ProgramCompileError::JointStateDuplicateTarget { state, target } => { - PackageProgramCompileErrorV1::JointStateDuplicateTarget { - state, - target: PackageProgramTargetIdV1::from_core(target), - } - } - ProgramCompileError::JointStateMissingTarget { state, target } => { - PackageProgramCompileErrorV1::JointStateMissingTarget { - state, - target: PackageProgramTargetIdV1::from_core(target), - } - } - ProgramCompileError::JointStateUnknownTarget { state, target } => { - PackageProgramCompileErrorV1::JointStateUnknownTarget { - state, - target: PackageProgramTargetIdV1::from_core(target), - } - } - ProgramCompileError::JointStateUnknownCandidate { - state, - target, - candidate, - } => PackageProgramCompileErrorV1::JointStateUnknownCandidate { - state, - target: PackageProgramTargetIdV1::from_core(target), - candidate: PackageProgramTargetCandidateIdV1::from_core(candidate), - }, - ProgramCompileError::InvalidJointOrder(error) => { - PackageProgramCompileErrorV1::InvalidJointOrder(map_joint_order_error(error)) - } - ProgramCompileError::EmptyObservationGroup { .. } => { - PackageProgramCompileErrorV1::EmptySurfaceInputPortSet - } - ProgramCompileError::EmptyOccurrenceSet => PackageProgramCompileErrorV1::EmptyOccurrenceSet, - ProgramCompileError::EmptyConstraintSet => PackageProgramCompileErrorV1::EmptyConstraintSet, - ProgramCompileError::EmptyOutputSet => PackageProgramCompileErrorV1::EmptyOutputSet, - ProgramCompileError::DuplicateConstraint { constraint } => { - PackageProgramCompileErrorV1::DuplicateConstraint { - constraint: PackageProgramConstraintIdV1::from_core(constraint), - } - } - ProgramCompileError::MissingConstraintOccurrence { - constraint, - occurrence, - } => PackageProgramCompileErrorV1::MissingConstraintOccurrence { - constraint: PackageProgramConstraintIdV1::from_core(constraint), - occurrence: PackageProgramOccurrenceIdV1::from_core(occurrence), - }, - ProgramCompileError::DuplicateOutputSlot { output } => { - PackageProgramCompileErrorV1::DuplicateOutputSlot { - output: PackageProgramOutputSlotIdV1::from_core(output), - } - } - ProgramCompileError::MissingOutputPaint { output, paint } => { - PackageProgramCompileErrorV1::MissingOutputPaint { - output: PackageProgramOutputSlotIdV1::from_core(output), - paint: PackageProgramPaintIdV1::from_core(paint), - } - } - ProgramCompileError::ResourceExhausted => PackageProgramCompileErrorV1::ResourceExhausted, - ProgramCompileError::InternalInvariant => PackageProgramCompileErrorV1::InternalInvariant, - } -} -fn map_session_update_error( - error: SessionUpdateError, -) -> PackageProgramUpdateErrorV1 { - let kind = match error { - // A borrowed matching owner keeps the exact Rc generation alive for - // the whole transaction; expiry here is therefore an internal breach. - SessionUpdateError::OwnerExpired => PackageProgramUpdateErrorKindV1::InternalInvariant, - SessionUpdateError::Observation(error) => map_observation_error(error), - SessionUpdateError::Plan(error) => map_plan_error(error), - SessionUpdateError::EvidenceBindingInvariant => { - PackageProgramUpdateErrorKindV1::InternalInvariant - } - }; - PackageProgramUpdateErrorV1::new(kind) -} - -fn map_observation_error(error: ObservationError) -> PackageProgramUpdateErrorKindV1 { - match error { - ObservationError::EmptyScenarioSet - | ObservationError::DuplicateScenarioId { .. } - | ObservationError::SchemaOrderedValueCountMismatch { .. } => { - PackageProgramUpdateErrorKindV1::InvalidObservation - } - ObservationError::RevisionOutOfOrder { .. } => { - PackageProgramUpdateErrorKindV1::RevisionOutOfOrder - } - ObservationError::RevisionConflict { .. } => { - PackageProgramUpdateErrorKindV1::RevisionConflict - } - ObservationError::ResourceExhausted => PackageProgramUpdateErrorKindV1::ResourceExhausted, - ObservationError::EmptyCompiledSurfaceInputSchema - | ObservationError::DuplicateCompiledSurfaceInputPort { .. } - | ObservationError::StreamMismatch { .. } - | ObservationError::DuplicateSurfaceInputBinding { .. } - | ObservationError::MissingSurfaceInputBinding { .. } - | ObservationError::UnexpectedSurfaceInputBinding { .. } => { - PackageProgramUpdateErrorKindV1::InternalInvariant - } - } -} - -fn map_plan_error(error: CoreProgramPlanErrorV1) -> PackageProgramUpdateErrorKindV1 { - match error { - ProgramSessionEvaluationError::ResourceExhausted => { - PackageProgramUpdateErrorKindV1::ResourceExhausted - } - ProgramSessionEvaluationError::Evaluator { .. } => { - PackageProgramUpdateErrorKindV1::EvaluationFailed - } - ProgramSessionEvaluationError::ObservationSchemaMismatch(_) - | ProgramSessionEvaluationError::ProgramTargetBinding { .. } - | ProgramSessionEvaluationError::ModeledOccurrence { .. } - | ProgramSessionEvaluationError::OutputVariesAcrossCases { .. } - | ProgramSessionEvaluationError::FinalRecheckViolation { .. } - | ProgramSessionEvaluationError::InternalInvariant => { - PackageProgramUpdateErrorKindV1::InternalInvariant - } - } -} - -#[cfg(test)] -mod compile_error_projection_tests { - use super::*; - - #[test] - fn operation_scope_is_a_zero_sized_borrow_marker() { - assert_eq!( - core::mem::size_of::>(), - 0 - ); - } - - #[test] - fn nested_joint_resource_exhaustion_keeps_its_exact_reason_and_site_kind() { - let error = map_program_compile_error(ProgramCompileError::InvalidJointOrder( - FiniteJointOrderErrorV1::ResourceExhausted, - )); - - assert_eq!( - error.kind(), - PackageProgramCompileErrorKindV1::InvalidJointOrder - ); - assert_eq!( - error, - PackageProgramCompileErrorV1::InvalidJointOrder( - PackageProgramJointOrderErrorV1::ResourceExhausted - ) - ); - } -} diff --git a/crates/labcolors-core/src/program.rs b/crates/labcolors-core/src/program.rs new file mode 100644 index 00000000..1221d0e7 --- /dev/null +++ b/crates/labcolors-core/src/program.rs @@ -0,0 +1,4078 @@ +//! Публичный декларативный контракт компиляции и исполнения цветовой программы. +//! +//! Клиент один раз описывает физический граф через [`DraftV1`]: исходные +//! сигналы, решаемые цели, Paint, Surface, их [`OccurrenceIdV1`], ограничения +//! и выходы. Идентификаторы непрозрачны: Core не выводит из них семантику. +//! [`DraftV1::compile`] проверяет граф целиком и возвращает [`OwnerV1`] — +//! единственного владельца конкретной скомпилированной эпохи. +//! +//! [`OwnerV1::instantiate`] создаёт потоковую [`SessionV1`]. На горячем пути +//! [`OwnerV1::update`] принимает физические сценарии в каноническом порядке +//! входов и атомарно возвращает [`ProjectionV1`] без повторного решения в +//! адаптере. Исторические доказательства принадлежат Session, но только тот же +//! Owner разрешает обновления и операции. +//! +//! Состояния проецируются однозначно: +//! +//! | Состояние | Операции | +//! |---|---| +//! | `Waiting` | нет | +//! | `Ready` | `Set` для каждого выхода | +//! | `Stale` | `Hold` последнего доказанного результата | +//! | `Failed` с прошлым результатом | `Hold` | +//! | `Failed` без прошлого результата | `Remove` | +//! +//! [`CertificateV1::Verified`] хранит выбранное состояние, все клетки +//! доказательства и сертифицированные выходы. [`CertificateV1::Conflict`] +//! хранит исчерпывающий конфликт по всем рассмотренным состояниям. +//! [`ContentIdentityV1`] идентифицирует каноническое содержание, но не даёт +//! полномочий живого [`OwnerV1`]. + +use core::iter::FusedIterator; +use core::marker::PhantomData; +use core::slice; + +use crate::Srgb8; +use crate::appearance::{OccurrenceId, OpacityInputId, PaintId, SurfaceId, SurfaceInputPortId}; +use crate::composition::CompositionProfileV1; +use crate::constraints::{ + ApplicableWcag22EvaluationErrorV1, ExactSrgb8IdentityV1, ProgramVisiblePointBindingV1, + ProgramVisiblePointPassEvidence, ProgramVisiblePointViolationEvidence, Wcag22Srgb8V1, +}; +use crate::joint::FiniteJointOrderErrorV1; +use crate::lcs_occurrence::{ + AdaptingLuminanceCdM2, AdmittedSrgb8TristimulusBindingV1, AppearanceContextDomainErrorV1, + AppearanceContextFieldV1 as CoreAppearanceContextFieldV1, AppearanceContextId, + AppearanceContextSchemaReleaseId, BackgroundLuminanceRatio, ColorSignal, ColorSignalViewV1, + ColorimetricFrameId, ColorimetricFrameReleaseId, IEC_SRGB_D65_XYZ_FRAME_V1, + ModeledLcsOccurrenceFormationErrorV1, NumericDomainError, ObserverProfileId, + OccurrenceFormationError, ReferenceWhiteId, SurroundProfileId, + TristimulusComponentV1 as CoreTristimulusComponentV1, TristimulusDomainErrorV1, + TristimulusSample, TristimulusScale, +}; +use crate::numerics::NumericalDecisionEvidenceV1; +use crate::observation::{ + ObservationError, ObservationHeadViewV1, ObservationSchemaMismatchV1, ObservationStreamId, + Revision, ScenarioId, SchemaOrderedScenarioSourceV1, UnknownReasonId, +}; +use crate::program_session::{ + CompiledCoreProgramV1, CompositionProfile, ConstraintId, ConstraintInvocation, + CoreProgramConstraintInvocationV1, CoreProgramDraftErrorV1, CoreProgramDraftV1, + CoreProgramEvaluatorErrorV1, CoreProgramEvaluatorsV1, CoreProgramPassEvidenceV1, + CoreProgramViolationEvidenceV1, DeclaredJointSelectionV1, JointCandidateStateV1, Occurrence, + OpacityInput, OutputBinding, OutputSlotId, Paint, ProgramCompileError, ProgramConflictV1, + ProgramConstraintCellV1, ProgramConstraintResultV1, ProgramContentIdentityV1, ProgramOutputV1, + ProgramSessionEvaluationError, ProgramSessionInstantiateError, ProgramSessionPlan, + ProgramVerifiedV1, Source, SourceId, Surface, Target, TargetCandidateChoiceV1, + TargetCandidateId, TargetCandidateV1 as CoreTargetCandidateV1, TargetId, +}; +use crate::session::{Session, SessionState, SessionUpdateError}; +use crate::wcag22::{ + Wcag22ClientDeclaredNotApplicableV1, Wcag22CriterionV1, Wcag22EvaluationErrorV1, + Wcag22LuminanceBoundsQ55V1, Wcag22ProfileIdV1, wcag22_profile_v1, +}; + +type CoreVerifiedV1 = ProgramVerifiedV1; +type CoreConflictV1 = ProgramConflictV1; +type CoreProgramPlanV1 = ProgramSessionPlan; +type CoreProgramSessionV1 = Session; +type CoreProgramStateV1 = SessionState; +type CoreProgramPlanErrorV1 = ProgramSessionEvaluationError; +type CoreProgramConstraintCellV1 = ProgramConstraintCellV1; +type CoreExactPassEvidenceV1 = ProgramVisiblePointPassEvidence; +type CoreExactViolationEvidenceV1 = ProgramVisiblePointViolationEvidence; +type CoreWcag22PassEvidenceV1 = ProgramVisiblePointPassEvidence; +type CoreWcag22ViolationEvidenceV1 = ProgramVisiblePointViolationEvidence; + +macro_rules! authored_id { + ($doc:literal, $name:ident, $core:ty) => { + #[doc = $doc] + #[repr(transparent)] + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] + #[must_use] + pub struct $name($core); + + impl $name { + /// Создаёт непрозрачный идентификатор из клиентского числового ключа. + pub const fn new(value: u32) -> Self { + Self(<$core>::new(value)) + } + + /// Возвращает исходный клиентский числовой ключ. + pub const fn value(self) -> u32 { + self.0.value() + } + + const fn from_core(value: $core) -> Self { + Self(value) + } + + const fn into_core(self) -> $core { + self.0 + } + } + + impl core::hash::Hash for $name { + fn hash(&self, state: &mut H) { + core::hash::Hash::hash(&self.value(), state); + } + } + }; +} + +macro_rules! projected_id { + ($doc:literal, $name:ident, $core:ty) => { + #[doc = $doc] + #[repr(transparent)] + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] + #[must_use] + pub struct $name($core); + + impl $name { + const fn from_core(value: $core) -> Self { + Self(value) + } + + /// Возвращает числовой ключ сохранённой provenance. + pub const fn value(self) -> u32 { + self.0.value() + } + } + + impl core::hash::Hash for $name { + fn hash(&self, state: &mut H) { + core::hash::Hash::hash(&self.value(), state); + } + } + }; +} + +authored_id!( + "Идентификатор объявленного исходного сигнала.", + SourceIdV1, + SourceId +); +authored_id!("Идентификатор решаемой цели.", TargetIdV1, TargetId); +authored_id!( + "Идентификатор конечного кандидата одной цели.", + TargetCandidateIdV1, + TargetCandidateId +); +authored_id!( + "Идентификатор объявленного входа прозрачности.", + OpacityInputIdV1, + OpacityInputId +); +authored_id!("Идентификатор узла Paint.", PaintIdV1, PaintId); +authored_id!( + "Идентификатор входного порта динамической поверхности.", + SurfaceInputPortIdV1, + SurfaceInputPortId +); +authored_id!("Идентификатор узла Surface.", SurfaceIdV1, SurfaceId); +authored_id!( + "Идентификатор физического наложения Paint на Surface.", + OccurrenceIdV1, + OccurrenceId +); +authored_id!( + "Идентификатор проверяемого ограничения.", + ConstraintIdV1, + ConstraintId +); +authored_id!( + "Идентификатор клиентского выходного слота.", + OutputSlotIdV1, + OutputSlotId +); +projected_id!( + "Идентификатор потока, сохранённый в наблюдении.", + StreamIdV1, + ObservationStreamId +); +projected_id!( + "Идентификатор сценария, сохранённый как provenance.", + ScenarioIdV1, + ScenarioId +); + +/// Один физический кандидат конечной цели. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct TargetCandidateV1(CoreTargetCandidateV1); + +impl TargetCandidateV1 { + /// Связывает непрозрачный ID кандидата с конкретным encoded sRGB8 сигналом. + pub const fn new(id: TargetCandidateIdV1, source: Srgb8) -> Self { + Self(CoreTargetCandidateV1::from_srgb8(id.into_core(), source)) + } +} + +/// Выбор одного кандидата для одной цели в совместном состоянии. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct JointChoiceV1(TargetCandidateChoiceV1); + +impl JointChoiceV1 { + /// Создаёт типизированную пару `цель → кандидат`. + pub const fn new(target: TargetIdV1, candidate: TargetCandidateIdV1) -> Self { + Self(TargetCandidateChoiceV1::new( + target.into_core(), + candidate.into_core(), + )) + } +} + +/// Полное явно объявленное состояние всех конечных целей. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct JointStateV1(JointCandidateStateV1); + +impl JointStateV1 { + /// Создаёт состояние из одного выбора для каждой конечной цели. + pub fn new(choices: Vec) -> Self { + Self(JointCandidateStateV1::new( + choices.into_iter().map(|choice| choice.0).collect(), + )) + } +} + +/// Зарегистрированный режим окружения CIECAM16. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SurroundV1 { + /// Среднее освещение окружения. + Average, + /// Приглушённое освещение окружения. + Dim, + /// Тёмное окружение. + Dark, +} + +impl SurroundV1 { + const fn into_core(self) -> SurroundProfileId { + match self { + Self::Average => SurroundProfileId::AverageV1, + Self::Dim => SurroundProfileId::DimV1, + Self::Dark => SurroundProfileId::DarkV1, + } + } +} + +/// Поле входного контекста восприятия, не прошедшее admission. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AppearanceContextFieldV1 { + /// Адаптирующая яркость в кд/м². + AdaptingLuminanceCdM2, + /// Безразмерное отношение фоновой яркости `Y_b/Y_w`. + BackgroundLuminanceRatioYbYw, +} + +/// Числовая причина отказа при формировании контекста восприятия. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NumericDomainErrorV1 { + /// Значение не является конечным числом. + NonFinite, + /// Значение отрицательно. + Negative, + /// Значение должно быть строго положительным. + NotPositive, + /// Значение превышает единицу. + AboveOne, + /// Угол оттенка находится вне полуинтервала `[0°, 360°)`. + HueOutOfRange, +} + +/// Закрытая классификация отказа admission контекста восприятия. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AppearanceContextErrorKindV1 { + /// Клиентское значение находится вне объявленного домена. + Domain, + /// Нарушен внутренний инвариант закрытого преобразования. + InternalInvariant, +} + +/// Типизированный отказ admission контекста восприятия. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct AppearanceContextErrorV1 { + kind: AppearanceContextErrorKindV1, + field: Option, + reason: Option, +} + +impl AppearanceContextErrorV1 { + /// Возвращает класс отказа. + pub const fn kind(self) -> AppearanceContextErrorKindV1 { + self.kind + } + + /// Возвращает отвергнутое поле, если отказ относится к входному домену. + pub const fn field(self) -> Option { + self.field + } + + /// Возвращает точную числовую причину, если отказ относится к входному домену. + pub const fn reason(self) -> Option { + self.reason + } + + fn from_core(error: AppearanceContextDomainErrorV1) -> Self { + let field = match error.field() { + CoreAppearanceContextFieldV1::AdaptingLuminanceCdM2 => { + AppearanceContextFieldV1::AdaptingLuminanceCdM2 + } + CoreAppearanceContextFieldV1::BackgroundLuminanceRatio => { + AppearanceContextFieldV1::BackgroundLuminanceRatioYbYw + } + }; + let reason = match error.reason() { + NumericDomainError::NonFinite => Some(NumericDomainErrorV1::NonFinite), + NumericDomainError::Negative => Some(NumericDomainErrorV1::Negative), + NumericDomainError::NotPositive => Some(NumericDomainErrorV1::NotPositive), + NumericDomainError::AboveOne => Some(NumericDomainErrorV1::AboveOne), + NumericDomainError::HueOutOfRange => None, + }; + match reason { + Some(reason) => Self { + kind: AppearanceContextErrorKindV1::Domain, + field: Some(field), + reason: Some(reason), + }, + None => Self { + kind: AppearanceContextErrorKindV1::InternalInvariant, + field: None, + reason: None, + }, + } + } +} + +/// Неизменяемый допущенный контекст восприятия. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct AppearanceContextV1(AppearanceContextId); + +impl AppearanceContextV1 { + const fn from_core(context: AppearanceContextId) -> Self { + Self(context) + } + + /// Допускает явные входы CIECAM16 для encoded sRGB8/D65. + /// + /// `background_luminance_ratio_yb_yw` — безразмерное `Y_b/Y_w` в `(0, 1]`, + /// а не абсолютная яркость. + pub fn try_new( + adapting_luminance_cd_m2: f64, + background_luminance_ratio_yb_yw: f64, + surround: SurroundV1, + ) -> Result { + let adapting_luminance_cd_m2 = AdaptingLuminanceCdM2::try_new(adapting_luminance_cd_m2) + .map_err(AppearanceContextErrorV1::from_core)?; + let background_luminance_ratio = + BackgroundLuminanceRatio::try_new(background_luminance_ratio_yb_yw) + .map_err(AppearanceContextErrorV1::from_core)?; + Ok(Self(AppearanceContextId::from_inputs( + AppearanceContextSchemaReleaseId::Ciecam16ViewingInputsV1, + IEC_SRGB_D65_XYZ_FRAME_V1, + adapting_luminance_cd_m2, + background_luminance_ratio, + surround.into_core(), + ))) + } + + /// Возвращает допущенную адаптирующую яркость в кд/м². + pub fn adapting_luminance_cd_m2(self) -> f64 { + self.0.adapting_luminance_cd_m2() + } + + /// Возвращает допущенное безразмерное отношение `Y_b/Y_w`. + pub fn background_luminance_ratio_yb_yw(self) -> f64 { + self.0.background_luminance_ratio() + } + + /// Возвращает зарегистрированный режим окружения. + pub const fn surround(self) -> SurroundV1 { + match self.0.surround_profile() { + SurroundProfileId::AverageV1 => SurroundV1::Average, + SurroundProfileId::DimV1 => SurroundV1::Dim, + SurroundProfileId::DarkV1 => SurroundV1::Dark, + } + } +} + +/// Закрытая классификация ошибки компиляции. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CompileErrorKindV1 { + /// Повторно объявлен исходный сигнал. + DuplicateSource, + /// Повторно объявлена цель. + DuplicateTarget, + /// В одной цели повторно объявлен ID кандидата. + DuplicateTargetCandidate, + /// Два кандидата одной цели задают одинаковый сигнал. + DuplicateTargetCandidateSignal, + /// Повторно объявлен вход прозрачности. + DuplicateOpacityInput, + /// Повторно объявлен входной порт поверхности. + DuplicateSurfaceInputPort, + /// Объявленный входной порт поверхности не используется. + UnusedSurfaceInputPort, + /// Один входной порт привязан к нескольким Surface. + DuplicateSurfaceInputBinding, + /// Повторно объявлен Paint. + DuplicatePaint, + /// Повторно объявлен Surface. + DuplicateSurface, + /// Повторно объявлен Occurrence. + DuplicateOccurrence, + /// Повторно объявлено ограничение. + DuplicateConstraint, + /// Повторно объявлен выходной слот. + DuplicateOutputSlot, + /// Цель ссылается на отсутствующий исходный сигнал. + MissingTargetSource, + /// Paint ссылается на отсутствующую цель. + MissingPaintTarget, + /// Paint ссылается на отсутствующий Paint. + MissingPaintSource, + /// Paint ссылается на отсутствующий вход прозрачности. + MissingPaintOpacityInput, + /// Surface ссылается на отсутствующий входной порт. + MissingSurfaceInputPort, + /// Surface ссылается на отсутствующий Occurrence. + MissingSurfaceOccurrence, + /// Occurrence ссылается на отсутствующий Paint. + MissingOccurrencePaint, + /// Occurrence ссылается на отсутствующий backdrop Surface. + MissingOccurrenceBackdrop, + /// Ограничение ссылается на отсутствующий Occurrence. + MissingConstraintOccurrence, + /// Выход ссылается на отсутствующий Paint. + MissingOutputPaint, + /// Граф Paint содержит цикл. + PaintCycle, + /// Граф рендера содержит цикл Surface/Occurrence. + RenderCycle, + /// Значение прозрачности находится вне `[0, 1]` или не конечно. + OpacityOutOfDomain, + /// Конечная цель не содержит кандидатов. + EmptyTargetDomain, + /// Конечная цель не участвует ни в одном ограничении. + UnconstrainedTarget, + /// Конечные цели образуют несвязанные компоненты. + DisconnectedFiniteTargets, + /// Выходной Paint не покрыт ни одним ограничением. + UnassessedOutput, + /// Для конечных целей не объявлен совместный порядок. + MissingJointSelection, + /// Совместный порядок объявлен без конечных целей. + JointSelectionWithoutTargets, + /// Состояние повторяет одну цель. + JointStateDuplicateTarget, + /// В состоянии отсутствует конечная цель. + JointStateMissingTarget, + /// Состояние ссылается на неизвестную цель. + JointStateUnknownTarget, + /// Состояние ссылается на неизвестного кандидата. + JointStateUnknownCandidate, + /// Совместный порядок не является полным конечным порядком. + InvalidJointOrder, + /// Не объявлено ни одного динамического входа поверхности. + EmptySurfaceInputPortSet, + /// Не объявлено ни одного физического Occurrence. + EmptyOccurrenceSet, + /// Не объявлено ни одного ограничения. + EmptyConstraintSet, + /// Не объявлено ни одного выхода. + EmptyOutputSet, + /// Для компиляции недостаточно памяти или адресного пространства. + ResourceExhausted, + /// Нарушен внутренний инвариант закрытого компилятора. + InternalInvariant, +} + +/// Типизированный ID узла, к которому относится ошибка компиляции. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CompileErrorHandleV1 { + /// Исходный сигнал. + Source(SourceIdV1), + /// Цель. + Target(TargetIdV1), + /// Кандидат цели. + TargetCandidate(TargetCandidateIdV1), + /// Вход прозрачности. + OpacityInput(OpacityInputIdV1), + /// Paint. + Paint(PaintIdV1), + /// Входной порт Surface. + SurfaceInputPort(SurfaceInputPortIdV1), + /// Surface. + Surface(SurfaceIdV1), + /// Occurrence. + Occurrence(OccurrenceIdV1), + /// Ограничение. + Constraint(ConstraintIdV1), + /// Выходной слот. + OutputSlot(OutputSlotIdV1), +} + +impl CompileErrorHandleV1 { + /// Возвращает клиентский числовой ключ независимо от пространства ID. + pub const fn value(self) -> u32 { + match self { + Self::Source(value) => value.value(), + Self::Target(value) => value.value(), + Self::TargetCandidate(value) => value.value(), + Self::OpacityInput(value) => value.value(), + Self::Paint(value) => value.value(), + Self::SurfaceInputPort(value) => value.value(), + Self::Surface(value) => value.value(), + Self::Occurrence(value) => value.value(), + Self::Constraint(value) => value.value(), + Self::OutputSlot(value) => value.value(), + } + } +} + +/// Точные участники одного цикла зависимостей Paint. +#[derive(Debug, PartialEq, Eq)] +pub struct PaintCycleV1 { + paints: Vec, +} + +impl PaintCycleV1 { + /// Возвращает участников цикла в каноническом порядке диагностики. + pub fn paints(&self) -> impl ExactSizeIterator + '_ { + self.paints.iter().copied().map(PaintIdV1::from_core) + } +} + +/// Точные участники одного цикла рендера. +#[derive(Debug, PartialEq, Eq)] +pub struct RenderCycleV1 { + surfaces: Vec, + occurrences: Vec, +} + +impl RenderCycleV1 { + /// Возвращает Surface-участников цикла. + pub fn surfaces(&self) -> impl ExactSizeIterator + '_ { + self.surfaces.iter().copied().map(SurfaceIdV1::from_core) + } + + /// Возвращает Occurrence-участников цикла. + pub fn occurrences(&self) -> impl ExactSizeIterator + '_ { + self.occurrences + .iter() + .copied() + .map(OccurrenceIdV1::from_core) + } +} + +/// Точная причина отказа явно объявленного конечного совместного порядка. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum JointOrderErrorV1 { + /// Одно измерение не содержит кандидатов. + EmptyDomain { + /// Индекс пустого измерения. + dimension: usize, + }, + /// Декартова мощность измерений не представима в `usize`. + CardinalityOverflow, + /// Явный порядок не содержит состояний. + EmptyOrder, + /// Состояние содержит неверное число измерений. + TupleArity { + /// Индекс состояния. + state: usize, + /// Требуемое число измерений. + expected: usize, + /// Фактическое число измерений. + actual: usize, + }, + /// Ординал кандидата выходит за домен измерения. + OrdinalOutOfDomain { + /// Индекс состояния. + state: usize, + /// Индекс измерения. + dimension: usize, + /// Некорректный ординал. + ordinal: usize, + /// Мощность домена измерения. + domain_len: usize, + }, + /// Два состояния задают один и тот же кортеж. + DuplicateTuple { + /// Индекс первого состояния. + first_state: usize, + /// Индекс повторного состояния. + duplicate_state: usize, + }, + /// Порядок не покрывает полный декартов домен. + IncompleteOrder { + /// Требуемое число состояний. + expected: usize, + /// Фактическое число состояний. + actual: usize, + }, + /// Для проверки порядка недостаточно ресурсов. + ResourceExhausted, +} + +/// Атомарная и полная ошибка компиляции объявленной программы. +/// +/// Enum авторитетен; [`Self::kind`], [`Self::primary_handle`] и +/// [`Self::related_handle`] — только удобные проекции полного payload. +#[derive(Debug, PartialEq, Eq)] +pub enum CompileErrorV1 { + /// Повторно объявлен исходный сигнал. + DuplicateSource { + /// Повторный ID. + source: SourceIdV1, + }, + /// Повторно объявлена цель. + DuplicateTarget { + /// Повторный ID. + target: TargetIdV1, + }, + /// Цель ссылается на отсутствующий исходный сигнал. + MissingTargetSource { + /// Ошибочная цель. + target: TargetIdV1, + /// Отсутствующий исходный сигнал. + source: SourceIdV1, + }, + /// Повторно объявлен вход прозрачности. + DuplicateOpacityInput { + /// Повторный ID. + input: OpacityInputIdV1, + }, + /// Повторно объявлен входной порт поверхности. + DuplicateSurfaceInputPort { + /// Повторный ID. + input: SurfaceInputPortIdV1, + }, + /// Объявленный входной порт не используется. + UnusedSurfaceInputPort { + /// Неиспользуемый ID. + input: SurfaceInputPortIdV1, + }, + /// Один входной порт привязан к двум Surface. + DuplicateSurfaceInputBinding { + /// Повторно привязанный порт. + input: SurfaceInputPortIdV1, + /// Первая Surface. + first: SurfaceIdV1, + /// Повторная Surface. + duplicate: SurfaceIdV1, + }, + /// Повторно объявлен Paint. + DuplicatePaint { + /// Повторный ID. + paint: PaintIdV1, + }, + /// Повторно объявлен Surface. + DuplicateSurface { + /// Повторный ID. + surface: SurfaceIdV1, + }, + /// Повторно объявлен Occurrence. + DuplicateOccurrence { + /// Повторный ID. + occurrence: OccurrenceIdV1, + }, + /// Paint ссылается на отсутствующую цель. + MissingPaintTarget { + /// Ошибочный Paint. + paint: PaintIdV1, + /// Отсутствующая цель. + target: TargetIdV1, + }, + /// Paint ссылается на отсутствующий исходный Paint. + MissingPaintSource { + /// Ошибочный Paint. + paint: PaintIdV1, + /// Отсутствующий исходный Paint. + source: PaintIdV1, + }, + /// Paint ссылается на отсутствующий вход прозрачности. + MissingPaintOpacityInput { + /// Ошибочный Paint. + paint: PaintIdV1, + /// Отсутствующий вход. + input: OpacityInputIdV1, + }, + /// Surface ссылается на отсутствующий входной порт. + MissingSurfaceInputPort { + /// Ошибочная Surface. + surface: SurfaceIdV1, + /// Отсутствующий порт. + input: SurfaceInputPortIdV1, + }, + /// Surface ссылается на отсутствующий Occurrence. + MissingSurfaceOccurrence { + /// Ошибочная Surface. + surface: SurfaceIdV1, + /// Отсутствующий Occurrence. + occurrence: OccurrenceIdV1, + }, + /// Occurrence ссылается на отсутствующий Paint. + MissingOccurrencePaint { + /// Ошибочный Occurrence. + occurrence: OccurrenceIdV1, + /// Отсутствующий Paint. + paint: PaintIdV1, + }, + /// Occurrence ссылается на отсутствующий backdrop Surface. + MissingOccurrenceBackdrop { + /// Ошибочный Occurrence. + occurrence: OccurrenceIdV1, + /// Отсутствующая Surface. + surface: SurfaceIdV1, + }, + /// Обнаружен цикл зависимостей Paint. + PaintCycle(PaintCycleV1), + /// Обнаружен цикл Surface/Occurrence. + RenderCycle(RenderCycleV1), + /// Вход прозрачности не является конечным числом в `[0, 1]`. + OpacityOutOfDomain { + /// Ошибочный вход. + input: OpacityInputIdV1, + }, + /// Конечная цель не содержит кандидатов. + EmptyTargetDomain { + /// Пустая цель. + target: TargetIdV1, + }, + /// В одной цели повторно объявлен ID кандидата. + DuplicateTargetCandidate { + /// Цель кандидата. + target: TargetIdV1, + /// Повторный кандидат. + candidate: TargetCandidateIdV1, + }, + /// Два кандидата одной цели имеют одинаковый физический сигнал. + DuplicateTargetCandidateSignal { + /// Цель кандидатов. + target: TargetIdV1, + /// Первый кандидат. + first: TargetCandidateIdV1, + /// Повторный кандидат. + duplicate: TargetCandidateIdV1, + /// Совпавший encoded sRGB8 сигнал. + encoded_srgb8: Srgb8, + }, + /// Конечная цель не участвует ни в одном ограничении. + UnconstrainedTarget { + /// Неограниченная цель. + target: TargetIdV1, + }, + /// Конечные цели образуют несвязанные компоненты. + DisconnectedFiniteTargets, + /// Выходной Paint не покрыт ни одним ограничением. + UnassessedOutput { + /// Непроверенный выход. + output: OutputSlotIdV1, + /// Его Paint. + paint: PaintIdV1, + }, + /// Для конечных целей не объявлен совместный порядок. + MissingJointSelection, + /// Совместный порядок объявлен без конечных целей. + JointSelectionWithoutTargets, + /// Состояние повторяет одну цель. + JointStateDuplicateTarget { + /// Индекс состояния. + state: usize, + /// Повторная цель. + target: TargetIdV1, + }, + /// В состоянии отсутствует конечная цель. + JointStateMissingTarget { + /// Индекс состояния. + state: usize, + /// Отсутствующая цель. + target: TargetIdV1, + }, + /// Состояние ссылается на неизвестную цель. + JointStateUnknownTarget { + /// Индекс состояния. + state: usize, + /// Неизвестная цель. + target: TargetIdV1, + }, + /// Состояние ссылается на неизвестного кандидата. + JointStateUnknownCandidate { + /// Индекс состояния. + state: usize, + /// Цель кандидата. + target: TargetIdV1, + /// Неизвестный кандидат. + candidate: TargetCandidateIdV1, + }, + /// Явный совместный порядок не является полным конечным порядком. + InvalidJointOrder(JointOrderErrorV1), + /// Не объявлено ни одного динамического входа поверхности. + EmptySurfaceInputPortSet, + /// Не объявлено ни одного физического Occurrence. + EmptyOccurrenceSet, + /// Не объявлено ни одного ограничения. + EmptyConstraintSet, + /// Не объявлено ни одного выхода. + EmptyOutputSet, + /// Повторно объявлено ограничение. + DuplicateConstraint { + /// Повторный ID. + constraint: ConstraintIdV1, + }, + /// Ограничение ссылается на отсутствующий Occurrence. + MissingConstraintOccurrence { + /// Ошибочное ограничение. + constraint: ConstraintIdV1, + /// Отсутствующий Occurrence. + occurrence: OccurrenceIdV1, + }, + /// Повторно объявлен выходной слот. + DuplicateOutputSlot { + /// Повторный ID. + output: OutputSlotIdV1, + }, + /// Выход ссылается на отсутствующий Paint. + MissingOutputPaint { + /// Ошибочный выход. + output: OutputSlotIdV1, + /// Отсутствующий Paint. + paint: PaintIdV1, + }, + /// Для компиляции недостаточно ресурсов. + ResourceExhausted, + /// Нарушен внутренний инвариант закрытого компилятора. + InternalInvariant, +} + +impl CompileErrorV1 { + /// Возвращает стабильный класс ошибки без потери полного payload. + pub const fn kind(&self) -> CompileErrorKindV1 { + use CompileErrorKindV1 as Kind; + + match self { + Self::DuplicateSource { .. } => Kind::DuplicateSource, + Self::DuplicateTarget { .. } => Kind::DuplicateTarget, + Self::MissingTargetSource { .. } => Kind::MissingTargetSource, + Self::DuplicateOpacityInput { .. } => Kind::DuplicateOpacityInput, + Self::DuplicateSurfaceInputPort { .. } => Kind::DuplicateSurfaceInputPort, + Self::UnusedSurfaceInputPort { .. } => Kind::UnusedSurfaceInputPort, + Self::DuplicateSurfaceInputBinding { .. } => Kind::DuplicateSurfaceInputBinding, + Self::DuplicatePaint { .. } => Kind::DuplicatePaint, + Self::DuplicateSurface { .. } => Kind::DuplicateSurface, + Self::DuplicateOccurrence { .. } => Kind::DuplicateOccurrence, + Self::MissingPaintTarget { .. } => Kind::MissingPaintTarget, + Self::MissingPaintSource { .. } => Kind::MissingPaintSource, + Self::MissingPaintOpacityInput { .. } => Kind::MissingPaintOpacityInput, + Self::MissingSurfaceInputPort { .. } => Kind::MissingSurfaceInputPort, + Self::MissingSurfaceOccurrence { .. } => Kind::MissingSurfaceOccurrence, + Self::MissingOccurrencePaint { .. } => Kind::MissingOccurrencePaint, + Self::MissingOccurrenceBackdrop { .. } => Kind::MissingOccurrenceBackdrop, + Self::PaintCycle(_) => Kind::PaintCycle, + Self::RenderCycle(_) => Kind::RenderCycle, + Self::OpacityOutOfDomain { .. } => Kind::OpacityOutOfDomain, + Self::EmptyTargetDomain { .. } => Kind::EmptyTargetDomain, + Self::DuplicateTargetCandidate { .. } => Kind::DuplicateTargetCandidate, + Self::DuplicateTargetCandidateSignal { .. } => Kind::DuplicateTargetCandidateSignal, + Self::UnconstrainedTarget { .. } => Kind::UnconstrainedTarget, + Self::DisconnectedFiniteTargets => Kind::DisconnectedFiniteTargets, + Self::UnassessedOutput { .. } => Kind::UnassessedOutput, + Self::MissingJointSelection => Kind::MissingJointSelection, + Self::JointSelectionWithoutTargets => Kind::JointSelectionWithoutTargets, + Self::JointStateDuplicateTarget { .. } => Kind::JointStateDuplicateTarget, + Self::JointStateMissingTarget { .. } => Kind::JointStateMissingTarget, + Self::JointStateUnknownTarget { .. } => Kind::JointStateUnknownTarget, + Self::JointStateUnknownCandidate { .. } => Kind::JointStateUnknownCandidate, + Self::InvalidJointOrder(_) => Kind::InvalidJointOrder, + Self::EmptySurfaceInputPortSet => Kind::EmptySurfaceInputPortSet, + Self::EmptyOccurrenceSet => Kind::EmptyOccurrenceSet, + Self::EmptyConstraintSet => Kind::EmptyConstraintSet, + Self::EmptyOutputSet => Kind::EmptyOutputSet, + Self::DuplicateConstraint { .. } => Kind::DuplicateConstraint, + Self::MissingConstraintOccurrence { .. } => Kind::MissingConstraintOccurrence, + Self::DuplicateOutputSlot { .. } => Kind::DuplicateOutputSlot, + Self::MissingOutputPaint { .. } => Kind::MissingOutputPaint, + Self::ResourceExhausted => Kind::ResourceExhausted, + Self::InternalInvariant => Kind::InternalInvariant, + } + } + + /// Возвращает основной типизированный ID, если ошибка локализуема одним узлом. + pub const fn primary_handle(&self) -> Option { + use CompileErrorHandleV1 as Handle; + + match self { + Self::DuplicateSource { source } => Some(Handle::Source(*source)), + Self::DuplicateTarget { target } + | Self::EmptyTargetDomain { target } + | Self::UnconstrainedTarget { target } + | Self::JointStateDuplicateTarget { target, .. } + | Self::JointStateMissingTarget { target, .. } + | Self::JointStateUnknownTarget { target, .. } + | Self::JointStateUnknownCandidate { target, .. } => Some(Handle::Target(*target)), + Self::MissingTargetSource { target, .. } + | Self::DuplicateTargetCandidate { target, .. } + | Self::DuplicateTargetCandidateSignal { target, .. } => Some(Handle::Target(*target)), + Self::DuplicateOpacityInput { input } | Self::OpacityOutOfDomain { input } => { + Some(Handle::OpacityInput(*input)) + } + Self::DuplicateSurfaceInputPort { input } + | Self::UnusedSurfaceInputPort { input } + | Self::DuplicateSurfaceInputBinding { input, .. } => { + Some(Handle::SurfaceInputPort(*input)) + } + Self::DuplicatePaint { paint } + | Self::MissingPaintTarget { paint, .. } + | Self::MissingPaintSource { paint, .. } + | Self::MissingPaintOpacityInput { paint, .. } => Some(Handle::Paint(*paint)), + Self::DuplicateSurface { surface } + | Self::MissingSurfaceInputPort { surface, .. } + | Self::MissingSurfaceOccurrence { surface, .. } => Some(Handle::Surface(*surface)), + Self::DuplicateOccurrence { occurrence } + | Self::MissingOccurrencePaint { occurrence, .. } + | Self::MissingOccurrenceBackdrop { occurrence, .. } => { + Some(Handle::Occurrence(*occurrence)) + } + Self::UnassessedOutput { output, .. } + | Self::DuplicateOutputSlot { output } + | Self::MissingOutputPaint { output, .. } => Some(Handle::OutputSlot(*output)), + Self::DuplicateConstraint { constraint } + | Self::MissingConstraintOccurrence { constraint, .. } => { + Some(Handle::Constraint(*constraint)) + } + Self::PaintCycle(_) + | Self::RenderCycle(_) + | Self::DisconnectedFiniteTargets + | Self::MissingJointSelection + | Self::JointSelectionWithoutTargets + | Self::InvalidJointOrder(_) + | Self::EmptySurfaceInputPortSet + | Self::EmptyOccurrenceSet + | Self::EmptyConstraintSet + | Self::EmptyOutputSet + | Self::ResourceExhausted + | Self::InternalInvariant => None, + } + } + + /// Возвращает связанный типизированный ID для ошибки отношения двух узлов. + pub const fn related_handle(&self) -> Option { + use CompileErrorHandleV1 as Handle; + + match self { + Self::MissingTargetSource { source, .. } => Some(Handle::Source(*source)), + Self::DuplicateSurfaceInputBinding { duplicate, .. } => { + Some(Handle::Surface(*duplicate)) + } + Self::MissingPaintTarget { target, .. } => Some(Handle::Target(*target)), + Self::MissingPaintSource { source, .. } => Some(Handle::Paint(*source)), + Self::MissingPaintOpacityInput { input, .. } => Some(Handle::OpacityInput(*input)), + Self::MissingSurfaceInputPort { input, .. } => Some(Handle::SurfaceInputPort(*input)), + Self::MissingSurfaceOccurrence { occurrence, .. } + | Self::MissingConstraintOccurrence { occurrence, .. } => { + Some(Handle::Occurrence(*occurrence)) + } + Self::MissingOccurrencePaint { paint, .. } + | Self::UnassessedOutput { paint, .. } + | Self::MissingOutputPaint { paint, .. } => Some(Handle::Paint(*paint)), + Self::MissingOccurrenceBackdrop { surface, .. } => Some(Handle::Surface(*surface)), + Self::DuplicateTargetCandidate { candidate, .. } + | Self::DuplicateTargetCandidateSignal { + duplicate: candidate, + .. + } + | Self::JointStateUnknownCandidate { candidate, .. } => { + Some(Handle::TargetCandidate(*candidate)) + } + Self::DuplicateSource { .. } + | Self::DuplicateTarget { .. } + | Self::DuplicateOpacityInput { .. } + | Self::DuplicateSurfaceInputPort { .. } + | Self::UnusedSurfaceInputPort { .. } + | Self::DuplicatePaint { .. } + | Self::DuplicateSurface { .. } + | Self::DuplicateOccurrence { .. } + | Self::PaintCycle(_) + | Self::RenderCycle(_) + | Self::OpacityOutOfDomain { .. } + | Self::EmptyTargetDomain { .. } + | Self::UnconstrainedTarget { .. } + | Self::DisconnectedFiniteTargets + | Self::MissingJointSelection + | Self::JointSelectionWithoutTargets + | Self::JointStateDuplicateTarget { .. } + | Self::JointStateMissingTarget { .. } + | Self::JointStateUnknownTarget { .. } + | Self::InvalidJointOrder(_) + | Self::EmptySurfaceInputPortSet + | Self::EmptyOccurrenceSet + | Self::EmptyConstraintSet + | Self::EmptyOutputSet + | Self::DuplicateConstraint { .. } + | Self::DuplicateOutputSlot { .. } + | Self::ResourceExhausted + | Self::InternalInvariant => None, + } + } +} + +/// Холодный декларативный builder канонической Program IR. +#[must_use] +pub struct DraftV1 { + inner: CoreProgramDraftV1, +} + +/// Ошибка изменения Draft до компиляции. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DraftErrorV1 { + /// Совместный порядок уже объявлен и не может быть молча заменён. + JointSelectionAlreadyDeclared, +} + +impl DraftV1 { + /// Создаёт пустой Draft. + pub fn new() -> Self { + Self { + inner: CoreProgramDraftV1::new(), + } + } + + /// Объявляет неизменяемый исходный encoded sRGB8 сигнал. + pub fn push_source(&mut self, id: SourceIdV1, source: Srgb8) -> &mut Self { + self.inner + .push_source(Source::new(id.into_core(), ColorSignal::from_srgb8(source))); + self + } + + /// Объявляет цель, физически равную исходному сигналу. + pub fn push_fixed_target(&mut self, id: TargetIdV1, source: SourceIdV1) -> &mut Self { + self.inner + .push_target(Target::fixed(id.into_core(), source.into_core())); + self + } + + /// Объявляет решаемую цель с конечным набором физических кандидатов. + pub fn push_finite_target( + &mut self, + id: TargetIdV1, + source: SourceIdV1, + candidates: Vec, + ) -> &mut Self { + self.inner.push_target(Target::finite( + id.into_core(), + source.into_core(), + candidates + .into_iter() + .map(|candidate| candidate.0) + .collect(), + )); + self + } + + /// Один раз задаёт полный порядок совместных состояний конечных целей. + pub fn set_joint_selection( + &mut self, + states: Vec, + ) -> Result<&mut Self, DraftErrorV1> { + self.inner + .set_joint_selection(DeclaredJointSelectionV1::new( + states.into_iter().map(|state| state.0).collect(), + )) + .map_err(|error| match error { + CoreProgramDraftErrorV1::JointSelectionAlreadyDeclared => { + DraftErrorV1::JointSelectionAlreadyDeclared + } + })?; + Ok(self) + } + + /// Объявляет один динамический вход поверхности. + pub fn push_surface_input_port(&mut self, input: SurfaceInputPortIdV1) -> &mut Self { + self.inner.push_surface_input_port(input.into_core()); + self + } + + /// Объявляет числовой вход прозрачности; домен проверяется при компиляции. + pub fn push_opacity_input(&mut self, id: OpacityInputIdV1, value: f64) -> &mut Self { + self.inner + .push_opacity_input(OpacityInput::new(id.into_core(), value)); + self + } + + /// Объявляет непрозрачный Paint, связанный с целью. + pub fn push_solid_paint(&mut self, id: PaintIdV1, target: TargetIdV1) -> &mut Self { + self.inner.push_paint(Paint::Solid { + id: id.into_core(), + target: target.into_core(), + }); + self + } + + /// Объявляет Paint как прозрачную версию другого Paint. + pub fn push_opacity_paint( + &mut self, + id: PaintIdV1, + source: PaintIdV1, + opacity: OpacityInputIdV1, + ) -> &mut Self { + self.inner.push_paint(Paint::Opacity { + id: id.into_core(), + source: source.into_core(), + opacity: opacity.into_core(), + }); + self + } + + /// Объявляет Surface, значение которой поступает из runtime-сценария. + pub fn push_input_surface( + &mut self, + id: SurfaceIdV1, + input: SurfaceInputPortIdV1, + ) -> &mut Self { + self.inner.push_surface(Surface::Input { + id: id.into_core(), + input: input.into_core(), + }); + self + } + + /// Объявляет Surface как видимый результат другого Occurrence. + pub fn push_occurrence_surface( + &mut self, + id: SurfaceIdV1, + occurrence: OccurrenceIdV1, + ) -> &mut Self { + self.inner.push_surface(Surface::FromOccurrence { + id: id.into_core(), + occurrence: occurrence.into_core(), + }); + self + } + + /// Объявляет encoded-sRGB8 source-over Occurrence в явном контексте. + pub fn push_source_over_occurrence( + &mut self, + id: OccurrenceIdV1, + subject: PaintIdV1, + against: SurfaceIdV1, + context: AppearanceContextV1, + ) -> &mut Self { + self.inner.push_occurrence(Occurrence::new( + id.into_core(), + subject.into_core(), + against.into_core(), + CompositionProfile::EncodedSrgb8SourceOverV1, + context.0, + )); + self + } + + /// Добавляет обязательное точное сравнение видимого sRGB8 результата. + pub fn push_exact_hard( + &mut self, + id: ConstraintIdV1, + occurrence: OccurrenceIdV1, + expected: Srgb8, + ) -> &mut Self { + self.inner.push_hard_constraint(ConstraintInvocation::hard( + id.into_core(), + occurrence.into_core(), + CoreProgramConstraintInvocationV1::ExactSrgb8(expected), + )); + self + } + + /// Добавляет диагностическое точное сравнение, не влияющее на выбор. + pub fn push_exact_report_only( + &mut self, + id: ConstraintIdV1, + occurrence: OccurrenceIdV1, + expected: Srgb8, + ) -> &mut Self { + self.inner + .push_report_constraint(ConstraintInvocation::report_only( + id.into_core(), + occurrence.into_core(), + CoreProgramConstraintInvocationV1::ExactSrgb8(expected), + )); + self + } + + /// Добавляет обязательный критерий WCAG 2.2 для видимого результата. + pub fn push_wcag22_hard( + &mut self, + id: ConstraintIdV1, + occurrence: OccurrenceIdV1, + criterion: Wcag22CriterionV1, + ) -> &mut Self { + self.inner.push_hard_constraint(ConstraintInvocation::hard( + id.into_core(), + occurrence.into_core(), + CoreProgramConstraintInvocationV1::Wcag22Srgb8(criterion), + )); + self + } + + /// Добавляет диагностический критерий WCAG 2.2, не влияющий на выбор. + pub fn push_wcag22_report_only( + &mut self, + id: ConstraintIdV1, + occurrence: OccurrenceIdV1, + criterion: Wcag22CriterionV1, + ) -> &mut Self { + self.inner + .push_report_constraint(ConstraintInvocation::report_only( + id.into_core(), + occurrence.into_core(), + CoreProgramConstraintInvocationV1::Wcag22Srgb8(criterion), + )); + self + } + + /// Связывает клиентский выходной слот с итоговым Paint. + pub fn push_output(&mut self, output: OutputSlotIdV1, paint: PaintIdV1) -> &mut Self { + self.inner + .push_output(OutputBinding::new(output.into_core(), paint.into_core())); + self + } + + /// Атомарно проверяет и компилирует весь граф. + pub fn compile(self) -> Result { + let compiled = self.inner.compile().map_err(map_program_compile_error)?; + Ok(OwnerV1::from_compiled(compiled)) + } +} + +impl Default for DraftV1 { + fn default() -> Self { + Self::new() + } +} + +/// Непрозрачный сильный владелец одной точной скомпилированной Program. +/// +/// Созданные им Session изменяются только через эту же аллокацию. Уничтожение +/// Owner отзывает обновления и операции, но исторические evidence остаются в +/// Session. +pub struct OwnerV1 { + compiled: CompiledCoreProgramV1, +} + +/// Отказ доступа из-за несовпадения точной owner-эпохи. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AccessErrorV1 { + /// Session была создана другой аллокацией Owner. + OwnerMismatch, +} + +impl OwnerV1 { + /// Внутренняя передача из канонического компилятора. + pub(crate) const fn from_compiled(compiled: CompiledCoreProgramV1) -> Self { + Self { compiled } + } + + /// Возвращает каноническую identity скомпилированного содержания. + /// + /// Identity доступна до первого update, но не заменяет полномочия этой + /// конкретной owner-эпохи. + pub fn content_identity(&self) -> ContentIdentityV1 { + ContentIdentityV1::from_core(self.compiled.content_identity()) + } + + /// Число значений Surface в каждом schema-ordered сценарии. + pub fn surface_input_port_count(&self) -> usize { + self.compiled.surface_input_ports().len() + } + + /// Канонический порядок входных портов для однократного binding на хосте. + pub fn surface_input_ports(&self) -> impl ExactSizeIterator + '_ { + self.compiled + .surface_input_ports() + .iter() + .copied() + .map(SurfaceInputPortIdV1::from_core) + } + + /// Канонический порядок непрозрачных выходных слотов. + pub fn output_slots(&self) -> impl ExactSizeIterator + '_ { + self.compiled + .outputs() + .map(|(slot, _paint)| OutputSlotIdV1::from_core(slot)) + } + + /// Проецирует операции только для Session этой точной owner-эпохи. + /// + /// Равенство [`ContentIdentityV1`] не даёт полномочий. + pub fn project<'owner, 'session>( + &'owner self, + session: &'session SessionV1, + ) -> Result, AccessErrorV1> { + if !self.compiled.owns_session(&session.session) { + return Err(AccessErrorV1::OwnerMismatch); + } + Ok(ProjectionV1 { + evidence: session.evidence(), + owner: self, + scope: BorrowScopeV1::new(self, session), + }) + } + + /// Атомарно допускает update и возвращает его неизменяемую проекцию. + /// + /// Несовпадение Owner проверяется до admission, аллокаций и вычисления. + pub fn update<'owner, 'session>( + &'owner self, + session: &'session mut SessionV1, + update: UpdateV1<'_>, + ) -> Result, UpdateErrorV1> { + if !self.compiled.owns_session(&session.session) { + return Err(UpdateErrorV1::OwnerMismatch); + } + session.apply_update(update)?; + Ok(ProjectionV1 { + evidence: session.evidence(), + owner: self, + scope: BorrowScopeV1::new(self, session), + }) + } + + /// Создаёт Session, привязанную к одному непрозрачному stream ID. + pub fn instantiate(&self, stream_id: u32) -> Result { + let stream = ObservationStreamId::new(stream_id); + let session = self + .compiled + .instantiate(stream) + .map_err(InstantiateErrorV1::from_core)?; + Ok(SessionV1 { + scenario_order_scratch: Vec::new(), + session, + }) + } +} + +/// Один заимствованный физический сценарий в скомпилированном schema order. +/// +/// ID сценария — непрозрачная provenance. `values` содержит ровно один encoded +/// sRGB8 на каждый [`OwnerV1::surface_input_ports`] в том же порядке. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ScenarioV1<'a> { + scenario_id: u32, + values: &'a [Srgb8], +} + +impl<'a> ScenarioV1<'a> { + /// Создаёт один одновременный физический кортеж. + pub const fn new(scenario_id: u32, values: &'a [Srgb8]) -> Self { + Self { + scenario_id, + values, + } + } +} + +/// Одно revision-bound обновление; stream принадлежит Session. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum UpdateV1<'a> { + /// Согласованные физические сценарии в порядке скомпилированной схемы. + Observed { + /// Монотонная ревизия входного наблюдения. + revision: u64, + /// Одновременные сценарии физического контекста. + scenarios: &'a [ScenarioV1<'a>], + }, + /// Наблюдение явно недоступно; Core не изобретает фон. + Unknown { + /// Монотонная ревизия входного наблюдения. + revision: u64, + /// Непрозрачная клиентская причина недоступности. + reason_id: u32, + }, +} + +/// Непрозрачная изменяемая Session одной Program и одного stream. +pub struct SessionV1 { + scenario_order_scratch: Vec, + session: CoreProgramSessionV1, +} + +impl SessionV1 { + /// Возвращает исторические evidence без права на операции. + pub fn evidence(&self) -> EvidenceViewV1<'_> { + EvidenceViewV1 { + session: &self.session, + } + } + + fn apply_update(&mut self, update: UpdateV1<'_>) -> Result<(), UpdateErrorV1> { + match update { + UpdateV1::Observed { + revision, + scenarios, + } => { + let source = ScenarioSourceV1(scenarios); + self.session + .update_schema_ordered( + Revision::new(revision), + &source, + &mut self.scenario_order_scratch, + ) + .map_err(map_session_update_error)?; + } + UpdateV1::Unknown { + revision, + reason_id, + } => { + self.session + .update_unknown(Revision::new(revision), UnknownReasonId::new(reason_id)) + .map_err(map_session_update_error)?; + } + } + Ok(()) + } +} + +struct ScenarioSourceV1<'a>(&'a [ScenarioV1<'a>]); + +impl SchemaOrderedScenarioSourceV1 for ScenarioSourceV1<'_> { + fn scenario_count(&self) -> usize { + self.0.len() + } + + fn scenario_id(&self, scenario_index: usize) -> ScenarioId { + ScenarioId::new(self.0[scenario_index].scenario_id) + } + + fn value_count(&self, scenario_index: usize) -> usize { + self.0[scenario_index].values.len() + } + + fn value(&self, scenario_index: usize, binding_index: usize) -> Srgb8 { + self.0[scenario_index].values[binding_index] + } +} + +/// Закрытая классификация lifecycle Session. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum StateKindV1 { + /// Допущенного вычислимого наблюдения ещё нет. + Waiting, + /// Текущая ревизия сертифицирована. + Ready, + /// Новое наблюдение недоступно, сохранён прошлый сертификат. + Stale, + /// Текущая ревизия имеет исчерпывающий конфликт. + Failed, +} + +/// Текущая сырая голова наблюдений независимо от evaluator lifecycle. +/// +/// Непустая голова хранит stream provenance, но не полномочия на операции. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ObservationHeadV1 { + /// Наблюдений ещё не было. + Empty, + /// Наблюдение явно недоступно. + Unknown { + /// Stream наблюдения. + stream: StreamIdV1, + /// Ревизия наблюдения. + revision: u64, + /// Непрозрачная причина недоступности. + reason_id: u32, + }, + /// Принят физический набор сценариев. + Observed { + /// Stream наблюдения. + stream: StreamIdV1, + /// Ревизия наблюдения. + revision: u64, + }, +} + +/// Заимствованное историческое evidence, принадлежащее Session. +#[derive(Clone, Copy)] +pub struct EvidenceViewV1<'a> { + session: &'a CoreProgramSessionV1, +} + +impl<'a> EvidenceViewV1<'a> { + const fn state(self) -> &'a CoreProgramStateV1 { + self.session.state() + } + + /// Возвращает lifecycle-класс текущего состояния. + pub const fn kind(self) -> StateKindV1 { + match self.state() { + SessionState::Waiting => StateKindV1::Waiting, + SessionState::Ready { .. } => StateKindV1::Ready, + SessionState::Stale { .. } => StateKindV1::Stale, + SessionState::Failed { .. } => StateKindV1::Failed, + } + } + + /// Возвращает сырую голову наблюдений вместе с provenance. + pub fn observation_head(self) -> ObservationHeadV1 { + match self.session.raw_head() { + ObservationHeadViewV1::Empty => ObservationHeadV1::Empty, + ObservationHeadViewV1::Unknown(unknown) => ObservationHeadV1::Unknown { + stream: StreamIdV1::from_core(unknown.stream()), + revision: unknown.revision().value(), + reason_id: unknown.reason().value(), + }, + ObservationHeadViewV1::Observed(observation) => ObservationHeadV1::Observed { + stream: StreamIdV1::from_core(observation.stream()), + revision: observation.revision().value(), + }, + } + } + + /// Индекс cause-сертификата в [`Self::certificates`] для `Failed`. + pub const fn cause_certificate_index(self) -> Option { + match self.state() { + SessionState::Failed { .. } => Some(0), + SessionState::Waiting | SessionState::Ready { .. } | SessionState::Stale { .. } => None, + } + } + + /// Сертификаты в каноническом порядке одного снимка. + pub fn certificates( + self, + ) -> impl ExactSizeIterator> + FusedIterator + 'a { + let (first, second) = match self.state() { + SessionState::Waiting => (None, None), + SessionState::Ready { current } | SessionState::Stale { previous: current } => { + (Some(CertificateV1::verified(current)), None) + } + SessionState::Failed { cause, previous } => ( + Some(CertificateV1::conflict(cause)), + previous.as_ref().map(CertificateV1::verified), + ), + }; + CertificatesV1::new(first, second) + } +} + +/// Нулевой lifetime-маркер точной пары Owner и неизменяемого снимка Session. +#[derive(Clone, Copy)] +struct BorrowScopeV1<'owner, 'session> { + _scope: PhantomData<(&'owner OwnerV1, &'session SessionV1)>, +} + +impl<'owner, 'session> BorrowScopeV1<'owner, 'session> { + const fn new(_owner: &'owner OwnerV1, _session: &'session SessionV1) -> Self { + Self { + _scope: PhantomData, + } + } +} + +/// Проверенная Owner-and-snapshot проекция evidence и операций. +#[derive(Clone, Copy)] +pub struct ProjectionV1<'owner, 'session> { + evidence: EvidenceViewV1<'session>, + owner: &'owner OwnerV1, + scope: BorrowScopeV1<'owner, 'session>, +} + +impl<'owner, 'session> ProjectionV1<'owner, 'session> { + /// Возвращает историческое evidence этого снимка. + pub const fn evidence(self) -> EvidenceViewV1<'session> { + self.evidence + } + + /// Возвращает полную каноническую последовательность операций состояния. + pub fn operations( + self, + ) -> impl ExactSizeIterator> + FusedIterator { + let inner = match self.evidence.state() { + SessionState::Waiting => OperationSourceV1::Empty, + SessionState::Ready { current } => { + debug_assert_eq!(current.outputs().len(), self.owner.compiled.output_count()); + debug_assert!( + current + .outputs() + .iter() + .enumerate() + .all(|(index, output)| self.owner.compiled.output_slot_at(index) + == Some(output.output())) + ); + OperationSourceV1::Set { + outputs: current.outputs().iter(), + certificate: VerifiedCertificateV1 { inner: current }, + scope: self.scope, + } + } + SessionState::Stale { previous } => OperationSourceV1::Hold { + outputs: previous.outputs().iter(), + certificate: VerifiedCertificateV1 { inner: previous }, + scope: self.scope, + }, + SessionState::Failed { + previous: Some(previous), + .. + } => OperationSourceV1::Hold { + outputs: previous.outputs().iter(), + certificate: VerifiedCertificateV1 { inner: previous }, + scope: self.scope, + }, + SessionState::Failed { previous: None, .. } => OperationSourceV1::Remove { + slots: OwnerOutputSlotsV1::new(&self.owner.compiled), + scope: self.scope, + }, + }; + OperationsV1 { inner } + } +} + +/// Collision-resistant адрес канонического физического содержания Program. +/// +/// Identity не идентифицирует owner-эпоху и не даёт runtime-полномочий. +#[repr(transparent)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ContentIdentityV1([u8; 32]); + +impl ContentIdentityV1 { + const fn from_core(value: ProgramContentIdentityV1) -> Self { + Self(*value.as_bytes()) + } + + /// Возвращает 256-битное каноническое представление identity. + pub const fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } +} + +/// Доказательство прохождения всех hard-клеток на полном physical support. +#[derive(Clone, Copy)] +pub struct VerifiedCertificateV1<'a> { + inner: &'a CoreVerifiedV1, +} + +impl<'a> VerifiedCertificateV1<'a> { + /// Возвращает identity скомпилированного содержания. + pub const fn content_identity(self) -> ContentIdentityV1 { + ContentIdentityV1::from_core(self.inner.report().content_identity()) + } + + /// Возвращает точное наблюдение, на котором выдан сертификат. + pub const fn observation(self) -> ObservationV1<'a> { + ObservationV1 { + inner: self.inner.report().observation(), + } + } + + /// Возвращает индекс выбранного состояния или `None` для fixed Program. + pub const fn selected_state_index(self) -> Option { + self.inner.selected_state_index() + } + + /// Возвращает все `case × constraint` клетки выбранного состояния. + pub fn cells(self) -> impl ExactSizeIterator> + FusedIterator + 'a { + self.inner + .report() + .cells() + .iter() + .map(VerifiedCellV1::from_core) + } + + /// Возвращает все сертифицированные выходы в каноническом порядке. + pub fn outputs( + self, + ) -> impl ExactSizeIterator> + FusedIterator + 'a { + self.inner + .outputs() + .iter() + .map(CertifiedOutputV1::from_core) + } +} + +/// Исчерпывающее доказательство, что каждое состояние нарушает hard-клетку. +#[derive(Clone, Copy)] +pub struct ConflictCertificateV1<'a> { + inner: &'a CoreConflictV1, +} + +impl<'a> ConflictCertificateV1<'a> { + /// Возвращает identity скомпилированного содержания. + pub const fn content_identity(self) -> ContentIdentityV1 { + ContentIdentityV1::from_core(self.inner.report().content_identity()) + } + + /// Возвращает точное наблюдение, вызвавшее конфликт. + pub const fn observation(self) -> ObservationV1<'a> { + ObservationV1 { + inner: self.inner.report().observation(), + } + } + + /// Возвращает число исчерпывающе рассмотренных состояний. + pub const fn considered_state_count(self) -> usize { + self.inner.considered_state_count() + } + + /// Возвращает все `state × case × constraint` клетки конфликта. + pub fn cells(self) -> impl ExactSizeIterator> + FusedIterator + 'a { + self.inner + .report() + .cells() + .iter() + .map(ConflictCellV1::from_core) + } +} + +/// Закрытая заимствованная проекция одного Core-owned сертификата. +/// +/// Сертификат заимствует только историю Session и может пережить Owner, +/// разрешивший исходную проекцию. +/// +/// ```no_run +/// use labcolors_core::program::{ +/// CertificateV1, OwnerV1, SessionV1, +/// }; +/// +/// fn retain_evidence<'session>( +/// owner: OwnerV1, +/// session: &'session SessionV1, +/// ) -> CertificateV1<'session> { +/// owner +/// .project(session) +/// .unwrap() +/// .evidence() +/// .certificates() +/// .next() +/// .unwrap() +/// } +/// ``` +#[derive(Clone, Copy)] +pub enum CertificateV1<'a> { + /// Все hard-клетки полного support прошли. + Verified(VerifiedCertificateV1<'a>), + /// Каждое рассмотренное состояние нарушает хотя бы одну hard-клетку. + Conflict(ConflictCertificateV1<'a>), +} + +impl<'a> CertificateV1<'a> { + const fn verified(value: &'a CoreVerifiedV1) -> Self { + Self::Verified(VerifiedCertificateV1 { inner: value }) + } + + const fn conflict(value: &'a CoreConflictV1) -> Self { + Self::Conflict(ConflictCertificateV1 { inner: value }) + } + + /// Возвращает identity скомпилированного содержания. + pub const fn content_identity(self) -> ContentIdentityV1 { + match self { + Self::Verified(value) => value.content_identity(), + Self::Conflict(value) => value.content_identity(), + } + } + + /// Возвращает точное revision-bound наблюдение сертификата. + pub const fn observation(self) -> ObservationV1<'a> { + match self { + Self::Verified(value) => value.observation(), + Self::Conflict(value) => value.observation(), + } + } + + #[cfg(test)] + pub(crate) fn observation_backing_ptr_for_test(self) -> *const () { + self.observation().inner.backing_ptr_for_test() + } +} + +/// Точное revision-bound наблюдение, сохранённое сертификатом. +#[derive(Clone, Copy)] +pub struct ObservationV1<'a> { + inner: &'a crate::observation::RevisionBoundObservationV1, +} + +impl<'a> ObservationV1<'a> { + /// Возвращает stream provenance наблюдения. + pub const fn stream(self) -> StreamIdV1 { + StreamIdV1::from_core(self.inner.stream()) + } + + /// Возвращает ревизию наблюдения. + pub const fn revision(self) -> u64 { + self.inner.revision().value() + } + + /// Возвращает каноническую schema, общую для всех физических cases. + /// + /// Позиция `i` соответствует позиции `i` в [`PhysicalCaseV1::values`]. + pub fn surface_input_ports( + self, + ) -> impl ExactSizeIterator + FusedIterator + 'a { + self.inner + .schema() + .iter() + .copied() + .map(SurfaceInputPortIdV1::from_core) + } + + /// Возвращает канонические уникальные физические cases. + /// + /// Дубликаты значений схлопываются, а их ID сохраняются в provenance. + pub fn physical_cases( + self, + ) -> impl ExactSizeIterator> + FusedIterator + 'a { + (0..self.inner.physical_case_count()).map(move |index| PhysicalCaseV1 { + observation: self.inner, + index, + }) + } +} + +/// Закрытое семейство сигналов физического case. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SignalV1 { + /// Encoded sRGB8 в IEC 61966-2-1 с белой точкой D65. + Iec61966Srgb8D65(Srgb8), +} + +/// Один канонический физический case и его полная provenance. +#[derive(Clone, Copy)] +pub struct PhysicalCaseV1<'a> { + observation: &'a crate::observation::RevisionBoundObservationV1, + index: usize, +} + +impl<'a> PhysicalCaseV1<'a> { + /// Возвращает значения case в каноническом schema order. + pub fn values(self) -> impl ExactSizeIterator + FusedIterator + 'a { + self.observation + .physical_values(self.index) + .expect("physical case originates from the same observation") + .iter() + .copied() + .map(|signal| match signal.view() { + ColorSignalViewV1::Iec61966Srgb8D65(value) => SignalV1::Iec61966Srgb8D65(value), + }) + } + + /// Возвращает все scenario ID, схлопнутые в этот физический case. + pub fn provenance(self) -> impl ExactSizeIterator + FusedIterator + 'a { + self.observation + .provenance(self.index) + .expect("physical case originates from the same observation") + .iter() + .copied() + .map(ScenarioIdV1::from_core) + } +} + +/// Роль одной constraint-клетки в выборе. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ConstraintModeV1 { + /// Нарушение запрещает состояние. + Hard, + /// Результат сохраняется, но не влияет на выбор. + ReportOnly, +} + +/// Одна клетка `case × constraint` выбранного или fixed состояния. +#[derive(Clone, Copy)] +pub struct VerifiedCellV1<'a> { + inner: &'a CoreProgramConstraintCellV1, +} + +impl<'a> VerifiedCellV1<'a> { + const fn from_core(inner: &'a CoreProgramConstraintCellV1) -> Self { + Self { inner } + } + + /// Возвращает индекс физического case. + pub const fn case_index(self) -> usize { + self.inner.case_index() + } + + /// Возвращает ID ограничения. + pub const fn constraint(self) -> ConstraintIdV1 { + ConstraintIdV1::from_core(self.inner.constraint()) + } + + /// Возвращает ID проверенного Occurrence. + pub const fn occurrence(self) -> OccurrenceIdV1 { + OccurrenceIdV1::from_core(self.inner.target()) + } + + /// Возвращает роль ограничения в выборе. + pub const fn mode(self) -> ConstraintModeV1 { + project_constraint_mode(self.inner) + } + + /// Возвращает типизированное сохранённое evidence. + pub fn assessment(self) -> AssessmentV1<'a> { + project_assessment(self.inner) + } +} + +/// Одна исчерпывающая клетка `state × case × constraint` конфликта. +#[derive(Clone, Copy)] +pub struct ConflictCellV1<'a> { + inner: &'a CoreProgramConstraintCellV1, +} + +impl<'a> ConflictCellV1<'a> { + const fn from_core(inner: &'a CoreProgramConstraintCellV1) -> Self { + Self { inner } + } + + /// Возвращает индекс рассмотренного состояния. + pub const fn state_index(self) -> usize { + self.inner.candidate_state_index() + } + + /// Возвращает индекс физического case. + pub const fn case_index(self) -> usize { + self.inner.case_index() + } + + /// Возвращает ID ограничения. + pub const fn constraint(self) -> ConstraintIdV1 { + ConstraintIdV1::from_core(self.inner.constraint()) + } + + /// Возвращает ID проверенного Occurrence. + pub const fn occurrence(self) -> OccurrenceIdV1 { + OccurrenceIdV1::from_core(self.inner.target()) + } + + /// Возвращает роль ограничения в выборе. + pub const fn mode(self) -> ConstraintModeV1 { + project_constraint_mode(self.inner) + } + + /// Возвращает типизированное сохранённое evidence. + pub fn assessment(self) -> AssessmentV1<'a> { + project_assessment(self.inner) + } +} + +const fn project_constraint_mode(cell: &CoreProgramConstraintCellV1) -> ConstraintModeV1 { + if cell.is_hard() { + ConstraintModeV1::Hard + } else { + ConstraintModeV1::ReportOnly + } +} + +fn project_assessment(cell: &CoreProgramConstraintCellV1) -> AssessmentV1<'_> { + match cell.result() { + ProgramConstraintResultV1::Pass(CoreProgramPassEvidenceV1::ExactSrgb8(evidence)) => { + AssessmentV1::ExactSrgb8(ExactSrgb8EvidenceV1 { + inner: ExactSrgb8EvidenceRefV1::Pass(evidence), + }) + } + ProgramConstraintResultV1::Violation(CoreProgramViolationEvidenceV1::ExactSrgb8( + evidence, + )) => AssessmentV1::ExactSrgb8(ExactSrgb8EvidenceV1 { + inner: ExactSrgb8EvidenceRefV1::Violation(evidence), + }), + ProgramConstraintResultV1::Pass(CoreProgramPassEvidenceV1::Wcag22Srgb8(evidence)) => { + AssessmentV1::Wcag22Srgb8(Wcag22Srgb8EvidenceV1 { + inner: Wcag22Srgb8EvidenceRefV1::Pass(evidence), + }) + } + ProgramConstraintResultV1::Violation(CoreProgramViolationEvidenceV1::Wcag22Srgb8( + evidence, + )) => AssessmentV1::Wcag22Srgb8(Wcag22Srgb8EvidenceV1 { + inner: Wcag22Srgb8EvidenceRefV1::Violation(evidence), + }), + } +} + +/// Закрытое семейство сохранённого evaluator evidence. +#[derive(Clone, Copy)] +pub enum AssessmentV1<'a> { + /// Evidence точного сравнения encoded sRGB8. + ExactSrgb8(ExactSrgb8EvidenceV1<'a>), + /// Evidence применимого критерия WCAG 2.2. + Wcag22Srgb8(Wcag22Srgb8EvidenceV1<'a>), +} + +impl<'a> AssessmentV1<'a> { + /// Возвращает несовместимый с противоположным исход классификатора. + pub const fn verdict(self) -> VerdictV1 { + match self { + Self::ExactSrgb8(value) => value.verdict(), + Self::Wcag22Srgb8(value) => value.verdict(), + } + } + + /// Возвращает общую физическую и моделированную привязку точки. + pub fn binding(self) -> PointBindingV1<'a> { + match self { + Self::ExactSrgb8(value) => value.binding(), + Self::Wcag22Srgb8(value) => value.binding(), + } + } +} + +/// Несовместимые сохранённые исходы классификатора. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum VerdictV1 { + /// Критерий доказан. + Pass, + /// Критерий доказанно нарушен. + Violation, +} + +#[derive(Clone, Copy)] +enum ExactSrgb8EvidenceRefV1<'a> { + Pass(&'a CoreExactPassEvidenceV1), + Violation(&'a CoreExactViolationEvidenceV1), +} + +/// Evidence точного sRGB8 сравнения с физикой и моделированным контекстом. +#[derive(Clone, Copy)] +pub struct ExactSrgb8EvidenceV1<'a> { + inner: ExactSrgb8EvidenceRefV1<'a>, +} + +impl<'a> ExactSrgb8EvidenceV1<'a> { + /// Возвращает сохранённый исход классификатора. + pub const fn verdict(self) -> VerdictV1 { + match self.inner { + ExactSrgb8EvidenceRefV1::Pass(_) => VerdictV1::Pass, + ExactSrgb8EvidenceRefV1::Violation(_) => VerdictV1::Violation, + } + } + + /// Возвращает ожидаемый encoded sRGB8 результат. + pub fn expected(self) -> Srgb8 { + match self.inner { + ExactSrgb8EvidenceRefV1::Pass(value) => value.target(), + ExactSrgb8EvidenceRefV1::Violation(value) => value.target(), + } + } + + /// Возвращает физическую и моделированную привязку точки. + pub fn binding(self) -> PointBindingV1<'a> { + let value = match self.inner { + ExactSrgb8EvidenceRefV1::Pass(value) => value.binding(), + ExactSrgb8EvidenceRefV1::Violation(value) => value.binding(), + }; + PointBindingV1 { inner: value } + } +} + +#[derive(Clone, Copy)] +enum Wcag22Srgb8EvidenceRefV1<'a> { + Pass(&'a CoreWcag22PassEvidenceV1), + Violation(&'a CoreWcag22ViolationEvidenceV1), +} + +/// WCAG 2.2 evidence вместе с физикой и моделированным контекстом. +#[derive(Clone, Copy)] +pub struct Wcag22Srgb8EvidenceV1<'a> { + inner: Wcag22Srgb8EvidenceRefV1<'a>, +} + +impl<'a> Wcag22Srgb8EvidenceV1<'a> { + /// Возвращает сохранённый исход классификатора. + pub const fn verdict(self) -> VerdictV1 { + match self.inner { + Wcag22Srgb8EvidenceRefV1::Pass(_) => VerdictV1::Pass, + Wcag22Srgb8EvidenceRefV1::Violation(_) => VerdictV1::Violation, + } + } + + /// Возвращает версию применённого WCAG 2.2 профиля. + pub fn profile_id(self) -> Wcag22ProfileIdV1 { + match self.inner { + Wcag22Srgb8EvidenceRefV1::Pass(value) => value.measurement().value().profile_id(), + Wcag22Srgb8EvidenceRefV1::Violation(value) => value.measurement().value().profile_id(), + } + } + + /// Возвращает применённый критерий. + pub fn criterion(self) -> Wcag22CriterionV1 { + match self.inner { + Wcag22Srgb8EvidenceRefV1::Pass(value) => value.measurement().value().criterion(), + Wcag22Srgb8EvidenceRefV1::Violation(value) => value.measurement().value().criterion(), + } + } + + /// Возвращает сертифицированные границы яркости foreground. + pub fn foreground_luminance(self) -> Wcag22LuminanceBoundsQ55V1 { + let measurement = match self.inner { + Wcag22Srgb8EvidenceRefV1::Pass(value) => value.measurement().value().measurement(), + Wcag22Srgb8EvidenceRefV1::Violation(value) => value.measurement().value().measurement(), + }; + measurement.foreground_luminance + } + + /// Возвращает сертифицированные границы яркости background. + pub fn background_luminance(self) -> Wcag22LuminanceBoundsQ55V1 { + let measurement = match self.inner { + Wcag22Srgb8EvidenceRefV1::Pass(value) => value.measurement().value().measurement(), + Wcag22Srgb8EvidenceRefV1::Violation(value) => value.measurement().value().measurement(), + }; + measurement.background_luminance + } + + /// Возвращает числовое доказательство устойчивости решения. + pub fn numerical_evidence(self) -> &'a NumericalDecisionEvidenceV1 { + match self.inner { + Wcag22Srgb8EvidenceRefV1::Pass(value) => value.measurement().value().evidence(), + Wcag22Srgb8EvidenceRefV1::Violation(value) => value.measurement().value().evidence(), + } + } + + /// Возвращает физическую и моделированную привязку точки. + pub fn binding(self) -> PointBindingV1<'a> { + let value = match self.inner { + Wcag22Srgb8EvidenceRefV1::Pass(value) => value.binding(), + Wcag22Srgb8EvidenceRefV1::Violation(value) => value.binding(), + }; + PointBindingV1 { inner: value } + } +} + +/// Общая привязка физической композиции и моделированного tristimulus/context. +#[derive(Clone, Copy)] +pub struct PointBindingV1<'a> { + inner: &'a ProgramVisiblePointBindingV1, +} + +impl<'a> PointBindingV1<'a> { + /// Возвращает закрытый тип точной физической композиции. + pub const fn physical(self) -> PhysicalPointV1<'a> { + match self.inner.physical().occurrence().profile() { + CompositionProfileV1::EncodedSrgb8SourceOverV1 => { + PhysicalPointV1::EncodedSrgb8SourceOver(EncodedSrgb8SourceOverV1 { + inner: self.inner, + }) + } + } + } + + /// Возвращает закрытый тип допущенного моделированного сигнала. + pub const fn modeled(self) -> ModeledPointV1<'a> { + match self.inner.modeled_lcs().provenance().binding() { + AdmittedSrgb8TristimulusBindingV1::Iec61966Srgb8ToCie1931TwoDegreeXyzD65RelativeY1V1 => { + ModeledPointV1::Iec61966Srgb8ToCie1931TwoDegreeXyzD65RelativeY1( + ModeledTristimulusV1 { inner: self.inner }, + ) + } + } + } +} + +/// Закрытое семейство точной физической композиции. +#[derive(Clone, Copy)] +pub enum PhysicalPointV1<'a> { + /// Encoded-sRGB8 source-over композиция. + EncodedSrgb8SourceOver(EncodedSrgb8SourceOverV1<'a>), +} + +/// Точная привязка одного encoded-sRGB8 source-over Occurrence. +#[derive(Clone, Copy)] +pub struct EncodedSrgb8SourceOverV1<'a> { + inner: &'a ProgramVisiblePointBindingV1, +} + +impl EncodedSrgb8SourceOverV1<'_> { + /// Возвращает ID накладываемого Paint. + pub const fn subject_paint(self) -> PaintIdV1 { + PaintIdV1::from_core(self.inner.physical().program_occurrence().subject()) + } + + /// Возвращает ID backdrop Surface. + pub const fn backdrop_surface(self) -> SurfaceIdV1 { + SurfaceIdV1::from_core( + self.inner + .physical() + .program_occurrence() + .backdrop_surface(), + ) + } + + /// Возвращает исходный encoded sRGB8 subject до композиции. + pub const fn subject(self) -> Srgb8 { + Srgb8::new(self.inner.physical().occurrence().subject_rgb()) + } + + /// Возвращает точную прозрачность subject в `[0, 1]`. + pub const fn opacity(self) -> f64 { + f64::from_bits(self.inner.physical().occurrence().subject_opacity_bits()) + } + + /// Возвращает observed encoded sRGB8 backdrop. + pub const fn backdrop(self) -> Srgb8 { + Srgb8::new(self.inner.physical().occurrence().backdrop_rgb()) + } + + /// Возвращает видимый encoded sRGB8 результат композиции. + pub const fn visible(self) -> Srgb8 { + Srgb8::new(self.inner.physical().occurrence().output_rgb()) + } +} + +/// Закрытое семейство provenance моделированного tristimulus. +#[derive(Clone, Copy)] +pub enum ModeledPointV1<'a> { + /// IEC sRGB8 → CIE 1931 2° XYZ D65 с относительным `Y=1`. + Iec61966Srgb8ToCie1931TwoDegreeXyzD65RelativeY1(ModeledTristimulusV1<'a>), +} + +/// Допущенный моделированный tristimulus и его контекст восприятия. +#[derive(Clone, Copy)] +pub struct ModeledTristimulusV1<'a> { + inner: &'a ProgramVisiblePointBindingV1, +} + +impl ModeledTristimulusV1<'_> { + /// Возвращает относительные координаты CIE XYZ. + pub fn xyz(self) -> [f64; 3] { + self.inner.modeled_lcs().derivation().sample().xyz() + } + + /// Возвращает явный контекст, использованный при моделировании. + pub const fn appearance_context(self) -> AppearanceContextV1 { + AppearanceContextV1(self.inner.modeled_lcs().occurrence().context()) + } +} + +/// Один Core-сертифицированный выходной Paint. +#[derive(Clone, Copy)] +pub struct CertifiedOutputV1<'a> { + inner: &'a ProgramOutputV1, +} + +impl<'a> CertifiedOutputV1<'a> { + const fn from_core(inner: &'a ProgramOutputV1) -> Self { + Self { inner } + } + + /// Возвращает клиентский выходной слот. + pub const fn output_slot(self) -> OutputSlotIdV1 { + OutputSlotIdV1::from_core((*self.inner).output()) + } + + /// Возвращает ID сертифицированного Paint. + pub const fn paint(self) -> PaintIdV1 { + PaintIdV1::from_core((*self.inner).paint().id()) + } + + /// Возвращает исходный encoded sRGB8 сигнал Paint. + pub const fn source(self) -> Srgb8 { + (*self.inner).paint().source() + } + + /// Возвращает сертифицированную прозрачность Paint. + pub const fn opacity(self) -> f64 { + (*self.inner).paint().opacity().value() + } +} + +/// Операция установки, структурно связанная с точным Verified-сертификатом. +#[derive(Clone, Copy)] +pub struct SetV1<'owner, 'session> { + output: &'session ProgramOutputV1, + certificate: VerifiedCertificateV1<'session>, + _scope: BorrowScopeV1<'owner, 'session>, +} + +impl<'session> SetV1<'_, 'session> { + /// Возвращает изменяемый клиентский выходной слот. + pub const fn output_slot(self) -> OutputSlotIdV1 { + OutputSlotIdV1::from_core((*self.output).output()) + } + + /// Возвращает исходный encoded sRGB8 сигнал результата. + pub const fn source(self) -> Srgb8 { + (*self.output).paint().source() + } + + /// Возвращает прозрачность результата. + pub const fn opacity(self) -> f64 { + (*self.output).paint().opacity().value() + } + + /// Возвращает сертификат, разрешивший эту операцию. + pub const fn certificate(self) -> VerifiedCertificateV1<'session> { + self.certificate + } +} + +/// Операция удаления результата без допустимого предыдущего значения. +#[derive(Clone, Copy)] +pub struct RemoveV1<'owner, 'session> { + output_slot: OutputSlotIdV1, + _scope: BorrowScopeV1<'owner, 'session>, +} + +impl RemoveV1<'_, '_> { + /// Возвращает удаляемый клиентский выходной слот. + pub const fn output_slot(self) -> OutputSlotIdV1 { + self.output_slot + } +} + +/// Операция удержания прошлого результата с его Verified-сертификатом. +#[derive(Clone, Copy)] +pub struct HoldV1<'owner, 'session> { + output: &'session ProgramOutputV1, + certificate: VerifiedCertificateV1<'session>, + _scope: BorrowScopeV1<'owner, 'session>, +} + +impl<'session> HoldV1<'_, 'session> { + /// Возвращает удерживаемый клиентский выходной слот. + pub const fn output_slot(self) -> OutputSlotIdV1 { + OutputSlotIdV1::from_core((*self.output).output()) + } + + /// Возвращает сертификат удерживаемого результата. + pub const fn certificate(self) -> VerifiedCertificateV1<'session> { + self.certificate + } +} + +/// Полное закрытое множество операций над непрозрачными выходными слотами. +/// +/// Каждый payload заимствует точные Owner и снимок Session. Скопированные +/// slot/source/opacity — только данные: runtime обязан перепроверить живую +/// пару непосредственно перед одним атомарным sink commit. +/// +/// ```compile_fail,E0515 +/// use labcolors_core::program::{ +/// OperationV1, OwnerV1, RemoveV1, +/// SessionV1, +/// }; +/// +/// fn escape_remove<'session>( +/// owner: OwnerV1, +/// session: &'session SessionV1, +/// ) -> RemoveV1<'session, 'session> { +/// match owner.project(session).unwrap().operations().next().unwrap() { +/// OperationV1::Remove(remove) => remove, +/// _ => panic!("fixture supplies Remove"), +/// } +/// } +/// ``` +/// +/// ```compile_fail,E0515 +/// use labcolors_core::program::{ +/// OperationV1, OwnerV1, RemoveV1, +/// }; +/// +/// fn escape_local_session<'owner>( +/// owner: &'owner OwnerV1, +/// ) -> RemoveV1<'owner, 'owner> { +/// let session = owner.instantiate(1).unwrap(); +/// match owner.project(&session).unwrap().operations().next().unwrap() { +/// OperationV1::Remove(remove) => remove, +/// _ => panic!("fixture supplies Remove"), +/// } +/// } +/// ``` +/// +/// ```compile_fail,E0515 +/// use labcolors_core::program::{ +/// OperationV1, OwnerV1, SessionV1, +/// SetV1, +/// }; +/// +/// fn escape_set<'session>( +/// owner: OwnerV1, +/// session: &'session SessionV1, +/// ) -> SetV1<'session, 'session> { +/// match owner.project(session).unwrap().operations().next().unwrap() { +/// OperationV1::Set(set) => set, +/// _ => panic!("fixture supplies Set"), +/// } +/// } +/// ``` +/// +/// ```compile_fail,E0515 +/// use labcolors_core::program::{ +/// HoldV1, OperationV1, OwnerV1, +/// SessionV1, +/// }; +/// +/// fn escape_hold<'session>( +/// owner: OwnerV1, +/// session: &'session SessionV1, +/// ) -> HoldV1<'session, 'session> { +/// match owner.project(session).unwrap().operations().next().unwrap() { +/// OperationV1::Hold(hold) => hold, +/// _ => panic!("fixture supplies Hold"), +/// } +/// } +/// ``` +/// +/// ```compile_fail,E0502 +/// use labcolors_core::program::{ +/// OperationV1, OwnerV1, SessionV1, +/// UpdateV1, +/// }; +/// +/// fn remove_blocks_session_mutation( +/// owner: &OwnerV1, +/// session: &mut SessionV1, +/// ) { +/// let remove = match owner.project(session).unwrap().operations().next().unwrap() { +/// OperationV1::Remove(remove) => remove, +/// _ => return, +/// }; +/// let _second = owner.update( +/// session, +/// UpdateV1::Unknown { +/// revision: 2, +/// reason_id: 7, +/// }, +/// ); +/// let _slot = remove.output_slot(); +/// } +/// ``` +#[derive(Clone, Copy)] +pub enum OperationV1<'owner, 'session> { + /// Установить сертифицированный результат. + Set(SetV1<'owner, 'session>), + /// Удалить результат, когда допустимого прошлого значения нет. + Remove(RemoveV1<'owner, 'session>), + /// Удержать последний сертифицированный результат. + Hold(HoldV1<'owner, 'session>), +} + +struct CertificatesV1<'a> { + values: [Option>; 2], + index: usize, + len: usize, +} + +impl<'a> CertificatesV1<'a> { + fn new(first: Option>, second: Option>) -> Self { + let len = usize::from(first.is_some()) + usize::from(second.is_some()); + debug_assert!(first.is_some() || second.is_none()); + Self { + values: [first, second], + index: 0, + len, + } + } +} + +impl<'a> Iterator for CertificatesV1<'a> { + type Item = CertificateV1<'a>; + + fn next(&mut self) -> Option { + if self.index == self.len { + return None; + } + let value = self.values[self.index]; + self.index += 1; + value + } + + fn size_hint(&self) -> (usize, Option) { + let remaining = self.len - self.index; + (remaining, Some(remaining)) + } +} + +impl ExactSizeIterator for CertificatesV1<'_> {} +impl FusedIterator for CertificatesV1<'_> {} + +struct OwnerOutputSlotsV1<'owner> { + compiled: &'owner CompiledCoreProgramV1, + index: usize, + len: usize, +} + +impl<'owner> OwnerOutputSlotsV1<'owner> { + fn new(compiled: &'owner CompiledCoreProgramV1) -> Self { + Self { + compiled, + index: 0, + len: compiled.output_count(), + } + } +} + +impl Iterator for OwnerOutputSlotsV1<'_> { + type Item = OutputSlotIdV1; + + fn next(&mut self) -> Option { + if self.index == self.len { + return None; + } + let output = self.compiled.output_slot_at(self.index)?; + self.index += 1; + Some(OutputSlotIdV1::from_core(output)) + } + + fn size_hint(&self) -> (usize, Option) { + let remaining = self.len - self.index; + (remaining, Some(remaining)) + } +} + +impl ExactSizeIterator for OwnerOutputSlotsV1<'_> {} +impl FusedIterator for OwnerOutputSlotsV1<'_> {} + +enum OperationSourceV1<'owner, 'session> { + Empty, + Set { + outputs: slice::Iter<'session, ProgramOutputV1>, + certificate: VerifiedCertificateV1<'session>, + scope: BorrowScopeV1<'owner, 'session>, + }, + Hold { + outputs: slice::Iter<'session, ProgramOutputV1>, + certificate: VerifiedCertificateV1<'session>, + scope: BorrowScopeV1<'owner, 'session>, + }, + Remove { + slots: OwnerOutputSlotsV1<'owner>, + scope: BorrowScopeV1<'owner, 'session>, + }, +} + +struct OperationsV1<'owner, 'session> { + inner: OperationSourceV1<'owner, 'session>, +} + +impl<'owner, 'session> Iterator for OperationsV1<'owner, 'session> { + type Item = OperationV1<'owner, 'session>; + + fn next(&mut self) -> Option { + match &mut self.inner { + OperationSourceV1::Empty => None, + OperationSourceV1::Set { + outputs, + certificate, + scope, + } => { + let output = outputs.next()?; + Some(OperationV1::Set(SetV1 { + output, + certificate: *certificate, + _scope: *scope, + })) + } + OperationSourceV1::Hold { + outputs, + certificate, + scope, + } => Some(OperationV1::Hold(HoldV1 { + output: outputs.next()?, + certificate: *certificate, + _scope: *scope, + })), + OperationSourceV1::Remove { slots, scope } => Some(OperationV1::Remove(RemoveV1 { + output_slot: slots.next()?, + _scope: *scope, + })), + } + } + + fn size_hint(&self) -> (usize, Option) { + let remaining = match &self.inner { + OperationSourceV1::Empty => 0, + OperationSourceV1::Set { outputs, .. } => outputs.len(), + OperationSourceV1::Hold { outputs, .. } => outputs.len(), + OperationSourceV1::Remove { slots, .. } => slots.len(), + }; + (remaining, Some(remaining)) + } +} + +impl ExactSizeIterator for OperationsV1<'_, '_> {} +impl FusedIterator for OperationsV1<'_, '_> {} + +/// Закрытая классификация ошибки создания Session. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum InstantiateErrorKindV1 { + /// Для создания Session недостаточно ресурсов. + ResourceExhausted, + /// Нарушен внутренний инвариант скомпилированной Program. + InternalInvariant, +} + +/// Непрозрачная ошибка создания Session. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct InstantiateErrorV1 { + kind: InstantiateErrorKindV1, +} + +impl InstantiateErrorV1 { + const fn new(kind: InstantiateErrorKindV1) -> Self { + Self { kind } + } + + fn from_core(error: ProgramSessionInstantiateError) -> Self { + let kind = match error { + ProgramSessionInstantiateError::ResourceExhausted => { + InstantiateErrorKindV1::ResourceExhausted + } + ProgramSessionInstantiateError::InternalInvariant => { + InstantiateErrorKindV1::InternalInvariant + } + }; + Self::new(kind) + } + + /// Возвращает стабильный класс ошибки. + pub const fn kind(self) -> InstantiateErrorKindV1 { + self.kind + } +} + +impl From for InstantiateErrorV1 { + fn from(kind: InstantiateErrorKindV1) -> Self { + Self::new(kind) + } +} + +/// Закрытая классификация ошибки одного атомарного update. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum UpdateErrorKindV1 { + /// Session принадлежит другой точной owner-эпохе. + OwnerMismatch, + /// Наблюдение нарушает скомпилированную schema. + InvalidObservation, + /// Ревизия старше уже принятой. + RevisionOutOfOrder, + /// Та же ревизия содержит другой payload. + RevisionConflict, + /// Для admission или вычисления недостаточно ресурсов. + ResourceExhausted, + /// Зарегистрированный evaluator не смог выполнить оценку. + EvaluationFailed, + /// Нарушен внутренний инвариант. + InternalInvariant, +} + +/// Фаза update, в которой закончился ограниченный ресурс. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum UpdatePhaseV1 { + /// Admission и канонизация физического наблюдения. + ObservationAdmission, + /// Вычисление, поиск и финальная перепроверка Program. + ProgramEvaluation, +} + +/// Точный отказ зарегистрированного evaluator-а. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EvaluatorFailureV1 { + /// Отказ зарегистрированного WCAG 2.2 evaluator-а. + Wcag22Srgb8 { + /// Версия evaluator-а и его численного доказательства. + profile_id: Wcag22ProfileIdV1, + /// Точная исходная ошибка WCAG 2.2 без строковой переклассификации. + source: Wcag22EvaluationErrorV1, + }, +} + +/// Точная причина расхождения observation со скомпилированным binding. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ObservationBindingFailureV1 { + /// Скомпилированная schema не содержит ни одного входного порта. + EmptyCompiledSurfaceInputSchema, + /// Один входной порт повторён в скомпилированной schema. + DuplicateCompiledSurfaceInputPort { + /// Повторённый непрозрачный ID порта. + input: SurfaceInputPortIdV1, + }, + /// Update относится к другому observation stream. + StreamMismatch { + /// Stream, принадлежащий Session. + expected: StreamIdV1, + /// Stream отвергнутого update. + actual: StreamIdV1, + }, + /// Один входной порт повторён внутри физического сценария. + DuplicateSurfaceInputBinding { + /// Provenance ошибочного сценария. + scenario: ScenarioIdV1, + /// Повторённый входной порт. + input: SurfaceInputPortIdV1, + }, + /// В физическом сценарии отсутствует обязательный входной порт. + MissingSurfaceInputBinding { + /// Provenance ошибочного сценария. + scenario: ScenarioIdV1, + /// Отсутствующий входной порт. + input: SurfaceInputPortIdV1, + }, + /// Физический сценарий содержит неизвестный входной порт. + UnexpectedSurfaceInputBinding { + /// Provenance ошибочного сценария. + scenario: ScenarioIdV1, + /// Неизвестный входной порт. + input: SurfaceInputPortIdV1, + }, + /// Канонический порядок портов разошёлся со скомпилированной schema. + SchemaMismatch { + /// Индекс физического сценария. + case_index: usize, + /// Первый несовпавший индекс binding. + binding_index: usize, + /// Ожидаемый порт либо конец скомпилированной schema. + expected: Option, + /// Фактический порт либо конец observation schema. + actual: Option, + }, +} + +/// Зарегистрированная identity XYZ-frame в диагностике закрытого Core. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ColorimetricFrameV1 { + /// CIE 1931 2°, IEC 61966-2-1 D65, относительная шкала `Y=1`, XYZ v1. + Iec61966Srgb8D65XyzRelativeY1V1, + /// Зарезервированный frame hostile-теста, недостижимый в production. + #[cfg(test)] + MutationSentinelV1, +} + +/// Компонент XYZ в точной диагностике. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TristimulusComponentV1 { + /// Компонент X. + X, + /// Компонент Y. + Y, + /// Компонент Z. + Z, +} + +/// Точная конечная XYZ-точка и её зарегистрированный frame. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct TristimulusSampleV1 { + xyz_bits: [u64; 3], + frame: ColorimetricFrameV1, +} + +impl TristimulusSampleV1 { + fn from_core(sample: TristimulusSample) -> Self { + Self { + xyz_bits: sample.xyz().map(f64::to_bits), + frame: map_colorimetric_frame(sample.frame()), + } + } + + /// Возвращает точные конечные XYZ-компоненты. + pub fn xyz(self) -> [f64; 3] { + self.xyz_bits.map(f64::from_bits) + } + + /// Возвращает зарегистрированный frame точки. + pub const fn frame(self) -> ColorimetricFrameV1 { + self.frame + } +} + +/// Точная причина, по которой Core не сформировал modeled LCS occurrence. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ModeledOccurrenceFailureV1 { + /// Детерминированное преобразование получило недопустимую XYZ-компоненту. + Tristimulus { + /// Ошибочная компонента. + component: TristimulusComponentV1, + /// Точная числовая причина. + reason: NumericDomainErrorV1, + }, + /// Stimulus и appearance context принадлежат разным frame. + FrameMismatch { + /// Frame modeled stimulus. + stimulus: ColorimetricFrameV1, + /// Frame appearance context. + context: ColorimetricFrameV1, + }, + /// Повторное вычисление provenance получило недопустимую XYZ-компоненту. + ProvenanceReplayFailed { + /// Ошибочная компонента. + component: TristimulusComponentV1, + /// Точная числовая причина. + reason: NumericDomainErrorV1, + }, + /// Записанная modeled-точка не совпала с повторным вычислением provenance. + RecordedSampleDoesNotReplay { + /// Записанная точка. + recorded: TristimulusSampleV1, + /// Повторно вычисленная точка. + replayed: TristimulusSampleV1, + }, + /// Точка occurrence не совпала с modeled provenance. + OccurrenceSampleMismatch { + /// Точка occurrence. + occurrence: TristimulusSampleV1, + /// Точка modeled provenance. + modeled: TristimulusSampleV1, + }, +} + +/// Точное недопустимое protocol-состояние зарегистрированного evaluator-а. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EvaluatorProtocolFailureV1 { + /// WCAG evaluator вернул kernel-ошибку, недостижимую для typed Program. + Wcag22Kernel { + /// Версия evaluator-а и его численного доказательства. + profile_id: Wcag22ProfileIdV1, + /// Точная исходная kernel-ошибка. + source: Wcag22EvaluationErrorV1, + }, + /// Hard-вызов получил только клиентскую декларацию неприменимости. + Wcag22ReportOnly { + /// Версия evaluator-а и его численного доказательства. + profile_id: Wcag22ProfileIdV1, + /// Точная клиентская декларация. + declaration: Wcag22ClientDeclaredNotApplicableV1, + }, + /// Evaluator проверил другой критерий. + Wcag22CriterionMismatch { + /// Запрошенный критерий. + requested: Wcag22CriterionV1, + /// Фактически проверенный критерий. + evaluated: Wcag22CriterionV1, + }, +} + +/// Машиночитаемая identity нарушенного внутреннего контракта. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum UpdateInvariantV1 { + /// Заимствованный matching Owner не удержал свою эпоху живой. + OwnerAuthority, + /// Каноническая observation schema разошлась со скомпилированным binding. + ObservationBinding, + /// Сохранённое evidence не принадлежит допускаемому observation. + EvidenceBinding, + /// Applicable evaluator вернул недопустимое protocol-состояние. + EvaluatorProtocol, + /// Physical и modeled точки одного evaluator-вызова разошлись. + PhysicalModeledBinding, + /// Зарегистрированный сигнал не сформировал свой LCS occurrence. + ModeledOccurrenceFormation, + /// Один выбранный state дал разные выходы в физических сценариях. + OutputCaseInvariance, + /// Детерминированная финальная перепроверка разошлась с поиском. + SelectionRecheck, + /// Закрытая программа нарушила собственную структуру исполнения. + ProgramEvaluation, +} + +/// Нарушенный внутренний контракт с точными subject и witness-фактами. +/// +/// Эти варианты недостижимы через типизированный public input. Payload нужен +/// для детерминированной диагностики и не превращает breach в цветовой verdict. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum UpdateInvariantFailureV1 { + /// Заимствованный matching Owner не удержал свою эпоху живой. + OwnerAuthority, + /// Каноническая observation schema разошлась со скомпилированным binding. + ObservationBinding { + /// Точное расхождение schema или stream. + source: ObservationBindingFailureV1, + }, + /// Сохранённое evidence не принадлежит допускаемому observation. + EvidenceBinding, + /// Applicable evaluator вернул недопустимое protocol-состояние. + EvaluatorProtocol { + /// Индекс физического сценария. + case_index: usize, + /// Непрозрачный ID проверяемого ограничения. + constraint: ConstraintIdV1, + /// Непрозрачный ID физического occurrence. + occurrence: OccurrenceIdV1, + /// Точный appearance context evaluator-вызова. + context: AppearanceContextV1, + /// Недопустимое protocol-состояние. + source: EvaluatorProtocolFailureV1, + }, + /// Physical и modeled точки одного evaluator-вызова разошлись. + PhysicalModeledBinding { + /// Индекс физического сценария. + case_index: usize, + /// Непрозрачный ID проверяемого ограничения. + constraint: ConstraintIdV1, + /// Непрозрачный ID физического occurrence. + occurrence: OccurrenceIdV1, + /// Точный appearance context evaluator-вызова. + context: AppearanceContextV1, + /// Физическая encoded sRGB8-точка. + physical: Srgb8, + /// Modeled encoded sRGB8-точка. + modeled: Srgb8, + }, + /// Зарегистрированный сигнал не сформировал свой LCS occurrence. + ModeledOccurrenceFormation { + /// Индекс физического сценария. + case_index: usize, + /// Непрозрачный ID occurrence. + occurrence: OccurrenceIdV1, + /// Intended appearance context формирования. + context: AppearanceContextV1, + /// Точная причина отказа формирования. + source: ModeledOccurrenceFailureV1, + }, + /// Один выбранный state дал разные выходы в физических сценариях. + OutputCaseInvariance { + /// Непрозрачный ID выходного слота. + output: OutputSlotIdV1, + /// Первый сценарий, задавший ожидаемое значение. + first_case: usize, + /// Сценарий с отличающимся значением. + actual_case: usize, + }, + /// Детерминированная финальная перепроверка разошлась с поиском. + SelectionRecheck { + /// Индекс выбранного joint state. + state_index: usize, + /// Индекс физического сценария. + case_index: usize, + /// Непрозрачный ID ограничения. + constraint: ConstraintIdV1, + /// Непрозрачный ID occurrence. + occurrence: OccurrenceIdV1, + /// Число hard-нарушений на финальной перепроверке. + hard_violation_count: usize, + }, + /// Закрытая программа нарушила собственную структуру исполнения. + ProgramEvaluation, +} + +impl UpdateInvariantFailureV1 { + /// Возвращает стабильную identity нарушенного контракта. + pub const fn contract(&self) -> UpdateInvariantV1 { + match self { + Self::OwnerAuthority => UpdateInvariantV1::OwnerAuthority, + Self::ObservationBinding { .. } => UpdateInvariantV1::ObservationBinding, + Self::EvidenceBinding => UpdateInvariantV1::EvidenceBinding, + Self::EvaluatorProtocol { .. } => UpdateInvariantV1::EvaluatorProtocol, + Self::PhysicalModeledBinding { .. } => UpdateInvariantV1::PhysicalModeledBinding, + Self::ModeledOccurrenceFormation { .. } => { + UpdateInvariantV1::ModeledOccurrenceFormation + } + Self::OutputCaseInvariance { .. } => UpdateInvariantV1::OutputCaseInvariance, + Self::SelectionRecheck { .. } => UpdateInvariantV1::SelectionRecheck, + Self::ProgramEvaluation => UpdateInvariantV1::ProgramEvaluation, + } + } +} + +/// Точная ошибка одного атомарного update. +/// +/// Любой variant оставляет observation head и lifecycle-состояние Core +/// неизменными. [`UpdateErrorKindV1`] — только удобная производная проекция: +/// авторитетные IDs и факты отказа находятся в этом enum. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum UpdateErrorV1 { + /// Session создана другой точной owner-эпохой. + OwnerMismatch, + /// Наблюдение не содержит ни одного физического сценария. + EmptyScenarioSet, + /// Один scenario ID повторён внутри наблюдения. + DuplicateScenarioId { + /// Повторённая непрозрачная provenance. + scenario: ScenarioIdV1, + }, + /// Число значений сценария не равно скомпилированной schema. + ScenarioValueCountMismatch { + /// Непрозрачная provenance ошибочного сценария. + scenario: ScenarioIdV1, + /// Число входов в скомпилированной schema. + expected: usize, + /// Фактическое число переданных значений. + actual: usize, + }, + /// Входная ревизия старше уже принятой. + RevisionOutOfOrder { + /// Текущая принятая ревизия. + current: u64, + /// Отвергнутая входная ревизия. + incoming: u64, + }, + /// Та же ревизия содержит другой payload. + RevisionConflict { + /// Ревизия с неоднозначным содержанием. + revision: u64, + }, + /// Ограниченный ресурс закончился до commit. + ResourceExhausted { + /// Фаза, которой не хватило ресурса. + phase: UpdatePhaseV1, + }, + /// Зарегистрированный evaluator не смог выполнить оценку. + EvaluationFailed { + /// Индекс физического сценария в каноническом наблюдении. + case_index: usize, + /// Непрозрачный ID проверяемого ограничения. + constraint: ConstraintIdV1, + /// Непрозрачный ID физического occurrence. + occurrence: OccurrenceIdV1, + /// Точный допущенный appearance context evaluator-вызова. + context: AppearanceContextV1, + /// Точная причина отказа и identity evaluator-а. + source: EvaluatorFailureV1, + }, + /// Нарушен внутренний контракт закрытого Core. + InternalInvariant { + /// Точный факт нарушения; identity выводится через [`UpdateInvariantFailureV1::contract`]. + source: UpdateInvariantFailureV1, + }, +} + +impl UpdateErrorV1 { + /// Возвращает стабильный класс ошибки без потери её payload. + pub const fn kind(&self) -> UpdateErrorKindV1 { + match self { + Self::OwnerMismatch => UpdateErrorKindV1::OwnerMismatch, + Self::EmptyScenarioSet + | Self::DuplicateScenarioId { .. } + | Self::ScenarioValueCountMismatch { .. } => UpdateErrorKindV1::InvalidObservation, + Self::RevisionOutOfOrder { .. } => UpdateErrorKindV1::RevisionOutOfOrder, + Self::RevisionConflict { .. } => UpdateErrorKindV1::RevisionConflict, + Self::ResourceExhausted { .. } => UpdateErrorKindV1::ResourceExhausted, + Self::EvaluationFailed { .. } => UpdateErrorKindV1::EvaluationFailed, + Self::InternalInvariant { .. } => UpdateErrorKindV1::InternalInvariant, + } + } +} + +fn map_joint_order_error(error: FiniteJointOrderErrorV1) -> JointOrderErrorV1 { + match error { + FiniteJointOrderErrorV1::EmptyDomain { dimension } => { + JointOrderErrorV1::EmptyDomain { dimension } + } + FiniteJointOrderErrorV1::CardinalityOverflow => JointOrderErrorV1::CardinalityOverflow, + FiniteJointOrderErrorV1::EmptyOrder => JointOrderErrorV1::EmptyOrder, + FiniteJointOrderErrorV1::TupleArity { + tuple, + expected, + actual, + } => JointOrderErrorV1::TupleArity { + state: tuple, + expected, + actual, + }, + FiniteJointOrderErrorV1::OrdinalOutOfDomain { + tuple, + dimension, + ordinal, + domain_len, + } => JointOrderErrorV1::OrdinalOutOfDomain { + state: tuple, + dimension, + ordinal, + domain_len, + }, + FiniteJointOrderErrorV1::DuplicateTuple { first, duplicate } => { + JointOrderErrorV1::DuplicateTuple { + first_state: first, + duplicate_state: duplicate, + } + } + FiniteJointOrderErrorV1::IncompleteOrder { expected, actual } => { + JointOrderErrorV1::IncompleteOrder { expected, actual } + } + FiniteJointOrderErrorV1::ResourceExhausted => JointOrderErrorV1::ResourceExhausted, + } +} + +fn map_program_compile_error(error: ProgramCompileError) -> CompileErrorV1 { + match error { + ProgramCompileError::DuplicateSource { source } => CompileErrorV1::DuplicateSource { + source: SourceIdV1::from_core(source), + }, + ProgramCompileError::DuplicateTarget { target } => CompileErrorV1::DuplicateTarget { + target: TargetIdV1::from_core(target), + }, + ProgramCompileError::MissingTargetSource { target, source } => { + CompileErrorV1::MissingTargetSource { + target: TargetIdV1::from_core(target), + source: SourceIdV1::from_core(source), + } + } + ProgramCompileError::DuplicateOpacityInput { input } => { + CompileErrorV1::DuplicateOpacityInput { + input: OpacityInputIdV1::from_core(input), + } + } + ProgramCompileError::DuplicateSurfaceInputPort { input } => { + CompileErrorV1::DuplicateSurfaceInputPort { + input: SurfaceInputPortIdV1::from_core(input), + } + } + ProgramCompileError::UnusedSurfaceInputPort { input } => { + CompileErrorV1::UnusedSurfaceInputPort { + input: SurfaceInputPortIdV1::from_core(input), + } + } + ProgramCompileError::DuplicateSurfaceInputBinding { + input, + first, + duplicate, + } => CompileErrorV1::DuplicateSurfaceInputBinding { + input: SurfaceInputPortIdV1::from_core(input), + first: SurfaceIdV1::from_core(first), + duplicate: SurfaceIdV1::from_core(duplicate), + }, + ProgramCompileError::DuplicatePaint { paint } => CompileErrorV1::DuplicatePaint { + paint: PaintIdV1::from_core(paint), + }, + ProgramCompileError::DuplicateSurface { surface } => CompileErrorV1::DuplicateSurface { + surface: SurfaceIdV1::from_core(surface), + }, + ProgramCompileError::DuplicateOccurrence { occurrence } => { + CompileErrorV1::DuplicateOccurrence { + occurrence: OccurrenceIdV1::from_core(occurrence), + } + } + ProgramCompileError::MissingPaintTarget { paint, target } => { + CompileErrorV1::MissingPaintTarget { + paint: PaintIdV1::from_core(paint), + target: TargetIdV1::from_core(target), + } + } + ProgramCompileError::MissingPaintSource { paint, source } => { + CompileErrorV1::MissingPaintSource { + paint: PaintIdV1::from_core(paint), + source: PaintIdV1::from_core(source), + } + } + ProgramCompileError::MissingPaintOpacityInput { paint, input } => { + CompileErrorV1::MissingPaintOpacityInput { + paint: PaintIdV1::from_core(paint), + input: OpacityInputIdV1::from_core(input), + } + } + ProgramCompileError::MissingSurfaceInputPort { surface, input } => { + CompileErrorV1::MissingSurfaceInputPort { + surface: SurfaceIdV1::from_core(surface), + input: SurfaceInputPortIdV1::from_core(input), + } + } + ProgramCompileError::MissingSurfaceOccurrence { + surface, + occurrence, + } => CompileErrorV1::MissingSurfaceOccurrence { + surface: SurfaceIdV1::from_core(surface), + occurrence: OccurrenceIdV1::from_core(occurrence), + }, + ProgramCompileError::MissingOccurrencePaint { occurrence, paint } => { + CompileErrorV1::MissingOccurrencePaint { + occurrence: OccurrenceIdV1::from_core(occurrence), + paint: PaintIdV1::from_core(paint), + } + } + ProgramCompileError::MissingOccurrenceBackdrop { + occurrence, + surface, + } => CompileErrorV1::MissingOccurrenceBackdrop { + occurrence: OccurrenceIdV1::from_core(occurrence), + surface: SurfaceIdV1::from_core(surface), + }, + ProgramCompileError::PaintCycle { paints } => { + CompileErrorV1::PaintCycle(PaintCycleV1 { paints }) + } + ProgramCompileError::RenderCycle { + surfaces, + occurrences, + } => CompileErrorV1::RenderCycle(RenderCycleV1 { + surfaces, + occurrences, + }), + 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), + candidate: TargetCandidateIdV1::from_core(candidate), + } + } + ProgramCompileError::DuplicateTargetCandidateSignal { + target, + first, + duplicate, + signal, + } => CompileErrorV1::DuplicateTargetCandidateSignal { + target: TargetIdV1::from_core(target), + first: TargetCandidateIdV1::from_core(first), + duplicate: TargetCandidateIdV1::from_core(duplicate), + encoded_srgb8: signal.srgb8(), + }, + ProgramCompileError::UnconstrainedTarget { target } => { + CompileErrorV1::UnconstrainedTarget { + target: TargetIdV1::from_core(target), + } + } + ProgramCompileError::DisconnectedFiniteTargets => CompileErrorV1::DisconnectedFiniteTargets, + ProgramCompileError::UnassessedOutput { output, paint } => { + CompileErrorV1::UnassessedOutput { + output: OutputSlotIdV1::from_core(output), + paint: PaintIdV1::from_core(paint), + } + } + ProgramCompileError::MissingJointSelection => CompileErrorV1::MissingJointSelection, + ProgramCompileError::JointSelectionWithoutTargets => { + CompileErrorV1::JointSelectionWithoutTargets + } + ProgramCompileError::JointStateDuplicateTarget { state, target } => { + CompileErrorV1::JointStateDuplicateTarget { + state, + target: TargetIdV1::from_core(target), + } + } + ProgramCompileError::JointStateMissingTarget { state, target } => { + CompileErrorV1::JointStateMissingTarget { + state, + target: TargetIdV1::from_core(target), + } + } + ProgramCompileError::JointStateUnknownTarget { state, target } => { + CompileErrorV1::JointStateUnknownTarget { + state, + target: TargetIdV1::from_core(target), + } + } + ProgramCompileError::JointStateUnknownCandidate { + state, + target, + candidate, + } => CompileErrorV1::JointStateUnknownCandidate { + state, + target: TargetIdV1::from_core(target), + candidate: TargetCandidateIdV1::from_core(candidate), + }, + ProgramCompileError::InvalidJointOrder(error) => { + CompileErrorV1::InvalidJointOrder(map_joint_order_error(error)) + } + ProgramCompileError::EmptyObservationGroup { .. } => { + CompileErrorV1::EmptySurfaceInputPortSet + } + ProgramCompileError::EmptyOccurrenceSet => CompileErrorV1::EmptyOccurrenceSet, + ProgramCompileError::EmptyConstraintSet => CompileErrorV1::EmptyConstraintSet, + ProgramCompileError::EmptyOutputSet => CompileErrorV1::EmptyOutputSet, + ProgramCompileError::DuplicateConstraint { constraint } => { + CompileErrorV1::DuplicateConstraint { + constraint: ConstraintIdV1::from_core(constraint), + } + } + ProgramCompileError::MissingConstraintOccurrence { + constraint, + occurrence, + } => CompileErrorV1::MissingConstraintOccurrence { + constraint: ConstraintIdV1::from_core(constraint), + occurrence: OccurrenceIdV1::from_core(occurrence), + }, + ProgramCompileError::DuplicateOutputSlot { output } => { + CompileErrorV1::DuplicateOutputSlot { + output: OutputSlotIdV1::from_core(output), + } + } + ProgramCompileError::MissingOutputPaint { output, paint } => { + CompileErrorV1::MissingOutputPaint { + output: OutputSlotIdV1::from_core(output), + paint: PaintIdV1::from_core(paint), + } + } + ProgramCompileError::ResourceExhausted => CompileErrorV1::ResourceExhausted, + ProgramCompileError::InternalInvariant => CompileErrorV1::InternalInvariant, + } +} + +const fn map_colorimetric_frame(frame: ColorimetricFrameId) -> ColorimetricFrameV1 { + match ( + frame.observer(), + frame.reference_white(), + frame.scale(), + frame.release(), + ) { + ( + ObserverProfileId::Cie1931TwoDegreeV1, + ReferenceWhiteId::Iec61966D65ChromaticityV1, + TristimulusScale::RelativeY1, + ColorimetricFrameReleaseId::XyzV1, + ) => ColorimetricFrameV1::Iec61966Srgb8D65XyzRelativeY1V1, + #[cfg(test)] + ( + ObserverProfileId::Cie1931TwoDegreeV1, + ReferenceWhiteId::Iec61966D65ChromaticityV1, + TristimulusScale::RelativeY1, + ColorimetricFrameReleaseId::MutationSentinelV1, + ) => ColorimetricFrameV1::MutationSentinelV1, + } +} + +const fn map_numeric_domain_error(error: NumericDomainError) -> NumericDomainErrorV1 { + match error { + NumericDomainError::NonFinite => NumericDomainErrorV1::NonFinite, + NumericDomainError::Negative => NumericDomainErrorV1::Negative, + NumericDomainError::NotPositive => NumericDomainErrorV1::NotPositive, + NumericDomainError::AboveOne => NumericDomainErrorV1::AboveOne, + NumericDomainError::HueOutOfRange => NumericDomainErrorV1::HueOutOfRange, + } +} + +const fn map_tristimulus_component( + component: CoreTristimulusComponentV1, +) -> TristimulusComponentV1 { + match component { + CoreTristimulusComponentV1::X => TristimulusComponentV1::X, + CoreTristimulusComponentV1::Y => TristimulusComponentV1::Y, + CoreTristimulusComponentV1::Z => TristimulusComponentV1::Z, + } +} + +const fn map_tristimulus_domain_error( + error: TristimulusDomainErrorV1, +) -> (TristimulusComponentV1, NumericDomainErrorV1) { + ( + map_tristimulus_component(error.component()), + map_numeric_domain_error(error.reason()), + ) +} + +fn map_modeled_occurrence_error( + error: ModeledLcsOccurrenceFormationErrorV1, +) -> ModeledOccurrenceFailureV1 { + match error { + ModeledLcsOccurrenceFormationErrorV1::Tristimulus(source) => { + let (component, reason) = map_tristimulus_domain_error(source); + ModeledOccurrenceFailureV1::Tristimulus { component, reason } + } + ModeledLcsOccurrenceFormationErrorV1::Formation( + OccurrenceFormationError::FrameMismatch { stimulus, context }, + ) => ModeledOccurrenceFailureV1::FrameMismatch { + stimulus: map_colorimetric_frame(stimulus), + context: map_colorimetric_frame(context), + }, + ModeledLcsOccurrenceFormationErrorV1::ProvenanceReplayFailed(source) => { + let (component, reason) = map_tristimulus_domain_error(source); + ModeledOccurrenceFailureV1::ProvenanceReplayFailed { component, reason } + } + ModeledLcsOccurrenceFormationErrorV1::RecordedSampleDoesNotReplay { + recorded, + replayed, + } => ModeledOccurrenceFailureV1::RecordedSampleDoesNotReplay { + recorded: TristimulusSampleV1::from_core(recorded), + replayed: TristimulusSampleV1::from_core(replayed), + }, + ModeledLcsOccurrenceFormationErrorV1::OccurrenceSampleMismatch { + occurrence, + modeled, + } => ModeledOccurrenceFailureV1::OccurrenceSampleMismatch { + occurrence: TristimulusSampleV1::from_core(occurrence), + modeled: TristimulusSampleV1::from_core(modeled), + }, + } +} + +fn map_observation_schema_mismatch( + error: ObservationSchemaMismatchV1, +) -> ObservationBindingFailureV1 { + let (case_index, binding_index, expected, actual) = error.into_parts(); + ObservationBindingFailureV1::SchemaMismatch { + case_index, + binding_index, + expected: expected.map(SurfaceInputPortIdV1::from_core), + actual: actual.map(SurfaceInputPortIdV1::from_core), + } +} + +fn map_session_update_error(error: SessionUpdateError) -> UpdateErrorV1 { + match error { + // A borrowed matching owner keeps the exact Rc generation alive for + // the whole transaction; expiry here is therefore an internal breach. + SessionUpdateError::OwnerExpired => UpdateErrorV1::InternalInvariant { + source: UpdateInvariantFailureV1::OwnerAuthority, + }, + SessionUpdateError::Observation(error) => map_observation_error(error), + SessionUpdateError::Plan(error) => map_plan_error(error), + SessionUpdateError::EvidenceBindingInvariant => UpdateErrorV1::InternalInvariant { + source: UpdateInvariantFailureV1::EvidenceBinding, + }, + } +} + +fn map_observation_error(error: ObservationError) -> UpdateErrorV1 { + match error { + ObservationError::EmptyCompiledSurfaceInputSchema => UpdateErrorV1::InternalInvariant { + source: UpdateInvariantFailureV1::ObservationBinding { + source: ObservationBindingFailureV1::EmptyCompiledSurfaceInputSchema, + }, + }, + ObservationError::DuplicateCompiledSurfaceInputPort { input } => { + UpdateErrorV1::InternalInvariant { + source: UpdateInvariantFailureV1::ObservationBinding { + source: ObservationBindingFailureV1::DuplicateCompiledSurfaceInputPort { + input: SurfaceInputPortIdV1::from_core(input), + }, + }, + } + } + ObservationError::StreamMismatch { expected, actual } => UpdateErrorV1::InternalInvariant { + source: UpdateInvariantFailureV1::ObservationBinding { + source: ObservationBindingFailureV1::StreamMismatch { + expected: StreamIdV1::from_core(expected), + actual: StreamIdV1::from_core(actual), + }, + }, + }, + ObservationError::EmptyScenarioSet => UpdateErrorV1::EmptyScenarioSet, + ObservationError::DuplicateScenarioId { scenario } => UpdateErrorV1::DuplicateScenarioId { + scenario: ScenarioIdV1::from_core(scenario), + }, + ObservationError::SchemaOrderedValueCountMismatch { + scenario, + expected, + actual, + } => UpdateErrorV1::ScenarioValueCountMismatch { + scenario: ScenarioIdV1::from_core(scenario), + expected, + actual, + }, + ObservationError::DuplicateSurfaceInputBinding { scenario, input } => { + UpdateErrorV1::InternalInvariant { + source: UpdateInvariantFailureV1::ObservationBinding { + source: ObservationBindingFailureV1::DuplicateSurfaceInputBinding { + scenario: ScenarioIdV1::from_core(scenario), + input: SurfaceInputPortIdV1::from_core(input), + }, + }, + } + } + ObservationError::MissingSurfaceInputBinding { scenario, input } => { + UpdateErrorV1::InternalInvariant { + source: UpdateInvariantFailureV1::ObservationBinding { + source: ObservationBindingFailureV1::MissingSurfaceInputBinding { + scenario: ScenarioIdV1::from_core(scenario), + input: SurfaceInputPortIdV1::from_core(input), + }, + }, + } + } + ObservationError::UnexpectedSurfaceInputBinding { scenario, input } => { + UpdateErrorV1::InternalInvariant { + source: UpdateInvariantFailureV1::ObservationBinding { + source: ObservationBindingFailureV1::UnexpectedSurfaceInputBinding { + scenario: ScenarioIdV1::from_core(scenario), + input: SurfaceInputPortIdV1::from_core(input), + }, + }, + } + } + ObservationError::RevisionOutOfOrder { current, incoming } => { + UpdateErrorV1::RevisionOutOfOrder { + current: current.value(), + incoming: incoming.value(), + } + } + ObservationError::RevisionConflict { revision } => UpdateErrorV1::RevisionConflict { + revision: revision.value(), + }, + ObservationError::ResourceExhausted => UpdateErrorV1::ResourceExhausted { + phase: UpdatePhaseV1::ObservationAdmission, + }, + } +} + +fn map_plan_error(error: CoreProgramPlanErrorV1) -> UpdateErrorV1 { + match error { + ProgramSessionEvaluationError::ObservationSchemaMismatch(source) => { + UpdateErrorV1::InternalInvariant { + source: UpdateInvariantFailureV1::ObservationBinding { + source: map_observation_schema_mismatch(source), + }, + } + } + ProgramSessionEvaluationError::ResourceExhausted => UpdateErrorV1::ResourceExhausted { + phase: UpdatePhaseV1::ProgramEvaluation, + }, + ProgramSessionEvaluationError::Evaluator { + case_index, + constraint, + occurrence, + context, + source, + } => match map_evaluator_error(source) { + Ok(source) => UpdateErrorV1::EvaluationFailed { + case_index, + constraint: ConstraintIdV1::from_core(constraint), + occurrence: OccurrenceIdV1::from_core(occurrence), + context: AppearanceContextV1::from_core(context), + source, + }, + Err(source) => UpdateErrorV1::InternalInvariant { + source: UpdateInvariantFailureV1::EvaluatorProtocol { + case_index, + constraint: ConstraintIdV1::from_core(constraint), + occurrence: OccurrenceIdV1::from_core(occurrence), + context: AppearanceContextV1::from_core(context), + source, + }, + }, + }, + ProgramSessionEvaluationError::ProgramTargetBinding { + case_index, + constraint, + occurrence, + context, + physical, + modeled, + } => UpdateErrorV1::InternalInvariant { + source: UpdateInvariantFailureV1::PhysicalModeledBinding { + case_index, + constraint: ConstraintIdV1::from_core(constraint), + occurrence: OccurrenceIdV1::from_core(occurrence), + context: AppearanceContextV1::from_core(context), + physical, + modeled, + }, + }, + ProgramSessionEvaluationError::ModeledOccurrence { + case_index, + occurrence, + context, + source, + } => UpdateErrorV1::InternalInvariant { + source: UpdateInvariantFailureV1::ModeledOccurrenceFormation { + case_index, + occurrence: OccurrenceIdV1::from_core(occurrence), + context: AppearanceContextV1::from_core(context), + source: map_modeled_occurrence_error(source), + }, + }, + ProgramSessionEvaluationError::OutputVariesAcrossCases { + output, + first_case, + actual_case, + } => UpdateErrorV1::InternalInvariant { + source: UpdateInvariantFailureV1::OutputCaseInvariance { + output: OutputSlotIdV1::from_core(output), + first_case, + actual_case, + }, + }, + ProgramSessionEvaluationError::FinalRecheckViolation { + state_index, + case_index, + constraint, + target, + hard_violation_count, + } => UpdateErrorV1::InternalInvariant { + source: UpdateInvariantFailureV1::SelectionRecheck { + state_index, + case_index, + constraint: ConstraintIdV1::from_core(constraint), + occurrence: OccurrenceIdV1::from_core(target), + hard_violation_count, + }, + }, + ProgramSessionEvaluationError::InternalInvariant => UpdateErrorV1::InternalInvariant { + source: UpdateInvariantFailureV1::ProgramEvaluation, + }, + } +} + +fn map_evaluator_error( + error: CoreProgramEvaluatorErrorV1, +) -> Result { + match error { + CoreProgramEvaluatorErrorV1::ExactSrgb8(source) => match source {}, + CoreProgramEvaluatorErrorV1::Wcag22Srgb8(source) => { + let profile_id = wcag22_profile_v1().profile_id; + match source { + ApplicableWcag22EvaluationErrorV1::Kernel( + source @ (Wcag22EvaluationErrorV1::ArtifactInvariantViolation { .. } + | Wcag22EvaluationErrorV1::EvidenceRegistryMismatch(_)), + ) => Ok(EvaluatorFailureV1::Wcag22Srgb8 { profile_id, source }), + ApplicableWcag22EvaluationErrorV1::Kernel(source) => { + Err(EvaluatorProtocolFailureV1::Wcag22Kernel { profile_id, source }) + } + ApplicableWcag22EvaluationErrorV1::ReportOnly { + profile_id, + declaration, + } => Err(EvaluatorProtocolFailureV1::Wcag22ReportOnly { + profile_id, + declaration, + }), + ApplicableWcag22EvaluationErrorV1::CriterionMismatch { + requested, + evaluated, + } => Err(EvaluatorProtocolFailureV1::Wcag22CriterionMismatch { + requested, + evaluated, + }), + } + } + } +} + +#[cfg(test)] +mod update_error_projection_tests { + use super::*; + use crate::wcag22::Wcag22ClientDeclaredNotApplicableV1; + + fn context() -> AppearanceContextV1 { + AppearanceContextV1::try_new(64.0, 0.2, SurroundV1::Average).unwrap() + } + + fn map_wcag_error(source: ApplicableWcag22EvaluationErrorV1) -> UpdateErrorV1 { + let context = context(); + map_plan_error(ProgramSessionEvaluationError::Evaluator { + case_index: 7, + constraint: ConstraintId::new(11), + occurrence: OccurrenceId::new(13), + context: context.0, + source: CoreProgramEvaluatorErrorV1::Wcag22Srgb8(source), + }) + } + + #[test] + fn resource_exhaustion_retains_its_exact_update_phase() { + let observation = map_observation_error(ObservationError::ResourceExhausted); + assert_eq!(observation.kind(), UpdateErrorKindV1::ResourceExhausted); + assert_eq!( + observation, + UpdateErrorV1::ResourceExhausted { + phase: UpdatePhaseV1::ObservationAdmission, + } + ); + let evaluation = map_plan_error(ProgramSessionEvaluationError::ResourceExhausted); + assert_eq!(evaluation.kind(), UpdateErrorKindV1::ResourceExhausted); + assert_eq!( + evaluation, + UpdateErrorV1::ResourceExhausted { + phase: UpdatePhaseV1::ProgramEvaluation, + } + ); + } + + #[test] + fn evaluator_artifact_failure_retains_every_actionable_fact() { + let error = map_wcag_error(ApplicableWcag22EvaluationErrorV1::Kernel( + Wcag22EvaluationErrorV1::ArtifactInvariantViolation { + criterion: Wcag22CriterionV1::Sc143TextLargeScale, + foreground: [1, 2, 3], + background: [4, 5, 6], + }, + )); + assert_eq!(error.kind(), UpdateErrorKindV1::EvaluationFailed); + assert_eq!( + error, + UpdateErrorV1::EvaluationFailed { + case_index: 7, + constraint: ConstraintIdV1::new(11), + occurrence: OccurrenceIdV1::new(13), + context: context(), + source: EvaluatorFailureV1::Wcag22Srgb8 { + profile_id: wcag22_profile_v1().profile_id, + source: Wcag22EvaluationErrorV1::ArtifactInvariantViolation { + criterion: Wcag22CriterionV1::Sc143TextLargeScale, + foreground: [1, 2, 3], + background: [4, 5, 6], + }, + }, + } + ); + + let error = map_wcag_error(ApplicableWcag22EvaluationErrorV1::Kernel( + Wcag22EvaluationErrorV1::EvidenceRegistryMismatch("registry-vs-proof".into()), + )); + assert_eq!(error.kind(), UpdateErrorKindV1::EvaluationFailed); + let UpdateErrorV1::EvaluationFailed { + source: + EvaluatorFailureV1::Wcag22Srgb8 { + profile_id, + source: Wcag22EvaluationErrorV1::EvidenceRegistryMismatch(message), + }, + .. + } = error + else { + panic!("registry mismatch must remain an evaluator failure"); + }; + assert_eq!(profile_id, wcag22_profile_v1().profile_id); + assert_eq!(message, "registry-vs-proof"); + } + + #[test] + fn impossible_wcag_protocol_states_are_not_misreported_as_colour_failures() { + let profile_id = wcag22_profile_v1().profile_id; + let declaration = Wcag22ClientDeclaredNotApplicableV1::try_new("client-scope").unwrap(); + let cases = [ + ( + ApplicableWcag22EvaluationErrorV1::Kernel(Wcag22EvaluationErrorV1::InvalidSrgb8 { + field: "foreground", + reason: "typed Program cannot create this".into(), + }), + EvaluatorProtocolFailureV1::Wcag22Kernel { + profile_id, + source: Wcag22EvaluationErrorV1::InvalidSrgb8 { + field: "foreground", + reason: "typed Program cannot create this".into(), + }, + }, + ), + ( + ApplicableWcag22EvaluationErrorV1::Kernel( + Wcag22EvaluationErrorV1::EmptyNotApplicableReason, + ), + EvaluatorProtocolFailureV1::Wcag22Kernel { + profile_id, + source: Wcag22EvaluationErrorV1::EmptyNotApplicableReason, + }, + ), + ( + ApplicableWcag22EvaluationErrorV1::ReportOnly { + profile_id, + declaration: declaration.clone(), + }, + EvaluatorProtocolFailureV1::Wcag22ReportOnly { + profile_id, + declaration, + }, + ), + ( + ApplicableWcag22EvaluationErrorV1::CriterionMismatch { + requested: Wcag22CriterionV1::Sc143TextDefault, + evaluated: Wcag22CriterionV1::Sc1411UiComponentOrState, + }, + EvaluatorProtocolFailureV1::Wcag22CriterionMismatch { + requested: Wcag22CriterionV1::Sc143TextDefault, + evaluated: Wcag22CriterionV1::Sc1411UiComponentOrState, + }, + ), + ]; + for (source, expected) in cases { + let error = map_wcag_error(source); + assert_eq!(error.kind(), UpdateErrorKindV1::InternalInvariant); + let expected = UpdateInvariantFailureV1::EvaluatorProtocol { + case_index: 7, + constraint: ConstraintIdV1::new(11), + occurrence: OccurrenceIdV1::new(13), + context: context(), + source: expected, + }; + assert_eq!(expected.contract(), UpdateInvariantV1::EvaluatorProtocol); + assert_eq!(error, UpdateErrorV1::InternalInvariant { source: expected }); + } + } + + #[test] + fn every_observation_binding_failure_retains_its_exact_payload() { + let cases = [ + ( + ObservationError::EmptyCompiledSurfaceInputSchema, + ObservationBindingFailureV1::EmptyCompiledSurfaceInputSchema, + ), + ( + ObservationError::DuplicateCompiledSurfaceInputPort { + input: SurfaceInputPortId::new(3), + }, + ObservationBindingFailureV1::DuplicateCompiledSurfaceInputPort { + input: SurfaceInputPortIdV1::new(3), + }, + ), + ( + ObservationError::StreamMismatch { + expected: ObservationStreamId::new(5), + actual: ObservationStreamId::new(7), + }, + ObservationBindingFailureV1::StreamMismatch { + expected: StreamIdV1::from_core(ObservationStreamId::new(5)), + actual: StreamIdV1::from_core(ObservationStreamId::new(7)), + }, + ), + ( + ObservationError::DuplicateSurfaceInputBinding { + scenario: ScenarioId::new(11), + input: SurfaceInputPortId::new(13), + }, + ObservationBindingFailureV1::DuplicateSurfaceInputBinding { + scenario: ScenarioIdV1::from_core(ScenarioId::new(11)), + input: SurfaceInputPortIdV1::new(13), + }, + ), + ( + ObservationError::MissingSurfaceInputBinding { + scenario: ScenarioId::new(17), + input: SurfaceInputPortId::new(19), + }, + ObservationBindingFailureV1::MissingSurfaceInputBinding { + scenario: ScenarioIdV1::from_core(ScenarioId::new(17)), + input: SurfaceInputPortIdV1::new(19), + }, + ), + ( + ObservationError::UnexpectedSurfaceInputBinding { + scenario: ScenarioId::new(23), + input: SurfaceInputPortId::new(29), + }, + ObservationBindingFailureV1::UnexpectedSurfaceInputBinding { + scenario: ScenarioIdV1::from_core(ScenarioId::new(23)), + input: SurfaceInputPortIdV1::new(29), + }, + ), + ]; + + for (source, expected) in cases { + let error = map_observation_error(source); + assert_eq!(error.kind(), UpdateErrorKindV1::InternalInvariant); + assert_eq!( + error, + UpdateErrorV1::InternalInvariant { + source: UpdateInvariantFailureV1::ObservationBinding { source: expected }, + } + ); + } + } + + #[test] + fn every_modeled_occurrence_failure_has_an_isomorphic_public_witness() { + use crate::lcs_occurrence::{ + MUTATION_SENTINEL_XYZ_FRAME_V1, ModeledLcsOccurrenceFormationErrorV1, + OccurrenceFormationError, TristimulusSample, + }; + + let domain = TristimulusSample::try_from_xyz_for_test( + [f64::NAN, 0.2, 0.3], + IEC_SRGB_D65_XYZ_FRAME_V1, + ) + .unwrap_err(); + let recorded = + TristimulusSample::try_from_xyz_for_test([0.1, 0.2, 0.3], IEC_SRGB_D65_XYZ_FRAME_V1) + .unwrap(); + let replayed = + TristimulusSample::try_from_xyz_for_test([0.1, 0.2, 0.4], IEC_SRGB_D65_XYZ_FRAME_V1) + .unwrap(); + + let cases = [ + ( + ModeledLcsOccurrenceFormationErrorV1::Tristimulus(domain), + ModeledOccurrenceFailureV1::Tristimulus { + component: TristimulusComponentV1::X, + reason: NumericDomainErrorV1::NonFinite, + }, + ), + ( + ModeledLcsOccurrenceFormationErrorV1::Formation( + OccurrenceFormationError::FrameMismatch { + stimulus: IEC_SRGB_D65_XYZ_FRAME_V1, + context: MUTATION_SENTINEL_XYZ_FRAME_V1, + }, + ), + ModeledOccurrenceFailureV1::FrameMismatch { + stimulus: ColorimetricFrameV1::Iec61966Srgb8D65XyzRelativeY1V1, + context: ColorimetricFrameV1::MutationSentinelV1, + }, + ), + ( + ModeledLcsOccurrenceFormationErrorV1::ProvenanceReplayFailed(domain), + ModeledOccurrenceFailureV1::ProvenanceReplayFailed { + component: TristimulusComponentV1::X, + reason: NumericDomainErrorV1::NonFinite, + }, + ), + ( + ModeledLcsOccurrenceFormationErrorV1::RecordedSampleDoesNotReplay { + recorded, + replayed, + }, + ModeledOccurrenceFailureV1::RecordedSampleDoesNotReplay { + recorded: TristimulusSampleV1::from_core(recorded), + replayed: TristimulusSampleV1::from_core(replayed), + }, + ), + ( + ModeledLcsOccurrenceFormationErrorV1::OccurrenceSampleMismatch { + occurrence: recorded, + modeled: replayed, + }, + ModeledOccurrenceFailureV1::OccurrenceSampleMismatch { + occurrence: TristimulusSampleV1::from_core(recorded), + modeled: TristimulusSampleV1::from_core(replayed), + }, + ), + ]; + + for (source, expected) in cases { + assert_eq!(map_modeled_occurrence_error(source), expected); + } + } + + #[test] + fn every_unreachable_core_failure_keeps_its_subject_and_witness_facts() { + use crate::lcs_occurrence::{ + MUTATION_SENTINEL_XYZ_FRAME_V1, ModeledLcsOccurrenceFormationErrorV1, + OccurrenceFormationError, + }; + use crate::observation::ObservationSchemaMismatchV1; + + let assert_invariant = |error: UpdateErrorV1, source: UpdateInvariantFailureV1| { + assert_eq!(error.kind(), UpdateErrorKindV1::InternalInvariant); + assert_eq!( + error, + UpdateErrorV1::InternalInvariant { + source: source.clone(), + } + ); + assert_eq!( + source.contract(), + match source { + UpdateInvariantFailureV1::OwnerAuthority => { + UpdateInvariantV1::OwnerAuthority + } + UpdateInvariantFailureV1::ObservationBinding { .. } => { + UpdateInvariantV1::ObservationBinding + } + UpdateInvariantFailureV1::EvidenceBinding => { + UpdateInvariantV1::EvidenceBinding + } + UpdateInvariantFailureV1::EvaluatorProtocol { .. } => { + UpdateInvariantV1::EvaluatorProtocol + } + UpdateInvariantFailureV1::PhysicalModeledBinding { .. } => { + UpdateInvariantV1::PhysicalModeledBinding + } + UpdateInvariantFailureV1::ModeledOccurrenceFormation { .. } => { + UpdateInvariantV1::ModeledOccurrenceFormation + } + UpdateInvariantFailureV1::OutputCaseInvariance { .. } => { + UpdateInvariantV1::OutputCaseInvariance + } + UpdateInvariantFailureV1::SelectionRecheck { .. } => { + UpdateInvariantV1::SelectionRecheck + } + UpdateInvariantFailureV1::ProgramEvaluation => { + UpdateInvariantV1::ProgramEvaluation + } + } + ); + }; + + assert_invariant( + map_session_update_error(SessionUpdateError::OwnerExpired), + UpdateInvariantFailureV1::OwnerAuthority, + ); + assert_invariant( + map_session_update_error(SessionUpdateError::EvidenceBindingInvariant), + UpdateInvariantFailureV1::EvidenceBinding, + ); + assert_invariant( + map_observation_error(ObservationError::EmptyCompiledSurfaceInputSchema), + UpdateInvariantFailureV1::ObservationBinding { + source: ObservationBindingFailureV1::EmptyCompiledSurfaceInputSchema, + }, + ); + + assert_invariant( + map_plan_error(ProgramSessionEvaluationError::ObservationSchemaMismatch( + ObservationSchemaMismatchV1::new(2, 3, None, None), + )), + UpdateInvariantFailureV1::ObservationBinding { + source: ObservationBindingFailureV1::SchemaMismatch { + case_index: 2, + binding_index: 3, + expected: None, + actual: None, + }, + }, + ); + assert_invariant( + map_plan_error(ProgramSessionEvaluationError::ProgramTargetBinding { + case_index: 2, + constraint: ConstraintId::new(3), + occurrence: OccurrenceId::new(4), + context: context().0, + physical: Srgb8::new([1, 2, 3]), + modeled: Srgb8::new([3, 2, 1]), + }), + UpdateInvariantFailureV1::PhysicalModeledBinding { + case_index: 2, + constraint: ConstraintIdV1::new(3), + occurrence: OccurrenceIdV1::new(4), + context: context(), + physical: Srgb8::new([1, 2, 3]), + modeled: Srgb8::new([3, 2, 1]), + }, + ); + assert_invariant( + map_plan_error(ProgramSessionEvaluationError::ModeledOccurrence { + case_index: 2, + occurrence: OccurrenceId::new(4), + context: context().0, + source: ModeledLcsOccurrenceFormationErrorV1::Formation( + OccurrenceFormationError::FrameMismatch { + stimulus: IEC_SRGB_D65_XYZ_FRAME_V1, + context: MUTATION_SENTINEL_XYZ_FRAME_V1, + }, + ), + }), + UpdateInvariantFailureV1::ModeledOccurrenceFormation { + case_index: 2, + occurrence: OccurrenceIdV1::new(4), + context: context(), + source: ModeledOccurrenceFailureV1::FrameMismatch { + stimulus: ColorimetricFrameV1::Iec61966Srgb8D65XyzRelativeY1V1, + context: ColorimetricFrameV1::MutationSentinelV1, + }, + }, + ); + assert_invariant( + map_plan_error(ProgramSessionEvaluationError::OutputVariesAcrossCases { + output: OutputSlotId::new(5), + first_case: 1, + actual_case: 2, + }), + UpdateInvariantFailureV1::OutputCaseInvariance { + output: OutputSlotIdV1::new(5), + first_case: 1, + actual_case: 2, + }, + ); + assert_invariant( + map_plan_error(ProgramSessionEvaluationError::FinalRecheckViolation { + state_index: 1, + case_index: 2, + constraint: ConstraintId::new(3), + target: OccurrenceId::new(4), + hard_violation_count: 1, + }), + UpdateInvariantFailureV1::SelectionRecheck { + state_index: 1, + case_index: 2, + constraint: ConstraintIdV1::new(3), + occurrence: OccurrenceIdV1::new(4), + hard_violation_count: 1, + }, + ); + assert_invariant( + map_plan_error(ProgramSessionEvaluationError::InternalInvariant), + UpdateInvariantFailureV1::ProgramEvaluation, + ); + } +} + +#[cfg(test)] +mod compile_error_projection_tests { + use super::*; + + #[test] + fn operation_scope_is_a_zero_sized_borrow_marker() { + assert_eq!(core::mem::size_of::>(), 0); + } + + #[test] + fn nested_joint_resource_exhaustion_keeps_its_exact_reason_and_site_kind() { + let error = map_program_compile_error(ProgramCompileError::InvalidJointOrder( + FiniteJointOrderErrorV1::ResourceExhausted, + )); + + assert_eq!(error.kind(), CompileErrorKindV1::InvalidJointOrder); + assert_eq!( + error, + CompileErrorV1::InvalidJointOrder(JointOrderErrorV1::ResourceExhausted) + ); + } +} diff --git a/crates/labcolors-core/src/program_mixed_evaluator_tests.rs b/crates/labcolors-core/src/program_mixed_evaluator_tests.rs index cd131e49..3eef0ab1 100644 --- a/crates/labcolors-core/src/program_mixed_evaluator_tests.rs +++ b/crates/labcolors-core/src/program_mixed_evaluator_tests.rs @@ -15,14 +15,10 @@ use crate::observation::{ ObservationGroupId, ObservationPayloadInput, ObservationStreamId, ObservationUpdateInput, ObservedScenarioSetInput, Revision, ScenarioId, ScenarioInput, SurfaceInputBinding, }; -use crate::package_bridge::{ - PackageProgramAccessErrorV1, PackageProgramAssessmentV1, PackageProgramCertificateV1, - PackageProgramConflictCellV1, PackageProgramModeledPointV1, PackageProgramObservationHeadV1, - PackageProgramObservationV1, PackageProgramOperationV1, PackageProgramOutputSlotIdV1, - PackageProgramOwnerV1, PackageProgramPhysicalPointV1, PackageProgramProjectionV1, - PackageProgramScenarioV1, PackageProgramSignalV1, PackageProgramStateKindV1, - PackageProgramSurroundV1, PackageProgramUpdateErrorKindV1, PackageProgramUpdateV1, - PackageProgramVerdictV1, PackageProgramVerifiedCellV1, +use crate::program::{ + AccessErrorV1, AssessmentV1, CertificateV1, ConflictCellV1, ModeledPointV1, ObservationHeadV1, + ObservationV1, OperationV1, OutputSlotIdV1, OwnerV1, PhysicalPointV1, ProjectionV1, ScenarioV1, + SignalV1, StateKindV1, SurroundV1, UpdateErrorKindV1, UpdateV1, VerdictV1, VerifiedCellV1, }; use crate::program_session::{ CORE_PROGRAM_ASSESSMENT_CALLS, CompiledCoreProgramV1, CompositionProfile, ConstraintId, @@ -199,14 +195,14 @@ fn fixed_translucent_program() -> CompiledCoreProgramV1 { .unwrap() } -fn assert_package_observation_matches_core( - package: PackageProgramObservationV1<'_>, +fn assert_public_observation_matches_core( + public: ObservationV1<'_>, core: &crate::observation::RevisionBoundObservationV1, ) { - assert_eq!(package.stream().value(), core.stream().value()); - assert_eq!(package.revision(), core.revision().value()); + assert_eq!(public.stream().value(), core.stream().value()); + assert_eq!(public.revision(), core.revision().value()); assert_eq!( - package + public .surface_input_ports() .map(|port| port.value()) .collect::>(), @@ -216,13 +212,13 @@ fn assert_package_observation_matches_core( .collect::>(), ); - let package_cases = package + let public_cases = public .physical_cases() .map(|case| { let values = case .values() .map(|value| match value { - PackageProgramSignalV1::Iec61966Srgb8D65(value) => value, + SignalV1::Iec61966Srgb8D65(value) => value, }) .collect::>(); let provenance = case @@ -249,11 +245,11 @@ fn assert_package_observation_matches_core( (values, provenance) }) .collect::>(); - assert_eq!(package_cases, core_cases); + assert_eq!(public_cases, core_cases); } -fn assert_package_binding_matches_core( - package: PackageProgramAssessmentV1<'_>, +fn assert_public_binding_matches_core( + public: AssessmentV1<'_>, core: &ProgramVisiblePointBindingV1, expected_occurrence: OccurrenceId, ) -> (Srgb8, Srgb8) { @@ -261,115 +257,113 @@ fn assert_package_binding_matches_core( let core_occurrence = core_physical.occurrence(); let core_program_occurrence = core_physical.program_occurrence(); assert_eq!(core_program_occurrence.occurrence(), expected_occurrence); - let PackageProgramPhysicalPointV1::EncodedSrgb8SourceOver(package_physical) = - package.binding().physical(); + let PhysicalPointV1::EncodedSrgb8SourceOver(public_physical) = public.binding().physical(); assert_eq!( - package_physical.subject_paint().value(), + public_physical.subject_paint().value(), core_program_occurrence.subject().value() ); assert_eq!( - package_physical.backdrop_surface().value(), + public_physical.backdrop_surface().value(), core_program_occurrence.backdrop_surface().value() ); assert_eq!( - package_physical.subject(), + public_physical.subject(), Srgb8::new(core_occurrence.subject_rgb()) ); assert_eq!( - package_physical.opacity().to_bits(), + public_physical.opacity().to_bits(), core_occurrence.subject_opacity_bits() ); assert_eq!( - package_physical.backdrop(), + public_physical.backdrop(), Srgb8::new(core_occurrence.backdrop_rgb()) ); assert_eq!( - package_physical.visible(), + public_physical.visible(), Srgb8::new(core_occurrence.output_rgb()) ); - let PackageProgramModeledPointV1::Iec61966Srgb8ToCie1931TwoDegreeXyzD65RelativeY1( - package_modeled, - ) = package.binding().modeled(); + let ModeledPointV1::Iec61966Srgb8ToCie1931TwoDegreeXyzD65RelativeY1(public_modeled) = + public.binding().modeled(); assert_eq!( - package_modeled.xyz().map(f64::to_bits), + public_modeled.xyz().map(f64::to_bits), core.modeled_lcs() .derivation() .sample() .xyz() .map(f64::to_bits) ); - let package_context = package_modeled.appearance_context(); + let public_context = public_modeled.appearance_context(); let core_context = core.modeled_lcs().occurrence().context(); assert_eq!( - package_context.adapting_luminance_cd_m2().to_bits(), + public_context.adapting_luminance_cd_m2().to_bits(), core_context.adapting_luminance_cd_m2().to_bits() ); assert_eq!( - package_context.background_luminance_ratio_yb_yw().to_bits(), + public_context.background_luminance_ratio_yb_yw().to_bits(), core_context.background_luminance_ratio().to_bits() ); let core_surround = match core_context.surround_profile() { - SurroundProfileId::AverageV1 => PackageProgramSurroundV1::Average, - SurroundProfileId::DimV1 => PackageProgramSurroundV1::Dim, - SurroundProfileId::DarkV1 => PackageProgramSurroundV1::Dark, + SurroundProfileId::AverageV1 => SurroundV1::Average, + SurroundProfileId::DimV1 => SurroundV1::Dim, + SurroundProfileId::DarkV1 => SurroundV1::Dark, }; - assert_eq!(package_context.surround(), core_surround); + assert_eq!(public_context.surround(), core_surround); - (package_physical.visible(), package_physical.backdrop()) + (public_physical.visible(), public_physical.backdrop()) } -fn assert_package_assessment_matches_core( - package: PackageProgramAssessmentV1<'_>, +fn assert_public_assessment_matches_core( + public: AssessmentV1<'_>, core: &ProgramConstraintResultV1, expected_occurrence: OccurrenceId, ) { - match (package, core) { + match (public, core) { ( - PackageProgramAssessmentV1::ExactSrgb8(package), + AssessmentV1::ExactSrgb8(public), ProgramConstraintResultV1::Pass(CoreProgramPassEvidenceV1::ExactSrgb8(core)), ) => { - assert_eq!(package.verdict(), PackageProgramVerdictV1::Pass); - assert_eq!(package.expected(), core.target()); - let (visible, _) = assert_package_binding_matches_core( - PackageProgramAssessmentV1::ExactSrgb8(package), + assert_eq!(public.verdict(), VerdictV1::Pass); + assert_eq!(public.expected(), core.target()); + let (visible, _) = assert_public_binding_matches_core( + AssessmentV1::ExactSrgb8(public), core.binding(), expected_occurrence, ); assert_eq!(visible, core.actual()); } ( - PackageProgramAssessmentV1::ExactSrgb8(package), + AssessmentV1::ExactSrgb8(public), ProgramConstraintResultV1::Violation(CoreProgramViolationEvidenceV1::ExactSrgb8(core)), ) => { - assert_eq!(package.verdict(), PackageProgramVerdictV1::Violation); - assert_eq!(package.expected(), core.target()); - let (visible, _) = assert_package_binding_matches_core( - PackageProgramAssessmentV1::ExactSrgb8(package), + assert_eq!(public.verdict(), VerdictV1::Violation); + assert_eq!(public.expected(), core.target()); + let (visible, _) = assert_public_binding_matches_core( + AssessmentV1::ExactSrgb8(public), core.binding(), expected_occurrence, ); assert_eq!(visible, core.actual()); } ( - PackageProgramAssessmentV1::Wcag22Srgb8(package), + AssessmentV1::Wcag22Srgb8(public), ProgramConstraintResultV1::Pass(CoreProgramPassEvidenceV1::Wcag22Srgb8(core)), ) => { - assert_eq!(package.verdict(), PackageProgramVerdictV1::Pass); + assert_eq!(public.verdict(), VerdictV1::Pass); let measurement = core.measurement().value(); - assert_eq!(package.profile_id(), measurement.profile_id()); - assert_eq!(package.criterion(), measurement.criterion()); + assert_eq!(public.profile_id(), measurement.profile_id()); + assert_eq!(public.criterion(), measurement.criterion()); assert_eq!( - package.foreground_luminance(), + public.foreground_luminance(), measurement.measurement().foreground_luminance ); assert_eq!( - package.background_luminance(), + public.background_luminance(), measurement.measurement().background_luminance ); - assert_eq!(package.numerical_evidence(), measurement.evidence()); - let (visible, backdrop) = assert_package_binding_matches_core( - PackageProgramAssessmentV1::Wcag22Srgb8(package), + assert_eq!(public.numerical_evidence(), measurement.evidence()); + let (visible, backdrop) = assert_public_binding_matches_core( + AssessmentV1::Wcag22Srgb8(public), core.binding(), expected_occurrence, ); @@ -377,69 +371,63 @@ fn assert_package_assessment_matches_core( assert_eq!(backdrop, Srgb8::new(measurement.measurement().background)); } ( - PackageProgramAssessmentV1::Wcag22Srgb8(package), + AssessmentV1::Wcag22Srgb8(public), ProgramConstraintResultV1::Violation(CoreProgramViolationEvidenceV1::Wcag22Srgb8(core)), ) => { - assert_eq!(package.verdict(), PackageProgramVerdictV1::Violation); + assert_eq!(public.verdict(), VerdictV1::Violation); let measurement = core.measurement().value(); - assert_eq!(package.profile_id(), measurement.profile_id()); - assert_eq!(package.criterion(), measurement.criterion()); + assert_eq!(public.profile_id(), measurement.profile_id()); + assert_eq!(public.criterion(), measurement.criterion()); assert_eq!( - package.foreground_luminance(), + public.foreground_luminance(), measurement.measurement().foreground_luminance ); assert_eq!( - package.background_luminance(), + public.background_luminance(), measurement.measurement().background_luminance ); - assert_eq!(package.numerical_evidence(), measurement.evidence()); - let (visible, backdrop) = assert_package_binding_matches_core( - PackageProgramAssessmentV1::Wcag22Srgb8(package), + assert_eq!(public.numerical_evidence(), measurement.evidence()); + let (visible, backdrop) = assert_public_binding_matches_core( + AssessmentV1::Wcag22Srgb8(public), core.binding(), expected_occurrence, ); assert_eq!(visible, Srgb8::new(measurement.measurement().foreground)); assert_eq!(backdrop, Srgb8::new(measurement.measurement().background)); } - _ => panic!("package assessment family or verdict drifted from Core"), + _ => panic!("public assessment family or verdict drifted from Core"), } } fn assert_verified_cell_matches_core( - package: PackageProgramVerifiedCellV1<'_>, + public: VerifiedCellV1<'_>, core: &ProgramConstraintCellV1, selected_state_index: usize, ) { assert_eq!(core.candidate_state_index(), selected_state_index); - assert_eq!(package.case_index(), core.case_index()); - assert_eq!(package.constraint().value(), core.constraint().value()); - assert_eq!(package.occurrence().value(), core.target().value()); + assert_eq!(public.case_index(), core.case_index()); + assert_eq!(public.constraint().value(), core.constraint().value()); + assert_eq!(public.occurrence().value(), core.target().value()); assert_eq!( - matches!( - package.mode(), - crate::package_bridge::PackageProgramConstraintModeV1::Hard - ), + matches!(public.mode(), crate::program::ConstraintModeV1::Hard), core.is_hard() ); - assert_package_assessment_matches_core(package.assessment(), core.result(), core.target()); + assert_public_assessment_matches_core(public.assessment(), core.result(), core.target()); } fn assert_conflict_cell_matches_core( - package: PackageProgramConflictCellV1<'_>, + public: ConflictCellV1<'_>, core: &ProgramConstraintCellV1, ) { - assert_eq!(package.state_index(), core.candidate_state_index()); - assert_eq!(package.case_index(), core.case_index()); - assert_eq!(package.constraint().value(), core.constraint().value()); - assert_eq!(package.occurrence().value(), core.target().value()); + assert_eq!(public.state_index(), core.candidate_state_index()); + assert_eq!(public.case_index(), core.case_index()); + assert_eq!(public.constraint().value(), core.constraint().value()); + assert_eq!(public.occurrence().value(), core.target().value()); assert_eq!( - matches!( - package.mode(), - crate::package_bridge::PackageProgramConstraintModeV1::Hard - ), + matches!(public.mode(), crate::program::ConstraintModeV1::Hard), core.is_hard() ); - assert_package_assessment_matches_core(package.assessment(), core.result(), core.target()); + assert_public_assessment_matches_core(public.assessment(), core.result(), core.target()); } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -518,20 +506,17 @@ fn consume_exact_fused( probe.iterator_laws_hold &= iterator.next().is_none(); } -fn consume_package_assessment( - assessment: PackageProgramAssessmentV1<'_>, - probe: &mut ProjectionProbe, -) { +fn consume_public_assessment(assessment: AssessmentV1<'_>, probe: &mut ProjectionProbe) { probe.mix(match assessment.verdict() { - PackageProgramVerdictV1::Pass => 1, - PackageProgramVerdictV1::Violation => 2, + VerdictV1::Pass => 1, + VerdictV1::Violation => 2, }); match assessment { - PackageProgramAssessmentV1::ExactSrgb8(evidence) => { + AssessmentV1::ExactSrgb8(evidence) => { probe.exact_assessments += 1; probe.mix_srgb8(evidence.expected()); } - PackageProgramAssessmentV1::Wcag22Srgb8(evidence) => { + AssessmentV1::Wcag22Srgb8(evidence) => { probe.wcag_assessments += 1; probe.mix_bytes(evidence.profile_id().key().as_bytes()); probe.mix_bytes(evidence.criterion().key().as_bytes()); @@ -543,8 +528,7 @@ fn consume_package_assessment( } } - let PackageProgramPhysicalPointV1::EncodedSrgb8SourceOver(physical) = - assessment.binding().physical(); + let PhysicalPointV1::EncodedSrgb8SourceOver(physical) = assessment.binding().physical(); probe.mix(u64::from(physical.subject_paint().value())); probe.mix(u64::from(physical.backdrop_surface().value())); probe.mix_srgb8(physical.subject()); @@ -552,7 +536,7 @@ fn consume_package_assessment( probe.mix_srgb8(physical.backdrop()); probe.mix_srgb8(physical.visible()); - let PackageProgramModeledPointV1::Iec61966Srgb8ToCie1931TwoDegreeXyzD65RelativeY1(modeled) = + let ModeledPointV1::Iec61966Srgb8ToCie1931TwoDegreeXyzD65RelativeY1(modeled) = assessment.binding().modeled(); for coordinate in modeled.xyz() { probe.mix(coordinate.to_bits()); @@ -561,24 +545,24 @@ fn consume_package_assessment( probe.mix(context.adapting_luminance_cd_m2().to_bits()); probe.mix(context.background_luminance_ratio_yb_yw().to_bits()); probe.mix(match context.surround() { - PackageProgramSurroundV1::Average => 1, - PackageProgramSurroundV1::Dim => 2, - PackageProgramSurroundV1::Dark => 3, + SurroundV1::Average => 1, + SurroundV1::Dim => 2, + SurroundV1::Dark => 3, }); } -fn consume_package_projection(projection: PackageProgramProjectionV1<'_, '_>) -> ProjectionProbe { +fn consume_public_projection(projection: ProjectionV1<'_, '_>) -> ProjectionProbe { let view = projection.evidence(); let mut probe = ProjectionProbe::new(); probe.mix(match view.kind() { - PackageProgramStateKindV1::Waiting => 1, - PackageProgramStateKindV1::Ready => 2, - PackageProgramStateKindV1::Failed => 3, - PackageProgramStateKindV1::Stale => 4, + StateKindV1::Waiting => 1, + StateKindV1::Ready => 2, + StateKindV1::Failed => 3, + StateKindV1::Stale => 4, }); match view.observation_head() { - PackageProgramObservationHeadV1::Empty => probe.mix(0), - PackageProgramObservationHeadV1::Unknown { + ObservationHeadV1::Empty => probe.mix(0), + ObservationHeadV1::Unknown { stream, revision, reason_id, @@ -588,7 +572,7 @@ fn consume_package_projection(projection: PackageProgramProjectionV1<'_, '_>) -> probe.mix(revision); probe.mix(u64::from(reason_id)); } - PackageProgramObservationHeadV1::Observed { stream, revision } => { + ObservationHeadV1::Observed { stream, revision } => { probe.mix(2); probe.mix(u64::from(stream.value())); probe.mix(revision); @@ -612,7 +596,7 @@ fn consume_package_projection(projection: PackageProgramProjectionV1<'_, '_>) -> probe.cases += 1; consume_exact_fused(case.values(), probe, |value, probe| { probe.values += 1; - let PackageProgramSignalV1::Iec61966Srgb8D65(value) = value; + let SignalV1::Iec61966Srgb8D65(value) = value; probe.mix_srgb8(value); }); consume_exact_fused(case.provenance(), probe, |scenario, probe| { @@ -621,7 +605,7 @@ fn consume_package_projection(projection: PackageProgramProjectionV1<'_, '_>) -> }); }); match certificate { - PackageProgramCertificateV1::Verified(verified) => { + CertificateV1::Verified(verified) => { probe.mix( verified .selected_state_index() @@ -633,10 +617,10 @@ fn consume_package_projection(projection: PackageProgramProjectionV1<'_, '_>) -> probe.mix(u64::from(cell.constraint().value())); probe.mix(u64::from(cell.occurrence().value())); probe.mix(match cell.mode() { - crate::package_bridge::PackageProgramConstraintModeV1::Hard => 1, - crate::package_bridge::PackageProgramConstraintModeV1::ReportOnly => 2, + crate::program::ConstraintModeV1::Hard => 1, + crate::program::ConstraintModeV1::ReportOnly => 2, }); - consume_package_assessment(cell.assessment(), probe); + consume_public_assessment(cell.assessment(), probe); }); consume_exact_fused(verified.outputs(), probe, |output, probe| { probe.outputs += 1; @@ -646,7 +630,7 @@ fn consume_package_projection(projection: PackageProgramProjectionV1<'_, '_>) -> probe.mix(output.opacity().to_bits()); }); } - PackageProgramCertificateV1::Conflict(conflict) => { + CertificateV1::Conflict(conflict) => { probe.mix(conflict.considered_state_count() as u64); consume_exact_fused(conflict.cells(), probe, |cell, probe| { probe.cells += 1; @@ -655,10 +639,10 @@ fn consume_package_projection(projection: PackageProgramProjectionV1<'_, '_>) -> probe.mix(u64::from(cell.constraint().value())); probe.mix(u64::from(cell.occurrence().value())); probe.mix(match cell.mode() { - crate::package_bridge::PackageProgramConstraintModeV1::Hard => 1, - crate::package_bridge::PackageProgramConstraintModeV1::ReportOnly => 2, + crate::program::ConstraintModeV1::Hard => 1, + crate::program::ConstraintModeV1::ReportOnly => 2, }); - consume_package_assessment(cell.assessment(), probe); + consume_public_assessment(cell.assessment(), probe); }); } } @@ -666,7 +650,7 @@ fn consume_package_projection(projection: PackageProgramProjectionV1<'_, '_>) -> consume_exact_fused(projection.operations(), &mut probe, |operation, probe| { probe.operations += 1; match operation { - PackageProgramOperationV1::Set(set) => { + OperationV1::Set(set) => { probe.mix(1); probe.mix(u64::from(set.output_slot().value())); probe.mix_srgb8(set.source()); @@ -674,11 +658,11 @@ fn consume_package_projection(projection: PackageProgramProjectionV1<'_, '_>) -> probe.mix_bytes(set.certificate().content_identity().as_bytes()); probe.mix(set.certificate().observation().revision()); } - PackageProgramOperationV1::Remove(remove) => { + OperationV1::Remove(remove) => { probe.mix(2); probe.mix(u64::from(remove.output_slot().value())); } - PackageProgramOperationV1::Hold(hold) => { + OperationV1::Hold(hold) => { probe.mix(3); probe.mix(u64::from(hold.output_slot().value())); probe.mix_bytes(hold.certificate().content_identity().as_bytes()); @@ -689,12 +673,8 @@ fn consume_package_projection(projection: PackageProgramProjectionV1<'_, '_>) -> std::hint::black_box(probe) } -fn assert_observed_head( - head: PackageProgramObservationHeadV1, - expected_stream: u32, - expected_revision: u64, -) { - let PackageProgramObservationHeadV1::Observed { stream, revision } = head else { +fn assert_observed_head(head: ObservationHeadV1, expected_stream: u32, expected_revision: u64) { + let ObservationHeadV1::Observed { stream, revision } = head else { panic!("raw evidence must remain a closed Observed payload"); }; assert_eq!(stream.value(), expected_stream); @@ -702,12 +682,12 @@ fn assert_observed_head( } fn assert_unknown_head( - head: PackageProgramObservationHeadV1, + head: ObservationHeadV1, expected_stream: u32, expected_revision: u64, expected_reason: u32, ) { - let PackageProgramObservationHeadV1::Unknown { + let ObservationHeadV1::Unknown { stream, revision, reason_id, @@ -808,40 +788,37 @@ fn one_program_retains_typed_exact_and_wcag22_outcomes() { } #[test] -fn fixed_package_certificate_retains_none_selection_and_nonunit_output_opacity() { - let owner = PackageProgramOwnerV1::from_compiled(fixed_translucent_program()); +fn fixed_public_certificate_retains_none_selection_and_nonunit_output_opacity() { + let owner = OwnerV1::from_compiled(fixed_translucent_program()); let mut session = owner.instantiate(STREAM.value()).unwrap(); let white = [Srgb8::new([0xFF; 3])]; - let scenarios = [PackageProgramScenarioV1::new(1, &white)]; + let scenarios = [ScenarioV1::new(1, &white)]; let projection = owner .update( &mut session, - PackageProgramUpdateV1::Observed { + UpdateV1::Observed { revision: 1, scenarios: &scenarios, }, ) .unwrap(); - let Some(PackageProgramCertificateV1::Verified(certificate)) = - projection.evidence().certificates().next() + let Some(CertificateV1::Verified(certificate)) = projection.evidence().certificates().next() else { panic!("the exact translucent midpoint must be verified"); }; assert_eq!(certificate.selected_state_index(), None); - let PackageProgramAssessmentV1::ExactSrgb8(assessment) = - certificate.cells().next().unwrap().assessment() + let AssessmentV1::ExactSrgb8(assessment) = certificate.cells().next().unwrap().assessment() else { panic!("the fixed Program has one Exact certificate cell"); }; - let PackageProgramPhysicalPointV1::EncodedSrgb8SourceOver(physical) = - assessment.binding().physical(); + let PhysicalPointV1::EncodedSrgb8SourceOver(physical) = assessment.binding().physical(); assert_eq!(physical.opacity().to_bits(), 0.5_f64.to_bits()); assert_eq!(physical.visible(), Srgb8::new([0x80; 3])); assert_eq!( certificate.outputs().next().unwrap().opacity().to_bits(), physical.opacity().to_bits() ); - let Some(PackageProgramOperationV1::Set(set)) = projection.operations().next() else { + let Some(OperationV1::Set(set)) = projection.operations().next() else { panic!("Verified must emit one Set"); }; assert_eq!(set.opacity().to_bits(), physical.opacity().to_bits()); @@ -926,13 +903,12 @@ fn mixed_family_conflict_is_exhaustive_and_keeps_report_only_non_gating() { } #[test] -fn package_projection_preserves_every_exposed_ready_and_conflict_field_against_core() { +fn public_projection_preserves_every_exposed_ready_and_conflict_field_against_core() { let ready_core_owner = finite_program([[0x80; 3], [0; 3]]); let ready_identity = ready_core_owner.content_identity(); - let ready_package_owner = - PackageProgramOwnerV1::from_compiled(finite_program([[0x80; 3], [0; 3]])); + let ready_public_owner = OwnerV1::from_compiled(finite_program([[0x80; 3], [0; 3]])); let mut ready_core_session = ready_core_owner.instantiate(STREAM).unwrap(); - let mut ready_package_session = ready_package_owner.instantiate(STREAM.value()).unwrap(); + let mut ready_public_session = ready_public_owner.instantiate(STREAM.value()).unwrap(); let ready_backdrops = [[0xFF; 3], [0xFF; 3], [0x80; 3]]; let SessionState::Ready { current: core_verified, @@ -945,71 +921,70 @@ fn package_projection_preserves_every_exposed_ready_and_conflict_field_against_c let ready_white = [Srgb8::new([0xFF; 3])]; let ready_gray = [Srgb8::new([0x80; 3])]; let ready_scenarios = [ - PackageProgramScenarioV1::new(1, &ready_white), - PackageProgramScenarioV1::new(2, &ready_white), - PackageProgramScenarioV1::new(3, &ready_gray), + ScenarioV1::new(1, &ready_white), + ScenarioV1::new(2, &ready_white), + ScenarioV1::new(3, &ready_gray), ]; - let package_ready = ready_package_owner + let public_ready = ready_public_owner .update( - &mut ready_package_session, - PackageProgramUpdateV1::Observed { + &mut ready_public_session, + UpdateV1::Observed { revision: 1, scenarios: &ready_scenarios, }, ) .unwrap(); - let mut package_certificates = package_ready.evidence().certificates(); - assert_eq!(package_certificates.len(), 1); - let Some(PackageProgramCertificateV1::Verified(package_verified)) = package_certificates.next() - else { + let mut public_certificates = public_ready.evidence().certificates(); + assert_eq!(public_certificates.len(), 1); + let Some(CertificateV1::Verified(public_verified)) = public_certificates.next() else { panic!("Ready must retain exactly one Verified certificate"); }; - assert!(package_certificates.next().is_none()); - assert!(package_certificates.next().is_none()); + assert!(public_certificates.next().is_none()); + assert!(public_certificates.next().is_none()); assert_eq!( - package_verified.content_identity().as_bytes(), + public_verified.content_identity().as_bytes(), ready_identity.as_bytes() ); - assert_package_observation_matches_core( - package_verified.observation(), + assert_public_observation_matches_core( + public_verified.observation(), core_verified.report().observation(), ); assert_eq!( - package_verified.selected_state_index(), + public_verified.selected_state_index(), core_verified.selected_state_index() ); let selected_state_index = core_verified.selected_state_index().unwrap(); - let mut package_cells = package_verified.cells(); + let mut public_cells = public_verified.cells(); let mut core_cells = core_verified.report().cells().iter(); - assert_eq!(package_cells.len(), core_cells.len()); - while let (Some(package), Some(core)) = (package_cells.next(), core_cells.next()) { - assert_verified_cell_matches_core(package, core, selected_state_index); - assert_eq!(package_cells.len(), core_cells.len()); + assert_eq!(public_cells.len(), core_cells.len()); + while let (Some(public), Some(core)) = (public_cells.next(), core_cells.next()) { + assert_verified_cell_matches_core(public, core, selected_state_index); + assert_eq!(public_cells.len(), core_cells.len()); } - assert!(package_cells.next().is_none()); - assert!(package_cells.next().is_none()); + assert!(public_cells.next().is_none()); + assert!(public_cells.next().is_none()); assert!(core_cells.next().is_none()); - let mut package_outputs = package_verified.outputs(); + let mut public_outputs = public_verified.outputs(); let mut core_outputs = core_verified.outputs().iter(); - assert_eq!(package_outputs.len(), core_outputs.len()); - while let (Some(package), Some(core)) = (package_outputs.next(), core_outputs.next()) { - assert_eq!(package.output_slot().value(), core.output().value()); - assert_eq!(package.paint().value(), core.paint().id().value()); - assert_eq!(package.source(), core.paint().source()); + assert_eq!(public_outputs.len(), core_outputs.len()); + while let (Some(public), Some(core)) = (public_outputs.next(), core_outputs.next()) { + assert_eq!(public.output_slot().value(), core.output().value()); + assert_eq!(public.paint().value(), core.paint().id().value()); + assert_eq!(public.source(), core.paint().source()); assert_eq!( - package.opacity().to_bits(), + public.opacity().to_bits(), core.paint().opacity().value().to_bits() ); - assert_eq!(package_outputs.len(), core_outputs.len()); + assert_eq!(public_outputs.len(), core_outputs.len()); } - assert!(package_outputs.next().is_none()); - assert!(package_outputs.next().is_none()); + assert!(public_outputs.next().is_none()); + assert!(public_outputs.next().is_none()); assert!(core_outputs.next().is_none()); - let mut ready_operations = package_ready.operations(); + let mut ready_operations = public_ready.operations(); assert_eq!(ready_operations.len(), core_verified.outputs().len()); for core_output in core_verified.outputs() { - let Some(PackageProgramOperationV1::Set(set)) = ready_operations.next() else { + let Some(OperationV1::Set(set)) = ready_operations.next() else { panic!("every certified output must become exactly one Set"); }; assert_eq!(set.output_slot().value(), core_output.output().value()); @@ -1020,11 +995,11 @@ fn package_projection_preserves_every_exposed_ready_and_conflict_field_against_c ); assert_eq!( set.certificate().content_identity(), - package_verified.content_identity() + public_verified.content_identity() ); assert_eq!( set.certificate().observation().revision(), - package_verified.observation().revision() + public_verified.observation().revision() ); } assert!(ready_operations.next().is_none()); @@ -1032,10 +1007,9 @@ fn package_projection_preserves_every_exposed_ready_and_conflict_field_against_c let conflict_core_owner = finite_program([[0; 3], [0xFF; 3]]); let conflict_identity = conflict_core_owner.content_identity(); - let conflict_package_owner = - PackageProgramOwnerV1::from_compiled(finite_program([[0; 3], [0xFF; 3]])); + let conflict_public_owner = OwnerV1::from_compiled(finite_program([[0; 3], [0xFF; 3]])); let mut conflict_core_session = conflict_core_owner.instantiate(STREAM).unwrap(); - let mut conflict_package_session = conflict_package_owner.instantiate(STREAM.value()).unwrap(); + let mut conflict_public_session = conflict_public_owner.instantiate(STREAM.value()).unwrap(); let conflict_backdrops = [[0xFF; 3], [0; 3]]; let SessionState::Failed { cause: core_conflict, @@ -1049,36 +1023,35 @@ fn package_projection_preserves_every_exposed_ready_and_conflict_field_against_c let conflict_white = [Srgb8::new([0xFF; 3])]; let conflict_black = [Srgb8::new([0; 3])]; let conflict_scenarios = [ - PackageProgramScenarioV1::new(1, &conflict_white), - PackageProgramScenarioV1::new(2, &conflict_black), + ScenarioV1::new(1, &conflict_white), + ScenarioV1::new(2, &conflict_black), ]; - let package_failed = conflict_package_owner + let public_failed = conflict_public_owner .update( - &mut conflict_package_session, - PackageProgramUpdateV1::Observed { + &mut conflict_public_session, + UpdateV1::Observed { revision: 1, scenarios: &conflict_scenarios, }, ) .unwrap(); - let mut package_certificates = package_failed.evidence().certificates(); - assert_eq!(package_certificates.len(), 1); - let Some(PackageProgramCertificateV1::Conflict(package_conflict)) = package_certificates.next() - else { + let mut public_certificates = public_failed.evidence().certificates(); + assert_eq!(public_certificates.len(), 1); + let Some(CertificateV1::Conflict(public_conflict)) = public_certificates.next() else { panic!("Failed without previous state must retain one Conflict certificate"); }; - assert!(package_certificates.next().is_none()); - assert!(package_certificates.next().is_none()); + assert!(public_certificates.next().is_none()); + assert!(public_certificates.next().is_none()); assert_eq!( - package_conflict.content_identity().as_bytes(), + public_conflict.content_identity().as_bytes(), conflict_identity.as_bytes() ); - assert_package_observation_matches_core( - package_conflict.observation(), + assert_public_observation_matches_core( + public_conflict.observation(), core_conflict.report().observation(), ); assert_eq!( - package_conflict.considered_state_count(), + public_conflict.considered_state_count(), core_conflict.considered_state_count() ); let core_passes = core_conflict @@ -1089,21 +1062,21 @@ fn package_projection_preserves_every_exposed_ready_and_conflict_field_against_c .count(); assert!(core_passes > 0); assert!(core_passes < core_conflict.report().cells().len()); - let mut package_cells = package_conflict.cells(); + let mut public_cells = public_conflict.cells(); let mut core_cells = core_conflict.report().cells().iter(); - assert_eq!(package_cells.len(), core_cells.len()); - while let (Some(package), Some(core)) = (package_cells.next(), core_cells.next()) { - assert_conflict_cell_matches_core(package, core); - assert_eq!(package_cells.len(), core_cells.len()); + assert_eq!(public_cells.len(), core_cells.len()); + while let (Some(public), Some(core)) = (public_cells.next(), core_cells.next()) { + assert_conflict_cell_matches_core(public, core); + assert_eq!(public_cells.len(), core_cells.len()); } - assert!(package_cells.next().is_none()); - assert!(package_cells.next().is_none()); + assert!(public_cells.next().is_none()); + assert!(public_cells.next().is_none()); assert!(core_cells.next().is_none()); - let mut failed_operations = package_failed.operations(); + let mut failed_operations = public_failed.operations(); assert_eq!(failed_operations.len(), 1); assert!(matches!( failed_operations.next(), - Some(PackageProgramOperationV1::Remove(_)) + Some(OperationV1::Remove(_)) )); assert!(failed_operations.next().is_none()); assert!(failed_operations.next().is_none()); @@ -1115,21 +1088,20 @@ fn committed_projection_is_zero_alloc_and_repeats_no_composite_transform_or_eval MODELED_TRISTIMULUS_DERIVATION_CALLS.with(|calls| calls.set(0)); CORE_PROGRAM_ASSESSMENT_CALLS.with(|calls| calls.set(0)); - let owner = PackageProgramOwnerV1::from_compiled(finite_program([[0; 3], [0xFF; 3]])); + let owner = OwnerV1::from_compiled(finite_program([[0; 3], [0xFF; 3]])); let mut session = owner.instantiate(STREAM.value()).unwrap(); let white = [Srgb8::new([0xFF; 3])]; - let white_only = [PackageProgramScenarioV1::new(1, &white)]; + let white_only = [ScenarioV1::new(1, &white)]; owner .update( &mut session, - PackageProgramUpdateV1::Observed { + UpdateV1::Observed { revision: 1, scenarios: &white_only, }, ) .unwrap(); - let Some(PackageProgramCertificateV1::Verified(ready_certificate)) = - session.evidence().certificates().next() + let Some(CertificateV1::Verified(ready_certificate)) = session.evidence().certificates().next() else { panic!("black must be selected for the white-only physical support"); }; @@ -1142,7 +1114,7 @@ fn committed_projection_is_zero_alloc_and_repeats_no_composite_transform_or_eval assert!(ready_derivations > 0); assert!(ready_assessments > 0); let (ready_probe, ready_allocations) = crate::test_support::measured_allocations(|| { - consume_package_projection(std::hint::black_box(owner.project(&session).unwrap())) + consume_public_projection(std::hint::black_box(owner.project(&session).unwrap())) }); assert_eq!(ready_allocations, 0); assert!(ready_probe.iterator_laws_hold); @@ -1172,7 +1144,7 @@ fn committed_projection_is_zero_alloc_and_repeats_no_composite_transform_or_eval owner .update( &mut session, - PackageProgramUpdateV1::Unknown { + UpdateV1::Unknown { revision: 2, reason_id: 7, }, @@ -1182,7 +1154,7 @@ fn committed_projection_is_zero_alloc_and_repeats_no_composite_transform_or_eval let stale_derivations = MODELED_TRISTIMULUS_DERIVATION_CALLS.with(core::cell::Cell::get); let stale_assessments = CORE_PROGRAM_ASSESSMENT_CALLS.with(core::cell::Cell::get); let (stale_probe, stale_allocations) = crate::test_support::measured_allocations(|| { - consume_package_projection(std::hint::black_box(owner.project(&session).unwrap())) + consume_public_projection(std::hint::black_box(owner.project(&session).unwrap())) }); assert_eq!(stale_allocations, 0); assert!(stale_probe.iterator_laws_hold); @@ -1205,14 +1177,11 @@ fn committed_projection_is_zero_alloc_and_repeats_no_composite_transform_or_eval ); let black = [Srgb8::new([0; 3])]; - let opposing_backdrops = [ - PackageProgramScenarioV1::new(1, &white), - PackageProgramScenarioV1::new(2, &black), - ]; + let opposing_backdrops = [ScenarioV1::new(1, &white), ScenarioV1::new(2, &black)]; owner .update( &mut session, - PackageProgramUpdateV1::Observed { + UpdateV1::Observed { revision: 3, scenarios: &opposing_backdrops, }, @@ -1222,7 +1191,7 @@ fn committed_projection_is_zero_alloc_and_repeats_no_composite_transform_or_eval let failed_derivations = MODELED_TRISTIMULUS_DERIVATION_CALLS.with(core::cell::Cell::get); let failed_assessments = CORE_PROGRAM_ASSESSMENT_CALLS.with(core::cell::Cell::get); let (failed_probe, failed_allocations) = crate::test_support::measured_allocations(|| { - consume_package_projection(std::hint::black_box(owner.project(&session).unwrap())) + consume_public_projection(std::hint::black_box(owner.project(&session).unwrap())) }); assert_eq!(failed_allocations, 0); assert!(failed_probe.iterator_laws_hold); @@ -1269,11 +1238,11 @@ fn observation_projection_is_invariant_under_every_scenario_permutation_and_keep let core_owner = finite_program([[0x80; 3], [0; 3]]); let content_identity = core_owner.content_identity(); - let package_owner = PackageProgramOwnerV1::from_compiled(finite_program([[0x80; 3], [0; 3]])); + let public_owner = OwnerV1::from_compiled(finite_program([[0x80; 3], [0; 3]])); let mut core_session = core_owner.instantiate(STREAM).unwrap(); - let mut package_session = package_owner.instantiate(STREAM.value()).unwrap(); - let package_values = BACKDROPS.map(|value| [Srgb8::new(value)]); - let mut first_package_backing = None; + let mut public_session = public_owner.instantiate(STREAM.value()).unwrap(); + let public_values = BACKDROPS.map(|value| [Srgb8::new(value)]); + let mut first_public_backing = None; let mut evaluation_counts_after_first = None; for permutation in PERMUTATIONS { @@ -1299,39 +1268,39 @@ fn observation_projection_is_invariant_under_every_scenario_permutation_and_keep else { panic!("black must pass both deduplicated physical cases"); }; - let package_scenarios = permutation - .map(|index| PackageProgramScenarioV1::new(IDS[index], &package_values[index])); - let package_state = package_owner + let public_scenarios = + permutation.map(|index| ScenarioV1::new(IDS[index], &public_values[index])); + let public_state = public_owner .update( - &mut package_session, - PackageProgramUpdateV1::Observed { + &mut public_session, + UpdateV1::Observed { revision: 1, - scenarios: &package_scenarios, + scenarios: &public_scenarios, }, ) .unwrap(); - let Some(PackageProgramCertificateV1::Verified(package_verified)) = - package_state.evidence().certificates().next() + let Some(CertificateV1::Verified(public_verified)) = + public_state.evidence().certificates().next() else { panic!("the canonical observation must keep one Verified certificate"); }; assert_eq!( - package_verified.content_identity().as_bytes(), + public_verified.content_identity().as_bytes(), content_identity.as_bytes() ); - assert_package_observation_matches_core( - package_verified.observation(), + assert_public_observation_matches_core( + public_verified.observation(), core_verified.report().observation(), ); - let projected_cases = package_verified + let projected_cases = public_verified .observation() .physical_cases() .map(|case| { let values = case .values() .map(|value| match value { - PackageProgramSignalV1::Iec61966Srgb8D65(value) => value, + SignalV1::Iec61966Srgb8D65(value) => value, }) .collect::>(); let provenance = case @@ -1349,11 +1318,10 @@ fn observation_projection_is_invariant_under_every_scenario_permutation_and_keep ] ); - let backing = PackageProgramCertificateV1::Verified(package_verified) - .observation_backing_ptr_for_test(); - match first_package_backing { + let backing = CertificateV1::Verified(public_verified).observation_backing_ptr_for_test(); + match first_public_backing { None => { - first_package_backing = Some(backing); + first_public_backing = Some(backing); evaluation_counts_after_first = Some(( crate::composition::source_over_evaluation_count(), MODELED_TRISTIMULUS_DERIVATION_CALLS.with(core::cell::Cell::get), @@ -1376,59 +1344,47 @@ fn observation_projection_is_invariant_under_every_scenario_permutation_and_keep } #[test] -fn concrete_package_bridge_projects_total_ready_and_stale_operations() { - let owner = PackageProgramOwnerV1::from_compiled(finite_program([[0x80; 3], [0; 3]])); +fn concrete_program_projects_total_ready_and_stale_operations() { + let owner = OwnerV1::from_compiled(finite_program([[0x80; 3], [0; 3]])); assert_eq!(owner.surface_input_port_count(), 1); assert_eq!( owner.output_slots().collect::>(), - [PackageProgramOutputSlotIdV1::new(OUTPUT.value())] + [OutputSlotIdV1::new(OUTPUT.value())] ); let mut session = owner.instantiate(11).unwrap(); let initial = session.evidence(); - assert_eq!(initial.kind(), PackageProgramStateKindV1::Waiting); - assert_eq!( - initial.observation_head(), - PackageProgramObservationHeadV1::Empty - ); + assert_eq!(initial.kind(), StateKindV1::Waiting); + assert_eq!(initial.observation_head(), ObservationHeadV1::Empty); assert_eq!(initial.certificates().len(), 0); assert_eq!(owner.project(&session).unwrap().operations().len(), 0); let white = [Srgb8::new([0xFF; 3])]; let gray = [Srgb8::new([0x80; 3])]; - let scenarios = [ - PackageProgramScenarioV1::new(2, &gray), - PackageProgramScenarioV1::new(1, &white), - ]; + let scenarios = [ScenarioV1::new(2, &gray), ScenarioV1::new(1, &white)]; let ready = owner .update( &mut session, - PackageProgramUpdateV1::Observed { + UpdateV1::Observed { revision: 1, scenarios: &scenarios, }, ) .unwrap(); let ready_evidence = ready.evidence(); - assert_eq!(ready_evidence.kind(), PackageProgramStateKindV1::Ready); + assert_eq!(ready_evidence.kind(), StateKindV1::Ready); assert_observed_head(ready_evidence.observation_head(), STREAM.value(), 1); assert_eq!(ready_evidence.cause_certificate_index(), None); let certificates = ready_evidence.certificates().collect::>(); assert_eq!(certificates.len(), 1); - assert!(matches!( - certificates[0], - PackageProgramCertificateV1::Verified(_) - )); + assert!(matches!(certificates[0], CertificateV1::Verified(_))); assert_eq!(certificates[0].observation().revision(), 1); let ready_backing = certificates[0].observation_backing_ptr_for_test(); let mut operations = ready.operations(); - let Some(PackageProgramOperationV1::Set(set)) = operations.next() else { + let Some(OperationV1::Set(set)) = operations.next() else { panic!("Ready must emit one Set operation"); }; - assert_eq!( - set.output_slot(), - PackageProgramOutputSlotIdV1::new(OUTPUT.value()) - ); + assert_eq!(set.output_slot(), OutputSlotIdV1::new(OUTPUT.value())); assert_eq!(set.source(), Srgb8::new([0; 3])); assert_eq!(set.opacity(), 1.0); assert_eq!( @@ -1440,21 +1396,18 @@ fn concrete_package_bridge_projects_total_ready_and_stale_operations() { certificates[0].content_identity() ); assert_eq!( - PackageProgramCertificateV1::Verified(set.certificate()).observation_backing_ptr_for_test(), + CertificateV1::Verified(set.certificate()).observation_backing_ptr_for_test(), ready_backing ); assert!(operations.next().is_none()); drop(operations); drop(certificates); - let reordered = [ - PackageProgramScenarioV1::new(1, &white), - PackageProgramScenarioV1::new(2, &gray), - ]; + let reordered = [ScenarioV1::new(1, &white), ScenarioV1::new(2, &gray)]; let replay = owner .update( &mut session, - PackageProgramUpdateV1::Observed { + UpdateV1::Observed { revision: 1, scenarios: &reordered, }, @@ -1467,10 +1420,10 @@ fn concrete_package_bridge_projects_total_ready_and_stale_operations() { "scenario permutation at the same revision must be exact idempotence" ); - let changed_same_revision = [PackageProgramScenarioV1::new(1, &white)]; + let changed_same_revision = [ScenarioV1::new(1, &white)]; let error = match owner.update( &mut session, - PackageProgramUpdateV1::Observed { + UpdateV1::Observed { revision: 1, scenarios: &changed_same_revision, }, @@ -1478,40 +1431,31 @@ fn concrete_package_bridge_projects_total_ready_and_stale_operations() { Ok(_) => panic!("changed payload at the same revision must be rejected"), Err(error) => error, }; - assert_eq!( - error.kind(), - PackageProgramUpdateErrorKindV1::RevisionConflict - ); - assert_eq!(session.evidence().kind(), PackageProgramStateKindV1::Ready); + assert_eq!(error.kind(), UpdateErrorKindV1::RevisionConflict); + assert_eq!(session.evidence().kind(), StateKindV1::Ready); assert_observed_head(session.evidence().observation_head(), STREAM.value(), 1); let stale = owner .update( &mut session, - PackageProgramUpdateV1::Unknown { + UpdateV1::Unknown { revision: 2, reason_id: 7, }, ) .unwrap(); let stale_evidence = stale.evidence(); - assert_eq!(stale_evidence.kind(), PackageProgramStateKindV1::Stale); + assert_eq!(stale_evidence.kind(), StateKindV1::Stale); assert_unknown_head(stale_evidence.observation_head(), STREAM.value(), 2, 7); let certificates = stale_evidence.certificates().collect::>(); assert_eq!(certificates.len(), 1); - assert!(matches!( - certificates[0], - PackageProgramCertificateV1::Verified(_) - )); + assert!(matches!(certificates[0], CertificateV1::Verified(_))); assert_eq!(certificates[0].observation().revision(), 1); let mut operations = stale.operations(); - let Some(PackageProgramOperationV1::Hold(hold)) = operations.next() else { + let Some(OperationV1::Hold(hold)) = operations.next() else { panic!("Stale must emit one Hold operation"); }; - assert_eq!( - hold.output_slot(), - PackageProgramOutputSlotIdV1::new(OUTPUT.value()) - ); + assert_eq!(hold.output_slot(), OutputSlotIdV1::new(OUTPUT.value())); assert_eq!( hold.certificate().observation().revision(), certificates[0].observation().revision() @@ -1521,56 +1465,49 @@ fn concrete_package_bridge_projects_total_ready_and_stale_operations() { certificates[0].content_identity() ); assert_eq!( - PackageProgramCertificateV1::Verified(hold.certificate()) - .observation_backing_ptr_for_test(), + CertificateV1::Verified(hold.certificate()).observation_backing_ptr_for_test(), ready_backing ); assert!(operations.next().is_none()); } #[test] -fn concrete_package_bridge_distinguishes_failed_remove_from_failed_hold() { +fn concrete_program_distinguishes_failed_remove_from_failed_hold() { let white = [Srgb8::new([0xFF; 3])]; let black = [Srgb8::new([0; 3])]; - let white_only = [PackageProgramScenarioV1::new(1, &white)]; + let white_only = [ScenarioV1::new(1, &white)]; - let owner = PackageProgramOwnerV1::from_compiled(finite_program([[0x80; 3], [0xFF; 3]])); + let owner = OwnerV1::from_compiled(finite_program([[0x80; 3], [0xFF; 3]])); let mut session = owner.instantiate(11).unwrap(); let failed = owner .update( &mut session, - PackageProgramUpdateV1::Observed { + UpdateV1::Observed { revision: 1, scenarios: &white_only, }, ) .unwrap(); let failed_evidence = failed.evidence(); - assert_eq!(failed_evidence.kind(), PackageProgramStateKindV1::Failed); + assert_eq!(failed_evidence.kind(), StateKindV1::Failed); assert_eq!(failed_evidence.cause_certificate_index(), Some(0)); let certificates = failed_evidence.certificates().collect::>(); assert_eq!(certificates.len(), 1); - assert!(matches!( - certificates[0], - PackageProgramCertificateV1::Conflict(_) - )); + assert!(matches!(certificates[0], CertificateV1::Conflict(_))); assert_eq!(certificates[0].observation().revision(), 1); let mut operations = failed.operations(); - let Some(PackageProgramOperationV1::Remove(remove)) = operations.next() else { + let Some(OperationV1::Remove(remove)) = operations.next() else { panic!("Failed without previous evidence must emit one Remove operation"); }; - assert_eq!( - remove.output_slot(), - PackageProgramOutputSlotIdV1::new(OUTPUT.value()) - ); + assert_eq!(remove.output_slot(), OutputSlotIdV1::new(OUTPUT.value())); assert!(operations.next().is_none()); - let owner = PackageProgramOwnerV1::from_compiled(finite_program([[0; 3], [0xFF; 3]])); + let owner = OwnerV1::from_compiled(finite_program([[0; 3], [0xFF; 3]])); let mut session = owner.instantiate(12).unwrap(); let previous = owner .update( &mut session, - PackageProgramUpdateV1::Observed { + UpdateV1::Observed { revision: 1, scenarios: &white_only, }, @@ -1582,31 +1519,28 @@ fn concrete_package_bridge_distinguishes_failed_remove_from_failed_hold() { .next() .unwrap() .observation_backing_ptr_for_test(); - let both = [ - PackageProgramScenarioV1::new(1, &white), - PackageProgramScenarioV1::new(2, &black), - ]; + let both = [ScenarioV1::new(1, &white), ScenarioV1::new(2, &black)]; let failed = owner .update( &mut session, - PackageProgramUpdateV1::Observed { + UpdateV1::Observed { revision: 2, scenarios: &both, }, ) .unwrap(); let failed_evidence = failed.evidence(); - assert_eq!(failed_evidence.kind(), PackageProgramStateKindV1::Failed); + assert_eq!(failed_evidence.kind(), StateKindV1::Failed); assert_eq!(failed_evidence.cause_certificate_index(), Some(0)); let certificates = failed_evidence.certificates().collect::>(); assert_eq!( certificates .iter() .map(|certificate| match certificate { - PackageProgramCertificateV1::Verified(value) => { + CertificateV1::Verified(value) => { ("verified", value.observation().revision()) } - PackageProgramCertificateV1::Conflict(value) => { + CertificateV1::Conflict(value) => { ("conflict", value.observation().revision()) } }) @@ -1614,73 +1548,60 @@ fn concrete_package_bridge_distinguishes_failed_remove_from_failed_hold() { [("conflict", 2), ("verified", 1)] ); let mut operations = failed.operations(); - let Some(PackageProgramOperationV1::Hold(hold)) = operations.next() else { + let Some(OperationV1::Hold(hold)) = operations.next() else { panic!("Failed with previous evidence must emit one Hold operation"); }; - assert_eq!( - hold.output_slot(), - PackageProgramOutputSlotIdV1::new(OUTPUT.value()) - ); + assert_eq!(hold.output_slot(), OutputSlotIdV1::new(OUTPUT.value())); assert_eq!(hold.certificate().observation().revision(), 1); assert_eq!( hold.certificate().content_identity(), certificates[1].content_identity() ); assert_eq!( - PackageProgramCertificateV1::Verified(hold.certificate()) - .observation_backing_ptr_for_test(), + CertificateV1::Verified(hold.certificate()).observation_backing_ptr_for_test(), previous_backing ); assert!(operations.next().is_none()); } #[test] -fn concrete_package_bridge_rejects_transport_shape_before_core_admission() { - let owner = PackageProgramOwnerV1::from_compiled(finite_program([[0x80; 3], [0; 3]])); +fn concrete_program_rejects_transport_shape_before_core_admission() { + let owner = OwnerV1::from_compiled(finite_program([[0x80; 3], [0; 3]])); let mut session = owner.instantiate(11).unwrap(); let empty_values = []; - let malformed = [PackageProgramScenarioV1::new(1, &empty_values)]; + let malformed = [ScenarioV1::new(1, &empty_values)]; let error = match owner.update( &mut session, - PackageProgramUpdateV1::Observed { + UpdateV1::Observed { revision: 1, scenarios: &malformed, }, ) { - Ok(_) => panic!("schema-short package input must fail before Core admission"), + Ok(_) => panic!("schema-short public input must fail before Core admission"), Err(error) => error, }; - assert_eq!( - error.kind(), - PackageProgramUpdateErrorKindV1::InvalidObservation - ); - assert_eq!( - session.evidence().kind(), - PackageProgramStateKindV1::Waiting - ); + assert_eq!(error.kind(), UpdateErrorKindV1::InvalidObservation); + assert_eq!(session.evidence().kind(), StateKindV1::Waiting); assert_eq!( session.evidence().observation_head(), - PackageProgramObservationHeadV1::Empty + ObservationHeadV1::Empty ); let white = [Srgb8::new([0xFF; 3])]; - let valid = [PackageProgramScenarioV1::new(1, &white)]; + let valid = [ScenarioV1::new(1, &white)]; owner .update( &mut session, - PackageProgramUpdateV1::Observed { + UpdateV1::Observed { revision: 2, scenarios: &valid, }, ) .unwrap(); - let duplicate = [ - PackageProgramScenarioV1::new(7, &white), - PackageProgramScenarioV1::new(7, &white), - ]; + let duplicate = [ScenarioV1::new(7, &white), ScenarioV1::new(7, &white)]; let error = match owner.update( &mut session, - PackageProgramUpdateV1::Observed { + UpdateV1::Observed { revision: 1, scenarios: &duplicate, }, @@ -1688,11 +1609,8 @@ fn concrete_package_bridge_rejects_transport_shape_before_core_admission() { Ok(_) => panic!("duplicate scenario IDs must precede revision admission"), Err(error) => error, }; - assert_eq!( - error.kind(), - PackageProgramUpdateErrorKindV1::InvalidObservation - ); - assert_eq!(session.evidence().kind(), PackageProgramStateKindV1::Ready); + assert_eq!(error.kind(), UpdateErrorKindV1::InvalidObservation); + assert_eq!(session.evidence().kind(), StateKindV1::Ready); assert_observed_head(session.evidence().observation_head(), 11, 2); } @@ -1705,16 +1623,16 @@ fn same_content_foreign_owner_is_rejected_before_admission_without_work() { let compiled_a = finite_program([[0x80; 3], [0; 3]]); let compiled_b = finite_program([[0x80; 3], [0; 3]]); assert_eq!(compiled_a.content_identity(), compiled_b.content_identity()); - let owner_a = PackageProgramOwnerV1::from_compiled(compiled_a); - let owner_b = PackageProgramOwnerV1::from_compiled(compiled_b); + let owner_a = OwnerV1::from_compiled(compiled_a); + let owner_b = OwnerV1::from_compiled(compiled_b); let mut session = owner_a.instantiate(STREAM.value()).unwrap(); let white = [Srgb8::new([0xFF; 3])]; - let observed = [PackageProgramScenarioV1::new(1, &white)]; + let observed = [ScenarioV1::new(1, &white)]; owner_a .update( &mut session, - PackageProgramUpdateV1::Observed { + UpdateV1::Observed { revision: 1, scenarios: &observed, }, @@ -1736,24 +1654,24 @@ fn same_content_foreign_owner_is_rejected_before_admission_without_work() { let (project_mismatch, project_allocations) = crate::test_support::measured_allocations(|| { matches!( owner_b.project(std::hint::black_box(&session)), - Err(PackageProgramAccessErrorV1::OwnerMismatch) + Err(AccessErrorV1::OwnerMismatch) ) }); assert!(project_mismatch); assert_eq!(project_allocations, 0); let empty = []; - let malformed = [PackageProgramScenarioV1::new(2, &empty)]; + let malformed = [ScenarioV1::new(2, &empty)]; let (update_mismatch, update_allocations) = crate::test_support::measured_allocations(|| { matches!( owner_b.update( std::hint::black_box(&mut session), - PackageProgramUpdateV1::Observed { + UpdateV1::Observed { revision: 2, scenarios: &malformed, }, ), - Err(error) if error.kind() == PackageProgramUpdateErrorKindV1::OwnerMismatch + Err(error) if error.kind() == UpdateErrorKindV1::OwnerMismatch ) }); assert!(update_mismatch); @@ -1768,7 +1686,7 @@ fn same_content_foreign_owner_is_rejected_before_admission_without_work() { ); let after = session.evidence(); - assert_eq!(after.kind(), PackageProgramStateKindV1::Ready); + assert_eq!(after.kind(), StateKindV1::Ready); assert_observed_head(after.observation_head(), STREAM.value(), 1); assert_eq!( after @@ -1783,25 +1701,25 @@ fn same_content_foreign_owner_is_rejected_before_admission_without_work() { #[test] fn raw_observation_head_preserves_unknown_reason_independently_of_lifecycle() { - let owner = PackageProgramOwnerV1::from_compiled(finite_program([[0x80; 3], [0; 3]])); + let owner = OwnerV1::from_compiled(finite_program([[0x80; 3], [0; 3]])); let mut session = owner.instantiate(STREAM.value()).unwrap(); assert_eq!( session.evidence().observation_head(), - PackageProgramObservationHeadV1::Empty + ObservationHeadV1::Empty ); owner .update( &mut session, - PackageProgramUpdateV1::Unknown { + UpdateV1::Unknown { revision: 1, reason_id: u32::MAX, }, ) .unwrap(); let unknown = session.evidence(); - assert_eq!(unknown.kind(), PackageProgramStateKindV1::Waiting); - let PackageProgramObservationHeadV1::Unknown { + assert_eq!(unknown.kind(), StateKindV1::Waiting); + let ObservationHeadV1::Unknown { stream, revision, reason_id, @@ -1817,7 +1735,7 @@ fn raw_observation_head_preserves_unknown_reason_independently_of_lifecycle() { owner .update( &mut other_session, - PackageProgramUpdateV1::Unknown { + UpdateV1::Unknown { revision: 1, reason_id: u32::MAX, }, @@ -1831,7 +1749,7 @@ fn raw_observation_head_preserves_unknown_reason_independently_of_lifecycle() { let conflicting = match owner.update( &mut session, - PackageProgramUpdateV1::Unknown { + UpdateV1::Unknown { revision: 1, reason_id: 0, }, @@ -1839,10 +1757,7 @@ fn raw_observation_head_preserves_unknown_reason_independently_of_lifecycle() { Ok(_) => panic!("same revision with another reason must conflict"), Err(error) => error, }; - assert_eq!( - conflicting.kind(), - PackageProgramUpdateErrorKindV1::RevisionConflict - ); + assert_eq!(conflicting.kind(), UpdateErrorKindV1::RevisionConflict); assert_unknown_head( session.evidence().observation_head(), STREAM.value(), @@ -1854,12 +1769,12 @@ fn raw_observation_head_preserves_unknown_reason_independently_of_lifecycle() { #[test] fn copied_raw_heads_outlive_owner_and_session_without_losing_provenance() { let (unknown, observed) = { - let owner = PackageProgramOwnerV1::from_compiled(finite_program([[0x80; 3], [0; 3]])); + let owner = OwnerV1::from_compiled(finite_program([[0x80; 3], [0; 3]])); let mut session = owner.instantiate(u32::MAX).unwrap(); owner .update( &mut session, - PackageProgramUpdateV1::Unknown { + UpdateV1::Unknown { revision: 1, reason_id: u32::MAX, }, @@ -1868,11 +1783,11 @@ fn copied_raw_heads_outlive_owner_and_session_without_losing_provenance() { let unknown = session.evidence().observation_head(); let white = [Srgb8::new([0xFF; 3])]; - let scenarios = [PackageProgramScenarioV1::new(1, &white)]; + let scenarios = [ScenarioV1::new(1, &white)]; owner .update( &mut session, - PackageProgramUpdateV1::Observed { + UpdateV1::Observed { revision: u64::MAX, scenarios: &scenarios, }, @@ -1891,15 +1806,15 @@ fn expired_owner_preserves_historical_evidence_but_equivalent_owner_has_no_autho let compiled_a = finite_program([[0x80; 3], [0; 3]]); let compiled_b = finite_program([[0x80; 3], [0; 3]]); assert_eq!(compiled_a.content_identity(), compiled_b.content_identity()); - let owner_a = PackageProgramOwnerV1::from_compiled(compiled_a); - let owner_b = PackageProgramOwnerV1::from_compiled(compiled_b); + let owner_a = OwnerV1::from_compiled(compiled_a); + let owner_b = OwnerV1::from_compiled(compiled_b); let mut session = owner_a.instantiate(STREAM.value()).unwrap(); let white = [Srgb8::new([0xFF; 3])]; - let observed = [PackageProgramScenarioV1::new(1, &white)]; + let observed = [ScenarioV1::new(1, &white)]; owner_a .update( &mut session, - PackageProgramUpdateV1::Observed { + UpdateV1::Observed { revision: 1, scenarios: &observed, }, @@ -1918,11 +1833,11 @@ fn expired_owner_preserves_historical_evidence_but_equivalent_owner_has_no_autho assert!(matches!( owner_b.project(&session), - Err(PackageProgramAccessErrorV1::OwnerMismatch) + Err(AccessErrorV1::OwnerMismatch) )); let mismatch = match owner_b.update( &mut session, - PackageProgramUpdateV1::Unknown { + UpdateV1::Unknown { revision: 2, reason_id: 9, }, @@ -1930,10 +1845,7 @@ fn expired_owner_preserves_historical_evidence_but_equivalent_owner_has_no_autho Ok(_) => panic!("equivalent recompile must not revive another owner generation"), Err(error) => error, }; - assert_eq!( - mismatch.kind(), - PackageProgramUpdateErrorKindV1::OwnerMismatch - ); + assert_eq!(mismatch.kind(), UpdateErrorKindV1::OwnerMismatch); assert_eq!( session .evidence() @@ -1947,20 +1859,17 @@ fn expired_owner_preserves_historical_evidence_but_equivalent_owner_has_no_autho #[test] fn raw_head_and_evaluator_lifecycle_form_the_exact_reachable_product() { - let owner = PackageProgramOwnerV1::from_compiled(finite_program([[0x80; 3], [0; 3]])); + let owner = OwnerV1::from_compiled(finite_program([[0x80; 3], [0; 3]])); let mut session = owner.instantiate(STREAM.value()).unwrap(); let white = [Srgb8::new([0xFF; 3])]; let black = [Srgb8::new([0; 3])]; - let white_only = [PackageProgramScenarioV1::new(1, &white)]; - let opposing = [ - PackageProgramScenarioV1::new(1, &white), - PackageProgramScenarioV1::new(2, &black), - ]; + let white_only = [ScenarioV1::new(1, &white)]; + let opposing = [ScenarioV1::new(1, &white), ScenarioV1::new(2, &black)]; owner .update( &mut session, - PackageProgramUpdateV1::Unknown { + UpdateV1::Unknown { revision: 1, reason_id: 3, }, @@ -1969,27 +1878,27 @@ fn raw_head_and_evaluator_lifecycle_form_the_exact_reachable_product() { owner .update( &mut session, - PackageProgramUpdateV1::Observed { + UpdateV1::Observed { revision: 2, scenarios: &white_only, }, ) .unwrap(); let ready = session.evidence(); - assert_eq!(ready.kind(), PackageProgramStateKindV1::Ready); + assert_eq!(ready.kind(), StateKindV1::Ready); assert_observed_head(ready.observation_head(), STREAM.value(), 2); owner .update( &mut session, - PackageProgramUpdateV1::Observed { + UpdateV1::Observed { revision: 3, scenarios: &opposing, }, ) .unwrap(); let failed = session.evidence(); - assert_eq!(failed.kind(), PackageProgramStateKindV1::Failed); + assert_eq!(failed.kind(), StateKindV1::Failed); assert_observed_head(failed.observation_head(), STREAM.value(), 3); assert_eq!( failed @@ -2002,14 +1911,14 @@ fn raw_head_and_evaluator_lifecycle_form_the_exact_reachable_product() { owner .update( &mut session, - PackageProgramUpdateV1::Unknown { + UpdateV1::Unknown { revision: 4, reason_id: 17, }, ) .unwrap(); let stale = session.evidence(); - assert_eq!(stale.kind(), PackageProgramStateKindV1::Stale); + assert_eq!(stale.kind(), StateKindV1::Stale); assert_unknown_head(stale.observation_head(), STREAM.value(), 4, 17); assert_eq!( stale @@ -2019,40 +1928,36 @@ fn raw_head_and_evaluator_lifecycle_form_the_exact_reachable_product() { [2] ); - let rejecting_owner = - PackageProgramOwnerV1::from_compiled(finite_program([[0x80; 3], [0xFF; 3]])); + let rejecting_owner = OwnerV1::from_compiled(finite_program([[0x80; 3], [0xFF; 3]])); let mut rejecting_session = rejecting_owner.instantiate(STREAM.value()).unwrap(); rejecting_owner .update( &mut rejecting_session, - PackageProgramUpdateV1::Observed { + UpdateV1::Observed { revision: 1, scenarios: &white_only, }, ) .unwrap(); - assert_eq!( - rejecting_session.evidence().kind(), - PackageProgramStateKindV1::Failed - ); + assert_eq!(rejecting_session.evidence().kind(), StateKindV1::Failed); rejecting_owner .update( &mut rejecting_session, - PackageProgramUpdateV1::Unknown { + UpdateV1::Unknown { revision: 2, reason_id: 23, }, ) .unwrap(); let waiting = rejecting_session.evidence(); - assert_eq!(waiting.kind(), PackageProgramStateKindV1::Waiting); + assert_eq!(waiting.kind(), StateKindV1::Waiting); assert_unknown_head(waiting.observation_head(), STREAM.value(), 2, 23); assert_eq!(waiting.certificates().len(), 0); } #[test] fn failed_without_previous_removes_every_output_in_canonical_exact_order() { - let owner = PackageProgramOwnerV1::from_compiled(finite_program_with_outputs( + let owner = OwnerV1::from_compiled(finite_program_with_outputs( [[0x80; 3], [0xFF; 3]], vec![ OutputBinding::new(SECOND_OUTPUT, PAINT), @@ -2061,38 +1966,35 @@ fn failed_without_previous_removes_every_output_in_canonical_exact_order() { )); let mut session = owner.instantiate(STREAM.value()).unwrap(); let white = [Srgb8::new([0xFF; 3])]; - let scenarios = [PackageProgramScenarioV1::new(1, &white)]; + let scenarios = [ScenarioV1::new(1, &white)]; let failed = owner .update( &mut session, - PackageProgramUpdateV1::Observed { + UpdateV1::Observed { revision: 1, scenarios: &scenarios, }, ) .unwrap(); - assert_eq!(failed.evidence().kind(), PackageProgramStateKindV1::Failed); + assert_eq!(failed.evidence().kind(), StateKindV1::Failed); assert_eq!(failed.evidence().certificates().len(), 1); let mut operations = failed.operations(); assert_eq!(operations.len(), 2); assert_eq!(operations.size_hint(), (2, Some(2))); - let Some(PackageProgramOperationV1::Remove(first)) = operations.next() else { + let Some(OperationV1::Remove(first)) = operations.next() else { panic!("Failed without previous evidence must remove the first output"); }; - assert_eq!( - first.output_slot(), - PackageProgramOutputSlotIdV1::new(OUTPUT.value()) - ); + assert_eq!(first.output_slot(), OutputSlotIdV1::new(OUTPUT.value())); assert_eq!(operations.len(), 1); assert_eq!(operations.size_hint(), (1, Some(1))); - let Some(PackageProgramOperationV1::Remove(second)) = operations.next() else { + let Some(OperationV1::Remove(second)) = operations.next() else { panic!("Failed without previous evidence must remove the second output"); }; assert_eq!( second.output_slot(), - PackageProgramOutputSlotIdV1::new(SECOND_OUTPUT.value()) + OutputSlotIdV1::new(SECOND_OUTPUT.value()) ); assert_eq!(operations.len(), 0); assert_eq!(operations.size_hint(), (0, Some(0))); @@ -2102,7 +2004,7 @@ fn failed_without_previous_removes_every_output_in_canonical_exact_order() { #[test] fn ready_and_stale_project_every_output_in_the_same_canonical_order() { - let owner = PackageProgramOwnerV1::from_compiled(finite_program_with_outputs( + let owner = OwnerV1::from_compiled(finite_program_with_outputs( [[0x80; 3], [0; 3]], vec![ OutputBinding::new(SECOND_OUTPUT, PAINT), @@ -2111,13 +2013,13 @@ fn ready_and_stale_project_every_output_in_the_same_canonical_order() { )); let mut session = owner.instantiate(STREAM.value()).unwrap(); let white = [Srgb8::new([0xFF; 3])]; - let scenarios = [PackageProgramScenarioV1::new(1, &white)]; + let scenarios = [ScenarioV1::new(1, &white)]; { let ready = owner .update( &mut session, - PackageProgramUpdateV1::Observed { + UpdateV1::Observed { revision: 1, scenarios: &scenarios, }, @@ -2126,7 +2028,7 @@ fn ready_and_stale_project_every_output_in_the_same_canonical_order() { let mut operations = ready.operations(); assert_eq!(operations.len(), 2); for expected in [OUTPUT, SECOND_OUTPUT] { - let Some(PackageProgramOperationV1::Set(set)) = operations.next() else { + let Some(OperationV1::Set(set)) = operations.next() else { panic!("Ready must set every compiled output"); }; assert_eq!(set.output_slot().value(), expected.value()); @@ -2138,7 +2040,7 @@ fn ready_and_stale_project_every_output_in_the_same_canonical_order() { let stale = owner .update( &mut session, - PackageProgramUpdateV1::Unknown { + UpdateV1::Unknown { revision: 2, reason_id: 7, }, @@ -2147,7 +2049,7 @@ fn ready_and_stale_project_every_output_in_the_same_canonical_order() { let mut operations = stale.operations(); assert_eq!(operations.len(), 2); for expected in [OUTPUT, SECOND_OUTPUT] { - let Some(PackageProgramOperationV1::Hold(hold)) = operations.next() else { + let Some(OperationV1::Hold(hold)) = operations.next() else { panic!("Stale must hold every previously verified output"); }; assert_eq!(hold.output_slot().value(), expected.value()); diff --git a/crates/labcolors-core/src/program_session.rs b/crates/labcolors-core/src/program_session.rs index 5c49e46b..83d151c3 100644 --- a/crates/labcolors-core/src/program_session.rs +++ b/crates/labcolors-core/src/program_session.rs @@ -1416,17 +1416,22 @@ pub enum ProgramSessionEvaluationError { Evaluator { case_index: usize, constraint: ConstraintId, + occurrence: OccurrenceId, + context: AppearanceContextId, source: EvaluationError, }, ProgramTargetBinding { case_index: usize, constraint: ConstraintId, + occurrence: OccurrenceId, + context: AppearanceContextId, physical: Srgb8, modeled: Srgb8, }, ModeledOccurrence { case_index: usize, - target: OccurrenceId, + occurrence: OccurrenceId, + context: AppearanceContextId, source: ModeledLcsOccurrenceFormationErrorV1, }, OutputVariesAcrossCases { @@ -1923,7 +1928,8 @@ where .map_err(|source| { ProgramSessionEvaluationError::ModeledOccurrence { case_index, - target: constraint.target_id, + occurrence: constraint.target_id, + context: binding.context, source, } })?; @@ -1947,6 +1953,8 @@ where ProgramSessionEvaluationError::ProgramTargetBinding { case_index, constraint: constraint.id, + occurrence: constraint.target_id, + context: modeled_lcs_occurrence.occurrence().context(), physical: source.physical(), modeled: source.modeled(), } @@ -1955,6 +1963,8 @@ where ProgramSessionEvaluationError::Evaluator { case_index, constraint: constraint.id, + occurrence: constraint.target_id, + context: modeled_lcs_occurrence.occurrence().context(), source, } } diff --git a/crates/labcolors-core/tests/program_boundary.rs b/crates/labcolors-core/tests/program_boundary.rs index cd0a2fc6..64b2acd0 100644 --- a/crates/labcolors-core/tests/program_boundary.rs +++ b/crates/labcolors-core/tests/program_boundary.rs @@ -7,22 +7,15 @@ use core::iter::FusedIterator; use labcolors_core::Srgb8; -use labcolors_core::package_bridge::{ - PackageProgramAppearanceContextErrorKindV1, PackageProgramAppearanceContextFieldV1, - PackageProgramAppearanceContextV1, PackageProgramAssessmentV1, PackageProgramCertificateV1, - PackageProgramCompileErrorHandleV1, PackageProgramCompileErrorKindV1, - PackageProgramCompileErrorV1, PackageProgramConstraintIdV1, PackageProgramDraftErrorV1, - PackageProgramDraftV1, PackageProgramInstantiateErrorV1, PackageProgramJointChoiceV1, - PackageProgramJointOrderErrorV1, PackageProgramJointStateV1, PackageProgramModeledPointV1, - PackageProgramNumericDomainErrorV1, PackageProgramObservationHeadV1, - PackageProgramOccurrenceIdV1, PackageProgramOpacityInputIdV1, PackageProgramOperationV1, - PackageProgramOutputSlotIdV1, PackageProgramOwnerV1, PackageProgramPaintIdV1, - PackageProgramPhysicalPointV1, PackageProgramProjectionV1, PackageProgramScenarioV1, - PackageProgramSessionV1, PackageProgramSignalV1, PackageProgramSourceIdV1, - PackageProgramStateKindV1, PackageProgramSurfaceIdV1, PackageProgramSurfaceInputPortIdV1, - PackageProgramSurroundV1, PackageProgramTargetCandidateIdV1, PackageProgramTargetCandidateV1, - PackageProgramTargetIdV1, PackageProgramUpdateErrorKindV1, PackageProgramUpdateV1, - PackageProgramVerdictV1, +use labcolors_core::program::{ + AppearanceContextErrorKindV1, AppearanceContextFieldV1, AppearanceContextV1, AssessmentV1, + CertificateV1, CompileErrorHandleV1, CompileErrorKindV1, CompileErrorV1, ConstraintIdV1, + DraftErrorV1, DraftV1, InstantiateErrorV1, JointChoiceV1, JointOrderErrorV1, JointStateV1, + ModeledPointV1, NumericDomainErrorV1, ObservationHeadV1, OccurrenceIdV1, OpacityInputIdV1, + OperationV1, OutputSlotIdV1, OwnerV1, PaintIdV1, PhysicalPointV1, ProjectionV1, ScenarioV1, + SessionV1, SignalV1, SourceIdV1, StateKindV1, SurfaceIdV1, SurfaceInputPortIdV1, SurroundV1, + TargetCandidateIdV1, TargetCandidateV1, TargetIdV1, UpdateErrorKindV1, UpdateErrorV1, UpdateV1, + VerdictV1, }; use labcolors_core::wcag22::Wcag22CriterionV1; @@ -30,7 +23,7 @@ fn exact_size(iterator: I) -> I { iterator } -fn compile_error(draft: PackageProgramDraftV1) -> PackageProgramCompileErrorV1 { +fn compile_error(draft: DraftV1) -> CompileErrorV1 { match draft.compile() { Ok(_) => panic!("the invalid authored program must not compile"), Err(error) => error, @@ -39,12 +32,12 @@ fn compile_error(draft: PackageProgramDraftV1) -> PackageProgramCompileErrorV1 { #[allow(dead_code)] fn wasm_can_use_only_the_concrete_owner_and_session( - owner: &PackageProgramOwnerV1, - session: &mut PackageProgramSessionV1, - scenarios: &[PackageProgramScenarioV1<'_>], -) -> Result<(), PackageProgramInstantiateErrorV1> { + owner: &OwnerV1, + session: &mut SessionV1, + scenarios: &[ScenarioV1<'_>], +) -> Result<(), InstantiateErrorV1> { let _independent_session = owner.instantiate(0xA11CE)?; - let update = PackageProgramUpdateV1::Observed { + let update = UpdateV1::Observed { revision: 1, scenarios, }; @@ -55,12 +48,12 @@ fn wasm_can_use_only_the_concrete_owner_and_session( Ok(()) } -fn assert_projection_is_owner_bound(projection: PackageProgramProjectionV1<'_, '_>) { +fn assert_projection_is_owner_bound(projection: ProjectionV1<'_, '_>) { let view = projection.evidence(); - let _kind: PackageProgramStateKindV1 = view.kind(); + let _kind: StateKindV1 = view.kind(); match view.observation_head() { - PackageProgramObservationHeadV1::Empty => {} - PackageProgramObservationHeadV1::Unknown { + ObservationHeadV1::Empty => {} + ObservationHeadV1::Unknown { stream, revision, reason_id, @@ -69,7 +62,7 @@ fn assert_projection_is_owner_bound(projection: PackageProgramProjectionV1<'_, ' let _: u64 = revision; let _: u32 = reason_id; } - PackageProgramObservationHeadV1::Observed { stream, revision } => { + ObservationHeadV1::Observed { stream, revision } => { let _ = stream.value(); let _: u64 = revision; } @@ -82,11 +75,11 @@ fn assert_projection_is_owner_bound(projection: PackageProgramProjectionV1<'_, ' let _stream_id = observation.stream().value(); let _revision = observation.revision(); for port in exact_size(observation.surface_input_ports()) { - let _: PackageProgramSurfaceInputPortIdV1 = port; + let _: SurfaceInputPortIdV1 = port; } for case in exact_size(observation.physical_cases()) { for value in exact_size(case.values()) { - let PackageProgramSignalV1::Iec61966Srgb8D65(value) = value; + let SignalV1::Iec61966Srgb8D65(value) = value; let _: Srgb8 = value; } for scenario in exact_size(case.provenance()) { @@ -101,12 +94,12 @@ fn assert_projection_is_owner_bound(projection: PackageProgramProjectionV1<'_, ' let _ = cell.occurrence().value(); let _ = cell.mode(); let assessment = cell.assessment(); - let _: PackageProgramVerdictV1 = assessment.verdict(); + let _: VerdictV1 = assessment.verdict(); match assessment { - PackageProgramAssessmentV1::ExactSrgb8(evidence) => { + AssessmentV1::ExactSrgb8(evidence) => { let _: Srgb8 = evidence.expected(); } - PackageProgramAssessmentV1::Wcag22Srgb8(evidence) => { + AssessmentV1::Wcag22Srgb8(evidence) => { let _ = evidence.profile_id(); let _ = evidence.criterion(); let _ = evidence.foreground_luminance(); @@ -115,17 +108,15 @@ fn assert_projection_is_owner_bound(projection: PackageProgramProjectionV1<'_, ' } } let binding = assessment.binding(); - let PackageProgramPhysicalPointV1::EncodedSrgb8SourceOver(physical) = - binding.physical(); + let PhysicalPointV1::EncodedSrgb8SourceOver(physical) = binding.physical(); let _ = physical.subject_paint().value(); let _ = physical.backdrop_surface().value(); let _: Srgb8 = physical.subject(); let _ = physical.opacity(); let _: Srgb8 = physical.backdrop(); let _: Srgb8 = physical.visible(); - let PackageProgramModeledPointV1::Iec61966Srgb8ToCie1931TwoDegreeXyzD65RelativeY1( - modeled, - ) = binding.modeled(); + let ModeledPointV1::Iec61966Srgb8ToCie1931TwoDegreeXyzD65RelativeY1(modeled) = + binding.modeled(); let _: [f64; 3] = modeled.xyz(); let context = modeled.appearance_context(); let _ = context.adapting_luminance_cd_m2(); @@ -134,7 +125,7 @@ fn assert_projection_is_owner_bound(projection: PackageProgramProjectionV1<'_, ' }}; } match certificate { - PackageProgramCertificateV1::Verified(verified) => { + CertificateV1::Verified(verified) => { let _ = verified.selected_state_index(); for cell in exact_size(verified.cells()) { inspect_cell!(cell); @@ -146,7 +137,7 @@ fn assert_projection_is_owner_bound(projection: PackageProgramProjectionV1<'_, ' let _ = output.opacity(); } } - PackageProgramCertificateV1::Conflict(conflict) => { + CertificateV1::Conflict(conflict) => { let _ = conflict.considered_state_count(); for cell in exact_size(conflict.cells()) { let _ = cell.state_index(); @@ -157,17 +148,17 @@ fn assert_projection_is_owner_bound(projection: PackageProgramProjectionV1<'_, ' } for operation in exact_size(projection.operations()) { match operation { - PackageProgramOperationV1::Set(set) => { - let _: PackageProgramOutputSlotIdV1 = set.output_slot(); + OperationV1::Set(set) => { + let _: OutputSlotIdV1 = set.output_slot(); let _: Srgb8 = set.source(); assert!(set.opacity().is_finite() && (0.0..=1.0).contains(&set.opacity())); let _ = set.certificate().content_identity(); } - PackageProgramOperationV1::Remove(remove) => { - let _: PackageProgramOutputSlotIdV1 = remove.output_slot(); + OperationV1::Remove(remove) => { + let _: OutputSlotIdV1 = remove.output_slot(); } - PackageProgramOperationV1::Hold(hold) => { - let _: PackageProgramOutputSlotIdV1 = hold.output_slot(); + OperationV1::Hold(hold) => { + let _: OutputSlotIdV1 = hold.output_slot(); let _ = hold.certificate().content_identity(); } } @@ -177,10 +168,10 @@ fn assert_projection_is_owner_bound(projection: PackageProgramProjectionV1<'_, ' #[allow(dead_code)] fn unknown_is_revision_bound_without_a_stream_or_generation_field( - owner: &PackageProgramOwnerV1, - session: &mut PackageProgramSessionV1, + owner: &OwnerV1, + session: &mut SessionV1, ) { - let update = PackageProgramUpdateV1::Unknown { + let update = UpdateV1::Unknown { revision: 2, reason_id: 7, }; @@ -188,10 +179,8 @@ fn unknown_is_revision_bound_without_a_stream_or_generation_field( } #[allow(dead_code)] -fn owner_mismatch_is_a_closed_package_error( - error: labcolors_core::package_bridge::PackageProgramUpdateErrorV1, -) { - assert_eq!(error.kind(), PackageProgramUpdateErrorKindV1::OwnerMismatch); +fn owner_mismatch_is_a_closed_public_error(error: labcolors_core::program::UpdateErrorV1) { + assert_eq!(error.kind(), UpdateErrorKindV1::OwnerMismatch); } #[test] @@ -203,27 +192,25 @@ fn external_boundary_uses_only_closed_concrete_types() { fn fixed_nested_draft( opacity_value: f64, - target_source: PackageProgramSourceIdV1, - declared_input: PackageProgramSurfaceInputPortIdV1, - used_input: PackageProgramSurfaceInputPortIdV1, -) -> PackageProgramDraftV1 { - let source = PackageProgramSourceIdV1::new(1); - let target = PackageProgramTargetIdV1::new(2); - let opacity = PackageProgramOpacityInputIdV1::new(3); - let solid = PackageProgramPaintIdV1::new(4); - let translucent = PackageProgramPaintIdV1::new(5); - let input_surface = PackageProgramSurfaceIdV1::new(6); - let nested_surface = PackageProgramSurfaceIdV1::new(7); - let first_occurrence = PackageProgramOccurrenceIdV1::new(8); - let second_occurrence = PackageProgramOccurrenceIdV1::new(9); - let exact = PackageProgramConstraintIdV1::new(10); - let wcag = PackageProgramConstraintIdV1::new(11); - let output = PackageProgramOutputSlotIdV1::new(12); - let context = - PackageProgramAppearanceContextV1::try_new(64.0, 0.2, PackageProgramSurroundV1::Dim) - .unwrap(); - - let mut draft = PackageProgramDraftV1::new(); + target_source: SourceIdV1, + declared_input: SurfaceInputPortIdV1, + used_input: SurfaceInputPortIdV1, +) -> DraftV1 { + let source = SourceIdV1::new(1); + let target = TargetIdV1::new(2); + let opacity = OpacityInputIdV1::new(3); + let solid = PaintIdV1::new(4); + let translucent = PaintIdV1::new(5); + let input_surface = SurfaceIdV1::new(6); + let nested_surface = SurfaceIdV1::new(7); + let first_occurrence = OccurrenceIdV1::new(8); + let second_occurrence = OccurrenceIdV1::new(9); + let exact = ConstraintIdV1::new(10); + let wcag = ConstraintIdV1::new(11); + let output = OutputSlotIdV1::new(12); + let context = AppearanceContextV1::try_new(64.0, 0.2, SurroundV1::Dim).unwrap(); + + let mut draft = DraftV1::new(); draft.push_source(source, Srgb8::new([0; 3])); draft.push_fixed_target(target, target_source); draft.push_surface_input_port(declared_input); @@ -240,58 +227,49 @@ fn fixed_nested_draft( draft } -fn attach_target_assessment(draft: &mut PackageProgramDraftV1, target: PackageProgramTargetIdV1) { - let paint = PackageProgramPaintIdV1::new(770); - let occurrence = PackageProgramOccurrenceIdV1::new(771); - let constraint = PackageProgramConstraintIdV1::new(772); - let context = - PackageProgramAppearanceContextV1::try_new(64.0, 0.2, PackageProgramSurroundV1::Average) - .unwrap(); +fn attach_target_assessment(draft: &mut DraftV1, target: TargetIdV1) { + let paint = PaintIdV1::new(770); + let occurrence = OccurrenceIdV1::new(771); + let constraint = ConstraintIdV1::new(772); + let context = AppearanceContextV1::try_new(64.0, 0.2, SurroundV1::Average).unwrap(); draft.push_solid_paint(paint, target); - draft.push_source_over_occurrence( - occurrence, - paint, - PackageProgramSurfaceIdV1::new(6), - context, - ); + draft.push_source_over_occurrence(occurrence, paint, SurfaceIdV1::new(6), context); draft.push_exact_report_only(constraint, occurrence, Srgb8::new([0; 3])); } #[test] fn external_authoring_lowers_the_actual_closed_program_and_returns_canonical_input_ports() { - let source = PackageProgramSourceIdV1::new(91); - let target = PackageProgramTargetIdV1::new(72); - let gray = PackageProgramTargetCandidateIdV1::new(8); - let black = PackageProgramTargetCandidateIdV1::new(3); - let paint = PackageProgramPaintIdV1::new(54); - let high_input = PackageProgramSurfaceInputPortIdV1::new(900); - let low_input = PackageProgramSurfaceInputPortIdV1::new(2); - let high_surface = PackageProgramSurfaceIdV1::new(401); - let low_surface = PackageProgramSurfaceIdV1::new(400); - let high_occurrence = PackageProgramOccurrenceIdV1::new(301); - let low_occurrence = PackageProgramOccurrenceIdV1::new(300); - let exact = PackageProgramConstraintIdV1::new(201); - let high_wcag = PackageProgramConstraintIdV1::new(203); - let low_wcag = PackageProgramConstraintIdV1::new(202); - let output = PackageProgramOutputSlotIdV1::new(101); - let context = - PackageProgramAppearanceContextV1::try_new(64.0, 0.2, PackageProgramSurroundV1::Average) - .unwrap(); - - let mut draft = PackageProgramDraftV1::new(); + let source = SourceIdV1::new(91); + let target = TargetIdV1::new(72); + let gray = TargetCandidateIdV1::new(8); + let black = TargetCandidateIdV1::new(3); + let paint = PaintIdV1::new(54); + let high_input = SurfaceInputPortIdV1::new(900); + let low_input = SurfaceInputPortIdV1::new(2); + let high_surface = SurfaceIdV1::new(401); + let low_surface = SurfaceIdV1::new(400); + let high_occurrence = OccurrenceIdV1::new(301); + let low_occurrence = OccurrenceIdV1::new(300); + let exact = ConstraintIdV1::new(201); + let high_wcag = ConstraintIdV1::new(203); + let low_wcag = ConstraintIdV1::new(202); + let output = OutputSlotIdV1::new(101); + let context = AppearanceContextV1::try_new(64.0, 0.2, SurroundV1::Average).unwrap(); + + let mut draft = DraftV1::new(); draft.push_source(source, Srgb8::new([0x80; 3])); draft.push_finite_target( target, source, vec![ - PackageProgramTargetCandidateV1::new(gray, Srgb8::new([0x80; 3])), - PackageProgramTargetCandidateV1::new(black, Srgb8::new([0; 3])), + TargetCandidateV1::new(gray, Srgb8::new([0x80; 3])), + TargetCandidateV1::new(black, Srgb8::new([0; 3])), ], ); draft .set_joint_selection(vec![ - PackageProgramJointStateV1::new(vec![PackageProgramJointChoiceV1::new(target, gray)]), - PackageProgramJointStateV1::new(vec![PackageProgramJointChoiceV1::new(target, black)]), + JointStateV1::new(vec![JointChoiceV1::new(target, gray)]), + JointStateV1::new(vec![JointChoiceV1::new(target, black)]), ]) .unwrap(); // Deliberately reverse numeric order. Core, not the caller, owns the hot @@ -323,19 +301,19 @@ fn external_authoring_lowers_the_actual_closed_program_and_returns_canonical_inp ); let mut session = owner.instantiate(44).unwrap(); let white = [Srgb8::new([0xFF; 3]), Srgb8::new([0xFF; 3])]; - let scenarios = [PackageProgramScenarioV1::new(7, &white)]; + let scenarios = [ScenarioV1::new(7, &white)]; let ready = owner .update( &mut session, - PackageProgramUpdateV1::Observed { + UpdateV1::Observed { revision: 1, scenarios: &scenarios, }, ) .unwrap(); - assert_eq!(ready.evidence().kind(), PackageProgramStateKindV1::Ready); + assert_eq!(ready.evidence().kind(), StateKindV1::Ready); let mut operations = ready.operations(); - let Some(PackageProgramOperationV1::Set(set)) = operations.next() else { + let Some(OperationV1::Set(set)) = operations.next() else { panic!("Ready must emit one Set operation"); }; assert_eq!(set.output_slot(), output); @@ -347,8 +325,8 @@ fn external_authoring_lowers_the_actual_closed_program_and_returns_canonical_inp #[test] fn authored_compile_is_atomic_and_projects_a_closed_error() { - let source = PackageProgramSourceIdV1::new(1); - let input = PackageProgramSurfaceInputPortIdV1::new(50); + let source = SourceIdV1::new(1); + let input = SurfaceInputPortIdV1::new(50); let mut draft = fixed_nested_draft(1.0, source, input, input); draft.push_source(source, Srgb8::new([0xFF; 3])); @@ -356,70 +334,204 @@ fn authored_compile_is_atomic_and_projects_a_closed_error() { Ok(_) => panic!("duplicate declaration must not compile"), Err(error) => error, }; - assert_eq!( - error.kind(), - PackageProgramCompileErrorKindV1::DuplicateSource - ); + assert_eq!(error.kind(), CompileErrorKindV1::DuplicateSource); assert_eq!( error.primary_handle(), - Some(PackageProgramCompileErrorHandleV1::Source(source)) + Some(CompileErrorHandleV1::Source(source)) ); assert_eq!(error.related_handle(), None); } #[test] fn every_physical_constructor_and_both_remaining_constraint_modes_execute() { - let input = PackageProgramSurfaceInputPortIdV1::new(50); - let draft = fixed_nested_draft(1.0, PackageProgramSourceIdV1::new(1), input, input); + let input = SurfaceInputPortIdV1::new(50); + let draft = fixed_nested_draft(1.0, SourceIdV1::new(1), input, input); let owner = draft.compile().unwrap(); assert_eq!(owner.surface_input_ports().collect::>(), [input]); let mut session = owner.instantiate(13).unwrap(); let white = [Srgb8::new([0xFF; 3])]; - let scenarios = [PackageProgramScenarioV1::new(1, &white)]; + let scenarios = [ScenarioV1::new(1, &white)]; let state = owner .update( &mut session, - PackageProgramUpdateV1::Observed { + UpdateV1::Observed { revision: 1, scenarios: &scenarios, }, ) .unwrap(); - assert_eq!(state.evidence().kind(), PackageProgramStateKindV1::Ready); - let Some(PackageProgramCertificateV1::Verified(certificate)) = - state.evidence().certificates().next() - else { + assert_eq!(state.evidence().kind(), StateKindV1::Ready); + let Some(CertificateV1::Verified(certificate)) = state.evidence().certificates().next() else { panic!("a fixed target must produce one Verified certificate"); }; assert_eq!(certificate.selected_state_index(), None); let mut operations = state.operations(); - let Some(PackageProgramOperationV1::Set(set)) = operations.next() else { + let Some(OperationV1::Set(set)) = operations.next() else { panic!("Ready must emit one Set operation"); }; - assert_eq!(set.output_slot(), PackageProgramOutputSlotIdV1::new(12)); + assert_eq!(set.output_slot(), OutputSlotIdV1::new(12)); assert_eq!(set.source(), Srgb8::new([0; 3])); assert_eq!(set.opacity(), 1.0); assert_eq!(set.certificate().observation().revision(), 1); assert!(operations.next().is_none()); } +#[test] +fn owner_and_update_errors_preserve_content_and_input_identity() { + let input = SurfaceInputPortIdV1::new(50); + let owner = fixed_nested_draft(1.0, SourceIdV1::new(1), input, input) + .compile() + .unwrap(); + let owner_identity = owner.content_identity(); + let mut session = owner.instantiate(13).unwrap(); + + let no_scenarios = []; + let error = match owner.update( + &mut session, + UpdateV1::Observed { + revision: 1, + scenarios: &no_scenarios, + }, + ) { + Ok(_) => panic!("an empty physical domain must fail"), + Err(error) => error, + }; + assert_eq!(error.kind(), UpdateErrorKindV1::InvalidObservation); + assert_eq!(error, UpdateErrorV1::EmptyScenarioSet); + + let white = [Srgb8::new([0xFF; 3])]; + let duplicate = [ScenarioV1::new(70, &white), ScenarioV1::new(70, &white)]; + let error = match owner.update( + &mut session, + UpdateV1::Observed { + revision: 1, + scenarios: &duplicate, + }, + ) { + Ok(_) => panic!("duplicate scenario provenance must fail"), + Err(error) => error, + }; + assert_eq!(error.kind(), UpdateErrorKindV1::InvalidObservation); + let UpdateErrorV1::DuplicateScenarioId { scenario } = error else { + panic!("duplicate provenance must retain its scenario ID"); + }; + assert_eq!(scenario.value(), 70); + + let no_values = []; + let malformed = [ScenarioV1::new(71, &no_values)]; + let error = match owner.update( + &mut session, + UpdateV1::Observed { + revision: 1, + scenarios: &malformed, + }, + ) { + Ok(_) => panic!("schema-short public input must fail"), + Err(error) => error, + }; + assert_eq!(error.kind(), UpdateErrorKindV1::InvalidObservation); + let UpdateErrorV1::ScenarioValueCountMismatch { + scenario, + expected, + actual, + } = error + else { + panic!("schema mismatch must retain its exact scenario and arity"); + }; + assert_eq!(scenario.value(), 71); + assert_eq!(expected, 1); + assert_eq!(actual, 0); + assert_eq!(session.evidence().kind(), StateKindV1::Waiting); + assert_eq!( + session.evidence().observation_head(), + ObservationHeadV1::Empty + ); + + let valid = [ScenarioV1::new(72, &white)]; + let projection = owner + .update( + &mut session, + UpdateV1::Observed { + revision: 2, + scenarios: &valid, + }, + ) + .unwrap(); + let certificate = projection.evidence().certificates().next().unwrap(); + assert_eq!(owner_identity, certificate.content_identity()); + + let error = match owner.update( + &mut session, + UpdateV1::Unknown { + revision: 1, + reason_id: 9, + }, + ) { + Ok(_) => panic!("older revision must fail"), + Err(error) => error, + }; + assert_eq!(error.kind(), UpdateErrorKindV1::RevisionOutOfOrder); + assert_eq!( + error, + UpdateErrorV1::RevisionOutOfOrder { + current: 2, + incoming: 1, + } + ); + + let error = match owner.update( + &mut session, + UpdateV1::Unknown { + revision: 2, + reason_id: 9, + }, + ) { + Ok(_) => panic!("changed payload at the same revision must fail"), + Err(error) => error, + }; + assert_eq!(error.kind(), UpdateErrorKindV1::RevisionConflict); + assert_eq!(error, UpdateErrorV1::RevisionConflict { revision: 2 }); + assert_eq!(session.evidence().kind(), StateKindV1::Ready); + let ObservationHeadV1::Observed { stream, revision } = session.evidence().observation_head() + else { + panic!("failed update must keep the last admitted observation"); + }; + assert_eq!(stream.value(), 13); + assert_eq!(revision, 2); + + let foreign = fixed_nested_draft(1.0, SourceIdV1::new(1), input, input) + .compile() + .unwrap(); + assert_eq!(foreign.content_identity(), owner.content_identity()); + let error = match foreign.update( + &mut session, + UpdateV1::Unknown { + revision: 3, + reason_id: 9, + }, + ) { + Ok(_) => panic!("equal content must not confer owner authority"), + Err(error) => error, + }; + assert_eq!(error.kind(), UpdateErrorKindV1::OwnerMismatch); + assert_eq!(error, UpdateErrorV1::OwnerMismatch); +} + #[test] fn certificate_and_set_retain_the_same_nonunit_opacity() { - let source = PackageProgramSourceIdV1::new(1); - let target = PackageProgramTargetIdV1::new(2); - let opacity = PackageProgramOpacityInputIdV1::new(3); - let solid = PackageProgramPaintIdV1::new(4); - let translucent = PackageProgramPaintIdV1::new(5); - let input = PackageProgramSurfaceInputPortIdV1::new(6); - let surface = PackageProgramSurfaceIdV1::new(7); - let occurrence = PackageProgramOccurrenceIdV1::new(8); - let constraint = PackageProgramConstraintIdV1::new(9); - let output = PackageProgramOutputSlotIdV1::new(10); - let context = - PackageProgramAppearanceContextV1::try_new(64.0, 0.2, PackageProgramSurroundV1::Average) - .unwrap(); - let mut draft = PackageProgramDraftV1::new(); + let source = SourceIdV1::new(1); + let target = TargetIdV1::new(2); + let opacity = OpacityInputIdV1::new(3); + let solid = PaintIdV1::new(4); + let translucent = PaintIdV1::new(5); + let input = SurfaceInputPortIdV1::new(6); + let surface = SurfaceIdV1::new(7); + let occurrence = OccurrenceIdV1::new(8); + let constraint = ConstraintIdV1::new(9); + let output = OutputSlotIdV1::new(10); + 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_fixed_target(target, source); draft.push_surface_input_port(input); @@ -434,28 +546,24 @@ fn certificate_and_set_retain_the_same_nonunit_opacity() { let owner = draft.compile().unwrap(); let mut session = owner.instantiate(17).unwrap(); let white = [Srgb8::new([0xFF; 3])]; - let scenarios = [PackageProgramScenarioV1::new(1, &white)]; + let scenarios = [ScenarioV1::new(1, &white)]; let state = owner .update( &mut session, - PackageProgramUpdateV1::Observed { + UpdateV1::Observed { revision: 1, scenarios: &scenarios, }, ) .unwrap(); - let Some(PackageProgramCertificateV1::Verified(certificate)) = - state.evidence().certificates().next() - else { + let Some(CertificateV1::Verified(certificate)) = state.evidence().certificates().next() else { panic!("the exact emitted midpoint must be verified"); }; - let PackageProgramAssessmentV1::ExactSrgb8(assessment) = - certificate.cells().next().unwrap().assessment() + let AssessmentV1::ExactSrgb8(assessment) = certificate.cells().next().unwrap().assessment() else { panic!("the authored exact constraint must retain Exact evidence"); }; - let PackageProgramPhysicalPointV1::EncodedSrgb8SourceOver(physical) = - assessment.binding().physical(); + let PhysicalPointV1::EncodedSrgb8SourceOver(physical) = assessment.binding().physical(); assert_eq!(physical.opacity().to_bits(), 0.5_f64.to_bits()); assert_eq!(physical.visible(), Srgb8::new([0x80; 3])); assert_eq!( @@ -463,7 +571,7 @@ fn certificate_and_set_retain_the_same_nonunit_opacity() { physical.opacity().to_bits() ); - let Some(PackageProgramOperationV1::Set(set)) = state.operations().next() else { + let Some(OperationV1::Set(set)) = state.operations().next() else { panic!("the verified output must emit one Set"); }; assert_eq!(set.opacity().to_bits(), physical.opacity().to_bits()); @@ -471,115 +579,83 @@ fn certificate_and_set_retain_the_same_nonunit_opacity() { #[test] fn invalid_context_and_opacity_are_typed_and_fail_closed() { - let context_error = - PackageProgramAppearanceContextV1::try_new(64.0, 1.01, PackageProgramSurroundV1::Dark) - .unwrap_err(); - assert_eq!( - context_error.kind(), - PackageProgramAppearanceContextErrorKindV1::Domain - ); + let context_error = AppearanceContextV1::try_new(64.0, 1.01, SurroundV1::Dark).unwrap_err(); + assert_eq!(context_error.kind(), AppearanceContextErrorKindV1::Domain); assert_eq!( context_error.field(), - Some(PackageProgramAppearanceContextFieldV1::BackgroundLuminanceRatioYbYw) - ); - assert_eq!( - context_error.reason(), - Some(PackageProgramNumericDomainErrorV1::AboveOne) + Some(AppearanceContextFieldV1::BackgroundLuminanceRatioYbYw) ); + assert_eq!(context_error.reason(), Some(NumericDomainErrorV1::AboveOne)); - let input = PackageProgramSurfaceInputPortIdV1::new(50); - let error = match fixed_nested_draft(f64::NAN, PackageProgramSourceIdV1::new(1), input, input) - .compile() - { + let input = SurfaceInputPortIdV1::new(50); + let error = match fixed_nested_draft(f64::NAN, SourceIdV1::new(1), input, input).compile() { Ok(_) => panic!("non-finite opacity must not compile"), Err(error) => error, }; - assert_eq!( - error.kind(), - PackageProgramCompileErrorKindV1::OpacityOutOfDomain - ); + assert_eq!(error.kind(), CompileErrorKindV1::OpacityOutOfDomain); assert_eq!( error.primary_handle(), - Some(PackageProgramCompileErrorHandleV1::OpacityInput( - PackageProgramOpacityInputIdV1::new(3) - )) + Some(CompileErrorHandleV1::OpacityInput(OpacityInputIdV1::new(3))) ); assert_eq!(error.related_handle(), None); } #[test] fn relational_compile_errors_keep_both_typed_handles() { - let input = PackageProgramSurfaceInputPortIdV1::new(50); - let missing_source = PackageProgramSourceIdV1::new(99); + let input = SurfaceInputPortIdV1::new(50); + let missing_source = SourceIdV1::new(99); let error = match fixed_nested_draft(1.0, missing_source, input, input).compile() { Ok(_) => panic!("a target cannot reference an undeclared source"), Err(error) => error, }; - assert_eq!( - error.kind(), - PackageProgramCompileErrorKindV1::MissingTargetSource - ); + assert_eq!(error.kind(), CompileErrorKindV1::MissingTargetSource); assert_eq!( error.primary_handle(), - Some(PackageProgramCompileErrorHandleV1::Target( - PackageProgramTargetIdV1::new(2) - )) + Some(CompileErrorHandleV1::Target(TargetIdV1::new(2))) ); assert_eq!( error.related_handle(), - Some(PackageProgramCompileErrorHandleV1::Source(missing_source)) + Some(CompileErrorHandleV1::Source(missing_source)) ); } #[test] fn declared_and_referenced_surface_inputs_cannot_drift() { - let declared = PackageProgramSurfaceInputPortIdV1::new(50); - let missing = PackageProgramSurfaceInputPortIdV1::new(51); - let error = match fixed_nested_draft(1.0, PackageProgramSourceIdV1::new(1), declared, missing) - .compile() - { + let declared = SurfaceInputPortIdV1::new(50); + let missing = SurfaceInputPortIdV1::new(51); + let error = match fixed_nested_draft(1.0, SourceIdV1::new(1), declared, missing).compile() { Ok(_) => panic!("an undeclared physical input must not compile"), Err(error) => error, }; - assert_eq!( - error.kind(), - PackageProgramCompileErrorKindV1::MissingSurfaceInputPort - ); + assert_eq!(error.kind(), CompileErrorKindV1::MissingSurfaceInputPort); assert_eq!( error.primary_handle(), - Some(PackageProgramCompileErrorHandleV1::Surface( - PackageProgramSurfaceIdV1::new(6) - )) + Some(CompileErrorHandleV1::Surface(SurfaceIdV1::new(6))) ); assert_eq!( error.related_handle(), - Some(PackageProgramCompileErrorHandleV1::SurfaceInputPort( - missing - )) + Some(CompileErrorHandleV1::SurfaceInputPort(missing)) ); } #[test] fn declared_input_ports_form_an_exact_bijection_with_input_surfaces() { - let input = PackageProgramSurfaceInputPortIdV1::new(50); - let extra = PackageProgramSurfaceInputPortIdV1::new(51); - let mut unused = fixed_nested_draft(1.0, PackageProgramSourceIdV1::new(1), input, input); + let input = SurfaceInputPortIdV1::new(50); + let extra = SurfaceInputPortIdV1::new(51); + let mut unused = fixed_nested_draft(1.0, SourceIdV1::new(1), input, input); unused.push_surface_input_port(extra); let error = match unused.compile() { Ok(_) => panic!("an unused declared input must not compile"), Err(error) => error, }; - assert_eq!( - error.kind(), - PackageProgramCompileErrorKindV1::UnusedSurfaceInputPort - ); + assert_eq!(error.kind(), CompileErrorKindV1::UnusedSurfaceInputPort); assert_eq!( error.primary_handle(), - Some(PackageProgramCompileErrorHandleV1::SurfaceInputPort(extra)) + Some(CompileErrorHandleV1::SurfaceInputPort(extra)) ); - let duplicate_surface = PackageProgramSurfaceIdV1::new(60); - let mut duplicate = fixed_nested_draft(1.0, PackageProgramSourceIdV1::new(1), input, input); + let duplicate_surface = SurfaceIdV1::new(60); + let mut duplicate = fixed_nested_draft(1.0, SourceIdV1::new(1), input, input); duplicate.push_input_surface(duplicate_surface, input); let error = match duplicate.compile() { Ok(_) => panic!("two input surfaces must not bind one declared port"), @@ -587,23 +663,21 @@ fn declared_input_ports_form_an_exact_bijection_with_input_surfaces() { }; assert_eq!( error.kind(), - PackageProgramCompileErrorKindV1::DuplicateSurfaceInputBinding + CompileErrorKindV1::DuplicateSurfaceInputBinding ); assert_eq!( error.primary_handle(), - Some(PackageProgramCompileErrorHandleV1::SurfaceInputPort(input)) + Some(CompileErrorHandleV1::SurfaceInputPort(input)) ); assert_eq!( error.related_handle(), - Some(PackageProgramCompileErrorHandleV1::Surface( - duplicate_surface - )) + Some(CompileErrorHandleV1::Surface(duplicate_surface)) ); assert_eq!( error, - PackageProgramCompileErrorV1::DuplicateSurfaceInputBinding { + CompileErrorV1::DuplicateSurfaceInputBinding { input, - first: PackageProgramSurfaceIdV1::new(6), + first: SurfaceIdV1::new(6), duplicate: duplicate_surface, } ); @@ -611,25 +685,25 @@ fn declared_input_ports_form_an_exact_bijection_with_input_surfaces() { #[test] fn duplicate_candidate_signal_preserves_both_candidates_and_exact_stimulus() { - let input = PackageProgramSurfaceInputPortIdV1::new(50); - let target = PackageProgramTargetIdV1::new(70); - let first = PackageProgramTargetCandidateIdV1::new(701); - let duplicate = PackageProgramTargetCandidateIdV1::new(702); + let input = SurfaceInputPortIdV1::new(50); + let target = TargetIdV1::new(70); + let first = TargetCandidateIdV1::new(701); + let duplicate = TargetCandidateIdV1::new(702); let encoded_srgb8 = Srgb8::new([17, 33, 65]); - let mut draft = fixed_nested_draft(1.0, PackageProgramSourceIdV1::new(1), input, input); + let mut draft = fixed_nested_draft(1.0, SourceIdV1::new(1), input, input); draft.push_finite_target( target, - PackageProgramSourceIdV1::new(1), + SourceIdV1::new(1), vec![ - PackageProgramTargetCandidateV1::new(first, encoded_srgb8), - PackageProgramTargetCandidateV1::new(duplicate, encoded_srgb8), + TargetCandidateV1::new(first, encoded_srgb8), + TargetCandidateV1::new(duplicate, encoded_srgb8), ], ); attach_target_assessment(&mut draft, target); assert_eq!( compile_error(draft), - PackageProgramCompileErrorV1::DuplicateTargetCandidateSignal { + CompileErrorV1::DuplicateTargetCandidateSignal { target, first, duplicate, @@ -640,71 +714,56 @@ fn duplicate_candidate_signal_preserves_both_candidates_and_exact_stimulus() { #[test] fn joint_diagnostics_preserve_state_and_total_order_details() { - let input = PackageProgramSurfaceInputPortIdV1::new(50); - let target = PackageProgramTargetIdV1::new(70); - let first = PackageProgramTargetCandidateIdV1::new(701); - let second = PackageProgramTargetCandidateIdV1::new(702); + let input = SurfaceInputPortIdV1::new(50); + let target = TargetIdV1::new(70); + let first = TargetCandidateIdV1::new(701); + let second = TargetCandidateIdV1::new(702); let candidates = vec![ - PackageProgramTargetCandidateV1::new(first, Srgb8::new([0; 3])), - PackageProgramTargetCandidateV1::new(second, Srgb8::new([255; 3])), + TargetCandidateV1::new(first, Srgb8::new([0; 3])), + TargetCandidateV1::new(second, Srgb8::new([255; 3])), ]; - let mut duplicate_target = - fixed_nested_draft(1.0, PackageProgramSourceIdV1::new(1), input, input); - duplicate_target.push_finite_target( - target, - PackageProgramSourceIdV1::new(1), - candidates.clone(), - ); + 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()); attach_target_assessment(&mut duplicate_target, target); duplicate_target - .set_joint_selection(vec![PackageProgramJointStateV1::new(vec![ - PackageProgramJointChoiceV1::new(target, first), - PackageProgramJointChoiceV1::new(target, second), + .set_joint_selection(vec![JointStateV1::new(vec![ + JointChoiceV1::new(target, first), + JointChoiceV1::new(target, second), ])]) .unwrap(); assert_eq!( compile_error(duplicate_target), - PackageProgramCompileErrorV1::JointStateDuplicateTarget { state: 0, target } + CompileErrorV1::JointStateDuplicateTarget { state: 0, target } ); - let mut incomplete = fixed_nested_draft(1.0, PackageProgramSourceIdV1::new(1), input, input); - incomplete.push_finite_target(target, PackageProgramSourceIdV1::new(1), candidates); + let mut incomplete = fixed_nested_draft(1.0, SourceIdV1::new(1), input, input); + incomplete.push_finite_target(target, SourceIdV1::new(1), candidates); attach_target_assessment(&mut incomplete, target); incomplete - .set_joint_selection(vec![PackageProgramJointStateV1::new(vec![ - PackageProgramJointChoiceV1::new(target, first), - ])]) + .set_joint_selection(vec![JointStateV1::new(vec![JointChoiceV1::new( + target, first, + )])]) .unwrap(); assert_eq!( compile_error(incomplete), - PackageProgramCompileErrorV1::InvalidJointOrder( - PackageProgramJointOrderErrorV1::IncompleteOrder { - expected: 2, - actual: 1, - } - ) + CompileErrorV1::InvalidJointOrder(JointOrderErrorV1::IncompleteOrder { + expected: 2, + actual: 1, + }) ); } #[test] fn dependency_cycles_retain_all_typed_core_members_without_reallocation() { - let input = PackageProgramSurfaceInputPortIdV1::new(50); - let first_paint = PackageProgramPaintIdV1::new(70); - let second_paint = PackageProgramPaintIdV1::new(71); - let mut paint_cycle = fixed_nested_draft(1.0, PackageProgramSourceIdV1::new(1), input, input); - paint_cycle.push_opacity_paint( - first_paint, - second_paint, - PackageProgramOpacityInputIdV1::new(3), - ); - paint_cycle.push_opacity_paint( - second_paint, - first_paint, - PackageProgramOpacityInputIdV1::new(3), - ); + let input = SurfaceInputPortIdV1::new(50); + let first_paint = PaintIdV1::new(70); + let second_paint = PaintIdV1::new(71); + let mut paint_cycle = fixed_nested_draft(1.0, SourceIdV1::new(1), input, input); + paint_cycle.push_opacity_paint(first_paint, second_paint, OpacityInputIdV1::new(3)); + paint_cycle.push_opacity_paint(second_paint, first_paint, OpacityInputIdV1::new(3)); let error = compile_error(paint_cycle); - let PackageProgramCompileErrorV1::PaintCycle(cycle) = error else { + let CompileErrorV1::PaintCycle(cycle) = error else { panic!("expected an exact paint cycle") }; assert_eq!( @@ -712,21 +771,19 @@ fn dependency_cycles_retain_all_typed_core_members_without_reallocation() { [first_paint, second_paint] ); - let cyclic_surface = PackageProgramSurfaceIdV1::new(80); - let cyclic_occurrence = PackageProgramOccurrenceIdV1::new(81); - let context = - PackageProgramAppearanceContextV1::try_new(64.0, 0.2, PackageProgramSurroundV1::Average) - .unwrap(); - let mut render_cycle = fixed_nested_draft(1.0, PackageProgramSourceIdV1::new(1), input, input); + let cyclic_surface = SurfaceIdV1::new(80); + let cyclic_occurrence = OccurrenceIdV1::new(81); + let context = AppearanceContextV1::try_new(64.0, 0.2, SurroundV1::Average).unwrap(); + let mut render_cycle = fixed_nested_draft(1.0, SourceIdV1::new(1), input, input); render_cycle.push_occurrence_surface(cyclic_surface, cyclic_occurrence); render_cycle.push_source_over_occurrence( cyclic_occurrence, - PackageProgramPaintIdV1::new(4), + PaintIdV1::new(4), cyclic_surface, context, ); let error = compile_error(render_cycle); - let PackageProgramCompileErrorV1::RenderCycle(cycle) = error else { + let CompileErrorV1::RenderCycle(cycle) = error else { panic!("expected an exact render cycle") }; assert_eq!(cycle.surfaces().collect::>(), [cyclic_surface]); @@ -734,23 +791,20 @@ fn dependency_cycles_retain_all_typed_core_members_without_reallocation() { } #[test] -fn the_code_owned_observation_group_reports_package_authored_port_semantics() { +fn the_code_owned_observation_group_reports_public_authored_port_semantics() { assert_eq!( - compile_error(PackageProgramDraftV1::new()), - PackageProgramCompileErrorV1::EmptySurfaceInputPortSet + compile_error(DraftV1::new()), + CompileErrorV1::EmptySurfaceInputPortSet ); } #[test] fn singleton_joint_order_cannot_be_silently_replaced() { - let mut draft = PackageProgramDraftV1::new(); + let mut draft = DraftV1::new(); draft.set_joint_selection(vec![]).unwrap(); let error = match draft.set_joint_selection(vec![]) { Ok(_) => panic!("a singleton declaration must not be replaced"), Err(error) => error, }; - assert_eq!( - error, - PackageProgramDraftErrorV1::JointSelectionAlreadyDeclared - ); + assert_eq!(error, DraftErrorV1::JointSelectionAlreadyDeclared); } diff --git a/crates/labcolors-core/tests/program_public_api.rs b/crates/labcolors-core/tests/program_public_api.rs new file mode 100644 index 00000000..be7fbb76 --- /dev/null +++ b/crates/labcolors-core/tests/program_public_api.rs @@ -0,0 +1,58 @@ +//! Минимальный внешний контракт лаконичной публичной поверхности Program. + +use labcolors_core::{Srgb8, program}; + +#[test] +fn public_program_api_is_module_qualified_without_transport_prefixes() { + let source = program::SourceIdV1::new(1); + let target = program::TargetIdV1::new(2); + let input = program::SurfaceInputPortIdV1::new(3); + let paint = program::PaintIdV1::new(4); + let surface = program::SurfaceIdV1::new(5); + let occurrence = program::OccurrenceIdV1::new(6); + let constraint = program::ConstraintIdV1::new(7); + let output = program::OutputSlotIdV1::new(8); + let context = + program::AppearanceContextV1::try_new(64.0, 0.2, program::SurroundV1::Average).unwrap(); + let mut draft = program::DraftV1::new(); + + draft.push_source(source, Srgb8::new([0, 0, 0])); + draft.push_fixed_target(target, source); + draft.push_surface_input_port(input); + draft.push_solid_paint(paint, target); + draft.push_input_surface(surface, input); + draft.push_source_over_occurrence(occurrence, paint, surface, context); + draft.push_exact_hard(constraint, occurrence, Srgb8::new([0, 0, 0])); + draft.push_output(output, paint); + + let owner = draft.compile().unwrap(); + let mut session = owner.instantiate(1).unwrap(); + let white = [Srgb8::new([255, 255, 255])]; + let scenarios = [program::ScenarioV1::new(1, &white)]; + let projection = owner + .update( + &mut session, + program::UpdateV1::Observed { + revision: 1, + scenarios: &scenarios, + }, + ) + .unwrap(); + let Some(program::OperationV1::Set(set)) = projection.operations().next() else { + panic!("the exact program must emit one certified Set"); + }; + assert_eq!(set.output_slot(), output); + assert_eq!(set.source(), Srgb8::new([0, 0, 0])); +} + +#[test] +fn public_internal_failure_keeps_fact_and_contract_as_one_consistent_value() { + let source = program::UpdateInvariantFailureV1::OwnerAuthority; + assert_eq!( + source.contract(), + program::UpdateInvariantV1::OwnerAuthority + ); + + let error = program::UpdateErrorV1::InternalInvariant { source }; + assert_eq!(error.kind(), program::UpdateErrorKindV1::InternalInvariant); +} From f2fb118f4ab1a49fcf2118db4aa92cda11507e60 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Sat, 25 Jul 2026 03:26:03 +0300 Subject: [PATCH 48/58] core: address Program API review --- .../src/generic_boundary_tests.rs | 57 ++++-- crates/labcolors-core/src/program.rs | 38 ++-- .../src/program_mixed_evaluator_tests.rs | 163 ++++++++++-------- .../labcolors-core/tests/program_boundary.rs | 2 +- 4 files changed, 157 insertions(+), 103 deletions(-) diff --git a/crates/labcolors-core/src/generic_boundary_tests.rs b/crates/labcolors-core/src/generic_boundary_tests.rs index 3680b13b..e180305e 100644 --- a/crates/labcolors-core/src/generic_boundary_tests.rs +++ b/crates/labcolors-core/src/generic_boundary_tests.rs @@ -69,6 +69,48 @@ fn contains_rust_identifier(source: &str, identifier: &str) -> bool { }) } +fn assert_only_in_compile_fail(source: &str, needle: &str) { + let mut in_compile_fail = false; + let mut occurrences = 0; + + for (line_index, line) in source.lines().enumerate() { + let doc = line.trim_start().strip_prefix("///").map(str::trim_start); + match doc { + Some("```compile_fail") => { + assert!( + !in_compile_fail, + "nested compile_fail fence before line {}", + line_index + 1, + ); + in_compile_fail = true; + continue; + } + Some("```") if in_compile_fail => { + in_compile_fail = false; + continue; + } + _ => {} + } + + let line_occurrences = line.matches(needle).count(); + if line_occurrences == 0 { + continue; + } + assert!( + in_compile_fail, + "`{needle}` escaped its negative compile_fail sentinel at line {}", + line_index + 1, + ); + occurrences += line_occurrences; + } + + assert!(!in_compile_fail, "unclosed compile_fail fence"); + assert!( + occurrences > 0, + "`{needle}` must remain covered by a negative API sentinel", + ); +} + fn production_rust_sources() -> Vec<(String, String)> { let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src"); let mut pending = vec![root.clone()]; @@ -163,8 +205,7 @@ fn stacked_program_module_is_visible_module_qualified_and_transport_neutral() { ); assert!( !PROGRAM_SOURCE.contains("PackageProgram") - && !contains_rust_identifier(PROGRAM_SOURCE, "package_bridge") - && !PROGRAM_SOURCE.contains("package "), + && !contains_rust_identifier(PROGRAM_SOURCE, "package_bridge"), "the public Program source must not retain transport-era vocabulary", ); @@ -178,16 +219,8 @@ fn stacked_program_module_is_visible_module_qualified_and_transport_neutral() { "{path} must not retain the superseded public path or prefix", ); } - assert_eq!( - LIB_SOURCE.matches("PackageProgram").count(), - 2, - "the old prefix may appear only in the two negative API sentinels", - ); - assert_eq!( - LIB_SOURCE.matches("package_bridge").count(), - 2, - "the old module may appear only in the two negative API sentinels", - ); + assert_only_in_compile_fail(LIB_SOURCE, "PackageProgram"); + assert_only_in_compile_fail(LIB_SOURCE, "package_bridge"); } #[test] diff --git a/crates/labcolors-core/src/program.rs b/crates/labcolors-core/src/program.rs index 1221d0e7..976c045f 100644 --- a/crates/labcolors-core/src/program.rs +++ b/crates/labcolors-core/src/program.rs @@ -302,7 +302,7 @@ impl AppearanceContextErrorV1 { self.kind } - /// Возвращает отвергнутое поле, если отказ относится к входному домену. + /// Возвращает отвергнутое поле, когда Core смог его локализовать. pub const fn field(self) -> Option { self.field } @@ -336,7 +336,7 @@ impl AppearanceContextErrorV1 { }, None => Self { kind: AppearanceContextErrorKindV1::InternalInvariant, - field: None, + field: Some(field), reason: None, }, } @@ -909,8 +909,8 @@ impl CompileErrorV1 { | Self::JointStateDuplicateTarget { target, .. } | Self::JointStateMissingTarget { target, .. } | Self::JointStateUnknownTarget { target, .. } - | Self::JointStateUnknownCandidate { target, .. } => Some(Handle::Target(*target)), - Self::MissingTargetSource { target, .. } + | Self::JointStateUnknownCandidate { target, .. } + | Self::MissingTargetSource { target, .. } | Self::DuplicateTargetCandidate { target, .. } | Self::DuplicateTargetCandidateSignal { target, .. } => Some(Handle::Target(*target)), Self::DuplicateOpacityInput { input } | Self::OpacityOutOfDomain { input } => { @@ -2443,17 +2443,13 @@ pub enum OperationV1<'owner, 'session> { struct CertificatesV1<'a> { values: [Option>; 2], index: usize, - len: usize, } impl<'a> CertificatesV1<'a> { fn new(first: Option>, second: Option>) -> Self { - let len = usize::from(first.is_some()) + usize::from(second.is_some()); - debug_assert!(first.is_some() || second.is_none()); Self { values: [first, second], index: 0, - len, } } } @@ -2462,16 +2458,21 @@ impl<'a> Iterator for CertificatesV1<'a> { type Item = CertificateV1<'a>; fn next(&mut self) -> Option { - if self.index == self.len { - return None; + while self.index < self.values.len() { + let value = self.values[self.index]; + self.index += 1; + if value.is_some() { + return value; + } } - let value = self.values[self.index]; - self.index += 1; - value + None } fn size_hint(&self) -> (usize, Option) { - let remaining = self.len - self.index; + let remaining = self.values[self.index..] + .iter() + .filter(|value| value.is_some()) + .count(); (remaining, Some(remaining)) } } @@ -2745,6 +2746,8 @@ pub enum TristimulusComponentV1 { /// Точная конечная XYZ-точка и её зарегистрированный frame. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct TristimulusSampleV1 { + /// Биты IEEE-754 сохраняют точный диагностический payload и отделяют + /// равенство записи от семантики сравнения floating-point. xyz_bits: [u64; 3], frame: ColorimetricFrameV1, } @@ -4055,13 +4058,18 @@ mod update_error_projection_tests { } #[cfg(test)] -mod compile_error_projection_tests { +mod operation_scope_tests { use super::*; #[test] fn operation_scope_is_a_zero_sized_borrow_marker() { assert_eq!(core::mem::size_of::>(), 0); } +} + +#[cfg(test)] +mod compile_error_projection_tests { + use super::*; #[test] fn nested_joint_resource_exhaustion_keeps_its_exact_reason_and_site_kind() { diff --git a/crates/labcolors-core/src/program_mixed_evaluator_tests.rs b/crates/labcolors-core/src/program_mixed_evaluator_tests.rs index 3eef0ab1..999d2ac1 100644 --- a/crates/labcolors-core/src/program_mixed_evaluator_tests.rs +++ b/crates/labcolors-core/src/program_mixed_evaluator_tests.rs @@ -3,8 +3,8 @@ use core::iter::FusedIterator; use crate::Srgb8; use crate::appearance::{OccurrenceId, OpacityInputId, PaintId, SurfaceId, SurfaceInputPortId}; use crate::constraints::{ - ExactConstraintIdentityV1, ExactIdentityCapabilityV1, ExactIdentityReleaseV1, - ProgramVisiblePointBindingV1, + ApplicableWcag22MeasurementV1, ExactConstraintIdentityV1, ExactIdentityCapabilityV1, + ExactIdentityReleaseV1, ProgramVisiblePointBindingV1, }; use crate::lcs_occurrence::{ AdaptingLuminanceCdM2, AppearanceContextId, AppearanceContextSchemaReleaseId, @@ -16,9 +16,10 @@ use crate::observation::{ ObservedScenarioSetInput, Revision, ScenarioId, ScenarioInput, SurfaceInputBinding, }; use crate::program::{ - AccessErrorV1, AssessmentV1, CertificateV1, ConflictCellV1, ModeledPointV1, ObservationHeadV1, - ObservationV1, OperationV1, OutputSlotIdV1, OwnerV1, PhysicalPointV1, ProjectionV1, ScenarioV1, - SignalV1, StateKindV1, SurroundV1, UpdateErrorKindV1, UpdateV1, VerdictV1, VerifiedCellV1, + AccessErrorV1, AssessmentV1, CertificateV1, ConflictCellV1, ConstraintModeV1, + ExactSrgb8EvidenceV1, ModeledPointV1, ObservationHeadV1, ObservationV1, OperationV1, + OutputSlotIdV1, OwnerV1, PhysicalPointV1, ProjectionV1, ScenarioV1, SignalV1, StateKindV1, + SurroundV1, UpdateErrorKindV1, UpdateV1, VerdictV1, VerifiedCellV1, Wcag22Srgb8EvidenceV1, }; use crate::program_session::{ CORE_PROGRAM_ASSESSMENT_CALLS, CompiledCoreProgramV1, CompositionProfile, ConstraintId, @@ -318,83 +319,95 @@ fn assert_public_assessment_matches_core( core: &ProgramConstraintResultV1, expected_occurrence: OccurrenceId, ) { + fn assert_exact_matches( + public: ExactSrgb8EvidenceV1<'_>, + verdict: VerdictV1, + expected: Srgb8, + actual: Srgb8, + binding: &ProgramVisiblePointBindingV1, + expected_occurrence: OccurrenceId, + ) { + assert_eq!(public.verdict(), verdict); + assert_eq!(public.expected(), expected); + let (visible, _) = assert_public_binding_matches_core( + AssessmentV1::ExactSrgb8(public), + binding, + expected_occurrence, + ); + assert_eq!(visible, actual); + } + + fn assert_wcag_matches( + public: Wcag22Srgb8EvidenceV1<'_>, + verdict: VerdictV1, + measurement: &ApplicableWcag22MeasurementV1, + binding: &ProgramVisiblePointBindingV1, + expected_occurrence: OccurrenceId, + ) { + assert_eq!(public.verdict(), verdict); + assert_eq!(public.profile_id(), measurement.profile_id()); + assert_eq!(public.criterion(), measurement.criterion()); + assert_eq!( + public.foreground_luminance(), + measurement.measurement().foreground_luminance + ); + assert_eq!( + public.background_luminance(), + measurement.measurement().background_luminance + ); + assert_eq!(public.numerical_evidence(), measurement.evidence()); + let (visible, backdrop) = assert_public_binding_matches_core( + AssessmentV1::Wcag22Srgb8(public), + binding, + expected_occurrence, + ); + assert_eq!(visible, Srgb8::new(measurement.measurement().foreground)); + assert_eq!(backdrop, Srgb8::new(measurement.measurement().background)); + } + match (public, core) { ( AssessmentV1::ExactSrgb8(public), ProgramConstraintResultV1::Pass(CoreProgramPassEvidenceV1::ExactSrgb8(core)), - ) => { - assert_eq!(public.verdict(), VerdictV1::Pass); - assert_eq!(public.expected(), core.target()); - let (visible, _) = assert_public_binding_matches_core( - AssessmentV1::ExactSrgb8(public), - core.binding(), - expected_occurrence, - ); - assert_eq!(visible, core.actual()); - } + ) => assert_exact_matches( + public, + VerdictV1::Pass, + core.target(), + core.actual(), + core.binding(), + expected_occurrence, + ), ( AssessmentV1::ExactSrgb8(public), ProgramConstraintResultV1::Violation(CoreProgramViolationEvidenceV1::ExactSrgb8(core)), - ) => { - assert_eq!(public.verdict(), VerdictV1::Violation); - assert_eq!(public.expected(), core.target()); - let (visible, _) = assert_public_binding_matches_core( - AssessmentV1::ExactSrgb8(public), - core.binding(), - expected_occurrence, - ); - assert_eq!(visible, core.actual()); - } + ) => assert_exact_matches( + public, + VerdictV1::Violation, + core.target(), + core.actual(), + core.binding(), + expected_occurrence, + ), ( AssessmentV1::Wcag22Srgb8(public), ProgramConstraintResultV1::Pass(CoreProgramPassEvidenceV1::Wcag22Srgb8(core)), - ) => { - assert_eq!(public.verdict(), VerdictV1::Pass); - let measurement = core.measurement().value(); - assert_eq!(public.profile_id(), measurement.profile_id()); - assert_eq!(public.criterion(), measurement.criterion()); - assert_eq!( - public.foreground_luminance(), - measurement.measurement().foreground_luminance - ); - assert_eq!( - public.background_luminance(), - measurement.measurement().background_luminance - ); - assert_eq!(public.numerical_evidence(), measurement.evidence()); - let (visible, backdrop) = assert_public_binding_matches_core( - AssessmentV1::Wcag22Srgb8(public), - core.binding(), - expected_occurrence, - ); - assert_eq!(visible, Srgb8::new(measurement.measurement().foreground)); - assert_eq!(backdrop, Srgb8::new(measurement.measurement().background)); - } + ) => assert_wcag_matches( + public, + VerdictV1::Pass, + core.measurement().value(), + core.binding(), + expected_occurrence, + ), ( AssessmentV1::Wcag22Srgb8(public), ProgramConstraintResultV1::Violation(CoreProgramViolationEvidenceV1::Wcag22Srgb8(core)), - ) => { - assert_eq!(public.verdict(), VerdictV1::Violation); - let measurement = core.measurement().value(); - assert_eq!(public.profile_id(), measurement.profile_id()); - assert_eq!(public.criterion(), measurement.criterion()); - assert_eq!( - public.foreground_luminance(), - measurement.measurement().foreground_luminance - ); - assert_eq!( - public.background_luminance(), - measurement.measurement().background_luminance - ); - assert_eq!(public.numerical_evidence(), measurement.evidence()); - let (visible, backdrop) = assert_public_binding_matches_core( - AssessmentV1::Wcag22Srgb8(public), - core.binding(), - expected_occurrence, - ); - assert_eq!(visible, Srgb8::new(measurement.measurement().foreground)); - assert_eq!(backdrop, Srgb8::new(measurement.measurement().background)); - } + ) => assert_wcag_matches( + public, + VerdictV1::Violation, + core.measurement().value(), + core.binding(), + expected_occurrence, + ), _ => panic!("public assessment family or verdict drifted from Core"), } } @@ -409,7 +422,7 @@ fn assert_verified_cell_matches_core( assert_eq!(public.constraint().value(), core.constraint().value()); assert_eq!(public.occurrence().value(), core.target().value()); assert_eq!( - matches!(public.mode(), crate::program::ConstraintModeV1::Hard), + matches!(public.mode(), ConstraintModeV1::Hard), core.is_hard() ); assert_public_assessment_matches_core(public.assessment(), core.result(), core.target()); @@ -424,7 +437,7 @@ fn assert_conflict_cell_matches_core( assert_eq!(public.constraint().value(), core.constraint().value()); assert_eq!(public.occurrence().value(), core.target().value()); assert_eq!( - matches!(public.mode(), crate::program::ConstraintModeV1::Hard), + matches!(public.mode(), ConstraintModeV1::Hard), core.is_hard() ); assert_public_assessment_matches_core(public.assessment(), core.result(), core.target()); @@ -617,8 +630,8 @@ fn consume_public_projection(projection: ProjectionV1<'_, '_>) -> ProjectionProb probe.mix(u64::from(cell.constraint().value())); probe.mix(u64::from(cell.occurrence().value())); probe.mix(match cell.mode() { - crate::program::ConstraintModeV1::Hard => 1, - crate::program::ConstraintModeV1::ReportOnly => 2, + ConstraintModeV1::Hard => 1, + ConstraintModeV1::ReportOnly => 2, }); consume_public_assessment(cell.assessment(), probe); }); @@ -639,8 +652,8 @@ fn consume_public_projection(projection: ProjectionV1<'_, '_>) -> ProjectionProb probe.mix(u64::from(cell.constraint().value())); probe.mix(u64::from(cell.occurrence().value())); probe.mix(match cell.mode() { - crate::program::ConstraintModeV1::Hard => 1, - crate::program::ConstraintModeV1::ReportOnly => 2, + ConstraintModeV1::Hard => 1, + ConstraintModeV1::ReportOnly => 2, }); consume_public_assessment(cell.assessment(), probe); }); diff --git a/crates/labcolors-core/tests/program_boundary.rs b/crates/labcolors-core/tests/program_boundary.rs index 64b2acd0..d3588da4 100644 --- a/crates/labcolors-core/tests/program_boundary.rs +++ b/crates/labcolors-core/tests/program_boundary.rs @@ -179,7 +179,7 @@ fn unknown_is_revision_bound_without_a_stream_or_generation_field( } #[allow(dead_code)] -fn owner_mismatch_is_a_closed_public_error(error: labcolors_core::program::UpdateErrorV1) { +fn owner_mismatch_is_a_closed_public_error(error: UpdateErrorV1) { assert_eq!(error.kind(), UpdateErrorKindV1::OwnerMismatch); } From 92f83760244ce7a1f31b540cadf69b12cd8bbfbc Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Sun, 26 Jul 2026 10:00:59 +0300 Subject: [PATCH 49/58] fix(ci): re-bind public Program proof capsule --- .../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 13d9b054..1ecfc3fc 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":"2e7c31a9b66fa310e0a7e33291bc167283157034703c47d86d7e22b5323acee6","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":"669326bce56a2901f7fbbd8b4c23f26f8b33daceb1471b81c98763940b41d3e4","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":"09be54900efe29ffdac8705efd0d6d613055c90d634446ca4b228f51a63997d0"},{"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":"e73b9c0b8c3a4112cb53987753d5a6b5f639774b0b872afaea9836e6647a2f7d"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/wcag22.rs","sha256":"856093c91159d8b3faab001f2d6524d33d7b16458a5a4e98ea65f8c62ab2694c"},{"kind":"rust-source","path":"crates/labcolors-core/src/hash.rs","sha256":"f97a0fd7d6ad3162f0f1dfb326fccfb7ed40da9a8fa67a5b8a239a1ae2ae49c3"},{"kind":"rust-source","path":"crates/labcolors-core/src/lcs_occurrence.rs","sha256":"6f202ad7425a235b9d18caba0c817fc33a2b8e042050a34f5ddff3fd09efc53d"},{"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":"887f139e6750cc99cd751b3c7e47276971293f74091130028a1746fa29ebb104"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"6f6a376ff036d3d65960c004e6566e1bca580f19f5bd3cd333a80b0da5b5c242"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"4f77643206077c080e5e9b182e896145bfb69bf3db8aa4c1ac7d4c5360ea8504"},{"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":"e93845aacc62968c9a21a1c7b8ef7222434b64c2100e3a177b3288051d03507d"} +{"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":"563b58088ed413b0a65c2c7d4282792559a756b9f9a51e03774d20afd8259b0a","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":"371a19f9da8337a13fb9a474c0f0395b35117822fd2d9293c6228b1f01e97ca4","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":"09be54900efe29ffdac8705efd0d6d613055c90d634446ca4b228f51a63997d0"},{"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":"51d7835a52b9eb4ab4888aef401ab145b1764eb2054ff7e856828fefc770a132"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/wcag22.rs","sha256":"856093c91159d8b3faab001f2d6524d33d7b16458a5a4e98ea65f8c62ab2694c"},{"kind":"rust-source","path":"crates/labcolors-core/src/hash.rs","sha256":"f97a0fd7d6ad3162f0f1dfb326fccfb7ed40da9a8fa67a5b8a239a1ae2ae49c3"},{"kind":"rust-source","path":"crates/labcolors-core/src/lcs_occurrence.rs","sha256":"6f202ad7425a235b9d18caba0c817fc33a2b8e042050a34f5ddff3fd09efc53d"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"28b21948812801582fc2c59e304df050fc131e3bfd0970fe34b3118c667f2885"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"8c9838107077775c51d80638ba0b59f9672d14347ca404dfd4c63e2fb62d1c45"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"6f6a376ff036d3d65960c004e6566e1bca580f19f5bd3cd333a80b0da5b5c242"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"4f77643206077c080e5e9b182e896145bfb69bf3db8aa4c1ac7d4c5360ea8504"},{"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":"e480c973e04de69c364ffb913a508e7cca82b8ec1760bc4a50634c2c5e8afc2a"} diff --git a/scripts/verify_point_support_surplus.py b/scripts/verify_point_support_surplus.py index 858b86ce..37c5fa71 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 = ( - "669326bce56a2901f7fbbd8b4c23f26f8b33daceb1471b81c98763940b41d3e4" + "371a19f9da8337a13fb9a474c0f0395b35117822fd2d9293c6228b1f01e97ca4" ) EXPECTED_Q55_PROOF_SHA256 = ( "ac59cf89503170c789223b91d775213a19d4e571ef930f2ea609fcd51b14defd" From 4eb89663e74daf70553c8735538382e6e3d6b5a1 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Sat, 25 Jul 2026 03:43:48 +0300 Subject: [PATCH 50/58] test: close compile-fail sentinel escape --- .../src/generic_boundary_tests.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/crates/labcolors-core/src/generic_boundary_tests.rs b/crates/labcolors-core/src/generic_boundary_tests.rs index e180305e..4a133ad0 100644 --- a/crates/labcolors-core/src/generic_boundary_tests.rs +++ b/crates/labcolors-core/src/generic_boundary_tests.rs @@ -92,12 +92,17 @@ fn assert_only_in_compile_fail(source: &str, needle: &str) { _ => {} } + assert!( + !in_compile_fail || doc.is_some() || line.trim().is_empty(), + "compile_fail sentinel was interrupted by live code at line {}", + line_index + 1, + ); let line_occurrences = line.matches(needle).count(); if line_occurrences == 0 { continue; } assert!( - in_compile_fail, + in_compile_fail && doc.is_some(), "`{needle}` escaped its negative compile_fail sentinel at line {}", line_index + 1, ); @@ -111,6 +116,16 @@ fn assert_only_in_compile_fail(source: &str, needle: &str) { ); } +#[test] +fn compile_fail_scanner_rejects_live_code_between_document_fences() { + let escaped = "/// ```compile_fail\npub type PackageProgram = u8;\n/// ```"; + assert!( + std::panic::catch_unwind(|| assert_only_in_compile_fail(escaped, "PackageProgram")) + .is_err(), + "a live declaration must never inherit compile_fail state from adjacent documentation", + ); +} + fn production_rust_sources() -> Vec<(String, String)> { let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src"); let mut pending = vec![root.clone()]; From 4fc952195374b13c7a2be5ced659a2332647dea5 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Sat, 25 Jul 2026 19:09:07 +0300 Subject: [PATCH 51/58] core: make joint order non-empty by construction and bound evidence cells `AdmittedFiniteJointOrderV1` stored one flat tuple slice, so an empty admitted order was representable and the evaluation path carried a runtime `state_count == 0 -> InternalInvariant` guard to reject it. The guard proved nothing about the type; it only re-checked a property the constructor already enforced. Split the order into `first + rest`. Non-emptiness becomes structural, `state_count()` is total, and the `InternalInvariant` branch in `prepare_program_evaluation_buffers` is deleted rather than left dead. The `joint_state_count: Option` parameter disappears with it: cell counts are now derived from the epoch itself. The same pass stops reserving an exhaustive-conflict buffer that no constraint can ever fill. `can_conflict` is false when every compiled constraint is report-only, so a report-only program no longer reserves `cases x constraints x states` cells and no longer reports `ResourceExhausted` for a conflict it cannot produce. `OwnerV1::evidence_cell_bounds` exposes the same arithmetic as a pure preflight, with `EvidenceBoundsErrorV1::CardinalityOverflow` as the only closed failure. It creates no Session and mutates no state. Verified locally on the CI-pinned toolchain: full workspace tests green, `cargo fmt --all --check` and `cargo clippy --workspace --all-targets -- -D warnings` clean. Co-Authored-By: Claude --- crates/labcolors-core/src/joint.rs | 29 ++- crates/labcolors-core/src/program.rs | 51 +++++ crates/labcolors-core/src/program_session.rs | 71 ++++-- .../labcolors-core/tests/program_boundary.rs | 207 +++++++++++++++++- 4 files changed, 325 insertions(+), 33 deletions(-) diff --git a/crates/labcolors-core/src/joint.rs b/crates/labcolors-core/src/joint.rs index 922cee76..60318a07 100644 --- a/crates/labcolors-core/src/joint.rs +++ b/crates/labcolors-core/src/joint.rs @@ -44,12 +44,19 @@ impl FiniteDomainOrdinalV1 { /// becomes an implicit tie-break. #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct AdmittedFiniteJointOrderV1 { - tuples: Box<[Box<[FiniteDomainOrdinalV1]>]>, + first: Box<[FiniteDomainOrdinalV1]>, + rest: Box<[Box<[FiniteDomainOrdinalV1]>]>, } impl AdmittedFiniteJointOrderV1 { - pub(crate) fn tuples(&self) -> impl ExactSizeIterator + '_ { - self.tuples.iter().map(Box::as_ref) + pub(crate) fn tuples(&self) -> impl Iterator + '_ { + std::iter::once(self.first.as_ref()).chain(self.rest.iter().map(Box::as_ref)) + } + + pub(crate) fn state_count(&self) -> usize { + // `first` makes the admitted order structurally non-empty; the + // remaining slice length is bounded by Rust's allocation limit. + self.rest.len() + 1 } } @@ -120,10 +127,10 @@ pub(crate) fn admit_finite_joint_order_v1( .map_err(|_| FiniteJointOrderErrorV1::ResourceExhausted)?; first_seen.resize(expected, usize::MAX); - let mut tuples = Vec::new(); - tuples - .try_reserve_exact(authored.len()) + let mut rest = Vec::new(); + rest.try_reserve_exact(authored.len() - 1) .map_err(|_| FiniteJointOrderErrorV1::ResourceExhausted)?; + let mut first_tuple = None; for (tuple_index, tuple) in authored.into_iter().enumerate() { if tuple.len() != domain_lengths.len() { return Err(FiniteJointOrderErrorV1::TupleArity { @@ -157,11 +164,17 @@ pub(crate) fn admit_finite_joint_order_v1( }); } *first = tuple_index; - tuples.push(tuple.into_boxed_slice()); + let tuple = tuple.into_boxed_slice(); + if first_tuple.is_none() { + first_tuple = Some(tuple); + } else { + rest.push(tuple); + } } Ok(AdmittedFiniteJointOrderV1 { - tuples: tuples.into_boxed_slice(), + first: first_tuple.ok_or(FiniteJointOrderErrorV1::EmptyOrder)?, + rest: rest.into_boxed_slice(), }) } use crate::session::SessionObservationBindingPermitV1; diff --git a/crates/labcolors-core/src/program.rs b/crates/labcolors-core/src/program.rs index 976c045f..c8e8558a 100644 --- a/crates/labcolors-core/src/program.rs +++ b/crates/labcolors-core/src/program.rs @@ -1258,6 +1258,37 @@ pub struct OwnerV1 { compiled: CompiledCoreProgramV1, } +/// Верхние границы числа клеток в новом сертификате одного Observed-update. +/// +/// Границы относятся только к текущим клеткам доказательства. Они не включают +/// сохранённый прошлый сертификат, observation/provenance, выходы, операции или +/// байты конкретного транспорта. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct EvidenceCellBoundsV1 { + verified_cells: usize, + conflict_cells: usize, +} + +impl EvidenceCellBoundsV1 { + /// Максимум клеток успешного сертификата. + pub const fn verified_cells(self) -> usize { + self.verified_cells + } + + /// Максимум клеток исчерпывающего конфликтного сертификата. + pub const fn conflict_cells(self) -> usize { + self.conflict_cells + } +} + +/// Закрытая причина невозможности вычислить границы сертификата. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EvidenceBoundsErrorV1 { + /// Произведение числа сценариев, ограничений и состояний не помещается в + /// адресное пространство платформы. + CardinalityOverflow, +} + /// Отказ доступа из-за несовпадения точной owner-эпохи. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AccessErrorV1 { @@ -1279,6 +1310,26 @@ impl OwnerV1 { ContentIdentityV1::from_core(self.compiled.content_identity()) } + /// Вычисляет верхние границы клеток для prospective Observed-update. + /// + /// `scenario_count` — число объявленных клиентом сценариев до admission. + /// Core сам схлопывает физически одинаковые сценарии, поэтому фактический + /// сертификат может быть короче. Нулевое значение разрешено только как + /// чистый арифметический preflight; пустой Observed-update по-прежнему не + /// допускается. Запрос не создаёт Session и не меняет состояние. + pub fn evidence_cell_bounds( + &self, + scenario_count: usize, + ) -> Result { + self.compiled + .evidence_cell_bounds(scenario_count) + .map(|(verified_cells, conflict_cells)| EvidenceCellBoundsV1 { + verified_cells, + conflict_cells, + }) + .ok_or(EvidenceBoundsErrorV1::CardinalityOverflow) + } + /// Число значений Surface в каждом schema-ordered сценарии. pub fn surface_input_port_count(&self) -> usize { self.compiled.surface_input_ports().len() diff --git a/crates/labcolors-core/src/program_session.rs b/crates/labcolors-core/src/program_session.rs index 83d151c3..b45e45b3 100644 --- a/crates/labcolors-core/src/program_session.rs +++ b/crates/labcolors-core/src/program_session.rs @@ -1014,6 +1014,12 @@ enum CompiledConstraintModeV1 { ReportOnly, } +impl CompiledConstraintModeV1 { + const fn rejects_candidate(self) -> bool { + matches!(self, Self::Hard) + } +} + struct CompiledPointConstraint { id: ConstraintId, target_id: OccurrenceId, @@ -1127,6 +1133,11 @@ where self.owner_generation.outputs.len() } + pub(crate) fn evidence_cell_bounds(&self, scenario_count: usize) -> Option<(usize, usize)> { + checked_program_epoch_evaluation_cell_counts(&self.owner_generation, scenario_count) + .map(|counts| (counts.selected, counts.exhaustive_conflict)) + } + pub(crate) fn output_slot_at(&self, index: usize) -> Option { self.owner_generation .outputs @@ -1268,7 +1279,7 @@ where } pub const fn is_hard(&self) -> bool { - matches!(self.mode, CompiledConstraintModeV1::Hard) + self.mode.rejects_candidate() } pub const fn result(&self) -> &ProgramConstraintResultV1 { @@ -1466,22 +1477,52 @@ fn checked_program_evaluation_cell_counts( physical_case_count: usize, constraint_count: usize, state_count: usize, + can_conflict: bool, ) -> Option { let selected = physical_case_count.checked_mul(constraint_count)?; - let exhaustive_conflict = selected.checked_mul(state_count)?; + let exhaustive_conflict = if can_conflict { + selected.checked_mul(state_count)? + } else { + 0 + }; Some(ProgramEvaluationCellCountsV1 { selected, exhaustive_conflict, }) } +fn checked_program_epoch_evaluation_cell_counts( + epoch: &ProgramEpochV1, + physical_case_count: usize, +) -> Option +where + Evaluation: ProgramConstraintEvaluatorSetV1, + ProgramConstraintInvocationOf: Copy, +{ + let state_count = epoch + .joint_selection + .as_ref() + .map(|selection| selection.order.state_count()) + .unwrap_or(1); + let can_conflict = epoch + .constraints + .iter() + .any(|constraint| constraint.mode.rejects_candidate()); + checked_program_evaluation_cell_counts( + physical_case_count, + epoch.constraints.len(), + state_count, + can_conflict, + ) +} + #[cfg(test)] pub(crate) fn checked_program_evaluation_cell_counts_for_test( physical_case_count: usize, constraint_count: usize, state_count: usize, ) -> Option<(usize, usize)> { - checked_program_evaluation_cell_counts(physical_case_count, constraint_count, state_count) + checked_program_evaluation_cell_counts(physical_case_count, constraint_count, state_count, true) .map(|counts| (counts.selected, counts.exhaustive_conflict)) } @@ -1585,7 +1626,6 @@ where fn prepare_program_evaluation_buffers( epoch: &ProgramEpochV1, observation: &RevisionBoundObservationV1, - joint_state_count: Option, ) -> Result< PreparedProgramEvaluationBuffersV1, ProgramSessionEvaluationError>, @@ -1594,22 +1634,15 @@ where Evaluation: ProgramConstraintEvaluatorSetV1, ProgramConstraintInvocationOf: Copy, { - let state_count = joint_state_count.unwrap_or(1); - if state_count == 0 { - return Err(ProgramSessionEvaluationError::InternalInvariant); - } - let counts = checked_program_evaluation_cell_counts( - observation.physical_case_count(), - epoch.constraints.len(), - state_count, - ) - .ok_or(ProgramSessionEvaluationError::ResourceExhausted)?; + let counts = + checked_program_epoch_evaluation_cell_counts(epoch, observation.physical_case_count()) + .ok_or(ProgramSessionEvaluationError::ResourceExhausted)?; let mut selected_cells = Vec::new(); try_reserve_program_evaluation_buffer(&mut selected_cells, counts.selected) .map_err(|()| ProgramSessionEvaluationError::ResourceExhausted)?; let mut conflict_cells = Vec::new(); - if joint_state_count.is_some() { + if epoch.joint_selection.is_some() && counts.exhaustive_conflict != 0 { try_reserve_program_evaluation_buffer(&mut conflict_cells, counts.exhaustive_conflict) .map_err(|()| ProgramSessionEvaluationError::ResourceExhausted)?; } @@ -1687,7 +1720,7 @@ where ProgramConstraintInvocationOf: Copy, { let Some(selection) = &epoch.joint_selection else { - let mut buffers = prepare_program_evaluation_buffers(epoch, &observation, None)?; + let mut buffers = prepare_program_evaluation_buffers(epoch, &observation)?; return collect_program_candidate_into( plan, epoch, @@ -1698,8 +1731,8 @@ where ); }; - let state_count = selection.order.tuples().len(); - let mut buffers = prepare_program_evaluation_buffers(epoch, &observation, Some(state_count))?; + let state_count = selection.order.state_count(); + let mut buffers = prepare_program_evaluation_buffers(epoch, &observation)?; for (state_index, tuple) in selection.order.tuples().enumerate() { apply_joint_candidate(plan, &epoch.finite_targets, tuple)?; if !scan_program_candidate(plan, epoch, &observation, state_index, None, None)? { @@ -1972,7 +2005,7 @@ where let result = match decision { HardDecision::Pass(evidence) => ProgramConstraintResultV1::Pass(evidence), HardDecision::Violation(evidence) => { - if matches!(constraint.mode, CompiledConstraintModeV1::Hard) { + if constraint.mode.rejects_candidate() { has_hard_violation = true; } ProgramConstraintResultV1::Violation(evidence) diff --git a/crates/labcolors-core/tests/program_boundary.rs b/crates/labcolors-core/tests/program_boundary.rs index d3588da4..e158ca95 100644 --- a/crates/labcolors-core/tests/program_boundary.rs +++ b/crates/labcolors-core/tests/program_boundary.rs @@ -10,12 +10,12 @@ use labcolors_core::Srgb8; use labcolors_core::program::{ AppearanceContextErrorKindV1, AppearanceContextFieldV1, AppearanceContextV1, AssessmentV1, CertificateV1, CompileErrorHandleV1, CompileErrorKindV1, CompileErrorV1, ConstraintIdV1, - DraftErrorV1, DraftV1, InstantiateErrorV1, JointChoiceV1, JointOrderErrorV1, JointStateV1, - ModeledPointV1, NumericDomainErrorV1, ObservationHeadV1, OccurrenceIdV1, OpacityInputIdV1, - OperationV1, OutputSlotIdV1, OwnerV1, PaintIdV1, PhysicalPointV1, ProjectionV1, ScenarioV1, - SessionV1, SignalV1, SourceIdV1, StateKindV1, SurfaceIdV1, SurfaceInputPortIdV1, SurroundV1, - TargetCandidateIdV1, TargetCandidateV1, TargetIdV1, UpdateErrorKindV1, UpdateErrorV1, UpdateV1, - VerdictV1, + DraftErrorV1, DraftV1, EvidenceBoundsErrorV1, InstantiateErrorV1, JointChoiceV1, + JointOrderErrorV1, JointStateV1, ModeledPointV1, NumericDomainErrorV1, ObservationHeadV1, + OccurrenceIdV1, OpacityInputIdV1, OperationV1, OutputSlotIdV1, OwnerV1, PaintIdV1, + PhysicalPointV1, ProjectionV1, ScenarioV1, SessionV1, SignalV1, SourceIdV1, StateKindV1, + SurfaceIdV1, SurfaceInputPortIdV1, SurroundV1, TargetCandidateIdV1, TargetCandidateV1, + TargetIdV1, UpdateErrorKindV1, UpdateErrorV1, UpdateV1, VerdictV1, }; use labcolors_core::wcag22::Wcag22CriterionV1; @@ -237,6 +237,201 @@ fn attach_target_assessment(draft: &mut DraftV1, target: TargetIdV1) { draft.push_exact_report_only(constraint, occurrence, Srgb8::new([0; 3])); } +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); + let input = SurfaceInputPortIdV1::new(5); + let paint = PaintIdV1::new(6); + let surface = SurfaceIdV1::new(7); + let occurrence = OccurrenceIdV1::new(8); + let constraint = ConstraintIdV1::new(9); + let output = OutputSlotIdV1::new(10); + 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![ + TargetCandidateV1::new(black, Srgb8::new([0; 3])), + TargetCandidateV1::new(white, Srgb8::new([255; 3])), + ], + ); + draft + .set_joint_selection(vec![ + JointStateV1::new(vec![JointChoiceV1::new(target, black)]), + JointStateV1::new(vec![JointChoiceV1::new(target, white)]), + ]) + .unwrap(); + draft.push_surface_input_port(input); + draft.push_solid_paint(paint, target); + draft.push_input_surface(surface, input); + draft.push_source_over_occurrence(occurrence, paint, surface, context); + if hard { + draft.push_exact_hard(constraint, occurrence, Srgb8::new([128; 3])); + } else { + draft.push_exact_report_only(constraint, occurrence, Srgb8::new([0; 3])); + } + draft.push_output(output, paint); + draft +} + +#[test] +fn evidence_cell_bounds_cover_fixed_and_joint_evaluation_laws() { + let input = SurfaceInputPortIdV1::new(50); + let fixed = fixed_nested_draft(1.0, SourceIdV1::new(1), input, input) + .compile() + .unwrap(); + + let empty = fixed.evidence_cell_bounds(0).unwrap(); + assert_eq!(empty.verified_cells(), 0); + assert_eq!(empty.conflict_cells(), 0); + + // The second fixed constraint is report-only. Counting only hard + // constraints would under-reserve a successful certificate. + let fixed_bounds = fixed.evidence_cell_bounds(3).unwrap(); + assert_eq!(fixed_bounds.verified_cells(), 6); + assert_eq!(fixed_bounds.conflict_cells(), 6); + + let report_only_joint = joint_draft(false).compile().unwrap(); + let report_only_bounds = report_only_joint.evidence_cell_bounds(4).unwrap(); + assert_eq!(report_only_bounds.verified_cells(), 4); + assert_eq!(report_only_bounds.conflict_cells(), 0); + + let hard_joint = joint_draft(true).compile().unwrap(); + let hard_bounds = hard_joint.evidence_cell_bounds(4).unwrap(); + assert_eq!(hard_bounds.verified_cells(), 4); + assert_eq!(hard_bounds.conflict_cells(), 8); +} + +#[test] +fn evidence_cell_bounds_report_both_checked_multiplication_overflows() { + let input = SurfaceInputPortIdV1::new(50); + let two_constraints = fixed_nested_draft(1.0, SourceIdV1::new(1), input, input) + .compile() + .unwrap(); + assert!(matches!( + two_constraints.evidence_cell_bounds(usize::MAX), + Err(EvidenceBoundsErrorV1::CardinalityOverflow) + )); + + // One constraint keeps the first product representable; the two-state + // joint order forces the independent exhaustive-conflict product to fail. + let two_states = joint_draft(true).compile().unwrap(); + assert!(matches!( + two_states.evidence_cell_bounds(usize::MAX), + Err(EvidenceBoundsErrorV1::CardinalityOverflow) + )); +} + +#[test] +fn evidence_cell_bounds_cover_actual_joint_conflict_and_duplicate_case_reduction() { + let joint = joint_draft(true).compile().unwrap(); + let mut joint_session = joint.instantiate(21).unwrap(); + let red = [Srgb8::new([255, 0, 0])]; + let blue = [Srgb8::new([0, 0, 255])]; + let unique_scenarios = [ScenarioV1::new(1, &red), ScenarioV1::new(2, &blue)]; + let joint_bounds = joint.evidence_cell_bounds(unique_scenarios.len()).unwrap(); + let projection = joint + .update( + &mut joint_session, + UpdateV1::Observed { + revision: 1, + scenarios: &unique_scenarios, + }, + ) + .unwrap(); + let Some(CertificateV1::Conflict(certificate)) = projection.evidence().certificates().next() + else { + panic!("both authored joint states must violate the hard exact constraint"); + }; + assert_eq!(certificate.cells().len(), joint_bounds.conflict_cells()); + + let input = SurfaceInputPortIdV1::new(50); + let fixed = fixed_nested_draft(1.0, SourceIdV1::new(1), input, input) + .compile() + .unwrap(); + let mut fixed_session = fixed.instantiate(22).unwrap(); + let white = [Srgb8::new([0xFF; 3])]; + let duplicate_scenarios = [ScenarioV1::new(1, &white), ScenarioV1::new(2, &white)]; + let fixed_bounds = fixed + .evidence_cell_bounds(duplicate_scenarios.len()) + .unwrap(); + let projection = fixed + .update( + &mut fixed_session, + UpdateV1::Observed { + revision: 1, + scenarios: &duplicate_scenarios, + }, + ) + .unwrap(); + let Some(CertificateV1::Verified(certificate)) = projection.evidence().certificates().next() + else { + panic!("duplicate physical scenarios must preserve a valid certificate"); + }; + assert!(certificate.cells().len() < fixed_bounds.verified_cells()); +} + +#[test] +fn evidence_cell_bounds_query_is_pure_across_session_updates() { + let input = SurfaceInputPortIdV1::new(50); + let owner = fixed_nested_draft(1.0, SourceIdV1::new(1), input, input) + .compile() + .unwrap(); + let expected = owner.evidence_cell_bounds(1).unwrap(); + assert_eq!(expected.verified_cells(), 2); + assert_eq!(expected.conflict_cells(), 2); + + let mut session = owner.instantiate(13).unwrap(); + let after_instantiation = owner.evidence_cell_bounds(1).unwrap(); + assert_eq!( + after_instantiation.verified_cells(), + expected.verified_cells() + ); + assert_eq!( + after_instantiation.conflict_cells(), + expected.conflict_cells() + ); + + let white = [Srgb8::new([0xFF; 3])]; + let scenarios = [ScenarioV1::new(1, &white)]; + { + let projection = owner + .update( + &mut session, + UpdateV1::Observed { + revision: 1, + scenarios: &scenarios, + }, + ) + .unwrap(); + let Some(CertificateV1::Verified(certificate)) = + projection.evidence().certificates().next() + else { + panic!("the fixed admissible program must produce Verified evidence"); + }; + assert_eq!(certificate.cells().len(), expected.verified_cells()); + } + + let after_update = owner.evidence_cell_bounds(1).unwrap(); + assert_eq!(after_update.verified_cells(), expected.verified_cells()); + assert_eq!(after_update.conflict_cells(), expected.conflict_cells()); + let projection = owner + .update( + &mut session, + UpdateV1::Observed { + revision: 2, + scenarios: &scenarios, + }, + ) + .unwrap(); + assert_eq!(projection.evidence().kind(), StateKindV1::Ready); +} + #[test] fn external_authoring_lowers_the_actual_closed_program_and_returns_canonical_input_ports() { let source = SourceIdV1::new(91); From 412da832f86c9e7f417cc2e69bfd16f26a83ffab Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Sun, 26 Jul 2026 11:07:43 +0300 Subject: [PATCH 52/58] docs: explain packed proof invariants --- crates/labcolors-core/src/joint.rs | 2 ++ crates/labcolors-core/src/program_session.rs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/crates/labcolors-core/src/joint.rs b/crates/labcolors-core/src/joint.rs index 60318a07..64689f67 100644 --- a/crates/labcolors-core/src/joint.rs +++ b/crates/labcolors-core/src/joint.rs @@ -173,6 +173,8 @@ pub(crate) fn admit_finite_joint_order_v1( } Ok(AdmittedFiniteJointOrderV1 { + // Empty input returned above, so the loop always materialises a first + // tuple; keep the typed branch instead of encoding that proof as panic. first: first_tuple.ok_or(FiniteJointOrderErrorV1::EmptyOrder)?, rest: rest.into_boxed_slice(), }) diff --git a/crates/labcolors-core/src/program_session.rs b/crates/labcolors-core/src/program_session.rs index b45e45b3..de47e97f 100644 --- a/crates/labcolors-core/src/program_session.rs +++ b/crates/labcolors-core/src/program_session.rs @@ -1503,6 +1503,8 @@ where .joint_selection .as_ref() .map(|selection| selection.order.state_count()) + // Without joint selection the epoch has one fixed configuration, so + // the exhaustive-cell multiplier remains the multiplicative identity. .unwrap_or(1); let can_conflict = epoch .constraints From f57baa2426757f60ebaa49d435e7097d850a55a8 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Sun, 26 Jul 2026 10:28:09 +0300 Subject: [PATCH 53/58] core: fail closed without current Program evidence --- .../src/generic_boundary_tests.rs | 8 +- crates/labcolors-core/src/program.rs | 84 ++----------- .../src/program_mixed_evaluator_tests.rs | 56 +++------ .../labcolors-core/tests/program_boundary.rs | 111 +++++++++++++++++- 4 files changed, 139 insertions(+), 120 deletions(-) diff --git a/crates/labcolors-core/src/generic_boundary_tests.rs b/crates/labcolors-core/src/generic_boundary_tests.rs index 4a133ad0..7bb06faf 100644 --- a/crates/labcolors-core/src/generic_boundary_tests.rs +++ b/crates/labcolors-core/src/generic_boundary_tests.rs @@ -411,10 +411,6 @@ fn public_session_keeps_evidence_but_owner_alone_grants_updates_and_operations() "pub struct RemoveV1<'owner, 'session> {", "impl RemoveV1<'_, '_>", ), - ( - "pub struct HoldV1<'owner, 'session> {", - "impl<'session> HoldV1<'_, 'session>", - ), ] { assert!( source_scope(PROGRAM_SOURCE, payload, end) @@ -422,6 +418,10 @@ fn public_session_keeps_evidence_but_owner_alone_grants_updates_and_operations() "{payload} must retain both owner and immutable Session borrows", ); } + assert!( + !PROGRAM_SOURCE.contains("HoldV1") && !PROGRAM_SOURCE.contains("OperationV1::Hold"), + "past evidence must not become a current emission authority", + ); } #[test] diff --git a/crates/labcolors-core/src/program.rs b/crates/labcolors-core/src/program.rs index c8e8558a..8ae2b5d1 100644 --- a/crates/labcolors-core/src/program.rs +++ b/crates/labcolors-core/src/program.rs @@ -18,9 +18,12 @@ //! |---|---| //! | `Waiting` | нет | //! | `Ready` | `Set` для каждого выхода | -//! | `Stale` | `Hold` последнего доказанного результата | -//! | `Failed` с прошлым результатом | `Hold` | -//! | `Failed` без прошлого результата | `Remove` | +//! | `Stale` | `Remove` для каждого выхода | +//! | `Failed` | `Remove` для каждого выхода | +//! +//! Прошлый Verified-сертификат остаётся в evidence для диагностики, но не +//! разрешает эмиссию: он относится к прошлому наблюдению, а не к текущему +//! неизвестному или нарушающему контексту. //! //! [`CertificateV1::Verified`] хранит выбранное состояние, все клетки //! доказательства и сертифицированные выходы. [`CertificateV1::Conflict`] @@ -1509,7 +1512,7 @@ pub enum StateKindV1 { Waiting, /// Текущая ревизия сертифицирована. Ready, - /// Новое наблюдение недоступно, сохранён прошлый сертификат. + /// Новое наблюдение недоступно; прошлый сертификат сохранён для диагностики. Stale, /// Текущая ревизия имеет исчерпывающий конфликт. Failed, @@ -1653,20 +1656,7 @@ impl<'owner, 'session> ProjectionV1<'owner, 'session> { scope: self.scope, } } - SessionState::Stale { previous } => OperationSourceV1::Hold { - outputs: previous.outputs().iter(), - certificate: VerifiedCertificateV1 { inner: previous }, - scope: self.scope, - }, - SessionState::Failed { - previous: Some(previous), - .. - } => OperationSourceV1::Hold { - outputs: previous.outputs().iter(), - certificate: VerifiedCertificateV1 { inner: previous }, - scope: self.scope, - }, - SessionState::Failed { previous: None, .. } => OperationSourceV1::Remove { + SessionState::Stale { .. } | SessionState::Failed { .. } => OperationSourceV1::Remove { slots: OwnerOutputSlotsV1::new(&self.owner.compiled), scope: self.scope, }, @@ -2350,7 +2340,7 @@ impl<'session> SetV1<'_, 'session> { } } -/// Операция удаления результата без допустимого предыдущего значения. +/// Операция удаления результата без сертификата для текущего контекста. #[derive(Clone, Copy)] pub struct RemoveV1<'owner, 'session> { output_slot: OutputSlotIdV1, @@ -2364,26 +2354,6 @@ impl RemoveV1<'_, '_> { } } -/// Операция удержания прошлого результата с его Verified-сертификатом. -#[derive(Clone, Copy)] -pub struct HoldV1<'owner, 'session> { - output: &'session ProgramOutputV1, - certificate: VerifiedCertificateV1<'session>, - _scope: BorrowScopeV1<'owner, 'session>, -} - -impl<'session> HoldV1<'_, 'session> { - /// Возвращает удерживаемый клиентский выходной слот. - pub const fn output_slot(self) -> OutputSlotIdV1 { - OutputSlotIdV1::from_core((*self.output).output()) - } - - /// Возвращает сертификат удерживаемого результата. - pub const fn certificate(self) -> VerifiedCertificateV1<'session> { - self.certificate - } -} - /// Полное закрытое множество операций над непрозрачными выходными слотами. /// /// Каждый payload заимствует точные Owner и снимок Session. Скопированные @@ -2440,23 +2410,6 @@ impl<'session> HoldV1<'_, 'session> { /// } /// ``` /// -/// ```compile_fail,E0515 -/// use labcolors_core::program::{ -/// HoldV1, OperationV1, OwnerV1, -/// SessionV1, -/// }; -/// -/// fn escape_hold<'session>( -/// owner: OwnerV1, -/// session: &'session SessionV1, -/// ) -> HoldV1<'session, 'session> { -/// match owner.project(session).unwrap().operations().next().unwrap() { -/// OperationV1::Hold(hold) => hold, -/// _ => panic!("fixture supplies Hold"), -/// } -/// } -/// ``` -/// /// ```compile_fail,E0502 /// use labcolors_core::program::{ /// OperationV1, OwnerV1, SessionV1, @@ -2485,10 +2438,8 @@ impl<'session> HoldV1<'_, 'session> { pub enum OperationV1<'owner, 'session> { /// Установить сертифицированный результат. Set(SetV1<'owner, 'session>), - /// Удалить результат, когда допустимого прошлого значения нет. + /// Удалить результат, когда текущий контекст не сертифицирован. Remove(RemoveV1<'owner, 'session>), - /// Удержать последний сертифицированный результат. - Hold(HoldV1<'owner, 'session>), } struct CertificatesV1<'a> { @@ -2575,11 +2526,6 @@ enum OperationSourceV1<'owner, 'session> { certificate: VerifiedCertificateV1<'session>, scope: BorrowScopeV1<'owner, 'session>, }, - Hold { - outputs: slice::Iter<'session, ProgramOutputV1>, - certificate: VerifiedCertificateV1<'session>, - scope: BorrowScopeV1<'owner, 'session>, - }, Remove { slots: OwnerOutputSlotsV1<'owner>, scope: BorrowScopeV1<'owner, 'session>, @@ -2608,15 +2554,6 @@ impl<'owner, 'session> Iterator for OperationsV1<'owner, 'session> { _scope: *scope, })) } - OperationSourceV1::Hold { - outputs, - certificate, - scope, - } => Some(OperationV1::Hold(HoldV1 { - output: outputs.next()?, - certificate: *certificate, - _scope: *scope, - })), OperationSourceV1::Remove { slots, scope } => Some(OperationV1::Remove(RemoveV1 { output_slot: slots.next()?, _scope: *scope, @@ -2628,7 +2565,6 @@ impl<'owner, 'session> Iterator for OperationsV1<'owner, 'session> { let remaining = match &self.inner { OperationSourceV1::Empty => 0, OperationSourceV1::Set { outputs, .. } => outputs.len(), - OperationSourceV1::Hold { outputs, .. } => outputs.len(), OperationSourceV1::Remove { slots, .. } => slots.len(), }; (remaining, Some(remaining)) diff --git a/crates/labcolors-core/src/program_mixed_evaluator_tests.rs b/crates/labcolors-core/src/program_mixed_evaluator_tests.rs index 999d2ac1..d270611c 100644 --- a/crates/labcolors-core/src/program_mixed_evaluator_tests.rs +++ b/crates/labcolors-core/src/program_mixed_evaluator_tests.rs @@ -675,12 +675,6 @@ fn consume_public_projection(projection: ProjectionV1<'_, '_>) -> ProjectionProb probe.mix(2); probe.mix(u64::from(remove.output_slot().value())); } - OperationV1::Hold(hold) => { - probe.mix(3); - probe.mix(u64::from(hold.output_slot().value())); - probe.mix_bytes(hold.certificate().content_identity().as_bytes()); - probe.mix(hold.certificate().observation().revision()); - } } }); std::hint::black_box(probe) @@ -1357,7 +1351,7 @@ fn observation_projection_is_invariant_under_every_scenario_permutation_and_keep } #[test] -fn concrete_program_projects_total_ready_and_stale_operations() { +fn concrete_program_projects_ready_and_fail_closed_stale_operations() { let owner = OwnerV1::from_compiled(finite_program([[0x80; 3], [0; 3]])); assert_eq!(owner.surface_input_port_count(), 1); assert_eq!( @@ -1464,28 +1458,20 @@ fn concrete_program_projects_total_ready_and_stale_operations() { assert_eq!(certificates.len(), 1); assert!(matches!(certificates[0], CertificateV1::Verified(_))); assert_eq!(certificates[0].observation().revision(), 1); - let mut operations = stale.operations(); - let Some(OperationV1::Hold(hold)) = operations.next() else { - panic!("Stale must emit one Hold operation"); - }; - assert_eq!(hold.output_slot(), OutputSlotIdV1::new(OUTPUT.value())); - assert_eq!( - hold.certificate().observation().revision(), - certificates[0].observation().revision() - ); assert_eq!( - hold.certificate().content_identity(), - certificates[0].content_identity() - ); - assert_eq!( - CertificateV1::Verified(hold.certificate()).observation_backing_ptr_for_test(), + certificates[0].observation_backing_ptr_for_test(), ready_backing ); + let mut operations = stale.operations(); + let Some(OperationV1::Remove(remove)) = operations.next() else { + panic!("Stale must remove an output that lacks current evidence"); + }; + assert_eq!(remove.output_slot(), OutputSlotIdV1::new(OUTPUT.value())); assert!(operations.next().is_none()); } #[test] -fn concrete_program_distinguishes_failed_remove_from_failed_hold() { +fn concrete_program_failed_always_removes_but_retains_previous_evidence() { let white = [Srgb8::new([0xFF; 3])]; let black = [Srgb8::new([0; 3])]; let white_only = [ScenarioV1::new(1, &white)]; @@ -1560,20 +1546,15 @@ fn concrete_program_distinguishes_failed_remove_from_failed_hold() { .collect::>(), [("conflict", 2), ("verified", 1)] ); - let mut operations = failed.operations(); - let Some(OperationV1::Hold(hold)) = operations.next() else { - panic!("Failed with previous evidence must emit one Hold operation"); - }; - assert_eq!(hold.output_slot(), OutputSlotIdV1::new(OUTPUT.value())); - assert_eq!(hold.certificate().observation().revision(), 1); - assert_eq!( - hold.certificate().content_identity(), - certificates[1].content_identity() - ); assert_eq!( - CertificateV1::Verified(hold.certificate()).observation_backing_ptr_for_test(), + certificates[1].observation_backing_ptr_for_test(), previous_backing ); + let mut operations = failed.operations(); + let Some(OperationV1::Remove(remove)) = operations.next() else { + panic!("Failed must remove an output that violates the current context"); + }; + assert_eq!(remove.output_slot(), OutputSlotIdV1::new(OUTPUT.value())); assert!(operations.next().is_none()); } @@ -2016,7 +1997,7 @@ fn failed_without_previous_removes_every_output_in_canonical_exact_order() { } #[test] -fn ready_and_stale_project_every_output_in_the_same_canonical_order() { +fn ready_sets_and_stale_removes_every_output_in_the_same_canonical_order() { let owner = OwnerV1::from_compiled(finite_program_with_outputs( [[0x80; 3], [0; 3]], vec![ @@ -2062,11 +2043,10 @@ fn ready_and_stale_project_every_output_in_the_same_canonical_order() { let mut operations = stale.operations(); assert_eq!(operations.len(), 2); for expected in [OUTPUT, SECOND_OUTPUT] { - let Some(OperationV1::Hold(hold)) = operations.next() else { - panic!("Stale must hold every previously verified output"); + let Some(OperationV1::Remove(remove)) = operations.next() else { + panic!("Stale must remove every output that lacks current evidence"); }; - assert_eq!(hold.output_slot().value(), expected.value()); - assert_eq!(hold.certificate().observation().revision(), 1); + assert_eq!(remove.output_slot().value(), expected.value()); } assert!(operations.next().is_none()); } diff --git a/crates/labcolors-core/tests/program_boundary.rs b/crates/labcolors-core/tests/program_boundary.rs index e158ca95..f85e21bc 100644 --- a/crates/labcolors-core/tests/program_boundary.rs +++ b/crates/labcolors-core/tests/program_boundary.rs @@ -157,10 +157,6 @@ fn assert_projection_is_owner_bound(projection: ProjectionV1<'_, '_>) { OperationV1::Remove(remove) => { let _: OutputSlotIdV1 = remove.output_slot(); } - OperationV1::Hold(hold) => { - let _: OutputSlotIdV1 = hold.output_slot(); - let _ = hold.certificate().content_identity(); - } } } let _ = certificate_count; @@ -572,6 +568,113 @@ fn every_physical_constructor_and_both_remaining_constraint_modes_execute() { assert!(operations.next().is_none()); } +#[test] +fn observed_violation_removes_outputs_but_retains_previous_certificate_as_evidence() { + let input = SurfaceInputPortIdV1::new(50); + let output = OutputSlotIdV1::new(12); + let owner = fixed_nested_draft(0.5, SourceIdV1::new(1), input, input) + .compile() + .unwrap(); + let mut session = owner.instantiate(13).unwrap(); + + let black = [Srgb8::new([0; 3])]; + let black_scenarios = [ScenarioV1::new(1, &black)]; + let ready = owner + .update( + &mut session, + UpdateV1::Observed { + revision: 1, + scenarios: &black_scenarios, + }, + ) + .unwrap(); + assert!(matches!( + ready.operations().next(), + Some(OperationV1::Set(_)) + )); + + let white = [Srgb8::new([0xFF; 3])]; + let white_scenarios = [ScenarioV1::new(2, &white)]; + let failed = owner + .update( + &mut session, + UpdateV1::Observed { + revision: 2, + scenarios: &white_scenarios, + }, + ) + .unwrap(); + assert_eq!(failed.evidence().kind(), StateKindV1::Failed); + let certificates = failed.evidence().certificates().collect::>(); + assert_eq!(certificates.len(), 2); + assert!(matches!(certificates[0], CertificateV1::Conflict(_))); + let CertificateV1::Verified(previous) = certificates[1] else { + panic!("the previous certificate must remain available as diagnostics"); + }; + assert_eq!(previous.observation().revision(), 1); + + let mut operations = failed.operations(); + let Some(OperationV1::Remove(remove)) = operations.next() else { + panic!("a known violation of the current context must remove the old output"); + }; + assert_eq!(remove.output_slot(), output); + assert!(operations.next().is_none()); +} + +#[test] +fn unknown_context_removes_outputs_but_retains_previous_certificate_as_evidence() { + let input = SurfaceInputPortIdV1::new(50); + let output = OutputSlotIdV1::new(12); + let owner = fixed_nested_draft(0.5, SourceIdV1::new(1), input, input) + .compile() + .unwrap(); + let mut session = owner.instantiate(13).unwrap(); + + let black = [Srgb8::new([0; 3])]; + let black_scenarios = [ScenarioV1::new(1, &black)]; + let ready = owner + .update( + &mut session, + UpdateV1::Observed { + revision: 1, + scenarios: &black_scenarios, + }, + ) + .unwrap(); + assert!(matches!( + ready.operations().next(), + Some(OperationV1::Set(_)) + )); + + let stale = owner + .update( + &mut session, + UpdateV1::Unknown { + revision: 2, + reason_id: 9, + }, + ) + .unwrap(); + assert_eq!(stale.evidence().kind(), StateKindV1::Stale); + let certificates = stale.evidence().certificates().collect::>(); + assert_eq!(certificates.len(), 1); + let CertificateV1::Verified(previous) = certificates[0] else { + panic!("the previous certificate must remain available as diagnostics"); + }; + assert_eq!(previous.observation().revision(), 1); + assert!(matches!( + stale.evidence().observation_head(), + ObservationHeadV1::Unknown { revision: 2, .. } + )); + + let mut operations = stale.operations(); + let Some(OperationV1::Remove(remove)) = operations.next() else { + panic!("unknown current context cannot authorize the old output"); + }; + assert_eq!(remove.output_slot(), output); + assert!(operations.next().is_none()); +} + #[test] fn owner_and_update_errors_preserve_content_and_input_identity() { let input = SurfaceInputPortIdV1::new(50); From 3dc76d79de5ea1c50c31d4341620a48e55fb7ae4 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Sun, 26 Jul 2026 11:05:04 +0300 Subject: [PATCH 54/58] core: revoke outputs on unknown session handoff --- crates/labcolors-core/src/program.rs | 33 ++++++++--- .../src/program_mixed_evaluator_tests.rs | 56 +++++++++++++++++++ 2 files changed, 81 insertions(+), 8 deletions(-) diff --git a/crates/labcolors-core/src/program.rs b/crates/labcolors-core/src/program.rs index 8ae2b5d1..49979e4d 100644 --- a/crates/labcolors-core/src/program.rs +++ b/crates/labcolors-core/src/program.rs @@ -16,14 +16,17 @@ //! //! | Состояние | Операции | //! |---|---| -//! | `Waiting` | нет | +//! | `Waiting` + `Empty` | нет | +//! | `Waiting` + допущенный `Unknown` | `Remove` для каждого выхода | //! | `Ready` | `Set` для каждого выхода | //! | `Stale` | `Remove` для каждого выхода | //! | `Failed` | `Remove` для каждого выхода | //! //! Прошлый Verified-сертификат остаётся в evidence для диагностики, но не //! разрешает эмиссию: он относится к прошлому наблюдению, а не к текущему -//! неизвестному или нарушающему контексту. +//! неизвестному или нарушающему контексту. Непустая сырая голова без текущего +//! Verified-сертификата также отзывает выходы: это закрывает передачу sink от +//! одной Session другой Session того же Owner. //! //! [`CertificateV1::Verified`] хранит выбранное состояние, все клетки //! доказательства и сертифицированные выходы. [`CertificateV1::Conflict`] @@ -1508,7 +1511,7 @@ impl SchemaOrderedScenarioSourceV1 for ScenarioSourceV1<'_> { /// Закрытая классификация lifecycle Session. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum StateKindV1 { - /// Допущенного вычислимого наблюдения ещё нет. + /// Допущенного вычислимого наблюдения ещё нет; сырая голова может быть `Unknown`. Waiting, /// Текущая ревизия сертифицирована. Ready, @@ -1639,7 +1642,14 @@ impl<'owner, 'session> ProjectionV1<'owner, 'session> { self, ) -> impl ExactSizeIterator> + FusedIterator { let inner = match self.evidence.state() { - SessionState::Waiting => OperationSourceV1::Empty, + SessionState::Waiting + if matches!( + self.evidence.session.raw_head(), + ObservationHeadViewV1::Empty + ) => + { + OperationSourceV1::Empty + } SessionState::Ready { current } => { debug_assert_eq!(current.outputs().len(), self.owner.compiled.output_count()); debug_assert!( @@ -1656,10 +1666,17 @@ impl<'owner, 'session> ProjectionV1<'owner, 'session> { scope: self.scope, } } - SessionState::Stale { .. } | SessionState::Failed { .. } => OperationSourceV1::Remove { - slots: OwnerOutputSlotsV1::new(&self.owner.compiled), - scope: self.scope, - }, + // `Waiting + Empty` — единственное состояние без действия и без + // полномочий на sink. После admission сырой головы любое состояние + // без текущего Verified-доказательства подчиняется одному закону + // отзыва. Так же fail-closed обрабатывается внутренне недостижимое + // сегодня сочетание `Waiting + Observed`. + SessionState::Waiting | SessionState::Stale { .. } | SessionState::Failed { .. } => { + OperationSourceV1::Remove { + slots: OwnerOutputSlotsV1::new(&self.owner.compiled), + scope: self.scope, + } + } }; OperationsV1 { inner } } diff --git a/crates/labcolors-core/src/program_mixed_evaluator_tests.rs b/crates/labcolors-core/src/program_mixed_evaluator_tests.rs index d270611c..1085f0a7 100644 --- a/crates/labcolors-core/src/program_mixed_evaluator_tests.rs +++ b/crates/labcolors-core/src/program_mixed_evaluator_tests.rs @@ -1470,6 +1470,62 @@ fn concrete_program_projects_ready_and_fail_closed_stale_operations() { assert!(operations.next().is_none()); } +#[test] +fn unknown_replacement_session_revokes_an_existing_owner_output() { + let owner = OwnerV1::from_compiled(finite_program([[0x80; 3], [0; 3]])); + let white = [Srgb8::new([0xFF; 3])]; + let scenarios = [ScenarioV1::new(1, &white)]; + + let mut first_session = owner.instantiate(11).unwrap(); + let ready = owner + .update( + &mut first_session, + UpdateV1::Observed { + revision: 1, + scenarios: &scenarios, + }, + ) + .unwrap(); + let mut sink = None; + for operation in ready.operations() { + match operation { + OperationV1::Set(set) => sink = Some((set.source(), set.opacity())), + OperationV1::Remove(_) => sink = None, + } + } + assert!( + sink.is_some(), + "the first Session must populate the shared sink" + ); + drop(first_session); + + let mut replacement_session = owner.instantiate(12).unwrap(); + let unknown = owner + .update( + &mut replacement_session, + UpdateV1::Unknown { + revision: 1, + reason_id: 7, + }, + ) + .unwrap(); + assert_eq!(unknown.evidence().kind(), StateKindV1::Waiting); + assert_unknown_head(unknown.evidence().observation_head(), 12, 1, 7); + assert_eq!(unknown.evidence().certificates().len(), 0); + + let mut operations = unknown.operations(); + let Some(OperationV1::Remove(remove)) = operations.next() else { + panic!("an explicit Unknown must revoke an existing owner output during handoff"); + }; + assert_eq!(remove.output_slot(), OutputSlotIdV1::new(OUTPUT.value())); + sink = None; + assert!(operations.next().is_none()); + assert!( + sink.is_none(), + "the replacement Session must not leave stale paint" + ); +} + #[test] fn concrete_program_failed_always_removes_but_retains_previous_evidence() { let white = [Srgb8::new([0xFF; 3])]; From a30d03d4b8049831b92c7f9cda4c754b621d6c04 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Sun, 26 Jul 2026 11:07:29 +0300 Subject: [PATCH 55/58] core: keep incomplete Program crate-private --- ...rt-reference-surplus-q55-bps-proof-v1.json | 2 +- .../src/generic_boundary_tests.rs | 53 +++++---- crates/labcolors-core/src/lib.rs | 72 ++++-------- crates/labcolors-core/src/program.rs | 108 ++---------------- .../program_api_tests.rs} | 8 +- .../program_boundary_tests.rs} | 25 ++-- scripts/verify_point_support_surplus.py | 2 +- 7 files changed, 80 insertions(+), 190 deletions(-) rename crates/labcolors-core/{tests/program_public_api.rs => src/program_api_tests.rs} (87%) rename crates/labcolors-core/{tests/program_boundary.rs => src/program_boundary_tests.rs} (98%) 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 1ecfc3fc..28b469c3 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":"563b58088ed413b0a65c2c7d4282792559a756b9f9a51e03774d20afd8259b0a","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":"371a19f9da8337a13fb9a474c0f0395b35117822fd2d9293c6228b1f01e97ca4","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":"09be54900efe29ffdac8705efd0d6d613055c90d634446ca4b228f51a63997d0"},{"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":"51d7835a52b9eb4ab4888aef401ab145b1764eb2054ff7e856828fefc770a132"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/wcag22.rs","sha256":"856093c91159d8b3faab001f2d6524d33d7b16458a5a4e98ea65f8c62ab2694c"},{"kind":"rust-source","path":"crates/labcolors-core/src/hash.rs","sha256":"f97a0fd7d6ad3162f0f1dfb326fccfb7ed40da9a8fa67a5b8a239a1ae2ae49c3"},{"kind":"rust-source","path":"crates/labcolors-core/src/lcs_occurrence.rs","sha256":"6f202ad7425a235b9d18caba0c817fc33a2b8e042050a34f5ddff3fd09efc53d"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"28b21948812801582fc2c59e304df050fc131e3bfd0970fe34b3118c667f2885"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"8c9838107077775c51d80638ba0b59f9672d14347ca404dfd4c63e2fb62d1c45"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"6f6a376ff036d3d65960c004e6566e1bca580f19f5bd3cd333a80b0da5b5c242"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"4f77643206077c080e5e9b182e896145bfb69bf3db8aa4c1ac7d4c5360ea8504"},{"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":"e480c973e04de69c364ffb913a508e7cca82b8ec1760bc4a50634c2c5e8afc2a"} +{"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":"1477d4fa428c54bab14ccdcc336e34c94aa68d70f768cfa26df289e103daf7f7","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":"fe841df4f7a63adc94423660b2ef4daefad539a3d3748bc9dafff08491be0ddd","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":"09be54900efe29ffdac8705efd0d6d613055c90d634446ca4b228f51a63997d0"},{"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":"51d7835a52b9eb4ab4888aef401ab145b1764eb2054ff7e856828fefc770a132"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/wcag22.rs","sha256":"856093c91159d8b3faab001f2d6524d33d7b16458a5a4e98ea65f8c62ab2694c"},{"kind":"rust-source","path":"crates/labcolors-core/src/hash.rs","sha256":"f97a0fd7d6ad3162f0f1dfb326fccfb7ed40da9a8fa67a5b8a239a1ae2ae49c3"},{"kind":"rust-source","path":"crates/labcolors-core/src/lcs_occurrence.rs","sha256":"6f202ad7425a235b9d18caba0c817fc33a2b8e042050a34f5ddff3fd09efc53d"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"51ab4da1ab650b063e57bf158b554f3fdc561f3e86d3ad60a660310991c7a7d1"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"8c9838107077775c51d80638ba0b59f9672d14347ca404dfd4c63e2fb62d1c45"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"6f6a376ff036d3d65960c004e6566e1bca580f19f5bd3cd333a80b0da5b5c242"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"4f77643206077c080e5e9b182e896145bfb69bf3db8aa4c1ac7d4c5360ea8504"},{"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":"bf25b5e0704aca428a73aa529f966f0f252be041d3171bca989d9672d3099acf"} diff --git a/crates/labcolors-core/src/generic_boundary_tests.rs b/crates/labcolors-core/src/generic_boundary_tests.rs index 7bb06faf..d35d5122 100644 --- a/crates/labcolors-core/src/generic_boundary_tests.rs +++ b/crates/labcolors-core/src/generic_boundary_tests.rs @@ -173,23 +173,25 @@ fn generic_physical_and_transport_modules_contain_no_client_or_legacy_vocabulary } #[test] -fn stacked_program_module_is_visible_module_qualified_and_transport_neutral() { +fn staged_program_module_is_private_module_qualified_and_transport_neutral() { let normalized_lib = LIB_SOURCE.split_whitespace().collect::>().join(" "); assert_eq!( LIB_SOURCE .lines() - .filter(|line| line.trim() == "pub mod program;") + .filter(|line| line.trim() == "pub(crate) mod program;") .count(), 1, - "the crate root must expose exactly one file-backed Program module", + "the crate root must retain exactly one crate-private file-backed Program module", ); assert!( - normalized_lib.contains("#[deny(missing_docs)] pub mod program;"), - "the public Program reference must stay complete by construction", + normalized_lib.contains("#[deny(missing_docs)] pub(crate) mod program;"), + "the staged Program candidate must stay documented but crate-private", ); assert!( - !normalized_lib.contains("#[doc(hidden)] pub mod program;"), - "the reviewed Program API must remain visible in rustdoc", + !LIB_SOURCE + .lines() + .any(|line| line.trim() == "pub mod program;"), + "the incomplete Program candidate must not become externally reachable", ); for introducer in ["pub use ", "pub type "] { @@ -203,7 +205,7 @@ fn stacked_program_module_is_visible_module_qualified_and_transport_neutral() { + 1]; assert!( !contains_rust_identifier(statement, "program"), - "Program types must stay module-qualified; found root alias `{statement}`", + "Program types must stay private and module-qualified; found root alias `{statement}`", ); remaining = &remaining[start + statement.len()..]; } @@ -212,7 +214,7 @@ fn stacked_program_module_is_visible_module_qualified_and_transport_neutral() { let source_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src"); assert!( source_root.join("program.rs").is_file(), - "the public Program implementation must remain file-backed", + "the staged Program implementation must remain file-backed", ); assert!( !source_root.join("package_bridge.rs").exists(), @@ -221,7 +223,7 @@ fn stacked_program_module_is_visible_module_qualified_and_transport_neutral() { assert!( !PROGRAM_SOURCE.contains("PackageProgram") && !contains_rust_identifier(PROGRAM_SOURCE, "package_bridge"), - "the public Program source must not retain transport-era vocabulary", + "the staged Program source must not retain transport-era vocabulary", ); for (path, source) in production_rust_sources() { @@ -236,10 +238,11 @@ fn stacked_program_module_is_visible_module_qualified_and_transport_neutral() { } assert_only_in_compile_fail(LIB_SOURCE, "PackageProgram"); assert_only_in_compile_fail(LIB_SOURCE, "package_bridge"); + assert_only_in_compile_fail(LIB_SOURCE, "use labcolors_core::program;"); } #[test] -fn public_program_draft_wraps_the_single_canonical_core_graph() { +fn staged_program_draft_wraps_the_single_canonical_core_graph() { assert_eq!( normalized_source_scope( PROGRAM_SOURCE, @@ -247,7 +250,7 @@ fn public_program_draft_wraps_the_single_canonical_core_graph() { "/// Ошибка изменения Draft до компиляции.", ), "pub struct DraftV1 { inner: CoreProgramDraftV1, }", - "the public seam must forward actual IR nodes into the sole Core draft", + "the staged seam must forward actual IR nodes into the sole Core draft", ); assert_eq!( normalized_source_scope( @@ -282,13 +285,13 @@ fn public_program_draft_wraps_the_single_canonical_core_graph() { ] { assert!( !PROGRAM_SOURCE.contains(forbidden), - "the concrete public lowerer must not acquire `{forbidden}`", + "the staged concrete lowerer must not acquire `{forbidden}`", ); } } #[test] -fn public_session_keeps_evidence_but_owner_alone_grants_updates_and_operations() { +fn staged_session_keeps_evidence_but_owner_alone_grants_updates_and_operations() { assert_eq!( normalized_source_scope(PROGRAM_SOURCE, "pub struct SessionV1 {", "impl SessionV1",), concat!( @@ -297,7 +300,7 @@ fn public_session_keeps_evidence_but_owner_alone_grants_updates_and_operations() "session: CoreProgramSessionV1, ", "}", ), - "the public Session must not duplicate owner schema, outputs, stream, or lifecycle state", + "the staged Session must not duplicate owner schema, outputs, stream, or lifecycle state", ); let session_api = source_scope( @@ -308,12 +311,12 @@ fn public_session_keeps_evidence_but_owner_alone_grants_updates_and_operations() assert_eq!( session_api.matches("pub fn evidence(").count(), 1, - "historical evidence is the Session's sole public projection", + "historical evidence is the Session's sole boundary projection", ); assert_eq!( session_api.matches("pub ").count(), 1, - "Session must not expose a second public authority by changing function qualifiers", + "Session must not expose a second authority by changing function qualifiers", ); for forbidden in [ "pub fn state(", @@ -375,21 +378,21 @@ fn public_session_keeps_evidence_but_owner_alone_grants_updates_and_operations() "owner mismatch must be rejected before admission, allocation, or evaluation", ); - let public_access_errors = + let staged_access_errors = source_scope(PROGRAM_SOURCE, "pub enum AccessErrorV1 {", "impl OwnerV1"); assert!( - public_access_errors.contains("OwnerMismatch,") - && !public_access_errors.contains("OwnerExpired"), + staged_access_errors.contains("OwnerMismatch,") + && !staged_access_errors.contains("OwnerExpired"), "operation projection must distinguish foreign ownership, not expose internal expiry", ); - let public_update_errors = source_scope( + let staged_update_errors = source_scope( PROGRAM_SOURCE, "pub enum UpdateErrorKindV1 {", "pub enum UpdateErrorV1 {", ); assert!( - public_update_errors.contains("OwnerMismatch,") - && !public_update_errors.contains("OwnerExpired"), + staged_update_errors.contains("OwnerMismatch,") + && !staged_update_errors.contains("OwnerExpired"), "owner expiry is an internal invariant after a matching owner borrow", ); assert!( @@ -1107,6 +1110,7 @@ fn existing_encoded_evaluators_delegate_program_targets_without_parallel_formula fn private_program_and_lcs_occurrence_types_are_not_publicly_exported() { for required in [ "pub(crate) mod lcs_occurrence;", + "pub(crate) mod program;", "pub(crate) mod program_session;", ] { assert!( @@ -1116,9 +1120,12 @@ fn private_program_and_lcs_occurrence_types_are_not_publicly_exported() { } for forbidden in [ "pub mod lcs_occurrence;", + "pub mod program;", "pub mod program_session;", "pub use lcs_occurrence::", "pub use crate::lcs_occurrence::", + "pub use program::", + "pub use crate::program::", "pub use program_session::", "pub use crate::program_session::", ] { diff --git a/crates/labcolors-core/src/lib.rs b/crates/labcolors-core/src/lib.rs index c1d72f4b..dd914c57 100644 --- a/crates/labcolors-core/src/lib.rs +++ b/crates/labcolors-core/src/lib.rs @@ -44,13 +44,17 @@ pub(crate) mod output_projection; ) )] pub(crate) mod point_support; +#[expect( + dead_code, + reason = "the complete Program candidate remains private until terminal C7c" +)] #[deny(missing_docs)] -pub mod program; +pub(crate) mod program; #[cfg_attr( not(test), expect( dead_code, - reason = "generic Program machinery is exposed only through the concrete public module" + reason = "generic Program machinery is used only through the staged concrete module" ) )] pub(crate) mod program_session; @@ -58,7 +62,7 @@ pub(crate) mod program_session; not(test), expect( dead_code, - reason = "release-registry internals are projected only through typed public evidence" + reason = "release-registry internals are projected only through typed Program evidence" ) )] pub(crate) mod release_registry; @@ -106,6 +110,12 @@ mod program_mixed_evaluator_tests; #[cfg(test)] mod program_identity_tests; +#[cfg(test)] +mod program_boundary_tests; + +#[cfg(test)] +mod program_api_tests; + #[cfg(test)] mod release_registry_tests; @@ -116,7 +126,7 @@ mod generic_boundary_tests; not(test), expect( dead_code, - reason = "raw observation ownership is exposed only through the public Program session" + reason = "raw observation ownership is used only through the staged Program session" ) )] pub(crate) mod observation; @@ -131,7 +141,7 @@ mod point_support_tests; not(test), expect( dead_code, - reason = "the generic Session engine is exposed only through the public Program owner" + reason = "the generic Session engine is used only through the staged Program owner" ) )] pub(crate) mod session; @@ -143,7 +153,7 @@ mod session_tests; not(test), expect( dead_code, - reason = "joint-selection internals are exposed only through the public Program contract" + reason = "joint-selection internals are used only through the staged Program contract" ) )] pub(crate) mod joint; @@ -359,51 +369,11 @@ pub struct NoHybridLpcSurfaceMetric; #[cfg(doctest)] pub struct NoPrematureScalarLpcApi; -/// Публичный Program API живёт только в одноимённом модуле и не сохраняет -/// транспортный префикс, старый путь или корневые реэкспорты. -/// -/// ``` -/// use labcolors_core::{Srgb8, program}; +/// Кандидат Program остаётся внутренним до завершения terminal C7c: неполную +/// emission/attachment/transaction поверхность нельзя случайно опубликовать. /// -/// let source = program::SourceIdV1::new(1); -/// let target = program::TargetIdV1::new(2); -/// let input = program::SurfaceInputPortIdV1::new(3); -/// let paint = program::PaintIdV1::new(4); -/// let surface = program::SurfaceIdV1::new(5); -/// let occurrence = program::OccurrenceIdV1::new(6); -/// let constraint = program::ConstraintIdV1::new(7); -/// let output = program::OutputSlotIdV1::new(8); -/// let context = program::AppearanceContextV1::try_new( -/// 64.0, -/// 0.2, -/// program::SurroundV1::Average, -/// ).unwrap(); -/// let mut draft = program::DraftV1::new(); -/// -/// draft.push_source(source, Srgb8::new([0, 0, 0])); -/// draft.push_fixed_target(target, source); -/// draft.push_surface_input_port(input); -/// draft.push_solid_paint(paint, target); -/// draft.push_input_surface(surface, input); -/// draft.push_source_over_occurrence(occurrence, paint, surface, context); -/// draft.push_exact_hard(constraint, occurrence, Srgb8::new([0, 0, 0])); -/// draft.push_output(output, paint); -/// -/// let owner = draft.compile().unwrap(); -/// let mut session = owner.instantiate(1).unwrap(); -/// let white = [Srgb8::new([255, 255, 255])]; -/// let scenarios = [program::ScenarioV1::new(1, &white)]; -/// let projection = owner.update( -/// &mut session, -/// program::UpdateV1::Observed { -/// revision: 1, -/// scenarios: &scenarios, -/// }, -/// ).unwrap(); -/// assert!(matches!( -/// projection.operations().next(), -/// Some(program::OperationV1::Set(_)), -/// )); +/// ```compile_fail +/// use labcolors_core::program; /// ``` /// /// ```compile_fail @@ -422,7 +392,7 @@ pub struct NoPrematureScalarLpcApi; /// use labcolors_core::DraftV1; /// ``` #[cfg(doctest)] -pub struct ProgramApiBoundary; +pub struct NoPrematureProgramApi; /// C8d recheck и F2 observation остаются деталями одной приватной Session; /// они не могут стать дополнительными public authoring/runtime roots. diff --git a/crates/labcolors-core/src/program.rs b/crates/labcolors-core/src/program.rs index 49979e4d..c5a37281 100644 --- a/crates/labcolors-core/src/program.rs +++ b/crates/labcolors-core/src/program.rs @@ -1,4 +1,8 @@ -//! Публичный декларативный контракт компиляции и исполнения цветовой программы. +//! Внутренний кандидат декларативного контракта цветовой программы. +//! +//! Модуль не публикуется до завершения emission, attachment и атомарной +//! транзакционной границы terminal C7c. Его закрытая поверхность уже служит +//! единственным concrete seam для внутренних проверок и дальнейших срезов. //! //! Клиент один раз описывает физический граф через [`DraftV1`]: исходные //! сигналы, решаемые цели, Paint, Surface, их [`OccurrenceIdV1`], ограничения @@ -1782,25 +1786,6 @@ impl<'a> ConflictCertificateV1<'a> { /// /// Сертификат заимствует только историю Session и может пережить Owner, /// разрешивший исходную проекцию. -/// -/// ```no_run -/// use labcolors_core::program::{ -/// CertificateV1, OwnerV1, SessionV1, -/// }; -/// -/// fn retain_evidence<'session>( -/// owner: OwnerV1, -/// session: &'session SessionV1, -/// ) -> CertificateV1<'session> { -/// owner -/// .project(session) -/// .unwrap() -/// .evidence() -/// .certificates() -/// .next() -/// .unwrap() -/// } -/// ``` #[derive(Clone, Copy)] pub enum CertificateV1<'a> { /// Все hard-клетки полного support прошли. @@ -2376,81 +2361,6 @@ impl RemoveV1<'_, '_> { /// Каждый payload заимствует точные Owner и снимок Session. Скопированные /// slot/source/opacity — только данные: runtime обязан перепроверить живую /// пару непосредственно перед одним атомарным sink commit. -/// -/// ```compile_fail,E0515 -/// use labcolors_core::program::{ -/// OperationV1, OwnerV1, RemoveV1, -/// SessionV1, -/// }; -/// -/// fn escape_remove<'session>( -/// owner: OwnerV1, -/// session: &'session SessionV1, -/// ) -> RemoveV1<'session, 'session> { -/// match owner.project(session).unwrap().operations().next().unwrap() { -/// OperationV1::Remove(remove) => remove, -/// _ => panic!("fixture supplies Remove"), -/// } -/// } -/// ``` -/// -/// ```compile_fail,E0515 -/// use labcolors_core::program::{ -/// OperationV1, OwnerV1, RemoveV1, -/// }; -/// -/// fn escape_local_session<'owner>( -/// owner: &'owner OwnerV1, -/// ) -> RemoveV1<'owner, 'owner> { -/// let session = owner.instantiate(1).unwrap(); -/// match owner.project(&session).unwrap().operations().next().unwrap() { -/// OperationV1::Remove(remove) => remove, -/// _ => panic!("fixture supplies Remove"), -/// } -/// } -/// ``` -/// -/// ```compile_fail,E0515 -/// use labcolors_core::program::{ -/// OperationV1, OwnerV1, SessionV1, -/// SetV1, -/// }; -/// -/// fn escape_set<'session>( -/// owner: OwnerV1, -/// session: &'session SessionV1, -/// ) -> SetV1<'session, 'session> { -/// match owner.project(session).unwrap().operations().next().unwrap() { -/// OperationV1::Set(set) => set, -/// _ => panic!("fixture supplies Set"), -/// } -/// } -/// ``` -/// -/// ```compile_fail,E0502 -/// use labcolors_core::program::{ -/// OperationV1, OwnerV1, SessionV1, -/// UpdateV1, -/// }; -/// -/// fn remove_blocks_session_mutation( -/// owner: &OwnerV1, -/// session: &mut SessionV1, -/// ) { -/// let remove = match owner.project(session).unwrap().operations().next().unwrap() { -/// OperationV1::Remove(remove) => remove, -/// _ => return, -/// }; -/// let _second = owner.update( -/// session, -/// UpdateV1::Unknown { -/// revision: 2, -/// reason_id: 7, -/// }, -/// ); -/// let _slot = remove.output_slot(); -/// } -/// ``` #[derive(Clone, Copy)] pub enum OperationV1<'owner, 'session> { /// Установить сертифицированный результат. @@ -2817,6 +2727,10 @@ pub enum ModeledOccurrenceFailureV1 { /// Точное недопустимое protocol-состояние зарегистрированного evaluator-а. #[derive(Debug, Clone, PartialEq, Eq)] +#[expect( + clippy::enum_variant_names, + reason = "the variant name preserves evaluator provenance as this closed family grows" +)] pub enum EvaluatorProtocolFailureV1 { /// WCAG evaluator вернул kernel-ошибку, недостижимую для typed Program. Wcag22Kernel { @@ -2866,7 +2780,7 @@ pub enum UpdateInvariantV1 { /// Нарушенный внутренний контракт с точными subject и witness-фактами. /// -/// Эти варианты недостижимы через типизированный public input. Payload нужен +/// Эти варианты недостижимы через типизированный boundary input. Payload нужен /// для детерминированной диагностики и не превращает breach в цветовой verdict. #[derive(Debug, Clone, PartialEq, Eq)] pub enum UpdateInvariantFailureV1 { @@ -3836,7 +3750,7 @@ mod update_error_projection_tests { } #[test] - fn every_modeled_occurrence_failure_has_an_isomorphic_public_witness() { + fn every_modeled_occurrence_failure_has_an_isomorphic_boundary_witness() { use crate::lcs_occurrence::{ MUTATION_SENTINEL_XYZ_FRAME_V1, ModeledLcsOccurrenceFormationErrorV1, OccurrenceFormationError, TristimulusSample, diff --git a/crates/labcolors-core/tests/program_public_api.rs b/crates/labcolors-core/src/program_api_tests.rs similarity index 87% rename from crates/labcolors-core/tests/program_public_api.rs rename to crates/labcolors-core/src/program_api_tests.rs index be7fbb76..0c6bbd9f 100644 --- a/crates/labcolors-core/tests/program_public_api.rs +++ b/crates/labcolors-core/src/program_api_tests.rs @@ -1,9 +1,9 @@ -//! Минимальный внешний контракт лаконичной публичной поверхности Program. +//! Минимальный внутренний контракт кандидата поверхности Program. -use labcolors_core::{Srgb8, program}; +use crate::{Srgb8, program}; #[test] -fn public_program_api_is_module_qualified_without_transport_prefixes() { +fn staged_program_api_is_module_qualified_without_transport_prefixes() { let source = program::SourceIdV1::new(1); let target = program::TargetIdV1::new(2); let input = program::SurfaceInputPortIdV1::new(3); @@ -46,7 +46,7 @@ fn public_program_api_is_module_qualified_without_transport_prefixes() { } #[test] -fn public_internal_failure_keeps_fact_and_contract_as_one_consistent_value() { +fn staged_internal_failure_keeps_fact_and_contract_as_one_consistent_value() { let source = program::UpdateInvariantFailureV1::OwnerAuthority; assert_eq!( source.contract(), diff --git a/crates/labcolors-core/tests/program_boundary.rs b/crates/labcolors-core/src/program_boundary_tests.rs similarity index 98% rename from crates/labcolors-core/tests/program_boundary.rs rename to crates/labcolors-core/src/program_boundary_tests.rs index f85e21bc..0f133d37 100644 --- a/crates/labcolors-core/tests/program_boundary.rs +++ b/crates/labcolors-core/src/program_boundary_tests.rs @@ -1,13 +1,12 @@ -//! External compile-and-runtime contract for the sole concrete Core Program seam. +//! Internal compile-and-runtime contract for the staged concrete Core Program seam. //! -//! This integration crate deliberately has no access to Core-private generic -//! evaluator/session machinery. Every reachable path uses only the closed -//! concrete boundary types. +//! These tests deliberately exercise only the closed concrete boundary types; +//! the module stays crate-private until the terminal public cut is complete. use core::iter::FusedIterator; -use labcolors_core::Srgb8; -use labcolors_core::program::{ +use crate::Srgb8; +use crate::program::{ AppearanceContextErrorKindV1, AppearanceContextFieldV1, AppearanceContextV1, AssessmentV1, CertificateV1, CompileErrorHandleV1, CompileErrorKindV1, CompileErrorV1, ConstraintIdV1, DraftErrorV1, DraftV1, EvidenceBoundsErrorV1, InstantiateErrorV1, JointChoiceV1, @@ -17,7 +16,7 @@ use labcolors_core::program::{ SurfaceIdV1, SurfaceInputPortIdV1, SurroundV1, TargetCandidateIdV1, TargetCandidateV1, TargetIdV1, UpdateErrorKindV1, UpdateErrorV1, UpdateV1, VerdictV1, }; -use labcolors_core::wcag22::Wcag22CriterionV1; +use crate::wcag22::Wcag22CriterionV1; fn exact_size(iterator: I) -> I { iterator @@ -175,13 +174,13 @@ fn unknown_is_revision_bound_without_a_stream_or_generation_field( } #[allow(dead_code)] -fn owner_mismatch_is_a_closed_public_error(error: UpdateErrorV1) { +fn owner_mismatch_is_a_closed_boundary_error(error: UpdateErrorV1) { assert_eq!(error.kind(), UpdateErrorKindV1::OwnerMismatch); } #[test] -fn external_boundary_uses_only_closed_concrete_types() { - // Reaching this test means the external crate compiled without importing +fn staged_boundary_uses_only_closed_concrete_types() { + // Reaching this test means the concrete seam compiled without importing // Program, evaluator traits, Session, or numeric generations. assert_eq!(core::mem::size_of::(), 3); } @@ -429,7 +428,7 @@ fn evidence_cell_bounds_query_is_pure_across_session_updates() { } #[test] -fn external_authoring_lowers_the_actual_closed_program_and_returns_canonical_input_ports() { +fn staged_authoring_lowers_the_actual_closed_program_and_returns_canonical_input_ports() { let source = SourceIdV1::new(91); let target = TargetIdV1::new(72); let gray = TargetCandidateIdV1::new(8); @@ -725,7 +724,7 @@ fn owner_and_update_errors_preserve_content_and_input_identity() { scenarios: &malformed, }, ) { - Ok(_) => panic!("schema-short public input must fail"), + Ok(_) => panic!("schema-short boundary input must fail"), Err(error) => error, }; assert_eq!(error.kind(), UpdateErrorKindV1::InvalidObservation); @@ -1089,7 +1088,7 @@ fn dependency_cycles_retain_all_typed_core_members_without_reallocation() { } #[test] -fn the_code_owned_observation_group_reports_public_authored_port_semantics() { +fn the_code_owned_observation_group_reports_authored_port_semantics() { assert_eq!( compile_error(DraftV1::new()), CompileErrorV1::EmptySurfaceInputPortSet diff --git a/scripts/verify_point_support_surplus.py b/scripts/verify_point_support_surplus.py index 37c5fa71..c1f4a09b 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 = ( - "371a19f9da8337a13fb9a474c0f0395b35117822fd2d9293c6228b1f01e97ca4" + "fe841df4f7a63adc94423660b2ef4daefad539a3d3748bc9dafff08491be0ddd" ) EXPECTED_Q55_PROOF_SHA256 = ( "ac59cf89503170c789223b91d775213a19d4e571ef930f2ea609fcd51b14defd" From cc0e06538385c2bb521f08444c43a368ca583fe7 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Sun, 26 Jul 2026 21:41:12 +0300 Subject: [PATCH 56/58] perf: bind smaller private Program wasm --- packages/colors/bench/wasm.json | 6 +++--- scripts/check-wasm-size-budget.mjs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/colors/bench/wasm.json b/packages/colors/bench/wasm.json index 43d7b1ea..a6747cef 100644 --- a/packages/colors/bench/wasm.json +++ b/packages/colors/bench/wasm.json @@ -19,12 +19,12 @@ "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-30115821523", + "source": "github-actions-run-30214988060", "platform": "linux-x64", - "rawBytes": 376985 + "rawBytes": 376707 }, "policy": { - "maxRawBytes": 376985, + "maxRawBytes": 376707, "basis": "program-content-identity", "gzip": "diagnostic-only" } diff --git a/scripts/check-wasm-size-budget.mjs b/scripts/check-wasm-size-budget.mjs index b41b24f6..fe920406 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 = - "73722a93248ba005c6b8cf1846e52c1344dcb9f85a3a2133a25c51dacbefdcfc"; + "46c8830fadaa96c50d784bff41329177b4bf8742dbd8d1b1bb175b3150c55107"; const SCHEMA_VERSION = 1; const CANONICAL_ARTIFACT = "packages/colors/pkg/labcolors_bg.wasm"; From 18cb0ffa5c42ae76aa758a00a9a9d4bed07ced28 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Sun, 26 Jul 2026 22:04:27 +0300 Subject: [PATCH 57/58] core: make staged Program unexportable --- .github/workflows/ci.yml | 4 + .../src/generic_boundary_tests.rs | 102 +++-- crates/labcolors-core/src/program.rs | 386 +++++++++--------- scripts/test_program_public_surface.py | 89 ++++ scripts/verify_program_public_surface.py | 190 +++++++++ 5 files changed, 527 insertions(+), 244 deletions(-) create mode 100755 scripts/test_program_public_surface.py create mode 100755 scripts/verify_program_public_surface.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d4c09683..32f170f9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -161,6 +161,10 @@ jobs: # reference from merging silently (main was green with ~81 broken links # because cargo doc was never gated — #26). run: RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps --locked + - name: staged Program is absent from the resolved public API + run: | + python3 scripts/test_program_public_surface.py + python3 scripts/verify_program_public_surface.py target/doc/labcolors_core - name: package labcolors-core and run extracted package doctests run: | set -euo pipefail diff --git a/crates/labcolors-core/src/generic_boundary_tests.rs b/crates/labcolors-core/src/generic_boundary_tests.rs index d35d5122..e40cfc45 100644 --- a/crates/labcolors-core/src/generic_boundary_tests.rs +++ b/crates/labcolors-core/src/generic_boundary_tests.rs @@ -193,23 +193,12 @@ fn staged_program_module_is_private_module_qualified_and_transport_neutral() { .any(|line| line.trim() == "pub mod program;"), "the incomplete Program candidate must not become externally reachable", ); - - for introducer in ["pub use ", "pub type "] { - let mut remaining = LIB_SOURCE; - while let Some(start) = remaining.find(introducer) { - let statement = &remaining[start - ..start - + remaining[start..] - .find(';') - .expect("root public declaration must terminate") - + 1]; - assert!( - !contains_rust_identifier(statement, "program"), - "Program types must stay private and module-qualified; found root alias `{statement}`", - ); - remaining = &remaining[start + statement.len()..]; - } - } + assert!( + PROGRAM_SOURCE + .lines() + .any(|line| line.trim() == "#![forbid(unreachable_pub)]"), + "rustc must reject accidentally over-visible items inside the staged module", + ); let source_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src"); assert!( @@ -246,10 +235,10 @@ fn staged_program_draft_wraps_the_single_canonical_core_graph() { assert_eq!( normalized_source_scope( PROGRAM_SOURCE, - "pub struct DraftV1 {", + "pub(crate) struct DraftV1 {", "/// Ошибка изменения Draft до компиляции.", ), - "pub struct DraftV1 { inner: CoreProgramDraftV1, }", + "pub(crate) struct DraftV1 { inner: CoreProgramDraftV1, }", "the staged seam must forward actual IR nodes into the sole Core draft", ); assert_eq!( @@ -279,9 +268,9 @@ fn staged_program_draft_wraps_the_single_canonical_core_graph() { "ObservationGroupIdV1", "OpacityIdV1", "SurfaceInputIdV1", - "pub fn push_opacity(", - "pub fn push_surface_input(", - "pub fn surface_input_slots(", + "push_opacity(", + "push_surface_input(", + "surface_input_slots(", ] { assert!( !PROGRAM_SOURCE.contains(forbidden), @@ -293,9 +282,13 @@ fn staged_program_draft_wraps_the_single_canonical_core_graph() { #[test] fn staged_session_keeps_evidence_but_owner_alone_grants_updates_and_operations() { assert_eq!( - normalized_source_scope(PROGRAM_SOURCE, "pub struct SessionV1 {", "impl SessionV1",), + normalized_source_scope( + PROGRAM_SOURCE, + "pub(crate) struct SessionV1 {", + "impl SessionV1", + ), concat!( - "pub struct SessionV1 { ", + "pub(crate) struct SessionV1 { ", "scenario_order_scratch: Vec, ", "session: CoreProgramSessionV1, ", "}", @@ -309,21 +302,21 @@ fn staged_session_keeps_evidence_but_owner_alone_grants_updates_and_operations() "struct ScenarioSourceV1", ); assert_eq!( - session_api.matches("pub fn evidence(").count(), + session_api.matches("pub(crate) fn evidence(").count(), 1, "historical evidence is the Session's sole boundary projection", ); assert_eq!( - session_api.matches("pub ").count(), + session_api.matches("pub(crate) ").count(), 1, "Session must not expose a second authority by changing function qualifiers", ); for forbidden in [ - "pub fn state(", - "pub fn update(", - "pub fn surface_input_port_count(", - "pub fn surface_input_ports(", - "pub fn output_slots(", + "fn state(", + "fn update(", + "fn surface_input_port_count(", + "fn surface_input_ports(", + "fn output_slots(", ] { assert!( !session_api.contains(forbidden), @@ -337,10 +330,10 @@ fn staged_session_keeps_evidence_but_owner_alone_grants_updates_and_operations() "struct BorrowScopeV1<'owner, 'session>", ); for forbidden in [ - "pub fn revision(", - "pub const fn revision(", - "pub fn stream(", - "pub const fn stream(", + "fn revision(", + "const fn revision(", + "fn stream(", + "const fn stream(", ] { assert!( !evidence_api.contains(forbidden), @@ -351,11 +344,11 @@ fn staged_session_keeps_evidence_but_owner_alone_grants_updates_and_operations() let owner_api = source_scope( PROGRAM_SOURCE, "impl OwnerV1 {", - "pub struct ScenarioV1<'a> {", + "pub(crate) struct ScenarioV1<'a> {", ); for required in [ - "pub fn project<'owner, 'session>(", - "pub fn update<'owner, 'session>(", + "pub(crate) fn project<'owner, 'session>(", + "pub(crate) fn update<'owner, 'session>(", ".owns_session(&session.session)", ] { assert!( @@ -365,8 +358,8 @@ fn staged_session_keeps_evidence_but_owner_alone_grants_updates_and_operations() } let update = source_scope( owner_api, - "pub fn update<'owner, 'session>(", - "pub fn instantiate(", + "pub(crate) fn update<'owner, 'session>(", + "pub(crate) fn instantiate(", ); assert!( update @@ -378,8 +371,11 @@ fn staged_session_keeps_evidence_but_owner_alone_grants_updates_and_operations() "owner mismatch must be rejected before admission, allocation, or evaluation", ); - let staged_access_errors = - source_scope(PROGRAM_SOURCE, "pub enum AccessErrorV1 {", "impl OwnerV1"); + let staged_access_errors = source_scope( + PROGRAM_SOURCE, + "pub(crate) enum AccessErrorV1 {", + "impl OwnerV1", + ); assert!( staged_access_errors.contains("OwnerMismatch,") && !staged_access_errors.contains("OwnerExpired"), @@ -387,8 +383,8 @@ fn staged_session_keeps_evidence_but_owner_alone_grants_updates_and_operations() ); let staged_update_errors = source_scope( PROGRAM_SOURCE, - "pub enum UpdateErrorKindV1 {", - "pub enum UpdateErrorV1 {", + "pub(crate) enum UpdateErrorKindV1 {", + "pub(crate) enum UpdateErrorV1 {", ); assert!( staged_update_errors.contains("OwnerMismatch,") @@ -396,8 +392,8 @@ fn staged_session_keeps_evidence_but_owner_alone_grants_updates_and_operations() "owner expiry is an internal invariant after a matching owner borrow", ); assert!( - PROGRAM_SOURCE.contains("pub enum UpdateErrorV1 {") - && !PROGRAM_SOURCE.contains("pub struct UpdateErrorV1 {") + PROGRAM_SOURCE.contains("pub(crate) enum UpdateErrorV1 {") + && !PROGRAM_SOURCE.contains("pub(crate) struct UpdateErrorV1 {") && PROGRAM_SOURCE .contains("fn map_observation_error(error: ObservationError) -> UpdateErrorV1",) && PROGRAM_SOURCE @@ -407,11 +403,11 @@ fn staged_session_keeps_evidence_but_owner_alone_grants_updates_and_operations() for (payload, end) in [ ( - "pub struct SetV1<'owner, 'session> {", + "pub(crate) struct SetV1<'owner, 'session> {", "impl<'session> SetV1<'_, 'session>", ), ( - "pub struct RemoveV1<'owner, 'session> {", + "pub(crate) struct RemoveV1<'owner, 'session> {", "impl RemoveV1<'_, '_>", ), ] { @@ -1107,7 +1103,7 @@ fn existing_encoded_evaluators_delegate_program_targets_without_parallel_formula } #[test] -fn private_program_and_lcs_occurrence_types_are_not_publicly_exported() { +fn staged_program_and_lcs_occurrence_modules_remain_private() { for required in [ "pub(crate) mod lcs_occurrence;", "pub(crate) mod program;", @@ -1122,16 +1118,10 @@ fn private_program_and_lcs_occurrence_types_are_not_publicly_exported() { "pub mod lcs_occurrence;", "pub mod program;", "pub mod program_session;", - "pub use lcs_occurrence::", - "pub use crate::lcs_occurrence::", - "pub use program::", - "pub use crate::program::", - "pub use program_session::", - "pub use crate::program_session::", ] { assert!( !LIB_SOURCE.contains(forbidden), - "lib.rs must not expose private Program/LCS occurrence surface `{forbidden}`", + "lib.rs must not publish a staged private module `{forbidden}`", ); } } diff --git a/crates/labcolors-core/src/program.rs b/crates/labcolors-core/src/program.rs index c5a37281..592d6530 100644 --- a/crates/labcolors-core/src/program.rs +++ b/crates/labcolors-core/src/program.rs @@ -38,6 +38,8 @@ //! [`ContentIdentityV1`] идентифицирует каноническое содержание, но не даёт //! полномочий живого [`OwnerV1`]. +#![forbid(unreachable_pub)] + use core::iter::FusedIterator; use core::marker::PhantomData; use core::slice; @@ -100,16 +102,16 @@ macro_rules! authored_id { #[repr(transparent)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] #[must_use] - pub struct $name($core); + pub(crate) struct $name($core); impl $name { /// Создаёт непрозрачный идентификатор из клиентского числового ключа. - pub const fn new(value: u32) -> Self { + pub(crate) const fn new(value: u32) -> Self { Self(<$core>::new(value)) } /// Возвращает исходный клиентский числовой ключ. - pub const fn value(self) -> u32 { + pub(crate) const fn value(self) -> u32 { self.0.value() } @@ -136,7 +138,7 @@ macro_rules! projected_id { #[repr(transparent)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] #[must_use] - pub struct $name($core); + pub(crate) struct $name($core); impl $name { const fn from_core(value: $core) -> Self { @@ -144,7 +146,7 @@ macro_rules! projected_id { } /// Возвращает числовой ключ сохранённой provenance. - pub const fn value(self) -> u32 { + pub(crate) const fn value(self) -> u32 { self.0.value() } } @@ -208,22 +210,22 @@ projected_id!( /// Один физический кандидат конечной цели. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct TargetCandidateV1(CoreTargetCandidateV1); +pub(crate) struct TargetCandidateV1(CoreTargetCandidateV1); impl TargetCandidateV1 { /// Связывает непрозрачный ID кандидата с конкретным encoded sRGB8 сигналом. - pub const fn new(id: TargetCandidateIdV1, source: Srgb8) -> Self { + pub(crate) const fn new(id: TargetCandidateIdV1, source: Srgb8) -> Self { Self(CoreTargetCandidateV1::from_srgb8(id.into_core(), source)) } } /// Выбор одного кандидата для одной цели в совместном состоянии. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct JointChoiceV1(TargetCandidateChoiceV1); +pub(crate) struct JointChoiceV1(TargetCandidateChoiceV1); impl JointChoiceV1 { /// Создаёт типизированную пару `цель → кандидат`. - pub const fn new(target: TargetIdV1, candidate: TargetCandidateIdV1) -> Self { + pub(crate) const fn new(target: TargetIdV1, candidate: TargetCandidateIdV1) -> Self { Self(TargetCandidateChoiceV1::new( target.into_core(), candidate.into_core(), @@ -233,11 +235,11 @@ impl JointChoiceV1 { /// Полное явно объявленное состояние всех конечных целей. #[derive(Debug, Clone, PartialEq, Eq)] -pub struct JointStateV1(JointCandidateStateV1); +pub(crate) struct JointStateV1(JointCandidateStateV1); impl JointStateV1 { /// Создаёт состояние из одного выбора для каждой конечной цели. - pub fn new(choices: Vec) -> Self { + pub(crate) fn new(choices: Vec) -> Self { Self(JointCandidateStateV1::new( choices.into_iter().map(|choice| choice.0).collect(), )) @@ -246,7 +248,7 @@ impl JointStateV1 { /// Зарегистрированный режим окружения CIECAM16. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum SurroundV1 { +pub(crate) enum SurroundV1 { /// Среднее освещение окружения. Average, /// Приглушённое освещение окружения. @@ -267,7 +269,7 @@ impl SurroundV1 { /// Поле входного контекста восприятия, не прошедшее admission. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum AppearanceContextFieldV1 { +pub(crate) enum AppearanceContextFieldV1 { /// Адаптирующая яркость в кд/м². AdaptingLuminanceCdM2, /// Безразмерное отношение фоновой яркости `Y_b/Y_w`. @@ -276,7 +278,7 @@ pub enum AppearanceContextFieldV1 { /// Числовая причина отказа при формировании контекста восприятия. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum NumericDomainErrorV1 { +pub(crate) enum NumericDomainErrorV1 { /// Значение не является конечным числом. NonFinite, /// Значение отрицательно. @@ -291,7 +293,7 @@ pub enum NumericDomainErrorV1 { /// Закрытая классификация отказа admission контекста восприятия. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum AppearanceContextErrorKindV1 { +pub(crate) enum AppearanceContextErrorKindV1 { /// Клиентское значение находится вне объявленного домена. Domain, /// Нарушен внутренний инвариант закрытого преобразования. @@ -300,7 +302,7 @@ pub enum AppearanceContextErrorKindV1 { /// Типизированный отказ admission контекста восприятия. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct AppearanceContextErrorV1 { +pub(crate) struct AppearanceContextErrorV1 { kind: AppearanceContextErrorKindV1, field: Option, reason: Option, @@ -308,17 +310,17 @@ pub struct AppearanceContextErrorV1 { impl AppearanceContextErrorV1 { /// Возвращает класс отказа. - pub const fn kind(self) -> AppearanceContextErrorKindV1 { + pub(crate) const fn kind(self) -> AppearanceContextErrorKindV1 { self.kind } /// Возвращает отвергнутое поле, когда Core смог его локализовать. - pub const fn field(self) -> Option { + pub(crate) const fn field(self) -> Option { self.field } /// Возвращает точную числовую причину, если отказ относится к входному домену. - pub const fn reason(self) -> Option { + pub(crate) const fn reason(self) -> Option { self.reason } @@ -355,7 +357,7 @@ impl AppearanceContextErrorV1 { /// Неизменяемый допущенный контекст восприятия. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct AppearanceContextV1(AppearanceContextId); +pub(crate) struct AppearanceContextV1(AppearanceContextId); impl AppearanceContextV1 { const fn from_core(context: AppearanceContextId) -> Self { @@ -366,7 +368,7 @@ impl AppearanceContextV1 { /// /// `background_luminance_ratio_yb_yw` — безразмерное `Y_b/Y_w` в `(0, 1]`, /// а не абсолютная яркость. - pub fn try_new( + pub(crate) fn try_new( adapting_luminance_cd_m2: f64, background_luminance_ratio_yb_yw: f64, surround: SurroundV1, @@ -386,17 +388,17 @@ impl AppearanceContextV1 { } /// Возвращает допущенную адаптирующую яркость в кд/м². - pub fn adapting_luminance_cd_m2(self) -> f64 { + pub(crate) fn adapting_luminance_cd_m2(self) -> f64 { self.0.adapting_luminance_cd_m2() } /// Возвращает допущенное безразмерное отношение `Y_b/Y_w`. - pub fn background_luminance_ratio_yb_yw(self) -> f64 { + pub(crate) fn background_luminance_ratio_yb_yw(self) -> f64 { self.0.background_luminance_ratio() } /// Возвращает зарегистрированный режим окружения. - pub const fn surround(self) -> SurroundV1 { + pub(crate) const fn surround(self) -> SurroundV1 { match self.0.surround_profile() { SurroundProfileId::AverageV1 => SurroundV1::Average, SurroundProfileId::DimV1 => SurroundV1::Dim, @@ -407,7 +409,7 @@ impl AppearanceContextV1 { /// Закрытая классификация ошибки компиляции. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum CompileErrorKindV1 { +pub(crate) enum CompileErrorKindV1 { /// Повторно объявлен исходный сигнал. DuplicateSource, /// Повторно объявлена цель. @@ -498,7 +500,7 @@ pub enum CompileErrorKindV1 { /// Типизированный ID узла, к которому относится ошибка компиляции. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum CompileErrorHandleV1 { +pub(crate) enum CompileErrorHandleV1 { /// Исходный сигнал. Source(SourceIdV1), /// Цель. @@ -523,7 +525,7 @@ pub enum CompileErrorHandleV1 { impl CompileErrorHandleV1 { /// Возвращает клиентский числовой ключ независимо от пространства ID. - pub const fn value(self) -> u32 { + pub(crate) const fn value(self) -> u32 { match self { Self::Source(value) => value.value(), Self::Target(value) => value.value(), @@ -541,32 +543,32 @@ impl CompileErrorHandleV1 { /// Точные участники одного цикла зависимостей Paint. #[derive(Debug, PartialEq, Eq)] -pub struct PaintCycleV1 { +pub(crate) struct PaintCycleV1 { paints: Vec, } impl PaintCycleV1 { /// Возвращает участников цикла в каноническом порядке диагностики. - pub fn paints(&self) -> impl ExactSizeIterator + '_ { + pub(crate) fn paints(&self) -> impl ExactSizeIterator + '_ { self.paints.iter().copied().map(PaintIdV1::from_core) } } /// Точные участники одного цикла рендера. #[derive(Debug, PartialEq, Eq)] -pub struct RenderCycleV1 { +pub(crate) struct RenderCycleV1 { surfaces: Vec, occurrences: Vec, } impl RenderCycleV1 { /// Возвращает Surface-участников цикла. - pub fn surfaces(&self) -> impl ExactSizeIterator + '_ { + pub(crate) fn surfaces(&self) -> impl ExactSizeIterator + '_ { self.surfaces.iter().copied().map(SurfaceIdV1::from_core) } /// Возвращает Occurrence-участников цикла. - pub fn occurrences(&self) -> impl ExactSizeIterator + '_ { + pub(crate) fn occurrences(&self) -> impl ExactSizeIterator + '_ { self.occurrences .iter() .copied() @@ -576,7 +578,7 @@ impl RenderCycleV1 { /// Точная причина отказа явно объявленного конечного совместного порядка. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum JointOrderErrorV1 { +pub(crate) enum JointOrderErrorV1 { /// Одно измерение не содержит кандидатов. EmptyDomain { /// Индекс пустого измерения. @@ -629,7 +631,7 @@ pub enum JointOrderErrorV1 { /// Enum авторитетен; [`Self::kind`], [`Self::primary_handle`] и /// [`Self::related_handle`] — только удобные проекции полного payload. #[derive(Debug, PartialEq, Eq)] -pub enum CompileErrorV1 { +pub(crate) enum CompileErrorV1 { /// Повторно объявлен исходный сигнал. DuplicateSource { /// Повторный ID. @@ -857,7 +859,7 @@ pub enum CompileErrorV1 { impl CompileErrorV1 { /// Возвращает стабильный класс ошибки без потери полного payload. - pub const fn kind(&self) -> CompileErrorKindV1 { + pub(crate) const fn kind(&self) -> CompileErrorKindV1 { use CompileErrorKindV1 as Kind; match self { @@ -908,7 +910,7 @@ impl CompileErrorV1 { } /// Возвращает основной типизированный ID, если ошибка локализуема одним узлом. - pub const fn primary_handle(&self) -> Option { + pub(crate) const fn primary_handle(&self) -> Option { use CompileErrorHandleV1 as Handle; match self { @@ -966,7 +968,7 @@ impl CompileErrorV1 { } /// Возвращает связанный типизированный ID для ошибки отношения двух узлов. - pub const fn related_handle(&self) -> Option { + pub(crate) const fn related_handle(&self) -> Option { use CompileErrorHandleV1 as Handle; match self { @@ -1028,41 +1030,41 @@ impl CompileErrorV1 { /// Холодный декларативный builder канонической Program IR. #[must_use] -pub struct DraftV1 { +pub(crate) struct DraftV1 { inner: CoreProgramDraftV1, } /// Ошибка изменения Draft до компиляции. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum DraftErrorV1 { +pub(crate) enum DraftErrorV1 { /// Совместный порядок уже объявлен и не может быть молча заменён. JointSelectionAlreadyDeclared, } impl DraftV1 { /// Создаёт пустой Draft. - pub fn new() -> Self { + pub(crate) fn new() -> Self { Self { inner: CoreProgramDraftV1::new(), } } /// Объявляет неизменяемый исходный encoded sRGB8 сигнал. - pub fn push_source(&mut self, id: SourceIdV1, source: Srgb8) -> &mut Self { + pub(crate) fn push_source(&mut self, id: SourceIdV1, source: Srgb8) -> &mut Self { self.inner .push_source(Source::new(id.into_core(), ColorSignal::from_srgb8(source))); self } /// Объявляет цель, физически равную исходному сигналу. - pub fn push_fixed_target(&mut self, id: TargetIdV1, source: SourceIdV1) -> &mut Self { + pub(crate) fn push_fixed_target(&mut self, id: TargetIdV1, source: SourceIdV1) -> &mut Self { self.inner .push_target(Target::fixed(id.into_core(), source.into_core())); self } /// Объявляет решаемую цель с конечным набором физических кандидатов. - pub fn push_finite_target( + pub(crate) fn push_finite_target( &mut self, id: TargetIdV1, source: SourceIdV1, @@ -1080,7 +1082,7 @@ impl DraftV1 { } /// Один раз задаёт полный порядок совместных состояний конечных целей. - pub fn set_joint_selection( + pub(crate) fn set_joint_selection( &mut self, states: Vec, ) -> Result<&mut Self, DraftErrorV1> { @@ -1097,20 +1099,20 @@ impl DraftV1 { } /// Объявляет один динамический вход поверхности. - pub fn push_surface_input_port(&mut self, input: SurfaceInputPortIdV1) -> &mut Self { + pub(crate) fn push_surface_input_port(&mut self, input: SurfaceInputPortIdV1) -> &mut Self { self.inner.push_surface_input_port(input.into_core()); self } /// Объявляет числовой вход прозрачности; домен проверяется при компиляции. - pub fn push_opacity_input(&mut self, id: OpacityInputIdV1, value: f64) -> &mut Self { + pub(crate) fn push_opacity_input(&mut self, id: OpacityInputIdV1, value: f64) -> &mut Self { self.inner .push_opacity_input(OpacityInput::new(id.into_core(), value)); self } /// Объявляет непрозрачный Paint, связанный с целью. - pub fn push_solid_paint(&mut self, id: PaintIdV1, target: TargetIdV1) -> &mut Self { + pub(crate) fn push_solid_paint(&mut self, id: PaintIdV1, target: TargetIdV1) -> &mut Self { self.inner.push_paint(Paint::Solid { id: id.into_core(), target: target.into_core(), @@ -1119,7 +1121,7 @@ impl DraftV1 { } /// Объявляет Paint как прозрачную версию другого Paint. - pub fn push_opacity_paint( + pub(crate) fn push_opacity_paint( &mut self, id: PaintIdV1, source: PaintIdV1, @@ -1134,7 +1136,7 @@ impl DraftV1 { } /// Объявляет Surface, значение которой поступает из runtime-сценария. - pub fn push_input_surface( + pub(crate) fn push_input_surface( &mut self, id: SurfaceIdV1, input: SurfaceInputPortIdV1, @@ -1147,7 +1149,7 @@ impl DraftV1 { } /// Объявляет Surface как видимый результат другого Occurrence. - pub fn push_occurrence_surface( + pub(crate) fn push_occurrence_surface( &mut self, id: SurfaceIdV1, occurrence: OccurrenceIdV1, @@ -1160,7 +1162,7 @@ impl DraftV1 { } /// Объявляет encoded-sRGB8 source-over Occurrence в явном контексте. - pub fn push_source_over_occurrence( + pub(crate) fn push_source_over_occurrence( &mut self, id: OccurrenceIdV1, subject: PaintIdV1, @@ -1178,7 +1180,7 @@ impl DraftV1 { } /// Добавляет обязательное точное сравнение видимого sRGB8 результата. - pub fn push_exact_hard( + pub(crate) fn push_exact_hard( &mut self, id: ConstraintIdV1, occurrence: OccurrenceIdV1, @@ -1193,7 +1195,7 @@ impl DraftV1 { } /// Добавляет диагностическое точное сравнение, не влияющее на выбор. - pub fn push_exact_report_only( + pub(crate) fn push_exact_report_only( &mut self, id: ConstraintIdV1, occurrence: OccurrenceIdV1, @@ -1209,7 +1211,7 @@ impl DraftV1 { } /// Добавляет обязательный критерий WCAG 2.2 для видимого результата. - pub fn push_wcag22_hard( + pub(crate) fn push_wcag22_hard( &mut self, id: ConstraintIdV1, occurrence: OccurrenceIdV1, @@ -1224,7 +1226,7 @@ impl DraftV1 { } /// Добавляет диагностический критерий WCAG 2.2, не влияющий на выбор. - pub fn push_wcag22_report_only( + pub(crate) fn push_wcag22_report_only( &mut self, id: ConstraintIdV1, occurrence: OccurrenceIdV1, @@ -1240,14 +1242,14 @@ impl DraftV1 { } /// Связывает клиентский выходной слот с итоговым Paint. - pub fn push_output(&mut self, output: OutputSlotIdV1, paint: PaintIdV1) -> &mut Self { + pub(crate) fn push_output(&mut self, output: OutputSlotIdV1, paint: PaintIdV1) -> &mut Self { self.inner .push_output(OutputBinding::new(output.into_core(), paint.into_core())); self } /// Атомарно проверяет и компилирует весь граф. - pub fn compile(self) -> Result { + pub(crate) fn compile(self) -> Result { let compiled = self.inner.compile().map_err(map_program_compile_error)?; Ok(OwnerV1::from_compiled(compiled)) } @@ -1264,7 +1266,7 @@ impl Default for DraftV1 { /// Созданные им Session изменяются только через эту же аллокацию. Уничтожение /// Owner отзывает обновления и операции, но исторические evidence остаются в /// Session. -pub struct OwnerV1 { +pub(crate) struct OwnerV1 { compiled: CompiledCoreProgramV1, } @@ -1274,26 +1276,26 @@ pub struct OwnerV1 { /// сохранённый прошлый сертификат, observation/provenance, выходы, операции или /// байты конкретного транспорта. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct EvidenceCellBoundsV1 { +pub(crate) struct EvidenceCellBoundsV1 { verified_cells: usize, conflict_cells: usize, } impl EvidenceCellBoundsV1 { /// Максимум клеток успешного сертификата. - pub const fn verified_cells(self) -> usize { + pub(crate) const fn verified_cells(self) -> usize { self.verified_cells } /// Максимум клеток исчерпывающего конфликтного сертификата. - pub const fn conflict_cells(self) -> usize { + pub(crate) const fn conflict_cells(self) -> usize { self.conflict_cells } } /// Закрытая причина невозможности вычислить границы сертификата. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum EvidenceBoundsErrorV1 { +pub(crate) enum EvidenceBoundsErrorV1 { /// Произведение числа сценариев, ограничений и состояний не помещается в /// адресное пространство платформы. CardinalityOverflow, @@ -1301,7 +1303,7 @@ pub enum EvidenceBoundsErrorV1 { /// Отказ доступа из-за несовпадения точной owner-эпохи. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum AccessErrorV1 { +pub(crate) enum AccessErrorV1 { /// Session была создана другой аллокацией Owner. OwnerMismatch, } @@ -1316,7 +1318,7 @@ impl OwnerV1 { /// /// Identity доступна до первого update, но не заменяет полномочия этой /// конкретной owner-эпохи. - pub fn content_identity(&self) -> ContentIdentityV1 { + pub(crate) fn content_identity(&self) -> ContentIdentityV1 { ContentIdentityV1::from_core(self.compiled.content_identity()) } @@ -1327,7 +1329,7 @@ impl OwnerV1 { /// сертификат может быть короче. Нулевое значение разрешено только как /// чистый арифметический preflight; пустой Observed-update по-прежнему не /// допускается. Запрос не создаёт Session и не меняет состояние. - pub fn evidence_cell_bounds( + pub(crate) fn evidence_cell_bounds( &self, scenario_count: usize, ) -> Result { @@ -1341,12 +1343,14 @@ impl OwnerV1 { } /// Число значений Surface в каждом schema-ordered сценарии. - pub fn surface_input_port_count(&self) -> usize { + pub(crate) fn surface_input_port_count(&self) -> usize { self.compiled.surface_input_ports().len() } /// Канонический порядок входных портов для однократного binding на хосте. - pub fn surface_input_ports(&self) -> impl ExactSizeIterator + '_ { + pub(crate) fn surface_input_ports( + &self, + ) -> impl ExactSizeIterator + '_ { self.compiled .surface_input_ports() .iter() @@ -1355,7 +1359,7 @@ impl OwnerV1 { } /// Канонический порядок непрозрачных выходных слотов. - pub fn output_slots(&self) -> impl ExactSizeIterator + '_ { + pub(crate) fn output_slots(&self) -> impl ExactSizeIterator + '_ { self.compiled .outputs() .map(|(slot, _paint)| OutputSlotIdV1::from_core(slot)) @@ -1364,7 +1368,7 @@ impl OwnerV1 { /// Проецирует операции только для Session этой точной owner-эпохи. /// /// Равенство [`ContentIdentityV1`] не даёт полномочий. - pub fn project<'owner, 'session>( + pub(crate) fn project<'owner, 'session>( &'owner self, session: &'session SessionV1, ) -> Result, AccessErrorV1> { @@ -1381,7 +1385,7 @@ impl OwnerV1 { /// Атомарно допускает update и возвращает его неизменяемую проекцию. /// /// Несовпадение Owner проверяется до admission, аллокаций и вычисления. - pub fn update<'owner, 'session>( + pub(crate) fn update<'owner, 'session>( &'owner self, session: &'session mut SessionV1, update: UpdateV1<'_>, @@ -1398,7 +1402,7 @@ impl OwnerV1 { } /// Создаёт Session, привязанную к одному непрозрачному stream ID. - pub fn instantiate(&self, stream_id: u32) -> Result { + pub(crate) fn instantiate(&self, stream_id: u32) -> Result { let stream = ObservationStreamId::new(stream_id); let session = self .compiled @@ -1416,14 +1420,14 @@ impl OwnerV1 { /// ID сценария — непрозрачная provenance. `values` содержит ровно один encoded /// sRGB8 на каждый [`OwnerV1::surface_input_ports`] в том же порядке. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct ScenarioV1<'a> { +pub(crate) struct ScenarioV1<'a> { scenario_id: u32, values: &'a [Srgb8], } impl<'a> ScenarioV1<'a> { /// Создаёт один одновременный физический кортеж. - pub const fn new(scenario_id: u32, values: &'a [Srgb8]) -> Self { + pub(crate) const fn new(scenario_id: u32, values: &'a [Srgb8]) -> Self { Self { scenario_id, values, @@ -1433,7 +1437,7 @@ impl<'a> ScenarioV1<'a> { /// Одно revision-bound обновление; stream принадлежит Session. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum UpdateV1<'a> { +pub(crate) enum UpdateV1<'a> { /// Согласованные физические сценарии в порядке скомпилированной схемы. Observed { /// Монотонная ревизия входного наблюдения. @@ -1451,14 +1455,14 @@ pub enum UpdateV1<'a> { } /// Непрозрачная изменяемая Session одной Program и одного stream. -pub struct SessionV1 { +pub(crate) struct SessionV1 { scenario_order_scratch: Vec, session: CoreProgramSessionV1, } impl SessionV1 { /// Возвращает исторические evidence без права на операции. - pub fn evidence(&self) -> EvidenceViewV1<'_> { + pub(crate) fn evidence(&self) -> EvidenceViewV1<'_> { EvidenceViewV1 { session: &self.session, } @@ -1514,7 +1518,7 @@ impl SchemaOrderedScenarioSourceV1 for ScenarioSourceV1<'_> { /// Закрытая классификация lifecycle Session. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum StateKindV1 { +pub(crate) enum StateKindV1 { /// Допущенного вычислимого наблюдения ещё нет; сырая голова может быть `Unknown`. Waiting, /// Текущая ревизия сертифицирована. @@ -1529,7 +1533,7 @@ pub enum StateKindV1 { /// /// Непустая голова хранит stream provenance, но не полномочия на операции. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ObservationHeadV1 { +pub(crate) enum ObservationHeadV1 { /// Наблюдений ещё не было. Empty, /// Наблюдение явно недоступно. @@ -1552,7 +1556,7 @@ pub enum ObservationHeadV1 { /// Заимствованное историческое evidence, принадлежащее Session. #[derive(Clone, Copy)] -pub struct EvidenceViewV1<'a> { +pub(crate) struct EvidenceViewV1<'a> { session: &'a CoreProgramSessionV1, } @@ -1562,7 +1566,7 @@ impl<'a> EvidenceViewV1<'a> { } /// Возвращает lifecycle-класс текущего состояния. - pub const fn kind(self) -> StateKindV1 { + pub(crate) const fn kind(self) -> StateKindV1 { match self.state() { SessionState::Waiting => StateKindV1::Waiting, SessionState::Ready { .. } => StateKindV1::Ready, @@ -1572,7 +1576,7 @@ impl<'a> EvidenceViewV1<'a> { } /// Возвращает сырую голову наблюдений вместе с provenance. - pub fn observation_head(self) -> ObservationHeadV1 { + pub(crate) fn observation_head(self) -> ObservationHeadV1 { match self.session.raw_head() { ObservationHeadViewV1::Empty => ObservationHeadV1::Empty, ObservationHeadViewV1::Unknown(unknown) => ObservationHeadV1::Unknown { @@ -1588,7 +1592,7 @@ impl<'a> EvidenceViewV1<'a> { } /// Индекс cause-сертификата в [`Self::certificates`] для `Failed`. - pub const fn cause_certificate_index(self) -> Option { + pub(crate) const fn cause_certificate_index(self) -> Option { match self.state() { SessionState::Failed { .. } => Some(0), SessionState::Waiting | SessionState::Ready { .. } | SessionState::Stale { .. } => None, @@ -1596,7 +1600,7 @@ impl<'a> EvidenceViewV1<'a> { } /// Сертификаты в каноническом порядке одного снимка. - pub fn certificates( + pub(crate) fn certificates( self, ) -> impl ExactSizeIterator> + FusedIterator + 'a { let (first, second) = match self.state() { @@ -1629,7 +1633,7 @@ impl<'owner, 'session> BorrowScopeV1<'owner, 'session> { /// Проверенная Owner-and-snapshot проекция evidence и операций. #[derive(Clone, Copy)] -pub struct ProjectionV1<'owner, 'session> { +pub(crate) struct ProjectionV1<'owner, 'session> { evidence: EvidenceViewV1<'session>, owner: &'owner OwnerV1, scope: BorrowScopeV1<'owner, 'session>, @@ -1637,12 +1641,12 @@ pub struct ProjectionV1<'owner, 'session> { impl<'owner, 'session> ProjectionV1<'owner, 'session> { /// Возвращает историческое evidence этого снимка. - pub const fn evidence(self) -> EvidenceViewV1<'session> { + pub(crate) const fn evidence(self) -> EvidenceViewV1<'session> { self.evidence } /// Возвращает полную каноническую последовательность операций состояния. - pub fn operations( + pub(crate) fn operations( self, ) -> impl ExactSizeIterator> + FusedIterator { let inner = match self.evidence.state() { @@ -1691,7 +1695,7 @@ impl<'owner, 'session> ProjectionV1<'owner, 'session> { /// Identity не идентифицирует owner-эпоху и не даёт runtime-полномочий. #[repr(transparent)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct ContentIdentityV1([u8; 32]); +pub(crate) struct ContentIdentityV1([u8; 32]); impl ContentIdentityV1 { const fn from_core(value: ProgramContentIdentityV1) -> Self { @@ -1699,37 +1703,39 @@ impl ContentIdentityV1 { } /// Возвращает 256-битное каноническое представление identity. - pub const fn as_bytes(&self) -> &[u8; 32] { + pub(crate) const fn as_bytes(&self) -> &[u8; 32] { &self.0 } } /// Доказательство прохождения всех hard-клеток на полном physical support. #[derive(Clone, Copy)] -pub struct VerifiedCertificateV1<'a> { +pub(crate) struct VerifiedCertificateV1<'a> { inner: &'a CoreVerifiedV1, } impl<'a> VerifiedCertificateV1<'a> { /// Возвращает identity скомпилированного содержания. - pub const fn content_identity(self) -> ContentIdentityV1 { + pub(crate) const fn content_identity(self) -> ContentIdentityV1 { ContentIdentityV1::from_core(self.inner.report().content_identity()) } /// Возвращает точное наблюдение, на котором выдан сертификат. - pub const fn observation(self) -> ObservationV1<'a> { + pub(crate) const fn observation(self) -> ObservationV1<'a> { ObservationV1 { inner: self.inner.report().observation(), } } /// Возвращает индекс выбранного состояния или `None` для fixed Program. - pub const fn selected_state_index(self) -> Option { + pub(crate) const fn selected_state_index(self) -> Option { self.inner.selected_state_index() } /// Возвращает все `case × constraint` клетки выбранного состояния. - pub fn cells(self) -> impl ExactSizeIterator> + FusedIterator + 'a { + pub(crate) fn cells( + self, + ) -> impl ExactSizeIterator> + FusedIterator + 'a { self.inner .report() .cells() @@ -1738,7 +1744,7 @@ impl<'a> VerifiedCertificateV1<'a> { } /// Возвращает все сертифицированные выходы в каноническом порядке. - pub fn outputs( + pub(crate) fn outputs( self, ) -> impl ExactSizeIterator> + FusedIterator + 'a { self.inner @@ -1750,30 +1756,32 @@ impl<'a> VerifiedCertificateV1<'a> { /// Исчерпывающее доказательство, что каждое состояние нарушает hard-клетку. #[derive(Clone, Copy)] -pub struct ConflictCertificateV1<'a> { +pub(crate) struct ConflictCertificateV1<'a> { inner: &'a CoreConflictV1, } impl<'a> ConflictCertificateV1<'a> { /// Возвращает identity скомпилированного содержания. - pub const fn content_identity(self) -> ContentIdentityV1 { + pub(crate) const fn content_identity(self) -> ContentIdentityV1 { ContentIdentityV1::from_core(self.inner.report().content_identity()) } /// Возвращает точное наблюдение, вызвавшее конфликт. - pub const fn observation(self) -> ObservationV1<'a> { + pub(crate) const fn observation(self) -> ObservationV1<'a> { ObservationV1 { inner: self.inner.report().observation(), } } /// Возвращает число исчерпывающе рассмотренных состояний. - pub const fn considered_state_count(self) -> usize { + pub(crate) const fn considered_state_count(self) -> usize { self.inner.considered_state_count() } /// Возвращает все `state × case × constraint` клетки конфликта. - pub fn cells(self) -> impl ExactSizeIterator> + FusedIterator + 'a { + pub(crate) fn cells( + self, + ) -> impl ExactSizeIterator> + FusedIterator + 'a { self.inner .report() .cells() @@ -1787,7 +1795,7 @@ impl<'a> ConflictCertificateV1<'a> { /// Сертификат заимствует только историю Session и может пережить Owner, /// разрешивший исходную проекцию. #[derive(Clone, Copy)] -pub enum CertificateV1<'a> { +pub(crate) enum CertificateV1<'a> { /// Все hard-клетки полного support прошли. Verified(VerifiedCertificateV1<'a>), /// Каждое рассмотренное состояние нарушает хотя бы одну hard-клетку. @@ -1804,7 +1812,7 @@ impl<'a> CertificateV1<'a> { } /// Возвращает identity скомпилированного содержания. - pub const fn content_identity(self) -> ContentIdentityV1 { + pub(crate) const fn content_identity(self) -> ContentIdentityV1 { match self { Self::Verified(value) => value.content_identity(), Self::Conflict(value) => value.content_identity(), @@ -1812,7 +1820,7 @@ impl<'a> CertificateV1<'a> { } /// Возвращает точное revision-bound наблюдение сертификата. - pub const fn observation(self) -> ObservationV1<'a> { + pub(crate) const fn observation(self) -> ObservationV1<'a> { match self { Self::Verified(value) => value.observation(), Self::Conflict(value) => value.observation(), @@ -1827,25 +1835,25 @@ impl<'a> CertificateV1<'a> { /// Точное revision-bound наблюдение, сохранённое сертификатом. #[derive(Clone, Copy)] -pub struct ObservationV1<'a> { +pub(crate) struct ObservationV1<'a> { inner: &'a crate::observation::RevisionBoundObservationV1, } impl<'a> ObservationV1<'a> { /// Возвращает stream provenance наблюдения. - pub const fn stream(self) -> StreamIdV1 { + pub(crate) const fn stream(self) -> StreamIdV1 { StreamIdV1::from_core(self.inner.stream()) } /// Возвращает ревизию наблюдения. - pub const fn revision(self) -> u64 { + pub(crate) const fn revision(self) -> u64 { self.inner.revision().value() } /// Возвращает каноническую schema, общую для всех физических cases. /// /// Позиция `i` соответствует позиции `i` в [`PhysicalCaseV1::values`]. - pub fn surface_input_ports( + pub(crate) fn surface_input_ports( self, ) -> impl ExactSizeIterator + FusedIterator + 'a { self.inner @@ -1858,7 +1866,7 @@ impl<'a> ObservationV1<'a> { /// Возвращает канонические уникальные физические cases. /// /// Дубликаты значений схлопываются, а их ID сохраняются в provenance. - pub fn physical_cases( + pub(crate) fn physical_cases( self, ) -> impl ExactSizeIterator> + FusedIterator + 'a { (0..self.inner.physical_case_count()).map(move |index| PhysicalCaseV1 { @@ -1870,21 +1878,21 @@ impl<'a> ObservationV1<'a> { /// Закрытое семейство сигналов физического case. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum SignalV1 { +pub(crate) enum SignalV1 { /// Encoded sRGB8 в IEC 61966-2-1 с белой точкой D65. Iec61966Srgb8D65(Srgb8), } /// Один канонический физический case и его полная provenance. #[derive(Clone, Copy)] -pub struct PhysicalCaseV1<'a> { +pub(crate) struct PhysicalCaseV1<'a> { observation: &'a crate::observation::RevisionBoundObservationV1, index: usize, } impl<'a> PhysicalCaseV1<'a> { /// Возвращает значения case в каноническом schema order. - pub fn values(self) -> impl ExactSizeIterator + FusedIterator + 'a { + pub(crate) fn values(self) -> impl ExactSizeIterator + FusedIterator + 'a { self.observation .physical_values(self.index) .expect("physical case originates from the same observation") @@ -1896,7 +1904,9 @@ impl<'a> PhysicalCaseV1<'a> { } /// Возвращает все scenario ID, схлопнутые в этот физический case. - pub fn provenance(self) -> impl ExactSizeIterator + FusedIterator + 'a { + pub(crate) fn provenance( + self, + ) -> impl ExactSizeIterator + FusedIterator + 'a { self.observation .provenance(self.index) .expect("physical case originates from the same observation") @@ -1908,7 +1918,7 @@ impl<'a> PhysicalCaseV1<'a> { /// Роль одной constraint-клетки в выборе. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ConstraintModeV1 { +pub(crate) enum ConstraintModeV1 { /// Нарушение запрещает состояние. Hard, /// Результат сохраняется, но не влияет на выбор. @@ -1917,7 +1927,7 @@ pub enum ConstraintModeV1 { /// Одна клетка `case × constraint` выбранного или fixed состояния. #[derive(Clone, Copy)] -pub struct VerifiedCellV1<'a> { +pub(crate) struct VerifiedCellV1<'a> { inner: &'a CoreProgramConstraintCellV1, } @@ -1927,34 +1937,34 @@ impl<'a> VerifiedCellV1<'a> { } /// Возвращает индекс физического case. - pub const fn case_index(self) -> usize { + pub(crate) const fn case_index(self) -> usize { self.inner.case_index() } /// Возвращает ID ограничения. - pub const fn constraint(self) -> ConstraintIdV1 { + pub(crate) const fn constraint(self) -> ConstraintIdV1 { ConstraintIdV1::from_core(self.inner.constraint()) } /// Возвращает ID проверенного Occurrence. - pub const fn occurrence(self) -> OccurrenceIdV1 { + pub(crate) const fn occurrence(self) -> OccurrenceIdV1 { OccurrenceIdV1::from_core(self.inner.target()) } /// Возвращает роль ограничения в выборе. - pub const fn mode(self) -> ConstraintModeV1 { + pub(crate) const fn mode(self) -> ConstraintModeV1 { project_constraint_mode(self.inner) } /// Возвращает типизированное сохранённое evidence. - pub fn assessment(self) -> AssessmentV1<'a> { + pub(crate) fn assessment(self) -> AssessmentV1<'a> { project_assessment(self.inner) } } /// Одна исчерпывающая клетка `state × case × constraint` конфликта. #[derive(Clone, Copy)] -pub struct ConflictCellV1<'a> { +pub(crate) struct ConflictCellV1<'a> { inner: &'a CoreProgramConstraintCellV1, } @@ -1964,32 +1974,32 @@ impl<'a> ConflictCellV1<'a> { } /// Возвращает индекс рассмотренного состояния. - pub const fn state_index(self) -> usize { + pub(crate) const fn state_index(self) -> usize { self.inner.candidate_state_index() } /// Возвращает индекс физического case. - pub const fn case_index(self) -> usize { + pub(crate) const fn case_index(self) -> usize { self.inner.case_index() } /// Возвращает ID ограничения. - pub const fn constraint(self) -> ConstraintIdV1 { + pub(crate) const fn constraint(self) -> ConstraintIdV1 { ConstraintIdV1::from_core(self.inner.constraint()) } /// Возвращает ID проверенного Occurrence. - pub const fn occurrence(self) -> OccurrenceIdV1 { + pub(crate) const fn occurrence(self) -> OccurrenceIdV1 { OccurrenceIdV1::from_core(self.inner.target()) } /// Возвращает роль ограничения в выборе. - pub const fn mode(self) -> ConstraintModeV1 { + pub(crate) const fn mode(self) -> ConstraintModeV1 { project_constraint_mode(self.inner) } /// Возвращает типизированное сохранённое evidence. - pub fn assessment(self) -> AssessmentV1<'a> { + pub(crate) fn assessment(self) -> AssessmentV1<'a> { project_assessment(self.inner) } } @@ -2029,7 +2039,7 @@ fn project_assessment(cell: &CoreProgramConstraintCellV1) -> AssessmentV1<'_> { /// Закрытое семейство сохранённого evaluator evidence. #[derive(Clone, Copy)] -pub enum AssessmentV1<'a> { +pub(crate) enum AssessmentV1<'a> { /// Evidence точного сравнения encoded sRGB8. ExactSrgb8(ExactSrgb8EvidenceV1<'a>), /// Evidence применимого критерия WCAG 2.2. @@ -2038,7 +2048,7 @@ pub enum AssessmentV1<'a> { impl<'a> AssessmentV1<'a> { /// Возвращает несовместимый с противоположным исход классификатора. - pub const fn verdict(self) -> VerdictV1 { + pub(crate) const fn verdict(self) -> VerdictV1 { match self { Self::ExactSrgb8(value) => value.verdict(), Self::Wcag22Srgb8(value) => value.verdict(), @@ -2046,7 +2056,7 @@ impl<'a> AssessmentV1<'a> { } /// Возвращает общую физическую и моделированную привязку точки. - pub fn binding(self) -> PointBindingV1<'a> { + pub(crate) fn binding(self) -> PointBindingV1<'a> { match self { Self::ExactSrgb8(value) => value.binding(), Self::Wcag22Srgb8(value) => value.binding(), @@ -2056,7 +2066,7 @@ impl<'a> AssessmentV1<'a> { /// Несовместимые сохранённые исходы классификатора. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum VerdictV1 { +pub(crate) enum VerdictV1 { /// Критерий доказан. Pass, /// Критерий доказанно нарушен. @@ -2071,13 +2081,13 @@ enum ExactSrgb8EvidenceRefV1<'a> { /// Evidence точного sRGB8 сравнения с физикой и моделированным контекстом. #[derive(Clone, Copy)] -pub struct ExactSrgb8EvidenceV1<'a> { +pub(crate) struct ExactSrgb8EvidenceV1<'a> { inner: ExactSrgb8EvidenceRefV1<'a>, } impl<'a> ExactSrgb8EvidenceV1<'a> { /// Возвращает сохранённый исход классификатора. - pub const fn verdict(self) -> VerdictV1 { + pub(crate) const fn verdict(self) -> VerdictV1 { match self.inner { ExactSrgb8EvidenceRefV1::Pass(_) => VerdictV1::Pass, ExactSrgb8EvidenceRefV1::Violation(_) => VerdictV1::Violation, @@ -2085,7 +2095,7 @@ impl<'a> ExactSrgb8EvidenceV1<'a> { } /// Возвращает ожидаемый encoded sRGB8 результат. - pub fn expected(self) -> Srgb8 { + pub(crate) fn expected(self) -> Srgb8 { match self.inner { ExactSrgb8EvidenceRefV1::Pass(value) => value.target(), ExactSrgb8EvidenceRefV1::Violation(value) => value.target(), @@ -2093,7 +2103,7 @@ impl<'a> ExactSrgb8EvidenceV1<'a> { } /// Возвращает физическую и моделированную привязку точки. - pub fn binding(self) -> PointBindingV1<'a> { + pub(crate) fn binding(self) -> PointBindingV1<'a> { let value = match self.inner { ExactSrgb8EvidenceRefV1::Pass(value) => value.binding(), ExactSrgb8EvidenceRefV1::Violation(value) => value.binding(), @@ -2110,13 +2120,13 @@ enum Wcag22Srgb8EvidenceRefV1<'a> { /// WCAG 2.2 evidence вместе с физикой и моделированным контекстом. #[derive(Clone, Copy)] -pub struct Wcag22Srgb8EvidenceV1<'a> { +pub(crate) struct Wcag22Srgb8EvidenceV1<'a> { inner: Wcag22Srgb8EvidenceRefV1<'a>, } impl<'a> Wcag22Srgb8EvidenceV1<'a> { /// Возвращает сохранённый исход классификатора. - pub const fn verdict(self) -> VerdictV1 { + pub(crate) const fn verdict(self) -> VerdictV1 { match self.inner { Wcag22Srgb8EvidenceRefV1::Pass(_) => VerdictV1::Pass, Wcag22Srgb8EvidenceRefV1::Violation(_) => VerdictV1::Violation, @@ -2124,7 +2134,7 @@ impl<'a> Wcag22Srgb8EvidenceV1<'a> { } /// Возвращает версию применённого WCAG 2.2 профиля. - pub fn profile_id(self) -> Wcag22ProfileIdV1 { + pub(crate) fn profile_id(self) -> Wcag22ProfileIdV1 { match self.inner { Wcag22Srgb8EvidenceRefV1::Pass(value) => value.measurement().value().profile_id(), Wcag22Srgb8EvidenceRefV1::Violation(value) => value.measurement().value().profile_id(), @@ -2132,7 +2142,7 @@ impl<'a> Wcag22Srgb8EvidenceV1<'a> { } /// Возвращает применённый критерий. - pub fn criterion(self) -> Wcag22CriterionV1 { + pub(crate) fn criterion(self) -> Wcag22CriterionV1 { match self.inner { Wcag22Srgb8EvidenceRefV1::Pass(value) => value.measurement().value().criterion(), Wcag22Srgb8EvidenceRefV1::Violation(value) => value.measurement().value().criterion(), @@ -2140,7 +2150,7 @@ impl<'a> Wcag22Srgb8EvidenceV1<'a> { } /// Возвращает сертифицированные границы яркости foreground. - pub fn foreground_luminance(self) -> Wcag22LuminanceBoundsQ55V1 { + pub(crate) fn foreground_luminance(self) -> Wcag22LuminanceBoundsQ55V1 { let measurement = match self.inner { Wcag22Srgb8EvidenceRefV1::Pass(value) => value.measurement().value().measurement(), Wcag22Srgb8EvidenceRefV1::Violation(value) => value.measurement().value().measurement(), @@ -2149,7 +2159,7 @@ impl<'a> Wcag22Srgb8EvidenceV1<'a> { } /// Возвращает сертифицированные границы яркости background. - pub fn background_luminance(self) -> Wcag22LuminanceBoundsQ55V1 { + pub(crate) fn background_luminance(self) -> Wcag22LuminanceBoundsQ55V1 { let measurement = match self.inner { Wcag22Srgb8EvidenceRefV1::Pass(value) => value.measurement().value().measurement(), Wcag22Srgb8EvidenceRefV1::Violation(value) => value.measurement().value().measurement(), @@ -2158,7 +2168,7 @@ impl<'a> Wcag22Srgb8EvidenceV1<'a> { } /// Возвращает числовое доказательство устойчивости решения. - pub fn numerical_evidence(self) -> &'a NumericalDecisionEvidenceV1 { + pub(crate) fn numerical_evidence(self) -> &'a NumericalDecisionEvidenceV1 { match self.inner { Wcag22Srgb8EvidenceRefV1::Pass(value) => value.measurement().value().evidence(), Wcag22Srgb8EvidenceRefV1::Violation(value) => value.measurement().value().evidence(), @@ -2166,7 +2176,7 @@ impl<'a> Wcag22Srgb8EvidenceV1<'a> { } /// Возвращает физическую и моделированную привязку точки. - pub fn binding(self) -> PointBindingV1<'a> { + pub(crate) fn binding(self) -> PointBindingV1<'a> { let value = match self.inner { Wcag22Srgb8EvidenceRefV1::Pass(value) => value.binding(), Wcag22Srgb8EvidenceRefV1::Violation(value) => value.binding(), @@ -2177,13 +2187,13 @@ impl<'a> Wcag22Srgb8EvidenceV1<'a> { /// Общая привязка физической композиции и моделированного tristimulus/context. #[derive(Clone, Copy)] -pub struct PointBindingV1<'a> { +pub(crate) struct PointBindingV1<'a> { inner: &'a ProgramVisiblePointBindingV1, } impl<'a> PointBindingV1<'a> { /// Возвращает закрытый тип точной физической композиции. - pub const fn physical(self) -> PhysicalPointV1<'a> { + pub(crate) const fn physical(self) -> PhysicalPointV1<'a> { match self.inner.physical().occurrence().profile() { CompositionProfileV1::EncodedSrgb8SourceOverV1 => { PhysicalPointV1::EncodedSrgb8SourceOver(EncodedSrgb8SourceOverV1 { @@ -2194,7 +2204,7 @@ impl<'a> PointBindingV1<'a> { } /// Возвращает закрытый тип допущенного моделированного сигнала. - pub const fn modeled(self) -> ModeledPointV1<'a> { + pub(crate) const fn modeled(self) -> ModeledPointV1<'a> { match self.inner.modeled_lcs().provenance().binding() { AdmittedSrgb8TristimulusBindingV1::Iec61966Srgb8ToCie1931TwoDegreeXyzD65RelativeY1V1 => { ModeledPointV1::Iec61966Srgb8ToCie1931TwoDegreeXyzD65RelativeY1( @@ -2207,25 +2217,25 @@ impl<'a> PointBindingV1<'a> { /// Закрытое семейство точной физической композиции. #[derive(Clone, Copy)] -pub enum PhysicalPointV1<'a> { +pub(crate) enum PhysicalPointV1<'a> { /// Encoded-sRGB8 source-over композиция. EncodedSrgb8SourceOver(EncodedSrgb8SourceOverV1<'a>), } /// Точная привязка одного encoded-sRGB8 source-over Occurrence. #[derive(Clone, Copy)] -pub struct EncodedSrgb8SourceOverV1<'a> { +pub(crate) struct EncodedSrgb8SourceOverV1<'a> { inner: &'a ProgramVisiblePointBindingV1, } impl EncodedSrgb8SourceOverV1<'_> { /// Возвращает ID накладываемого Paint. - pub const fn subject_paint(self) -> PaintIdV1 { + pub(crate) const fn subject_paint(self) -> PaintIdV1 { PaintIdV1::from_core(self.inner.physical().program_occurrence().subject()) } /// Возвращает ID backdrop Surface. - pub const fn backdrop_surface(self) -> SurfaceIdV1 { + pub(crate) const fn backdrop_surface(self) -> SurfaceIdV1 { SurfaceIdV1::from_core( self.inner .physical() @@ -2235,54 +2245,54 @@ impl EncodedSrgb8SourceOverV1<'_> { } /// Возвращает исходный encoded sRGB8 subject до композиции. - pub const fn subject(self) -> Srgb8 { + pub(crate) const fn subject(self) -> Srgb8 { Srgb8::new(self.inner.physical().occurrence().subject_rgb()) } /// Возвращает точную прозрачность subject в `[0, 1]`. - pub const fn opacity(self) -> f64 { + pub(crate) const fn opacity(self) -> f64 { f64::from_bits(self.inner.physical().occurrence().subject_opacity_bits()) } /// Возвращает observed encoded sRGB8 backdrop. - pub const fn backdrop(self) -> Srgb8 { + pub(crate) const fn backdrop(self) -> Srgb8 { Srgb8::new(self.inner.physical().occurrence().backdrop_rgb()) } /// Возвращает видимый encoded sRGB8 результат композиции. - pub const fn visible(self) -> Srgb8 { + pub(crate) const fn visible(self) -> Srgb8 { Srgb8::new(self.inner.physical().occurrence().output_rgb()) } } /// Закрытое семейство provenance моделированного tristimulus. #[derive(Clone, Copy)] -pub enum ModeledPointV1<'a> { +pub(crate) enum ModeledPointV1<'a> { /// IEC sRGB8 → CIE 1931 2° XYZ D65 с относительным `Y=1`. Iec61966Srgb8ToCie1931TwoDegreeXyzD65RelativeY1(ModeledTristimulusV1<'a>), } /// Допущенный моделированный tristimulus и его контекст восприятия. #[derive(Clone, Copy)] -pub struct ModeledTristimulusV1<'a> { +pub(crate) struct ModeledTristimulusV1<'a> { inner: &'a ProgramVisiblePointBindingV1, } impl ModeledTristimulusV1<'_> { /// Возвращает относительные координаты CIE XYZ. - pub fn xyz(self) -> [f64; 3] { + pub(crate) fn xyz(self) -> [f64; 3] { self.inner.modeled_lcs().derivation().sample().xyz() } /// Возвращает явный контекст, использованный при моделировании. - pub const fn appearance_context(self) -> AppearanceContextV1 { + pub(crate) const fn appearance_context(self) -> AppearanceContextV1 { AppearanceContextV1(self.inner.modeled_lcs().occurrence().context()) } } /// Один Core-сертифицированный выходной Paint. #[derive(Clone, Copy)] -pub struct CertifiedOutputV1<'a> { +pub(crate) struct CertifiedOutputV1<'a> { inner: &'a ProgramOutputV1, } @@ -2292,29 +2302,29 @@ impl<'a> CertifiedOutputV1<'a> { } /// Возвращает клиентский выходной слот. - pub const fn output_slot(self) -> OutputSlotIdV1 { + pub(crate) const fn output_slot(self) -> OutputSlotIdV1 { OutputSlotIdV1::from_core((*self.inner).output()) } /// Возвращает ID сертифицированного Paint. - pub const fn paint(self) -> PaintIdV1 { + pub(crate) const fn paint(self) -> PaintIdV1 { PaintIdV1::from_core((*self.inner).paint().id()) } /// Возвращает исходный encoded sRGB8 сигнал Paint. - pub const fn source(self) -> Srgb8 { + pub(crate) const fn source(self) -> Srgb8 { (*self.inner).paint().source() } /// Возвращает сертифицированную прозрачность Paint. - pub const fn opacity(self) -> f64 { + pub(crate) const fn opacity(self) -> f64 { (*self.inner).paint().opacity().value() } } /// Операция установки, структурно связанная с точным Verified-сертификатом. #[derive(Clone, Copy)] -pub struct SetV1<'owner, 'session> { +pub(crate) struct SetV1<'owner, 'session> { output: &'session ProgramOutputV1, certificate: VerifiedCertificateV1<'session>, _scope: BorrowScopeV1<'owner, 'session>, @@ -2322,36 +2332,36 @@ pub struct SetV1<'owner, 'session> { impl<'session> SetV1<'_, 'session> { /// Возвращает изменяемый клиентский выходной слот. - pub const fn output_slot(self) -> OutputSlotIdV1 { + pub(crate) const fn output_slot(self) -> OutputSlotIdV1 { OutputSlotIdV1::from_core((*self.output).output()) } /// Возвращает исходный encoded sRGB8 сигнал результата. - pub const fn source(self) -> Srgb8 { + pub(crate) const fn source(self) -> Srgb8 { (*self.output).paint().source() } /// Возвращает прозрачность результата. - pub const fn opacity(self) -> f64 { + pub(crate) const fn opacity(self) -> f64 { (*self.output).paint().opacity().value() } /// Возвращает сертификат, разрешивший эту операцию. - pub const fn certificate(self) -> VerifiedCertificateV1<'session> { + pub(crate) const fn certificate(self) -> VerifiedCertificateV1<'session> { self.certificate } } /// Операция удаления результата без сертификата для текущего контекста. #[derive(Clone, Copy)] -pub struct RemoveV1<'owner, 'session> { +pub(crate) struct RemoveV1<'owner, 'session> { output_slot: OutputSlotIdV1, _scope: BorrowScopeV1<'owner, 'session>, } impl RemoveV1<'_, '_> { /// Возвращает удаляемый клиентский выходной слот. - pub const fn output_slot(self) -> OutputSlotIdV1 { + pub(crate) const fn output_slot(self) -> OutputSlotIdV1 { self.output_slot } } @@ -2362,7 +2372,7 @@ impl RemoveV1<'_, '_> { /// slot/source/opacity — только данные: runtime обязан перепроверить живую /// пару непосредственно перед одним атомарным sink commit. #[derive(Clone, Copy)] -pub enum OperationV1<'owner, 'session> { +pub(crate) enum OperationV1<'owner, 'session> { /// Установить сертифицированный результат. Set(SetV1<'owner, 'session>), /// Удалить результат, когда текущий контекст не сертифицирован. @@ -2503,7 +2513,7 @@ impl FusedIterator for OperationsV1<'_, '_> {} /// Закрытая классификация ошибки создания Session. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum InstantiateErrorKindV1 { +pub(crate) enum InstantiateErrorKindV1 { /// Для создания Session недостаточно ресурсов. ResourceExhausted, /// Нарушен внутренний инвариант скомпилированной Program. @@ -2512,7 +2522,7 @@ pub enum InstantiateErrorKindV1 { /// Непрозрачная ошибка создания Session. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct InstantiateErrorV1 { +pub(crate) struct InstantiateErrorV1 { kind: InstantiateErrorKindV1, } @@ -2534,7 +2544,7 @@ impl InstantiateErrorV1 { } /// Возвращает стабильный класс ошибки. - pub const fn kind(self) -> InstantiateErrorKindV1 { + pub(crate) const fn kind(self) -> InstantiateErrorKindV1 { self.kind } } @@ -2547,7 +2557,7 @@ impl From for InstantiateErrorV1 { /// Закрытая классификация ошибки одного атомарного update. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum UpdateErrorKindV1 { +pub(crate) enum UpdateErrorKindV1 { /// Session принадлежит другой точной owner-эпохе. OwnerMismatch, /// Наблюдение нарушает скомпилированную schema. @@ -2566,7 +2576,7 @@ pub enum UpdateErrorKindV1 { /// Фаза update, в которой закончился ограниченный ресурс. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum UpdatePhaseV1 { +pub(crate) enum UpdatePhaseV1 { /// Admission и канонизация физического наблюдения. ObservationAdmission, /// Вычисление, поиск и финальная перепроверка Program. @@ -2575,7 +2585,7 @@ pub enum UpdatePhaseV1 { /// Точный отказ зарегистрированного evaluator-а. #[derive(Debug, Clone, PartialEq, Eq)] -pub enum EvaluatorFailureV1 { +pub(crate) enum EvaluatorFailureV1 { /// Отказ зарегистрированного WCAG 2.2 evaluator-а. Wcag22Srgb8 { /// Версия evaluator-а и его численного доказательства. @@ -2587,7 +2597,7 @@ pub enum EvaluatorFailureV1 { /// Точная причина расхождения observation со скомпилированным binding. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ObservationBindingFailureV1 { +pub(crate) enum ObservationBindingFailureV1 { /// Скомпилированная schema не содержит ни одного входного порта. EmptyCompiledSurfaceInputSchema, /// Один входной порт повторён в скомпилированной schema. @@ -2638,7 +2648,7 @@ pub enum ObservationBindingFailureV1 { /// Зарегистрированная identity XYZ-frame в диагностике закрытого Core. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ColorimetricFrameV1 { +pub(crate) enum ColorimetricFrameV1 { /// CIE 1931 2°, IEC 61966-2-1 D65, относительная шкала `Y=1`, XYZ v1. Iec61966Srgb8D65XyzRelativeY1V1, /// Зарезервированный frame hostile-теста, недостижимый в production. @@ -2648,7 +2658,7 @@ pub enum ColorimetricFrameV1 { /// Компонент XYZ в точной диагностике. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum TristimulusComponentV1 { +pub(crate) enum TristimulusComponentV1 { /// Компонент X. X, /// Компонент Y. @@ -2659,7 +2669,7 @@ pub enum TristimulusComponentV1 { /// Точная конечная XYZ-точка и её зарегистрированный frame. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct TristimulusSampleV1 { +pub(crate) struct TristimulusSampleV1 { /// Биты IEEE-754 сохраняют точный диагностический payload и отделяют /// равенство записи от семантики сравнения floating-point. xyz_bits: [u64; 3], @@ -2675,19 +2685,19 @@ impl TristimulusSampleV1 { } /// Возвращает точные конечные XYZ-компоненты. - pub fn xyz(self) -> [f64; 3] { + pub(crate) fn xyz(self) -> [f64; 3] { self.xyz_bits.map(f64::from_bits) } /// Возвращает зарегистрированный frame точки. - pub const fn frame(self) -> ColorimetricFrameV1 { + pub(crate) const fn frame(self) -> ColorimetricFrameV1 { self.frame } } /// Точная причина, по которой Core не сформировал modeled LCS occurrence. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ModeledOccurrenceFailureV1 { +pub(crate) enum ModeledOccurrenceFailureV1 { /// Детерминированное преобразование получило недопустимую XYZ-компоненту. Tristimulus { /// Ошибочная компонента. @@ -2731,7 +2741,7 @@ pub enum ModeledOccurrenceFailureV1 { clippy::enum_variant_names, reason = "the variant name preserves evaluator provenance as this closed family grows" )] -pub enum EvaluatorProtocolFailureV1 { +pub(crate) enum EvaluatorProtocolFailureV1 { /// WCAG evaluator вернул kernel-ошибку, недостижимую для typed Program. Wcag22Kernel { /// Версия evaluator-а и его численного доказательства. @@ -2757,7 +2767,7 @@ pub enum EvaluatorProtocolFailureV1 { /// Машиночитаемая identity нарушенного внутреннего контракта. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum UpdateInvariantV1 { +pub(crate) enum UpdateInvariantV1 { /// Заимствованный matching Owner не удержал свою эпоху живой. OwnerAuthority, /// Каноническая observation schema разошлась со скомпилированным binding. @@ -2783,7 +2793,7 @@ pub enum UpdateInvariantV1 { /// Эти варианты недостижимы через типизированный boundary input. Payload нужен /// для детерминированной диагностики и не превращает breach в цветовой verdict. #[derive(Debug, Clone, PartialEq, Eq)] -pub enum UpdateInvariantFailureV1 { +pub(crate) enum UpdateInvariantFailureV1 { /// Заимствованный matching Owner не удержал свою эпоху живой. OwnerAuthority, /// Каноническая observation schema разошлась со скомпилированным binding. @@ -2860,7 +2870,7 @@ pub enum UpdateInvariantFailureV1 { impl UpdateInvariantFailureV1 { /// Возвращает стабильную identity нарушенного контракта. - pub const fn contract(&self) -> UpdateInvariantV1 { + pub(crate) const fn contract(&self) -> UpdateInvariantV1 { match self { Self::OwnerAuthority => UpdateInvariantV1::OwnerAuthority, Self::ObservationBinding { .. } => UpdateInvariantV1::ObservationBinding, @@ -2883,7 +2893,7 @@ impl UpdateInvariantFailureV1 { /// неизменными. [`UpdateErrorKindV1`] — только удобная производная проекция: /// авторитетные IDs и факты отказа находятся в этом enum. #[derive(Debug, Clone, PartialEq, Eq)] -pub enum UpdateErrorV1 { +pub(crate) enum UpdateErrorV1 { /// Session создана другой точной owner-эпохой. OwnerMismatch, /// Наблюдение не содержит ни одного физического сценария. @@ -2941,7 +2951,7 @@ pub enum UpdateErrorV1 { impl UpdateErrorV1 { /// Возвращает стабильный класс ошибки без потери её payload. - pub const fn kind(&self) -> UpdateErrorKindV1 { + pub(crate) const fn kind(&self) -> UpdateErrorKindV1 { match self { Self::OwnerMismatch => UpdateErrorKindV1::OwnerMismatch, Self::EmptyScenarioSet diff --git a/scripts/test_program_public_surface.py b/scripts/test_program_public_surface.py new file mode 100755 index 00000000..026fd59f --- /dev/null +++ b/scripts/test_program_public_surface.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 +"""Mutation and fail-closed tests for the resolved rustdoc surface gate.""" + +from __future__ import annotations + +import tempfile +import unittest +from pathlib import Path + +from verify_program_public_surface import ( + RustdocShapeError, + program_public_surface, +) + + +class ProgramPublicSurfaceTests(unittest.TestCase): + def setUp(self) -> None: + self.temporary = tempfile.TemporaryDirectory() + self.addCleanup(self.temporary.cleanup) + self.docs = Path(self.temporary.name) / "doc" + self.crate = self.docs / "labcolors_core" + self.crate.mkdir(parents=True) + + def write_all(self, *hrefs: str) -> None: + links = "".join(f'
  • item
  • ' for href in hrefs) + (self.crate / "all.html").write_text( + f'
      {links}
    ', + encoding="utf-8", + ) + + def write_item(self, relative: str, body: str) -> None: + path = self.crate / relative + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(f"{body}", encoding="utf-8") + + def test_clean_resolved_source_is_accepted(self) -> None: + self.write_all("struct.Srgb8.html") + self.write_item( + "struct.Srgb8.html", + 'Source', + ) + count, leaks = program_public_surface(self.crate) + self.assertEqual(count, 1) + self.assertEqual(leaks, []) + + def test_arbitrary_root_reexport_alias_is_rejected_by_origin(self) -> None: + self.write_all("struct.AnyClientName.html") + self.write_item( + "struct.AnyClientName.html", + 'Source', + ) + count, leaks = program_public_surface(self.crate) + self.assertEqual(count, 1) + self.assertEqual([leak.public_item for leak in leaks], ["struct.AnyClientName.html"]) + + def test_nested_alias_is_rejected_without_a_name_allowlist(self) -> None: + self.write_all("facade/struct.UnrelatedName.html") + self.write_item( + "facade/struct.UnrelatedName.html", + 'Source', + ) + _, leaks = program_public_surface(self.crate) + self.assertEqual(len(leaks), 1) + + def test_public_type_alias_route_to_program_is_rejected(self) -> None: + self.write_all("type.Alias.html") + self.write_item( + "type.Alias.html", + ( + 'Source' + 'Draft' + ), + ) + _, leaks = program_public_surface(self.crate) + self.assertEqual(len(leaks), 1) + + def test_missing_item_page_fails_closed(self) -> None: + self.write_all("struct.Missing.html") + with self.assertRaises(RustdocShapeError): + program_public_surface(self.crate) + + def test_empty_public_inventory_fails_closed(self) -> None: + self.write_all() + with self.assertRaises(RustdocShapeError): + program_public_surface(self.crate) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/scripts/verify_program_public_surface.py b/scripts/verify_program_public_surface.py new file mode 100755 index 00000000..c891389d --- /dev/null +++ b/scripts/verify_program_public_surface.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python3 +"""Fail closed when staged Program code reaches the rendered public Rust API.""" + +from __future__ import annotations + +import argparse +import sys +from dataclasses import dataclass +from html.parser import HTMLParser +from pathlib import Path +from urllib.parse import unquote, urlsplit + + +class RustdocShapeError(RuntimeError): + """The rustdoc tree is incomplete or no longer has the verified shape.""" + + +@dataclass(frozen=True) +class ProgramLeak: + public_item: str + route: str + + +class _AllItemsParser(HTMLParser): + def __init__(self) -> None: + super().__init__(convert_charrefs=True) + self.depth = 0 + self.all_items_depth: int | None = None + self.hrefs: list[str] = [] + + def handle_starttag( + self, tag: str, attrs: list[tuple[str, str | None]] + ) -> None: + self.depth += 1 + values = dict(attrs) + classes = set((values.get("class") or "").split()) + if tag == "ul" and "all-items" in classes: + if self.all_items_depth is not None: + raise RustdocShapeError("nested rustdoc all-items lists are ambiguous") + self.all_items_depth = self.depth + elif self.all_items_depth is not None and tag == "a": + href = values.get("href") + if href: + self.hrefs.append(href) + + def handle_endtag(self, tag: str) -> None: + if tag == "ul" and self.all_items_depth == self.depth: + self.all_items_depth = None + self.depth -= 1 + if self.depth < 0: + raise RustdocShapeError("malformed rustdoc HTML nesting") + + +class _LinkParser(HTMLParser): + def __init__(self) -> None: + super().__init__(convert_charrefs=True) + self.links: list[tuple[frozenset[str], str]] = [] + + def handle_starttag( + self, tag: str, attrs: list[tuple[str, str | None]] + ) -> None: + if tag != "a": + return + values = dict(attrs) + href = values.get("href") + if href: + self.links.append( + (frozenset((values.get("class") or "").split()), href) + ) + + +def _read(path: Path) -> str: + try: + return path.read_text(encoding="utf-8") + except (OSError, UnicodeError) as error: + raise RustdocShapeError(f"cannot read {path}: {error}") from error + + +def _inside(path: Path, root: Path) -> bool: + try: + path.relative_to(root) + return True + except ValueError: + return False + + +def _resolve_local_link(page: Path, href: str, docs_root: Path) -> Path | None: + parsed = urlsplit(href) + if parsed.scheme or parsed.netloc or not parsed.path: + return None + decoded = unquote(parsed.path) + if decoded.startswith("/"): + raise RustdocShapeError(f"absolute local rustdoc link is unsupported: {href}") + resolved = (page.parent / decoded).resolve() + if not _inside(resolved, docs_root): + raise RustdocShapeError(f"rustdoc link escapes its output tree: {href}") + return resolved + + +def public_item_pages(crate_doc_root: Path) -> list[tuple[str, Path]]: + crate_doc_root = crate_doc_root.resolve() + docs_root = crate_doc_root.parent + all_items = crate_doc_root / "all.html" + parser = _AllItemsParser() + parser.feed(_read(all_items)) + parser.close() + if parser.all_items_depth is not None: + raise RustdocShapeError("rustdoc all-items list is not closed") + if not parser.hrefs: + raise RustdocShapeError("rustdoc all.html contains no public item links") + + pages: list[tuple[str, Path]] = [] + seen: set[Path] = set() + for href in parser.hrefs: + page = _resolve_local_link(all_items, href, docs_root) + if page is None or not _inside(page, crate_doc_root): + raise RustdocShapeError(f"public item is not a local crate page: {href}") + if page.suffix != ".html": + raise RustdocShapeError(f"public item does not resolve to HTML: {href}") + if page in seen: + raise RustdocShapeError(f"duplicate public rustdoc item page: {href}") + if not page.is_file(): + raise RustdocShapeError(f"public rustdoc item page is missing: {href}") + seen.add(page) + pages.append((href, page)) + return pages + + +def program_public_surface(crate_doc_root: Path) -> tuple[int, list[ProgramLeak]]: + crate_doc_root = crate_doc_root.resolve() + docs_root = crate_doc_root.parent + forbidden_source = (docs_root / "src/labcolors_core/program.rs.html").resolve() + forbidden_module = (crate_doc_root / "program").resolve() + pages = public_item_pages(crate_doc_root) + leaks: list[ProgramLeak] = [] + + for public_item, page in pages: + if _inside(page, forbidden_module): + leaks.append(ProgramLeak(public_item, str(page.relative_to(docs_root)))) + continue + + parser = _LinkParser() + parser.feed(_read(page)) + parser.close() + source_links = 0 + for classes, href in parser.links: + route = _resolve_local_link(page, href, docs_root) + if route is None: + continue + if "src" in classes: + source_links += 1 + if route == forbidden_source or _inside(route, forbidden_module): + leaks.append(ProgramLeak(public_item, href)) + break + if source_links == 0: + raise RustdocShapeError( + f"public rustdoc item has no compiler-emitted source link: {public_item}" + ) + + return len(pages), leaks + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser() + parser.add_argument( + "crate_doc_root", + nargs="?", + default="target/doc/labcolors_core", + type=Path, + ) + args = parser.parse_args(argv) + try: + item_count, leaks = program_public_surface(args.crate_doc_root) + except RustdocShapeError as error: + print(f"Program public rustdoc surface: FAIL: {error}", file=sys.stderr) + return 1 + if leaks: + print("Program public rustdoc surface: FAIL:", file=sys.stderr) + for leak in leaks: + print( + f" public item {leak.public_item} reaches staged program via {leak.route}", + file=sys.stderr, + ) + return 1 + print(f"Program public rustdoc surface: PASS; public_items={item_count}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From dcd49435df1a0a07e20ac345d6488081e08119d7 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Sun, 26 Jul 2026 22:21:13 +0300 Subject: [PATCH 58/58] ci: explain rustdoc surface mismatch --- scripts/test_program_public_surface.py | 12 ++++++++++++ scripts/verify_program_public_surface.py | 4 +++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/scripts/test_program_public_surface.py b/scripts/test_program_public_surface.py index 026fd59f..9bfe50c4 100755 --- a/scripts/test_program_public_surface.py +++ b/scripts/test_program_public_surface.py @@ -79,6 +79,18 @@ def test_missing_item_page_fails_closed(self) -> None: with self.assertRaises(RustdocShapeError): program_public_surface(self.crate) + def test_missing_source_class_names_the_rustdoc_toolchain_contract(self) -> None: + self.write_all("struct.Srgb8.html") + self.write_item( + "struct.Srgb8.html", + 'Source', + ) + with self.assertRaisesRegex( + RustdocShapeError, + 'expected rustdoc class "src".*markup or toolchain version is incompatible', + ): + program_public_surface(self.crate) + def test_empty_public_inventory_fails_closed(self) -> None: self.write_all() with self.assertRaises(RustdocShapeError): diff --git a/scripts/verify_program_public_surface.py b/scripts/verify_program_public_surface.py index c891389d..36279310 100755 --- a/scripts/verify_program_public_surface.py +++ b/scripts/verify_program_public_surface.py @@ -154,7 +154,9 @@ def program_public_surface(crate_doc_root: Path) -> tuple[int, list[ProgramLeak] break if source_links == 0: raise RustdocShapeError( - f"public rustdoc item has no compiler-emitted source link: {public_item}" + "public rustdoc item has no compiler-emitted source link with " + 'expected rustdoc class "src"; rustdoc HTML markup or toolchain ' + f"version is incompatible: {public_item}" ) return len(pages), leaks