diff --git a/crates/labcolors-core/contracts/clean-set-srgb8-v1/receipt-v1.json b/crates/labcolors-core/contracts/clean-set-srgb8-v1/receipt-v1.json index fe9aa3a5..141b78b2 100644 --- a/crates/labcolors-core/contracts/clean-set-srgb8-v1/receipt-v1.json +++ b/crates/labcolors-core/contracts/clean-set-srgb8-v1/receipt-v1.json @@ -55,11 +55,11 @@ "sha256": "b30300edd3910e3d9da1e56896ce14c3db508b1a6fc5608e01032a5ad95777b1" }, { - "bytes": 34218, + "bytes": 33725, "license": "MIT", "path": "crates/labcolors-core/src/observation.rs", "role": "observation_runtime_source", - "sha256": "8c9838107077775c51d80638ba0b59f9672d14347ca404dfd4c63e2fb62d1c45" + "sha256": "90e2ca62acac950fc38e711369a9fd3ef24262f7776f1e8ed2aeac92733a4911" }, { "bytes": 38023, diff --git a/crates/labcolors-core/contracts/clean-set-srgb8-v1/receipt-v1.sha256 b/crates/labcolors-core/contracts/clean-set-srgb8-v1/receipt-v1.sha256 index e7fd80b6..acbb19a3 100644 --- a/crates/labcolors-core/contracts/clean-set-srgb8-v1/receipt-v1.sha256 +++ b/crates/labcolors-core/contracts/clean-set-srgb8-v1/receipt-v1.sha256 @@ -1 +1 @@ -d561d07320fd62fc7287aff30c630f1ee2cd05745cf15c1421951137975e42c7 receipt-v1.json +0401de26bd038c26fc2948042b6e48f3feebd95b0377dfca32ab36983f80580a receipt-v1.json 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 c1b22c4e..8d83f5ea 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":"ebcc08779d73839d6a194ec35c772071ea999d68f64a2a31efe8d81811b82c98","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":"838315cee50b75ff823cdb76c2ed5a72667a4b6a688a11dfc29b9eb8a5bc459c","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":"019899f04cf91a29e4710609cd8d8c334784833a0972204cfedb179517a1e732"},{"kind":"rust-source","path":"crates/labcolors-core/src/composition.rs","sha256":"195a67327a3bd86d7816b634481389930bf68577bb1202fad14c2ea152df8625"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/exact.rs","sha256":"892576a8621185352583e63dc0a1aacac32e32a8063b6fe24ae16d4ff9dce7cb"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/mod.rs","sha256":"aba84c05a203af12ef2e445334409d9bd385a854c9058f0e30bbf8542addddbc"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/wcag22.rs","sha256":"856093c91159d8b3faab001f2d6524d33d7b16458a5a4e98ea65f8c62ab2694c"},{"kind":"rust-source","path":"crates/labcolors-core/src/hash.rs","sha256":"f97a0fd7d6ad3162f0f1dfb326fccfb7ed40da9a8fa67a5b8a239a1ae2ae49c3"},{"kind":"rust-source","path":"crates/labcolors-core/src/lcs_occurrence.rs","sha256":"78d37406e9bdc37f126b72987c9c92b452c13b3233c0aeb0a75ed25dadb83a68"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"b30300edd3910e3d9da1e56896ce14c3db508b1a6fc5608e01032a5ad95777b1"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"8c9838107077775c51d80638ba0b59f9672d14347ca404dfd4c63e2fb62d1c45"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"6f6a376ff036d3d65960c004e6566e1bca580f19f5bd3cd333a80b0da5b5c242"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"faf5bdffa7bc82a17d4ead99d34f1a22c24b0b511e253e8280ded86629c24e2e"},{"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":"eb6b342c6a60a6ef38680f7145928bef2ca80b1049438946f1c6936b885499b0"} +{"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":"39ccc41facf3f4218fc245d76167acfde420dcd54c07905387d1f748eea441e2","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":"bc676c172691493be0b2a2a0910505a9b6047ca02e3e49d39fb4ffa8dbd92e4f","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":"019899f04cf91a29e4710609cd8d8c334784833a0972204cfedb179517a1e732"},{"kind":"rust-source","path":"crates/labcolors-core/src/composition.rs","sha256":"195a67327a3bd86d7816b634481389930bf68577bb1202fad14c2ea152df8625"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/exact.rs","sha256":"892576a8621185352583e63dc0a1aacac32e32a8063b6fe24ae16d4ff9dce7cb"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/mod.rs","sha256":"aba84c05a203af12ef2e445334409d9bd385a854c9058f0e30bbf8542addddbc"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/wcag22.rs","sha256":"856093c91159d8b3faab001f2d6524d33d7b16458a5a4e98ea65f8c62ab2694c"},{"kind":"rust-source","path":"crates/labcolors-core/src/hash.rs","sha256":"f97a0fd7d6ad3162f0f1dfb326fccfb7ed40da9a8fa67a5b8a239a1ae2ae49c3"},{"kind":"rust-source","path":"crates/labcolors-core/src/lcs_occurrence.rs","sha256":"78d37406e9bdc37f126b72987c9c92b452c13b3233c0aeb0a75ed25dadb83a68"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"b30300edd3910e3d9da1e56896ce14c3db508b1a6fc5608e01032a5ad95777b1"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"90e2ca62acac950fc38e711369a9fd3ef24262f7776f1e8ed2aeac92733a4911"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"6f6a376ff036d3d65960c004e6566e1bca580f19f5bd3cd333a80b0da5b5c242"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"faf5bdffa7bc82a17d4ead99d34f1a22c24b0b511e253e8280ded86629c24e2e"},{"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":"fbd0929026e90f32da5f8b616a0e83ad902ad629302d3c20b48677286a80336e"} diff --git a/crates/labcolors-core/src/observation.rs b/crates/labcolors-core/src/observation.rs index 686d4c5f..a0525004 100644 --- a/crates/labcolors-core/src/observation.rs +++ b/crates/labcolors-core/src/observation.rs @@ -615,12 +615,13 @@ 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. +/// port bindings. One caller-owned index scratch is sorted first by opaque +/// scenario ID for canonical validation and then by physical tuple plus ID for +/// canonical materialization. Both passes are bounded by comparison sorting; +/// admission 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, @@ -638,17 +639,23 @@ pub(crate) fn prepare_schema_ordered_observation< 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) + .try_reserve_exact(scenario_count) .map_err(|_| ObservationError::ResourceExhausted)?; - order_scratch.resize(id_table_len, usize::MAX); + order_scratch.extend(0..scenario_count); + order_scratch.sort_unstable_by_key(|&scenario_index| source.scenario_id(scenario_index)); + + if let Some(duplicate) = order_scratch + .windows(2) + .find(|pair| source.scenario_id(pair[0]) == source.scenario_id(pair[1])) + { + return Err(ObservationError::DuplicateScenarioId { + scenario: source.scenario_id(duplicate[0]), + }); + } - for scenario_index in 0..scenario_count { + for &scenario_index in order_scratch.iter() { let actual = source.value_count(scenario_index); if actual != schema.as_slice().len() { return Err(ObservationError::SchemaOrderedValueCountMismatch { @@ -657,27 +664,8 @@ pub(crate) fn prepare_schema_ordered_observation< 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()) }); diff --git a/crates/labcolors-core/src/observation_tests.rs b/crates/labcolors-core/src/observation_tests.rs index 579af3a2..983da339 100644 --- a/crates/labcolors-core/src/observation_tests.rs +++ b/crates/labcolors-core/src/observation_tests.rs @@ -1,3 +1,9 @@ +use core::cell::Cell; +use std::collections::{BTreeMap, BTreeSet}; + +use proptest::prelude::*; +use proptest::test_runner::{Config, RngAlgorithm, TestRng, TestRunner}; + use crate::Srgb8; use crate::appearance::{ AppearanceBindings, AppearanceGraphSpec, BindingError, ColorInputId, CompositionProfileV1, @@ -9,8 +15,8 @@ use crate::observation::{ CanonicalObservationSchemaV1, ObservationError, ObservationHeadViewV1, ObservationOwnerV1, ObservationPayloadInput, ObservationStreamId, ObservationUpdateInput, ObservedScenarioSetInput, PreparedObservationUpdateV1, Revision, RevisionBoundObservationV1, RevisionBoundUnknownV1, - ScenarioId, ScenarioInput, SurfaceInputBinding, UnknownReasonId, - canonicalize_observation_schema, prepare_observation, + ScenarioId, ScenarioInput, SchemaOrderedScenarioSourceV1, SurfaceInputBinding, UnknownReasonId, + canonicalize_observation_schema, prepare_observation, prepare_schema_ordered_observation, }; const PORT_A: SurfaceInputPortId = SurfaceInputPortId::new(10); @@ -161,6 +167,112 @@ fn revision_bound(state: &TestState) -> &RevisionBoundObservationV1 { .expect("expected current admitted observation") } +#[derive(Debug, Clone, PartialEq, Eq)] +struct OrderedScenario { + id: ScenarioId, + values: Vec, +} + +#[derive(Debug)] +struct OrderedSource { + scenarios: Vec, + scenario_id_reads: Cell, + value_reads: Cell, +} + +impl OrderedSource { + fn new(scenarios: Vec) -> Self { + Self { + scenarios, + scenario_id_reads: Cell::new(0), + value_reads: Cell::new(0), + } + } + + fn scenario_id_reads(&self) -> usize { + self.scenario_id_reads.get() + } + + fn value_reads(&self) -> usize { + self.value_reads.get() + } +} + +impl SchemaOrderedScenarioSourceV1 for OrderedSource { + fn scenario_count(&self) -> usize { + self.scenarios.len() + } + + fn scenario_id(&self, scenario_index: usize) -> ScenarioId { + self.scenario_id_reads.set(self.scenario_id_reads.get() + 1); + self.scenarios[scenario_index].id + } + + fn value_count(&self, scenario_index: usize) -> usize { + self.scenarios[scenario_index].values.len() + } + + fn value(&self, scenario_index: usize, binding_index: usize) -> Srgb8 { + self.value_reads.set(self.value_reads.get() + 1); + self.scenarios[scenario_index].values[binding_index] + } +} + +struct UnrepresentableOrderedSource; + +impl SchemaOrderedScenarioSourceV1 for UnrepresentableOrderedSource { + fn scenario_count(&self) -> usize { + usize::MAX + } + + fn scenario_id(&self, _scenario_index: usize) -> ScenarioId { + panic!("scratch preflight must precede every source access") + } + + fn value_count(&self, _scenario_index: usize) -> usize { + panic!("scratch preflight must precede every source access") + } + + fn value(&self, _scenario_index: usize, _binding_index: usize) -> Srgb8 { + panic!("scratch preflight must precede every source access") + } +} + +fn ordered_scenario(id: u32, values: impl IntoIterator) -> OrderedScenario { + OrderedScenario { + id: ScenarioId::new(id), + values: values.into_iter().map(Srgb8::new).collect(), + } +} + +fn visit_permutations(values: &mut [T], start: usize, visit: &mut impl FnMut(&[T])) { + if start == values.len() { + visit(values); + return; + } + for index in start..values.len() { + values.swap(start, index); + visit_permutations(values, start + 1, visit); + values.swap(start, index); + } +} + +fn prepare_ordered<'owner>( + state: &'owner mut TestState, + revision: u64, + source: &OrderedSource, + order_scratch: &mut Vec, +) -> Result, ObservationError> { + prepare_schema_ordered_observation( + &mut state.owner, + state.stream, + &state.schema, + Revision::new(revision), + source, + order_scratch, + ) +} + #[test] fn authored_color_and_observed_surface_ports_are_distinct_through_execution() { let _sealed_adapter_contract: fn( @@ -765,3 +877,261 @@ fn invalid_schema_and_bindings_are_typed_and_deterministic() { assert_eq!(state.head(), ObservationHeadViewV1::Empty); } } + +#[test] +fn schema_ordered_duplicate_error_is_the_minimum_id_before_shape_and_revision() { + let mut scenarios = vec![ + ordered_scenario(7, [[7; 3]]), + ordered_scenario(3, [[3; 3]]), + ordered_scenario(1, []), + ordered_scenario(7, [[70; 3]]), + ordered_scenario(3, [[30; 3]]), + ]; + let mut state = TestState::new(STREAM, vec![PORT_A]).unwrap(); + state.apply(unknown_update(STREAM, 10, 1)).unwrap(); + + let mut checked = 0; + visit_permutations(&mut scenarios, 0, &mut |permutation| { + let source = OrderedSource::new(permutation.to_vec()); + let mut order_scratch = Vec::new(); + let error = prepare_ordered(&mut state, 1, &source, &mut order_scratch) + .err() + .expect("duplicate IDs must be rejected"); + assert_eq!( + error, + ObservationError::DuplicateScenarioId { + scenario: ScenarioId::new(3), + } + ); + checked += 1; + }); + assert_eq!(checked, 120); +} + +#[test] +fn schema_ordered_shape_error_is_the_minimum_id_before_revision() { + let mut scenarios = vec![ + ordered_scenario(9, [[9; 3]]), + ordered_scenario(5, []), + ordered_scenario(2, [[2; 3], [20; 3]]), + ]; + let mut state = TestState::new(STREAM, vec![PORT_A]).unwrap(); + state.apply(unknown_update(STREAM, 10, 1)).unwrap(); + + visit_permutations(&mut scenarios, 0, &mut |permutation| { + let source = OrderedSource::new(permutation.to_vec()); + let mut order_scratch = Vec::new(); + let error = prepare_ordered(&mut state, 1, &source, &mut order_scratch) + .err() + .expect("malformed scenario width must be rejected"); + assert_eq!( + error, + ObservationError::SchemaOrderedValueCountMismatch { + scenario: ScenarioId::new(2), + expected: 1, + actual: 2, + } + ); + }); +} + +#[test] +fn schema_ordered_scenario_id_reads_stay_inside_a_sorting_envelope() { + const SCENARIO_COUNT: usize = 1 << 10; + const COLLIDING_ID_STRIDE: u32 = 1 << 11; + const SORT_COUNT: usize = 2; + const IDS_PER_COMPARISON: usize = 2; + // Это mutation-budget, не коэффициент production API: запас остаётся + // существенно ниже квадратичного collision-корпуса и меняется только по + // измеренному дрейфу стратегии стандартной сортировки на поддерживаемом Rust. + const COMPARISON_ENVELOPE_PER_LEVEL: usize = 8; + + let source = OrderedSource::new( + (0..SCENARIO_COUNT) + .rev() + .map(|index| ordered_scenario(index as u32 * COLLIDING_ID_STRIDE, [[0x80; 3]])) + .collect(), + ); + let mut state = TestState::new(STREAM, vec![PORT_A]).unwrap(); + let mut order_scratch = Vec::new(); + let prepared = prepare_ordered(&mut state, 1, &source, &mut order_scratch) + .expect("unique opaque IDs must be admitted"); + drop(prepared); + + let logarithmic_levels = usize::BITS as usize - (SCENARIO_COUNT - 1).leading_zeros() as usize; + let read_budget = SORT_COUNT + * IDS_PER_COMPARISON + * COMPARISON_ENVELOPE_PER_LEVEL + * SCENARIO_COUNT + * logarithmic_levels; + assert!( + source.scenario_id_reads() <= read_budget, + "{} ScenarioId reads exceeded the O(n log n) admission envelope of {read_budget}", + source.scenario_id_reads(), + ); +} + +fn schema_ordered_value_read_count(scenario_count: usize, binding_count: usize) -> usize { + assert!(scenario_count > 0); + assert!(binding_count > 0); + let source = OrderedSource::new( + (0..scenario_count) + .rev() + .map(|index| { + let id = ((index * 5) % scenario_count) as u32; + let key = Srgb8::new([(index >> 16) as u8, (index >> 8) as u8, index as u8]); + let mut values = vec![Srgb8::new([0; 3]); binding_count]; + values[binding_count - 1] = key; + OrderedScenario { + id: ScenarioId::new(id), + values, + } + }) + .collect(), + ); + let schema = (0..binding_count) + .map(|index| SurfaceInputPortId::new(index as u32 + 1)) + .collect(); + let mut state = TestState::new(STREAM, schema).unwrap(); + let mut order_scratch = Vec::new(); + let prepared = prepare_ordered(&mut state, 1, &source, &mut order_scratch) + .expect("the bounded-work corpus must be valid"); + drop(prepared); + source.value_reads() +} + +#[test] +fn schema_ordered_value_reads_scale_linearly_with_width_and_n_log_n_with_count() { + const SMALL_COUNT: usize = 1 << 6; + const LARGE_COUNT: usize = 1 << 9; + const WIDE_SCHEMA: usize = 1 << 3; + // При восьмикратном росте n квадратичный mutant растёт в 64 раза, а n log n + // здесь — в 12; двукратный запас допускает toolchain-дрейф, но не n². + const N_LOG_N_HEADROOM: usize = 2; + + let small_narrow = schema_ordered_value_read_count(SMALL_COUNT, 1); + let small_wide = schema_ordered_value_read_count(SMALL_COUNT, WIDE_SCHEMA); + let large_narrow = schema_ordered_value_read_count(LARGE_COUNT, 1); + let large_wide = schema_ordered_value_read_count(LARGE_COUNT, WIDE_SCHEMA); + + assert_eq!(small_wide, small_narrow * WIDE_SCHEMA); + assert_eq!(large_wide, large_narrow * WIDE_SCHEMA); + + let small_levels = SMALL_COUNT.ilog2() as usize; + let large_levels = LARGE_COUNT.ilog2() as usize; + assert!( + large_narrow * SMALL_COUNT * small_levels + <= small_narrow * LARGE_COUNT * large_levels * N_LOG_N_HEADROOM, + "value reads grew faster than the n log n envelope: {small_narrow} -> {large_narrow}", + ); +} + +#[test] +fn schema_ordered_scratch_exhaustion_precedes_every_source_access() { + let mut state = TestState::new(STREAM, vec![PORT_A]).unwrap(); + let mut order_scratch = Vec::new(); + + assert!(matches!( + prepare_schema_ordered_observation( + &mut state.owner, + state.stream, + &state.schema, + Revision::new(1), + &UnrepresentableOrderedSource, + &mut order_scratch, + ), + Err(ObservationError::ResourceExhausted) + )); + assert_eq!(state.head(), ObservationHeadViewV1::Empty); +} + +fn schema_ordered_admission_matches_oracle( + raw: Vec<(u32, [u8; 6])>, + shift: usize, + reverse: bool, +) -> Result<(), TestCaseError> { + let mut by_id = BTreeMap::new(); + for (id, bytes) in raw { + let first_class = bytes[0] & 0b11; + let second_class = bytes[1] & 0b1; + by_id.insert( + ScenarioId::new(id), + [Srgb8::new([first_class; 3]), Srgb8::new([second_class; 3])], + ); + } + let mut scenarios = by_id + .into_iter() + .map(|(id, values)| OrderedScenario { + id, + values: values.to_vec(), + }) + .collect::>(); + let scenario_count = scenarios.len(); + scenarios.rotate_left(shift % scenario_count); + if reverse { + scenarios.reverse(); + } + + let mut oracle = BTreeMap::, BTreeSet>::new(); + for scenario in &scenarios { + oracle + .entry(scenario.values.clone()) + .or_default() + .insert(scenario.id); + } + let expected = oracle + .into_iter() + .map(|(values, provenance)| (values, provenance.into_iter().collect::>())) + .collect::>(); + + let source = OrderedSource::new(scenarios); + let mut state = TestState::new(STREAM, vec![PORT_A, PORT_B]).unwrap(); + let mut order_scratch = Vec::new(); + let prepared = prepare_ordered(&mut state, 1, &source, &mut order_scratch) + .map_err(|error| TestCaseError::fail(format!("valid oracle input failed: {error:?}")))?; + let PreparedObservationUpdateV1::Observed(prepared) = prepared else { + return Err(TestCaseError::fail( + "a fresh valid observation was not materialized", + )); + }; + let observation = prepared.observation(); + let actual = (0..observation.physical_case_count()) + .map(|case_index| { + let values = observation + .physical_values(case_index) + .expect("case index originates from the admitted observation") + .iter() + .map(|value| value.srgb8()) + .collect::>(); + let provenance = observation + .provenance(case_index) + .expect("case index originates from the admitted observation") + .to_vec(); + (values, provenance) + }) + .collect::>(); + + prop_assert_eq!(actual, expected); + Ok(()) +} + +#[test] +fn schema_ordered_admission_matches_an_independent_ordered_map_oracle() { + let config = Config { + cases: 256, + failure_persistence: None, + ..Config::default() + }; + let mut runner = + TestRunner::new_with_rng(config, TestRng::deterministic_rng(RngAlgorithm::ChaCha)); + runner + .run( + &( + prop::collection::vec((any::(), any::<[u8; 6]>()), 1..40), + any::(), + any::(), + ), + |(raw, shift, reverse)| schema_ordered_admission_matches_oracle(raw, shift, reverse), + ) + .expect("deterministic schema-ordered differential property failed"); +} diff --git a/crates/labcolors-core/src/program_mixed_evaluator_tests.rs b/crates/labcolors-core/src/program_mixed_evaluator_tests.rs index 434dd881..4a83419b 100644 --- a/crates/labcolors-core/src/program_mixed_evaluator_tests.rs +++ b/crates/labcolors-core/src/program_mixed_evaluator_tests.rs @@ -1494,6 +1494,59 @@ fn observation_projection_is_invariant_under_every_scenario_permutation_and_keep } } +#[test] +fn warmed_schema_ordered_exact_replay_reuses_session_scratch_without_allocation() { + let owner = OwnerV1::from_compiled(finite_program([[0x80; 3], [0; 3]])); + let mut session = owner.instantiate(STREAM.value()).unwrap(); + let white = [Srgb8::new([0xFF; 3])]; + let gray = [Srgb8::new([0x80; 3])]; + let first = [ + ScenarioV1::new(9, &white), + ScenarioV1::new(4, &gray), + ScenarioV1::new(3, &white), + ]; + let initial = owner + .prepare_update( + &mut session, + UpdateV1::Observed { + revision: 1, + scenarios: &first, + }, + ) + .unwrap() + .commit(); + let initial_backing = initial + .certificates() + .next() + .expect("the valid update must retain one certificate") + .observation_backing_ptr_for_test(); + + let replay = [ + ScenarioV1::new(3, &white), + ScenarioV1::new(9, &white), + ScenarioV1::new(4, &gray), + ]; + let (replay_backing, allocations) = crate::test_support::measured_allocations(|| { + owner + .prepare_update( + std::hint::black_box(&mut session), + UpdateV1::Observed { + revision: 1, + scenarios: std::hint::black_box(&replay), + }, + ) + .expect("a permuted exact replay must be idempotent") + .commit() + .certificates() + .next() + .expect("idempotence must preserve the certificate") + .observation_backing_ptr_for_test() + }); + + assert_eq!(allocations, 0); + assert_eq!(replay_backing, initial_backing); +} + #[test] fn concrete_program_retains_ready_and_fail_closed_stale_evidence() { let owner = OwnerV1::from_compiled(finite_program([[0x80; 3], [0; 3]])); diff --git a/scripts/verify_point_support_surplus.py b/scripts/verify_point_support_surplus.py index 85514bee..c8c82c43 100755 --- a/scripts/verify_point_support_surplus.py +++ b/scripts/verify_point_support_surplus.py @@ -58,7 +58,7 @@ SOURCE_BINDING_LAW = "point-support-rust-whole-file-semantic-cone-v2" SOURCE_BINDING_DOMAIN = b"labcolors.point-support.rust-whole-file-semantic-cone.v2" EXPECTED_SOURCE_CAPSULE_SHA256 = ( - "838315cee50b75ff823cdb76c2ed5a72667a4b6a688a11dfc29b9eb8a5bc459c" + "bc676c172691493be0b2a2a0910505a9b6047ca02e3e49d39fb4ffa8dbd92e4f" ) EXPECTED_Q55_PROOF_SHA256 = ( "ac59cf89503170c789223b91d775213a19d4e571ef930f2ea609fcd51b14defd"