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"),