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 b122af0f..36de05b4 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":"dad1bfaa7fd06945c66102ea388034f52f5edf4a99d02d6312b79ab2053d819c","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":"016cbe658a2bf0b289d1647cad0982b54fa4a9aec7b56934e2439adfb9eb8454","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":"91d56e7b86a6dfc11691b67da97852d7b26678a6cad8932abed3b622783e3dd9"},{"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":"6b0c493d78b4cb91f6d34ed94ee93aaaf94ad95818c66d2e8301ee2fe2e3f6f5"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"8c9838107077775c51d80638ba0b59f9672d14347ca404dfd4c63e2fb62d1c45"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"6f6a376ff036d3d65960c004e6566e1bca580f19f5bd3cd333a80b0da5b5c242"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"4f77643206077c080e5e9b182e896145bfb69bf3db8aa4c1ac7d4c5360ea8504"},{"kind":"rust-source","path":"crates/labcolors-core/src/srgb8.rs","sha256":"6c95324eb05476f35f75375a9af0b2b4a41b8b2978c46e67d2ce1aea5adde342"},{"kind":"rust-source","path":"crates/labcolors-core/src/wcag22.rs","sha256":"7ba7864eb7e73789bad6c63c64a4dc2dcc08c2da6921375fb9564fca230c2780"},{"kind":"rust-source","path":"crates/labcolors-core/src/wcag22/kernel.rs","sha256":"c97980c1ca2c7ea9cabff9c8d2fb7282773cca180ae15948391c29c9d6196040"},{"kind":"rust-source","path":"crates/labcolors-core/src/wcag22/q55_data.rs","sha256":"af4d23d6b70c45ce6efa839e7dda4bb0a61f6aae43cb805af6fa9b29e6c3bae2"},{"kind":"rust-source","path":"crates/labcolors-core/src/wcag22_evidence.rs","sha256":"3c5a75b07254c6071a64700af208a64987d0f0ea9698eadc54a9e74585ce1f72"}],"source_negative_controls":43,"universal_algebraic_certificate":{"basis_point_scale_instantiation":10000,"domain":"integers; Q55 scale Q>0; anchor L>=D>=0; lighter monotonicity L2>=L1>D>=0; darker monotonicity L>D2>=D1>=0; current/baseline denominators b,q>0; basis-point scale B>0 instantiated as 10000; p>0; a>=0; 0<=drop_bps<=B","identities":["three explicit anchor-surplus formulas after denominator clearing","reference distance is monotone increasing in lighter L","reference distance is monotone decreasing in darker D","positive-baseline retained threshold is p*(B-drop)/(q*B)","a/b >= p*(B-drop)/(q*B) iff a*q*B >= p*(B-drop)*b"],"method":"exact-sparse-integer-polynomial-identities-plus-positive-denominator-order-lemma-v1","nonpositive_baseline_case":"max(baseline,0)=0; retained threshold is exactly zero","symbolic_mutation_controls":{"anchor_coefficients_and_denominator":6,"retained_cross_product":5},"wolfram_language_cross_check":{"query":"FullSimplify[{20 g/d - 0 == 20 g/d, 20 g/d - 2 == (20 g - 2 d)/d, 20 g/d - 7/2 == (40 g - 7 d)/(2 d), Equivalent[a/b >= p (s-x)/(q s), a q s >= p (s-x) b], Max[p/q, 0] (s-x)/s == Piecewise[{{0, p <= 0}}, p (s-x)/(q s)]}, Assumptions -> Element[{a,b,p,q,s,x,g,d}, Integers] && a >= 0 && b > 0 && q > 0 && s > 0 && 0 <= x <= s && d > 0 && g >= 0]","query_sha256":"8cdbb9964583030c8b92498961896cb2a98613f1cb31eb7c54acdf8e16beff10","result":"{True, True, True, True, True}","result_sha256":"13a8f2ee8d0fde335a638e46d7cc8a8427b9a1437c77d22cfcf925bb87fa6303"}},"verifier_sha256":"156a7295c56113b1630e082fe561620726c97cec336b85f99169f8840155b899"} +{"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":"b8392d5a38f51a4b8d77d444d56cb601f047f467c9050541d49c7a0208dde765","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":"0d8d2f761fdc9c44812f18840c50af8a7e2b5d54084533b799312693a5441c61","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":"b226b475e604224ff92b5361b2c81416686000175e77ed73db522df18eba00a1"},{"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":"8a13da43fddf3cc05597c3347a4df7e4b9bd6fdea3d20267671a9c013f4e4cd3"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"8c9838107077775c51d80638ba0b59f9672d14347ca404dfd4c63e2fb62d1c45"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"6f6a376ff036d3d65960c004e6566e1bca580f19f5bd3cd333a80b0da5b5c242"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"4f77643206077c080e5e9b182e896145bfb69bf3db8aa4c1ac7d4c5360ea8504"},{"kind":"rust-source","path":"crates/labcolors-core/src/srgb8.rs","sha256":"6c95324eb05476f35f75375a9af0b2b4a41b8b2978c46e67d2ce1aea5adde342"},{"kind":"rust-source","path":"crates/labcolors-core/src/wcag22.rs","sha256":"7ba7864eb7e73789bad6c63c64a4dc2dcc08c2da6921375fb9564fca230c2780"},{"kind":"rust-source","path":"crates/labcolors-core/src/wcag22/kernel.rs","sha256":"c97980c1ca2c7ea9cabff9c8d2fb7282773cca180ae15948391c29c9d6196040"},{"kind":"rust-source","path":"crates/labcolors-core/src/wcag22/q55_data.rs","sha256":"af4d23d6b70c45ce6efa839e7dda4bb0a61f6aae43cb805af6fa9b29e6c3bae2"},{"kind":"rust-source","path":"crates/labcolors-core/src/wcag22_evidence.rs","sha256":"3c5a75b07254c6071a64700af208a64987d0f0ea9698eadc54a9e74585ce1f72"}],"source_negative_controls":43,"universal_algebraic_certificate":{"basis_point_scale_instantiation":10000,"domain":"integers; Q55 scale Q>0; anchor L>=D>=0; lighter monotonicity L2>=L1>D>=0; darker monotonicity L>D2>=D1>=0; current/baseline denominators b,q>0; basis-point scale B>0 instantiated as 10000; p>0; a>=0; 0<=drop_bps<=B","identities":["three explicit anchor-surplus formulas after denominator clearing","reference distance is monotone increasing in lighter L","reference distance is monotone decreasing in darker D","positive-baseline retained threshold is p*(B-drop)/(q*B)","a/b >= p*(B-drop)/(q*B) iff a*q*B >= p*(B-drop)*b"],"method":"exact-sparse-integer-polynomial-identities-plus-positive-denominator-order-lemma-v1","nonpositive_baseline_case":"max(baseline,0)=0; retained threshold is exactly zero","symbolic_mutation_controls":{"anchor_coefficients_and_denominator":6,"retained_cross_product":5},"wolfram_language_cross_check":{"query":"FullSimplify[{20 g/d - 0 == 20 g/d, 20 g/d - 2 == (20 g - 2 d)/d, 20 g/d - 7/2 == (40 g - 7 d)/(2 d), Equivalent[a/b >= p (s-x)/(q s), a q s >= p (s-x) b], Max[p/q, 0] (s-x)/s == Piecewise[{{0, p <= 0}}, p (s-x)/(q s)]}, Assumptions -> Element[{a,b,p,q,s,x,g,d}, Integers] && a >= 0 && b > 0 && q > 0 && s > 0 && 0 <= x <= s && d > 0 && g >= 0]","query_sha256":"8cdbb9964583030c8b92498961896cb2a98613f1cb31eb7c54acdf8e16beff10","result":"{True, True, True, True, True}","result_sha256":"13a8f2ee8d0fde335a638e46d7cc8a8427b9a1437c77d22cfcf925bb87fa6303"}},"verifier_sha256":"b8909246cf4665fa5535a68ed9f151da408d80212eb1fa350a5ddefe15567565"} diff --git a/crates/labcolors-core/src/appearance.rs b/crates/labcolors-core/src/appearance.rs index 7775a42e..3889ad3e 100644 --- a/crates/labcolors-core/src/appearance.rs +++ b/crates/labcolors-core/src/appearance.rs @@ -798,16 +798,17 @@ pub(crate) struct CompiledOccurrenceSlotV1 { id: OccurrenceId, } -/// Versioned counterfactual declared for one modeled point target. +/// Версионированное правило контрфактического отсутствия для одной +/// моделируемой целевой точки. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum PointOccurrenceAbsenceReleaseV1 { - /// Replace the target Occurrence output with its own resolved backdrop, - /// then replay every unchanged downstream occurrence and quantization. + /// Заменить результат целевого `Occurrence` его уже вычисленной подложкой, + /// затем повторить все неизменённые downstream-`Occurrence` и квантование. BypassOwnBackdropV1, } -/// Compiler-minted terminal root authority. A naked OccurrenceId cannot -/// authorize a final modeled-result claim. +/// Созданное компилятором полномочие терминального корня. Один `OccurrenceId` +/// не разрешает утверждать финальный моделируемый результат. #[derive(Debug, Clone)] pub(crate) struct CompiledPointPresentationRootV1 { graph_instance: Arc<()>, @@ -820,7 +821,8 @@ impl CompiledPointPresentationRootV1 { } } -/// Unique target-to-root ancestry proven on the compiler cold edge. +/// Однозначный путь предков от цели к корню, доказанный на холодной границе +/// компилятора. #[derive(Debug, Clone)] pub(crate) struct CompiledPointPresentationPathV1 { graph_instance: Arc<()>, @@ -858,6 +860,15 @@ pub(crate) enum PointPresentationPathErrorV1 { InternalInvariant, } +/// Типизированный отказ до начала точного контрфакта точки. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum PointOccurrenceAbsenceReplayErrorV1 { + /// В плоском буфере вызывающей стороны нет места для полного пересчёта. + InsufficientCapacity, + /// Результат вычисления и созданный компилятором путь принадлежат разным графам. + IncompatibleEvaluation, +} + /// Канонический compiled IR с индексными ссылками: после проверки bindings /// исполнение самих Paint/Surface/Occurrence узлов линейно по их числу. /// @@ -1297,6 +1308,21 @@ pub(crate) struct SourceOverCertificateV1 { } impl SourceOverCertificateV1 { + fn compose( + profile: CompositionProfileV1, + subject_rgb: [u8; 3], + subject_opacity: crate::composition::AdmittedOpacityV1, + backdrop_rgb: [u8; 3], + ) -> Self { + Self { + profile, + subject_rgb, + subject_opacity, + backdrop_rgb, + output_rgb: profile.composite(subject_rgb, subject_opacity, backdrop_rgb), + } + } + /// Replay the exact code-owned composition law certified by this value. #[cfg(test)] pub(crate) fn replay(&self) -> [u8; 3] { @@ -1316,6 +1342,10 @@ impl SourceOverCertificateV1 { self.subject_opacity.bits() } + const fn subject_opacity(&self) -> crate::composition::AdmittedOpacityV1 { + self.subject_opacity + } + pub(crate) const fn backdrop_rgb(&self) -> [u8; 3] { self.backdrop_rgb } @@ -1325,6 +1355,115 @@ impl SourceOverCertificateV1 { } } +/// Точный финальный домен точки одного смоделированного `Occurrence` после +/// полного пересчёта оставшегося пути представления к корню. +/// +/// `Empty` означает отсутствие вклада `Occurrence` в байты именно выбранного +/// корня этого пересчёта. Это ничего не утверждает о других терминальных корнях +/// и не является положительным свидетельством о восприятии или качестве. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ExactFinalOwnedPointDomainV1 { + Empty, + Singleton { visible: [u8; 3] }, +} + +/// Один точный шаг версионированной интервенции отсутствия. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum PointOccurrenceAbsenceStepV1 { + Removed { + occurrence: OccurrenceId, + normal: SourceOverCertificateV1, + }, + Propagated { + occurrence: OccurrenceId, + normal: SourceOverCertificateV1, + counterfactual: SourceOverCertificateV1, + }, +} + +impl PointOccurrenceAbsenceStepV1 { + pub(crate) const fn occurrence(self) -> OccurrenceId { + match self { + Self::Removed { occurrence, .. } | Self::Propagated { occurrence, .. } => occurrence, + } + } + + pub(crate) const fn normal(self) -> SourceOverCertificateV1 { + match self { + Self::Removed { normal, .. } | Self::Propagated { normal, .. } => normal, + } + } + + pub(crate) const fn counterfactual_output(self) -> [u8; 3] { + match self { + Self::Removed { normal, .. } => normal.backdrop_rgb(), + Self::Propagated { counterfactual, .. } => counterfactual.output_rgb(), + } + } +} + +/// Заимствованный результат одного точного контрфактического пересчёта. +/// +/// Результат связывает версию интервенции и профили композиции шагов, но сам по +/// себе не связывает ревизию, идентичность `Program` или сценарий и потому не +/// является причинным сертификатом. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct PointOccurrenceAbsenceReplayV1<'steps> { + release: PointOccurrenceAbsenceReleaseV1, + /// Срез непуст по построению: compiler-minted path всегда содержит хотя бы + /// корень, а другого конструктора результата в модуле нет. + steps: &'steps [PointOccurrenceAbsenceStepV1], +} + +impl PointOccurrenceAbsenceReplayV1<'_> { + fn first(&self) -> PointOccurrenceAbsenceStepV1 { + self.steps + .first() + .copied() + .unwrap_or_else(|| unreachable!("созданный компилятором пересчёт непуст")) + } + + fn last(&self) -> PointOccurrenceAbsenceStepV1 { + self.steps + .last() + .copied() + .unwrap_or_else(|| unreachable!("созданный компилятором пересчёт непуст")) + } + + pub(crate) fn target(&self) -> OccurrenceId { + self.first().occurrence() + } + + pub(crate) fn root(&self) -> OccurrenceId { + self.last().occurrence() + } + + pub(crate) const fn release(&self) -> PointOccurrenceAbsenceReleaseV1 { + self.release + } + + pub(crate) fn normal_root(&self) -> [u8; 3] { + self.last().normal().output_rgb() + } + + pub(crate) fn counterfactual_root(&self) -> [u8; 3] { + self.last().counterfactual_output() + } + + pub(crate) fn domain(&self) -> ExactFinalOwnedPointDomainV1 { + let normal = self.normal_root(); + if normal == self.counterfactual_root() { + ExactFinalOwnedPointDomainV1::Empty + } else { + ExactFinalOwnedPointDomainV1::Singleton { visible: normal } + } + } + + pub(crate) const fn steps(&self) -> &[PointOccurrenceAbsenceStepV1] { + self.steps + } +} + /// Разрешённое применение Paint к Surface. Сертификат структурно принадлежит /// occurrence; `surfaceFrom` второго сертификата не создаёт. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -1546,6 +1685,7 @@ fn initialise_workspace_slots( /// these values while a consumer is still reading them. #[derive(Debug)] pub(crate) struct AppearanceEvaluationView<'program, 'workspace> { + graph_instance: &'program Arc<()>, program: CompiledAppearanceProgram<'program>, workspace: &'workspace AppearanceWorkspace, } @@ -1613,6 +1753,67 @@ impl AppearanceEvaluationView<'_, '_> { }) } + /// Пересчитывает доказанную компилятором цепочку точки по версионированному + /// правилу отсутствия без аллокации и частичного результата. + /// + /// Новые шаги дописываются в конец `steps`, а результат заимствует только + /// добавленный диапазон. При переиспользовании scratch вызывающая сторона + /// очищает или укорачивает его; недостаточная свободная ёмкость отвергается + /// до композиции и до изменения буфера. + pub(crate) fn replay_point_occurrence_absence_into<'steps>( + &self, + path: &CompiledPointPresentationPathV1, + release: PointOccurrenceAbsenceReleaseV1, + steps: &'steps mut Vec, + ) -> Result, PointOccurrenceAbsenceReplayErrorV1> { + if !Arc::ptr_eq(self.graph_instance, &path.graph_instance) { + return Err(PointOccurrenceAbsenceReplayErrorV1::IncompatibleEvaluation); + } + // Новый вариант release обязан сломать компиляцию здесь, а не молча + // получить семантику единственного текущего правила. + let PointOccurrenceAbsenceReleaseV1::BypassOwnBackdropV1 = release; + if steps.capacity().saturating_sub(steps.len()) < path.occurrences.len() { + return Err(PointOccurrenceAbsenceReplayErrorV1::InsufficientCapacity); + } + let start = steps.len(); + let mut counterfactual_previous = None; + for (path_index, slot) in path.occurrences.iter().copied().enumerate() { + let occurrence = self.occurrence_at(slot).unwrap_or_else(|| { + unreachable!("путь того же графа содержит канонические позиции") + }); + let normal = *occurrence.certificate(); + let counterfactual_output = if path_index == 0 { + let absent_output = normal.backdrop_rgb(); + steps.push(PointOccurrenceAbsenceStepV1::Removed { + occurrence: slot.id, + normal, + }); + absent_output + } else { + let counterfactual_backdrop = counterfactual_previous + .unwrap_or_else(|| unreachable!("каждый пересчёт начинается с удалённой цели")); + let counterfactual = SourceOverCertificateV1::compose( + normal.profile(), + normal.subject_rgb(), + normal.subject_opacity(), + counterfactual_backdrop, + ); + steps.push(PointOccurrenceAbsenceStepV1::Propagated { + occurrence: slot.id, + normal, + counterfactual, + }); + counterfactual.output_rgb() + }; + counterfactual_previous = Some(counterfactual_output); + } + let end = steps.len(); + Ok(PointOccurrenceAbsenceReplayV1 { + release, + steps: &steps[start..end], + }) + } + fn try_to_owned(&self) -> Result { let mut paints = Vec::new(); paints @@ -1700,9 +1901,9 @@ impl CompiledAppearanceGraph { Some(CompiledOccurrenceSlotV1 { index, id }) } - /// Mint authority only for an occurrence that is not consumed by another - /// occurrence in this point graph. Intermediate layers therefore cannot - /// be mistaken for final modeled results. + /// Создаёт полномочие только для `Occurrence`, который не потребляется + /// другим `Occurrence` этого точечного графа: промежуточный слой нельзя + /// принять за финальный моделируемый результат. pub(crate) fn compile_point_presentation_root( &self, terminal: OccurrenceId, @@ -1728,7 +1929,8 @@ impl CompiledAppearanceGraph { }) } - /// Prove one target's membership in the root's unique backdrop ancestry. + /// Доказывает принадлежность цели однозначной цепочке предков подложки + /// выбранного корня. pub(crate) fn compile_point_presentation_path( &self, target: OccurrenceId, @@ -1746,10 +1948,10 @@ impl CompiledAppearanceGraph { .try_reserve_exact(self.occurrences.len()) .map_err(|_| PointPresentationPathErrorV1::ResourceExhausted)?; - // `compile()` rejects `RenderCycle` through canonical functional - // topology. Every hop therefore moves to a unique ancestor and the - // walk terminates within `occurrences.len()` nodes; that same finite - // upper bound makes the exact reservation safe. + // `compile()` отклоняет `RenderCycle` при построении канонической + // функциональной топологии. Каждый переход идёт к единственному предку, + // поэтому обход завершается не более чем за `occurrences.len()` узлов; + // та же граница позволяет безопасно зарезервировать точный объём. let mut current = root_slot.index; loop { let spec = self @@ -1816,7 +2018,8 @@ impl CompiledAppearanceGraph { bindings: &AdmittedAppearanceBindings, workspace: &'workspace mut AppearanceWorkspace, ) -> Result, BindingError> { - self.program().evaluate_admitted_into(bindings, workspace) + self.program() + .evaluate_admitted_into(&self.instance, bindings, workspace) } /// Cold convenience внутри Core для callers, которым нужен owned result. @@ -1939,6 +2142,7 @@ impl<'program> CompiledAppearanceProgram<'program> { fn evaluate_admitted_into<'workspace>( self, + graph_instance: &'program Arc<()>, bindings: &AdmittedAppearanceBindings, workspace: &'workspace mut AppearanceWorkspace, ) -> Result, BindingError> { @@ -1980,6 +2184,7 @@ impl<'program> CompiledAppearanceProgram<'program> { ); Ok(AppearanceEvaluationView { + graph_instance, program: self, workspace, }) @@ -2048,18 +2253,13 @@ impl<'program> CompiledAppearanceProgram<'program> { let subject = resolved_paints[spec.subject].unwrap_or_else(|| unreachable!()); let backdrop = resolved_surfaces[spec.against].unwrap_or_else(|| unreachable!()); - let visible = spec.profile.composite( + let certificate = SourceOverCertificateV1::compose( + spec.profile, subject.source.bytes(), subject.opacity, backdrop.bytes(), ); - let certificate = SourceOverCertificateV1 { - profile: spec.profile, - subject_rgb: subject.source.bytes(), - subject_opacity: subject.opacity, - backdrop_rgb: backdrop.bytes(), - output_rgb: visible, - }; + let visible = certificate.output_rgb(); resolved_occurrences[index] = Some(ResolvedOccurrence { id: spec.id, subject: spec.subject_id, diff --git a/crates/labcolors-core/src/appearance_replay_tests.rs b/crates/labcolors-core/src/appearance_replay_tests.rs new file mode 100644 index 00000000..688a6d98 --- /dev/null +++ b/crates/labcolors-core/src/appearance_replay_tests.rs @@ -0,0 +1,603 @@ +//! Тесты точного контрфактического пересчёта точки. +//! +//! Независимый эталон ниже исполняет объявленный порядок операций binary64 через +//! точные диадические целые и округляет только на двух границах binary64. Боевой +//! композитор он не вызывает. + +use proptest::prelude::*; + +use crate::Srgb8; +use crate::appearance::{ + AppearanceBindings, AppearanceGraphSpec, ColorInputId, CompositionProfileV1, + ExactFinalOwnedPointDomainV1, OccurrenceId, OccurrenceSpec, OpacityInputId, PaintId, PaintSpec, + PointOccurrenceAbsenceReleaseV1, PointOccurrenceAbsenceReplayErrorV1, + PointOccurrenceAbsenceStepV1, SurfaceId, SurfaceInputPortId, SurfaceSpec, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +struct ReplaySummary { + target: OccurrenceId, + root: OccurrenceId, + release: PointOccurrenceAbsenceReleaseV1, + normal_root: [u8; 3], + counterfactual_root: [u8; 3], + domain: ExactFinalOwnedPointDomainV1, + steps: Vec, +} + +fn color(index: usize) -> ColorInputId { + ColorInputId::new(u32::try_from(10 + index).unwrap()) +} + +fn opacity(index: usize) -> OpacityInputId { + OpacityInputId::new(u32::try_from(100 + index).unwrap()) +} + +fn solid(index: usize) -> PaintId { + PaintId::new(u32::try_from(200 + index * 2).unwrap()) +} + +fn translucent(index: usize) -> PaintId { + PaintId::new(u32::try_from(201 + index * 2).unwrap()) +} + +fn occurrence(index: usize) -> OccurrenceId { + OccurrenceId::new(u32::try_from(500 + index).unwrap()) +} + +fn derived_surface(index: usize) -> SurfaceId { + SurfaceId::new(u32::try_from(400 + index).unwrap()) +} + +fn chain( + layers: &[([u8; 3], f64)], + backdrop: [u8; 3], +) -> ( + crate::appearance::CompiledAppearanceGraph, + AppearanceBindings, +) { + assert!(!layers.is_empty()); + let input = SurfaceInputPortId::new(300); + let input_surface = SurfaceId::new(301); + let mut colors = Vec::new(); + let mut opacities = Vec::new(); + let mut paints = Vec::new(); + let mut surfaces = vec![SurfaceSpec::Input { + id: input_surface, + port: input, + }]; + let mut occurrences = Vec::new(); + let mut color_bindings = Vec::new(); + let mut opacity_bindings = Vec::new(); + + for (index, &(source, alpha)) in layers.iter().enumerate() { + colors.push(color(index)); + opacities.push(opacity(index)); + paints.push(PaintSpec::Solid { + id: solid(index), + color: color(index), + }); + paints.push(PaintSpec::Opacity { + id: translucent(index), + source: solid(index), + opacity: opacity(index), + }); + occurrences.push(OccurrenceSpec { + id: occurrence(index), + subject: translucent(index), + against: if index == 0 { + input_surface + } else { + derived_surface(index - 1) + }, + profile: CompositionProfileV1::EncodedSrgb8SourceOverV1, + }); + surfaces.push(SurfaceSpec::FromOccurrence { + id: derived_surface(index), + occurrence: occurrence(index), + }); + color_bindings.push((color(index), Srgb8::new(source))); + opacity_bindings.push((opacity(index), alpha)); + } + + // Порядок списков в декларации не является порядком исполнения. Разворот + // заставляет тесты пересчёта отвергать следование порядку объявления. + paints.reverse(); + surfaces.reverse(); + occurrences.reverse(); + let graph = AppearanceGraphSpec::new( + colors, + vec![input], + opacities, + paints, + surfaces, + occurrences, + ) + .compile() + .unwrap(); + let bindings = AppearanceBindings::new( + color_bindings, + vec![(input, Srgb8::new(backdrop))], + opacity_bindings, + ); + (graph, bindings) +} + +fn replay(layers: &[([u8; 3], f64)], backdrop: [u8; 3], target_index: usize) -> ReplaySummary { + let (graph, bindings) = chain(layers, backdrop); + let root_id = occurrence(layers.len() - 1); + let target_id = occurrence(target_index); + let root = graph.compile_point_presentation_root(root_id).unwrap(); + let path = graph + .compile_point_presentation_path(target_id, &root) + .unwrap(); + let admitted = graph.admit_bindings(&bindings).unwrap(); + let mut workspace = graph.new_workspace().unwrap(); + let evaluation = graph + .evaluate_admitted_into(&admitted, &mut workspace) + .unwrap(); + let mut steps = Vec::with_capacity(path.len()); + let result = evaluation + .replay_point_occurrence_absence_into( + &path, + PointOccurrenceAbsenceReleaseV1::BypassOwnBackdropV1, + &mut steps, + ) + .unwrap(); + ReplaySummary { + target: result.target(), + root: result.root(), + release: result.release(), + normal_root: result.normal_root(), + counterfactual_root: result.counterfactual_root(), + domain: result.domain(), + steps: result.steps().to_vec(), + } +} + +fn exact_source_over_oracle(tint: [u8; 3], alpha: f64, backdrop: [u8; 3]) -> [u8; 3] { + let (alpha_numerator, alpha_denominator_shift) = exact_binary64_dyadic(alpha); + std::array::from_fn(|channel| { + let backdrop = i128::from(backdrop[channel]); + let delta = i128::from(tint[channel]) - backdrop; + let rounded_product = + round_dyadic_to_binary64(delta * alpha_numerator, alpha_denominator_shift); + let (product_numerator, product_denominator_shift) = exact_binary64_dyadic(rounded_product); + let exact_sum_numerator = (backdrop << product_denominator_shift) + product_numerator; + let rounded_sum = round_dyadic_to_binary64(exact_sum_numerator, product_denominator_shift); + let (sum_numerator, sum_denominator_shift) = exact_binary64_dyadic(rounded_sum); + let denominator = 1_i128 << sum_denominator_shift; + u8::try_from((sum_numerator + denominator / 2) / denominator).unwrap() + }) +} + +/// Декодировать конечные binary64 ограниченного эталона в точные знаковые +/// диадические числа. Корпус свойств удерживает операнды в нормальном диапазоне. +fn exact_binary64_dyadic(value: f64) -> (i128, u32) { + assert!(value.is_finite()); + if value == 0.0 { + return (0, 0); + } + let bits = value.to_bits(); + let negative = bits >> 63 != 0; + let exponent = i32::try_from((bits >> 52) & 0x7ff).unwrap(); + assert_ne!( + exponent, 0, + "корпус эталона исключает субнормальные операнды" + ); + let mut mantissa = i128::from((bits & ((1_u64 << 52) - 1)) | (1_u64 << 52)); + if negative { + mantissa = -mantissa; + } + let binary_exponent = exponent - 1023 - 52; + if binary_exponent >= 0 { + (mantissa << u32::try_from(binary_exponent).unwrap(), 0) + } else { + let denominator_shift = u32::try_from(-binary_exponent).unwrap(); + // Потребитель сдвигает 8-битную подложку на тот же порядок. При 119 + // старший бит занимает позицию 126, а оставшийся запас больше 53-битной + // мантиссы слагаемого; 120 уже затронул бы знаковый бит i128. + assert!( + denominator_shift < 120, + "диадическое число эталона должно помещаться в i128" + ); + (mantissa, denominator_shift) + } +} + +/// Округлить ограниченное точное диадическое число до binary64 целочисленным +/// методом до ближайшего с выбором чётного при равенстве. +fn round_dyadic_to_binary64(numerator: i128, denominator_shift: u32) -> f64 { + if numerator == 0 { + return 0.0; + } + let negative = numerator < 0; + let magnitude = numerator.unsigned_abs(); + let bit_len = 128_u32 - magnitude.leading_zeros(); + let mut discarded = bit_len.saturating_sub(53); + let mut significand = magnitude >> discarded; + if discarded != 0 { + let remainder_mask = (1_u128 << discarded) - 1; + let remainder = magnitude & remainder_mask; + let half = 1_u128 << (discarded - 1); + if remainder > half || (remainder == half && significand & 1 == 1) { + significand += 1; + } + } + if significand == 1_u128 << 53 { + significand >>= 1; + discarded += 1; + } + let significand_bits = 128_u32 - significand.leading_zeros(); + let value_power = i32::try_from(discarded).unwrap() - i32::try_from(denominator_shift).unwrap(); + let unbiased_exponent = value_power + i32::try_from(significand_bits).unwrap() - 1; + assert!((-1022..=1023).contains(&unbiased_exponent)); + let normalized = significand << (53 - significand_bits); + let fraction = u64::try_from(normalized - (1_u128 << 52)).unwrap(); + let exponent = u64::try_from(unbiased_exponent + 1023).unwrap(); + let sign = u64::from(negative) << 63; + f64::from_bits(sign | (exponent << 52) | fraction) +} + +fn normal_oracle(layers: &[([u8; 3], f64)], backdrop: [u8; 3]) -> Vec<[u8; 3]> { + let mut outputs = Vec::with_capacity(layers.len()); + let mut current = backdrop; + for &(tint, alpha) in layers { + current = exact_source_over_oracle(tint, alpha, current); + outputs.push(current); + } + outputs +} + +fn absent_oracle(layers: &[([u8; 3], f64)], backdrop: [u8; 3], target_index: usize) -> [u8; 3] { + let normal = normal_oracle(layers, backdrop); + let mut current = if target_index == 0 { + backdrop + } else { + normal[target_index - 1] + }; + for &(tint, alpha) in &layers[target_index + 1..] { + current = exact_source_over_oracle(tint, alpha, current); + } + current +} + +#[test] +fn red_replay_removes_target_then_replays_the_same_downstream_order() { + let layers = [ + ([230, 20, 90], 0.37), + ([10, 210, 40], 0.61), + ([40, 70, 250], 0.43), + ]; + let backdrop = [248, 241, 229]; + let result = replay(&layers, backdrop, 0); + let normal = normal_oracle(&layers, backdrop); + let absent = absent_oracle(&layers, backdrop, 0); + + assert_eq!(result.target, occurrence(0)); + assert_eq!(result.root, occurrence(2)); + assert_eq!( + result.release, + PointOccurrenceAbsenceReleaseV1::BypassOwnBackdropV1 + ); + assert_eq!(result.normal_root, normal[2]); + assert_eq!(result.counterfactual_root, absent); + assert_eq!(result.steps.len(), 3); + assert_eq!(result.steps[0].occurrence(), occurrence(0)); + assert_eq!(result.steps[0].counterfactual_output(), backdrop); + for (index, expected_normal) in normal.iter().copied().enumerate().skip(1) { + assert_eq!(result.steps[index].occurrence(), occurrence(index)); + assert_eq!(result.steps[index].normal().output_rgb(), expected_normal); + } + assert_ne!( + absent, + exact_source_over_oracle( + layers[1].0, + layers[1].1, + exact_source_over_oracle(layers[2].0, layers[2].1, backdrop) + ), + "пример обязан обнаруживать обратный порядок оставшегося пересчёта" + ); +} + +#[test] +fn exact_owned_domain_is_empty_when_downstream_quantization_erases_the_target() { + let layers = [([0; 3], 0.01), ([0; 3], 0.95)]; + let result = replay(&layers, [255; 3], 0); + let normal = normal_oracle(&layers, [255; 3]); + assert_ne!( + normal[0], [255; 3], + "цель обязана изменить собственное наложение" + ); + assert_eq!(normal[1], absent_oracle(&layers, [255; 3], 0)); + assert_eq!(result.domain, ExactFinalOwnedPointDomainV1::Empty); +} + +#[test] +fn exact_owned_domain_is_singleton_only_for_a_final_byte_contribution() { + let layers = [([0, 80, 240], 0.5)]; + let result = replay(&layers, [255, 240, 220], 0); + assert_ne!(result.normal_root, result.counterfactual_root); + assert_eq!( + result.domain, + ExactFinalOwnedPointDomainV1::Singleton { + visible: result.normal_root, + } + ); +} + +#[test] +fn alpha_endpoints_and_a_later_opaque_layer_obey_the_same_domain_law() { + let backdrop = [31, 47, 89]; + let absent = replay(&[([240, 30, 10], 0.0)], backdrop, 0); + assert_eq!(absent.normal_root, backdrop); + assert_eq!(absent.domain, ExactFinalOwnedPointDomainV1::Empty); + + let present = replay(&[([240, 30, 10], 1.0)], backdrop, 0); + assert_eq!(present.normal_root, [240, 30, 10]); + assert_eq!( + present.domain, + ExactFinalOwnedPointDomainV1::Singleton { + visible: [240, 30, 10], + } + ); + + let erased = replay(&[([240, 30, 10], 1.0), ([5, 200, 90], 1.0)], backdrop, 0); + assert_eq!(erased.normal_root, [5, 200, 90]); + assert_eq!(erased.counterfactual_root, [5, 200, 90]); + assert_eq!(erased.domain, ExactFinalOwnedPointDomainV1::Empty); +} + +#[test] +fn final_domain_is_scoped_to_one_selected_root_in_a_fanout_graph() { + let input = SurfaceInputPortId::new(300); + let input_surface = SurfaceId::new(301); + let shared_surface = derived_surface(0); + let mut paints = Vec::new(); + for index in 0..3 { + paints.push(PaintSpec::Solid { + id: solid(index), + color: color(index), + }); + paints.push(PaintSpec::Opacity { + id: translucent(index), + source: solid(index), + opacity: opacity(index), + }); + } + paints.reverse(); + let graph = AppearanceGraphSpec::new( + (0..3).map(color).collect(), + vec![input], + (0..3).map(opacity).collect(), + paints, + vec![ + SurfaceSpec::FromOccurrence { + id: shared_surface, + occurrence: occurrence(0), + }, + SurfaceSpec::Input { + id: input_surface, + port: input, + }, + ], + vec![ + OccurrenceSpec { + id: occurrence(2), + subject: translucent(2), + against: shared_surface, + profile: CompositionProfileV1::EncodedSrgb8SourceOverV1, + }, + OccurrenceSpec { + id: occurrence(0), + subject: translucent(0), + against: input_surface, + profile: CompositionProfileV1::EncodedSrgb8SourceOverV1, + }, + OccurrenceSpec { + id: occurrence(1), + subject: translucent(1), + against: shared_surface, + profile: CompositionProfileV1::EncodedSrgb8SourceOverV1, + }, + ], + ) + .compile() + .unwrap(); + let bindings = AppearanceBindings::new( + vec![ + (color(0), Srgb8::new([240, 30, 10])), + (color(1), Srgb8::new([5, 200, 90])), + (color(2), Srgb8::new([220, 180, 70])), + ], + vec![(input, Srgb8::new([31, 47, 89]))], + vec![(opacity(0), 0.5), (opacity(1), 1.0), (opacity(2), 0.25)], + ); + let admitted = graph.admit_bindings(&bindings).unwrap(); + let mut workspace = graph.new_workspace().unwrap(); + let evaluation = graph + .evaluate_admitted_into(&admitted, &mut workspace) + .unwrap(); + + let opaque_root = graph + .compile_point_presentation_root(occurrence(1)) + .unwrap(); + let opaque_path = graph + .compile_point_presentation_path(occurrence(0), &opaque_root) + .unwrap(); + let mut opaque_steps = Vec::with_capacity(opaque_path.len()); + let opaque = evaluation + .replay_point_occurrence_absence_into( + &opaque_path, + PointOccurrenceAbsenceReleaseV1::BypassOwnBackdropV1, + &mut opaque_steps, + ) + .unwrap(); + assert_eq!(opaque.root(), occurrence(1)); + assert_eq!(opaque.domain(), ExactFinalOwnedPointDomainV1::Empty); + assert_eq!( + opaque + .steps() + .iter() + .map(|step| step.occurrence()) + .collect::>(), + vec![occurrence(0), occurrence(1)] + ); + + let translucent_root = graph + .compile_point_presentation_root(occurrence(2)) + .unwrap(); + let translucent_path = graph + .compile_point_presentation_path(occurrence(0), &translucent_root) + .unwrap(); + let mut translucent_steps = Vec::with_capacity(translucent_path.len()); + let translucent = evaluation + .replay_point_occurrence_absence_into( + &translucent_path, + PointOccurrenceAbsenceReleaseV1::BypassOwnBackdropV1, + &mut translucent_steps, + ) + .unwrap(); + assert_eq!(translucent.root(), occurrence(2)); + assert_ne!(translucent.normal_root(), translucent.counterfactual_root()); + assert_eq!( + translucent.domain(), + ExactFinalOwnedPointDomainV1::Singleton { + visible: translucent.normal_root(), + } + ); + assert_eq!( + translucent + .steps() + .iter() + .map(|step| step.occurrence()) + .collect::>(), + vec![occurrence(0), occurrence(2)] + ); +} + +#[test] +fn replay_rejects_foreign_evaluation_before_touching_the_replay_buffer() { + let layers = [([10, 20, 30], 0.5), ([220, 180, 70], 0.25)]; + let (origin, _) = chain(&layers, [255; 3]); + let root = origin + .compile_point_presentation_root(occurrence(1)) + .unwrap(); + let path = origin + .compile_point_presentation_path(occurrence(0), &root) + .unwrap(); + let (foreign, bindings) = chain(&layers, [255; 3]); + let admitted = foreign.admit_bindings(&bindings).unwrap(); + let mut workspace = foreign.new_workspace().unwrap(); + let evaluation = foreign + .evaluate_admitted_into(&admitted, &mut workspace) + .unwrap(); + let mut steps = Vec::with_capacity(path.len()); + + crate::composition::reset_source_over_evaluation_count(); + assert_eq!( + evaluation.replay_point_occurrence_absence_into( + &path, + PointOccurrenceAbsenceReleaseV1::BypassOwnBackdropV1, + &mut steps, + ), + Err(PointOccurrenceAbsenceReplayErrorV1::IncompatibleEvaluation) + ); + assert!(steps.is_empty()); + assert_eq!(crate::composition::source_over_evaluation_count(), 0); +} + +#[test] +fn replay_preflights_capacity_and_returns_only_its_appended_range() { + let layers = [([10, 20, 30], 0.5), ([220, 180, 70], 0.25)]; + let (graph, bindings) = chain(&layers, [255; 3]); + let root = graph + .compile_point_presentation_root(occurrence(1)) + .unwrap(); + let path = graph + .compile_point_presentation_path(occurrence(0), &root) + .unwrap(); + let admitted = graph.admit_bindings(&bindings).unwrap(); + let mut workspace = graph.new_workspace().unwrap(); + let evaluation = graph + .evaluate_admitted_into(&admitted, &mut workspace) + .unwrap(); + let sentinel = *evaluation.occurrence(occurrence(0)).unwrap().certificate(); + let mut short = Vec::with_capacity(path.len()); + short.push(PointOccurrenceAbsenceStepV1::Removed { + occurrence: OccurrenceId::new(999), + normal: sentinel, + }); + + crate::composition::reset_source_over_evaluation_count(); + assert_eq!( + evaluation.replay_point_occurrence_absence_into( + &path, + PointOccurrenceAbsenceReleaseV1::BypassOwnBackdropV1, + &mut short, + ), + Err(PointOccurrenceAbsenceReplayErrorV1::InsufficientCapacity) + ); + assert_eq!(short.len(), 1); + assert_eq!(crate::composition::source_over_evaluation_count(), 0); + + let mut exact = Vec::with_capacity(path.len() + 1); + exact.push(PointOccurrenceAbsenceStepV1::Removed { + occurrence: OccurrenceId::new(999), + normal: sentinel, + }); + let storage = (exact.as_ptr(), exact.capacity()); + let (summary, allocations) = crate::test_support::measured_allocations(|| { + let result = evaluation + .replay_point_occurrence_absence_into( + &path, + PointOccurrenceAbsenceReleaseV1::BypassOwnBackdropV1, + &mut exact, + ) + .unwrap(); + ( + result.steps().len(), + result.steps()[0].occurrence(), + result.steps()[1].occurrence(), + ) + }); + assert_eq!(summary, (path.len(), occurrence(0), occurrence(1))); + assert_eq!(allocations, 0); + assert_eq!( + crate::composition::source_over_evaluation_count(), + path.len() - 1 + ); + assert_eq!((exact.as_ptr(), exact.capacity()), storage); +} + +proptest! { + // Зафиксированный O1a-B CI-ратчет: 2^11 случаев не даёт изменению default + // `proptest` молча ослабить корпус; другой бюджет требует явного решения. + #![proptest_config(ProptestConfig::with_cases(2_048))] + + #[test] + fn replay_matches_an_independent_encoded_srgb8_oracle( + sources in prop::array::uniform4(any::<[u8; 3]>()), + backdrop in any::<[u8; 3]>(), + alpha_numerators in prop::array::uniform4(0_u16..=u16::MAX), + target_index in 0_usize..4, + ) { + let alphas = alpha_numerators.map(|value| f64::from(value) / f64::from(u16::MAX)); + let layers = std::array::from_fn::<_, 4, _>(|index| (sources[index], alphas[index])); + let expected_normal = normal_oracle(&layers, backdrop); + let expected_absent = absent_oracle(&layers, backdrop, target_index); + let result = replay(&layers, backdrop, target_index); + + prop_assert_eq!(result.normal_root, expected_normal[3]); + prop_assert_eq!(result.counterfactual_root, expected_absent); + prop_assert_eq!(result.steps.len(), 4 - target_index); + let expected_domain = if expected_normal[3] == expected_absent { + ExactFinalOwnedPointDomainV1::Empty + } else { + ExactFinalOwnedPointDomainV1::Singleton { + visible: expected_normal[3], + } + }; + prop_assert_eq!(result.domain, expected_domain); + } +} diff --git a/crates/labcolors-core/src/lib.rs b/crates/labcolors-core/src/lib.rs index fd24de86..b43c5700 100644 --- a/crates/labcolors-core/src/lib.rs +++ b/crates/labcolors-core/src/lib.rs @@ -89,6 +89,9 @@ mod agnostic_gates; #[cfg(test)] mod appearance_graph_tests; +#[cfg(test)] +mod appearance_replay_tests; + #[cfg(test)] mod lcs_occurrence_tests; diff --git a/scripts/verify_point_support_surplus.py b/scripts/verify_point_support_surplus.py index 5ae0bc33..1400516c 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 = ( - "016cbe658a2bf0b289d1647cad0982b54fa4a9aec7b56934e2439adfb9eb8454" + "0d8d2f761fdc9c44812f18840c50af8a7e2b5d54084533b799312693a5441c61" ) EXPECTED_Q55_PROOF_SHA256 = ( "ac59cf89503170c789223b91d775213a19d4e571ef930f2ea609fcd51b14defd"