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 91783e52..0a9cf24e 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 @@ -6,11 +6,11 @@ }, "artifacts": [ { - "bytes": 87048, + "bytes": 87722, "license": "MIT", "path": "crates/labcolors-core/src/appearance.rs", "role": "appearance_executor_source", - "sha256": "d31c6a7d3b0a4c02532759befaef442e3a99687263a9027771365638b6cc3324" + "sha256": "66266904fc84afb03a3a774a8aaa0da23fe1e05a8b04abb14d5f18820e17a85d" }, { "bytes": 6373, @@ -48,18 +48,18 @@ "sha256": "8bf5d5a1c0f00ce245a1ecb18b923aa1631483345962d72b633e466c242a8a1d" }, { - "bytes": 13680, + "bytes": 13746, "license": "MIT", "path": "crates/labcolors-core/src/lib.rs", "role": "module_registration_source", - "sha256": "db3be029d1ee35471f8c311ae4ec59babd06fcca26f8a1dcd381ef28bc937690" + "sha256": "84d7ce86895355a572254846d38c3766d9007bf21f0ed81e3334196cbc964165" }, { - "bytes": 38255, + "bytes": 39439, "license": "MIT", "path": "crates/labcolors-core/src/observation.rs", "role": "observation_runtime_source", - "sha256": "d31f5d5f95fbb2300c90899a26905c3cd2804be7c0e9290999005fd9e2c8f6d6" + "sha256": "b0cfec5c9fe798abd5492260aac3caf87685f7e27f4f9628b01f386ef3f6ac7d" }, { "bytes": 7951, @@ -83,39 +83,39 @@ "sha256": "0c8a7bb59464e1ddcace07b01ba24a64f8c9d3aeff547651dc38c276a0fc3322" }, { - "bytes": 70121, + "bytes": 70163, "license": "MIT", "path": "crates/labcolors-core/src/program/attachment/tests.rs", "role": "point_attachment_tests", - "sha256": "2c0876c61196f6492d0f250789ded9644bad1de9ff92c9dea2a14fc55e87f18e" + "sha256": "ac8c390a74c8aaea9e04d1006f3604d72f94a980ce26645d08593f025c4fc011" }, { - "bytes": 164302, + "bytes": 186280, "license": "MIT", "path": "crates/labcolors-core/src/program.rs", "role": "program_facade_source", - "sha256": "a2e3f0cf8fd5445a474c804bbd962d244d9eb42ce2c7ca099ac8c0793de6563e" + "sha256": "706bb420b7359bc02bce92763fb3c5acd01ae4428e74d10781ce9f019c020948" }, { - "bytes": 71867, + "bytes": 79583, "license": "MIT", "path": "crates/labcolors-core/src/program_identity.rs", "role": "program_identity_source", - "sha256": "b59f1f5fe0c71637471498635240de6a381c6b8f11308424e81c0f3e0c6b12d0" + "sha256": "0cef3e938c5511fdae96a022cd8c3771cd8e2cc00aab64f0cc7c7f3788985251" }, { - "bytes": 174358, + "bytes": 215114, "license": "MIT", "path": "crates/labcolors-core/src/program_session.rs", "role": "program_source", - "sha256": "13306b6a817c5509eba31550a1358c5a1d6deaae9d4344ff915d98738f8fd12f" + "sha256": "c5438be8ce85b40e77b9bde66c8a05277ed7d2244c34c34031d4e59599e64c9b" }, { "bytes": 21872, "license": "MIT", "path": "crates/labcolors-core/src/program_clean_set_tests.rs", "role": "program_tests", - "sha256": "16fb30ac575eb3ae1caba9b482673c3ad83e87211a7b50401bc2e8f27880e519" + "sha256": "3ab731c8ba05a673d35427867f18c906b458a93613bffdb41ce98802212cbd4c" }, { "bytes": 11370, diff --git a/crates/labcolors-core/contracts/clean-set-srgb8-v1/receipt-v1.sha256 b/crates/labcolors-core/contracts/clean-set-srgb8-v1/receipt-v1.sha256 index 36d81e31..61c60ea2 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 @@ -269e85bd887ddc6a09e86d183338ba5b26a9dc506762429eee7a65f981c6d31f receipt-v1.json +32aaea5546d65865902d21f02c216119066e736e79e92dc273917a667bd4cb9b 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 472d8a66..cc27e578 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":"bcc505425f92927a4587fcd70525cf6cb6d86537e321de87c41e3735dcbd27c1","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":"ad5f8cf281a796533e4784c827193fd5aa723a1161facdbfa6c48308099b4ba0","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":"d31c6a7d3b0a4c02532759befaef442e3a99687263a9027771365638b6cc3324"},{"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":"db3be029d1ee35471f8c311ae4ec59babd06fcca26f8a1dcd381ef28bc937690"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"d31f5d5f95fbb2300c90899a26905c3cd2804be7c0e9290999005fd9e2c8f6d6"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"6f6a376ff036d3d65960c004e6566e1bca580f19f5bd3cd333a80b0da5b5c242"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"383b163f9e715e3f5b313a3c29f962968dec3df2414cc8172e2def6b43236a83"},{"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":"3c6fca559708dfdd82ed102b376aa0dcc57cbef6e39f6e21b8e4da81b05ffcb6"} +{"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":"18730f783ac5837e4794f85b86c98242a532a6020182de7c93a6f94cb087eea0","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":"f9a56da33469e5765c62fae6ab7fcb1f870a550cd9cd29dc160a48f65091bcbd","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":"66266904fc84afb03a3a774a8aaa0da23fe1e05a8b04abb14d5f18820e17a85d"},{"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":"344999d12f68d7de1cc145180b2d12cf7e436b2b1eebc257327dbbbfd3d57c07"},{"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":"84d7ce86895355a572254846d38c3766d9007bf21f0ed81e3334196cbc964165"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"b0cfec5c9fe798abd5492260aac3caf87685f7e27f4f9628b01f386ef3f6ac7d"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"6f6a376ff036d3d65960c004e6566e1bca580f19f5bd3cd333a80b0da5b5c242"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"383b163f9e715e3f5b313a3c29f962968dec3df2414cc8172e2def6b43236a83"},{"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":"f61a8a0b6ae30a626ee659a4a5ec666491c1786a3dad2eb0245d430e878957e9"} diff --git a/crates/labcolors-core/src/appearance.rs b/crates/labcolors-core/src/appearance.rs index 0f195ca7..2f8aab06 100644 --- a/crates/labcolors-core/src/appearance.rs +++ b/crates/labcolors-core/src/appearance.rs @@ -1215,6 +1215,19 @@ impl AdmittedAppearanceBindings { Ok(()) } + /// Читает одно заранее связанное значение Target без поиска и аллокации. + /// + /// Slot минтит тот же compiled graph, которому принадлежат bindings. + /// Проверка ordinal и номинального ID не даёт evidence прочитать значение + /// через slot чужого поколения. + pub(crate) fn paint_input_at( + &self, + slot: CompiledPaintInputSlotV1, + ) -> Option { + let (bound, value) = self.paint_inputs.get(slot.index)?; + (*bound == slot.id).then_some(*value) + } + /// Borrow the admitted Surface-input slice in its exact canonical order. pub(crate) fn surface_inputs_canonical( &self, diff --git a/crates/labcolors-core/src/constraints/mod.rs b/crates/labcolors-core/src/constraints/mod.rs index 6853ed78..73f541a9 100644 --- a/crates/labcolors-core/src/constraints/mod.rs +++ b/crates/labcolors-core/src/constraints/mod.rs @@ -23,6 +23,15 @@ pub(crate) use exact::{ #[cfg(test)] pub(crate) use exact::ExactIdentityPassV1; +mod relation; +pub(crate) use relation::{ + CoreIntrinsicUnaryInvocationV1, CoreIntrinsicUnaryMeasurementV1, CoreIntrinsicUnaryPassV1, + CoreIntrinsicUnaryViolationV1, CoreRelationInvocationV1, CoreRelationMeasurementV1, + CoreRelationPassV1, CoreRelationViolationV1, ExactSrgb8IntrinsicUnaryCapabilityV1, + ExactSrgb8IntrinsicUnaryIdentityV1, ExactSrgb8IntrinsicUnaryReleaseV1, + ExactSrgb8RelationCapabilityV1, ExactSrgb8RelationIdentityV1, ExactSrgb8RelationReleaseV1, +}; + mod wcag22; pub(crate) use wcag22::{ @@ -496,6 +505,17 @@ pub(crate) enum ProgramConstraintContentV1 { capability: Wcag22Srgb8CapabilityV1, criterion: Wcag22CriterionV1, }, + ExactSrgb8IntrinsicUnary { + identity: ExactSrgb8IntrinsicUnaryIdentityV1, + release: ExactSrgb8IntrinsicUnaryReleaseV1, + capability: ExactSrgb8IntrinsicUnaryCapabilityV1, + expected: Srgb8, + }, + ExactSrgb8Relation { + identity: ExactSrgb8RelationIdentityV1, + release: ExactSrgb8RelationReleaseV1, + capability: ExactSrgb8RelationCapabilityV1, + }, #[cfg(test)] ModeledLcsProbe { release: ProgramLcsDependencyReleaseV1, diff --git a/crates/labcolors-core/src/constraints/relation.rs b/crates/labcolors-core/src/constraints/relation.rs new file mode 100644 index 00000000..8fd6f131 --- /dev/null +++ b/crates/labcolors-core/src/constraints/relation.rs @@ -0,0 +1,292 @@ +use crate::Srgb8; +use crate::constraints::{HardDecision, ProgramConstraintContentV1}; + +fn exact_srgb8_equal(left: Srgb8, right: Srgb8) -> bool { + left.bytes() == right.bytes() +} + +/// Принадлежащая Core identity точного байтового равенства однородных endpoints. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ExactSrgb8RelationIdentityV1 { + HomogeneousEndpointEqualityV1, +} + +/// Версия точного закона для пары endpoints. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ExactSrgb8RelationReleaseV1 { + V1, +} + +/// Capability намеренно уже перцептивной либо renderer identity. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ExactSrgb8RelationCapabilityV1 { + HomogeneousEncodedSrgb8PairV1, +} + +/// Сырая пара хранится независимо от жёсткой классификации. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct ExactSrgb8RelationMeasurementV1 { + reference: Srgb8, + candidate: Srgb8, +} + +impl ExactSrgb8RelationMeasurementV1 { + pub(crate) const fn reference(self) -> Srgb8 { + self.reference + } + + pub(crate) const fn candidate(self) -> Srgb8 { + self.candidate + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct ExactSrgb8RelationPassV1; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct ExactSrgb8RelationViolationV1; + +/// Профиль точного технического отношения без перцептивных утверждений. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub(crate) struct ExactSrgb8RelationV1; + +impl ExactSrgb8RelationV1 { + pub(crate) const fn identity(self) -> ExactSrgb8RelationIdentityV1 { + ExactSrgb8RelationIdentityV1::HomogeneousEndpointEqualityV1 + } + + pub(crate) const fn release(self) -> ExactSrgb8RelationReleaseV1 { + ExactSrgb8RelationReleaseV1::V1 + } + + pub(crate) const fn capability(self) -> ExactSrgb8RelationCapabilityV1 { + ExactSrgb8RelationCapabilityV1::HomogeneousEncodedSrgb8PairV1 + } + + pub(crate) fn assess( + self, + reference: Srgb8, + candidate: Srgb8, + ) -> ( + ExactSrgb8RelationMeasurementV1, + HardDecision, + ) { + let measurement = ExactSrgb8RelationMeasurementV1 { + reference, + candidate, + }; + let decision = if exact_srgb8_equal(reference, candidate) { + HardDecision::Pass(ExactSrgb8RelationPassV1) + } else { + HardDecision::Violation(ExactSrgb8RelationViolationV1) + }; + (measurement, decision) + } +} + +/// Принадлежащая Core identity точного равенства source одного intrinsic Paint. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ExactSrgb8IntrinsicUnaryIdentityV1 { + SourceEqualityV1, +} + +/// Версия точного unary-закона над source Paint. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ExactSrgb8IntrinsicUnaryReleaseV1 { + V1, +} + +/// Capability не обещает равенство alpha или результата композитинга. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ExactSrgb8IntrinsicUnaryCapabilityV1 { + EncodedSrgb8PaintSourceV1, +} + +/// Сырые expected и actual сохраняются независимо от вердикта. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct ExactSrgb8IntrinsicUnaryMeasurementV1 { + expected: Srgb8, + actual: Srgb8, +} + +impl ExactSrgb8IntrinsicUnaryMeasurementV1 { + pub(crate) const fn expected(self) -> Srgb8 { + self.expected + } + + pub(crate) const fn actual(self) -> Srgb8 { + self.actual + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct ExactSrgb8IntrinsicUnaryPassV1; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct ExactSrgb8IntrinsicUnaryViolationV1; + +/// Точный unary-профиль проверяет только source и не теряет полный Paint binding. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub(crate) struct ExactSrgb8IntrinsicUnaryV1; + +impl ExactSrgb8IntrinsicUnaryV1 { + pub(crate) const fn identity(self) -> ExactSrgb8IntrinsicUnaryIdentityV1 { + ExactSrgb8IntrinsicUnaryIdentityV1::SourceEqualityV1 + } + + pub(crate) const fn release(self) -> ExactSrgb8IntrinsicUnaryReleaseV1 { + ExactSrgb8IntrinsicUnaryReleaseV1::V1 + } + + pub(crate) const fn capability(self) -> ExactSrgb8IntrinsicUnaryCapabilityV1 { + ExactSrgb8IntrinsicUnaryCapabilityV1::EncodedSrgb8PaintSourceV1 + } + + pub(crate) fn assess( + self, + expected: Srgb8, + actual: Srgb8, + ) -> ( + ExactSrgb8IntrinsicUnaryMeasurementV1, + HardDecision, + ) { + let measurement = ExactSrgb8IntrinsicUnaryMeasurementV1 { expected, actual }; + let decision = if exact_srgb8_equal(expected, actual) { + HardDecision::Pass(ExactSrgb8IntrinsicUnaryPassV1) + } else { + HardDecision::Violation(ExactSrgb8IntrinsicUnaryViolationV1) + }; + (measurement, decision) + } +} + +/// Закрытый registry intrinsic-unary профилей Core. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum CoreIntrinsicUnaryInvocationV1 { + ExactSrgb8 { expected: Srgb8 }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum CoreIntrinsicUnaryMeasurementV1 { + ExactSrgb8(ExactSrgb8IntrinsicUnaryMeasurementV1), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum CoreIntrinsicUnaryPassV1 { + ExactSrgb8(ExactSrgb8IntrinsicUnaryPassV1), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum CoreIntrinsicUnaryViolationV1 { + ExactSrgb8(ExactSrgb8IntrinsicUnaryViolationV1), +} + +impl CoreIntrinsicUnaryInvocationV1 { + pub(crate) const fn exact_srgb8(expected: Srgb8) -> Self { + Self::ExactSrgb8 { expected } + } + + pub(crate) const fn content(self) -> ProgramConstraintContentV1 { + match self { + Self::ExactSrgb8 { expected } => { + let profile = ExactSrgb8IntrinsicUnaryV1; + ProgramConstraintContentV1::ExactSrgb8IntrinsicUnary { + identity: profile.identity(), + release: profile.release(), + capability: profile.capability(), + expected, + } + } + } + } + + pub(crate) fn assess( + self, + actual: Srgb8, + ) -> ( + CoreIntrinsicUnaryMeasurementV1, + HardDecision, + ) { + match self { + Self::ExactSrgb8 { expected } => { + let (measurement, decision) = ExactSrgb8IntrinsicUnaryV1.assess(expected, actual); + let decision = match decision { + HardDecision::Pass(proof) => { + HardDecision::Pass(CoreIntrinsicUnaryPassV1::ExactSrgb8(proof)) + } + HardDecision::Violation(proof) => { + HardDecision::Violation(CoreIntrinsicUnaryViolationV1::ExactSrgb8(proof)) + } + }; + ( + CoreIntrinsicUnaryMeasurementV1::ExactSrgb8(measurement), + decision, + ) + } + } + } +} + +/// Закрытый registry однородных directional-профилей Core. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum CoreRelationInvocationV1 { + ExactSrgb8, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum CoreRelationMeasurementV1 { + ExactSrgb8(ExactSrgb8RelationMeasurementV1), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum CoreRelationPassV1 { + ExactSrgb8(ExactSrgb8RelationPassV1), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum CoreRelationViolationV1 { + ExactSrgb8(ExactSrgb8RelationViolationV1), +} + +impl CoreRelationInvocationV1 { + pub(crate) const fn exact_srgb8() -> Self { + Self::ExactSrgb8 + } + + pub(crate) const fn content(self) -> ProgramConstraintContentV1 { + match self { + Self::ExactSrgb8 => { + let profile = ExactSrgb8RelationV1; + ProgramConstraintContentV1::ExactSrgb8Relation { + identity: profile.identity(), + release: profile.release(), + capability: profile.capability(), + } + } + } + } + + pub(crate) fn assess( + self, + reference: Srgb8, + candidate: Srgb8, + ) -> ( + CoreRelationMeasurementV1, + HardDecision, + ) { + match self { + Self::ExactSrgb8 => { + let (measurement, decision) = ExactSrgb8RelationV1.assess(reference, candidate); + let decision = match decision { + HardDecision::Pass(proof) => { + HardDecision::Pass(CoreRelationPassV1::ExactSrgb8(proof)) + } + HardDecision::Violation(proof) => { + HardDecision::Violation(CoreRelationViolationV1::ExactSrgb8(proof)) + } + }; + (CoreRelationMeasurementV1::ExactSrgb8(measurement), decision) + } + } + } +} diff --git a/crates/labcolors-core/src/generic_boundary_tests.rs b/crates/labcolors-core/src/generic_boundary_tests.rs index 59425d08..64953966 100644 --- a/crates/labcolors-core/src/generic_boundary_tests.rs +++ b/crates/labcolors-core/src/generic_boundary_tests.rs @@ -8,6 +8,7 @@ const APPEARANCE_SOURCE: &str = include_str!("appearance.rs"); const CLEAN_SET_SOURCE: &str = include_str!("clean_set.rs"); const CONSTRAINTS_SOURCE: &str = include_str!("constraints/mod.rs"); const EXACT_CONSTRAINT_SOURCE: &str = include_str!("constraints/exact.rs"); +const RELATION_CONSTRAINT_SOURCE: &str = include_str!("constraints/relation.rs"); const JOINT_SOURCE: &str = include_str!("joint.rs"); const LIB_SOURCE: &str = include_str!("lib.rs"); const LCS_OCCURRENCE_SOURCE: &str = include_str!("lcs_occurrence.rs"); @@ -18,15 +19,18 @@ const PROGRAM_SOURCE: &str = include_str!("program.rs"); const POINT_SUPPORT_SOURCE: &str = include_str!("point_support.rs"); const PROGRAM_IDENTITY_SOURCE: &str = include_str!("program_identity.rs"); const PROGRAM_SESSION_SOURCE: &str = include_str!("program_session.rs"); +const RELATION_SOURCE: &str = include_str!("relation.rs"); const SESSION_SOURCE: &str = include_str!("session.rs"); const WCAG22_CONSTRAINT_SOURCE: &str = include_str!("constraints/wcag22.rs"); -const GENERIC_SOURCES: [(&str, &str); 5] = [ +const GENERIC_SOURCES: [(&str, &str); 7] = [ ("appearance.rs", APPEARANCE_SOURCE), + ("constraints/relation.rs", RELATION_CONSTRAINT_SOURCE), ("lcs_occurrence.rs", LCS_OCCURRENCE_SOURCE), ("program/attachment.rs", PROGRAM_ATTACHMENT_SOURCE), ("program_identity.rs", PROGRAM_IDENTITY_SOURCE), ("program_session.rs", PROGRAM_SESSION_SOURCE), + ("relation.rs", RELATION_SOURCE), ]; const CLEAN_SET_PROGRAM_SOURCES: &[(&str, &str)] = &[ @@ -264,6 +268,16 @@ fn production_rust_sources() -> Vec<(String, String)> { sources } +#[test] +fn generic_source_inventory_covers_relation_topology_and_evaluators() { + for required in ["relation.rs", "constraints/relation.rs"] { + assert!( + GENERIC_SOURCES.iter().any(|(path, _)| *path == required), + "generic vocabulary ratchet must cover {required}", + ); + } +} + #[test] fn generic_physical_and_transport_modules_contain_no_client_or_legacy_vocabulary() { for (path, source) in GENERIC_SOURCES { @@ -1299,7 +1313,9 @@ fn program_session_keeps_physical_evidence_separate_from_lazy_lcs_capability() { "ModeledLcsOccurrenceV1::from_signal_in_context(", "modeled_occurrences: Vec>", "ProgramPointAssessmentErrorV1::Binding", - "ProgramSessionEvaluationError::ModeledOccurrence", + // Evaluation failures stay evaluator-generic; a topology-specific + // error variant would recreate a parallel unary execution contract. + "ProgramSessionEvaluationError::VisibleUnary", ] { assert!( !PROGRAM_SESSION_SOURCE.contains(forbidden), @@ -1431,7 +1447,7 @@ fn program_session_keeps_physical_evidence_separate_from_lazy_lcs_capability() { "let outputs = compile_outputs(", ); for required in [ - "compile_constraints::( &graph, &all_occurrence_contexts, &point_presentations, &program.constraints, )?", + "compile_constraints::( &program, &graph, &dependency_index, &mut dependency_scratch, &all_occurrence_contexts, &point_presentations, &program.constraints, )?", "compact_constraint_contexts(&all_occurrence_contexts, &mut constraints)?", ] { assert!( @@ -1447,14 +1463,13 @@ fn program_session_keeps_physical_evidence_separate_from_lazy_lcs_capability() { ); for required in [ "targets.sort_unstable(); targets.dedup();", - "CompiledProgramConstraintBodyV1::ModeledOccurrence { target_id, .. } => { Some(*target_id) }", - "CompiledProgramConstraintBodyV1::PointPresentation { .. } => None", - ".binary_search_by_key(target_id, |binding| binding.occurrence)", + "CompiledProgramConstraintBodyV1::VisibleUnary { occurrence, .. } => { targets.push(*occurrence); }", + ".binary_search_by_key(occurrence, |binding| binding.occurrence)", "*occurrence_context_index = index;", ] { assert!( compaction.contains(required), - "cold compilation must deduplicate targets and remap every constraint; missing `{required}`", + "cold compilation must deduplicate targets and remap unary constraints; missing `{required}`", ); } @@ -1467,7 +1482,7 @@ fn program_session_keeps_physical_evidence_separate_from_lazy_lcs_capability() { !hot_evaluation.contains("binary_search"), "hot Program evaluation must consume compile-time direct indices without searching", ); - assert!(hot_evaluation.contains(".get(occurrence_context_index)")); + assert!(hot_evaluation.contains(".get(*occurrence_context_index)")); for required in [ "pub(crate) struct ProgramLcsPointAdapterV1", @@ -1543,11 +1558,16 @@ fn program_identity_binds_lcs_releases_only_through_lcs_constraint_content() { "DOMAIN_V4", "PROGRAM_SCHEMA_V4", "compile_program_content_identity_v4", + "ProgramContentIdentityV5", + "ContentIdentityV5", + "DOMAIN_V5", + "PROGRAM_SCHEMA_V5", + "compile_program_content_identity_v5", ] { for (path, source) in identity_sources { assert!( !contains_rust_identifier(source, retired), - "the V5 content-address cut must not retain legacy identity symbol `{retired}` in {path}", + "the V6 content-address cut must not retain legacy identity symbol `{retired}` in {path}", ); } } @@ -1555,13 +1575,14 @@ fn program_identity_binds_lcs_releases_only_through_lcs_constraint_content() { assert!(!PROGRAM_IDENTITY_SOURCE.contains("labcolors.program-content-identity.v2")); assert!(!PROGRAM_IDENTITY_SOURCE.contains("labcolors.program-content-identity.v3")); assert!(!PROGRAM_IDENTITY_SOURCE.contains("labcolors.program-content-identity.v4")); + assert!(!PROGRAM_IDENTITY_SOURCE.contains("labcolors.program-content-identity.v5")); for required in [ - "const DOMAIN_V5: &[u8] = b\"labcolors.program-content-identity.v5\\0\";", - "pub(super) const PROGRAM_SCHEMA_V5: u8 = 5;", + "const DOMAIN_V6: &[u8] = b\"labcolors.program-content-identity.v6\\0\";", + "pub(super) const PROGRAM_SCHEMA_V6: u8 = 6;", ] { assert!( PROGRAM_IDENTITY_SOURCE.contains(required), - "the V5 content-address type must bind its exact domain and schema tag; missing `{required}`", + "the V6 content-address type must bind its exact domain and schema tag; missing `{required}`", ); } @@ -1932,7 +1953,6 @@ fn current_og0_program_epoch_has_one_explicit_group_without_scenario_scope_creep for forbidden in [ "Vec", "ScenarioId", - "ScenarioSet", "ObservedScenarioSet", "GraphTemplate", "Cartesian", @@ -1946,6 +1966,7 @@ fn current_og0_program_epoch_has_one_explicit_group_without_scenario_scope_creep "current OG0 transport module must not acquire `{forbidden}` scope" ); } + assert!(PROGRAM_SESSION_SOURCE.contains("NonEmptyScenarioSetV1")); } #[test] diff --git a/crates/labcolors-core/src/lib.rs b/crates/labcolors-core/src/lib.rs index db954eb0..5c7c0c83 100644 --- a/crates/labcolors-core/src/lib.rs +++ b/crates/labcolors-core/src/lib.rs @@ -59,6 +59,7 @@ pub(crate) mod program; ) )] pub(crate) mod program_session; +pub(crate) mod relation; #[cfg_attr( not(test), expect( @@ -124,6 +125,8 @@ mod program_boundary_tests; mod program_api_tests; #[cfg(test)] mod program_clean_set_tests; +#[cfg(test)] +mod program_relation_tests; #[cfg(test)] mod release_registry_tests; diff --git a/crates/labcolors-core/src/observation.rs b/crates/labcolors-core/src/observation.rs index 734c2d8f..c396da85 100644 --- a/crates/labcolors-core/src/observation.rs +++ b/crates/labcolors-core/src/observation.rs @@ -6,6 +6,7 @@ use core::cmp::Ordering; use core::ops::Range; +use std::num::NonZeroUsize; use std::rc::Rc; use crate::Srgb8; @@ -454,6 +455,37 @@ impl RevisionBoundObservationV1 { } } +/// Типизированное представление доказывает непустоту набора допущенных +/// физических сценариев до входа в evaluator hot path. +#[derive(Debug, Clone, Copy)] +pub(crate) struct NonEmptyScenarioSetV1<'a> { + observation: &'a RevisionBoundObservationV1, + len: NonZeroUsize, +} + +impl<'a> NonEmptyScenarioSetV1<'a> { + /// `None` здесь означает нарушение приватного инварианта, а не пустой + /// восстанавливаемый public input. + pub(crate) fn from_admitted(observation: &'a RevisionBoundObservationV1) -> Option { + Some(Self { + observation, + len: NonZeroUsize::new(observation.physical_case_count())?, + }) + } + + pub(crate) const fn observation(self) -> &'a RevisionBoundObservationV1 { + self.observation + } + + pub(crate) const fn len(self) -> NonZeroUsize { + self.len + } + + pub(crate) fn physical_values(self, case_index: usize) -> Option<&'a [ColorSignal]> { + self.observation.physical_values(case_index) + } +} + /// Revision-bound current `Unknown` payload. It contains no previous evidence. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) struct RevisionBoundUnknownV1 { diff --git a/crates/labcolors-core/src/program.rs b/crates/labcolors-core/src/program.rs index d3e2250f..2e93cbcc 100644 --- a/crates/labcolors-core/src/program.rs +++ b/crates/labcolors-core/src/program.rs @@ -24,7 +24,7 @@ //! [`CertificateV1::Verified`] хранит выбранное состояние, все клетки //! доказательства и сертифицированные Paint outputs. [`CertificateV1::Conflict`] //! хранит исчерпывающий конфликт по всем рассмотренным состояниям. -//! [`ContentIdentityV5`] идентифицирует каноническое содержание, но не даёт +//! [`ContentIdentityV6`] идентифицирует каноническое содержание, но не даёт //! полномочий живого [`OwnerV1`]. #![forbid(unreachable_pub)] @@ -40,7 +40,9 @@ use crate::appearance::{ }; use crate::composition::{AdmittedOpacityV1, CompositionProfileV1, OpacityAdmissionErrorV1}; use crate::constraints::{ - ApplicableWcag22EvaluationErrorV1, ExactSrgb8IdentityV1, ProgramVisiblePointBindingV1, + ApplicableWcag22EvaluationErrorV1, CoreIntrinsicUnaryMeasurementV1, CoreIntrinsicUnaryPassV1, + CoreIntrinsicUnaryViolationV1, CoreRelationMeasurementV1, CoreRelationPassV1, + CoreRelationViolationV1, ExactSrgb8IdentityV1, ProgramVisiblePointBindingV1, ProgramVisiblePointPassEvidence, ProgramVisiblePointViolationEvidence, Wcag22Srgb8V1, }; use crate::joint::FiniteJointOrderErrorV1; @@ -67,11 +69,17 @@ use crate::program_session::{ PointPresentationRootV1, PointPresentationTargetV1, PresentationRootId, ProgramCompileError, ProgramConflictV1, ProgramConstraintCellV1, ProgramConstraintPassEvidenceV1, ProgramConstraintResultV1, ProgramConstraintSubjectV1, ProgramConstraintViolationEvidenceV1, - ProgramContentIdentityV5, ProgramPaintOutputV1, ProgramSessionEvaluationError, - ProgramSessionInstantiateError, ProgramSessionPlan, ProgramVerifiedV1, Source, SourceId, - Surface, Target, TargetCandidateChoiceV1, TargetCandidateId, - TargetCandidateV1 as CoreTargetCandidateV1, TargetId, + ProgramContentIdentityV6, ProgramIntrinsicPaintBindingV1, ProgramIntrinsicUnaryPassEvidenceV1, + ProgramIntrinsicUnaryViolationEvidenceV1, ProgramPaintOutputV1, + ProgramRelationMemberDecisionV1, ProgramRelationMemberEvidenceV1, ProgramReportV1, + ProgramSessionEvaluationError, ProgramSessionInstantiateError, ProgramSessionPlan, + ProgramVerifiedV1, ProgramVisibleRelationBindingV1, Source, SourceId, Surface, Target, + TargetCandidateChoiceV1, TargetCandidateId, TargetCandidateV1 as CoreTargetCandidateV1, + TargetId, }; +#[cfg(test)] +pub(crate) use crate::relation::DirectedRelationErrorV1; +pub(crate) use crate::relation::DirectedRelationV1; use crate::session::{ PreparedSessionTransition, Session, SessionState, SessionUpdateError, SessionView, }; @@ -89,6 +97,7 @@ type CoreProgramSessionViewV1<'a> = SessionView<'a, CoreProgramPlanV1>; type CorePreparedSessionTransitionV1<'a> = PreparedSessionTransition<'a, CoreProgramPlanV1>; type CoreProgramPlanErrorV1 = ProgramSessionEvaluationError; type CoreProgramConstraintCellV1 = ProgramConstraintCellV1; +type CoreProgramReportV1 = ProgramReportV1; type CoreExactPassEvidenceV1 = ProgramVisiblePointPassEvidence; type CoreExactViolationEvidenceV1 = ProgramVisiblePointViolationEvidence; type CoreWcag22PassEvidenceV1 = ProgramVisiblePointPassEvidence; @@ -266,6 +275,10 @@ pub(crate) enum PaintValueErrorV1 { pub(crate) struct PaintValueV1(EncodedPointPaintValueV1); impl PaintValueV1 { + const fn from_core(value: EncodedPointPaintValueV1) -> Self { + Self(value) + } + /// Принимает только физически определённый straight alpha и канонизирует /// оба знака нуля, чтобы одно значение имело одно identity-представление. pub(crate) fn try_new(source: Srgb8, opacity: f64) -> Result { @@ -545,6 +558,20 @@ pub(crate) enum CompileErrorKindV1 { MissingOccurrenceBackdrop, /// Ограничение ссылается на отсутствующий Occurrence. MissingConstraintOccurrence, + /// Intrinsic-unary ограничение ссылается на отсутствующую Target. + MissingIntrinsicUnaryTarget, + /// Reference intrinsic-отношения ссылается на отсутствующую Target. + MissingIntrinsicRelationReference, + /// Candidate intrinsic-отношения ссылается на отсутствующую Target. + MissingIntrinsicRelationCandidate, + /// Reference visible-отношения ссылается на отсутствующий Occurrence. + MissingVisibleRelationReference, + /// Candidate visible-отношения ссылается на отсутствующий Occurrence. + MissingVisibleRelationCandidate, + /// Intrinsic reference зависит от выбираемого finite Target. + SolverDependentIntrinsicRelationReference, + /// Visible reference зависит от выбираемого finite Target. + SolverDependentVisibleRelationReference, /// Ограничение clean-set ссылается на необъявленную цель представления. MissingConstraintPresentationTarget, /// Выход ссылается на отсутствующий Paint. @@ -973,6 +1000,57 @@ pub(crate) enum CompileErrorV1 { /// Отсутствующий Occurrence. occurrence: OccurrenceIdV1, }, + /// Intrinsic-unary ограничение ссылается на отсутствующую Target. + MissingIntrinsicUnaryTarget { + /// Ошибочное ограничение. + constraint: ConstraintIdV1, + /// Отсутствующая Target. + target: TargetIdV1, + }, + /// Reference intrinsic-отношения ссылается на отсутствующую Target. + MissingIntrinsicRelationReference { + /// Ошибочное ограничение. + constraint: ConstraintIdV1, + /// Отсутствующий reference. + reference: TargetIdV1, + }, + /// Candidate intrinsic-отношения ссылается на отсутствующую Target. + MissingIntrinsicRelationCandidate { + /// Ошибочное ограничение. + constraint: ConstraintIdV1, + /// Отсутствующий candidate. + candidate: TargetIdV1, + }, + /// Reference visible-отношения ссылается на отсутствующий Occurrence. + MissingVisibleRelationReference { + /// Ошибочное ограничение. + constraint: ConstraintIdV1, + /// Отсутствующий reference. + reference: OccurrenceIdV1, + }, + /// Candidate visible-отношения ссылается на отсутствующий Occurrence. + MissingVisibleRelationCandidate { + /// Ошибочное ограничение. + constraint: ConstraintIdV1, + /// Отсутствующий candidate. + candidate: OccurrenceIdV1, + }, + /// Intrinsic reference зависит от выбираемого finite Target. + SolverDependentIntrinsicRelationReference { + /// Ошибочное ограничение. + constraint: ConstraintIdV1, + /// Solver-зависимый reference. + reference: TargetIdV1, + }, + /// Visible reference зависит от выбираемого finite Target. + SolverDependentVisibleRelationReference { + /// Ошибочное ограничение. + constraint: ConstraintIdV1, + /// Solver-зависимый reference. + reference: OccurrenceIdV1, + /// Finite Target, из-за которого reference зависит от solver state. + target: TargetIdV1, + }, /// Ограничение clean-set ссылается не на целиком объявленную цель представления. MissingConstraintPresentationTarget { /// Ошибочное ограничение. @@ -1060,6 +1138,21 @@ impl CompileErrorV1 { Self::EmptyOutputSet => Kind::EmptyOutputSet, Self::DuplicateConstraint { .. } => Kind::DuplicateConstraint, Self::MissingConstraintOccurrence { .. } => Kind::MissingConstraintOccurrence, + Self::MissingIntrinsicUnaryTarget { .. } => Kind::MissingIntrinsicUnaryTarget, + Self::MissingIntrinsicRelationReference { .. } => { + Kind::MissingIntrinsicRelationReference + } + Self::MissingIntrinsicRelationCandidate { .. } => { + Kind::MissingIntrinsicRelationCandidate + } + Self::MissingVisibleRelationReference { .. } => Kind::MissingVisibleRelationReference, + Self::MissingVisibleRelationCandidate { .. } => Kind::MissingVisibleRelationCandidate, + Self::SolverDependentIntrinsicRelationReference { .. } => { + Kind::SolverDependentIntrinsicRelationReference + } + Self::SolverDependentVisibleRelationReference { .. } => { + Kind::SolverDependentVisibleRelationReference + } Self::MissingConstraintPresentationTarget { .. } => { Kind::MissingConstraintPresentationTarget } @@ -1120,6 +1213,13 @@ impl CompileErrorV1 { | Self::MissingOutputPaint { output, .. } => Some(Handle::OutputSlot(*output)), Self::DuplicateConstraint { constraint } | Self::MissingConstraintOccurrence { constraint, .. } + | Self::MissingIntrinsicUnaryTarget { constraint, .. } + | Self::MissingIntrinsicRelationReference { constraint, .. } + | Self::MissingIntrinsicRelationCandidate { constraint, .. } + | Self::MissingVisibleRelationReference { constraint, .. } + | Self::MissingVisibleRelationCandidate { constraint, .. } + | Self::SolverDependentIntrinsicRelationReference { constraint, .. } + | Self::SolverDependentVisibleRelationReference { constraint, .. } | Self::MissingConstraintPresentationTarget { constraint, .. } => { Some(Handle::Constraint(*constraint)) } @@ -1153,6 +1253,14 @@ impl CompileErrorV1 { Self::MissingSurfaceInputPort { input, .. } => Some(Handle::SurfaceInputPort(*input)), Self::MissingSurfaceOccurrence { occurrence, .. } | Self::MissingConstraintOccurrence { occurrence, .. } + | Self::MissingVisibleRelationReference { + reference: occurrence, + .. + } + | Self::MissingVisibleRelationCandidate { + candidate: occurrence, + .. + } | Self::MissingConstraintPresentationTarget { occurrence, .. } | Self::MissingPresentationRootOccurrence { occurrence, .. } | Self::PresentationRootConsumedDownstream { occurrence, .. } @@ -1161,6 +1269,19 @@ impl CompileErrorV1 { | Self::PointPresentationOccurrenceOutsideRootAncestry { occurrence, .. } => { Some(Handle::Occurrence(*occurrence)) } + Self::MissingIntrinsicRelationReference { reference, .. } + | Self::MissingIntrinsicUnaryTarget { + target: reference, .. + } + | Self::SolverDependentIntrinsicRelationReference { reference, .. } => { + Some(Handle::Target(*reference)) + } + Self::MissingIntrinsicRelationCandidate { candidate, .. } => { + Some(Handle::Target(*candidate)) + } + Self::SolverDependentVisibleRelationReference { target, .. } => { + Some(Handle::Target(*target)) + } Self::MissingOccurrencePaint { paint, .. } | Self::UnassessedOutput { paint, .. } | Self::MissingOutputPaint { paint, .. } => Some(Handle::Paint(*paint)), @@ -1382,29 +1503,30 @@ impl DraftV1 { } /// Добавляет обязательное точное сравнение видимого sRGB8 результата. - pub(crate) fn push_exact_hard( + pub(crate) fn push_exact_visible_unary_hard( &mut self, id: ConstraintIdV1, occurrence: OccurrenceIdV1, expected: Srgb8, ) -> &mut Self { - self.inner.push_hard_constraint(ConstraintInvocation::hard( - id.into_core(), - occurrence.into_core(), - CoreProgramConstraintInvocationV1::ExactSrgb8(expected), - )); + self.inner + .push_hard_constraint(ConstraintInvocation::visible_unary_hard( + id.into_core(), + occurrence.into_core(), + CoreProgramConstraintInvocationV1::ExactSrgb8(expected), + )); self } /// Добавляет диагностическое точное сравнение, не влияющее на выбор. - pub(crate) fn push_exact_report_only( + pub(crate) fn push_exact_visible_unary_report_only( &mut self, id: ConstraintIdV1, occurrence: OccurrenceIdV1, expected: Srgb8, ) -> &mut Self { self.inner - .push_report_constraint(ConstraintInvocation::report_only( + .push_report_constraint(ConstraintInvocation::visible_unary_report_only( id.into_core(), occurrence.into_core(), CoreProgramConstraintInvocationV1::ExactSrgb8(expected), @@ -1412,30 +1534,73 @@ impl DraftV1 { self } + /// Добавляет обязательную exact-проверку intrinsic source одной Target. + pub(crate) fn push_exact_intrinsic_unary_hard( + &mut self, + id: ConstraintIdV1, + target: TargetIdV1, + expected: Srgb8, + ) -> &mut Self { + self.inner + .push_exact_intrinsic_unary_hard(id.into_core(), target.into_core(), expected); + self + } + + /// Добавляет обязательное exact-отношение между intrinsic Target signals. + pub(crate) fn push_exact_intrinsic_relation_hard( + &mut self, + id: ConstraintIdV1, + relation: DirectedRelationV1, + ) -> &mut Self { + self.inner.push_exact_intrinsic_relation_hard( + id.into_core(), + relation + .try_map(TargetIdV1::into_core) + .expect("facade TargetId is a transparent bijection over Core TargetId"), + ); + self + } + + /// Добавляет обязательное exact-отношение между final modeled Occurrences. + pub(crate) fn push_exact_visible_relation_hard( + &mut self, + id: ConstraintIdV1, + relation: DirectedRelationV1, + ) -> &mut Self { + self.inner.push_exact_visible_relation_hard( + id.into_core(), + relation + .try_map(OccurrenceIdV1::into_core) + .expect("facade OccurrenceId is a transparent bijection over Core OccurrenceId"), + ); + self + } + /// Добавляет обязательный критерий WCAG 2.2 для видимого результата. - pub(crate) fn push_wcag22_hard( + pub(crate) fn push_wcag22_visible_unary_hard( &mut self, id: ConstraintIdV1, occurrence: OccurrenceIdV1, criterion: Wcag22CriterionV1, ) -> &mut Self { - self.inner.push_hard_constraint(ConstraintInvocation::hard( - id.into_core(), - occurrence.into_core(), - CoreProgramConstraintInvocationV1::Wcag22Srgb8(criterion), - )); + self.inner + .push_hard_constraint(ConstraintInvocation::visible_unary_hard( + id.into_core(), + occurrence.into_core(), + CoreProgramConstraintInvocationV1::Wcag22Srgb8(criterion), + )); self } /// Добавляет диагностический критерий WCAG 2.2, не влияющий на выбор. - pub(crate) fn push_wcag22_report_only( + pub(crate) fn push_wcag22_visible_unary_report_only( &mut self, id: ConstraintIdV1, occurrence: OccurrenceIdV1, criterion: Wcag22CriterionV1, ) -> &mut Self { self.inner - .push_report_constraint(ConstraintInvocation::report_only( + .push_report_constraint(ConstraintInvocation::visible_unary_report_only( id.into_core(), occurrence.into_core(), CoreProgramConstraintInvocationV1::Wcag22Srgb8(criterion), @@ -1558,8 +1723,8 @@ impl OwnerV1 { /// /// Identity доступна до первого update, но не заменяет полномочия этой /// конкретной owner-эпохи. - pub(crate) fn content_identity(&self) -> ContentIdentityV5 { - ContentIdentityV5::from_core(self.compiled.content_identity()) + pub(crate) fn content_identity(&self) -> ContentIdentityV6 { + ContentIdentityV6::from_core(self.compiled.content_identity()) } /// Вычисляет верхние границы клеток для prospective Observed-update. @@ -1879,10 +2044,10 @@ impl<'session> PreparedSessionTransitionV1<'session> { /// Identity не идентифицирует owner-эпоху и не даёт runtime-полномочий. #[repr(transparent)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub(crate) struct ContentIdentityV5([u8; 32]); +pub(crate) struct ContentIdentityV6([u8; 32]); -impl ContentIdentityV5 { - const fn from_core(value: ProgramContentIdentityV5) -> Self { +impl ContentIdentityV6 { + const fn from_core(value: ProgramContentIdentityV6) -> Self { Self(*value.as_bytes()) } @@ -1900,8 +2065,8 @@ pub(crate) struct VerifiedCertificateV1<'a> { impl<'a> VerifiedCertificateV1<'a> { /// Возвращает identity скомпилированного содержания. - pub(crate) const fn content_identity(self) -> ContentIdentityV5 { - ContentIdentityV5::from_core(self.inner.report().content_identity()) + pub(crate) const fn content_identity(self) -> ContentIdentityV6 { + ContentIdentityV6::from_core(self.inner.report().content_identity()) } /// Возвращает точное наблюдение, на котором выдан сертификат. @@ -1920,11 +2085,11 @@ impl<'a> VerifiedCertificateV1<'a> { pub(crate) fn cells( self, ) -> impl ExactSizeIterator> + FusedIterator + 'a { - self.inner - .report() + let report = self.inner.report(); + report .cells() .iter() - .map(VerifiedCellV1::from_core) + .map(move |cell| VerifiedCellV1::from_core(report, cell)) } /// Возвращает все сертифицированные Paint outputs в каноническом порядке. @@ -1946,8 +2111,8 @@ pub(crate) struct ConflictCertificateV1<'a> { impl<'a> ConflictCertificateV1<'a> { /// Возвращает identity скомпилированного содержания. - pub(crate) const fn content_identity(self) -> ContentIdentityV5 { - ContentIdentityV5::from_core(self.inner.report().content_identity()) + pub(crate) const fn content_identity(self) -> ContentIdentityV6 { + ContentIdentityV6::from_core(self.inner.report().content_identity()) } /// Возвращает точное наблюдение, вызвавшее конфликт. @@ -1966,11 +2131,11 @@ impl<'a> ConflictCertificateV1<'a> { pub(crate) fn cells( self, ) -> impl ExactSizeIterator> + FusedIterator + 'a { - self.inner - .report() + let report = self.inner.report(); + report .cells() .iter() - .map(ConflictCellV1::from_core) + .map(move |cell| ConflictCellV1::from_core(report, cell)) } } @@ -1996,7 +2161,7 @@ impl<'a> CertificateV1<'a> { } /// Возвращает identity скомпилированного содержания. - pub(crate) const fn content_identity(self) -> ContentIdentityV5 { + pub(crate) const fn content_identity(self) -> ContentIdentityV6 { match self { Self::Verified(value) => value.content_identity(), Self::Conflict(value) => value.content_identity(), @@ -2122,10 +2287,20 @@ pub(crate) enum ConstraintModeV1 { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum ConstraintSubjectV1 { /// Видимый результат одного `Occurrence` в объявленном для него контексте. - ModeledOccurrence { + VisibleUnary { occurrence: OccurrenceIdV1, context: AppearanceContextV1, }, + IntrinsicUnary { + target: TargetIdV1, + }, + IntrinsicRelation { + reference: TargetIdV1, + }, + VisibleRelation { + reference: OccurrenceIdV1, + context: AppearanceContextV1, + }, /// Итоговый вклад целевого `Occurrence` в конкретный терминальный корень. PointPresentation { root: PresentationRootIdV1, @@ -2136,13 +2311,29 @@ pub(crate) enum ConstraintSubjectV1 { const fn project_constraint_subject(subject: ProgramConstraintSubjectV1) -> ConstraintSubjectV1 { match subject { - ProgramConstraintSubjectV1::ModeledOccurrence { + ProgramConstraintSubjectV1::VisibleUnary { occurrence, context, - } => ConstraintSubjectV1::ModeledOccurrence { + } => ConstraintSubjectV1::VisibleUnary { occurrence: OccurrenceIdV1::from_core(occurrence), context: AppearanceContextV1::from_core(context), }, + ProgramConstraintSubjectV1::IntrinsicUnary { target } => { + ConstraintSubjectV1::IntrinsicUnary { + target: TargetIdV1::from_core(target), + } + } + ProgramConstraintSubjectV1::IntrinsicRelation { reference } => { + ConstraintSubjectV1::IntrinsicRelation { + reference: TargetIdV1::from_core(reference), + } + } + ProgramConstraintSubjectV1::VisibleRelation { reference, context } => { + ConstraintSubjectV1::VisibleRelation { + reference: OccurrenceIdV1::from_core(reference), + context: AppearanceContextV1::from_core(context), + } + } ProgramConstraintSubjectV1::PointPresentation { target, terminal } => { ConstraintSubjectV1::PointPresentation { root: PresentationRootIdV1::from_core(target.root()), @@ -2156,12 +2347,16 @@ const fn project_constraint_subject(subject: ProgramConstraintSubjectV1) -> Cons /// Одна клетка `case × constraint` выбранного или fixed состояния. #[derive(Clone, Copy)] pub(crate) struct VerifiedCellV1<'a> { + report: &'a CoreProgramReportV1, inner: &'a CoreProgramConstraintCellV1, } impl<'a> VerifiedCellV1<'a> { - const fn from_core(inner: &'a CoreProgramConstraintCellV1) -> Self { - Self { inner } + const fn from_core( + report: &'a CoreProgramReportV1, + inner: &'a CoreProgramConstraintCellV1, + ) -> Self { + Self { report, inner } } /// Возвращает индекс физического case. @@ -2186,19 +2381,23 @@ impl<'a> VerifiedCellV1<'a> { /// Возвращает типизированное сохранённое evidence. pub(crate) fn assessment(self) -> AssessmentV1<'a> { - project_assessment(self.inner) + project_assessment(self.report, self.inner) } } /// Одна исчерпывающая клетка `state × case × constraint` конфликта. #[derive(Clone, Copy)] pub(crate) struct ConflictCellV1<'a> { + report: &'a CoreProgramReportV1, inner: &'a CoreProgramConstraintCellV1, } impl<'a> ConflictCellV1<'a> { - const fn from_core(inner: &'a CoreProgramConstraintCellV1) -> Self { - Self { inner } + const fn from_core( + report: &'a CoreProgramReportV1, + inner: &'a CoreProgramConstraintCellV1, + ) -> Self { + Self { report, inner } } /// Возвращает индекс рассмотренного состояния. @@ -2228,7 +2427,7 @@ impl<'a> ConflictCellV1<'a> { /// Возвращает типизированное сохранённое evidence. pub(crate) fn assessment(self) -> AssessmentV1<'a> { - project_assessment(self.inner) + project_assessment(self.report, self.inner) } } @@ -2240,27 +2439,30 @@ const fn project_constraint_mode(cell: &CoreProgramConstraintCellV1) -> Constrai } } -fn project_assessment(cell: &CoreProgramConstraintCellV1) -> AssessmentV1<'_> { +fn project_assessment<'a>( + report: &'a CoreProgramReportV1, + cell: &'a CoreProgramConstraintCellV1, +) -> AssessmentV1<'a> { match cell.result() { - ProgramConstraintResultV1::Pass(ProgramConstraintPassEvidenceV1::ModeledOccurrence( + ProgramConstraintResultV1::Pass(ProgramConstraintPassEvidenceV1::VisibleUnary( CoreProgramPassEvidenceV1::ExactSrgb8(evidence), )) => AssessmentV1::ExactSrgb8(ExactSrgb8EvidenceV1 { inner: ExactSrgb8EvidenceRefV1::Pass(evidence), }), ProgramConstraintResultV1::Violation( - ProgramConstraintViolationEvidenceV1::ModeledOccurrence( + ProgramConstraintViolationEvidenceV1::VisibleUnary( CoreProgramViolationEvidenceV1::ExactSrgb8(evidence), ), ) => AssessmentV1::ExactSrgb8(ExactSrgb8EvidenceV1 { inner: ExactSrgb8EvidenceRefV1::Violation(evidence), }), - ProgramConstraintResultV1::Pass(ProgramConstraintPassEvidenceV1::ModeledOccurrence( + ProgramConstraintResultV1::Pass(ProgramConstraintPassEvidenceV1::VisibleUnary( CoreProgramPassEvidenceV1::Wcag22Srgb8(evidence), )) => AssessmentV1::Wcag22Srgb8(Wcag22Srgb8EvidenceV1 { inner: Wcag22Srgb8EvidenceRefV1::Pass(evidence), }), ProgramConstraintResultV1::Violation( - ProgramConstraintViolationEvidenceV1::ModeledOccurrence( + ProgramConstraintViolationEvidenceV1::VisibleUnary( CoreProgramViolationEvidenceV1::Wcag22Srgb8(evidence), ), ) => AssessmentV1::Wcag22Srgb8(Wcag22Srgb8EvidenceV1 { @@ -2276,6 +2478,32 @@ fn project_assessment(cell: &CoreProgramConstraintCellV1) -> AssessmentV1<'_> { ) => AssessmentV1::DeclaredSrgb8CleanSet(DeclaredSrgb8CleanSetEvidenceV1 { inner: DeclaredSrgb8CleanSetEvidenceRefV1::Violation(evidence), }), + ProgramConstraintResultV1::Pass(ProgramConstraintPassEvidenceV1::IntrinsicUnary( + evidence, + )) => AssessmentV1::IntrinsicUnary(IntrinsicUnaryEvidenceV1 { + inner: IntrinsicUnaryEvidenceRefV1::Pass(evidence), + }), + ProgramConstraintResultV1::Violation( + ProgramConstraintViolationEvidenceV1::IntrinsicUnary(evidence), + ) => AssessmentV1::IntrinsicUnary(IntrinsicUnaryEvidenceV1 { + inner: IntrinsicUnaryEvidenceRefV1::Violation(evidence), + }), + ProgramConstraintResultV1::Pass(ProgramConstraintPassEvidenceV1::Relation(span)) => { + AssessmentV1::Relation(RelationEvidenceV1 { + verdict: VerdictV1::Pass, + members: report + .relation_members_for(*span) + .unwrap_or_else(|| unreachable!("report owns every relation span")), + }) + } + ProgramConstraintResultV1::Violation(ProgramConstraintViolationEvidenceV1::Relation( + span, + )) => AssessmentV1::Relation(RelationEvidenceV1 { + verdict: VerdictV1::Violation, + members: report + .relation_members_for(*span) + .unwrap_or_else(|| unreachable!("report owns every relation span")), + }), } } @@ -2286,6 +2514,8 @@ pub(crate) enum AssessmentV1<'a> { ExactSrgb8(ExactSrgb8EvidenceV1<'a>), /// Evidence применимого критерия WCAG 2.2. Wcag22Srgb8(Wcag22Srgb8EvidenceV1<'a>), + IntrinsicUnary(IntrinsicUnaryEvidenceV1<'a>), + Relation(RelationEvidenceV1<'a>), /// Свидетельство закреплённого пакетом clean-set над финальным результатом /// представления. DeclaredSrgb8CleanSet(DeclaredSrgb8CleanSetEvidenceV1<'a>), @@ -2297,11 +2527,308 @@ impl AssessmentV1<'_> { match self { Self::ExactSrgb8(value) => value.verdict(), Self::Wcag22Srgb8(value) => value.verdict(), + Self::IntrinsicUnary(value) => value.verdict(), + Self::Relation(value) => value.verdict(), Self::DeclaredSrgb8CleanSet(value) => value.verdict(), } } } +/// Полное заимствованное evidence одного intrinsic-unary ограничения. +#[derive(Clone, Copy)] +enum IntrinsicUnaryEvidenceRefV1<'a> { + Pass(&'a ProgramIntrinsicUnaryPassEvidenceV1), + Violation(&'a ProgramIntrinsicUnaryViolationEvidenceV1), +} + +#[derive(Clone, Copy)] +pub(crate) struct IntrinsicUnaryEvidenceV1<'a> { + inner: IntrinsicUnaryEvidenceRefV1<'a>, +} + +impl IntrinsicUnaryEvidenceV1<'_> { + pub(crate) const fn verdict(self) -> VerdictV1 { + match self.inner { + IntrinsicUnaryEvidenceRefV1::Pass(_) => VerdictV1::Pass, + IntrinsicUnaryEvidenceRefV1::Violation(_) => VerdictV1::Violation, + } + } + + pub(crate) const fn binding(self) -> IntrinsicPaintBindingV1 { + let binding = match self.inner { + IntrinsicUnaryEvidenceRefV1::Pass(value) => value.binding(), + IntrinsicUnaryEvidenceRefV1::Violation(value) => value.binding(), + }; + IntrinsicPaintBindingV1::from_core(binding) + } + + pub(crate) const fn measurement(self) -> IntrinsicUnaryMeasurementV1 { + let measurement = match self.inner { + IntrinsicUnaryEvidenceRefV1::Pass(value) => value.measurement(), + IntrinsicUnaryEvidenceRefV1::Violation(value) => value.measurement(), + }; + match measurement { + CoreIntrinsicUnaryMeasurementV1::ExactSrgb8(value) => { + IntrinsicUnaryMeasurementV1::ExactSrgb8(ExactSrgb8UnaryMeasurementV1 { + expected: value.expected(), + actual: value.actual(), + }) + } + } + } + + pub(crate) const fn proof(self) -> IntrinsicUnaryProofV1 { + match self.inner { + IntrinsicUnaryEvidenceRefV1::Pass(value) => match value.proof() { + CoreIntrinsicUnaryPassV1::ExactSrgb8(_) => IntrinsicUnaryProofV1::ExactSrgb8Pass, + }, + IntrinsicUnaryEvidenceRefV1::Violation(value) => match value.proof() { + CoreIntrinsicUnaryViolationV1::ExactSrgb8(_) => { + IntrinsicUnaryProofV1::ExactSrgb8Violation + } + }, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct IntrinsicPaintBindingV1 { + target: TargetIdV1, + value: PaintValueV1, +} + +impl IntrinsicPaintBindingV1 { + const fn from_core(value: ProgramIntrinsicPaintBindingV1) -> Self { + Self { + target: TargetIdV1::from_core(value.target()), + value: PaintValueV1::from_core(value.value()), + } + } + + pub(crate) const fn target(self) -> TargetIdV1 { + self.target + } + + pub(crate) const fn value(self) -> PaintValueV1 { + self.value + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct ExactSrgb8UnaryMeasurementV1 { + expected: Srgb8, + actual: Srgb8, +} + +impl ExactSrgb8UnaryMeasurementV1 { + pub(crate) const fn expected(self) -> Srgb8 { + self.expected + } + + pub(crate) const fn actual(self) -> Srgb8 { + self.actual + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum IntrinsicUnaryMeasurementV1 { + ExactSrgb8(ExactSrgb8UnaryMeasurementV1), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum IntrinsicUnaryProofV1 { + ExactSrgb8Pass, + ExactSrgb8Violation, +} + +/// Полное заимствованное member-evidence одного directional-ограничения. +#[derive(Clone, Copy)] +pub(crate) struct RelationEvidenceV1<'a> { + verdict: VerdictV1, + members: &'a [ProgramRelationMemberEvidenceV1], +} + +impl<'a> RelationEvidenceV1<'a> { + pub(crate) const fn verdict(self) -> VerdictV1 { + self.verdict + } + + pub(crate) fn member_count(self) -> usize { + self.members.len() + } + + pub(crate) fn members( + self, + ) -> impl ExactSizeIterator> + FusedIterator + 'a { + self.members.iter().map(RelationMemberV1::from_core) + } +} + +#[derive(Clone, Copy)] +pub(crate) enum RelationMemberV1<'a> { + Intrinsic(IntrinsicRelationMemberV1<'a>), + Visible(VisibleRelationMemberV1<'a>), +} + +impl<'a> RelationMemberV1<'a> { + fn from_core(inner: &'a ProgramRelationMemberEvidenceV1) -> Self { + match inner { + ProgramRelationMemberEvidenceV1::Intrinsic { .. } => { + Self::Intrinsic(IntrinsicRelationMemberV1 { inner }) + } + ProgramRelationMemberEvidenceV1::Visible { .. } => { + Self::Visible(VisibleRelationMemberV1 { inner }) + } + } + } + + pub(crate) const fn measurement(self) -> RelationMeasurementV1 { + let measurement = match self { + Self::Intrinsic(value) => value.inner.measurement(), + Self::Visible(value) => value.inner.measurement(), + }; + project_relation_measurement(measurement) + } + + pub(crate) const fn verdict(self) -> VerdictV1 { + match self { + Self::Intrinsic(value) => value.verdict(), + Self::Visible(value) => value.verdict(), + } + } + + pub(crate) const fn proof(self) -> RelationMemberProofV1 { + let decision = match self { + Self::Intrinsic(value) => value.inner.decision(), + Self::Visible(value) => value.inner.decision(), + }; + project_relation_proof(decision) + } +} + +#[derive(Clone, Copy)] +pub(crate) struct IntrinsicRelationMemberV1<'a> { + inner: &'a ProgramRelationMemberEvidenceV1, +} + +impl IntrinsicRelationMemberV1<'_> { + pub(crate) const fn reference(self) -> IntrinsicPaintBindingV1 { + let Some((reference, _)) = self.inner.intrinsic_bindings() else { + unreachable!() + }; + IntrinsicPaintBindingV1::from_core(*reference) + } + + pub(crate) const fn candidate(self) -> IntrinsicPaintBindingV1 { + let Some((_, candidate)) = self.inner.intrinsic_bindings() else { + unreachable!() + }; + IntrinsicPaintBindingV1::from_core(*candidate) + } + + pub(crate) const fn verdict(self) -> VerdictV1 { + project_relation_verdict(self.inner.decision()) + } +} + +#[derive(Clone, Copy)] +pub(crate) struct VisibleRelationMemberV1<'a> { + inner: &'a ProgramRelationMemberEvidenceV1, +} + +impl<'a> VisibleRelationMemberV1<'a> { + pub(crate) const fn reference(self) -> VisibleRelationEndpointV1<'a> { + let Some((reference, _)) = self.inner.visible_bindings() else { + unreachable!() + }; + VisibleRelationEndpointV1 { inner: reference } + } + + pub(crate) const fn candidate(self) -> VisibleRelationEndpointV1<'a> { + let Some((_, candidate)) = self.inner.visible_bindings() else { + unreachable!() + }; + VisibleRelationEndpointV1 { inner: candidate } + } + + pub(crate) const fn verdict(self) -> VerdictV1 { + project_relation_verdict(self.inner.decision()) + } +} + +#[derive(Clone, Copy)] +pub(crate) struct VisibleRelationEndpointV1<'a> { + inner: &'a ProgramVisibleRelationBindingV1, +} + +impl<'a> VisibleRelationEndpointV1<'a> { + pub(crate) const fn occurrence(self) -> OccurrenceIdV1 { + OccurrenceIdV1::from_core(self.inner.occurrence()) + } + + pub(crate) const fn binding(self) -> PointBindingV1<'a> { + PointBindingV1 { + inner: self.inner.physical_ref(), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct ExactSrgb8RelationMeasurementV1 { + reference: Srgb8, + candidate: Srgb8, +} + +impl ExactSrgb8RelationMeasurementV1 { + pub(crate) const fn reference(self) -> Srgb8 { + self.reference + } + + pub(crate) const fn candidate(self) -> Srgb8 { + self.candidate + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum RelationMeasurementV1 { + ExactSrgb8(ExactSrgb8RelationMeasurementV1), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum RelationMemberProofV1 { + ExactSrgb8Pass, + ExactSrgb8Violation, +} + +const fn project_relation_measurement(value: CoreRelationMeasurementV1) -> RelationMeasurementV1 { + match value { + CoreRelationMeasurementV1::ExactSrgb8(value) => { + RelationMeasurementV1::ExactSrgb8(ExactSrgb8RelationMeasurementV1 { + reference: value.reference(), + candidate: value.candidate(), + }) + } + } +} + +const fn project_relation_verdict(value: ProgramRelationMemberDecisionV1) -> VerdictV1 { + match value { + ProgramRelationMemberDecisionV1::Pass(_) => VerdictV1::Pass, + ProgramRelationMemberDecisionV1::Violation(_) => VerdictV1::Violation, + } +} + +const fn project_relation_proof(value: ProgramRelationMemberDecisionV1) -> RelationMemberProofV1 { + match value { + ProgramRelationMemberDecisionV1::Pass(CoreRelationPassV1::ExactSrgb8(_)) => { + RelationMemberProofV1::ExactSrgb8Pass + } + ProgramRelationMemberDecisionV1::Violation(CoreRelationViolationV1::ExactSrgb8(_)) => { + RelationMemberProofV1::ExactSrgb8Violation + } + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum DeclaredSrgb8CleanSetViolationKindV1 { FinalOwnedDomainAbsent, @@ -3238,6 +3765,56 @@ fn map_program_compile_error(error: ProgramCompileError) -> CompileErrorV1 { constraint: ConstraintIdV1::from_core(constraint), occurrence: OccurrenceIdV1::from_core(occurrence), }, + ProgramCompileError::MissingIntrinsicUnaryTarget { constraint, target } => { + CompileErrorV1::MissingIntrinsicUnaryTarget { + constraint: ConstraintIdV1::from_core(constraint), + target: TargetIdV1::from_core(target), + } + } + ProgramCompileError::MissingIntrinsicRelationReference { + constraint, + reference, + } => CompileErrorV1::MissingIntrinsicRelationReference { + constraint: ConstraintIdV1::from_core(constraint), + reference: TargetIdV1::from_core(reference), + }, + ProgramCompileError::MissingIntrinsicRelationCandidate { + constraint, + candidate, + } => CompileErrorV1::MissingIntrinsicRelationCandidate { + constraint: ConstraintIdV1::from_core(constraint), + candidate: TargetIdV1::from_core(candidate), + }, + ProgramCompileError::MissingVisibleRelationReference { + constraint, + reference, + } => CompileErrorV1::MissingVisibleRelationReference { + constraint: ConstraintIdV1::from_core(constraint), + reference: OccurrenceIdV1::from_core(reference), + }, + ProgramCompileError::MissingVisibleRelationCandidate { + constraint, + candidate, + } => CompileErrorV1::MissingVisibleRelationCandidate { + constraint: ConstraintIdV1::from_core(constraint), + candidate: OccurrenceIdV1::from_core(candidate), + }, + ProgramCompileError::SolverDependentIntrinsicRelationReference { + constraint, + reference, + } => CompileErrorV1::SolverDependentIntrinsicRelationReference { + constraint: ConstraintIdV1::from_core(constraint), + reference: TargetIdV1::from_core(reference), + }, + ProgramCompileError::SolverDependentVisibleRelationReference { + constraint, + reference, + target, + } => CompileErrorV1::SolverDependentVisibleRelationReference { + constraint: ConstraintIdV1::from_core(constraint), + reference: OccurrenceIdV1::from_core(reference), + target: TargetIdV1::from_core(target), + }, ProgramCompileError::MissingConstraintPresentationTarget { constraint, root, @@ -3789,7 +4366,7 @@ mod update_error_projection_tests { state_index: 1, case_index: 2, constraint: ConstraintId::new(3), - subject: ProgramConstraintSubjectV1::ModeledOccurrence { + subject: ProgramConstraintSubjectV1::VisibleUnary { occurrence: OccurrenceId::new(4), context: subject_context, }, @@ -3799,7 +4376,7 @@ mod update_error_projection_tests { state_index: 1, case_index: 2, constraint: ConstraintIdV1::new(3), - subject: ConstraintSubjectV1::ModeledOccurrence { + subject: ConstraintSubjectV1::VisibleUnary { occurrence: OccurrenceIdV1::new(4), context: AppearanceContextV1::from_core(subject_context), }, diff --git a/crates/labcolors-core/src/program/attachment/tests.rs b/crates/labcolors-core/src/program/attachment/tests.rs index d3eace2e..f9fdeb66 100644 --- a/crates/labcolors-core/src/program/attachment/tests.rs +++ b/crates/labcolors-core/src/program/attachment/tests.rs @@ -1023,7 +1023,7 @@ fn allocator_owner() -> OwnerV1 { draft.push_source_over_occurrence(INNER, PAINT, INPUT_SURFACE, context); draft.push_point_presentation_root(ROOT, INNER); draft.push_point_presentation_target(ROOT, INNER); - draft.push_wcag22_hard( + draft.push_wcag22_visible_unary_hard( ConstraintIdV1::new(10), INNER, Wcag22CriterionV1::Sc143TextDefault, @@ -1075,7 +1075,7 @@ fn owner_with_terminal_presentation( if include_terminal { draft.push_point_presentation_target(ROOT, TERMINAL); } - draft.push_exact_hard(ConstraintIdV1::new(10), INNER, expected); + draft.push_exact_visible_unary_hard(ConstraintIdV1::new(10), INNER, expected); for output in outputs { draft.push_output(*output, PAINT); } @@ -1352,7 +1352,7 @@ fn selected_nonopaque_finite_paint_reaches_sink_and_render_authority_atomically( draft.push_source_over_occurrence(INNER, PAINT, INPUT_SURFACE, context); draft.push_point_presentation_root(ROOT, INNER); draft.push_point_presentation_target(ROOT, INNER); - draft.push_exact_hard(ConstraintIdV1::new(10), INNER, Srgb8::new([0x80; 3])); + draft.push_exact_visible_unary_hard(ConstraintIdV1::new(10), INNER, Srgb8::new([0x80; 3])); draft.push_output(OUTPUT_A, PAINT); let owner = draft.compile().unwrap(); diff --git a/crates/labcolors-core/src/program_api_tests.rs b/crates/labcolors-core/src/program_api_tests.rs index 4c97f457..38e4e652 100644 --- a/crates/labcolors-core/src/program_api_tests.rs +++ b/crates/labcolors-core/src/program_api_tests.rs @@ -31,7 +31,7 @@ fn staged_program_api_is_module_qualified_without_transport_prefixes() { draft.push_source_over_occurrence(occurrence, paint, surface, context); draft.push_point_presentation_root(root, occurrence); draft.push_point_presentation_target(root, occurrence); - draft.push_exact_hard(constraint, occurrence, Srgb8::new([0, 0, 0])); + draft.push_exact_visible_unary_hard(constraint, occurrence, Srgb8::new([0, 0, 0])); draft.push_output(output, paint); let owner = draft.compile().unwrap(); diff --git a/crates/labcolors-core/src/program_boundary_tests.rs b/crates/labcolors-core/src/program_boundary_tests.rs index 44bf9ea7..a312b332 100644 --- a/crates/labcolors-core/src/program_boundary_tests.rs +++ b/crates/labcolors-core/src/program_boundary_tests.rs @@ -9,7 +9,7 @@ use crate::Srgb8; use crate::program::{ AppearanceContextErrorKindV1, AppearanceContextFieldV1, AppearanceContextV1, AssessmentV1, CertificateV1, CompileErrorHandleV1, CompileErrorKindV1, CompileErrorV1, ConstraintIdV1, - ConstraintSubjectV1, ContentIdentityV5, DraftErrorV1, DraftV1, EvidenceBoundsErrorV1, + ConstraintSubjectV1, ContentIdentityV6, DraftErrorV1, DraftV1, EvidenceBoundsErrorV1, EvidenceViewV1, FinitePaintDomainV1, InstantiateErrorV1, JointChoiceV1, JointOrderErrorV1, JointStateV1, NumericDomainErrorV1, ObservationHeadV1, OccurrenceIdV1, OpacityInputIdV1, OutputSlotIdV1, OwnerV1, PaintIdV1, PaintValueV1, PhysicalPointV1, PresentationRootIdV1, @@ -115,7 +115,7 @@ fn assert_evidence_snapshot(view: EvidenceViewV1<'_>) { let _ = cell.case_index(); let _ = cell.constraint().value(); match cell.subject() { - ConstraintSubjectV1::ModeledOccurrence { + ConstraintSubjectV1::VisibleUnary { occurrence, context, } => { @@ -131,6 +131,16 @@ fn assert_evidence_snapshot(view: EvidenceViewV1<'_>) { let _ = occurrence.value(); let _ = terminal.value(); } + ConstraintSubjectV1::IntrinsicUnary { target } => { + let _ = target.value(); + } + ConstraintSubjectV1::IntrinsicRelation { reference } => { + let _ = reference.value(); + } + ConstraintSubjectV1::VisibleRelation { reference, context } => { + let _ = reference.value(); + let _ = context.adapting_luminance_cd_m2(); + } } let _ = cell.mode(); let assessment = cell.assessment(); @@ -154,6 +164,14 @@ fn assert_evidence_snapshot(view: EvidenceViewV1<'_>) { let _ = evidence.rejected_blue_interval(); None } + AssessmentV1::IntrinsicUnary(evidence) => { + let _ = evidence.verdict(); + None + } + AssessmentV1::Relation(evidence) => { + let _ = evidence.member_count(); + None + } }; if let Some(binding) = binding { let PhysicalPointV1::EncodedSrgb8SourceOver(physical) = binding.physical(); @@ -260,8 +278,8 @@ fn fixed_nested_draft( derived_surface, context, ); - draft.push_exact_hard(exact, NESTED_INNER_OCCURRENCE, Srgb8::new([0; 3])); - draft.push_wcag22_report_only( + draft.push_exact_visible_unary_hard(exact, NESTED_INNER_OCCURRENCE, Srgb8::new([0; 3])); + draft.push_wcag22_visible_unary_report_only( wcag, NESTED_TERMINAL_OCCURRENCE, Wcag22CriterionV1::Sc143TextDefault, @@ -422,7 +440,7 @@ fn attach_target_assessment(draft: &mut DraftV1, target: TargetIdV1) { let context = AppearanceContextV1::try_new(64.0, 0.2, SurroundV1::Average).unwrap(); draft.push_solid_paint(paint, target); draft.push_source_over_occurrence(occurrence, paint, NESTED_INPUT_SURFACE, context); - draft.push_exact_report_only(constraint, occurrence, Srgb8::new([0; 3])); + draft.push_exact_visible_unary_report_only(constraint, occurrence, Srgb8::new([0; 3])); } fn joint_draft(hard: bool) -> DraftV1 { @@ -456,9 +474,9 @@ fn joint_draft(hard: bool) -> DraftV1 { draft.push_input_surface(surface, input); draft.push_source_over_occurrence(occurrence, paint, surface, context); if hard { - draft.push_exact_hard(constraint, occurrence, Srgb8::new([128; 3])); + draft.push_exact_visible_unary_hard(constraint, occurrence, Srgb8::new([128; 3])); } else { - draft.push_exact_report_only(constraint, occurrence, Srgb8::new([0; 3])); + draft.push_exact_visible_unary_report_only(constraint, occurrence, Srgb8::new([0; 3])); } draft.push_output(output, paint); draft @@ -501,7 +519,7 @@ fn finite_paint_candidates_are_not_a_cartesian_source_opacity_domain() { draft.push_solid_paint(paint, target); draft.push_input_surface(surface, input); draft.push_source_over_occurrence(occurrence, paint, surface, context); - draft.push_exact_hard(constraint, occurrence, Srgb8::new([0xFF; 3])); + draft.push_exact_visible_unary_hard(constraint, occurrence, Srgb8::new([0xFF; 3])); draft.push_output(output, paint); let owner = draft.compile().unwrap(); @@ -595,7 +613,7 @@ fn evidence_cell_bounds_are_independent_of_internal_causal_replay_storage() { draft.push_source_over_occurrence(inner, paint, input_surface, context); draft.push_occurrence_surface(derived_surface, inner); draft.push_source_over_occurrence(terminal, paint, derived_surface, context); - draft.push_exact_hard(constraint, terminal, Srgb8::new([0; 3])); + draft.push_exact_visible_unary_hard(constraint, terminal, Srgb8::new([0; 3])); draft.push_point_presentation_root(root, terminal); draft.push_point_presentation_target(root, inner); draft.push_output(output, paint); @@ -753,13 +771,13 @@ fn staged_authoring_lowers_the_actual_closed_program_and_returns_canonical_input draft.push_input_surface(low_surface, low_input); draft.push_source_over_occurrence(high_occurrence, paint, high_surface, context); draft.push_source_over_occurrence(low_occurrence, paint, low_surface, context); - draft.push_exact_report_only(exact, high_occurrence, Srgb8::new([0; 3])); - draft.push_wcag22_hard( + draft.push_exact_visible_unary_report_only(exact, high_occurrence, Srgb8::new([0; 3])); + draft.push_wcag22_visible_unary_hard( high_wcag, high_occurrence, Wcag22CriterionV1::Sc143TextDefault, ); - draft.push_wcag22_hard( + draft.push_wcag22_visible_unary_hard( low_wcag, low_occurrence, Wcag22CriterionV1::Sc143TextDefault, @@ -991,7 +1009,7 @@ fn owner_and_update_errors_preserve_content_and_input_identity() { let owner = fixed_nested_draft(1.0, SourceIdV1::new(1), input, input) .compile() .unwrap(); - let owner_identity: ContentIdentityV5 = owner.content_identity(); + let owner_identity: ContentIdentityV6 = owner.content_identity(); let mut session = owner.instantiate(13).unwrap(); let no_scenarios = []; @@ -1148,7 +1166,7 @@ fn certificate_and_set_retain_the_same_nonunit_opacity() { draft.push_opacity_paint(translucent, solid, opacity); draft.push_input_surface(surface, input); draft.push_source_over_occurrence(occurrence, translucent, surface, context); - draft.push_exact_hard(constraint, occurrence, Srgb8::new([0x80; 3])); + draft.push_exact_visible_unary_hard(constraint, occurrence, Srgb8::new([0x80; 3])); draft.push_output(output, translucent); let owner = draft.compile().unwrap(); diff --git a/crates/labcolors-core/src/program_clean_set_tests.rs b/crates/labcolors-core/src/program_clean_set_tests.rs index 758507e6..ecbca795 100644 --- a/crates/labcolors-core/src/program_clean_set_tests.rs +++ b/crates/labcolors-core/src/program_clean_set_tests.rs @@ -444,7 +444,7 @@ fn clean_family_fresh_recheck_failure_retains_the_presentation_subject() { ); } -fn opaque_named_clean_identity(name: u32) -> program::ContentIdentityV5 { +fn opaque_named_clean_identity(name: u32) -> program::ContentIdentityV6 { let source = program::SourceIdV1::new(name); let target = program::TargetIdV1::new(name); let port = program::SurfaceInputPortIdV1::new(name); diff --git a/crates/labcolors-core/src/program_identity.rs b/crates/labcolors-core/src/program_identity.rs index efef885e..76e4b769 100644 --- a/crates/labcolors-core/src/program_identity.rs +++ b/crates/labcolors-core/src/program_identity.rs @@ -7,15 +7,16 @@ use super::*; -const DOMAIN_V5: &[u8] = b"labcolors.program-content-identity.v5\0"; -// Максимальный V5-цвет принадлежит ограничению clean-set: тег вершины, -// семейство и полный дайджест выпуска. Явная граница устраняет аллокацию на -// каждую вершину и требует пересмотра при расширении схемы вместо скрытого -// лимита времени исполнения. -const COLOR_CAPACITY: usize = 1 + 1 + 32; +const DOMAIN_V6: &[u8] = b"labcolors.program-content-identity.v6\0"; +// V6 резервирует фиксированную protocol boundary под два тега и полный +// clean-set release digest. Это не ручный список размеров остальных вариантов: +// каждый writer использует проверяемый push и отвергает расширение схемы вместо +// усечения данных или аллокации на вершину. +const CLEAN_SET_COLOR_BYTES_V1: usize = 1 + 1 + 32; +const COLOR_CAPACITY: usize = CLEAN_SET_COLOR_BYTES_V1; mod release_tag { - pub(super) const PROGRAM_SCHEMA_V5: u8 = 5; + pub(super) const PROGRAM_SCHEMA_V6: u8 = 6; pub(super) const DECLARED_TOTAL_ORDER_V1: u8 = 1; pub(super) const FRESH_FULL_RECHECK_V1: u8 = 1; pub(super) const ATOMIC_OBSERVATION_GROUP_V1: u8 = 1; @@ -47,6 +48,14 @@ mod release_tag { pub(super) const EXACT_SRGB8_IDENTITY_V1: u8 = 1; pub(super) const EXACT_SRGB8_RELEASE_V1: u8 = 1; pub(super) const EXACT_SRGB8_CAPABILITY_V1: u8 = 1; + pub(super) const EXACT_SRGB8_INTRINSIC_UNARY_FAMILY_V1: u8 = 4; + pub(super) const EXACT_SRGB8_INTRINSIC_UNARY_IDENTITY_V1: u8 = 1; + pub(super) const EXACT_SRGB8_INTRINSIC_UNARY_RELEASE_V1: u8 = 1; + pub(super) const EXACT_SRGB8_INTRINSIC_UNARY_CAPABILITY_V1: u8 = 1; + pub(super) const EXACT_SRGB8_RELATION_FAMILY_V1: u8 = 5; + pub(super) const EXACT_SRGB8_RELATION_IDENTITY_V1: u8 = 1; + pub(super) const EXACT_SRGB8_RELATION_RELEASE_V1: u8 = 1; + pub(super) const EXACT_SRGB8_RELATION_CAPABILITY_V1: u8 = 1; #[cfg(test)] pub(super) const EXACT_SRGB8_IDENTITY_MUTATION_SENTINEL_V1: u8 = 2; #[cfg(test)] @@ -63,17 +72,17 @@ mod release_tag { pub(super) const WCAG22_SC_1_4_11_GRAPHICAL_OBJECT: u8 = 4; pub(super) const DECLARED_SRGB8_CLEAN_SET_FAMILY_V1: u8 = 3; #[cfg(test)] - pub(super) const MODELED_LCS_PROBE_FAMILY_V1: u8 = 4; + pub(super) const MODELED_LCS_PROBE_FAMILY_V1: u8 = 6; } -/// Устойчивый к коллизиям адрес канонизированного содержимого Program V5. +/// Устойчивый к коллизиям адрес канонизированного содержимого Program V6. /// /// SHA-256 не делает адрес инъективным. Адрес не связывает пространства opaque /// ID и не подтверждает владельца, поколение либо revision. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub(crate) struct ProgramContentIdentityV5([u8; 32]); +pub(crate) struct ProgramContentIdentityV6([u8; 32]); -impl ProgramContentIdentityV5 { +impl ProgramContentIdentityV6 { pub(crate) const fn as_bytes(&self) -> &[u8; 32] { &self.0 } @@ -126,6 +135,12 @@ impl VertexColorV1 { fn as_slice(&self) -> &[u8] { &self.bytes[..usize::from(self.len)] } + + const fn tag(&self) -> u8 { + // `new` делает tag частью типа вершины до любого fallible writer-а; + // фиксированный backing array поэтому не требует slice-индексации. + self.bytes[0] + } } mod vertex_tag { @@ -152,9 +167,20 @@ mod vertex_tag { pub(super) const PRESENTATION_TARGET: u8 = 21; } -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -#[repr(u8)] -enum EdgeRoleV1 { +macro_rules! declare_edge_roles_v1 { + ($($name:ident = $value:literal),+ $(,)?) => { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] + #[repr(u8)] + enum EdgeRoleV1 { + $($name = $value),+ + } + + #[cfg(test)] + const EDGE_ROLES_V1: &[EdgeRoleV1] = &[$(EdgeRoleV1::$name),+]; + }; +} + +declare_edge_roles_v1! { ProgramMember = 1, TargetSource = 2, TargetCandidate = 3, @@ -166,7 +192,7 @@ enum EdgeRoleV1 { DerivedSurfaceOccurrence = 9, OccurrenceSubjectPaint = 10, OccurrenceBackdropSurface = 11, - ConstraintOccurrence = 12, + VisibleUnary = 12, OutputPaint = 13, SelectionState = 14, StateChoice = 15, @@ -176,6 +202,12 @@ enum EdgeRoleV1 { PresentationTargetRoot = 19, PresentationTargetOccurrence = 20, ConstraintPresentationTarget = 21, + IntrinsicUnary = 22, + IntrinsicReference = 23, + IntrinsicCandidate = 24, + VisibleReference = 25, + VisibleCandidate = 26, + ObservationGroup = 27, } #[derive(Debug, Clone, Copy)] @@ -338,7 +370,7 @@ fn program_root_color() -> Result { // производных возможностей связываются только с теми ограничениями, // которые их исполняют. for release in [ - release_tag::PROGRAM_SCHEMA_V5, + release_tag::PROGRAM_SCHEMA_V6, release_tag::DECLARED_TOTAL_ORDER_V1, release_tag::FRESH_FULL_RECHECK_V1, release_tag::ATOMIC_OBSERVATION_GROUP_V1, @@ -521,6 +553,52 @@ fn constraint_color( })?; color.push_u8(wcag_criterion_tag(criterion))?; } + ProgramConstraintContentV1::ExactSrgb8IntrinsicUnary { + identity, + release, + capability, + expected, + } => { + color.push_u8(release_tag::EXACT_SRGB8_INTRINSIC_UNARY_FAMILY_V1)?; + color.push_u8(match identity { + crate::constraints::ExactSrgb8IntrinsicUnaryIdentityV1::SourceEqualityV1 => { + release_tag::EXACT_SRGB8_INTRINSIC_UNARY_IDENTITY_V1 + } + })?; + color.push_u8(match release { + crate::constraints::ExactSrgb8IntrinsicUnaryReleaseV1::V1 => { + release_tag::EXACT_SRGB8_INTRINSIC_UNARY_RELEASE_V1 + } + })?; + color.push_u8(match capability { + crate::constraints::ExactSrgb8IntrinsicUnaryCapabilityV1::EncodedSrgb8PaintSourceV1 => { + release_tag::EXACT_SRGB8_INTRINSIC_UNARY_CAPABILITY_V1 + } + })?; + color.push_srgb8(expected)?; + } + ProgramConstraintContentV1::ExactSrgb8Relation { + identity, + release, + capability, + } => { + color.push_u8(release_tag::EXACT_SRGB8_RELATION_FAMILY_V1)?; + color.push_u8(match identity { + crate::constraints::ExactSrgb8RelationIdentityV1::HomogeneousEndpointEqualityV1 => { + release_tag::EXACT_SRGB8_RELATION_IDENTITY_V1 + } + })?; + color.push_u8(match release { + crate::constraints::ExactSrgb8RelationReleaseV1::V1 => { + release_tag::EXACT_SRGB8_RELATION_RELEASE_V1 + } + })?; + color.push_u8(match capability { + crate::constraints::ExactSrgb8RelationCapabilityV1::HomogeneousEncodedSrgb8PairV1 => { + release_tag::EXACT_SRGB8_RELATION_CAPABILITY_V1 + } + })?; + } #[cfg(test)] ProgramConstraintContentV1::ModeledLcsProbe { release } => { color.push_u8(release_tag::MODELED_LCS_PROBE_FAMILY_V1)?; @@ -558,6 +636,15 @@ fn declared_srgb8_clean_set_constraint_color( ) } +#[cfg(test)] +fn clean_set_final_recheck_mutant_release_v1() -> [u8; 32] { + let mut release = crate::clean_set::EXACT_NOMINAL_SRGB8_CLEAN_SET_RELEASE_SHA256_V1; + // Инъекция меняет один заранее объявленный бит: тест доказывает, что + // final-recheck связывает новый release, а не случайно другой дайджест. + release[0] ^= 1; + release +} + fn declared_srgb8_clean_set_constraint_color_for_release( mode_tag: u8, release: [u8; 32], @@ -587,44 +674,110 @@ fn presentation_target_vertex( Ok(vertex) } +struct ConstraintGraphBindingContextV1<'a, Evaluation> { + evaluator: &'a Evaluation, + presentation_targets: &'a [(PointPresentationTargetV1, usize)], + targets: &'a IdIndexV1, + occurrences: &'a IdIndexV1, + observation_group: usize, +} + fn add_constraint_graph_binding( graph: &mut GraphBuilderV1, - evaluator: &Evaluation, - presentation_targets: &[(PointPresentationTargetV1, usize)], - occurrences: &IdIndexV1, + context: &ConstraintGraphBindingContextV1<'_, Evaluation>, mode_tag: u8, - body: ProgramConstraintBodyV1>, + body: &ProgramConstraintBodyV1>, ) -> Result<(), ProgramCompileError> where Evaluation: ProgramConstraintEvaluatorSetV1, { - let (color, target, role) = match body { - ProgramConstraintBodyV1::ModeledOccurrence { - occurrence, - invocation, - } => ( - constraint_color(mode_tag, evaluator.constraint_content(invocation))?, - occurrences.get(occurrence)?, - EdgeRoleV1::ConstraintOccurrence, - ), - ProgramConstraintBodyV1::DeclaredSrgb8CleanSet { target } => ( - declared_srgb8_clean_set_constraint_color(mode_tag)?, - presentation_target_vertex(presentation_targets, target)?, - EdgeRoleV1::ConstraintPresentationTarget, - ), + let color = match body { + ProgramConstraintBodyV1::VisibleUnary { invocation, .. } => { + constraint_color(mode_tag, context.evaluator.constraint_content(*invocation))? + } + ProgramConstraintBodyV1::IntrinsicUnary { invocation, .. } => { + constraint_color(mode_tag, invocation.content())? + } + ProgramConstraintBodyV1::IntrinsicRelation { invocation, .. } + | ProgramConstraintBodyV1::VisibleRelation { invocation, .. } => { + constraint_color(mode_tag, invocation.content())? + } + ProgramConstraintBodyV1::DeclaredSrgb8CleanSet { .. } => { + declared_srgb8_clean_set_constraint_color(mode_tag)? + } #[cfg(test)] - ProgramConstraintBodyV1::DeclaredSrgb8CleanSetFinalRecheckMutant { target } => { - let mut release = crate::clean_set::EXACT_NOMINAL_SRGB8_CLEAN_SET_RELEASE_SHA256_V1; - release[0] ^= 1; - ( - declared_srgb8_clean_set_constraint_color_for_release(mode_tag, release)?, - presentation_target_vertex(presentation_targets, target)?, - EdgeRoleV1::ConstraintPresentationTarget, - ) + ProgramConstraintBodyV1::DeclaredSrgb8CleanSetFinalRecheckMutant { .. } => { + declared_srgb8_clean_set_constraint_color_for_release( + mode_tag, + clean_set_final_recheck_mutant_release_v1(), + )? } }; let vertex = graph.add_member(color)?; - graph.add_edge(vertex, target, role) + match body { + ProgramConstraintBodyV1::VisibleUnary { occurrence, .. } => { + graph.add_edge( + vertex, + context.occurrences.get(*occurrence)?, + EdgeRoleV1::VisibleUnary, + )?; + } + ProgramConstraintBodyV1::IntrinsicUnary { target, .. } => { + graph.add_edge( + vertex, + context.targets.get(*target)?, + EdgeRoleV1::IntrinsicUnary, + )?; + } + ProgramConstraintBodyV1::IntrinsicRelation { relation, .. } => { + graph.add_edge( + vertex, + context.targets.get(relation.reference())?, + EdgeRoleV1::IntrinsicReference, + )?; + for candidate in relation.candidates().iter().copied() { + graph.add_edge( + vertex, + context.targets.get(candidate)?, + EdgeRoleV1::IntrinsicCandidate, + )?; + } + } + ProgramConstraintBodyV1::VisibleRelation { relation, .. } => { + graph.add_edge( + vertex, + context.occurrences.get(relation.reference())?, + EdgeRoleV1::VisibleReference, + )?; + for candidate in relation.candidates().iter().copied() { + graph.add_edge( + vertex, + context.occurrences.get(candidate)?, + EdgeRoleV1::VisibleCandidate, + )?; + } + } + ProgramConstraintBodyV1::DeclaredSrgb8CleanSet { target } => { + graph.add_edge( + vertex, + presentation_target_vertex(context.presentation_targets, *target)?, + EdgeRoleV1::ConstraintPresentationTarget, + )?; + } + #[cfg(test)] + ProgramConstraintBodyV1::DeclaredSrgb8CleanSetFinalRecheckMutant { target } => { + graph.add_edge( + vertex, + presentation_target_vertex(context.presentation_targets, *target)?, + EdgeRoleV1::ConstraintPresentationTarget, + )?; + } + } + graph.add_edge( + vertex, + context.observation_group, + EdgeRoleV1::ObservationGroup, + ) } fn build_graph( @@ -817,24 +970,27 @@ where )?; } + let constraint_context = ConstraintGraphBindingContextV1 { + evaluator: &program.evaluator, + presentation_targets: &presentation_targets, + targets: &targets, + occurrences: &occurrences, + observation_group: group, + }; for constraint in &program.constraints.hard { add_constraint_graph_binding( &mut graph, - &program.evaluator, - &presentation_targets, - &occurrences, + &constraint_context, vertex_tag::CONSTRAINT_HARD, - *constraint.body(), + constraint.body(), )?; } for constraint in &program.constraints.report_only { add_constraint_graph_binding( &mut graph, - &program.evaluator, - &presentation_targets, - &occurrences, + &constraint_context, vertex_tag::CONSTRAINT_REPORT_ONLY, - *constraint.body(), + constraint.body(), )?; } for output in &program.outputs { @@ -872,6 +1028,45 @@ where graph.finish() } +#[cfg(test)] +pub(crate) fn graph_schema_for_test( + program: &Program, +) -> Result<(Vec, Vec), ProgramCompileError> +where + Evaluation: ProgramConstraintEvaluatorSetV1, + ProgramConstraintInvocationOf: Copy, +{ + let graph = build_graph(program)?; + let mut vertex_tags = Vec::new(); + vertex_tags + .try_reserve_exact(graph.colors.len()) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + vertex_tags.extend(graph.colors.iter().map(VertexColorV1::tag)); + vertex_tags.sort_unstable(); + vertex_tags.dedup(); + + let mut edge_roles = Vec::new(); + edge_roles + .try_reserve_exact(graph.edge_count) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + for adjacency in &graph.adjacency { + edge_roles.extend( + adjacency + .iter() + .filter(|arc| arc.direction == 0) + .map(|arc| arc.role as u8), + ); + } + edge_roles.sort_unstable(); + edge_roles.dedup(); + Ok((vertex_tags, edge_roles)) +} + +#[cfg(test)] +pub(crate) const fn edge_role_count_for_test() -> usize { + EDGE_ROLES_V1.len() +} + struct PartitionV1 { cells: Vec>, } @@ -1137,7 +1332,7 @@ fn serialize_leaf( .and_then(|value| value.checked_add(color.as_slice().len())) .ok_or(ProgramCompileError::ResourceExhausted) })?; - let capacity = DOMAIN_V5 + let capacity = DOMAIN_V6 .len() .checked_add(16) .and_then(|value| value.checked_add(color_bytes)) @@ -1147,7 +1342,7 @@ fn serialize_leaf( output .try_reserve_exact(capacity) .map_err(|_| ProgramCompileError::ResourceExhausted)?; - output.extend_from_slice(DOMAIN_V5); + output.extend_from_slice(DOMAIN_V6); push_u64_bytes(&mut output, usize_as_u64(graph.colors.len())?); push_u64_bytes(&mut output, usize_as_u64(graph.edge_count)?); @@ -1481,9 +1676,9 @@ fn canonical_preimage(graph: &CanonicalGraphV1) -> Result, ProgramCompil canonical_search(graph).map(|(preimage, _)| preimage) } -pub(super) fn compile_program_content_identity_v5( +pub(super) fn compile_program_content_identity_v6( program: &Program, -) -> Result +) -> Result where Evaluation: ProgramConstraintEvaluatorSetV1, ProgramConstraintInvocationOf: Copy, @@ -1491,13 +1686,22 @@ where let graph = build_graph(program)?; let preimage = canonical_preimage(&graph)?; let digest = crate::sha256::digest(&preimage); - Ok(ProgramContentIdentityV5(*digest.as_bytes())) + Ok(ProgramContentIdentityV6(*digest.as_bytes())) } #[cfg(test)] mod tests { use super::*; + #[test] + fn clean_set_final_recheck_mutant_release_changes_exactly_one_declared_bit() { + let source = crate::clean_set::EXACT_NOMINAL_SRGB8_CLEAN_SET_RELEASE_SHA256_V1; + let mutant = clean_set_final_recheck_mutant_release_v1(); + + assert_eq!(mutant[0], source[0] ^ 1); + assert_eq!(&mutant[1..], &source[1..]); + } + #[test] fn declared_clean_set_constraint_color_binds_every_release_digest_byte() { let release = crate::clean_set::EXACT_NOMINAL_SRGB8_CLEAN_SET_RELEASE_SHA256_V1; diff --git a/crates/labcolors-core/src/program_identity_tests.rs b/crates/labcolors-core/src/program_identity_tests.rs index 948e97b2..dc085405 100644 --- a/crates/labcolors-core/src/program_identity_tests.rs +++ b/crates/labcolors-core/src/program_identity_tests.rs @@ -13,10 +13,11 @@ use crate::program_session::{ CoreProgramConstraintInvocationV1, CoreProgramEvaluatorsV1, CoreProgramV1, DeclaredJointSelectionV1, FinitePaintDomainV1, JointCandidateStateV1, ObservationGroup, Occurrence, OpacityInput, OutputBinding, OutputSlotId, Paint, PointPresentationRootV1, - PointPresentationTargetV1, PresentationRootId, Program, ProgramContentIdentityV5, Source, + PointPresentationTargetV1, PresentationRootId, Program, ProgramContentIdentityV6, Source, SourceId, Surface, Target, TargetCandidateChoiceV1, TargetCandidateId, TargetCandidateV1, TargetId, }; +use crate::relation::DirectedRelationV1; use crate::wcag22::Wcag22CriterionV1; fn signal(value: [u8; 3]) -> ColorSignal { @@ -133,12 +134,12 @@ fn fixed_program( ), ]; let mut hard = vec![ - ConstraintInvocation::hard( + ConstraintInvocation::visible_unary_hard( ids.constraints[0], ids.occurrences[0], CoreProgramConstraintInvocationV1::ExactSrgb8(Srgb8::new([0x10, 0x20, 0x30])), ), - ConstraintInvocation::hard( + ConstraintInvocation::visible_unary_hard( ids.constraints[1], ids.occurrences[1], CoreProgramConstraintInvocationV1::ExactSrgb8(second_signal), @@ -273,7 +274,7 @@ fn presentation_topology_ignores_root_names_and_declaration_order() { } #[test] -fn presentation_root_terminal_relation_changes_v5_identity() { +fn presentation_root_terminal_relation_changes_v6_identity() { let ids = canonical_full_ids(); let root = PresentationRootId::new(1); let first = full_program(ids, false, FullMutation::None) @@ -294,7 +295,7 @@ fn presentation_root_terminal_relation_changes_v5_identity() { } #[test] -fn presentation_target_relation_and_multiplicity_change_v5_identity() { +fn presentation_target_relation_and_multiplicity_change_v6_identity() { let ids = canonical_full_ids(); let root = PresentationRootId::new(1); let nested_target = full_program(ids, false, FullMutation::None) @@ -336,7 +337,7 @@ fn presentation_target_relation_and_multiplicity_change_v5_identity() { } #[test] -fn canonical_v5_digest_is_cross_platform_golden() { +fn canonical_v6_digest_is_cross_platform_golden() { let ids = FixedIds { sources: [SourceId::new(10), SourceId::new(20)], targets: [TargetId::new(30), TargetId::new(40)], @@ -357,12 +358,12 @@ fn canonical_v5_digest_is_cross_platform_golden() { .compile() .unwrap(); - let identity: ProgramContentIdentityV5 = compiled.content_identity(); + let identity: ProgramContentIdentityV6 = compiled.content_identity(); assert_eq!( identity.as_bytes(), &[ - 238, 31, 74, 246, 76, 225, 22, 249, 250, 182, 134, 250, 248, 170, 128, 78, 215, 239, - 212, 220, 176, 186, 208, 196, 83, 238, 38, 240, 187, 77, 186, 122, + 22, 145, 115, 252, 75, 115, 120, 67, 104, 170, 31, 25, 152, 105, 250, 250, 144, 214, + 124, 251, 74, 89, 154, 255, 6, 58, 32, 21, 25, 171, 71, 214, ] ); } @@ -549,7 +550,7 @@ fn full_program(ids: FullIds, reverse_unordered: bool, mutation: FullMutation) - context(SurroundProfileId::DarkV1), ), ]; - let mut hard = vec![ConstraintInvocation::hard( + let mut hard = vec![ConstraintInvocation::visible_unary_hard( ids.constraints[0], if matches!(mutation, FullMutation::ConstraintTarget) { ids.occurrences[1] @@ -571,39 +572,84 @@ fn full_program(ids: FullIds, reverse_unordered: bool, mutation: FullMutation) - }; let mut report_only = Vec::new(); if matches!(mutation, FullMutation::ConstraintMode) { - report_only.push(ConstraintInvocation::report_only( + report_only.push(ConstraintInvocation::visible_unary_report_only( ids.constraints[1], ids.occurrences[1], second_invocation, )); } else { - hard.push(ConstraintInvocation::hard( + hard.push(ConstraintInvocation::visible_unary_hard( ids.constraints[1], ids.occurrences[1], second_invocation, )); } if matches!(mutation, FullMutation::CompleteSchemaGolden) { - report_only.push(ConstraintInvocation::report_only( + report_only.push(ConstraintInvocation::visible_unary_report_only( ConstraintId::new(1_001), ids.occurrences[2], CoreProgramConstraintInvocationV1::Wcag22Srgb8( Wcag22CriterionV1::Sc1411GraphicalObject, ), )); + report_only.push(ConstraintInvocation::visible_unary_report_only( + ConstraintId::new(1_010), + ids.occurrences[2], + CoreProgramConstraintInvocationV1::ExactSrgb8(Srgb8::new([0x21, 0x32, 0x43])), + )); + hard.push(ConstraintInvocation::visible_unary_hard( + ConstraintId::new(1_011), + ids.occurrences[2], + CoreProgramConstraintInvocationV1::Wcag22Srgb8( + Wcag22CriterionV1::Sc1411UiComponentOrState, + ), + )); } - hard.push(ConstraintInvocation::hard( + hard.push(ConstraintInvocation::visible_unary_hard( ids.constraints[2], ids.occurrences[2], CoreProgramConstraintInvocationV1::ExactSrgb8(Srgb8::new([0x21, 0x32, 0x43])), )); if matches!(mutation, FullMutation::ConstraintMultiplicity) { - hard.push(ConstraintInvocation::hard( + hard.push(ConstraintInvocation::visible_unary_hard( ConstraintId::new(1_000), ids.occurrences[1], CoreProgramConstraintInvocationV1::ExactSrgb8(Srgb8::new([0x44, 0x55, 0x66])), )); } + if matches!(mutation, FullMutation::CompleteSchemaGolden) { + let fixed_target = TargetId::new(1_003); + let fixed_occurrence = OccurrenceId::new(1_005); + targets.push(Target::fixed(fixed_target, ids.sources[0])); + paints.push(Paint::Solid { + id: PaintId::new(1_004), + target: fixed_target, + }); + occurrences.push(Occurrence::new( + fixed_occurrence, + PaintId::new(1_004), + ids.surfaces[0], + CompositionProfile::EncodedSrgb8SourceOverV1, + context(SurroundProfileId::AverageV1), + )); + hard.push(ConstraintInvocation::exact_intrinsic_unary_hard( + ConstraintId::new(1_006), + ids.targets[0], + candidate_signals[0][0].srgb8(), + )); + hard.push(ConstraintInvocation::exact_intrinsic_relation_hard( + ConstraintId::new(1_007), + DirectedRelationV1::try_new(fixed_target, vec![ids.targets[0]]).unwrap(), + )); + hard.push(ConstraintInvocation::exact_visible_relation_hard( + ConstraintId::new(1_008), + DirectedRelationV1::try_new(fixed_occurrence, vec![ids.occurrences[0]]).unwrap(), + )); + hard.push(ConstraintInvocation::declared_srgb8_clean_set_hard( + ConstraintId::new(1_009), + PointPresentationTargetV1::new(PresentationRootId::new(1_002), ids.occurrences[0]), + )); + } let mut outputs = vec![ OutputBinding::new( ids.outputs[0], @@ -719,9 +765,9 @@ fn canonical_full_ids() -> FullIds { } #[test] -fn complete_program_schema_v5_digest_is_cross_platform_golden() { - // Вместе с fixed golden этот Program содержит каждый V5 vertex/edge tag, - // обе constraint families и оба режима. Случайная смена кодировки требует +fn complete_program_schema_v6_digest_is_cross_platform_golden() { + // Вместе с fixed golden этот Program содержит каждый V6 vertex/edge tag, + // все constraint topology и оба режима. Случайная смена кодировки требует // явной смены версии, а не тихого перевыпуска прежнего content address. let compiled = full_program( canonical_full_ids(), @@ -731,11 +777,25 @@ fn complete_program_schema_v5_digest_is_cross_platform_golden() { .compile() .unwrap(); + let schema = crate::program_session::program_identity_graph_schema_for_test(&full_program( + canonical_full_ids(), + false, + FullMutation::CompleteSchemaGolden, + )) + .unwrap(); + assert_eq!(schema.0, (1_u8..=21).collect::>()); + let edge_role_count = crate::program_session::program_identity_edge_role_count_for_test(); + assert_eq!(schema.1.len(), edge_role_count); + assert_eq!( + schema.1, + (1_u8..=u8::try_from(edge_role_count).unwrap()).collect::>() + ); + assert_eq!( compiled.content_identity().as_bytes(), &[ - 96, 211, 110, 208, 129, 129, 17, 133, 60, 202, 30, 74, 62, 176, 156, 216, 200, 37, 22, - 103, 116, 138, 153, 152, 50, 46, 193, 238, 135, 21, 22, 38, + 221, 253, 12, 128, 175, 69, 147, 163, 56, 186, 249, 2, 80, 29, 40, 110, 150, 16, 126, + 147, 176, 229, 2, 154, 50, 91, 7, 120, 82, 36, 247, 174, ] ); } @@ -903,7 +963,7 @@ fn nested_surface_program( .copied() .enumerate() .map(|(index, occurrence)| { - ConstraintInvocation::hard( + ConstraintInvocation::visible_unary_hard( ConstraintId::new(14 + index as u32), occurrence, CoreProgramConstraintInvocationV1::ExactSrgb8(Srgb8::new([ @@ -1009,7 +1069,7 @@ fn paint_shape_program(shape: SubjectPaintShape) -> CoreProgramV1 { context(SurroundProfileId::AverageV1), )], ConstraintSet::new( - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( ConstraintId::new(13), occurrence, CoreProgramConstraintInvocationV1::ExactSrgb8(Srgb8::new([0x20, 0x30, 0x40])), @@ -1101,12 +1161,12 @@ fn source_alias_program(shared: bool) -> CoreProgramV1 { ], ConstraintSet::new( vec![ - ConstraintInvocation::hard( + ConstraintInvocation::visible_unary_hard( ConstraintId::new(12), occurrences[0], CoreProgramConstraintInvocationV1::ExactSrgb8(Srgb8::new([0x30, 0x40, 0x50])), ), - ConstraintInvocation::hard( + ConstraintInvocation::visible_unary_hard( ConstraintId::new(13), occurrences[1], CoreProgramConstraintInvocationV1::ExactSrgb8(Srgb8::new([0x30, 0x40, 0x50])), @@ -1172,7 +1232,7 @@ fn finite_program_with_opacity(reverse_order: bool, first_opacity: f64) -> CoreP context(SurroundProfileId::AverageV1), )], ConstraintSet::new( - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( ConstraintId::new(10), occurrence, CoreProgramConstraintInvocationV1::ExactSrgb8(Srgb8::new([0; 3])), @@ -1231,7 +1291,7 @@ fn finite_program_with_permuted_source_colors(swap_source_signals: bool) -> Core context(SurroundProfileId::AverageV1), )], ConstraintSet::new( - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( ConstraintId::new(11), occurrence, CoreProgramConstraintInvocationV1::ExactSrgb8(Srgb8::new([0; 3])), @@ -1289,7 +1349,7 @@ fn fixed_single_target_program() -> CoreProgramV1 { context(SurroundProfileId::AverageV1), )], ConstraintSet::new( - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( ConstraintId::new(10), occurrence, CoreProgramConstraintInvocationV1::ExactSrgb8(Srgb8::new([0; 3])), @@ -1318,7 +1378,7 @@ fn content_identity_retains_the_explicit_joint_state_order() { } #[test] -fn finite_candidate_opacity_is_part_of_v5_content_identity() { +fn finite_candidate_opacity_is_part_of_v6_content_identity() { let quarter = finite_program_with_opacity(false, 0.25).compile().unwrap(); let half = finite_program_with_opacity(false, 0.5).compile().unwrap(); @@ -1376,7 +1436,7 @@ fn regular_incidence_program(kind: RegularIncidence) -> CoreProgramV1 { .iter() .enumerate() .map(|(index, occurrence)| { - ConstraintInvocation::hard( + ConstraintInvocation::visible_unary_hard( ConstraintId::new(60 + index as u32), occurrence.id(), CoreProgramConstraintInvocationV1::ExactSrgb8(Srgb8::new([0x20; 3])), diff --git a/crates/labcolors-core/src/program_joint_integration_tests.rs b/crates/labcolors-core/src/program_joint_integration_tests.rs index 530e0d46..c49b2777 100644 --- a/crates/labcolors-core/src/program_joint_integration_tests.rs +++ b/crates/labcolors-core/src/program_joint_integration_tests.rs @@ -301,6 +301,63 @@ impl ProgramConstraintEvaluatorSetV1 for FinalViolationDiagnosticErrorEvaluatorS } } +/// Один кандидат с четырьмя hard cells и report-only poison: search проходит +/// полностью, fresh recheck сохраняет PASS, затем три VIOLATION. Любой +/// вызов report-only возвращает VIOLATION, поэтому exact count доказывает +/// исключение диагностики из терминального hard-вердикта. +#[derive(Debug, Clone, Default)] +struct MultiViolationFinalRecheckEvaluatorSetV1 { + calls: std::rc::Rc>, +} + +impl MultiViolationFinalRecheckEvaluatorSetV1 { + fn calls(&self) -> usize { + self.calls.get() + } +} + +impl ProgramConstraintEvaluatorSetV1 for MultiViolationFinalRecheckEvaluatorSetV1 { + type Invocation = Srgb8; + type PassEvidence = DiagnosticPoisonPassV1; + type ViolationEvidence = DiagnosticPoisonViolationV1; + type Error = core::convert::Infallible; + + fn assess( + &self, + point: ProgramPointOccurrenceV1, + invocation: Self::Invocation, + ) -> Result< + HardDecision, + ProgramPointAssessmentErrorV1, + > { + let call = self.calls.get(); + self.calls.set(call + 1); + let binding = point.binding(); + if invocation == DIAGNOSTIC_INVOCATION { + return Ok(HardDecision::Violation(DiagnosticPoisonViolationV1( + binding, + ))); + } + Ok(match call { + 0..=4 => HardDecision::Pass(DiagnosticPoisonPassV1(binding)), + 5..=7 => HardDecision::Violation(DiagnosticPoisonViolationV1(binding)), + _ => unreachable!("fixture has exactly one four-cell search and recheck"), + }) + } + + fn pass_binding(evidence: &Self::PassEvidence) -> ProgramVisiblePointBindingV1 { + evidence.0 + } + + fn violation_binding(evidence: &Self::ViolationEvidence) -> ProgramVisiblePointBindingV1 { + evidence.0 + } + + fn constraint_content(&self, invocation: Self::Invocation) -> ProgramConstraintContentV1 { + ExactSrgb8IdentityV1.program_constraint_content_v1(invocation) + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum InjectedEvaluatorFailureV1 { CandidateSearch, @@ -632,7 +689,7 @@ fn nested_two_target_program( ), ], ConstraintSet::new( - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( ConstraintId::new(1), UPPER_OCCURRENCE, Wcag22CriterionV1::Sc143TextLargeScale, @@ -714,7 +771,7 @@ fn nested_alpha_exact_program() -> Program { ), ], ConstraintSet::new( - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( ConstraintId::new(1), UPPER_OCCURRENCE, Srgb8::new([0x80; 3]), @@ -829,7 +886,7 @@ fn alpha_renamed_nested_program( ), ], ConstraintSet::new( - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( ConstraintId::new(1), UPPER_OCCURRENCE, Wcag22CriterionV1::Sc143TextLargeScale, @@ -881,7 +938,7 @@ fn evaluator_unwind_does_not_consume_the_reusable_evaluation_arena() { let compiled = point_program( signal(0), Target::fixed(TARGET, SOURCE), - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( ConstraintId::new(1), OCCURRENCE, Wcag22CriterionV1::Sc143TextDefault, @@ -921,7 +978,7 @@ fn prepared_transition_unwind_returns_the_reusable_evaluation_arena() { let compiled = point_program( signal(0), Target::fixed(TARGET, SOURCE), - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( ConstraintId::new(1), OCCURRENCE, Wcag22CriterionV1::Sc143TextDefault, @@ -985,7 +1042,7 @@ fn terminal_safety_rejects_an_output_outside_every_assessment_cone() { appearance_context(), )], ConstraintSet::new( - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( ConstraintId::new(1), OCCURRENCE, Wcag22CriterionV1::Sc143TextLargeScale, @@ -1038,7 +1095,7 @@ fn terminal_safety_rejects_an_unconstrained_finite_target() { appearance_context(), )], ConstraintSet::new( - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( ConstraintId::new(1), UPPER_OCCURRENCE, Wcag22CriterionV1::Sc143TextLargeScale, @@ -1114,12 +1171,12 @@ fn independent_finite_target_components_are_rejected_before_global_product_searc ], ConstraintSet::new( vec![ - ConstraintInvocation::hard( + ConstraintInvocation::visible_unary_hard( ConstraintId::new(1), OCCURRENCE, Wcag22CriterionV1::Sc143TextLargeScale, ), - ConstraintInvocation::hard( + ConstraintInvocation::visible_unary_hard( ConstraintId::new(2), UPPER_OCCURRENCE, Wcag22CriterionV1::Sc143TextLargeScale, @@ -1151,12 +1208,12 @@ fn independent_finite_target_components_are_rejected_before_global_product_searc fn candidate_passing_one_hard_cell_and_failing_another_is_never_certified() { let compiled = program( vec![ - ConstraintInvocation::hard( + ConstraintInvocation::visible_unary_hard( ConstraintId::new(1), OCCURRENCE, Wcag22CriterionV1::Sc143TextLargeScale, ), - ConstraintInvocation::hard( + ConstraintInvocation::visible_unary_hard( ConstraintId::new(2), OCCURRENCE, Wcag22CriterionV1::Sc143TextDefault, @@ -1189,12 +1246,12 @@ fn candidate_passing_one_hard_cell_and_failing_another_is_never_certified() { #[test] fn report_only_violation_is_retained_but_does_not_select() { let compiled = program( - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( ConstraintId::new(1), OCCURRENCE, Wcag22CriterionV1::Sc143TextLargeScale, )], - vec![ConstraintInvocation::report_only( + vec![ConstraintInvocation::visible_unary_report_only( ConstraintId::new(2), OCCURRENCE, Wcag22CriterionV1::Sc143TextDefault, @@ -1223,7 +1280,7 @@ fn report_only_violation_is_retained_but_does_not_select() { fn report_only_assessment_admits_output_but_cannot_override_explicit_selection_order() { let compiled = program( vec![], - vec![ConstraintInvocation::report_only( + vec![ConstraintInvocation::visible_unary_report_only( ConstraintId::new(1), OCCURRENCE, Wcag22CriterionV1::Sc143TextDefault, @@ -1247,7 +1304,7 @@ fn report_only_assessment_admits_output_but_cannot_override_explicit_selection_o #[test] fn no_feasible_joint_state_commits_no_output_and_retains_previous_certificate() { let compiled = program( - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( ConstraintId::new(1), OCCURRENCE, Wcag22CriterionV1::Sc143TextLargeScale, @@ -1287,7 +1344,7 @@ fn no_feasible_joint_state_commits_no_output_and_retains_previous_certificate() fn candidate_declaration_permutation_preserves_explicit_physical_order() { let make = |candidates| { program( - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( ConstraintId::new(1), OCCURRENCE, Wcag22CriterionV1::Sc143TextLargeScale, @@ -1499,7 +1556,7 @@ fn rejected_state_runs_once_and_selected_state_runs_fresh_recheck_twice() { appearance_context(), )], ConstraintSet::new( - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( ConstraintId::new(1), OCCURRENCE, Wcag22CriterionV1::Sc143TextLargeScale, @@ -1543,7 +1600,7 @@ fn evaluator_error_aborts_both_candidate_search_and_fresh_recheck() { let compiled = point_program( signal(0), target(vec![candidate(FIRST, 0xFF)]), - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( constraint, OCCURRENCE, Srgb8::new([0xFF; 3]), @@ -1591,12 +1648,12 @@ fn report_evaluator_error_cannot_poison_candidate_search_or_change_selection() { let compiled = point_program( signal(0), target(vec![candidate(FIRST, 0x55), candidate(SECOND, 0xFF)]), - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( ConstraintId::new(1), OCCURRENCE, Wcag22CriterionV1::Sc143TextLargeScale, )], - vec![ConstraintInvocation::report_only( + vec![ConstraintInvocation::visible_unary_report_only( ConstraintId::new(2), OCCURRENCE, report_invocation, @@ -1639,12 +1696,12 @@ fn hard_conflict_runs_report_only_once_in_the_exhaustive_full_pass() { let compiled = point_program( signal(0), target(vec![candidate(FIRST, 0xAA), candidate(SECOND, 0xFF)]), - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( ConstraintId::new(1), OCCURRENCE, Wcag22CriterionV1::Sc143TextLargeScale, )], - vec![ConstraintInvocation::report_only( + vec![ConstraintInvocation::visible_unary_report_only( ConstraintId::new(2), OCCURRENCE, Wcag22CriterionV1::Sc143TextDefault, @@ -1687,7 +1744,7 @@ fn fixed_program_without_finite_targets_executes_one_complete_evidence_pass() { signal(0xFF), Target::fixed(TARGET, SOURCE), vec![], - vec![ConstraintInvocation::report_only( + vec![ConstraintInvocation::visible_unary_report_only( ConstraintId::new(1), OCCURRENCE, Wcag22CriterionV1::Sc143TextDefault, @@ -1717,12 +1774,12 @@ fn fixed_hard_conflict_still_collects_report_only_evidence() { let compiled = point_program( signal(0xAA), Target::fixed(TARGET, SOURCE), - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( hard, OCCURRENCE, Wcag22CriterionV1::Sc143TextLargeScale, )], - vec![ConstraintInvocation::report_only( + vec![ConstraintInvocation::visible_unary_report_only( report, OCCURRENCE, Wcag22CriterionV1::Sc143TextDefault, @@ -1767,12 +1824,12 @@ fn exhaustive_conflict_freezes_every_state_case_hard_cell_before_any_diagnostic( let compiled = point_program( signal(0), target(vec![candidate(FIRST, 0xAA), candidate(SECOND, 0xBB)]), - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( hard, OCCURRENCE, HARD_INVOCATION, )], - vec![ConstraintInvocation::report_only( + vec![ConstraintInvocation::visible_unary_report_only( report, OCCURRENCE, DIAGNOSTIC_INVOCATION, @@ -1826,7 +1883,7 @@ fn exhaustive_conflict_freezes_every_state_case_hard_cell_before_any_diagnostic( fn successful_search_allocations_do_not_scale_with_rejected_states() { let compile = |candidates, order| { program( - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( ConstraintId::new(1), OCCURRENCE, Wcag22CriterionV1::Sc143TextLargeScale, @@ -1910,7 +1967,7 @@ fn every_required_joint_arena_reservation_is_fail_before_work_and_retryable() { appearance_context(), )], ConstraintSet::new( - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( ConstraintId::new(1), OCCURRENCE, Wcag22CriterionV1::Sc143TextLargeScale, @@ -1991,7 +2048,7 @@ fn every_fallible_fixed_preflight_reservation_precedes_evaluator_work() { appearance_context(), )], ConstraintSet::new( - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( ConstraintId::new(1), OCCURRENCE, Wcag22CriterionV1::Sc143TextLargeScale, @@ -2081,7 +2138,7 @@ fn counting_fixed_program( appearance_context(), )], ConstraintSet::new( - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( ConstraintId::new(1), OCCURRENCE, Wcag22CriterionV1::Sc143TextLargeScale, @@ -2170,12 +2227,12 @@ fn lower_id_diagnostic_cannot_consume_the_selected_state_final_recheck() { let compiled = point_program( signal(0), target(vec![candidate(FIRST, 0xFF)]), - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( hard, OCCURRENCE, Srgb8::new([0xFF; 3]), )], - vec![ConstraintInvocation::report_only( + vec![ConstraintInvocation::visible_unary_report_only( ConstraintId::new(1), OCCURRENCE, Srgb8::new([0xFF; 3]), @@ -2202,7 +2259,7 @@ fn lower_id_diagnostic_cannot_consume_the_selected_state_final_recheck() { state_index: 0, case_index: 0, constraint: hard, - subject: ProgramConstraintSubjectV1::ModeledOccurrence { + subject: ProgramConstraintSubjectV1::VisibleUnary { occurrence: OCCURRENCE, context: appearance_context(), }, @@ -2219,12 +2276,12 @@ fn diagnostic_error_cannot_mask_a_selected_state_final_recheck_violation() { let compiled = point_program( signal(0), target(vec![candidate(FIRST, 0xFF)]), - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( hard, OCCURRENCE, HARD_INVOCATION, )], - vec![ConstraintInvocation::report_only( + vec![ConstraintInvocation::visible_unary_report_only( ConstraintId::new(2), OCCURRENCE, DIAGNOSTIC_INVOCATION, @@ -2246,7 +2303,7 @@ fn diagnostic_error_cannot_mask_a_selected_state_final_recheck_violation() { state_index: 0, case_index: 0, constraint: hard, - subject: ProgramConstraintSubjectV1::ModeledOccurrence { + subject: ProgramConstraintSubjectV1::VisibleUnary { occurrence: OCCURRENCE, context: appearance_context(), }, @@ -2256,6 +2313,72 @@ fn diagnostic_error_cannot_mask_a_selected_state_final_recheck_violation() { assert_eq!(probe.diagnostic_calls(), 0); } +#[test] +fn final_recheck_reports_only_hard_violations_and_their_exact_count() { + let evaluator = MultiViolationFinalRecheckEvaluatorSetV1::default(); + let probe = evaluator.clone(); + let first_violation = ConstraintId::new(2); + let compiled = point_program( + signal(0), + target(vec![candidate(FIRST, 0xFF)]), + vec![ + ConstraintInvocation::visible_unary_hard( + ConstraintId::new(1), + OCCURRENCE, + Srgb8::new([1; 3]), + ), + ConstraintInvocation::visible_unary_hard( + first_violation, + OCCURRENCE, + Srgb8::new([2; 3]), + ), + ConstraintInvocation::visible_unary_hard( + ConstraintId::new(3), + OCCURRENCE, + Srgb8::new([3; 3]), + ), + ConstraintInvocation::visible_unary_hard( + ConstraintId::new(4), + OCCURRENCE, + Srgb8::new([4; 3]), + ), + ], + vec![ConstraintInvocation::visible_unary_report_only( + ConstraintId::new(5), + OCCURRENCE, + DIAGNOSTIC_INVOCATION, + )], + evaluator, + ) + .with_joint_selection(DeclaredJointSelectionV1::new(vec![state(FIRST)])) + .compile() + .unwrap(); + let mut session = compiled.instantiate(STREAM).unwrap(); + + let error = match session.commit(update(1, 0x00)) { + Ok(_) => panic!("three failures in the fresh recheck must reject the selected state"), + Err(error) => error, + }; + assert_eq!( + error, + SessionUpdateError::Plan(ProgramSessionEvaluationError::FinalRecheckViolation { + state_index: 0, + case_index: 0, + constraint: first_violation, + subject: ProgramConstraintSubjectV1::VisibleUnary { + occurrence: OCCURRENCE, + context: appearance_context(), + }, + hard_violation_count: 3, + }), + ); + assert_eq!( + probe.calls(), + 8, + "search and fresh hard recheck must be complete without invoking report-only poison" + ); +} + #[test] fn final_recheck_violation_is_typed_and_retains_the_previous_certificate() { let evaluator = FinalRecheckMutantProgramEvaluatorV1::default(); @@ -2281,7 +2404,7 @@ fn final_recheck_violation_is_typed_and_retains_the_previous_certificate() { appearance_context(), )], ConstraintSet::new( - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( ConstraintId::new(1), OCCURRENCE, Srgb8::new([0xFF; 3]), @@ -2312,7 +2435,7 @@ fn final_recheck_violation_is_typed_and_retains_the_previous_certificate() { state_index: 0, case_index: 0, constraint: ConstraintId::new(1), - subject: ProgramConstraintSubjectV1::ModeledOccurrence { + subject: ProgramConstraintSubjectV1::VisibleUnary { occurrence: OCCURRENCE, context: appearance_context(), }, @@ -2328,7 +2451,7 @@ fn final_recheck_violation_is_typed_and_retains_the_previous_certificate() { #[test] fn duplicate_physical_candidate_value_is_typed_and_declaration_order_invariant() { let compile = |candidates| match program( - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( ConstraintId::new(1), OCCURRENCE, Wcag22CriterionV1::Sc143TextLargeScale, @@ -2364,7 +2487,7 @@ fn equal_sources_with_distinct_opacity_are_distinct_admitted_candidates() { { let compiled = program( vec![], - vec![ConstraintInvocation::report_only( + vec![ConstraintInvocation::visible_unary_report_only( ConstraintId::new(1), OCCURRENCE, Wcag22CriterionV1::Sc143TextLargeScale, @@ -2391,7 +2514,7 @@ fn equal_sources_with_distinct_opacity_are_distinct_admitted_candidates() { #[test] fn selected_atomic_candidate_preserves_opacity_through_fresh_recheck_and_output() { let compiled = program( - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( ConstraintId::new(1), OCCURRENCE, Wcag22CriterionV1::Sc143TextLargeScale, @@ -2415,7 +2538,7 @@ fn selected_atomic_candidate_preserves_opacity_through_fresh_recheck_and_output( #[test] fn atomic_candidate_matches_fixed_opacity_topology_physics_but_not_identity() { let finite = program( - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( ConstraintId::new(1), OCCURRENCE, Wcag22CriterionV1::Sc143TextLargeScale, @@ -2458,7 +2581,7 @@ fn atomic_candidate_matches_fixed_opacity_topology_physics_but_not_identity() { appearance_context(), )], ConstraintSet::new( - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( ConstraintId::new(1), OCCURRENCE, Wcag22CriterionV1::Sc143TextLargeScale, @@ -2496,7 +2619,7 @@ fn atomic_candidate_matches_fixed_opacity_topology_physics_but_not_identity() { #[test] fn duplicate_joint_tuple_is_rejected_before_runtime() { let error = match program( - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( ConstraintId::new(1), OCCURRENCE, Wcag22CriterionV1::Sc143TextLargeScale, diff --git a/crates/labcolors-core/src/program_lcs_integration_tests.rs b/crates/labcolors-core/src/program_lcs_integration_tests.rs index 9f02a234..f0b887ae 100644 --- a/crates/labcolors-core/src/program_lcs_integration_tests.rs +++ b/crates/labcolors-core/src/program_lcs_integration_tests.rs @@ -139,7 +139,7 @@ fn compiled_program( let mut hard = declarations .iter() .map(|(occurrence, constraint, _)| { - ConstraintInvocation::hard(*constraint, *occurrence, expected_visible) + ConstraintInvocation::visible_unary_hard(*constraint, *occurrence, expected_visible) }) .collect::>(); let mut outputs = vec![OutputBinding::new(BLACK_OUTPUT, TRANSLUCENT_BLACK)]; @@ -211,7 +211,7 @@ fn compiled_wcag_program(opacity: f64) -> CompiledProgram { context(SurroundProfileId::AverageV1), )], ConstraintSet::new( - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( AVERAGE_CONSTRAINT, AVERAGE_OCCURRENCE, Wcag22CriterionV1::Sc143TextDefault, @@ -256,12 +256,12 @@ fn compiled_duplicate_constraint_program() -> CompiledProgram CompiledCoreProgramV1 { context(), )], ConstraintSet::new( - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( EXACT_CONSTRAINT, OCCURRENCE, CoreProgramConstraintInvocationV1::ExactSrgb8(Srgb8::new([0x80; 3])), @@ -326,6 +326,9 @@ fn assert_public_binding_matches_core( AssessmentV1::DeclaredSrgb8CleanSet(_) => { panic!("fixture contains only occurrence-subject evaluators") } + AssessmentV1::IntrinsicUnary(_) | AssessmentV1::Relation(_) => { + panic!("fixture contains only visible-unary evaluators") + } }; let PhysicalPointV1::EncodedSrgb8SourceOver(public_physical) = public_binding.physical(); assert_eq!( @@ -427,7 +430,7 @@ fn assert_public_assessment_matches_core( match (public, core) { ( AssessmentV1::ExactSrgb8(public), - ProgramConstraintResultV1::Pass(ProgramConstraintPassEvidenceV1::ModeledOccurrence( + ProgramConstraintResultV1::Pass(ProgramConstraintPassEvidenceV1::VisibleUnary( CoreProgramPassEvidenceV1::ExactSrgb8(core), )), ) => assert_exact_matches( @@ -441,7 +444,7 @@ fn assert_public_assessment_matches_core( ( AssessmentV1::ExactSrgb8(public), ProgramConstraintResultV1::Violation( - ProgramConstraintViolationEvidenceV1::ModeledOccurrence( + ProgramConstraintViolationEvidenceV1::VisibleUnary( CoreProgramViolationEvidenceV1::ExactSrgb8(core), ), ), @@ -455,7 +458,7 @@ fn assert_public_assessment_matches_core( ), ( AssessmentV1::Wcag22Srgb8(public), - ProgramConstraintResultV1::Pass(ProgramConstraintPassEvidenceV1::ModeledOccurrence( + ProgramConstraintResultV1::Pass(ProgramConstraintPassEvidenceV1::VisibleUnary( CoreProgramPassEvidenceV1::Wcag22Srgb8(core), )), ) => assert_wcag_matches( @@ -468,7 +471,7 @@ fn assert_public_assessment_matches_core( ( AssessmentV1::Wcag22Srgb8(public), ProgramConstraintResultV1::Violation( - ProgramConstraintViolationEvidenceV1::ModeledOccurrence( + ProgramConstraintViolationEvidenceV1::VisibleUnary( CoreProgramViolationEvidenceV1::Wcag22Srgb8(core), ), ), @@ -492,17 +495,22 @@ fn assert_verified_cell_matches_core( assert_eq!(public.case_index(), core.case_index()); assert_eq!(public.constraint().value(), core.constraint().value()); let (occurrence, context) = match core.subject() { - ProgramConstraintSubjectV1::ModeledOccurrence { + ProgramConstraintSubjectV1::VisibleUnary { occurrence, context, } => (occurrence, context), ProgramConstraintSubjectV1::PointPresentation { .. } => { panic!("fixture contains only occurrence-subject evaluators") } + ProgramConstraintSubjectV1::IntrinsicUnary { .. } + | ProgramConstraintSubjectV1::IntrinsicRelation { .. } + | ProgramConstraintSubjectV1::VisibleRelation { .. } => { + panic!("fixture contains only visible-unary evaluators") + } }; assert_eq!( public.subject(), - ConstraintSubjectV1::ModeledOccurrence { + ConstraintSubjectV1::VisibleUnary { occurrence: crate::program::OccurrenceIdV1::new(occurrence.value()), context: crate::program::AppearanceContextV1::try_new( context.adapting_luminance_cd_m2(), @@ -531,15 +539,20 @@ fn assert_conflict_cell_matches_core( assert_eq!(public.case_index(), core.case_index()); assert_eq!(public.constraint().value(), core.constraint().value()); let (occurrence, context) = match core.subject() { - ProgramConstraintSubjectV1::ModeledOccurrence { + ProgramConstraintSubjectV1::VisibleUnary { occurrence, context, } => (occurrence, context), ProgramConstraintSubjectV1::PointPresentation { .. } => { panic!("fixture contains only occurrence-subject evaluators") } + ProgramConstraintSubjectV1::IntrinsicUnary { .. } + | ProgramConstraintSubjectV1::IntrinsicRelation { .. } + | ProgramConstraintSubjectV1::VisibleRelation { .. } => { + panic!("fixture contains only visible-unary evaluators") + } }; - let ConstraintSubjectV1::ModeledOccurrence { + let ConstraintSubjectV1::VisibleUnary { occurrence: public_occurrence, context: public_context, } = public.subject() @@ -673,6 +686,9 @@ fn consume_public_assessment(assessment: AssessmentV1<'_>, probe: &mut EvidenceP ); None } + AssessmentV1::IntrinsicUnary(_) | AssessmentV1::Relation(_) => { + panic!("fixture contains no intrinsic or relation constraints") + } }; let Some(binding) = binding else { @@ -805,7 +821,7 @@ fn consume_public_evidence(view: EvidenceViewV1<'_>) -> EvidenceProbe { probe.mix(cell.case_index() as u64); probe.mix(u64::from(cell.constraint().value())); match cell.subject() { - ConstraintSubjectV1::ModeledOccurrence { occurrence, .. } => { + ConstraintSubjectV1::VisibleUnary { occurrence, .. } => { probe.mix(u64::from(occurrence.value())); } ConstraintSubjectV1::PointPresentation { @@ -817,6 +833,15 @@ fn consume_public_evidence(view: EvidenceViewV1<'_>) -> EvidenceProbe { probe.mix(u64::from(occurrence.value())); probe.mix(u64::from(terminal.value())); } + ConstraintSubjectV1::IntrinsicUnary { target } => { + probe.mix(u64::from(target.value())); + } + ConstraintSubjectV1::IntrinsicRelation { reference } => { + probe.mix(u64::from(reference.value())); + } + ConstraintSubjectV1::VisibleRelation { reference, .. } => { + probe.mix(u64::from(reference.value())); + } } probe.mix(match cell.mode() { ConstraintModeV1::Hard => 1, @@ -840,7 +865,7 @@ fn consume_public_evidence(view: EvidenceViewV1<'_>) -> EvidenceProbe { probe.mix(cell.case_index() as u64); probe.mix(u64::from(cell.constraint().value())); match cell.subject() { - ConstraintSubjectV1::ModeledOccurrence { occurrence, .. } => { + ConstraintSubjectV1::VisibleUnary { occurrence, .. } => { probe.mix(u64::from(occurrence.value())); } ConstraintSubjectV1::PointPresentation { @@ -852,6 +877,15 @@ fn consume_public_evidence(view: EvidenceViewV1<'_>) -> EvidenceProbe { probe.mix(u64::from(occurrence.value())); probe.mix(u64::from(terminal.value())); } + ConstraintSubjectV1::IntrinsicUnary { target } => { + probe.mix(u64::from(target.value())); + } + ConstraintSubjectV1::IntrinsicRelation { reference } => { + probe.mix(u64::from(reference.value())); + } + ConstraintSubjectV1::VisibleRelation { reference, .. } => { + probe.mix(u64::from(reference.value())); + } } probe.mix(match cell.mode() { ConstraintModeV1::Hard => 1, @@ -917,12 +951,12 @@ fn one_program_retains_typed_exact_and_wcag22_outcomes() { )], ConstraintSet::new( vec![ - ConstraintInvocation::hard( + ConstraintInvocation::visible_unary_hard( EXACT_CONSTRAINT, OCCURRENCE, CoreProgramConstraintInvocationV1::ExactSrgb8(Srgb8::new([0; 3])), ), - ConstraintInvocation::hard( + ConstraintInvocation::visible_unary_hard( WCAG_CONSTRAINT, OCCURRENCE, CoreProgramConstraintInvocationV1::Wcag22Srgb8( @@ -945,7 +979,7 @@ fn one_program_retains_typed_exact_and_wcag22_outcomes() { panic!("one case times two heterogeneous constraints must produce two cells"); }; - let ProgramConstraintResultV1::Pass(ProgramConstraintPassEvidenceV1::ModeledOccurrence( + let ProgramConstraintResultV1::Pass(ProgramConstraintPassEvidenceV1::VisibleUnary( CoreProgramPassEvidenceV1::ExactSrgb8(evidence), )) = exact.result() else { @@ -962,7 +996,7 @@ fn one_program_retains_typed_exact_and_wcag22_outcomes() { ); assert_eq!(evidence.binding().context(), declared_context); - let ProgramConstraintResultV1::Pass(ProgramConstraintPassEvidenceV1::ModeledOccurrence( + let ProgramConstraintResultV1::Pass(ProgramConstraintPassEvidenceV1::VisibleUnary( CoreProgramPassEvidenceV1::Wcag22Srgb8(evidence), )) = wcag.result() else { @@ -1037,7 +1071,7 @@ fn mixed_families_select_only_a_state_that_passes_every_case_then_recheck_it() { for cell in [cells[0].result(), cells[2].result()] { assert!(matches!( cell, - ProgramConstraintResultV1::Pass(ProgramConstraintPassEvidenceV1::ModeledOccurrence( + ProgramConstraintResultV1::Pass(ProgramConstraintPassEvidenceV1::VisibleUnary( CoreProgramPassEvidenceV1::ExactSrgb8(_) )) )); @@ -1045,7 +1079,7 @@ fn mixed_families_select_only_a_state_that_passes_every_case_then_recheck_it() { for cell in [cells[1].result(), cells[3].result()] { assert!(matches!( cell, - ProgramConstraintResultV1::Pass(ProgramConstraintPassEvidenceV1::ModeledOccurrence( + ProgramConstraintResultV1::Pass(ProgramConstraintPassEvidenceV1::VisibleUnary( CoreProgramPassEvidenceV1::Wcag22Srgb8(_) )) )); @@ -1082,19 +1116,15 @@ fn mixed_family_conflict_is_exhaustive_and_keeps_report_only_non_gating() { assert!(cells.iter().all(|cell| cell.result().is_violation())); assert!(matches!( cells[0].result(), - ProgramConstraintResultV1::Violation( - ProgramConstraintViolationEvidenceV1::ModeledOccurrence( - CoreProgramViolationEvidenceV1::ExactSrgb8(_) - ) - ) + ProgramConstraintResultV1::Violation(ProgramConstraintViolationEvidenceV1::VisibleUnary( + CoreProgramViolationEvidenceV1::ExactSrgb8(_) + )) )); assert!(matches!( cells[1].result(), - ProgramConstraintResultV1::Violation( - ProgramConstraintViolationEvidenceV1::ModeledOccurrence( - CoreProgramViolationEvidenceV1::Wcag22Srgb8(_) - ) - ) + ProgramConstraintResultV1::Violation(ProgramConstraintViolationEvidenceV1::VisibleUnary( + CoreProgramViolationEvidenceV1::Wcag22Srgb8(_) + )) )); } diff --git a/crates/labcolors-core/src/program_point_causality_tests.rs b/crates/labcolors-core/src/program_point_causality_tests.rs index 4ffb606b..f5d6126e 100644 --- a/crates/labcolors-core/src/program_point_causality_tests.rs +++ b/crates/labcolors-core/src/program_point_causality_tests.rs @@ -22,6 +22,7 @@ use crate::program_session::{ Target, TargetCandidateChoiceV1, TargetCandidateId, TargetCandidateV1, TargetId, checked_program_point_causal_cardinality_for_test, fail_program_preflight_reservation_for_test, program_report_cardinality_is_exact_for_test, selected_program_storage_is_prepared_for_test, + storage_has_spare_capacity_for_test, }; use crate::session::{SessionState, SessionUpdateError}; use crate::session_tests::CommitSessionUpdateForTest as _; @@ -77,7 +78,7 @@ fn preflight_program_with_mode( ) -> crate::program_session::CompiledProgram { let constraints = if hard { ConstraintSet::new( - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( CONSTRAINT, OCCURRENCE, Wcag22CriterionV1::Sc143TextLargeScale, @@ -87,7 +88,7 @@ fn preflight_program_with_mode( } else { ConstraintSet::new( vec![], - vec![ConstraintInvocation::report_only( + vec![ConstraintInvocation::visible_unary_report_only( CONSTRAINT, OCCURRENCE, Wcag22CriterionV1::Sc143TextLargeScale, @@ -167,7 +168,7 @@ fn joint_preflight_program( context(), )], ConstraintSet::new( - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( CONSTRAINT, OCCURRENCE, Wcag22CriterionV1::Sc143TextLargeScale, @@ -319,7 +320,7 @@ fn fanout_program() -> crate::program_session::CompiledProgram crate::program_session::CompiledProgram program::DraftV1 { + let target = program::TargetIdV1::new(21); + let candidate = program::TargetCandidateIdV1::new(22); + let paint = program::PaintIdV1::new(23); + let port = program::SurfaceInputPortIdV1::new(24); + let surface = program::SurfaceIdV1::new(25); + let occurrence = program::OccurrenceIdV1::new(26); + let expected = Srgb8::new([0xFF; 3]); + let context = + program::AppearanceContextV1::try_new(64.0, 0.2, program::SurroundV1::Average).unwrap(); + let domain = program::FinitePaintDomainV1::try_new(vec![program::TargetCandidateV1::new( + candidate, + program::PaintValueV1::try_new(expected, 0.5).unwrap(), + )]) + .unwrap(); + let mut draft = program::DraftV1::new(); + draft.push_finite_target(target, domain); + draft + .set_joint_selection(vec![program::JointStateV1::new(vec![ + program::JointChoiceV1::new(target, candidate), + ])]) + .unwrap(); + draft.push_solid_paint(paint, target); + draft.push_surface_input_port(port); + draft.push_input_surface(surface, port); + draft.push_source_over_occurrence(occurrence, paint, surface, context); + draft.push_exact_intrinsic_unary_hard(program::ConstraintIdV1::new(27), target, expected); + if include_visible_assessment { + draft.push_exact_visible_unary_hard( + program::ConstraintIdV1::new(28), + occurrence, + Srgb8::new([0x80; 3]), + ); + } + draft.push_output(program::OutputSlotIdV1::new(29), paint); + draft +} + +#[test] +fn intrinsic_unary_filters_finite_target_without_pretending_to_assess_its_output() { + let error = match finite_intrinsic_unary_draft(false).compile() { + Err(error) => error, + Ok(_) => panic!("intrinsic coverage must not masquerade as visible assessment"), + }; + assert_eq!(error.kind(), program::CompileErrorKindV1::UnassessedOutput); + + let owner = finite_intrinsic_unary_draft(true).compile().unwrap(); + let mut session = owner.instantiate(30).unwrap(); + let black = [Srgb8::new([0; 3])]; + let scenarios = [program::ScenarioV1::new(31, &black)]; + let evidence = owner + .commit( + &mut session, + program::UpdateV1::Observed { + revision: 1, + scenarios: &scenarios, + }, + ) + .unwrap(); + let Some(program::CertificateV1::Verified(verified)) = evidence.certificates().next() else { + panic!("the only finite candidate satisfies both typed constraints"); + }; + assert_eq!(verified.selected_state_index(), Some(0)); + let cell = verified + .cells() + .find(|cell| { + matches!( + cell.subject(), + program::ConstraintSubjectV1::IntrinsicUnary { .. } + ) + }) + .expect("intrinsic unary cell must be retained"); + let program::AssessmentV1::IntrinsicUnary(evidence) = cell.assessment() else { + panic!("intrinsic unary must retain its own evidence family"); + }; + assert_eq!(evidence.verdict(), program::VerdictV1::Pass); + assert_eq!( + evidence.proof(), + program::IntrinsicUnaryProofV1::ExactSrgb8Pass + ); + let binding = evidence.binding(); + assert_eq!(binding.target(), program::TargetIdV1::new(21)); + assert_eq!(binding.value().source(), Srgb8::new([0xFF; 3])); + assert_eq!(binding.value().opacity().to_bits(), 0.5_f64.to_bits()); + let program::IntrinsicUnaryMeasurementV1::ExactSrgb8(measurement) = evidence.measurement(); + assert_eq!(measurement.expected(), Srgb8::new([0xFF; 3])); + assert_eq!(measurement.actual(), Srgb8::new([0xFF; 3])); +} + +#[test] +fn intrinsic_relation_compares_source_but_retains_each_endpoints_full_alpha() { + let source = program::SourceIdV1::new(32); + let reference_target = program::TargetIdV1::new(33); + let candidate_target = program::TargetIdV1::new(34); + let candidate = program::TargetCandidateIdV1::new(35); + let reference_paint = program::PaintIdV1::new(36); + let candidate_paint = program::PaintIdV1::new(37); + let port = program::SurfaceInputPortIdV1::new(38); + let surface = program::SurfaceIdV1::new(39); + let occurrence = program::OccurrenceIdV1::new(40); + let white = Srgb8::new([0xFF; 3]); + let context = + program::AppearanceContextV1::try_new(64.0, 0.2, program::SurroundV1::Average).unwrap(); + let mut draft = program::DraftV1::new(); + draft.push_source(source, white); + draft.push_fixed_target(reference_target, source); + draft.push_finite_target( + candidate_target, + program::FinitePaintDomainV1::try_new(vec![program::TargetCandidateV1::new( + candidate, + program::PaintValueV1::try_new(white, 0.5).unwrap(), + )]) + .unwrap(), + ); + draft + .set_joint_selection(vec![program::JointStateV1::new(vec![ + program::JointChoiceV1::new(candidate_target, candidate), + ])]) + .unwrap(); + draft.push_solid_paint(reference_paint, reference_target); + draft.push_solid_paint(candidate_paint, candidate_target); + draft.push_surface_input_port(port); + draft.push_input_surface(surface, port); + draft.push_source_over_occurrence(occurrence, candidate_paint, surface, context); + draft.push_exact_intrinsic_relation_hard( + program::ConstraintIdV1::new(41), + program::DirectedRelationV1::try_new(reference_target, vec![candidate_target]).unwrap(), + ); + draft.push_exact_visible_unary_hard( + program::ConstraintIdV1::new(42), + occurrence, + Srgb8::new([0x80; 3]), + ); + draft.push_output(program::OutputSlotIdV1::new(43), candidate_paint); + + let owner = draft.compile().unwrap(); + let mut session = owner.instantiate(44).unwrap(); + let black = [Srgb8::new([0; 3])]; + let scenarios = [program::ScenarioV1::new(45, &black)]; + let evidence = owner + .commit( + &mut session, + program::UpdateV1::Observed { + revision: 1, + scenarios: &scenarios, + }, + ) + .unwrap(); + let Some(program::CertificateV1::Verified(verified)) = evidence.certificates().next() else { + panic!("equal sources must pass independently of endpoint alpha"); + }; + let relation = verified + .cells() + .find_map(|cell| match cell.assessment() { + program::AssessmentV1::Relation(evidence) => Some(evidence), + _ => None, + }) + .expect("relation evidence must be retained"); + let mut members = relation.members(); + let Some(program::RelationMemberV1::Intrinsic(member)) = members.next() else { + panic!("intrinsic relation must retain intrinsic endpoint bindings"); + }; + assert!(members.next().is_none()); + assert_eq!(member.reference().target(), reference_target); + assert_eq!(member.reference().value().source(), white); + assert_eq!( + member.reference().value().opacity().to_bits(), + 1.0_f64.to_bits() + ); + assert_eq!(member.candidate().target(), candidate_target); + assert_eq!(member.candidate().value().source(), white); + assert_eq!( + member.candidate().value().opacity().to_bits(), + 0.5_f64.to_bits() + ); + let projected = program::RelationMemberV1::Intrinsic(member); + let program::RelationMeasurementV1::ExactSrgb8(measurement) = projected.measurement(); + assert_eq!(measurement.reference(), white); + assert_eq!(measurement.candidate(), white); + assert_eq!(projected.verdict(), program::VerdictV1::Pass); + assert_eq!( + projected.proof(), + program::RelationMemberProofV1::ExactSrgb8Pass + ); +} + +fn fixed_relation_draft() -> ( + program::DraftV1, + [program::TargetIdV1; 2], + [program::OccurrenceIdV1; 2], +) { + let sources = [program::SourceIdV1::new(40), program::SourceIdV1::new(41)]; + let targets = [program::TargetIdV1::new(42), program::TargetIdV1::new(43)]; + let paints = [program::PaintIdV1::new(44), program::PaintIdV1::new(45)]; + let occurrences = [ + program::OccurrenceIdV1::new(46), + program::OccurrenceIdV1::new(47), + ]; + let port = program::SurfaceInputPortIdV1::new(48); + let surface = program::SurfaceIdV1::new(49); + let context = + program::AppearanceContextV1::try_new(64.0, 0.2, program::SurroundV1::Average).unwrap(); + let mut draft = program::DraftV1::new(); + for (source, value) in sources.into_iter().zip([[0x10; 3], [0x20; 3]]) { + draft.push_source(source, Srgb8::new(value)); + } + for index in 0..2 { + draft.push_fixed_target(targets[index], sources[index]); + draft.push_solid_paint(paints[index], targets[index]); + } + draft.push_surface_input_port(port); + draft.push_input_surface(surface, port); + for index in 0..2 { + draft.push_source_over_occurrence(occurrences[index], paints[index], surface, context); + } + draft.push_exact_visible_unary_hard( + program::ConstraintIdV1::new(50), + occurrences[0], + Srgb8::new([0x10; 3]), + ); + draft.push_output(program::OutputSlotIdV1::new(51), paints[0]); + (draft, targets, occurrences) +} + +fn compile_error_kind(draft: program::DraftV1) -> program::CompileErrorKindV1 { + match draft.compile() { + Err(error) => error.kind(), + Ok(_) => panic!("fixture must be rejected by the typed compiler invariant"), + } +} + +#[test] +fn relation_endpoints_and_intrinsic_unary_target_fail_with_typed_errors() { + let (mut draft, _targets, _) = fixed_relation_draft(); + draft.push_exact_intrinsic_unary_hard( + program::ConstraintIdV1::new(52), + program::TargetIdV1::new(999), + Srgb8::new([0; 3]), + ); + assert_eq!( + compile_error_kind(draft), + program::CompileErrorKindV1::MissingIntrinsicUnaryTarget + ); + + let (mut draft, targets, _) = fixed_relation_draft(); + draft.push_exact_intrinsic_relation_hard( + program::ConstraintIdV1::new(52), + program::DirectedRelationV1::try_new(program::TargetIdV1::new(999), vec![targets[0]]) + .unwrap(), + ); + assert_eq!( + compile_error_kind(draft), + program::CompileErrorKindV1::MissingIntrinsicRelationReference + ); + + let (mut draft, targets, _) = fixed_relation_draft(); + draft.push_exact_intrinsic_relation_hard( + program::ConstraintIdV1::new(52), + program::DirectedRelationV1::try_new(targets[0], vec![program::TargetIdV1::new(999)]) + .unwrap(), + ); + assert_eq!( + compile_error_kind(draft), + program::CompileErrorKindV1::MissingIntrinsicRelationCandidate + ); + + let (mut draft, _, occurrences) = fixed_relation_draft(); + draft.push_exact_visible_relation_hard( + program::ConstraintIdV1::new(52), + program::DirectedRelationV1::try_new( + program::OccurrenceIdV1::new(999), + vec![occurrences[0]], + ) + .unwrap(), + ); + assert_eq!( + compile_error_kind(draft), + program::CompileErrorKindV1::MissingVisibleRelationReference + ); + + let (mut draft, _, occurrences) = fixed_relation_draft(); + draft.push_exact_visible_relation_hard( + program::ConstraintIdV1::new(52), + program::DirectedRelationV1::try_new( + occurrences[0], + vec![program::OccurrenceIdV1::new(999)], + ) + .unwrap(), + ); + assert_eq!( + compile_error_kind(draft), + program::CompileErrorKindV1::MissingVisibleRelationCandidate + ); +} + +#[test] +fn both_relation_levels_select_the_matching_finite_candidate_before_fresh_capture() { + for visible in [false, true] { + let reference_source = program::SourceIdV1::new(80); + let reference_target = program::TargetIdV1::new(81); + let finite_target = program::TargetIdV1::new(82); + let mismatch = program::TargetCandidateIdV1::new(83); + let matching = program::TargetCandidateIdV1::new(84); + let reference_paint = program::PaintIdV1::new(85); + let finite_paint = program::PaintIdV1::new(86); + let port = program::SurfaceInputPortIdV1::new(87); + let surface = program::SurfaceIdV1::new(88); + let reference_occurrence = program::OccurrenceIdV1::new(89); + let finite_occurrence = program::OccurrenceIdV1::new(90); + let context = + program::AppearanceContextV1::try_new(64.0, 0.2, program::SurroundV1::Average).unwrap(); + let expected = Srgb8::new([0x20; 3]); + let mut draft = program::DraftV1::new(); + draft.push_source(reference_source, expected); + draft.push_fixed_target(reference_target, reference_source); + draft.push_finite_target( + finite_target, + program::FinitePaintDomainV1::try_new(vec![ + program::TargetCandidateV1::new( + mismatch, + program::PaintValueV1::opaque(Srgb8::new([0x10; 3])), + ), + program::TargetCandidateV1::new(matching, program::PaintValueV1::opaque(expected)), + ]) + .unwrap(), + ); + draft + .set_joint_selection(vec![ + program::JointStateV1::new(vec![program::JointChoiceV1::new( + finite_target, + mismatch, + )]), + program::JointStateV1::new(vec![program::JointChoiceV1::new( + finite_target, + matching, + )]), + ]) + .unwrap(); + draft.push_solid_paint(reference_paint, reference_target); + draft.push_solid_paint(finite_paint, finite_target); + draft.push_surface_input_port(port); + draft.push_input_surface(surface, port); + draft.push_source_over_occurrence(reference_occurrence, reference_paint, surface, context); + draft.push_source_over_occurrence(finite_occurrence, finite_paint, surface, context); + let relation_id = program::ConstraintIdV1::new(91); + if visible { + draft.push_exact_visible_relation_hard( + relation_id, + program::DirectedRelationV1::try_new(reference_occurrence, vec![finite_occurrence]) + .unwrap(), + ); + } else { + draft.push_exact_intrinsic_relation_hard( + relation_id, + program::DirectedRelationV1::try_new(reference_target, vec![finite_target]) + .unwrap(), + ); + } + draft.push_exact_visible_unary_report_only( + program::ConstraintIdV1::new(92), + finite_occurrence, + expected, + ); + draft.push_output(program::OutputSlotIdV1::new(93), finite_paint); + + let owner = draft.compile().unwrap(); + let mut session = owner.instantiate(94).unwrap(); + let backdrop = [Srgb8::new([0; 3])]; + let scenarios = [program::ScenarioV1::new(95, &backdrop)]; + let evidence = owner + .commit( + &mut session, + program::UpdateV1::Observed { + revision: 1, + scenarios: &scenarios, + }, + ) + .unwrap(); + let Some(program::CertificateV1::Verified(verified)) = evidence.certificates().next() + else { + panic!("the matching finite candidate must be selected"); + }; + assert_eq!(verified.selected_state_index(), Some(1)); + assert_eq!(verified.outputs().next().unwrap().source(), expected); + let relation = verified + .cells() + .find_map(|cell| match cell.assessment() { + program::AssessmentV1::Relation(relation) => Some(relation), + _ => None, + }) + .expect("fresh selected-state recheck must retain relation evidence"); + assert_eq!(relation.verdict(), program::VerdictV1::Pass); + assert_eq!(relation.member_count(), 1); + assert_eq!( + matches!( + relation.members().next().unwrap(), + program::RelationMemberV1::Visible(_) + ), + visible, + ); + } +} + +fn solver_dependent_reference_draft(visible: bool) -> program::DraftV1 { + let fixed_source = program::SourceIdV1::new(60); + let finite_target = program::TargetIdV1::new(61); + let fixed_target = program::TargetIdV1::new(62); + let finite_candidate = program::TargetCandidateIdV1::new(63); + let finite_paint = program::PaintIdV1::new(64); + let fixed_paint = program::PaintIdV1::new(65); + let port = program::SurfaceInputPortIdV1::new(66); + let surface = program::SurfaceIdV1::new(67); + let finite_occurrence = program::OccurrenceIdV1::new(68); + let fixed_occurrence = program::OccurrenceIdV1::new(69); + let context = + program::AppearanceContextV1::try_new(64.0, 0.2, program::SurroundV1::Average).unwrap(); + let mut draft = program::DraftV1::new(); + draft.push_source(fixed_source, Srgb8::new([0x20; 3])); + draft.push_finite_target( + finite_target, + program::FinitePaintDomainV1::try_new(vec![program::TargetCandidateV1::new( + finite_candidate, + program::PaintValueV1::opaque(Srgb8::new([0x20; 3])), + )]) + .unwrap(), + ); + draft.push_fixed_target(fixed_target, fixed_source); + draft + .set_joint_selection(vec![program::JointStateV1::new(vec![ + program::JointChoiceV1::new(finite_target, finite_candidate), + ])]) + .unwrap(); + draft.push_solid_paint(finite_paint, finite_target); + draft.push_solid_paint(fixed_paint, fixed_target); + draft.push_surface_input_port(port); + draft.push_input_surface(surface, port); + draft.push_source_over_occurrence(finite_occurrence, finite_paint, surface, context); + draft.push_source_over_occurrence(fixed_occurrence, fixed_paint, surface, context); + draft.push_exact_visible_unary_hard( + program::ConstraintIdV1::new(70), + finite_occurrence, + Srgb8::new([0x20; 3]), + ); + if visible { + draft.push_exact_visible_relation_hard( + program::ConstraintIdV1::new(71), + program::DirectedRelationV1::try_new(finite_occurrence, vec![fixed_occurrence]) + .unwrap(), + ); + } else { + draft.push_exact_intrinsic_relation_hard( + program::ConstraintIdV1::new(71), + program::DirectedRelationV1::try_new(finite_target, vec![fixed_target]).unwrap(), + ); + } + draft.push_output(program::OutputSlotIdV1::new(72), finite_paint); + draft +} + +#[test] +fn directional_reference_cannot_move_with_solver_state() { + assert_eq!( + compile_error_kind(solver_dependent_reference_draft(false)), + program::CompileErrorKindV1::SolverDependentIntrinsicRelationReference + ); + assert_eq!( + compile_error_kind(solver_dependent_reference_draft(true)), + program::CompileErrorKindV1::SolverDependentVisibleRelationReference + ); +} + +fn relation_identity(offset: u32, reverse_candidates: bool) -> program::ContentIdentityV6 { + let sources = [0, 1, 2].map(|index| program::SourceIdV1::new(offset + index)); + let targets = [0, 1, 2].map(|index| program::TargetIdV1::new(offset + 10 + index)); + let paints = [0, 1, 2].map(|index| program::PaintIdV1::new(offset + 20 + index)); + let occurrences = [0, 1, 2].map(|index| program::OccurrenceIdV1::new(offset + 30 + index)); + let port = program::SurfaceInputPortIdV1::new(offset + 40); + let surface = program::SurfaceIdV1::new(offset + 41); + let context = + program::AppearanceContextV1::try_new(64.0, 0.2, program::SurroundV1::Average).unwrap(); + let mut draft = program::DraftV1::new(); + for index in 0..3 { + draft.push_source(sources[index], Srgb8::new([0x20 + index as u8; 3])); + draft.push_fixed_target(targets[index], sources[index]); + draft.push_solid_paint(paints[index], targets[index]); + } + draft.push_surface_input_port(port); + draft.push_input_surface(surface, port); + for index in 0..3 { + draft.push_source_over_occurrence(occurrences[index], paints[index], surface, context); + } + let mut target_candidates = vec![targets[1], targets[2]]; + let mut occurrence_candidates = vec![occurrences[1], occurrences[2]]; + if reverse_candidates { + target_candidates.reverse(); + occurrence_candidates.reverse(); + } + draft.push_exact_intrinsic_relation_hard( + program::ConstraintIdV1::new(offset + 50), + program::DirectedRelationV1::try_new(targets[0], target_candidates).unwrap(), + ); + draft.push_exact_visible_relation_hard( + program::ConstraintIdV1::new(offset + 51), + program::DirectedRelationV1::try_new(occurrences[0], occurrence_candidates).unwrap(), + ); + draft.push_output(program::OutputSlotIdV1::new(offset + 52), paints[0]); + draft.compile().unwrap().content_identity() +} + +#[test] +fn relation_identity_ignores_opaque_names_and_candidate_declaration_order() { + let canonical = relation_identity(100, false); + assert_eq!( + canonical, + relation_identity(1_000, false), + "opaque ID renaming must not change content identity", + ); + assert_eq!( + canonical, + relation_identity(100, true), + "candidate declaration order must not change content identity", + ); +} + +#[test] +fn visible_relation_checks_every_candidate_in_every_admitted_scenario() { + let context = + program::AppearanceContextV1::try_new(64.0, 0.2, program::SurroundV1::Average).unwrap(); + let white_source = program::SourceIdV1::new(1); + let middle_source = program::SourceIdV1::new(2); + let white_target = program::TargetIdV1::new(3); + let middle_target = program::TargetIdV1::new(4); + let half_opacity = program::OpacityInputIdV1::new(5); + let white_paint = program::PaintIdV1::new(6); + let half_white_paint = program::PaintIdV1::new(7); + let middle_paint = program::PaintIdV1::new(8); + let surface_port = program::SurfaceInputPortIdV1::new(9); + let backdrop = program::SurfaceIdV1::new(10); + let leading_unconstrained_occurrence = program::OccurrenceIdV1::new(11); + let reference = program::OccurrenceIdV1::new(12); + let first_candidate = program::OccurrenceIdV1::new(13); + let interleaved_unconstrained_occurrence = program::OccurrenceIdV1::new(14); + let second_candidate = program::OccurrenceIdV1::new(15); + let third_candidate = program::OccurrenceIdV1::new(16); + let relation = program::DirectedRelationV1::try_new( + reference, + vec![first_candidate, second_candidate, third_candidate], + ) + .unwrap(); + let mut draft = program::DraftV1::new(); + + draft.push_source(white_source, Srgb8::new([0xFF; 3])); + draft.push_source(middle_source, Srgb8::new([0x80; 3])); + draft.push_fixed_target(white_target, white_source); + draft.push_fixed_target(middle_target, middle_source); + draft.push_opacity_input(half_opacity, 0.5); + draft.push_solid_paint(white_paint, white_target); + draft.push_opacity_paint(half_white_paint, white_paint, half_opacity); + draft.push_solid_paint(middle_paint, middle_target); + draft.push_surface_input_port(surface_port); + draft.push_input_surface(backdrop, surface_port); + // Unconstrained occurrences до reference и между candidates делают сдвиг + // full→compact неаффинным. Evidence ниже поэтому кусает как полный пропуск + // remap, так и prefix/ordinal mutant для reference и любого candidate. + draft.push_source_over_occurrence( + leading_unconstrained_occurrence, + middle_paint, + backdrop, + context, + ); + draft.push_source_over_occurrence(reference, half_white_paint, backdrop, context); + draft.push_source_over_occurrence(first_candidate, middle_paint, backdrop, context); + draft.push_source_over_occurrence( + interleaved_unconstrained_occurrence, + half_white_paint, + backdrop, + context, + ); + draft.push_source_over_occurrence(second_candidate, middle_paint, backdrop, context); + draft.push_source_over_occurrence(third_candidate, half_white_paint, backdrop, context); + draft.push_exact_visible_relation_hard(program::ConstraintIdV1::new(22), relation); + draft.push_output(program::OutputSlotIdV1::new(17), half_white_paint); + draft.push_output(program::OutputSlotIdV1::new(18), middle_paint); + + let owner = draft.compile().unwrap(); + let mut session = owner.instantiate(19).unwrap(); + let black = [Srgb8::new([0x00; 3])]; + let white = [Srgb8::new([0xFF; 3])]; + let scenarios = [ + program::ScenarioV1::new(20, &black), + program::ScenarioV1::new(21, &white), + ]; + let evidence = owner + .commit( + &mut session, + program::UpdateV1::Observed { + revision: 1, + scenarios: &scenarios, + }, + ) + .unwrap(); + + // На чёрном фоне half-white и opaque #808080 оба дают #808080. На белом + // reference и последний candidate дают #FFFFFF, а первые два candidates — + // #808080. Вектор violation, violation, pass сохраняет два нарушения для + // XOR-mutant и одновременно кусает перестановку candidate measurements. + assert_eq!(evidence.kind(), program::StateKindV1::Failed); + let mut certificates = evidence.certificates(); + let Some(program::CertificateV1::Conflict(conflict)) = certificates.next() else { + panic!("the last relation coordinate must produce a hard conflict"); + }; + assert!(certificates.next().is_none()); + assert_eq!(conflict.considered_state_count(), 1); + assert_eq!(conflict.observation().physical_cases().len(), 2); + + let cells = conflict.cells().collect::>(); + assert_eq!(cells.len(), 2); + let mut observed_member_count = 0; + let mut violating_coordinates = Vec::new(); + for cell in cells { + let case_index = cell.case_index(); + let program::AssessmentV1::Relation(relation) = cell.assessment() else { + panic!("directional constraint must expose relation evidence"); + }; + assert_eq!(relation.member_count(), 3); + let mut cell_has_violation = false; + for (candidate_index, member) in relation.members().enumerate() { + observed_member_count += 1; + let program::RelationMemberV1::Visible(member) = member else { + panic!("visible relation cannot project intrinsic endpoints"); + }; + assert_eq!(member.reference().occurrence(), reference); + let expected_candidate = + [first_candidate, second_candidate, third_candidate][candidate_index]; + assert_eq!(member.candidate().occurrence(), expected_candidate); + + let program::RelationMeasurementV1::ExactSrgb8(measurement) = + program::RelationMemberV1::Visible(member).measurement(); + let program::PhysicalPointV1::EncodedSrgb8SourceOver(reference_physical) = + member.reference().binding().physical(); + let program::PhysicalPointV1::EncodedSrgb8SourceOver(candidate_physical) = + member.candidate().binding().physical(); + assert_eq!(measurement.reference(), reference_physical.visible()); + assert_eq!(measurement.candidate(), candidate_physical.visible()); + + let is_violation = member.verdict() == program::VerdictV1::Violation; + assert_eq!( + program::RelationMemberV1::Visible(member).proof(), + if is_violation { + program::RelationMemberProofV1::ExactSrgb8Violation + } else { + program::RelationMemberProofV1::ExactSrgb8Pass + } + ); + if is_violation { + cell_has_violation = true; + violating_coordinates.push((case_index, candidate_index)); + } + } + assert_eq!( + relation.verdict(), + if cell_has_violation { + program::VerdictV1::Violation + } else { + program::VerdictV1::Pass + }, + "cell passes iff every member passes", + ); + } + assert_eq!(observed_member_count, 6); + assert_eq!(violating_coordinates, vec![(1, 0), (1, 1)]); +} diff --git a/crates/labcolors-core/src/program_session.rs b/crates/labcolors-core/src/program_session.rs index 216d77af..e3120c06 100644 --- a/crates/labcolors-core/src/program_session.rs +++ b/crates/labcolors-core/src/program_session.rs @@ -43,10 +43,12 @@ use crate::clean_set::{ }; use crate::composition::CompositionProfileV1; use crate::constraints::{ - Evaluator, ExactSrgb8IdentityV1, HardDecision, ProgramConstraintContentV1, - ProgramPointAssessmentErrorV1, ProgramPointEvaluatorContentV1, ProgramPointEvaluatorV1, - ProgramPointInvocation, ProgramPointOccurrenceV1, ProgramPointTargetV1, - ProgramVisiblePointBindingV1, ProgramVisiblePointPassEvidence, + CoreIntrinsicUnaryInvocationV1, CoreIntrinsicUnaryMeasurementV1, CoreIntrinsicUnaryPassV1, + CoreIntrinsicUnaryViolationV1, CoreRelationInvocationV1, CoreRelationMeasurementV1, + CoreRelationPassV1, CoreRelationViolationV1, Evaluator, ExactSrgb8IdentityV1, HardDecision, + ProgramConstraintContentV1, ProgramPointAssessmentErrorV1, ProgramPointEvaluatorContentV1, + ProgramPointEvaluatorV1, ProgramPointInvocation, ProgramPointOccurrenceV1, + ProgramPointTargetV1, ProgramVisiblePointBindingV1, ProgramVisiblePointPassEvidence, ProgramVisiblePointViolationEvidence, Wcag22Srgb8V1, assess_program_point_hard, }; use crate::joint::{ @@ -55,10 +57,11 @@ use crate::joint::{ }; use crate::lcs_occurrence::{AppearanceContextId, ColorSignal}; use crate::observation::{ - CanonicalObservationSchemaV1, OBSERVATION_ARENA_SLOT_COUNT_V1, ObservationArenaSlotV1, - ObservationError, ObservationGroupId, ObservationSchemaMismatchV1, ObservationStreamId, - RevisionBoundObservationV1, canonicalize_observation_schema, + CanonicalObservationSchemaV1, NonEmptyScenarioSetV1, OBSERVATION_ARENA_SLOT_COUNT_V1, + ObservationArenaSlotV1, ObservationError, ObservationGroupId, ObservationSchemaMismatchV1, + ObservationStreamId, RevisionBoundObservationV1, canonicalize_observation_schema, }; +use crate::relation::DirectedRelationV1; use crate::session::{ Session, SessionDecision, SessionEvidenceV1, SessionObservationBindingPermitV1, SessionPlanV1, private as session_private, @@ -67,7 +70,11 @@ use crate::wcag22::Wcag22CriterionV1; #[path = "program_identity.rs"] mod identity; -pub(crate) use identity::ProgramContentIdentityV5; +pub(crate) use identity::ProgramContentIdentityV6; +#[cfg(test)] +pub(crate) use identity::edge_role_count_for_test as program_identity_edge_role_count_for_test; +#[cfg(test)] +pub(crate) use identity::graph_schema_for_test as program_identity_graph_schema_for_test; /// Opaque identity of one immutable authored colour source. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -486,12 +493,24 @@ pub enum ReportModeV1 {} /// объектом. Закрытая сумма не позволяет оценщику `Occurrence` и конвенции /// `PointPresentation` притвориться взаимозаменяемыми либо хранить объект /// ограничения отдельным полем. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub(crate) enum ProgramConstraintBodyV1 { - ModeledOccurrence { + VisibleUnary { occurrence: OccurrenceId, invocation: Invocation, }, + IntrinsicUnary { + target: TargetId, + invocation: CoreIntrinsicUnaryInvocationV1, + }, + IntrinsicRelation { + relation: DirectedRelationV1, + invocation: CoreRelationInvocationV1, + }, + VisibleRelation { + relation: DirectedRelationV1, + invocation: CoreRelationInvocationV1, + }, DeclaredSrgb8CleanSet { target: PointPresentationTargetV1, }, @@ -502,7 +521,7 @@ pub(crate) enum ProgramConstraintBodyV1 { } /// Одно атомарное типизированное ограничение над одним точным физическим объектом. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct ConstraintInvocation { id: ConstraintId, body: ProgramConstraintBodyV1, @@ -510,17 +529,64 @@ pub struct ConstraintInvocation { } impl ConstraintInvocation { - pub const fn hard(id: ConstraintId, target: OccurrenceId, invocation: Invocation) -> Self { + pub const fn visible_unary_hard( + id: ConstraintId, + occurrence: OccurrenceId, + invocation: Invocation, + ) -> Self { Self { id, - body: ProgramConstraintBodyV1::ModeledOccurrence { - occurrence: target, + body: ProgramConstraintBodyV1::VisibleUnary { + occurrence, invocation, }, mode: PhantomData, } } + pub(crate) const fn exact_intrinsic_unary_hard( + id: ConstraintId, + target: TargetId, + expected: Srgb8, + ) -> Self { + Self { + id, + body: ProgramConstraintBodyV1::IntrinsicUnary { + target, + invocation: CoreIntrinsicUnaryInvocationV1::exact_srgb8(expected), + }, + mode: PhantomData, + } + } + + pub(crate) const fn exact_intrinsic_relation_hard( + id: ConstraintId, + relation: DirectedRelationV1, + ) -> Self { + Self { + id, + body: ProgramConstraintBodyV1::IntrinsicRelation { + relation, + invocation: CoreRelationInvocationV1::exact_srgb8(), + }, + mode: PhantomData, + } + } + + pub(crate) const fn exact_visible_relation_hard( + id: ConstraintId, + relation: DirectedRelationV1, + ) -> Self { + Self { + id, + body: ProgramConstraintBodyV1::VisibleRelation { + relation, + invocation: CoreRelationInvocationV1::exact_srgb8(), + }, + mode: PhantomData, + } + } + pub(crate) const fn declared_srgb8_clean_set_hard( id: ConstraintId, target: PointPresentationTargetV1, @@ -547,15 +613,15 @@ impl ConstraintInvocation { } impl ConstraintInvocation { - pub const fn report_only( + pub const fn visible_unary_report_only( id: ConstraintId, - target: OccurrenceId, + occurrence: OccurrenceId, invocation: Invocation, ) -> Self { Self { id, - body: ProgramConstraintBodyV1::ModeledOccurrence { - occurrence: target, + body: ProgramConstraintBodyV1::VisibleUnary { + occurrence, invocation, }, mode: PhantomData, @@ -1024,6 +1090,46 @@ impl CoreProgramDraftV1 { self.program.constraints.hard.push(constraint); } + pub(crate) fn push_exact_intrinsic_relation_hard( + &mut self, + id: ConstraintId, + relation: DirectedRelationV1, + ) { + self.program + .constraints + .hard + .push(ConstraintInvocation::exact_intrinsic_relation_hard( + id, relation, + )); + } + + pub(crate) fn push_exact_intrinsic_unary_hard( + &mut self, + id: ConstraintId, + target: TargetId, + expected: Srgb8, + ) { + self.program + .constraints + .hard + .push(ConstraintInvocation::exact_intrinsic_unary_hard( + id, target, expected, + )); + } + + pub(crate) fn push_exact_visible_relation_hard( + &mut self, + id: ConstraintId, + relation: DirectedRelationV1, + ) { + self.program + .constraints + .hard + .push(ConstraintInvocation::exact_visible_relation_hard( + id, relation, + )); + } + pub(crate) fn push_report_constraint( &mut self, constraint: ConstraintInvocation, @@ -1230,6 +1336,35 @@ pub enum ProgramCompileError { constraint: ConstraintId, occurrence: OccurrenceId, }, + MissingIntrinsicUnaryTarget { + constraint: ConstraintId, + target: TargetId, + }, + MissingIntrinsicRelationReference { + constraint: ConstraintId, + reference: TargetId, + }, + MissingIntrinsicRelationCandidate { + constraint: ConstraintId, + candidate: TargetId, + }, + MissingVisibleRelationReference { + constraint: ConstraintId, + reference: OccurrenceId, + }, + MissingVisibleRelationCandidate { + constraint: ConstraintId, + candidate: OccurrenceId, + }, + SolverDependentIntrinsicRelationReference { + constraint: ConstraintId, + reference: TargetId, + }, + SolverDependentVisibleRelationReference { + constraint: ConstraintId, + reference: OccurrenceId, + target: TargetId, + }, MissingConstraintPresentationTarget { constraint: ConstraintId, root: PresentationRootId, @@ -1341,14 +1476,41 @@ std::thread_local! { }; } -#[derive(Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct CompiledIntrinsicRelationEndpointV1 { + target_id: TargetId, + target: CompiledPaintInputSlotV1, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct CompiledVisibleRelationEndpointV1 { + occurrence: OccurrenceId, + slot: CompiledOccurrenceSlotV1, + occurrence_context_index: usize, +} + enum CompiledProgramConstraintBodyV1 { - ModeledOccurrence { - target_id: OccurrenceId, - target: CompiledOccurrenceSlotV1, + VisibleUnary { + occurrence: OccurrenceId, + slot: CompiledOccurrenceSlotV1, occurrence_context_index: usize, invocation: Invocation, }, + IntrinsicUnary { + target_id: TargetId, + target: CompiledPaintInputSlotV1, + invocation: CoreIntrinsicUnaryInvocationV1, + }, + IntrinsicRelation { + reference: CompiledIntrinsicRelationEndpointV1, + candidates: Box<[CompiledIntrinsicRelationEndpointV1]>, + invocation: CoreRelationInvocationV1, + }, + VisibleRelation { + reference: CompiledVisibleRelationEndpointV1, + candidates: Box<[CompiledVisibleRelationEndpointV1]>, + invocation: CoreRelationInvocationV1, + }, PointPresentation { presentation_ordinal: usize, terminal: OccurrenceId, @@ -1362,6 +1524,18 @@ struct CompiledPointConstraint { body: CompiledProgramConstraintBodyV1, } +fn compiled_relation_member_count( + constraint: &CompiledPointConstraint, +) -> usize { + match &constraint.body { + CompiledProgramConstraintBodyV1::IntrinsicRelation { candidates, .. } => candidates.len(), + CompiledProgramConstraintBodyV1::VisibleRelation { candidates, .. } => candidates.len(), + CompiledProgramConstraintBodyV1::VisibleUnary { .. } + | CompiledProgramConstraintBodyV1::IntrinsicUnary { .. } + | CompiledProgramConstraintBodyV1::PointPresentation { .. } => 0, + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] struct CompiledOutputBinding { output: OutputSlotId, @@ -1447,10 +1621,32 @@ pub(crate) enum PointOutputPresentationBindErrorV1 { #[derive(Debug, Clone, Copy, PartialEq, Eq)] struct CompiledOccurrenceContextV1 { occurrence: OccurrenceId, - target: CompiledOccurrenceSlotV1, + slot: CompiledOccurrenceSlotV1, context: AppearanceContextId, } +fn coordinate_pair_matches( + expected_left: &Left, + expected_right: &Right, + actual_left: &Left, + actual_right: &Right, +) -> bool { + expected_left == actual_left && expected_right == actual_right +} + +#[cfg(test)] +pub(crate) fn compiled_occurrence_coordinate_pair_matches_for_test< + Left: PartialEq, + Right: PartialEq, +>( + expected_left: Left, + expected_right: Right, + actual_left: Left, + actual_right: Right, +) -> bool { + coordinate_pair_matches(&expected_left, &expected_right, &actual_left, &actual_right) +} + struct CompiledObservationGroupV1 { id: ObservationGroupId, schema: CanonicalObservationSchemaV1, @@ -1619,7 +1815,7 @@ where Evaluation: ProgramConstraintEvaluatorSetV1, ProgramConstraintInvocationOf: Copy, { - content_identity: ProgramContentIdentityV5, + content_identity: ProgramContentIdentityV6, evaluator: Evaluation, graph: CompiledAppearanceGraph, binding_template: AdmittedAppearanceBindings, @@ -1666,7 +1862,7 @@ where /// Opaque ID и порядок неупорядоченных объявлений исключены; явный joint /// order входит в адрес. Адрес не подтверждает поколение владельца и не /// заменяет revision-bound evidence. - pub fn content_identity(&self) -> ProgramContentIdentityV5 { + pub fn content_identity(&self) -> ProgramContentIdentityV6 { self.owner_generation.content_identity } @@ -1861,16 +2057,198 @@ fn map_session_instantiate_error(error: BindingError) -> ProgramSessionInstantia /// `Occurrence`, и тем самым не подменяют финальный корень внутренним цветом. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum ProgramConstraintSubjectV1 { - ModeledOccurrence { + VisibleUnary { occurrence: OccurrenceId, context: AppearanceContextId, }, + IntrinsicUnary { + target: TargetId, + }, + IntrinsicRelation { + reference: TargetId, + }, + VisibleRelation { + reference: OccurrenceId, + context: AppearanceContextId, + }, PointPresentation { target: PointPresentationTargetV1, terminal: OccurrenceId, }, } +/// Полная intrinsic-привязка Paint. Закон может читать только `source()`, но +/// для аудита сохраняется всё атомарное значение source+alpha. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct ProgramIntrinsicPaintBindingV1 { + target: TargetId, + value: EncodedPointPaintValueV1, +} + +impl ProgramIntrinsicPaintBindingV1 { + pub(crate) const fn target(self) -> TargetId { + self.target + } + + pub(crate) const fn value(self) -> EncodedPointPaintValueV1 { + self.value + } +} + +/// Полная final-visible привязка одного моделируемого физического сценария. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct ProgramVisibleRelationBindingV1 { + occurrence: OccurrenceId, + physical: ProgramVisiblePointBindingV1, +} + +impl ProgramVisibleRelationBindingV1 { + pub(crate) const fn occurrence(self) -> OccurrenceId { + self.occurrence + } + + pub(crate) const fn physical_ref(&self) -> &ProgramVisiblePointBindingV1 { + &self.physical + } +} + +/// Вердикт участника хранится отдельно от сырого exact-измерения. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ProgramRelationMemberDecisionV1 { + Pass(CoreRelationPassV1), + Violation(CoreRelationViolationV1), +} + +/// Одно однородное свидетельство reference→candidate в плоском report arena. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ProgramRelationMemberEvidenceV1 { + Intrinsic { + reference: ProgramIntrinsicPaintBindingV1, + candidate: ProgramIntrinsicPaintBindingV1, + measurement: CoreRelationMeasurementV1, + decision: ProgramRelationMemberDecisionV1, + }, + Visible { + reference: ProgramVisibleRelationBindingV1, + candidate: ProgramVisibleRelationBindingV1, + measurement: CoreRelationMeasurementV1, + decision: ProgramRelationMemberDecisionV1, + }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct ProgramIntrinsicUnaryPassEvidenceV1 { + binding: ProgramIntrinsicPaintBindingV1, + measurement: CoreIntrinsicUnaryMeasurementV1, + proof: CoreIntrinsicUnaryPassV1, +} + +impl ProgramIntrinsicUnaryPassEvidenceV1 { + pub(crate) const fn binding(self) -> ProgramIntrinsicPaintBindingV1 { + self.binding + } + + pub(crate) const fn measurement(self) -> CoreIntrinsicUnaryMeasurementV1 { + self.measurement + } + + pub(crate) const fn proof(self) -> CoreIntrinsicUnaryPassV1 { + self.proof + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct ProgramIntrinsicUnaryViolationEvidenceV1 { + binding: ProgramIntrinsicPaintBindingV1, + measurement: CoreIntrinsicUnaryMeasurementV1, + proof: CoreIntrinsicUnaryViolationV1, +} + +impl ProgramIntrinsicUnaryViolationEvidenceV1 { + pub(crate) const fn binding(self) -> ProgramIntrinsicPaintBindingV1 { + self.binding + } + + pub(crate) const fn measurement(self) -> CoreIntrinsicUnaryMeasurementV1 { + self.measurement + } + + pub(crate) const fn proof(self) -> CoreIntrinsicUnaryViolationV1 { + self.proof + } +} + +impl ProgramRelationMemberEvidenceV1 { + pub(crate) const fn measurement(self) -> CoreRelationMeasurementV1 { + match self { + Self::Intrinsic { measurement, .. } | Self::Visible { measurement, .. } => measurement, + } + } + + pub(crate) const fn decision(self) -> ProgramRelationMemberDecisionV1 { + match self { + Self::Intrinsic { decision, .. } | Self::Visible { decision, .. } => decision, + } + } + + pub(crate) const fn intrinsic_bindings( + &self, + ) -> Option<( + &ProgramIntrinsicPaintBindingV1, + &ProgramIntrinsicPaintBindingV1, + )> { + match self { + Self::Intrinsic { + reference, + candidate, + .. + } => Some((reference, candidate)), + Self::Visible { .. } => None, + } + } + + pub(crate) const fn visible_bindings( + &self, + ) -> Option<( + &ProgramVisibleRelationBindingV1, + &ProgramVisibleRelationBindingV1, + )> { + match self { + Self::Visible { + reference, + candidate, + .. + } => Some((reference, candidate)), + Self::Intrinsic { .. } => None, + } + } +} + +/// Непустой диапазон в плоском relation-member arena отчёта. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct NonEmptyRelationMemberSpanV1 { + start: usize, + len: NonZeroUsize, +} + +impl NonEmptyRelationMemberSpanV1 { + fn from_start_and_len(start: usize, len: usize) -> Option { + start.checked_add(len)?; + Some(Self { + start, + len: NonZeroUsize::new(len)?, + }) + } + + fn get( + self, + storage: &[ProgramRelationMemberEvidenceV1], + ) -> Option<&[ProgramRelationMemberEvidenceV1]> { + let end = self.start.checked_add(self.len.get())?; + storage.get(self.start..end) + } +} + /// Точное положительное свидетельство закреплённого пакетом clean-set над /// непустым финальным доменом точки. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -1917,7 +2295,9 @@ pub(crate) enum ProgramConstraintPassEvidenceV1 where Evaluation: ProgramConstraintEvaluatorSetV1, { - ModeledOccurrence(Evaluation::PassEvidence), + VisibleUnary(Evaluation::PassEvidence), + IntrinsicUnary(ProgramIntrinsicUnaryPassEvidenceV1), + Relation(NonEmptyRelationMemberSpanV1), DeclaredSrgb8CleanSet(DeclaredSrgb8CleanSetPassV1), } @@ -1925,7 +2305,9 @@ pub(crate) enum ProgramConstraintViolationEvidenceV1 where Evaluation: ProgramConstraintEvaluatorSetV1, { - ModeledOccurrence(Evaluation::ViolationEvidence), + VisibleUnary(Evaluation::ViolationEvidence), + IntrinsicUnary(ProgramIntrinsicUnaryViolationEvidenceV1), + Relation(NonEmptyRelationMemberSpanV1), DeclaredSrgb8CleanSet(DeclaredSrgb8CleanSetViolationV1), } @@ -2142,7 +2524,7 @@ pub(crate) enum ProgramPointCausalConsideredStateV1 { /// одного моделируемого terminal root. Оно ничего не утверждает о пикселе /// браузера, восприятии или качестве цвета. pub(crate) struct ProgramPointCausalEvidenceV1<'report, State> { - content_identity: ProgramContentIdentityV5, + content_identity: ProgramContentIdentityV6, observation: &'report RevisionBoundObservationV1, record: &'report ProgramPointCausalRecordV1, steps: &'report [PointOccurrenceAbsenceStepV1], @@ -2163,7 +2545,7 @@ where .unwrap_or_else(|| unreachable!("тип replay span запрещает пустой пересчёт")) } - pub(crate) const fn content_identity(&self) -> ProgramContentIdentityV5 { + pub(crate) const fn content_identity(&self) -> ProgramContentIdentityV6 { self.content_identity } @@ -2221,7 +2603,7 @@ pub struct ProgramReportV1 where Evaluation: ProgramConstraintEvaluatorSetV1, { - content_identity: ProgramContentIdentityV5, + content_identity: ProgramContentIdentityV6, observation: RevisionBoundObservationV1, arena: ProgramEvaluationArenaLeaseV1, } @@ -2232,7 +2614,7 @@ where { /// Адрес содержимого Program, по которому построен report; это не /// идентификатор поколения и не runtime-authority. - pub const fn content_identity(&self) -> ProgramContentIdentityV5 { + pub const fn content_identity(&self) -> ProgramContentIdentityV6 { self.content_identity } @@ -2244,6 +2626,13 @@ where &self.arena.storage.cells } + pub(crate) fn relation_members_for( + &self, + span: NonEmptyRelationMemberSpanV1, + ) -> Option<&[ProgramRelationMemberEvidenceV1]> { + span.get(&self.arena.storage.relation_members) + } + #[cfg(test)] pub(crate) fn storage_capacities_for_test(&self) -> [usize; 3] { [ @@ -2521,6 +2910,8 @@ where struct ProgramEvaluationCardinalityV1 { selected: usize, exhaustive_conflict: usize, + selected_relation_members: usize, + exhaustive_relation_members: usize, selected_point_records: usize, exhaustive_point_records: usize, selected_replay_steps: usize, @@ -2532,6 +2923,7 @@ fn checked_program_evaluation_cardinality( constraint_count: usize, point_presentation_count: usize, replay_steps_per_case: usize, + relation_members_per_case: usize, state_count: usize, can_conflict: bool, ) -> Option { @@ -2542,12 +2934,18 @@ fn checked_program_evaluation_cardinality( can_conflict, )?; let selected_point_records = physical_case_count.checked_mul(point_presentation_count)?; + let selected_relation_members = physical_case_count.checked_mul(relation_members_per_case)?; let selected_replay_steps = physical_case_count.checked_mul(replay_steps_per_case)?; let exhaustive_point_records = if can_conflict { selected_point_records.checked_mul(state_count)? } else { 0 }; + let exhaustive_relation_members = if can_conflict { + selected_relation_members.checked_mul(state_count)? + } else { + 0 + }; let exhaustive_replay_steps = if can_conflict { selected_replay_steps.checked_mul(state_count)? } else { @@ -2556,6 +2954,8 @@ fn checked_program_evaluation_cardinality( Some(ProgramEvaluationCardinalityV1 { selected: cells.selected, exhaustive_conflict: cells.exhaustive_conflict, + selected_relation_members, + exhaustive_relation_members, selected_point_records, exhaustive_point_records, selected_replay_steps, @@ -2580,6 +2980,11 @@ where epoch.constraints.len(), epoch.point_presentations.len(), epoch.point_presentations.steps_per_case(), + epoch + .constraints + .iter() + .map(compiled_relation_member_count) + .try_fold(0_usize, usize::checked_add)?, state_count, can_conflict, ) @@ -2609,6 +3014,7 @@ pub(crate) fn checked_program_point_causal_cardinality_for_test( constraint_count, point_presentation_count, replay_steps_per_case, + 0, state_count, can_conflict, ) @@ -2743,6 +3149,7 @@ where Evaluation: ProgramConstraintEvaluatorSetV1, { cells: Vec>, + relation_members: Vec, point_causal_records: Vec, point_causal_steps: Vec, outputs: Vec, @@ -2755,6 +3162,7 @@ where const fn empty() -> Self { Self { cells: Vec::new(), + relation_members: Vec::new(), point_causal_records: Vec::new(), point_causal_steps: Vec::new(), outputs: Vec::new(), @@ -2763,6 +3171,7 @@ where fn clear(&mut self) { self.cells.clear(); + self.relation_members.clear(); self.point_causal_records.clear(); self.point_causal_steps.clear(); self.outputs.clear(); @@ -2878,38 +3287,59 @@ where } // Порядок координат совпадает с физическим владением отчёта: constraint cells, -// causal records и плоские replay steps. -fn program_report_cardinality_is_exact(actual: [usize; 3], expected: [usize; 3]) -> bool { +// relation members, causal records и плоские replay steps. +fn program_report_cardinality_is_exact(actual: [usize; 4], expected: [usize; 4]) -> bool { actual == expected } #[cfg(test)] pub(crate) fn program_report_cardinality_is_exact_for_test( - actual: [usize; 3], - expected: [usize; 3], + actual: [usize; 4], + expected: [usize; 4], ) -> bool { program_report_cardinality_is_exact(actual, expected) } -// Четвёртая координата — output arena. Избыточная ёмкость допустима, но все -// арены обязаны быть пусты и независимо покрывать заранее рассчитанный объём. +fn storage_has_spare_capacity( + lengths: [usize; N], + capacities: [usize; N], + additional: [usize; N], +) -> bool { + lengths + .into_iter() + .zip(capacities) + .zip(additional) + .all(|((length, capacity), additional)| { + capacity + .checked_sub(length) + .is_some_and(|spare| spare >= additional) + }) +} + +#[cfg(test)] +pub(crate) fn storage_has_spare_capacity_for_test( + lengths: [usize; N], + capacities: [usize; N], + additional: [usize; N], +) -> bool { + storage_has_spare_capacity(lengths, capacities, additional) +} + +// Пятая координата — output arena. Избыточная ёмкость допустима, но все арены +// обязаны быть пусты и независимо покрывать заранее рассчитанный объём. fn selected_program_storage_is_prepared( - lengths: [usize; 4], - capacities: [usize; 4], - required: [usize; 4], + lengths: [usize; 5], + capacities: [usize; 5], + required: [usize; 5], ) -> bool { - lengths == [0; 4] - && capacities[0] >= required[0] - && capacities[1] >= required[1] - && capacities[2] >= required[2] - && capacities[3] >= required[3] + lengths == [0; 5] && storage_has_spare_capacity(lengths, capacities, required) } #[cfg(test)] pub(crate) fn selected_program_storage_is_prepared_for_test( - lengths: [usize; 4], - capacities: [usize; 4], - required: [usize; 4], + lengths: [usize; 5], + capacities: [usize; 5], + required: [usize; 5], ) -> bool { selected_program_storage_is_prepared(lengths, capacities, required) } @@ -2920,11 +3350,120 @@ struct ProgramPointCausalBuffersV1<'buffers> { steps: &'buffers mut Vec, } +enum ProgramConstraintEvidenceCaptureV1<'buffers, Evaluation> +where + Evaluation: ProgramConstraintEvaluatorSetV1, +{ + None, + Report { + cells: &'buffers mut Vec>, + relation_members: &'buffers mut Vec, + }, +} + +/// Единственный accumulator плоской relation-arena. Физическая семья +/// endpoint-ов влияет только на member payload; capacity, span и verdict едины. +struct ProgramRelationEvidenceAccumulatorV1<'capture> { + relation_members: Option<&'capture mut Vec>, + start: usize, + expected: NonZeroUsize, + written: usize, + has_violation: bool, +} + +impl<'capture> ProgramRelationEvidenceAccumulatorV1<'capture> { + fn try_begin<'buffers, Evaluation>( + capture: &'capture mut ProgramConstraintEvidenceCaptureV1<'buffers, Evaluation>, + candidate_count: usize, + ) -> Result + where + Evaluation: ProgramConstraintEvaluatorSetV1, + { + let expected = NonZeroUsize::new(candidate_count).ok_or(())?; + let relation_members = match capture { + ProgramConstraintEvidenceCaptureV1::None => None, + ProgramConstraintEvidenceCaptureV1::Report { + relation_members, .. + } => { + if !storage_has_spare_capacity( + [relation_members.len()], + [relation_members.capacity()], + [candidate_count], + ) { + return Err(()); + } + Some(&mut **relation_members) + } + }; + let start = relation_members.as_ref().map_or(0, |members| members.len()); + Ok(Self { + relation_members, + start, + expected, + written: 0, + has_violation: false, + }) + } + + fn push(&mut self, member: ProgramRelationMemberEvidenceV1) -> Result<(), ()> { + if self.written >= self.expected.get() { + return Err(()); + } + self.has_violation |= matches!( + member.decision(), + ProgramRelationMemberDecisionV1::Violation(_) + ); + if let Some(relation_members) = self.relation_members.as_mut() { + relation_members.push(member); + } + self.written = self.written.checked_add(1).ok_or(())?; + Ok(()) + } + + fn finish(self) -> Option<(Option, bool)> { + if self.written != self.expected.get() { + return None; + } + let span = match self.relation_members { + None => None, + Some(relation_members) => { + let expected_end = self.start.checked_add(self.expected.get())?; + if relation_members.len() != expected_end { + return None; + } + Some(NonEmptyRelationMemberSpanV1::from_start_and_len( + self.start, + self.expected.get(), + )?) + } + }; + Some((span, self.has_violation)) + } +} + +fn project_relation_result( + span: Option, + has_violation: bool, +) -> Option> +where + Evaluation: ProgramConstraintEvaluatorSetV1, +{ + span.map(|span| { + if has_violation { + ProgramConstraintResultV1::Violation(ProgramConstraintViolationEvidenceV1::Relation( + span, + )) + } else { + ProgramConstraintResultV1::Pass(ProgramConstraintPassEvidenceV1::Relation(span)) + } + }) +} + struct ProgramCandidateCollectionV1<'buffers, Evaluation> where Evaluation: ProgramConstraintEvaluatorSetV1, { - cells: Option<&'buffers mut Vec>>, + evidence: ProgramConstraintEvidenceCaptureV1<'buffers, Evaluation>, outputs: Option<&'buffers mut Vec>, point_causal: Option>, } @@ -2935,7 +3474,7 @@ where { const fn none() -> Self { Self { - cells: None, + evidence: ProgramConstraintEvidenceCaptureV1::None, outputs: None, point_causal: None, } @@ -2944,7 +3483,7 @@ where fn prepare_program_evaluation_arena( epoch: &ProgramEpochV1, - observation: &RevisionBoundObservationV1, + scenario_set: NonEmptyScenarioSetV1<'_>, arena: &mut ProgramEvaluationArenaV1, ) -> Result< ProgramEvaluationCardinalityV1, @@ -2954,9 +3493,8 @@ where Evaluation: ProgramConstraintEvaluatorSetV1, ProgramConstraintInvocationOf: Copy, { - let counts = - checked_program_epoch_evaluation_cardinality(epoch, observation.physical_case_count()) - .ok_or(ProgramSessionEvaluationError::ResourceExhausted)?; + let counts = checked_program_epoch_evaluation_cardinality(epoch, scenario_set.len().get()) + .ok_or(ProgramSessionEvaluationError::ResourceExhausted)?; // Search и exhaustive conflict взаимоисключают друг друга в одном update. // Покомпонентный максимум резервируется до evaluator work: старые два @@ -2967,6 +3505,13 @@ where counts.selected.max(counts.exhaustive_conflict), ) .map_err(|()| ProgramSessionEvaluationError::ResourceExhausted)?; + try_reserve_program_evaluation_buffer( + &mut arena.relation_members, + counts + .selected_relation_members + .max(counts.exhaustive_relation_members), + ) + .map_err(|()| ProgramSessionEvaluationError::ResourceExhausted)?; try_reserve_program_evaluation_buffer( &mut arena.point_causal_records, counts @@ -3088,11 +3633,13 @@ where workspace, presentation_cache, }; - let counts = prepare_program_evaluation_arena(epoch, &observation, arena.storage_mut())?; + let scenario_set = NonEmptyScenarioSetV1::from_admitted(&observation) + .ok_or(ProgramSessionEvaluationError::InternalInvariant)?; + let counts = prepare_program_evaluation_arena(epoch, scenario_set, arena.storage_mut())?; let outcome = evaluate_program_session_into( &mut runtime, epoch, - &observation, + scenario_set, arena.storage_mut(), counts, )?; @@ -3125,7 +3672,7 @@ where fn evaluate_program_session_into( runtime: &mut ProgramEvaluationRuntimeV1<'_>, epoch: &ProgramEpochV1, - observation: &RevisionBoundObservationV1, + scenario_set: NonEmptyScenarioSetV1<'_>, arena: &mut ProgramEvaluationArenaV1, counts: ProgramEvaluationCardinalityV1, ) -> Result< @@ -3141,7 +3688,7 @@ where return collect_program_candidate_into( runtime, epoch, - observation, + scenario_set, None, 1, arena, @@ -3158,7 +3705,7 @@ where if !scan_program_candidate( runtime, epoch, - observation, + scenario_set, state_index, ProgramEvaluationPhaseV1::Hard, ProgramCandidateCollectionV1::none(), @@ -3169,7 +3716,7 @@ where match collect_program_candidate_into( runtime, epoch, - observation, + scenario_set, Some(state_index), state_index + 1, arena, @@ -3193,18 +3740,21 @@ where if !selected_program_storage_is_prepared( [ arena.cells.len(), + arena.relation_members.len(), arena.point_causal_records.len(), arena.point_causal_steps.len(), arena.outputs.len(), ], [ arena.cells.capacity(), + arena.relation_members.capacity(), arena.point_causal_records.capacity(), arena.point_causal_steps.capacity(), arena.outputs.capacity(), ], [ counts.exhaustive_conflict, + counts.exhaustive_relation_members, counts.exhaustive_point_records, counts.exhaustive_replay_steps, 0, @@ -3219,11 +3769,14 @@ where if !scan_program_candidate( runtime, epoch, - observation, + scenario_set, state_index, ProgramEvaluationPhaseV1::Hard, ProgramCandidateCollectionV1 { - cells: Some(&mut arena.cells), + evidence: ProgramConstraintEvidenceCaptureV1::Report { + cells: &mut arena.cells, + relation_members: &mut arena.relation_members, + }, outputs: None, point_causal: Some(ProgramPointCausalBuffersV1 { considered_state_index: Some(state_index), @@ -3245,11 +3798,14 @@ where if scan_program_candidate( runtime, epoch, - observation, + scenario_set, state_index, ProgramEvaluationPhaseV1::ReportOnly, ProgramCandidateCollectionV1 { - cells: Some(&mut arena.cells), + evidence: ProgramConstraintEvidenceCaptureV1::Report { + cells: &mut arena.cells, + relation_members: &mut arena.relation_members, + }, outputs: None, point_causal: None, }, @@ -3261,11 +3817,13 @@ where if !program_report_cardinality_is_exact( [ arena.cells.len(), + arena.relation_members.len(), arena.point_causal_records.len(), arena.point_causal_steps.len(), ], [ counts.exhaustive_conflict, + counts.exhaustive_relation_members, counts.exhaustive_point_records, counts.exhaustive_replay_steps, ], @@ -3299,7 +3857,7 @@ where fn collect_program_candidate_into( runtime: &mut ProgramEvaluationRuntimeV1<'_>, epoch: &ProgramEpochV1, - observation: &RevisionBoundObservationV1, + scenario_set: NonEmptyScenarioSetV1<'_>, selected_state_index: Option, considered_state_count: usize, arena: &mut ProgramEvaluationArenaV1, @@ -3313,23 +3871,27 @@ where ProgramConstraintInvocationOf: Copy, { let expected_cell_count = counts.selected; + let expected_relation_member_count = counts.selected_relation_members; let expected_point_record_count = counts.selected_point_records; let expected_replay_step_count = counts.selected_replay_steps; if !selected_program_storage_is_prepared( [ arena.cells.len(), + arena.relation_members.len(), arena.point_causal_records.len(), arena.point_causal_steps.len(), arena.outputs.len(), ], [ arena.cells.capacity(), + arena.relation_members.capacity(), arena.point_causal_records.capacity(), arena.point_causal_steps.capacity(), arena.outputs.capacity(), ], [ expected_cell_count, + expected_relation_member_count, expected_point_record_count, expected_replay_step_count, epoch.outputs.len(), @@ -3348,11 +3910,14 @@ where scan_program_candidate( runtime, epoch, - observation, + scenario_set, candidate_state_index, ProgramEvaluationPhaseV1::Hard, ProgramCandidateCollectionV1 { - cells: Some(&mut arena.cells), + evidence: ProgramConstraintEvidenceCaptureV1::Report { + cells: &mut arena.cells, + relation_members: &mut arena.relation_members, + }, outputs: Some(&mut arena.outputs), point_causal: Some(ProgramPointCausalBuffersV1 { considered_state_index: None, @@ -3367,10 +3932,13 @@ where if let Some(state_index) = selected_state_index.filter(|_| has_hard_violation) { // Search only nominates a finite state. Its fresh hard recheck owns the // terminal verdict, so diagnostics cannot mask or mutate that failure. + if arena.cells.iter().any(|cell| !cell.is_hard()) { + return Err(ProgramSessionEvaluationError::InternalInvariant); + } let mut violations = arena .cells .iter() - .filter(|cell| cell.is_hard() && cell.result().is_violation()); + .filter(|cell| cell.result().is_violation()); let first = violations .next() .ok_or(ProgramSessionEvaluationError::InternalInvariant)?; @@ -3392,11 +3960,14 @@ where if scan_program_candidate( runtime, epoch, - observation, + scenario_set, candidate_state_index, ProgramEvaluationPhaseV1::ReportOnly, ProgramCandidateCollectionV1 { - cells: Some(&mut arena.cells), + evidence: ProgramConstraintEvidenceCaptureV1::Report { + cells: &mut arena.cells, + relation_members: &mut arena.relation_members, + }, outputs: (!has_hard_constraints).then_some(&mut arena.outputs), point_causal, }, @@ -3407,11 +3978,13 @@ where if !program_report_cardinality_is_exact( [ arena.cells.len(), + arena.relation_members.len(), arena.point_causal_records.len(), arena.point_causal_steps.len(), ], [ expected_cell_count, + expected_relation_member_count, expected_point_record_count, expected_replay_step_count, ], @@ -3442,10 +4015,38 @@ where }); } +fn resolve_visible_relation_endpoint( + evaluation: &AppearanceEvaluationView<'_, '_>, + contexts: &[CompiledOccurrenceContextV1], + endpoint: CompiledVisibleRelationEndpointV1, +) -> Option<(Srgb8, ProgramVisibleRelationBindingV1)> { + let source = evaluation.occurrence_at(endpoint.slot)?; + if source.visible() != source.certificate().output_rgb() { + return None; + } + let context = contexts.get(endpoint.occurrence_context_index)?; + if !coordinate_pair_matches( + &context.occurrence, + &context.slot, + &endpoint.occurrence, + &endpoint.slot, + ) { + return None; + } + let point = ProgramPointOccurrenceV1::from_resolved(source, context.context); + Some(( + Srgb8::new(source.visible()), + ProgramVisibleRelationBindingV1 { + occurrence: endpoint.occurrence, + physical: point.binding(), + }, + )) +} + fn scan_program_candidate( runtime: &mut ProgramEvaluationRuntimeV1<'_>, epoch: &ProgramEpochV1, - observation: &RevisionBoundObservationV1, + scenario_set: NonEmptyScenarioSetV1<'_>, candidate_state_index: usize, phase: ProgramEvaluationPhaseV1, collection: ProgramCandidateCollectionV1<'_, Evaluation>, @@ -3455,10 +4056,11 @@ where ProgramConstraintInvocationOf: Copy, { let ProgramCandidateCollectionV1 { - mut cells, + mut evidence, mut outputs, mut point_causal, } = collection; + let observation = scenario_set.observation(); let schema = &epoch.observation_group.schema; if !observation.shares_schema_backing_with(schema) { observation @@ -3467,11 +4069,11 @@ where return Err(ProgramSessionEvaluationError::InternalInvariant); } - let case_count = observation.physical_case_count(); + let case_count = scenario_set.len().get(); let mut has_hard_violation = false; let mut output_mismatch = None; for case_index in 0..case_count { - let values = observation + let values = scenario_set .physical_values(case_index) .ok_or(ProgramSessionEvaluationError::InternalInvariant)?; if values.len() != schema.as_slice().len() { @@ -3500,17 +4102,17 @@ where if let Some(point_causal) = point_causal.as_mut() { // Предварительный расчёт зарезервировал арены целиком. Локальная // проверка не даёт причинному replay незаметно начать аллоцировать. - if point_causal - .records - .capacity() - .saturating_sub(point_causal.records.len()) - < epoch.point_presentations.len() - || point_causal - .steps - .capacity() - .saturating_sub(point_causal.steps.len()) - < epoch.point_presentations.steps_per_case() - { + if !storage_has_spare_capacity( + [point_causal.records.len(), point_causal.steps.len()], + [ + point_causal.records.capacity(), + point_causal.steps.capacity(), + ], + [ + epoch.point_presentations.len(), + epoch.point_presentations.steps_per_case(), + ], + ) { return Err(ProgramSessionEvaluationError::InternalInvariant); } for (presentation_ordinal, presentation) in epoch.point_presentations.iter().enumerate() @@ -3542,40 +4144,45 @@ where .iter() .filter(|constraint| phase.includes(constraint.mode)) { - let (subject, result) = match constraint.body { - CompiledProgramConstraintBodyV1::ModeledOccurrence { - target_id, - target, + let (subject, result, is_violation) = match &constraint.body { + CompiledProgramConstraintBodyV1::VisibleUnary { + occurrence, + slot, occurrence_context_index, invocation, } => { let source = evaluation - .occurrence_at(target) + .occurrence_at(*slot) .ok_or(ProgramSessionEvaluationError::InternalInvariant)?; if source.visible() != source.certificate().output_rgb() { return Err(ProgramSessionEvaluationError::InternalInvariant); } let binding = epoch .occurrence_contexts - .get(occurrence_context_index) + .get(*occurrence_context_index) .ok_or(ProgramSessionEvaluationError::InternalInvariant)?; - if binding.occurrence != target_id || binding.target != target { + if !coordinate_pair_matches( + &binding.occurrence, + &binding.slot, + occurrence, + slot, + ) { return Err(ProgramSessionEvaluationError::InternalInvariant); } let point = ProgramPointOccurrenceV1::from_resolved(source, binding.context); - let decision = Evaluation::assess(&epoch.evaluator, point, invocation) + let decision = Evaluation::assess(&epoch.evaluator, point, *invocation) .map_err(|error| match error { ProgramPointAssessmentErrorV1::Evaluator(source) => { ProgramSessionEvaluationError::Evaluator { case_index, constraint: constraint.id, - occurrence: target_id, + occurrence: *occurrence, context: binding.context, source, } } })?; - let result = match decision { + let (result, is_violation) = match decision { HardDecision::Pass(evidence) => { debug_assert_eq!( Evaluation::pass_binding(&evidence).physical(), @@ -3585,8 +4192,11 @@ where Evaluation::pass_binding(&evidence).context(), binding.context, ); - ProgramConstraintResultV1::Pass( - ProgramConstraintPassEvidenceV1::ModeledOccurrence(evidence), + ( + ProgramConstraintResultV1::Pass( + ProgramConstraintPassEvidenceV1::VisibleUnary(evidence), + ), + false, ) } HardDecision::Violation(evidence) => { @@ -3598,17 +4208,175 @@ where Evaluation::violation_binding(&evidence).context(), binding.context, ); - ProgramConstraintResultV1::Violation( - ProgramConstraintViolationEvidenceV1::ModeledOccurrence(evidence), + ( + ProgramConstraintResultV1::Violation( + ProgramConstraintViolationEvidenceV1::VisibleUnary(evidence), + ), + true, ) } }; ( - ProgramConstraintSubjectV1::ModeledOccurrence { - occurrence: target_id, + ProgramConstraintSubjectV1::VisibleUnary { + occurrence: *occurrence, context: binding.context, }, - result, + Some(result), + is_violation, + ) + } + CompiledProgramConstraintBodyV1::IntrinsicUnary { + target_id, + target, + invocation, + } => { + let value = runtime + .bindings + .paint_input_at(*target) + .ok_or(ProgramSessionEvaluationError::InternalInvariant)?; + let binding = ProgramIntrinsicPaintBindingV1 { + target: *target_id, + value, + }; + let (measurement, decision) = invocation.assess(value.source()); + let (result, is_violation) = match decision { + HardDecision::Pass(proof) => ( + ProgramConstraintResultV1::Pass( + ProgramConstraintPassEvidenceV1::IntrinsicUnary( + ProgramIntrinsicUnaryPassEvidenceV1 { + binding, + measurement, + proof, + }, + ), + ), + false, + ), + HardDecision::Violation(proof) => ( + ProgramConstraintResultV1::Violation( + ProgramConstraintViolationEvidenceV1::IntrinsicUnary( + ProgramIntrinsicUnaryViolationEvidenceV1 { + binding, + measurement, + proof, + }, + ), + ), + true, + ), + }; + ( + ProgramConstraintSubjectV1::IntrinsicUnary { target: *target_id }, + Some(result), + is_violation, + ) + } + CompiledProgramConstraintBodyV1::IntrinsicRelation { + reference, + candidates, + invocation, + } => { + let mut relation_evidence = ProgramRelationEvidenceAccumulatorV1::try_begin( + &mut evidence, + candidates.len(), + ) + .map_err(|()| ProgramSessionEvaluationError::InternalInvariant)?; + let reference_value = runtime + .bindings + .paint_input_at(reference.target) + .ok_or(ProgramSessionEvaluationError::InternalInvariant)?; + let reference_binding = ProgramIntrinsicPaintBindingV1 { + target: reference.target_id, + value: reference_value, + }; + for candidate in candidates.iter() { + let candidate_value = runtime + .bindings + .paint_input_at(candidate.target) + .ok_or(ProgramSessionEvaluationError::InternalInvariant)?; + let candidate_binding = ProgramIntrinsicPaintBindingV1 { + target: candidate.target_id, + value: candidate_value, + }; + let (measurement, decision) = + invocation.assess(reference_value.source(), candidate_value.source()); + let decision = match decision { + HardDecision::Pass(pass) => ProgramRelationMemberDecisionV1::Pass(pass), + HardDecision::Violation(evidence) => { + ProgramRelationMemberDecisionV1::Violation(evidence) + } + }; + relation_evidence + .push(ProgramRelationMemberEvidenceV1::Intrinsic { + reference: reference_binding, + candidate: candidate_binding, + measurement, + decision, + }) + .map_err(|()| ProgramSessionEvaluationError::InternalInvariant)?; + } + let (span, has_violation) = relation_evidence + .finish() + .ok_or(ProgramSessionEvaluationError::InternalInvariant)?; + ( + ProgramConstraintSubjectV1::IntrinsicRelation { + reference: reference.target_id, + }, + project_relation_result(span, has_violation), + has_violation, + ) + } + CompiledProgramConstraintBodyV1::VisibleRelation { + reference, + candidates, + invocation, + } => { + let mut relation_evidence = ProgramRelationEvidenceAccumulatorV1::try_begin( + &mut evidence, + candidates.len(), + ) + .map_err(|()| ProgramSessionEvaluationError::InternalInvariant)?; + let (reference_visible, reference_binding) = resolve_visible_relation_endpoint( + &evaluation, + &epoch.occurrence_contexts, + *reference, + ) + .ok_or(ProgramSessionEvaluationError::InternalInvariant)?; + for candidate in candidates.iter().copied() { + let (candidate_visible, candidate_binding) = + resolve_visible_relation_endpoint( + &evaluation, + &epoch.occurrence_contexts, + candidate, + ) + .ok_or(ProgramSessionEvaluationError::InternalInvariant)?; + let (measurement, decision) = + invocation.assess(reference_visible, candidate_visible); + let decision = match decision { + HardDecision::Pass(pass) => ProgramRelationMemberDecisionV1::Pass(pass), + HardDecision::Violation(evidence) => { + ProgramRelationMemberDecisionV1::Violation(evidence) + } + }; + relation_evidence + .push(ProgramRelationMemberEvidenceV1::Visible { + reference: reference_binding, + candidate: candidate_binding, + measurement, + decision, + }) + .map_err(|()| ProgramSessionEvaluationError::InternalInvariant)?; + } + let (span, has_violation) = relation_evidence + .finish() + .ok_or(ProgramSessionEvaluationError::InternalInvariant)?; + ( + ProgramConstraintSubjectV1::VisibleRelation { + reference: reference.occurrence, + context: reference_binding.physical.context(), + }, + project_relation_result(span, has_violation), + has_violation, ) } CompiledProgramConstraintBodyV1::PointPresentation { @@ -3619,14 +4387,14 @@ where let presentation = epoch .point_presentations .entries - .get(presentation_ordinal) + .get(*presentation_ordinal) .ok_or(ProgramSessionEvaluationError::InternalInvariant)?; - if presentation.terminal != terminal { + if presentation.terminal != *terminal { return Err(ProgramSessionEvaluationError::InternalInvariant); } let resolved = runtime .presentation_cache - .resolve(&evaluation, presentation_ordinal, presentation, None) + .resolve(&evaluation, *presentation_ordinal, presentation, None) .map_err(|()| ProgramSessionEvaluationError::InternalInvariant)?; let result = if convention.forces_absent_mutation() { ProgramConstraintResultV1::Violation( @@ -3667,6 +4435,7 @@ where } } }; + let is_violation = result.is_violation(); ( ProgramConstraintSubjectV1::PointPresentation { target: PointPresentationTargetV1 { @@ -3674,16 +4443,18 @@ where occurrence: presentation.target, absence_release: presentation.absence_release, }, - terminal, + terminal: *terminal, }, - result, + Some(result), + is_violation, ) } }; - if constraint.mode.rejects_candidate() && result.is_violation() { + if constraint.mode.rejects_candidate() && is_violation { has_hard_violation = true; } - if let Some(cells) = cells.as_deref_mut() { + if let ProgramConstraintEvidenceCaptureV1::Report { cells, .. } = &mut evidence { + let result = result.ok_or(ProgramSessionEvaluationError::InternalInvariant)?; cells.push(ProgramConstraintCellV1 { candidate_state_index, case_index, @@ -3847,18 +4618,28 @@ where &mut program.presentation_roots, &mut program.presentation_targets, )?; + let dependency_index = index_program_dependencies(&program)?; + let mut dependency_scratch = ProgramDependencyScratchV1::new(&program)?; let mut constraints = compile_constraints::( + &program, &graph, + &dependency_index, + &mut dependency_scratch, &all_occurrence_contexts, &point_presentations, &program.constraints, )?; - validate_terminal_dependency_cone(&program, &constraints)?; + validate_terminal_dependency_cone( + &program, + &constraints, + &dependency_index, + &mut dependency_scratch, + )?; let constraint_phases = CompiledConstraintPhasesV1::from_authored(&program.constraints); let occurrence_contexts = compact_constraint_contexts(&all_occurrence_contexts, &mut constraints)?; let outputs = compile_outputs(&graph, &mut program.outputs)?; - let content_identity = identity::compile_program_content_identity_v5(&program)?; + let content_identity = identity::compile_program_content_identity_v6(&program)?; Ok(ProgramEpochV1 { content_identity, evaluator: program.evaluator, @@ -4191,6 +4972,8 @@ fn false_slots(len: usize) -> Result, ProgramCompileError> { fn validate_terminal_dependency_cone( program: &Program, constraints: &[CompiledPointConstraint>], + index: &IndexedProgramDependenciesV1, + scratch: &mut ProgramDependencyScratchV1, ) -> Result<(), ProgramCompileError> where Evaluation: ProgramConstraintEvaluatorSetV1, @@ -4206,14 +4989,21 @@ where return Ok(()); } - let index = index_program_dependencies(program)?; - let mut scratch = ProgramDependencyScratchV1::new(program)?; - scratch.scan( - &index, - constraints.iter().map(compiled_constraint_dependency_root), - )?; + let mut constrained_targets = false_slots(program.targets.len())?; + let mut assessed_paints = false_slots(program.paints.len())?; + for constraint in constraints { + mark_constraint_coverage( + program, + index, + scratch, + constraint, + &mut constrained_targets, + Some(&mut assessed_paints), + )?; + } for (target_index, target) in program.targets.iter().enumerate() { - if matches!(&target.intent, TargetIntentV1::Finite(_)) && !scratch.targets[target_index] { + if matches!(&target.intent, TargetIntentV1::Finite(_)) && !constrained_targets[target_index] + { return Err(ProgramCompileError::UnconstrainedFiniteTarget { target: target.id }); } } @@ -4221,7 +5011,7 @@ where let paint_index = index .paint(output.paint) .ok_or(ProgramCompileError::InternalInvariant)?; - if !scratch.paints[paint_index] { + if !assessed_paints[paint_index] { return Err(ProgramCompileError::UnassessedOutput { output: output.output, paint: output.paint, @@ -4236,10 +5026,20 @@ where .count(); if finite_count > 1 { let mut has_common_assessment = false; - for target in constraints.iter().map(compiled_constraint_dependency_root) { - scratch.scan(&index, [target])?; + let mut per_constraint_targets = false_slots(program.targets.len())?; + for constraint in constraints { + per_constraint_targets.fill(false); + mark_constraint_coverage( + program, + index, + scratch, + constraint, + &mut per_constraint_targets, + None, + )?; if program.targets.iter().enumerate().all(|(index, target)| { - !matches!(&target.intent, TargetIntentV1::Finite(_)) || scratch.targets[index] + !matches!(&target.intent, TargetIntentV1::Finite(_)) + || per_constraint_targets[index] }) { has_common_assessment = true; break; @@ -4252,13 +5052,103 @@ where Ok(()) } -fn compiled_constraint_dependency_root( +fn mark_constraint_coverage( + program: &Program, + index: &IndexedProgramDependenciesV1, + scratch: &mut ProgramDependencyScratchV1, constraint: &CompiledPointConstraint, -) -> OccurrenceId { + constrained_targets: &mut [bool], + assessed_paints: Option<&mut [bool]>, +) -> Result<(), ProgramCompileError> +where + Evaluation: ProgramConstraintEvaluatorSetV1, + ProgramConstraintInvocationOf: Copy, +{ match &constraint.body { - CompiledProgramConstraintBodyV1::ModeledOccurrence { target_id, .. } => *target_id, - CompiledProgramConstraintBodyV1::PointPresentation { terminal, .. } => *terminal, + CompiledProgramConstraintBodyV1::IntrinsicUnary { target_id, .. } => { + let ordinal = program + .targets + .binary_search_by_key(target_id, |candidate| candidate.id) + .map_err(|_| ProgramCompileError::InternalInvariant)?; + *constrained_targets + .get_mut(ordinal) + .ok_or(ProgramCompileError::InternalInvariant)? = true; + Ok(()) + } + CompiledProgramConstraintBodyV1::IntrinsicRelation { + reference, + candidates, + .. + } => { + for target in std::iter::once(reference.target_id) + .chain(candidates.iter().map(|candidate| candidate.target_id)) + { + let ordinal = program + .targets + .binary_search_by_key(&target, |candidate| candidate.id) + .map_err(|_| ProgramCompileError::InternalInvariant)?; + *constrained_targets + .get_mut(ordinal) + .ok_or(ProgramCompileError::InternalInvariant)? = true; + } + Ok(()) + } + CompiledProgramConstraintBodyV1::VisibleUnary { occurrence, .. } => { + merge_visible_constraint_coverage( + index, + scratch, + [*occurrence], + constrained_targets, + assessed_paints, + ) + } + CompiledProgramConstraintBodyV1::VisibleRelation { + reference, + candidates, + .. + } => merge_visible_constraint_coverage( + index, + scratch, + std::iter::once(reference.occurrence) + .chain(candidates.iter().map(|candidate| candidate.occurrence)), + constrained_targets, + assessed_paints, + ), + CompiledProgramConstraintBodyV1::PointPresentation { terminal, .. } => { + merge_visible_constraint_coverage( + index, + scratch, + [*terminal], + constrained_targets, + assessed_paints, + ) + } + } +} + +fn merge_visible_constraint_coverage( + index: &IndexedProgramDependenciesV1, + scratch: &mut ProgramDependencyScratchV1, + roots: impl IntoIterator, + constrained_targets: &mut [bool], + assessed_paints: Option<&mut [bool]>, +) -> Result<(), ProgramCompileError> { + scratch.scan(index, roots)?; + if scratch.targets.len() != constrained_targets.len() { + return Err(ProgramCompileError::InternalInvariant); } + for (destination, reached) in constrained_targets.iter_mut().zip(&scratch.targets) { + *destination |= *reached; + } + if let Some(assessed_paints) = assessed_paints { + if scratch.paints.len() != assessed_paints.len() { + return Err(ProgramCompileError::InternalInvariant); + } + for (destination, reached) in assessed_paints.iter_mut().zip(&scratch.paints) { + *destination |= *reached; + } + } + Ok(()) } fn map_observation_schema_compile_error(error: ObservationError) -> ProgramCompileError { @@ -4268,10 +5158,10 @@ fn map_observation_schema_compile_error(error: ObservationError) -> ProgramCompi } } -struct LoweredConstraint { +struct LoweredConstraint<'a, Invocation> { id: ConstraintId, mode: CompiledConstraintModeV1, - body: ProgramConstraintBodyV1, + body: &'a ProgramConstraintBodyV1, } fn compile_targets( @@ -4480,12 +5370,12 @@ fn compile_occurrence_contexts( let index = contexts .binary_search_by_key(&occurrence, |(declared, _)| *declared) .map_err(|_| ProgramCompileError::InternalInvariant)?; - let target = graph + let slot = graph .bind_occurrence(occurrence) .ok_or(ProgramCompileError::InternalInvariant)?; compiled.push(CompiledOccurrenceContextV1 { occurrence, - target, + slot, context: contexts[index].1, }); } @@ -4525,7 +5415,10 @@ fn compile_declared_clean_set_body( } fn compile_constraints( + program: &Program, graph: &CompiledAppearanceGraph, + dependency_index: &IndexedProgramDependenciesV1, + dependency_scratch: &mut ProgramDependencyScratchV1, occurrence_contexts: &[CompiledOccurrenceContextV1], presentations: &CompiledPointPresentationsV1, authored: &ConstraintSet>, @@ -4549,7 +5442,7 @@ where lowered.extend(authored.hard.iter().map(|constraint| LoweredConstraint { id: constraint.id, mode: CompiledConstraintModeV1::Hard, - body: *constraint.body(), + body: constraint.body(), })); lowered.extend( authored @@ -4558,7 +5451,7 @@ where .map(|constraint| LoweredConstraint { id: constraint.id, mode: CompiledConstraintModeV1::ReportOnly, - body: *constraint.body(), + body: constraint.body(), }), ); lowered.sort_unstable_by_key(|constraint| constraint.id); @@ -4577,34 +5470,180 @@ where .map_err(|_| ProgramCompileError::ResourceExhausted)?; for constraint in lowered { let body = match constraint.body { - ProgramConstraintBodyV1::ModeledOccurrence { + ProgramConstraintBodyV1::VisibleUnary { occurrence, invocation, } => { - let target = graph.bind_occurrence(occurrence).ok_or( + let slot = graph.bind_occurrence(*occurrence).ok_or( ProgramCompileError::MissingConstraintOccurrence { constraint: constraint.id, - occurrence, + occurrence: *occurrence, }, )?; let occurrence_context_index = occurrence_contexts - .binary_search_by_key(&occurrence, |binding| binding.occurrence) + .binary_search_by_key(occurrence, |binding| binding.occurrence) .map_err(|_| ProgramCompileError::InternalInvariant)?; - if occurrence_contexts[occurrence_context_index].target != target { + if occurrence_contexts[occurrence_context_index].slot != slot { return Err(ProgramCompileError::InternalInvariant); } - CompiledProgramConstraintBodyV1::ModeledOccurrence { - target_id: occurrence, - target, + CompiledProgramConstraintBodyV1::VisibleUnary { + occurrence: *occurrence, + slot, occurrence_context_index, - invocation, + invocation: *invocation, + } + } + ProgramConstraintBodyV1::IntrinsicUnary { target, invocation } => { + if program + .targets + .binary_search_by_key(target, |candidate| candidate.id) + .is_err() + { + return Err(ProgramCompileError::MissingIntrinsicUnaryTarget { + constraint: constraint.id, + target: *target, + }); + } + CompiledProgramConstraintBodyV1::IntrinsicUnary { + target_id: *target, + target: graph + .bind_paint_input(target_paint_input_id(*target)) + .ok_or(ProgramCompileError::InternalInvariant)?, + invocation: *invocation, + } + } + ProgramConstraintBodyV1::IntrinsicRelation { + relation, + invocation, + } => { + let reference_id = relation.reference(); + let reference_index = program + .targets + .binary_search_by_key(&reference_id, |target| target.id) + .map_err(|_| ProgramCompileError::MissingIntrinsicRelationReference { + constraint: constraint.id, + reference: reference_id, + })?; + if matches!( + program.targets[reference_index].intent, + TargetIntentV1::Finite(_) + ) { + return Err( + ProgramCompileError::SolverDependentIntrinsicRelationReference { + constraint: constraint.id, + reference: reference_id, + }, + ); + } + let reference = CompiledIntrinsicRelationEndpointV1 { + target_id: reference_id, + target: graph + .bind_paint_input(target_paint_input_id(reference_id)) + .ok_or(ProgramCompileError::InternalInvariant)?, + }; + let mut candidates = Vec::new(); + candidates + .try_reserve_exact(relation.candidates().len()) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + for candidate_id in relation.candidates().iter().copied() { + if program + .targets + .binary_search_by_key(&candidate_id, |target| target.id) + .is_err() + { + return Err(ProgramCompileError::MissingIntrinsicRelationCandidate { + constraint: constraint.id, + candidate: candidate_id, + }); + } + candidates.push(CompiledIntrinsicRelationEndpointV1 { + target_id: candidate_id, + target: graph + .bind_paint_input(target_paint_input_id(candidate_id)) + .ok_or(ProgramCompileError::InternalInvariant)?, + }); + } + CompiledProgramConstraintBodyV1::IntrinsicRelation { + reference, + candidates: candidates.into_boxed_slice(), + invocation: *invocation, + } + } + ProgramConstraintBodyV1::VisibleRelation { + relation, + invocation, + } => { + let compile_endpoint = |occurrence: OccurrenceId, + missing: ProgramCompileError| + -> Result< + CompiledVisibleRelationEndpointV1, + ProgramCompileError, + > { + let slot = graph.bind_occurrence(occurrence).ok_or(missing)?; + let occurrence_context_index = occurrence_contexts + .binary_search_by_key(&occurrence, |binding| binding.occurrence) + .map_err(|_| ProgramCompileError::InternalInvariant)?; + if occurrence_contexts[occurrence_context_index].slot != slot { + return Err(ProgramCompileError::InternalInvariant); + } + Ok(CompiledVisibleRelationEndpointV1 { + occurrence, + slot, + occurrence_context_index, + }) + }; + let reference_id = relation.reference(); + let reference = compile_endpoint( + reference_id, + ProgramCompileError::MissingVisibleRelationReference { + constraint: constraint.id, + reference: reference_id, + }, + )?; + let mut candidates = Vec::new(); + candidates + .try_reserve_exact(relation.candidates().len()) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + for candidate_id in relation.candidates().iter().copied() { + candidates.push(compile_endpoint( + candidate_id, + ProgramCompileError::MissingVisibleRelationCandidate { + constraint: constraint.id, + candidate: candidate_id, + }, + )?); + } + dependency_scratch.scan(dependency_index, [reference_id])?; + if let Some(target) = + program + .targets + .iter() + .enumerate() + .find_map(|(index, target)| { + (dependency_scratch.targets[index] + && matches!(target.intent, TargetIntentV1::Finite(_))) + .then_some(target.id) + }) + { + return Err( + ProgramCompileError::SolverDependentVisibleRelationReference { + constraint: constraint.id, + reference: reference_id, + target, + }, + ); + } + CompiledProgramConstraintBodyV1::VisibleRelation { + reference, + candidates: candidates.into_boxed_slice(), + invocation: *invocation, } } ProgramConstraintBodyV1::DeclaredSrgb8CleanSet { target } => { compile_declared_clean_set_body( presentations, constraint.id, - target, + *target, DeclaredSrgb8CleanSetV1::package_pinned(), )? } @@ -4613,7 +5652,7 @@ where compile_declared_clean_set_body( presentations, constraint.id, - target, + *target, DeclaredSrgb8CleanSetV1::final_recheck_mutant(), )? } @@ -4631,20 +5670,42 @@ fn compact_constraint_contexts( all: &[CompiledOccurrenceContextV1], constraints: &mut [CompiledPointConstraint], ) -> Result, ProgramCompileError> { + let target_count = constraints + .iter() + .try_fold(0_usize, |count, constraint| { + count.checked_add(match &constraint.body { + CompiledProgramConstraintBodyV1::VisibleUnary { .. } => 1, + CompiledProgramConstraintBodyV1::VisibleRelation { candidates, .. } => { + candidates.len().checked_add(1)? + } + CompiledProgramConstraintBodyV1::IntrinsicUnary { .. } + | CompiledProgramConstraintBodyV1::IntrinsicRelation { .. } + | CompiledProgramConstraintBodyV1::PointPresentation { .. } => 0, + }) + }) + .ok_or(ProgramCompileError::ResourceExhausted)?; let mut targets = Vec::new(); targets - .try_reserve_exact(constraints.len()) + .try_reserve_exact(target_count) .map_err(|_| ProgramCompileError::ResourceExhausted)?; - targets.extend( - constraints - .iter() - .filter_map(|constraint| match &constraint.body { - CompiledProgramConstraintBodyV1::ModeledOccurrence { target_id, .. } => { - Some(*target_id) - } - CompiledProgramConstraintBodyV1::PointPresentation { .. } => None, - }), - ); + for constraint in constraints.iter() { + match &constraint.body { + CompiledProgramConstraintBodyV1::VisibleUnary { occurrence, .. } => { + targets.push(*occurrence); + } + CompiledProgramConstraintBodyV1::VisibleRelation { + reference, + candidates, + .. + } => { + targets.push(reference.occurrence); + targets.extend(candidates.iter().map(|candidate| candidate.occurrence)); + } + CompiledProgramConstraintBodyV1::IntrinsicUnary { .. } + | CompiledProgramConstraintBodyV1::IntrinsicRelation { .. } + | CompiledProgramConstraintBodyV1::PointPresentation { .. } => {} + } + } targets.sort_unstable(); targets.dedup(); @@ -4660,21 +5721,37 @@ fn compact_constraint_contexts( } for constraint in constraints { - if let CompiledProgramConstraintBodyV1::ModeledOccurrence { - target_id, - target, + if let CompiledProgramConstraintBodyV1::VisibleUnary { + occurrence, + slot, occurrence_context_index, .. } = &mut constraint.body { let index = compact - .binary_search_by_key(target_id, |binding| binding.occurrence) + .binary_search_by_key(occurrence, |binding| binding.occurrence) .map_err(|_| ProgramCompileError::InternalInvariant)?; - if compact[index].target != *target { + if compact[index].slot != *slot { return Err(ProgramCompileError::InternalInvariant); } *occurrence_context_index = index; } + if let CompiledProgramConstraintBodyV1::VisibleRelation { + reference, + candidates, + .. + } = &mut constraint.body + { + for endpoint in std::iter::once(reference).chain(candidates.iter_mut()) { + let index = compact + .binary_search_by_key(&endpoint.occurrence, |binding| binding.occurrence) + .map_err(|_| ProgramCompileError::InternalInvariant)?; + if compact[index].slot != endpoint.slot { + return Err(ProgramCompileError::InternalInvariant); + } + endpoint.occurrence_context_index = index; + } + } } Ok(compact.into_boxed_slice()) } diff --git a/crates/labcolors-core/src/program_session_tests.rs b/crates/labcolors-core/src/program_session_tests.rs index e6bc8e3d..fd3cb960 100644 --- a/crates/labcolors-core/src/program_session_tests.rs +++ b/crates/labcolors-core/src/program_session_tests.rs @@ -176,7 +176,7 @@ fn exact_compiled_with_point_presentations( 0.5, BACKDROP, ConstraintSet::new( - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( REQUIRED, OCCURRENCE, Srgb8::new([0x80; 3]), @@ -202,14 +202,18 @@ fn exact_compiled_with_point_presentations( #[test] fn authored_modes_are_marker_typed_and_values_preserve_exact_ids() { - let hard = ConstraintInvocation::hard(REQUIRED, OCCURRENCE, Srgb8::new([0x80; 3])); - let report = - ConstraintInvocation::report_only(ConstraintId::new(51), OCCURRENCE, Srgb8::new([0x81; 3])); + let hard = + ConstraintInvocation::visible_unary_hard(REQUIRED, OCCURRENCE, Srgb8::new([0x80; 3])); + let report = ConstraintInvocation::visible_unary_report_only( + ConstraintId::new(51), + OCCURRENCE, + Srgb8::new([0x81; 3]), + ); let set = ConstraintSet::new(vec![hard], vec![report]); assert_eq!(set.hard()[0].id(), REQUIRED); assert_eq!( *set.hard()[0].body(), - ProgramConstraintBodyV1::ModeledOccurrence { + ProgramConstraintBodyV1::VisibleUnary { occurrence: OCCURRENCE, invocation: Srgb8::new([0x80; 3]), }, @@ -282,7 +286,7 @@ fn empty_domains_have_stable_precedence() { appearance_context(), )], ConstraintSet::new( - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( REQUIRED, OCCURRENCE, Srgb8::new([0; 3]), @@ -315,7 +319,7 @@ fn empty_domains_have_stable_precedence() { }], vec![], ConstraintSet::new( - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( REQUIRED, OCCURRENCE, Srgb8::new([0; 3]), @@ -346,7 +350,7 @@ fn empty_domains_have_stable_precedence() { 0.5, BACKDROP, ConstraintSet::new( - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( REQUIRED, OCCURRENCE, Srgb8::new([0x80; 3]), @@ -370,12 +374,12 @@ fn physical_errors_precede_constraint_and_output_errors() { 0.5, missing_surface, ConstraintSet::new( - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( duplicate, OccurrenceId::new(998), Srgb8::new([0; 3]), )], - vec![ConstraintInvocation::report_only( + vec![ConstraintInvocation::visible_unary_report_only( duplicate, OccurrenceId::new(997), Srgb8::new([0; 3]), @@ -404,12 +408,12 @@ fn constraint_and_output_error_precedence_is_canonical() { 0.5, BACKDROP, ConstraintSet::new( - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( duplicate, missing_occurrence, Srgb8::new([0; 3]), )], - vec![ConstraintInvocation::report_only( + vec![ConstraintInvocation::visible_unary_report_only( duplicate, OCCURRENCE, Srgb8::new([0; 3]), @@ -432,7 +436,7 @@ fn constraint_and_output_error_precedence_is_canonical() { 0.5, BACKDROP, ConstraintSet::new( - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( REQUIRED, missing_occurrence, Srgb8::new([0; 3]), @@ -457,7 +461,7 @@ fn constraint_and_output_error_precedence_is_canonical() { 0.5, BACKDROP, ConstraintSet::new( - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( REQUIRED, OCCURRENCE, Srgb8::new([0x80; 3]), @@ -480,7 +484,7 @@ fn constraint_and_output_error_precedence_is_canonical() { 0.5, BACKDROP, ConstraintSet::new( - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( REQUIRED, OCCURRENCE, Srgb8::new([0x80; 3]), @@ -509,12 +513,12 @@ fn compile_canonicalizes_constraints_and_outputs_independent_of_mode_lists() { 0.5, BACKDROP, ConstraintSet::new( - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( high, OCCURRENCE, Srgb8::new([0x80; 3]), )], - vec![ConstraintInvocation::report_only( + vec![ConstraintInvocation::visible_unary_report_only( low, OCCURRENCE, Srgb8::new([0x80; 3]), @@ -630,7 +634,7 @@ fn point_output_presentation_uses_the_selected_target_subject_not_the_terminal_s ), ], ConstraintSet::new( - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( REQUIRED, terminal_occurrence, Srgb8::new([40, 50, 60]), @@ -710,7 +714,7 @@ fn point_output_presentation_binding_reports_each_exact_failure() { #[test] fn compiled_program_owner_pin_keeps_the_exact_generation_alive_until_drop() { let compiled = exact_compiled(ConstraintSet::new( - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( REQUIRED, OCCURRENCE, Srgb8::new([0x80; 3]), @@ -760,7 +764,7 @@ fn canonical_helpers_and_checked_cardinality_fail_closed() { #[test] fn independently_instantiated_streams_expire_with_their_compiled_owner_generation() { let compiled = exact_compiled(ConstraintSet::new( - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( REQUIRED, OCCURRENCE, Srgb8::new([0x80; 3]), @@ -788,7 +792,7 @@ fn independently_instantiated_streams_expire_with_their_compiled_owner_generatio #[test] fn program_sessions_each_prewarm_three_arenas_over_the_owner_canonical_schema() { let compiled = exact_compiled(ConstraintSet::new( - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( REQUIRED, OCCURRENCE, Srgb8::new([0x80; 3]), @@ -879,8 +883,8 @@ fn multi_case_hard_failure_retains_the_full_matrix_without_outputs() { let high = ConstraintId::new(2); let compiled = exact_compiled(ConstraintSet::new( vec![ - ConstraintInvocation::hard(high, OCCURRENCE, Srgb8::new([0x00; 3])), - ConstraintInvocation::hard(low, OCCURRENCE, Srgb8::new([0x80; 3])), + ConstraintInvocation::visible_unary_hard(high, OCCURRENCE, Srgb8::new([0x00; 3])), + ConstraintInvocation::visible_unary_hard(low, OCCURRENCE, Srgb8::new([0x80; 3])), ], vec![], )); @@ -917,7 +921,7 @@ fn multi_case_hard_failure_retains_the_full_matrix_without_outputs() { assert!(cells.iter().all(|cell| cell.is_hard())); assert!(cells.iter().all(|cell| { cell.subject() - == ProgramConstraintSubjectV1::ModeledOccurrence { + == ProgramConstraintSubjectV1::VisibleUnary { occurrence: OCCURRENCE, context: appearance_context(), } @@ -938,12 +942,12 @@ fn mixed_modes_retain_the_full_canonical_matrix_without_outputs_on_hard_failure( let diagnostic = ConstraintId::new(1); let required = ConstraintId::new(2); let compiled = exact_compiled(ConstraintSet::new( - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( required, OCCURRENCE, Srgb8::new([0x00; 3]), )], - vec![ConstraintInvocation::report_only( + vec![ConstraintInvocation::visible_unary_report_only( diagnostic, OCCURRENCE, Srgb8::new([0x80; 3]), @@ -985,7 +989,7 @@ fn mixed_modes_retain_the_full_canonical_matrix_without_outputs_on_hard_failure( ); assert!(cells.iter().all(|cell| { cell.subject() - == ProgramConstraintSubjectV1::ModeledOccurrence { + == ProgramConstraintSubjectV1::VisibleUnary { occurrence: OCCURRENCE, context: appearance_context(), } @@ -999,7 +1003,7 @@ fn report_only_violations_do_not_block_program_scope_paint_outputs() { let diagnostic = ConstraintId::new(7); let compiled = exact_compiled(ConstraintSet::new( vec![], - vec![ConstraintInvocation::report_only( + vec![ConstraintInvocation::visible_unary_report_only( diagnostic, OCCURRENCE, Srgb8::new([0x7F; 3]), @@ -1102,7 +1106,7 @@ fn nested_surface_uses_the_lower_occurrence_before_assessing_the_upper() { ), ], ConstraintSet::new( - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( REQUIRED, UPPER, Srgb8::new([0xC0; 3]), @@ -1132,7 +1136,7 @@ fn nested_surface_uses_the_lower_occurrence_before_assessing_the_upper() { #[test] fn raw_head_and_program_report_share_one_observation_backing() { let compiled = exact_compiled(ConstraintSet::new( - vec![ConstraintInvocation::hard( + vec![ConstraintInvocation::visible_unary_hard( REQUIRED, OCCURRENCE, Srgb8::new([0x80; 3]), diff --git a/crates/labcolors-core/src/relation.rs b/crates/labcolors-core/src/relation.rs new file mode 100644 index 00000000..c32c2725 --- /dev/null +++ b/crates/labcolors-core/src/relation.rs @@ -0,0 +1,104 @@ +//! Каноническая алгебра направленных отношений между opaque ID. +//! +//! Модуль определяет только topology и admission. Он не приписывает ID +//! человеческий смысл и не выбирает evaluator, physical level или policy. + +/// Ошибка admission направленного отношения. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum DirectedRelationErrorV1 { + /// Направленное отношение обязано иметь хотя бы одного кандидата. + EmptyCandidates, + /// Один кандидат объявлен повторно. + DuplicateCandidate { candidate: T }, + /// Reference не может одновременно быть собственным кандидатом. + ReferenceInCandidates { reference: T }, +} + +/// Направленное отношение `reference → non-empty canonical candidates`. +/// +/// Candidate declaration order не является семантикой. После admission набор +/// отсортирован и уникален, а reference доказанно в него не входит. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct DirectedRelationV1 { + reference: T, + candidates: Box<[T]>, +} + +impl DirectedRelationV1 +where + T: Copy + Ord, +{ + /// Парсит authored topology до помещения в Draft. + pub(crate) fn try_new( + reference: T, + mut candidates: Vec, + ) -> Result> { + if candidates.is_empty() { + return Err(DirectedRelationErrorV1::EmptyCandidates); + } + candidates.sort_unstable(); + if let Some(candidate) = candidates + .windows(2) + .find(|pair| pair[0] == pair[1]) + .map(|pair| pair[0]) + { + return Err(DirectedRelationErrorV1::DuplicateCandidate { candidate }); + } + if candidates.binary_search(&reference).is_ok() { + return Err(DirectedRelationErrorV1::ReferenceInCandidates { reference }); + } + Ok(Self { + reference, + candidates: candidates.into_boxed_slice(), + }) + } + + pub(crate) const fn reference(&self) -> T { + self.reference + } + + pub(crate) fn candidates(&self) -> &[T] { + &self.candidates + } + + /// Переносит topology в другой ID-тип и заново доказывает её инварианты. + /// + /// Даже внутренний mapper нельзя считать инъективным по комментарию: типы + /// обязаны не позволять ему создать повтор кандидата или reference. + pub(crate) fn try_map( + self, + mut map: impl FnMut(T) -> U, + ) -> Result, DirectedRelationErrorV1> + where + U: Copy + Ord, + { + let reference = map(self.reference); + let candidates = self.candidates.iter().copied().map(map).collect::>(); + DirectedRelationV1::try_new(reference, candidates) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn remap_rechecks_reference_exclusion_instead_of_trusting_the_mapper() { + let relation = DirectedRelationV1::try_new(0_u8, vec![1_u8]).unwrap(); + + assert_eq!( + relation.try_map(|_| 0_u16), + Err(DirectedRelationErrorV1::ReferenceInCandidates { reference: 0 }) + ); + } + + #[test] + fn remap_rechecks_candidate_uniqueness_instead_of_trusting_the_mapper() { + let relation = DirectedRelationV1::try_new(0_u8, vec![1_u8, 2_u8]).unwrap(); + + assert_eq!( + relation.try_map(|id| u16::from(id != 0)), + Err(DirectedRelationErrorV1::DuplicateCandidate { candidate: 1 }) + ); + } +} diff --git a/scripts/verify_point_support_surplus.py b/scripts/verify_point_support_surplus.py index 3ac2b39a..9e0730a1 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 = ( - "ad5f8cf281a796533e4784c827193fd5aa723a1161facdbfa6c48308099b4ba0" + "f9a56da33469e5765c62fae6ab7fcb1f870a550cd9cd29dc160a48f65091bcbd" ) EXPECTED_Q55_PROOF_SHA256 = ( "ac59cf89503170c789223b91d775213a19d4e571ef930f2ea609fcd51b14defd"