diff --git a/.cargo/mutants.toml b/.cargo/mutants.toml index 73b015a5..fbed3457 100644 --- a/.cargo/mutants.toml +++ b/.cargo/mutants.toml @@ -6,6 +6,8 @@ # # Список является явным выбранным срезом, а не «полным workspace». Расширение # скоупа меняет discovered-population digest и потому не может пройти незаметно. +# Exact family certificate/membership входит в тот же выбранный correctness-срез; +# это не отдельный запуск и не обещание покрытия всего workspace. examine_globs = [ "crates/labcolors-core/src/alpha.rs", "crates/labcolors-core/src/analog.rs", @@ -13,6 +15,8 @@ examine_globs = [ "crates/labcolors-core/src/composition.rs", "crates/labcolors-core/src/constraints/mod.rs", "crates/labcolors-core/src/constraints/exact.rs", + "crates/labcolors-core/src/constraints/family.rs", + "crates/labcolors-core/src/family.rs", "crates/labcolors-core/src/glow.rs", "crates/labcolors-core/src/material.rs", "crates/labcolors-core/src/numerical_plan.rs", diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 488232da..a55087d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -219,6 +219,11 @@ jobs: run: python3 scripts/test_mutation.py - name: cargo test run: cargo test --workspace --locked + - name: exhaustive 24-bit family membership oracle + run: >- + cargo test -p labcolors-core + family_tests::axis_membership_matches_the_full_srgb8_cube_oracle + --locked -- --ignored --exact - name: prove core capability projection boundary shell: bash run: | 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 e6ab6e71..f791b4c0 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 @@ -48,11 +48,11 @@ "sha256": "8bf5d5a1c0f00ce245a1ecb18b923aa1631483345962d72b633e466c242a8a1d" }, { - "bytes": 13746, + "bytes": 13830, "license": "MIT", "path": "crates/labcolors-core/src/lib.rs", "role": "module_registration_source", - "sha256": "84d7ce86895355a572254846d38c3766d9007bf21f0ed81e3334196cbc964165" + "sha256": "1fdb8645a1f05c61232e362b89ef583198c131c493c3fab20c5ec545d02c6539" }, { "bytes": 39439, @@ -90,32 +90,32 @@ "sha256": "ac8c390a74c8aaea9e04d1006f3604d72f94a980ce26645d08593f025c4fc011" }, { - "bytes": 186280, + "bytes": 195880, "license": "MIT", "path": "crates/labcolors-core/src/program.rs", "role": "program_facade_source", - "sha256": "706bb420b7359bc02bce92763fb3c5acd01ae4428e74d10781ce9f019c020948" + "sha256": "86e372337ad2f21779596c8e608c6c18465577d336a2fc0a5dfcff0aaead5ec7" }, { - "bytes": 79583, + "bytes": 82393, "license": "MIT", "path": "crates/labcolors-core/src/program_identity.rs", "role": "program_identity_source", - "sha256": "0cef3e938c5511fdae96a022cd8c3771cd8e2cc00aab64f0cc7c7f3788985251" + "sha256": "c6ae7d7e24240aa4249df1bf74f0e95711b6b889bd3650acdf11e1dd82f34123" }, { - "bytes": 215114, + "bytes": 220403, "license": "MIT", "path": "crates/labcolors-core/src/program_session.rs", "role": "program_source", - "sha256": "c5438be8ce85b40e77b9bde66c8a05277ed7d2244c34c34031d4e59599e64c9b" + "sha256": "db522c68cc992d71f806997123ead19fe5d7064d13b47580ea3fc33f95f02d00" }, { "bytes": 21872, "license": "MIT", "path": "crates/labcolors-core/src/program_clean_set_tests.rs", "role": "program_tests", - "sha256": "3ab731c8ba05a673d35427867f18c906b458a93613bffdb41ce98802212cbd4c" + "sha256": "3fe1cc66f9154d37b5c9704b8d48c0710b9046cf792b756cd9f8d71bf9ba9e6e" }, { "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 6278d8f8..12b026a0 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 @@ -943c48063baac082a12b795f2bc9dda1ca7be875cd1403a6eae89339a18da0f9 receipt-v1.json +682da61a4becf096e83fef3052e12ee247b0a12359f4583ddf8a2ed04178175d 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 cc27e578..42fe0425 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":"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"} +{"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":"f8e7564811ebd793ab1dd8b2943f1591dcbc0fe914e4a4ebd8d164dfd55bfcba","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":"da7ae91894b26c300b14df3b5c858bf238aeaba1d46a6c946a08550dc02aa74f","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":"b4c4b5605db468a3ec2ddf437d310c52279e02585d5e01ad823450f0b471f4d2"},{"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":"1fdb8645a1f05c61232e362b89ef583198c131c493c3fab20c5ec545d02c6539"},{"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":"b5f342bf8cc92cd51bd242726caf84ff4a9c1a361477c902690a49139c7a53af"} diff --git a/crates/labcolors-core/src/constraints/family.rs b/crates/labcolors-core/src/constraints/family.rs new file mode 100644 index 00000000..5ec1509f --- /dev/null +++ b/crates/labcolors-core/src/constraints/family.rs @@ -0,0 +1,52 @@ +//! Метаданные code-owned evaluator-а принадлежности точному образу family. + +use crate::family::{ + AdmittedFamilySetV1, FamilyMembershipMeasurementV1, FamilyMembershipPassV1, + FamilyMembershipViolationV1, +}; +use crate::lcs_occurrence::ColorSignal; + +use super::HardDecision; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum FamilyMembershipIdentityV1 { + ExactImageMembershipV1, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum FamilyMembershipReleaseV1 { + V1, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum FamilyMembershipCapabilityV1 { + Iec61966Srgb8D65V1, +} + +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub(crate) struct FamilyMembershipV1; + +impl FamilyMembershipV1 { + pub(crate) const fn identity(self) -> FamilyMembershipIdentityV1 { + FamilyMembershipIdentityV1::ExactImageMembershipV1 + } + + pub(crate) const fn release(self) -> FamilyMembershipReleaseV1 { + FamilyMembershipReleaseV1::V1 + } + + pub(crate) const fn capability(self) -> FamilyMembershipCapabilityV1 { + FamilyMembershipCapabilityV1::Iec61966Srgb8D65V1 + } + + pub(crate) fn assess( + self, + family: &AdmittedFamilySetV1, + signal: ColorSignal, + ) -> ( + FamilyMembershipMeasurementV1, + HardDecision, + ) { + family.assess(signal) + } +} diff --git a/crates/labcolors-core/src/constraints/mod.rs b/crates/labcolors-core/src/constraints/mod.rs index 73f541a9..33c3be28 100644 --- a/crates/labcolors-core/src/constraints/mod.rs +++ b/crates/labcolors-core/src/constraints/mod.rs @@ -20,14 +20,21 @@ pub(crate) use exact::{ ExactPassEvidenceV1, ExactSrgb8IdentityV1, ExactViolationEvidenceV1, }; +mod family; +pub(crate) use family::{ + FamilyMembershipCapabilityV1, FamilyMembershipIdentityV1, FamilyMembershipReleaseV1, + FamilyMembershipV1, +}; + #[cfg(test)] pub(crate) use exact::ExactIdentityPassV1; mod relation; pub(crate) use relation::{ - CoreIntrinsicUnaryInvocationV1, CoreIntrinsicUnaryMeasurementV1, CoreIntrinsicUnaryPassV1, - CoreIntrinsicUnaryViolationV1, CoreRelationInvocationV1, CoreRelationMeasurementV1, - CoreRelationPassV1, CoreRelationViolationV1, ExactSrgb8IntrinsicUnaryCapabilityV1, + CompiledCoreIntrinsicUnaryInvocationV1, CoreIntrinsicUnaryInvocationV1, + CoreIntrinsicUnaryMeasurementV1, CoreIntrinsicUnaryPassV1, CoreIntrinsicUnaryViolationV1, + CoreRelationInvocationV1, CoreRelationMeasurementV1, CoreRelationPassV1, + CoreRelationViolationV1, ExactSrgb8IntrinsicUnaryCapabilityV1, ExactSrgb8IntrinsicUnaryIdentityV1, ExactSrgb8IntrinsicUnaryReleaseV1, ExactSrgb8RelationCapabilityV1, ExactSrgb8RelationIdentityV1, ExactSrgb8RelationReleaseV1, }; @@ -511,6 +518,11 @@ pub(crate) enum ProgramConstraintContentV1 { capability: ExactSrgb8IntrinsicUnaryCapabilityV1, expected: Srgb8, }, + FamilyMembership { + identity: FamilyMembershipIdentityV1, + release: FamilyMembershipReleaseV1, + capability: FamilyMembershipCapabilityV1, + }, ExactSrgb8Relation { identity: ExactSrgb8RelationIdentityV1, release: ExactSrgb8RelationReleaseV1, diff --git a/crates/labcolors-core/src/constraints/relation.rs b/crates/labcolors-core/src/constraints/relation.rs index 8fd6f131..57de3309 100644 --- a/crates/labcolors-core/src/constraints/relation.rs +++ b/crates/labcolors-core/src/constraints/relation.rs @@ -1,5 +1,10 @@ use crate::Srgb8; -use crate::constraints::{HardDecision, ProgramConstraintContentV1}; +use crate::constraints::{FamilyMembershipV1, HardDecision, ProgramConstraintContentV1}; +use crate::family::{ + FamilyDeclarationV1, FamilyId, FamilyMembershipMeasurementV1, FamilyMembershipPassV1, + FamilyMembershipViolationV1, +}; +use crate::lcs_occurrence::ColorSignal; fn exact_srgb8_equal(left: Srgb8, right: Srgb8) -> bool { left.bytes() == right.bytes() @@ -164,21 +169,39 @@ impl ExactSrgb8IntrinsicUnaryV1 { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum CoreIntrinsicUnaryInvocationV1 { ExactSrgb8 { expected: Srgb8 }, + FamilyMembership { family: FamilyId }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum CompiledCoreIntrinsicUnaryInvocationV1 { + ExactSrgb8 { + expected: Srgb8, + }, + FamilyMembership { + family: FamilyId, + family_index: usize, + }, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum CoreIntrinsicUnaryMeasurementV1 { ExactSrgb8(ExactSrgb8IntrinsicUnaryMeasurementV1), + FamilyMembership { + family: FamilyId, + measurement: FamilyMembershipMeasurementV1, + }, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum CoreIntrinsicUnaryPassV1 { ExactSrgb8(ExactSrgb8IntrinsicUnaryPassV1), + FamilyMembership(FamilyMembershipPassV1), } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum CoreIntrinsicUnaryViolationV1 { ExactSrgb8(ExactSrgb8IntrinsicUnaryViolationV1), + FamilyMembership(FamilyMembershipViolationV1), } impl CoreIntrinsicUnaryInvocationV1 { @@ -186,6 +209,10 @@ impl CoreIntrinsicUnaryInvocationV1 { Self::ExactSrgb8 { expected } } + pub(crate) const fn family_membership(family: FamilyId) -> Self { + Self::FamilyMembership { family } + } + pub(crate) const fn content(self) -> ProgramConstraintContentV1 { match self { Self::ExactSrgb8 { expected } => { @@ -197,16 +224,27 @@ impl CoreIntrinsicUnaryInvocationV1 { expected, } } + Self::FamilyMembership { .. } => { + let profile = FamilyMembershipV1; + ProgramConstraintContentV1::FamilyMembership { + identity: profile.identity(), + release: profile.release(), + capability: profile.capability(), + } + } } } +} +impl CompiledCoreIntrinsicUnaryInvocationV1 { pub(crate) fn assess( self, actual: Srgb8, - ) -> ( + families: &[FamilyDeclarationV1], + ) -> Option<( CoreIntrinsicUnaryMeasurementV1, HardDecision, - ) { + )> { match self { Self::ExactSrgb8 { expected } => { let (measurement, decision) = ExactSrgb8IntrinsicUnaryV1.assess(expected, actual); @@ -218,10 +256,40 @@ impl CoreIntrinsicUnaryInvocationV1 { HardDecision::Violation(CoreIntrinsicUnaryViolationV1::ExactSrgb8(proof)) } }; - ( + Some(( CoreIntrinsicUnaryMeasurementV1::ExactSrgb8(measurement), decision, - ) + )) + } + Self::FamilyMembership { + family, + family_index, + } => { + // Compile-time связывает индекс с этим exact FamilyId; `None` + // здесь означает порчу compiled graph, а не штатное отсутствие + // evidence, и вызывающий слой переводит его в InternalInvariant. + let declaration = families.get(family_index)?; + if declaration.id() != family { + return None; + } + let set = declaration.set(); + let (measurement, decision) = + FamilyMembershipV1.assess(set, ColorSignal::from_srgb8(actual)); + let decision = match decision { + HardDecision::Pass(proof) => { + HardDecision::Pass(CoreIntrinsicUnaryPassV1::FamilyMembership(proof)) + } + HardDecision::Violation(proof) => HardDecision::Violation( + CoreIntrinsicUnaryViolationV1::FamilyMembership(proof), + ), + }; + Some(( + CoreIntrinsicUnaryMeasurementV1::FamilyMembership { + family, + measurement, + }, + decision, + )) } } } diff --git a/crates/labcolors-core/src/family.rs b/crates/labcolors-core/src/family.rs new file mode 100644 index 00000000..089ecf9c --- /dev/null +++ b/crates/labcolors-core/src/family.rs @@ -0,0 +1,864 @@ +//! Точный конечный образ versioned family-generator-а. +//! +//! Family здесь — множество физических [`ColorSignal`], а не роль, оттенок или +//! обещание визуальной чистоты. Объявленное множество полно по определению; +//! независимо вычисленный образ допускается только после исчерпывающего +//! равенства канонических множеств. SHA-256 адресует весь проверенный объект. + +#[cfg(test)] +use crate::Srgb8; +use crate::constraints::HardDecision; +use crate::lcs_occurrence::{ColorSignal, OutputProfileId}; +use crate::sha256::Hasher; + +#[cfg(test)] +pub(crate) use assess_counter::FAMILY_MEMBERSHIP_ASSESS_CALLS; + +#[cfg(test)] +mod assess_counter { + std::thread_local! { + pub(crate) static FAMILY_MEMBERSHIP_ASSESS_CALLS: core::cell::Cell = + const { core::cell::Cell::new(0) }; + } +} + +const GENERATOR_DOMAIN_V1: &[u8] = b"labcolors.family-generator-content-identity.v1\0"; +const GENERATOR_PARAMETERS_DOMAIN_V1: &[u8] = b"labcolors.family-generator-parameters.v1\0"; +const IMAGE_DOMAIN_V1: &[u8] = b"labcolors.family-image-identity.v1\0"; +const FAMILY_DOMAIN_V1: &[u8] = b"labcolors.family-certificate-content-identity.v1\0"; +const GENERATOR_PARAMETERS_CODEC_V1: u8 = 1; +const IMAGE_CODEC_V1: u8 = 1; +const FAMILY_CERTIFICATE_CODEC_V1: u8 = 1; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[repr(u8)] +enum OutputProfileTagV1 { + // Дискриминант принадлежит canonical codec family V1; новый output profile + // требует нового tag, sensitivity vectors и явного решения, может ли один + // family-set вообще содержать сигналы разных профилей. + Iec61966Srgb8D65V1 = 1, +} + +/// Непрозрачный клиентский ключ одного объявленного family-set. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct FamilyId(u32); + +impl FamilyId { + pub(crate) const fn new(value: u32) -> Self { + Self(value) + } + + pub(crate) const fn value(self) -> u32 { + self.0 + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct FamilyGeneratorContentIdentityV1([u8; 32]); + +impl FamilyGeneratorContentIdentityV1 { + pub(crate) const fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct FamilyImageContentIdentityV1([u8; 32]); + +impl FamilyImageContentIdentityV1 { + pub(crate) const fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct FamilyContentIdentityV1([u8; 32]); + +impl FamilyContentIdentityV1 { + pub(crate) const fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum FamilyGeneratorReleaseV1 { + DeclaredFiniteImageV1, + #[cfg(test)] + EncodedSrgb8EqualChannelAxisV1, + #[cfg(test)] + EncodedSrgb8RedBlueDiagonalV1, + #[cfg(test)] + NonInjectiveUnorderedFixtureV1, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum FamilyImageProofReleaseV1 { + DeclaredImageIsDefinitionV1, + #[cfg(test)] + ExhaustiveCanonicalImageComparisonV1, +} + +/// Закрытое определение генератора с конечным каноническим доменом. +/// +/// Технические оси существуют только как proof fixtures. Production-вариант +/// определяет ровно объявленный конечный образ и не приписывает ему human meaning. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum CompleteFamilyGeneratorV1 { + DeclaredFiniteImageV1 { + definition: Vec, + }, + #[cfg(test)] + EncodedSrgb8EqualChannelAxisV1, + #[cfg(test)] + EncodedSrgb8RedBlueDiagonalV1, + #[cfg(test)] + NonInjectiveUnorderedFixtureV1 { + permuted: bool, + provenance: u8, + }, +} + +/// Повторяемое определение генератора, сохранённое каждым сертификатом. +/// +/// У declared-image каноническое множество само является определением. Будущие +/// параметрические providers обязаны хранить здесь все свои точные входы. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum FamilyGeneratorDescriptorV1 { + DeclaredFiniteImageV1, + #[cfg(test)] + EncodedSrgb8EqualChannelAxisV1, + #[cfg(test)] + EncodedSrgb8RedBlueDiagonalV1, + #[cfg(test)] + NonInjectiveUnorderedFixtureV1 { + permuted: bool, + provenance: u8, + }, +} + +impl CompleteFamilyGeneratorV1 { + pub(crate) fn try_declared_finite_image_v1( + mut definition: Vec, + ) -> Result { + canonicalize(&mut definition); + if definition.is_empty() { + return Err(FamilyImageErrorV1::EmptyGeneratorDomain); + } + Ok(Self::DeclaredFiniteImageV1 { definition }) + } + + #[cfg(test)] + pub(crate) const fn encoded_srgb8_equal_channel_axis_v1() -> Self { + Self::EncodedSrgb8EqualChannelAxisV1 + } + + #[cfg(test)] + pub(crate) const fn encoded_srgb8_red_blue_diagonal_v1() -> Self { + Self::EncodedSrgb8RedBlueDiagonalV1 + } + + #[cfg(test)] + pub(crate) const fn noninjective_unordered_fixture_v1() -> Self { + Self::NonInjectiveUnorderedFixtureV1 { + permuted: false, + provenance: 0, + } + } + + #[cfg(test)] + pub(crate) const fn permuted_noninjective_unordered_fixture_v1() -> Self { + Self::NonInjectiveUnorderedFixtureV1 { + permuted: true, + provenance: 0, + } + } + + #[cfg(test)] + pub(crate) const fn noninjective_fixture_with_provenance_v1(provenance: u8) -> Self { + Self::NonInjectiveUnorderedFixtureV1 { + permuted: false, + provenance, + } + } + + /// Перечисляет полный ordinal-образ генератора до превращения его в множество. + /// + /// У ordinal-generator-а порядок и повторы принадлежат identity генератора; + /// следующий шаг отдельно канонизирует множество для membership. + /// Declared-image уже определён как каноническое множество, поэтому порядок + /// и повторы входной записи являются лишь представлением. + pub(crate) fn into_complete_output( + self, + ) -> Result { + let members = match self { + Self::DeclaredFiniteImageV1 { definition } => definition, + #[cfg(test)] + Self::EncodedSrgb8EqualChannelAxisV1 => { + let mut members = Vec::new(); + members + .try_reserve_exact(256) + .map_err(|_| FamilyImageErrorV1::ResourceExhausted)?; + members.extend((0_u16..=255).map(|value| { + let value = value as u8; + ColorSignal::from_srgb8(Srgb8::new([value; 3])) + })); + members + } + #[cfg(test)] + Self::EncodedSrgb8RedBlueDiagonalV1 => { + let mut members = Vec::new(); + members + .try_reserve_exact(256) + .map_err(|_| FamilyImageErrorV1::ResourceExhausted)?; + members.extend((0_u16..=255).map(|value| { + let value = value as u8; + ColorSignal::from_srgb8(Srgb8::new([value, 0, 255 - value])) + })); + members + } + #[cfg(test)] + Self::NonInjectiveUnorderedFixtureV1 { permuted, .. } => { + let mut members = Vec::new(); + members + .try_reserve_exact(4) + .map_err(|_| FamilyImageErrorV1::ResourceExhausted)?; + let (first, second) = if permuted { (10, 20) } else { (20, 10) }; + members.extend([ + ColorSignal::from_srgb8(Srgb8::new([first; 3])), + ColorSignal::from_srgb8(Srgb8::new([second; 3])), + ColorSignal::from_srgb8(Srgb8::new([first; 3])), + ColorSignal::from_srgb8(Srgb8::new([second; 3])), + ]); + members + } + }; + Ok(UnverifiedFamilyImageV1 { members }) + } + + const fn release(&self) -> FamilyGeneratorReleaseV1 { + match self { + Self::DeclaredFiniteImageV1 { .. } => FamilyGeneratorReleaseV1::DeclaredFiniteImageV1, + #[cfg(test)] + Self::EncodedSrgb8EqualChannelAxisV1 => { + FamilyGeneratorReleaseV1::EncodedSrgb8EqualChannelAxisV1 + } + #[cfg(test)] + Self::EncodedSrgb8RedBlueDiagonalV1 => { + FamilyGeneratorReleaseV1::EncodedSrgb8RedBlueDiagonalV1 + } + #[cfg(test)] + Self::NonInjectiveUnorderedFixtureV1 { .. } => { + FamilyGeneratorReleaseV1::NonInjectiveUnorderedFixtureV1 + } + } + } + + const fn descriptor(&self) -> FamilyGeneratorDescriptorV1 { + match self { + Self::DeclaredFiniteImageV1 { .. } => { + FamilyGeneratorDescriptorV1::DeclaredFiniteImageV1 + } + #[cfg(test)] + Self::EncodedSrgb8EqualChannelAxisV1 => { + FamilyGeneratorDescriptorV1::EncodedSrgb8EqualChannelAxisV1 + } + #[cfg(test)] + Self::EncodedSrgb8RedBlueDiagonalV1 => { + FamilyGeneratorDescriptorV1::EncodedSrgb8RedBlueDiagonalV1 + } + #[cfg(test)] + Self::NonInjectiveUnorderedFixtureV1 { + permuted, + provenance, + } => FamilyGeneratorDescriptorV1::NonInjectiveUnorderedFixtureV1 { + permuted: *permuted, + provenance: *provenance, + }, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct UnverifiedFamilyImageV1 { + members: Vec, +} + +impl UnverifiedFamilyImageV1 { + #[cfg(test)] + pub(crate) const fn new(members: Vec) -> Self { + Self { members } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct FamilyImageCertificateV1 { + generator_release: FamilyGeneratorReleaseV1, + generator_content_identity: FamilyGeneratorContentIdentityV1, + image_content_identity: FamilyImageContentIdentityV1, + family_content_identity: FamilyContentIdentityV1, + output_profile: OutputProfileId, + proof_release: FamilyImageProofReleaseV1, + preimage_count: u64, + member_count: u64, +} + +impl FamilyImageCertificateV1 { + #[cfg(test)] + pub(crate) const fn generator_release(self) -> FamilyGeneratorReleaseV1 { + self.generator_release + } + + #[cfg(test)] + pub(crate) const fn generator_content_identity(self) -> FamilyGeneratorContentIdentityV1 { + self.generator_content_identity + } + + #[cfg(test)] + pub(crate) const fn image_content_identity(self) -> FamilyImageContentIdentityV1 { + self.image_content_identity + } + + pub(crate) const fn family_content_identity(self) -> FamilyContentIdentityV1 { + self.family_content_identity + } + + #[cfg(test)] + pub(crate) const fn proof_release(self) -> FamilyImageProofReleaseV1 { + self.proof_release + } + + #[cfg(test)] + pub(crate) const fn preimage_count(self) -> u64 { + self.preimage_count + } + + #[cfg(test)] + pub(crate) const fn member_count(self) -> u64 { + self.member_count + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct AdmittedFamilySetV1 { + generator: FamilyGeneratorDescriptorV1, + certificate: FamilyImageCertificateV1, + members: Vec, +} + +impl AdmittedFamilySetV1 { + pub(crate) const fn certificate(&self) -> FamilyImageCertificateV1 { + self.certificate + } + + pub(crate) fn assess( + &self, + signal: ColorSignal, + ) -> ( + FamilyMembershipMeasurementV1, + HardDecision, + ) { + #[cfg(test)] + FAMILY_MEMBERSHIP_ASSESS_CALLS.with(|calls| calls.set(calls.get() + 1)); + let measurement = FamilyMembershipMeasurementV1 { + family: self.certificate.family_content_identity, + signal, + }; + let decision = match self + .members + .binary_search_by_key(&canonical_signal_key(signal), |member| { + canonical_signal_key(*member) + }) { + Ok(_) => HardDecision::Pass(FamilyMembershipPassV1), + Err(_) => HardDecision::Violation(FamilyMembershipViolationV1), + }; + (measurement, decision) + } + + /// Повторно связывает полный образ и все поля сертификата до компиляции. + pub(crate) fn verify(&self) -> Result<(), FamilyImageErrorV1> { + if self.members.is_empty() { + return Err(FamilyImageErrorV1::EmptyGeneratorDomain); + } + if self + .members + .windows(2) + .any(|pair| canonical_signal_key(pair[0]) >= canonical_signal_key(pair[1])) + { + return Err(FamilyImageErrorV1::NonCanonicalAdmittedImage); + } + let release = self.generator.release(); + if release != self.certificate.generator_release { + return Err(FamilyImageErrorV1::CertificateMismatch); + } + let replay = self.generator.replay(&self.members)?; + let (generator_content_identity, preimage_count) = match replay { + GeneratorReplayV1::Declared(output) => ( + generator_content_identity_for(self.generator, output)?, + u64::try_from(output.len()).map_err(|_| FamilyImageErrorV1::ResourceExhausted)?, + ), + #[cfg(test)] + GeneratorReplayV1::Generated(mut output) => { + let generator_content_identity = + generator_content_identity_for(self.generator, &output)?; + let preimage_count = u64::try_from(output.len()) + .map_err(|_| FamilyImageErrorV1::ResourceExhausted)?; + canonicalize(&mut output); + if first_set_mismatch(&output, &self.members) != (None, None) { + return Err(FamilyImageErrorV1::CertificateMismatch); + } + (generator_content_identity, preimage_count) + } + }; + let proof_is_admitted = match ( + self.certificate.generator_release, + self.certificate.proof_release, + ) { + ( + FamilyGeneratorReleaseV1::DeclaredFiniteImageV1, + FamilyImageProofReleaseV1::DeclaredImageIsDefinitionV1, + ) => true, + #[cfg(test)] + ( + FamilyGeneratorReleaseV1::DeclaredFiniteImageV1 + | FamilyGeneratorReleaseV1::EncodedSrgb8EqualChannelAxisV1 + | FamilyGeneratorReleaseV1::EncodedSrgb8RedBlueDiagonalV1 + | FamilyGeneratorReleaseV1::NonInjectiveUnorderedFixtureV1, + FamilyImageProofReleaseV1::ExhaustiveCanonicalImageComparisonV1, + ) => true, + #[cfg(test)] + ( + FamilyGeneratorReleaseV1::EncodedSrgb8EqualChannelAxisV1 + | FamilyGeneratorReleaseV1::EncodedSrgb8RedBlueDiagonalV1 + | FamilyGeneratorReleaseV1::NonInjectiveUnorderedFixtureV1, + FamilyImageProofReleaseV1::DeclaredImageIsDefinitionV1, + ) => false, + }; + if !proof_is_admitted { + return Err(FamilyImageErrorV1::CertificateMismatch); + } + let expected = certificate_for( + self.certificate.generator_release, + generator_content_identity, + &self.members, + preimage_count, + self.certificate.proof_release, + )?; + if expected != self.certificate { + return Err(FamilyImageErrorV1::CertificateMismatch); + } + Ok(()) + } + + #[cfg(test)] + pub(crate) fn corrupt_first_member_for_test(&mut self, replacement: ColorSignal) { + if let Some(first) = self.members.first_mut() { + *first = replacement; + } + } + + #[cfg(test)] + pub(crate) fn recertify_proof_for_test(&mut self, proof: FamilyImageProofReleaseV1) { + self.certificate = certificate_for( + self.certificate.generator_release, + self.certificate.generator_content_identity, + &self.members, + self.certificate.preimage_count, + proof, + ) + .expect("the admitted fixture already has one output profile"); + } + + #[cfg(test)] + pub(crate) fn recertify_preimage_count_for_test(&mut self, preimage_count: u64) { + self.certificate = certificate_for( + self.certificate.generator_release, + self.certificate.generator_content_identity, + &self.members, + preimage_count, + self.certificate.proof_release, + ) + .expect("the admitted fixture already has one output profile"); + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct FamilyDeclarationV1 { + id: FamilyId, + set: AdmittedFamilySetV1, +} + +impl FamilyDeclarationV1 { + pub(crate) const fn new(id: FamilyId, set: AdmittedFamilySetV1) -> Self { + Self { id, set } + } + + pub(crate) const fn id(&self) -> FamilyId { + self.id + } + + pub(crate) const fn set(&self) -> &AdmittedFamilySetV1 { + &self.set + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct FamilyMembershipMeasurementV1 { + family: FamilyContentIdentityV1, + signal: ColorSignal, +} + +impl FamilyMembershipMeasurementV1 { + pub(crate) const fn family(self) -> FamilyContentIdentityV1 { + self.family + } + + pub(crate) const fn signal(self) -> ColorSignal { + self.signal + } +} + +/// Membership связывают family identity и исходный сигнал в measurement; +/// proof не повторяет координаты конкретного физического представления set-а. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct FamilyMembershipPassV1; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct FamilyMembershipViolationV1; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum FamilyImageErrorV1 { + EmptyGeneratorDomain, + #[cfg(test)] + ImageMismatch { + missing: Option, + extraneous: Option, + }, + NonCanonicalAdmittedImage, + CertificateMismatch, + ResourceExhausted, +} + +#[cfg(test)] +pub(crate) fn verify_complete_family_image_v1( + generator: CompleteFamilyGeneratorV1, + mut proposed: UnverifiedFamilyImageV1, +) -> Result { + let release = generator.release(); + let descriptor = generator.descriptor(); + let generated = generator.into_complete_output()?; + if generated.members.is_empty() { + return Err(FamilyImageErrorV1::EmptyGeneratorDomain); + } + let preimage_count = u64::try_from(generated.members.len()) + .map_err(|_| FamilyImageErrorV1::ResourceExhausted)?; + let generator_content_identity = + generator_content_identity_for(descriptor, &generated.members)?; + let mut expected_members = generated.members; + canonicalize(&mut expected_members); + canonicalize(&mut proposed.members); + let mismatch = first_set_mismatch(&expected_members, &proposed.members); + if mismatch != (None, None) { + return Err(FamilyImageErrorV1::ImageMismatch { + missing: mismatch.0, + extraneous: mismatch.1, + }); + } + let members = proposed.members; + let certificate = certificate_for( + release, + generator_content_identity, + &members, + preimage_count, + FamilyImageProofReleaseV1::ExhaustiveCanonicalImageComparisonV1, + )?; + let admitted = AdmittedFamilySetV1 { + generator: descriptor, + certificate, + members, + }; + admitted.verify()?; + Ok(admitted) +} + +pub(crate) fn admit_declared_family_image_v1( + definition: Vec, +) -> Result { + let generator = CompleteFamilyGeneratorV1::try_declared_finite_image_v1(definition)?; + let release = generator.release(); + let descriptor = generator.descriptor(); + let generated = generator.into_complete_output()?.members; + let preimage_count = + u64::try_from(generated.len()).map_err(|_| FamilyImageErrorV1::ResourceExhausted)?; + let generator_content_identity = generator_content_identity_for(descriptor, &generated)?; + let certificate = certificate_for( + release, + generator_content_identity, + &generated, + preimage_count, + FamilyImageProofReleaseV1::DeclaredImageIsDefinitionV1, + )?; + let admitted = AdmittedFamilySetV1 { + generator: descriptor, + certificate, + members: generated, + }; + admitted.verify()?; + Ok(admitted) +} + +fn canonicalize(members: &mut Vec) { + members.sort_unstable_by_key(|member| canonical_signal_key(*member)); + members.dedup_by_key(|member| canonical_signal_key(*member)); +} + +/// Канонический codec V1 задан явно и не зависит от layout/derive порядка типа. +const fn canonical_signal_key(signal: ColorSignal) -> (u8, [u8; 3]) { + ( + output_profile_tag(signal.output_profile()) as u8, + signal.srgb8().bytes(), + ) +} + +#[cfg(test)] +fn first_set_mismatch( + expected: &[ColorSignal], + actual: &[ColorSignal], +) -> (Option, Option) { + let (mut expected_index, mut actual_index) = (0, 0); + let (mut missing, mut extraneous) = (None, None); + while expected_index < expected.len() || actual_index < actual.len() { + match (expected.get(expected_index), actual.get(actual_index)) { + (Some(expected), Some(actual)) + if canonical_signal_key(*expected) == canonical_signal_key(*actual) => + { + expected_index += 1; + actual_index += 1; + } + (Some(expected), Some(actual)) + if canonical_signal_key(*expected) < canonical_signal_key(*actual) => + { + missing.get_or_insert(*expected); + expected_index += 1; + } + (Some(_), Some(actual)) => { + extraneous.get_or_insert(*actual); + actual_index += 1; + } + (Some(expected), None) => { + missing.get_or_insert(*expected); + expected_index += 1; + } + (None, Some(actual)) => { + extraneous.get_or_insert(*actual); + actual_index += 1; + } + (None, None) => break, + } + if missing.is_some() && extraneous.is_some() { + break; + } + } + (missing, extraneous) +} + +fn certificate_for( + generator_release: FamilyGeneratorReleaseV1, + generator_content_identity: FamilyGeneratorContentIdentityV1, + members: &[ColorSignal], + preimage_count: u64, + proof_release: FamilyImageProofReleaseV1, +) -> Result { + let first = members + .first() + .copied() + .ok_or(FamilyImageErrorV1::EmptyGeneratorDomain)?; + let output_profile = first.output_profile(); + let member_count = + u64::try_from(members.len()).map_err(|_| FamilyImageErrorV1::ResourceExhausted)?; + let image_content_identity = FamilyImageContentIdentityV1(image_digest( + IMAGE_DOMAIN_V1, + IMAGE_CODEC_V1, + output_profile, + member_count, + members, + )); + let mut hasher = Hasher::new(); + hasher.update(FAMILY_DOMAIN_V1); + hasher.update(&[ + FAMILY_CERTIFICATE_CODEC_V1, + generator_release_tag(generator_release), + ]); + hasher.update(generator_content_identity.as_bytes()); + hasher.update(image_content_identity.as_bytes()); + hasher.update(&[ + output_profile_tag(output_profile) as u8, + proof_release_tag(proof_release), + ]); + hasher.update(&preimage_count.to_be_bytes()); + hasher.update(&member_count.to_be_bytes()); + let family_content_identity = FamilyContentIdentityV1(*hasher.finalize().as_bytes()); + Ok(FamilyImageCertificateV1 { + generator_release, + generator_content_identity, + image_content_identity, + family_content_identity, + output_profile, + proof_release, + preimage_count, + member_count, + }) +} + +fn image_digest( + domain: &[u8], + release: u8, + profile: OutputProfileId, + count: u64, + members: &[ColorSignal], +) -> [u8; 32] { + let mut hasher = Hasher::new(); + hasher.update(domain); + hasher.update(&[release, output_profile_tag(profile) as u8]); + hasher.update(&count.to_be_bytes()); + for member in members.iter().copied() { + hasher.update(&[output_profile_tag(member.output_profile()) as u8]); + hasher.update(&member.srgb8().bytes()); + } + *hasher.finalize().as_bytes() +} + +const fn output_profile_tag(profile: OutputProfileId) -> OutputProfileTagV1 { + match profile { + OutputProfileId::Iec61966Srgb8D65V1 => OutputProfileTagV1::Iec61966Srgb8D65V1, + } +} + +const fn generator_release_tag(release: FamilyGeneratorReleaseV1) -> u8 { + match release { + FamilyGeneratorReleaseV1::DeclaredFiniteImageV1 => 1, + #[cfg(test)] + FamilyGeneratorReleaseV1::EncodedSrgb8EqualChannelAxisV1 => 2, + #[cfg(test)] + FamilyGeneratorReleaseV1::EncodedSrgb8RedBlueDiagonalV1 => 3, + #[cfg(test)] + FamilyGeneratorReleaseV1::NonInjectiveUnorderedFixtureV1 => 4, + } +} + +const fn proof_release_tag(release: FamilyImageProofReleaseV1) -> u8 { + match release { + FamilyImageProofReleaseV1::DeclaredImageIsDefinitionV1 => 1, + #[cfg(test)] + FamilyImageProofReleaseV1::ExhaustiveCanonicalImageComparisonV1 => 2, + } +} + +fn generator_content_identity_for( + descriptor: FamilyGeneratorDescriptorV1, + output: &[ColorSignal], +) -> Result { + let first = output + .first() + .copied() + .ok_or(FamilyImageErrorV1::EmptyGeneratorDomain)?; + let count = u64::try_from(output.len()).map_err(|_| FamilyImageErrorV1::ResourceExhausted)?; + let mut hasher = Hasher::new(); + hasher.update(GENERATOR_DOMAIN_V1); + hasher.update(&[ + generator_release_tag(descriptor.release()), + output_profile_tag(first.output_profile()) as u8, + ]); + descriptor.update_parameter_identity(&mut hasher); + hasher.update(&count.to_be_bytes()); + for (ordinal, member) in (0_u64..).zip(output.iter().copied()) { + hasher.update(&ordinal.to_be_bytes()); + hasher.update(&[output_profile_tag(member.output_profile()) as u8]); + hasher.update(&member.srgb8().bytes()); + } + Ok(FamilyGeneratorContentIdentityV1( + *hasher.finalize().as_bytes(), + )) +} + +enum GeneratorReplayV1<'a> { + Declared(&'a [ColorSignal]), + #[cfg(test)] + Generated(Vec), +} + +impl FamilyGeneratorDescriptorV1 { + const fn release(self) -> FamilyGeneratorReleaseV1 { + match self { + Self::DeclaredFiniteImageV1 => FamilyGeneratorReleaseV1::DeclaredFiniteImageV1, + #[cfg(test)] + Self::EncodedSrgb8EqualChannelAxisV1 => { + FamilyGeneratorReleaseV1::EncodedSrgb8EqualChannelAxisV1 + } + #[cfg(test)] + Self::EncodedSrgb8RedBlueDiagonalV1 => { + FamilyGeneratorReleaseV1::EncodedSrgb8RedBlueDiagonalV1 + } + #[cfg(test)] + Self::NonInjectiveUnorderedFixtureV1 { .. } => { + FamilyGeneratorReleaseV1::NonInjectiveUnorderedFixtureV1 + } + } + } + + fn update_parameter_identity(self, hasher: &mut Hasher) { + hasher.update(GENERATOR_PARAMETERS_DOMAIN_V1); + match self { + Self::DeclaredFiniteImageV1 => { + hasher.update(&[GENERATOR_PARAMETERS_CODEC_V1, 0]); + } + #[cfg(test)] + Self::EncodedSrgb8EqualChannelAxisV1 => { + hasher.update(&[GENERATOR_PARAMETERS_CODEC_V1, 0]); + } + #[cfg(test)] + Self::EncodedSrgb8RedBlueDiagonalV1 => { + hasher.update(&[GENERATOR_PARAMETERS_CODEC_V1, 0]); + } + #[cfg(test)] + Self::NonInjectiveUnorderedFixtureV1 { provenance, .. } => { + // Перестановку связывает сам ordinal-output ниже. Здесь остаётся + // независимый parameter provenance, чтобы тесты не могли + // взаимно маскировать две части generator identity. + hasher.update(&[GENERATOR_PARAMETERS_CODEC_V1, 1, provenance]); + } + } + } + + fn replay( + self, + declared_members: &[ColorSignal], + ) -> Result, FamilyImageErrorV1> { + match self { + Self::DeclaredFiniteImageV1 => Ok(GeneratorReplayV1::Declared(declared_members)), + #[cfg(test)] + Self::EncodedSrgb8EqualChannelAxisV1 => Ok(GeneratorReplayV1::Generated( + CompleteFamilyGeneratorV1::EncodedSrgb8EqualChannelAxisV1 + .into_complete_output()? + .members, + )), + #[cfg(test)] + Self::EncodedSrgb8RedBlueDiagonalV1 => Ok(GeneratorReplayV1::Generated( + CompleteFamilyGeneratorV1::EncodedSrgb8RedBlueDiagonalV1 + .into_complete_output()? + .members, + )), + #[cfg(test)] + Self::NonInjectiveUnorderedFixtureV1 { + permuted, + provenance, + } => Ok(GeneratorReplayV1::Generated( + CompleteFamilyGeneratorV1::NonInjectiveUnorderedFixtureV1 { + permuted, + provenance, + } + .into_complete_output()? + .members, + )), + } + } +} diff --git a/crates/labcolors-core/src/family_tests.rs b/crates/labcolors-core/src/family_tests.rs new file mode 100644 index 00000000..c5f16a88 --- /dev/null +++ b/crates/labcolors-core/src/family_tests.rs @@ -0,0 +1,527 @@ +//! Контракт точного образа генератора family, которым владеет код. + +use crate::Srgb8; +use crate::constraints::HardDecision; +use crate::family::{ + AdmittedFamilySetV1, CompleteFamilyGeneratorV1, FamilyImageErrorV1, FamilyImageProofReleaseV1, + FamilyMembershipPassV1, FamilyMembershipViolationV1, UnverifiedFamilyImageV1, + admit_declared_family_image_v1, verify_complete_family_image_v1, +}; +use crate::lcs_occurrence::ColorSignal; +use proptest::prelude::*; +use std::collections::BTreeSet; + +fn signal(bytes: [u8; 3]) -> ColorSignal { + ColorSignal::from_srgb8(Srgb8::new(bytes)) +} + +fn is_member(admitted: &AdmittedFamilySetV1, value: ColorSignal) -> bool { + matches!(admitted.assess(value).1, HardDecision::Pass(_)) +} + +#[test] +fn verifier_rejects_both_missing_and_extraneous_image_members() { + let generator = CompleteFamilyGeneratorV1::encoded_srgb8_equal_channel_axis_v1(); + let missing = signal([0, 0, 0]); + let extraneous = signal([0, 0, 1]); + let mut proposed = (1_u16..=255) + .map(|value| { + let value = value as u8; + signal([value, value, value]) + }) + .collect::>(); + proposed.push(extraneous); + + assert_eq!( + verify_complete_family_image_v1(generator, UnverifiedFamilyImageV1::new(proposed)), + Err(FamilyImageErrorV1::ImageMismatch { + missing: Some(missing), + extraneous: Some(extraneous), + }), + ); +} + +#[test] +fn exact_axis_and_chromatic_fixture_use_one_verifier_and_membership_law() { + let axis = CompleteFamilyGeneratorV1::encoded_srgb8_equal_channel_axis_v1(); + let axis_image = UnverifiedFamilyImageV1::new( + (0_u16..=255) + .map(|value| { + let value = value as u8; + signal([value; 3]) + }) + .collect(), + ); + let axis = verify_complete_family_image_v1(axis, axis_image).unwrap(); + let chromatic = CompleteFamilyGeneratorV1::encoded_srgb8_red_blue_diagonal_v1(); + let chromatic_image = UnverifiedFamilyImageV1::new( + (0_u16..=255) + .map(|value| { + let value = value as u8; + signal([value, 0, 255 - value]) + }) + .collect(), + ); + let chromatic = verify_complete_family_image_v1(chromatic, chromatic_image).unwrap(); + + assert_eq!(axis.certificate().member_count(), 256); + assert_eq!(chromatic.certificate().member_count(), 256); + assert_ne!( + axis.certificate().family_content_identity(), + chromatic.certificate().family_content_identity(), + ); + for value in 0_u16..=255 { + let value = value as u8; + assert!(matches!( + axis.assess(signal([value, value, value])).1, + HardDecision::Pass(_), + )); + assert!(matches!( + chromatic.assess(signal([value, 0, 255 - value])).1, + HardDecision::Pass(_), + )); + } +} + +#[test] +#[ignore = "full 24-bit domain oracle runs once in CI outside mutation tests"] +fn axis_membership_matches_the_full_srgb8_cube_oracle() { + let generator = CompleteFamilyGeneratorV1::encoded_srgb8_equal_channel_axis_v1(); + let image = UnverifiedFamilyImageV1::new( + (0_u16..=255) + .map(|value| { + let value = value as u8; + signal([value; 3]) + }) + .collect(), + ); + let admitted = verify_complete_family_image_v1(generator, image).unwrap(); + + for red in 0_u16..=255 { + for green in 0_u16..=255 { + for blue in 0_u16..=255 { + let bytes = [red as u8, green as u8, blue as u8]; + assert_eq!( + is_member(&admitted, signal(bytes)), + bytes[0] == bytes[1] && bytes[1] == bytes[2], + "full-domain disagreement at {bytes:?}", + ); + } + } + } +} + +#[test] +fn proposed_representation_order_and_duplicates_do_not_change_the_image() { + let generator = CompleteFamilyGeneratorV1::encoded_srgb8_equal_channel_axis_v1(); + let canonical = UnverifiedFamilyImageV1::new( + (0_u16..=255) + .map(|value| { + let value = value as u8; + signal([value; 3]) + }) + .collect(), + ); + let expected = verify_complete_family_image_v1(generator.clone(), canonical).unwrap(); + let mut reordered = (0_u16..=255) + .rev() + .flat_map(|value| { + let value = value as u8; + [signal([value; 3]), signal([value; 3])] + }) + .collect::>(); + reordered.rotate_left(73); + let actual = + verify_complete_family_image_v1(generator, UnverifiedFamilyImageV1::new(reordered)) + .unwrap(); + + assert_eq!( + expected.certificate().image_content_identity(), + actual.certificate().image_content_identity(), + ); + assert_eq!( + expected.certificate().family_content_identity(), + actual.certificate().family_content_identity(), + ); + for value in [ + signal([0; 3]), + signal([127; 3]), + signal([255; 3]), + signal([1, 2, 3]), + ] { + assert_eq!(is_member(&expected, value), is_member(&actual, value)); + } +} + +#[test] +fn production_declared_set_is_nonempty_canonical_and_content_addressed() { + assert_eq!( + admit_declared_family_image_v1(Vec::new()), + Err(FamilyImageErrorV1::EmptyGeneratorDomain), + ); + let first = admit_declared_family_image_v1(vec![ + signal([9, 8, 7]), + signal([1, 2, 3]), + signal([9, 8, 7]), + ]) + .unwrap(); + let second = + admit_declared_family_image_v1(vec![signal([1, 2, 3]), signal([9, 8, 7])]).unwrap(); + + assert_eq!(first.certificate(), second.certificate()); + assert_eq!(first.certificate().member_count(), 2); + assert_eq!( + first.certificate().proof_release(), + FamilyImageProofReleaseV1::DeclaredImageIsDefinitionV1, + ); + assert!(is_member(&first, signal([1, 2, 3]))); + assert!(!is_member(&first, signal([1, 2, 4]))); +} + +#[test] +fn membership_proofs_carry_no_storage_coordinates() { + assert_eq!(core::mem::size_of::(), 0); + assert_eq!(core::mem::size_of::(), 0,); +} + +#[test] +fn declared_family_codec_matches_independent_sha256_golden_v1() { + let admitted = admit_declared_family_image_v1(vec![ + signal([9, 8, 7]), + signal([1, 2, 3]), + signal([9, 8, 7]), + ]) + .unwrap(); + let certificate = admitted.certificate(); + + // These bytes come from an independent Python hashlib construction of the + // documented V1 preimages. Changing them requires an explicit codec/release + // decision; deriving them through this Rust path would make the oracle vacuous. + assert_eq!( + *certificate.generator_content_identity().as_bytes(), + [ + 0x5a, 0xa1, 0x0b, 0x2e, 0xa4, 0xa7, 0x6a, 0x55, 0x47, 0x9c, 0xa0, 0xec, 0xd8, 0xce, + 0xba, 0x04, 0x7e, 0x8d, 0xc4, 0x4d, 0xd4, 0x9f, 0x8b, 0x16, 0x08, 0x35, 0x3d, 0xfa, + 0xbe, 0xe8, 0x74, 0xf2, + ], + ); + assert_eq!( + *certificate.image_content_identity().as_bytes(), + [ + 0xe0, 0x1b, 0xaa, 0xb6, 0x6b, 0x05, 0x8f, 0x96, 0xaa, 0x45, 0x12, 0xc1, 0x36, 0xfa, + 0x49, 0xdf, 0x71, 0x57, 0xb4, 0x19, 0x35, 0x56, 0xd0, 0x40, 0xf1, 0x60, 0xaf, 0x00, + 0x03, 0x29, 0xf3, 0x00, + ], + ); + assert_eq!( + *certificate.family_content_identity().as_bytes(), + [ + 0x9d, 0xb4, 0x15, 0xca, 0xa4, 0x84, 0x0b, 0x3b, 0xb3, 0xd0, 0x94, 0x61, 0x11, 0x43, + 0x02, 0x9b, 0x80, 0xe1, 0x78, 0xbc, 0x58, 0x9d, 0x5b, 0x33, 0x5f, 0x02, 0x76, 0x44, + 0xf5, 0x97, 0x28, 0x83, + ], + ); +} + +#[test] +fn image_identity_changes_when_one_canonical_member_changes() { + let first = admit_declared_family_image_v1(vec![signal([1, 2, 3]), signal([9, 8, 7])]).unwrap(); + let second = + admit_declared_family_image_v1(vec![signal([1, 2, 4]), signal([9, 8, 7])]).unwrap(); + + assert_eq!( + first.certificate().generator_release(), + second.certificate().generator_release(), + ); + assert_ne!( + first.certificate().image_content_identity(), + second.certificate().image_content_identity(), + ); + assert_ne!( + first.certificate().family_content_identity(), + second.certificate().family_content_identity(), + ); +} + +#[test] +fn identical_image_under_distinct_generator_releases_has_distinct_provenance() { + let image = (0_u16..=255) + .map(|value| { + let value = value as u8; + signal([value; 3]) + }) + .collect::>(); + let declared = admit_declared_family_image_v1(image.clone()).unwrap(); + let fixture_generator = CompleteFamilyGeneratorV1::encoded_srgb8_equal_channel_axis_v1(); + let fixture = + verify_complete_family_image_v1(fixture_generator, UnverifiedFamilyImageV1::new(image)) + .unwrap(); + + assert_eq!( + declared.certificate().image_content_identity(), + fixture.certificate().image_content_identity(), + ); + assert_ne!( + declared.certificate().generator_content_identity(), + fixture.certificate().generator_content_identity(), + ); + assert_ne!( + declared.certificate().family_content_identity(), + fixture.certificate().family_content_identity(), + ); + assert_eq!( + fixture.certificate().proof_release(), + FamilyImageProofReleaseV1::ExhaustiveCanonicalImageComparisonV1, + ); +} + +#[test] +fn proof_release_distinguishes_definition_from_exhaustive_verification() { + let image = vec![signal([9, 8, 7]), signal([1, 2, 3])]; + let direct = admit_declared_family_image_v1(image.clone()).unwrap(); + let generator = CompleteFamilyGeneratorV1::try_declared_finite_image_v1(image).unwrap(); + let exhaustive = verify_complete_family_image_v1( + generator, + UnverifiedFamilyImageV1::new(vec![signal([1, 2, 3]), signal([9, 8, 7])]), + ) + .unwrap(); + + assert_eq!( + direct.certificate().generator_content_identity(), + exhaustive.certificate().generator_content_identity(), + ); + assert_eq!( + direct.certificate().image_content_identity(), + exhaustive.certificate().image_content_identity(), + ); + assert_ne!( + direct.certificate().proof_release(), + exhaustive.certificate().proof_release(), + ); + assert_ne!( + direct.certificate().family_content_identity(), + exhaustive.certificate().family_content_identity(), + ); +} + +#[test] +fn noninjective_unordered_generator_binds_preimage_and_canonical_image_separately() { + let generator = CompleteFamilyGeneratorV1::noninjective_unordered_fixture_v1(); + let admitted = verify_complete_family_image_v1( + generator, + UnverifiedFamilyImageV1::new(vec![signal([10; 3]), signal([20; 3])]), + ) + .unwrap(); + + assert_eq!(admitted.certificate().preimage_count(), 4); + assert_eq!(admitted.certificate().member_count(), 2); + assert!(is_member(&admitted, signal([10; 3]))); + assert!(is_member(&admitted, signal([20; 3]))); + assert!(!is_member(&admitted, signal([15; 3]))); + admitted.verify().unwrap(); +} + +#[test] +fn generator_identity_binds_ordinal_mapping_within_one_release() { + let first_generator = CompleteFamilyGeneratorV1::noninjective_unordered_fixture_v1(); + let second_generator = CompleteFamilyGeneratorV1::permuted_noninjective_unordered_fixture_v1(); + let proposed = || UnverifiedFamilyImageV1::new(vec![signal([10; 3]), signal([20; 3])]); + let first = verify_complete_family_image_v1(first_generator, proposed()).unwrap(); + let second = verify_complete_family_image_v1(second_generator, proposed()).unwrap(); + + assert_eq!( + first.certificate().generator_release(), + second.certificate().generator_release(), + ); + assert_eq!( + first.certificate().image_content_identity(), + second.certificate().image_content_identity(), + ); + assert_eq!( + first.certificate().preimage_count(), + second.certificate().preimage_count(), + ); + assert_ne!( + first.certificate().generator_content_identity(), + second.certificate().generator_content_identity(), + ); + assert_ne!( + first.certificate().family_content_identity(), + second.certificate().family_content_identity(), + ); +} + +#[test] +fn generator_identity_binds_parameters_even_when_quantized_output_is_identical() { + let first_generator = CompleteFamilyGeneratorV1::noninjective_fixture_with_provenance_v1(7); + let second_generator = CompleteFamilyGeneratorV1::noninjective_fixture_with_provenance_v1(8); + let proposed = || UnverifiedFamilyImageV1::new(vec![signal([10; 3]), signal([20; 3])]); + let first = verify_complete_family_image_v1(first_generator, proposed()).unwrap(); + let second = verify_complete_family_image_v1(second_generator, proposed()).unwrap(); + + assert_eq!( + first.certificate().generator_release(), + second.certificate().generator_release(), + ); + assert_eq!( + first.certificate().image_content_identity(), + second.certificate().image_content_identity(), + ); + assert_ne!( + first.certificate().generator_content_identity(), + second.certificate().generator_content_identity(), + ); + assert_ne!( + first.certificate().family_content_identity(), + second.certificate().family_content_identity(), + ); +} + +#[test] +fn membership_measurement_carries_family_and_signal_for_every_exact_verdict() { + let admitted = admit_declared_family_image_v1(vec![signal([10; 3]), signal([20; 3])]).unwrap(); + let expected_family = admitted.certificate().family_content_identity(); + let mut expected_violation = None; + for query in [signal([0; 3]), signal([15; 3]), signal([30; 3])] { + let (measurement, HardDecision::Violation(proof)) = admitted.assess(query) else { + panic!("every query is outside the declared set"); + }; + assert_eq!(measurement.family(), expected_family); + assert_eq!(measurement.signal(), query); + if let Some(expected) = expected_violation { + assert_eq!(proof, expected); + } else { + expected_violation = Some(proof); + } + } + + let mut expected_pass = None; + for included in [signal([10; 3]), signal([20; 3])] { + let (measurement, HardDecision::Pass(proof)) = admitted.assess(included) else { + panic!("declared member must pass"); + }; + assert_eq!(measurement.family(), expected_family); + assert_eq!(measurement.signal(), included); + if let Some(expected) = expected_pass { + assert_eq!(proof, expected); + } else { + expected_pass = Some(proof); + } + } +} + +#[test] +fn corrupted_admitted_storage_fails_closed_before_program_use() { + let mut admitted = admit_declared_family_image_v1(vec![signal([1, 2, 3])]).unwrap(); + admitted.corrupt_first_member_for_test(signal([4, 5, 6])); + + assert_eq!( + admitted.verify(), + Err(FamilyImageErrorV1::CertificateMismatch) + ); +} + +#[test] +fn corrupted_generated_image_fails_independent_generator_replay() { + let generator = CompleteFamilyGeneratorV1::encoded_srgb8_equal_channel_axis_v1(); + let proposed = generator.clone().into_complete_output().unwrap(); + let mut admitted = verify_complete_family_image_v1(generator, proposed).unwrap(); + // Остаётся строго между прежними first и second members, поэтому отказ + // доказывает replay generator-а, а не только проверку сортировки. + admitted.corrupt_first_member_for_test(signal([0, 0, 1])); + + assert_eq!( + admitted.verify(), + Err(FamilyImageErrorV1::CertificateMismatch), + ); +} + +#[test] +fn coherent_but_unadmitted_proof_release_fails_closed() { + let generator = CompleteFamilyGeneratorV1::encoded_srgb8_equal_channel_axis_v1(); + let proposed = generator.clone().into_complete_output().unwrap(); + let mut admitted = verify_complete_family_image_v1(generator, proposed).unwrap(); + admitted.recertify_proof_for_test(FamilyImageProofReleaseV1::DeclaredImageIsDefinitionV1); + + assert_eq!( + admitted.verify(), + Err(FamilyImageErrorV1::CertificateMismatch), + ); +} + +#[test] +fn coherent_but_wrong_preimage_count_fails_generator_replay() { + let generator = CompleteFamilyGeneratorV1::encoded_srgb8_equal_channel_axis_v1(); + let proposed = generator.clone().into_complete_output().unwrap(); + let mut admitted = verify_complete_family_image_v1(generator, proposed).unwrap(); + admitted.recertify_preimage_count_for_test(257); + + assert_eq!( + admitted.verify(), + Err(FamilyImageErrorV1::CertificateMismatch), + ); +} + +proptest! { + #[test] + fn declared_membership_matches_an_independent_btree_oracle( + raw in prop::collection::vec(any::<[u8; 3]>(), 1..512), + queries in prop::collection::vec(any::<[u8; 3]>(), 0..256), + ) { + let oracle = raw.iter().copied().collect::>(); + let admitted = admit_declared_family_image_v1( + raw.into_iter().map(signal).collect(), + ).unwrap(); + + for query in queries { + prop_assert_eq!(is_member(&admitted, signal(query)), oracle.contains(&query)); + } + } +} + +#[test] +fn repeated_membership_assessment_allocates_nothing() { + let admitted = admit_declared_family_image_v1(vec![signal([100, 100, 100])]).unwrap(); + + let (_, allocations) = crate::test_support::measured_allocations(|| { + let mut checksum = 0_usize; + for value in [ + signal([0, 0, 0]), + signal([100, 100, 100]), + signal([100, 100, 101]), + signal([255, 255, 255]), + ] { + let (measurement, decision) = admitted.assess(value); + checksum ^= measurement.signal().srgb8().bytes()[0] as usize; + checksum ^= match decision { + HardDecision::Pass(_) => 0xA5, + HardDecision::Violation(_) => 0x5A, + }; + } + checksum + }); + assert_eq!(allocations, 0); +} + +#[test] +fn fixture_materializers_match_independent_formulas() { + for generator in [ + CompleteFamilyGeneratorV1::encoded_srgb8_equal_channel_axis_v1(), + CompleteFamilyGeneratorV1::encoded_srgb8_red_blue_diagonal_v1(), + ] { + let generated = generator.clone().into_complete_output().unwrap(); + let admitted = verify_complete_family_image_v1(generator.clone(), generated).unwrap(); + for value in 0_u16..=255 { + let value = value as u8; + let expected = match generator { + CompleteFamilyGeneratorV1::EncodedSrgb8EqualChannelAxisV1 => signal([value; 3]), + CompleteFamilyGeneratorV1::EncodedSrgb8RedBlueDiagonalV1 => { + signal([value, 0, 255 - value]) + } + CompleteFamilyGeneratorV1::DeclaredFiniteImageV1 { .. } => unreachable!(), + CompleteFamilyGeneratorV1::NonInjectiveUnorderedFixtureV1 { .. } => unreachable!(), + }; + assert!(is_member(&admitted, expected)); + } + } +} diff --git a/crates/labcolors-core/src/generic_boundary_tests.rs b/crates/labcolors-core/src/generic_boundary_tests.rs index 64953966..ce1e4a67 100644 --- a/crates/labcolors-core/src/generic_boundary_tests.rs +++ b/crates/labcolors-core/src/generic_boundary_tests.rs @@ -8,6 +8,8 @@ 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 FAMILY_CONSTRAINT_SOURCE: &str = include_str!("constraints/family.rs"); +const FAMILY_SOURCE: &str = include_str!("family.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"); @@ -23,9 +25,11 @@ 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); 7] = [ +const GENERIC_SOURCES: [(&str, &str); 9] = [ ("appearance.rs", APPEARANCE_SOURCE), + ("constraints/family.rs", FAMILY_CONSTRAINT_SOURCE), ("constraints/relation.rs", RELATION_CONSTRAINT_SOURCE), + ("family.rs", FAMILY_SOURCE), ("lcs_occurrence.rs", LCS_OCCURRENCE_SOURCE), ("program/attachment.rs", PROGRAM_ATTACHMENT_SOURCE), ("program_identity.rs", PROGRAM_IDENTITY_SOURCE), @@ -270,7 +274,12 @@ fn production_rust_sources() -> Vec<(String, String)> { #[test] fn generic_source_inventory_covers_relation_topology_and_evaluators() { - for required in ["relation.rs", "constraints/relation.rs"] { + for required in [ + "family.rs", + "constraints/family.rs", + "relation.rs", + "constraints/relation.rs", + ] { assert!( GENERIC_SOURCES.iter().any(|(path, _)| *path == required), "generic vocabulary ratchet must cover {required}", @@ -1563,11 +1572,16 @@ fn program_identity_binds_lcs_releases_only_through_lcs_constraint_content() { "DOMAIN_V5", "PROGRAM_SCHEMA_V5", "compile_program_content_identity_v5", + "ProgramContentIdentityV6", + "ContentIdentityV6", + "DOMAIN_V6", + "PROGRAM_SCHEMA_V6", + "compile_program_content_identity_v6", ] { for (path, source) in identity_sources { assert!( !contains_rust_identifier(source, retired), - "the V6 content-address cut must not retain legacy identity symbol `{retired}` in {path}", + "the V7 content-address cut must not retain legacy identity symbol `{retired}` in {path}", ); } } @@ -1576,13 +1590,14 @@ fn program_identity_binds_lcs_releases_only_through_lcs_constraint_content() { 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")); + assert!(!PROGRAM_IDENTITY_SOURCE.contains("labcolors.program-content-identity.v6")); for required in [ - "const DOMAIN_V6: &[u8] = b\"labcolors.program-content-identity.v6\\0\";", - "pub(super) const PROGRAM_SCHEMA_V6: u8 = 6;", + "const DOMAIN_V7: &[u8] = b\"labcolors.program-content-identity.v7\\0\";", + "pub(super) const PROGRAM_SCHEMA_V7: u8 = 7;", ] { assert!( PROGRAM_IDENTITY_SOURCE.contains(required), - "the V6 content-address type must bind its exact domain and schema tag; missing `{required}`", + "the V7 content-address type must bind its exact domain and schema tag; missing `{required}`", ); } diff --git a/crates/labcolors-core/src/lib.rs b/crates/labcolors-core/src/lib.rs index 5c7c0c83..90c7b7a5 100644 --- a/crates/labcolors-core/src/lib.rs +++ b/crates/labcolors-core/src/lib.rs @@ -9,6 +9,7 @@ pub mod wcag22_evidence; pub(crate) mod clean_set; pub(crate) mod composition; +mod family; pub(crate) mod spaces; pub use srgb8::Srgb8; @@ -128,6 +129,9 @@ mod program_clean_set_tests; #[cfg(test)] mod program_relation_tests; +#[cfg(test)] +mod program_family_tests; + #[cfg(test)] mod release_registry_tests; @@ -166,6 +170,9 @@ pub(crate) mod joint; #[cfg(test)] mod constraint_tests; +#[cfg(test)] +mod family_tests; + #[cfg(test)] mod clean_set_tests; diff --git a/crates/labcolors-core/src/program.rs b/crates/labcolors-core/src/program.rs index 2e93cbcc..e7522146 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`] //! хранит исчерпывающий конфликт по всем рассмотренным состояниям. -//! [`ContentIdentityV6`] идентифицирует каноническое содержание, но не даёт +//! [`ContentIdentityV7`] идентифицирует каноническое содержание, но не даёт //! полномочий живого [`OwnerV1`]. #![forbid(unreachable_pub)] @@ -45,6 +45,12 @@ use crate::constraints::{ CoreRelationViolationV1, ExactSrgb8IdentityV1, ProgramVisiblePointBindingV1, ProgramVisiblePointPassEvidence, ProgramVisiblePointViolationEvidence, Wcag22Srgb8V1, }; +use crate::family::{ + AdmittedFamilySetV1, FamilyContentIdentityV1 as CoreFamilyContentIdentityV1, + FamilyDeclarationV1, FamilyId, FamilyImageErrorV1, + FamilyMembershipMeasurementV1 as CoreFamilyMembershipMeasurementV1, + admit_declared_family_image_v1, +}; use crate::joint::FiniteJointOrderErrorV1; use crate::lcs_occurrence::{ AdaptingLuminanceCdM2, AppearanceContextDomainErrorV1, @@ -69,7 +75,7 @@ use crate::program_session::{ PointPresentationRootV1, PointPresentationTargetV1, PresentationRootId, ProgramCompileError, ProgramConflictV1, ProgramConstraintCellV1, ProgramConstraintPassEvidenceV1, ProgramConstraintResultV1, ProgramConstraintSubjectV1, ProgramConstraintViolationEvidenceV1, - ProgramContentIdentityV6, ProgramIntrinsicPaintBindingV1, ProgramIntrinsicUnaryPassEvidenceV1, + ProgramContentIdentityV7, ProgramIntrinsicPaintBindingV1, ProgramIntrinsicUnaryPassEvidenceV1, ProgramIntrinsicUnaryViolationEvidenceV1, ProgramPaintOutputV1, ProgramRelationMemberDecisionV1, ProgramRelationMemberEvidenceV1, ProgramReportV1, ProgramSessionEvaluationError, ProgramSessionInstantiateError, ProgramSessionPlan, @@ -172,6 +178,11 @@ authored_id!( SourceId ); authored_id!("Идентификатор решаемой цели.", TargetIdV1, TargetId); +authored_id!( + "Идентификатор объявленного family-set.", + FamilyIdV1, + FamilyId +); authored_id!( "Идентификатор конечного кандидата одной цели.", TargetCandidateIdV1, @@ -214,6 +225,55 @@ projected_id!( StreamIdV1, ObservationStreamId ); + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct FamilySetV1(AdmittedFamilySetV1); + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum FamilySetAdmissionErrorV1 { + Empty, + ResourceExhausted, + InternalInvariant, +} + +impl FamilySetV1 { + pub(crate) fn try_from_srgb8_image( + image: Vec, + ) -> Result { + let mut signals = Vec::new(); + signals + .try_reserve_exact(image.len()) + .map_err(|_| FamilySetAdmissionErrorV1::ResourceExhausted)?; + signals.extend(image.into_iter().map(ColorSignal::from_srgb8)); + admit_declared_family_image_v1(signals) + .map(Self) + .map_err(|error| match error { + FamilyImageErrorV1::EmptyGeneratorDomain => FamilySetAdmissionErrorV1::Empty, + FamilyImageErrorV1::ResourceExhausted => { + FamilySetAdmissionErrorV1::ResourceExhausted + } + FamilyImageErrorV1::NonCanonicalAdmittedImage + | FamilyImageErrorV1::CertificateMismatch => { + FamilySetAdmissionErrorV1::InternalInvariant + } + #[cfg(test)] + FamilyImageErrorV1::ImageMismatch { .. } => { + FamilySetAdmissionErrorV1::InternalInvariant + } + }) + } + + /// Возвращает адрес неизменяемого сертификата допущенного family-set. + pub(crate) const fn content_identity(&self) -> FamilyContentIdentityV1 { + FamilyContentIdentityV1::from_core(self.0.certificate().family_content_identity()) + } + + #[cfg(test)] + pub(crate) fn corrupt_first_member_for_test(&mut self, replacement: Srgb8) { + self.0 + .corrupt_first_member_for_test(ColorSignal::from_srgb8(replacement)); + } +} projected_id!( "Идентификатор сценария, сохранённый как provenance.", ScenarioIdV1, @@ -502,6 +562,12 @@ pub(crate) enum CompileErrorKindV1 { DuplicateSource, /// Повторно объявлена цель. DuplicateTarget, + /// Повторно объявлен family-set. + DuplicateFamily, + /// Сертификат family-set не прошёл полный replay. + InvalidFamilyImage, + /// Family-set объявлен, но не связан ни с одним ограничением. + UnusedFamily, /// В одной цели повторно объявлен ID кандидата. DuplicateTargetCandidate, /// Два кандидата одной цели задают одинаковое атомарное Paint value. @@ -560,6 +626,8 @@ pub(crate) enum CompileErrorKindV1 { MissingConstraintOccurrence, /// Intrinsic-unary ограничение ссылается на отсутствующую Target. MissingIntrinsicUnaryTarget, + /// Family membership ссылается на отсутствующий family-set. + MissingConstraintFamily, /// Reference intrinsic-отношения ссылается на отсутствующую Target. MissingIntrinsicRelationReference, /// Candidate intrinsic-отношения ссылается на отсутствующую Target. @@ -623,6 +691,8 @@ pub(crate) enum CompileErrorHandleV1 { Source(SourceIdV1), /// Цель. Target(TargetIdV1), + /// Точное множество family. + Family(FamilyIdV1), /// Кандидат цели. TargetCandidate(TargetCandidateIdV1), /// Вход прозрачности. @@ -649,6 +719,7 @@ impl CompileErrorHandleV1 { match self { Self::Source(value) => value.value(), Self::Target(value) => value.value(), + Self::Family(value) => value.value(), Self::TargetCandidate(value) => value.value(), Self::OpacityInput(value) => value.value(), Self::Paint(value) => value.value(), @@ -756,6 +827,21 @@ pub(crate) enum CompileErrorV1 { /// Повторный ID. target: TargetIdV1, }, + /// Повторно объявлен один opaque family ID. + DuplicateFamily { + /// Повторный family ID. + family: FamilyIdV1, + }, + /// Допущенный образ family не прошёл повторную верификацию. + InvalidFamilyImage { + /// Ошибочный family ID. + family: FamilyIdV1, + }, + /// Объявленный family-set не используется ни одним constraint. + UnusedFamily { + /// Неиспользуемый family ID. + family: FamilyIdV1, + }, /// Фиксированная цель ссылается на отсутствующий исходный сигнал. MissingFixedSource { /// Ошибочная фиксированная цель. @@ -1007,6 +1093,13 @@ pub(crate) enum CompileErrorV1 { /// Отсутствующая Target. target: TargetIdV1, }, + /// Ограничение принадлежности ссылается на отсутствующий family-set. + MissingConstraintFamily { + /// Ошибочное ограничение. + constraint: ConstraintIdV1, + /// Отсутствующий family ID. + family: FamilyIdV1, + }, /// Reference intrinsic-отношения ссылается на отсутствующую Target. MissingIntrinsicRelationReference { /// Ошибочное ограничение. @@ -1086,6 +1179,9 @@ impl CompileErrorV1 { match self { Self::DuplicateSource { .. } => Kind::DuplicateSource, Self::DuplicateTarget { .. } => Kind::DuplicateTarget, + Self::DuplicateFamily { .. } => Kind::DuplicateFamily, + Self::InvalidFamilyImage { .. } => Kind::InvalidFamilyImage, + Self::UnusedFamily { .. } => Kind::UnusedFamily, Self::MissingFixedSource { .. } => Kind::MissingFixedSource, Self::DuplicateOpacityInput { .. } => Kind::DuplicateOpacityInput, Self::DuplicateSurfaceInputPort { .. } => Kind::DuplicateSurfaceInputPort, @@ -1139,6 +1235,7 @@ impl CompileErrorV1 { Self::DuplicateConstraint { .. } => Kind::DuplicateConstraint, Self::MissingConstraintOccurrence { .. } => Kind::MissingConstraintOccurrence, Self::MissingIntrinsicUnaryTarget { .. } => Kind::MissingIntrinsicUnaryTarget, + Self::MissingConstraintFamily { .. } => Kind::MissingConstraintFamily, Self::MissingIntrinsicRelationReference { .. } => { Kind::MissingIntrinsicRelationReference } @@ -1178,6 +1275,9 @@ impl CompileErrorV1 { | Self::MissingFixedSource { target, .. } | Self::DuplicateTargetCandidate { target, .. } | Self::DuplicateTargetCandidateValue { target, .. } => Some(Handle::Target(*target)), + Self::DuplicateFamily { family } + | Self::InvalidFamilyImage { family } + | Self::UnusedFamily { family } => Some(Handle::Family(*family)), Self::DuplicateOpacityInput { input } | Self::OpacityOutOfDomain { input } => { Some(Handle::OpacityInput(*input)) } @@ -1214,6 +1314,7 @@ impl CompileErrorV1 { Self::DuplicateConstraint { constraint } | Self::MissingConstraintOccurrence { constraint, .. } | Self::MissingIntrinsicUnaryTarget { constraint, .. } + | Self::MissingConstraintFamily { constraint, .. } | Self::MissingIntrinsicRelationReference { constraint, .. } | Self::MissingIntrinsicRelationCandidate { constraint, .. } | Self::MissingVisibleRelationReference { constraint, .. } @@ -1279,6 +1380,7 @@ impl CompileErrorV1 { Self::MissingIntrinsicRelationCandidate { candidate, .. } => { Some(Handle::Target(*candidate)) } + Self::MissingConstraintFamily { family, .. } => Some(Handle::Family(*family)), Self::SolverDependentVisibleRelationReference { target, .. } => { Some(Handle::Target(*target)) } @@ -1296,6 +1398,9 @@ impl CompileErrorV1 { } Self::DuplicateSource { .. } | Self::DuplicateTarget { .. } + | Self::DuplicateFamily { .. } + | Self::InvalidFamilyImage { .. } + | Self::UnusedFamily { .. } | Self::DuplicateOpacityInput { .. } | Self::DuplicateSurfaceInputPort { .. } | Self::UnusedSurfaceInputPort { .. } @@ -1374,6 +1479,13 @@ impl DraftV1 { self } + /// Объявляет одно точное допущенное множество без клиентской семантики. + pub(crate) fn push_family(&mut self, id: FamilyIdV1, set: FamilySetV1) -> &mut Self { + self.inner + .push_family(FamilyDeclarationV1::new(id.into_core(), set.0)); + self + } + /// Один раз задаёт полный порядок совместных состояний конечных целей. pub(crate) fn set_joint_selection( &mut self, @@ -1546,6 +1658,36 @@ impl DraftV1 { self } + /// Требует принадлежности исходного сигнала объявленному точному множеству. + pub(crate) fn push_intrinsic_family_membership_hard( + &mut self, + id: ConstraintIdV1, + target: TargetIdV1, + family: FamilyIdV1, + ) -> &mut Self { + self.inner.push_intrinsic_family_membership_hard( + id.into_core(), + target.into_core(), + family.into_core(), + ); + self + } + + /// Диагностирует принадлежность без права исключать состояние-кандидат. + pub(crate) fn push_intrinsic_family_membership_report_only( + &mut self, + id: ConstraintIdV1, + target: TargetIdV1, + family: FamilyIdV1, + ) -> &mut Self { + self.inner.push_intrinsic_family_membership_report_only( + id.into_core(), + target.into_core(), + family.into_core(), + ); + self + } + /// Добавляет обязательное exact-отношение между intrinsic Target signals. pub(crate) fn push_exact_intrinsic_relation_hard( &mut self, @@ -1723,8 +1865,8 @@ impl OwnerV1 { /// /// Identity доступна до первого update, но не заменяет полномочия этой /// конкретной owner-эпохи. - pub(crate) fn content_identity(&self) -> ContentIdentityV6 { - ContentIdentityV6::from_core(self.compiled.content_identity()) + pub(crate) fn content_identity(&self) -> ContentIdentityV7 { + ContentIdentityV7::from_core(self.compiled.content_identity()) } /// Вычисляет верхние границы клеток для prospective Observed-update. @@ -2044,10 +2186,10 @@ impl<'session> PreparedSessionTransitionV1<'session> { /// Identity не идентифицирует owner-эпоху и не даёт runtime-полномочий. #[repr(transparent)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub(crate) struct ContentIdentityV6([u8; 32]); +pub(crate) struct ContentIdentityV7([u8; 32]); -impl ContentIdentityV6 { - const fn from_core(value: ProgramContentIdentityV6) -> Self { +impl ContentIdentityV7 { + const fn from_core(value: ProgramContentIdentityV7) -> Self { Self(*value.as_bytes()) } @@ -2065,8 +2207,8 @@ pub(crate) struct VerifiedCertificateV1<'a> { impl<'a> VerifiedCertificateV1<'a> { /// Возвращает identity скомпилированного содержания. - pub(crate) const fn content_identity(self) -> ContentIdentityV6 { - ContentIdentityV6::from_core(self.inner.report().content_identity()) + pub(crate) const fn content_identity(self) -> ContentIdentityV7 { + ContentIdentityV7::from_core(self.inner.report().content_identity()) } /// Возвращает точное наблюдение, на котором выдан сертификат. @@ -2111,8 +2253,8 @@ pub(crate) struct ConflictCertificateV1<'a> { impl<'a> ConflictCertificateV1<'a> { /// Возвращает identity скомпилированного содержания. - pub(crate) const fn content_identity(self) -> ContentIdentityV6 { - ContentIdentityV6::from_core(self.inner.report().content_identity()) + pub(crate) const fn content_identity(self) -> ContentIdentityV7 { + ContentIdentityV7::from_core(self.inner.report().content_identity()) } /// Возвращает точное наблюдение, вызвавшее конфликт. @@ -2161,7 +2303,7 @@ impl<'a> CertificateV1<'a> { } /// Возвращает identity скомпилированного содержания. - pub(crate) const fn content_identity(self) -> ContentIdentityV6 { + pub(crate) const fn content_identity(self) -> ContentIdentityV7 { match self { Self::Verified(value) => value.content_identity(), Self::Conflict(value) => value.content_identity(), @@ -2574,6 +2716,12 @@ impl IntrinsicUnaryEvidenceV1<'_> { actual: value.actual(), }) } + CoreIntrinsicUnaryMeasurementV1::FamilyMembership { + family, + measurement, + } => IntrinsicUnaryMeasurementV1::FamilyMembership( + FamilyMembershipMeasurementV1::from_core(family, measurement), + ), } } @@ -2581,11 +2729,17 @@ impl IntrinsicUnaryEvidenceV1<'_> { match self.inner { IntrinsicUnaryEvidenceRefV1::Pass(value) => match value.proof() { CoreIntrinsicUnaryPassV1::ExactSrgb8(_) => IntrinsicUnaryProofV1::ExactSrgb8Pass, + CoreIntrinsicUnaryPassV1::FamilyMembership(_) => { + IntrinsicUnaryProofV1::FamilyMembershipPass + } }, IntrinsicUnaryEvidenceRefV1::Violation(value) => match value.proof() { CoreIntrinsicUnaryViolationV1::ExactSrgb8(_) => { IntrinsicUnaryProofV1::ExactSrgb8Violation } + CoreIntrinsicUnaryViolationV1::FamilyMembership(_) => { + IntrinsicUnaryProofV1::FamilyMembershipViolation + } }, } } @@ -2632,13 +2786,72 @@ impl ExactSrgb8UnaryMeasurementV1 { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum IntrinsicUnaryMeasurementV1 { + /// Измерение точного равенства. ExactSrgb8(ExactSrgb8UnaryMeasurementV1), + /// Измерение принадлежности точному образу family. + FamilyMembership(FamilyMembershipMeasurementV1), +} + +/// Устойчивый к коллизиям адрес одного допущенного сертификата family. +#[repr(transparent)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct FamilyContentIdentityV1([u8; 32]); + +impl FamilyContentIdentityV1 { + const fn from_core(value: CoreFamilyContentIdentityV1) -> Self { + Self(*value.as_bytes()) + } + + /// Возвращает точные байты адреса. + pub(crate) const fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } +} + +/// Точный сигнал и сертификат family, проверенные одним вызовом. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct FamilyMembershipMeasurementV1 { + family: FamilyIdV1, + content: FamilyContentIdentityV1, + signal: Srgb8, +} + +impl FamilyMembershipMeasurementV1 { + const fn from_core(family: FamilyId, value: CoreFamilyMembershipMeasurementV1) -> Self { + Self { + family: FamilyIdV1::from_core(family), + content: FamilyContentIdentityV1::from_core(value.family()), + signal: value.signal().srgb8(), + } + } + + /// Возвращает opaque-объявление family, связанное ограничением. + pub(crate) const fn family(self) -> FamilyIdV1 { + self.family + } + + /// Возвращает адрес полного допущенного сертификата family. + pub(crate) const fn content(self) -> FamilyContentIdentityV1 { + self.content + } + + /// Возвращает классифицированный точный исходный сигнал. + pub(crate) const fn signal(self) -> Srgb8 { + self.signal + } } +/// Взаимоисключающие точные доказательства одного intrinsic-unary вызова. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum IntrinsicUnaryProofV1 { + /// Точное равенство выполнено. ExactSrgb8Pass, + /// Точное равенство нарушено. ExactSrgb8Violation, + /// Принадлежность точному образу family подтверждена. + FamilyMembershipPass, + /// Принадлежность точному образу family нарушена. + FamilyMembershipViolation, } /// Полное заимствованное member-evidence одного directional-ограничения. @@ -3538,6 +3751,15 @@ fn map_program_compile_error(error: ProgramCompileError) -> CompileErrorV1 { ProgramCompileError::DuplicateTarget { target } => CompileErrorV1::DuplicateTarget { target: TargetIdV1::from_core(target), }, + ProgramCompileError::DuplicateFamily { family } => CompileErrorV1::DuplicateFamily { + family: FamilyIdV1::from_core(family), + }, + ProgramCompileError::InvalidFamilyImage { family } => CompileErrorV1::InvalidFamilyImage { + family: FamilyIdV1::from_core(family), + }, + ProgramCompileError::UnusedFamily { family } => CompileErrorV1::UnusedFamily { + family: FamilyIdV1::from_core(family), + }, ProgramCompileError::MissingFixedSource { target, source } => { CompileErrorV1::MissingFixedSource { target: TargetIdV1::from_core(target), @@ -3771,6 +3993,12 @@ fn map_program_compile_error(error: ProgramCompileError) -> CompileErrorV1 { target: TargetIdV1::from_core(target), } } + ProgramCompileError::MissingConstraintFamily { constraint, family } => { + CompileErrorV1::MissingConstraintFamily { + constraint: ConstraintIdV1::from_core(constraint), + family: FamilyIdV1::from_core(family), + } + } ProgramCompileError::MissingIntrinsicRelationReference { constraint, reference, diff --git a/crates/labcolors-core/src/program_boundary_tests.rs b/crates/labcolors-core/src/program_boundary_tests.rs index a312b332..e330f0c7 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, ContentIdentityV6, DraftErrorV1, DraftV1, EvidenceBoundsErrorV1, + ConstraintSubjectV1, ContentIdentityV7, DraftErrorV1, DraftV1, EvidenceBoundsErrorV1, EvidenceViewV1, FinitePaintDomainV1, InstantiateErrorV1, JointChoiceV1, JointOrderErrorV1, JointStateV1, NumericDomainErrorV1, ObservationHeadV1, OccurrenceIdV1, OpacityInputIdV1, OutputSlotIdV1, OwnerV1, PaintIdV1, PaintValueV1, PhysicalPointV1, PresentationRootIdV1, @@ -1009,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: ContentIdentityV6 = owner.content_identity(); + let owner_identity: ContentIdentityV7 = owner.content_identity(); let mut session = owner.instantiate(13).unwrap(); let no_scenarios = []; diff --git a/crates/labcolors-core/src/program_clean_set_tests.rs b/crates/labcolors-core/src/program_clean_set_tests.rs index ecbca795..a98a3ed5 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::ContentIdentityV6 { +fn opaque_named_clean_identity(name: u32) -> program::ContentIdentityV7 { 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_family_tests.rs b/crates/labcolors-core/src/program_family_tests.rs new file mode 100644 index 00000000..bbc1b2fe --- /dev/null +++ b/crates/labcolors-core/src/program_family_tests.rs @@ -0,0 +1,649 @@ +//! Интеграция точного допущенного образа family в Program. + +use crate::family::FAMILY_MEMBERSHIP_ASSESS_CALLS; +use crate::program_boundary_tests::CommitProgramUpdateForTest as _; +use crate::{Srgb8, program}; + +const TARGET: program::TargetIdV1 = program::TargetIdV1::new(1); +const FAMILY: program::FamilyIdV1 = program::FamilyIdV1::new(2); +const SECOND_FAMILY: program::FamilyIdV1 = program::FamilyIdV1::new(102); +const PAINT: program::PaintIdV1 = program::PaintIdV1::new(3); +const PORT: program::SurfaceInputPortIdV1 = program::SurfaceInputPortIdV1::new(4); +const SURFACE: program::SurfaceIdV1 = program::SurfaceIdV1::new(5); +const OCCURRENCE: program::OccurrenceIdV1 = program::OccurrenceIdV1::new(6); +const FAMILY_CONSTRAINT: program::ConstraintIdV1 = program::ConstraintIdV1::new(7); +const VISIBLE_CONSTRAINT: program::ConstraintIdV1 = program::ConstraintIdV1::new(8); +const OUTPUT: program::OutputSlotIdV1 = program::OutputSlotIdV1::new(9); +const ROOT: program::PresentationRootIdV1 = program::PresentationRootIdV1::new(11); + +fn context() -> program::AppearanceContextV1 { + program::AppearanceContextV1::try_new(64.0, 0.2, program::SurroundV1::Average).unwrap() +} + +fn family(values: &[[u8; 3]]) -> program::FamilySetV1 { + program::FamilySetV1::try_from_srgb8_image( + values.iter().copied().map(Srgb8::new).collect::>(), + ) + .unwrap() +} + +fn finite_base_draft( + candidates: &[(program::TargetCandidateIdV1, Srgb8, f64)], +) -> program::DraftV1 { + let domain = program::FinitePaintDomainV1::try_new( + candidates + .iter() + .map(|(id, source, alpha)| { + program::TargetCandidateV1::new( + *id, + program::PaintValueV1::try_new(*source, *alpha).unwrap(), + ) + }) + .collect(), + ) + .unwrap(); + let mut draft = program::DraftV1::new(); + draft.push_finite_target(TARGET, domain); + draft + .set_joint_selection( + candidates + .iter() + .map(|(candidate, _, _)| { + program::JointStateV1::new(vec![program::JointChoiceV1::new( + TARGET, *candidate, + )]) + }) + .collect(), + ) + .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_output(OUTPUT, PAINT); + draft +} + +fn finite_family_draft( + candidates: &[(program::TargetCandidateIdV1, Srgb8, f64)], + family_values: &[[u8; 3]], + declare_family: bool, + family_mode_hard: bool, + expected_visible: Srgb8, +) -> program::DraftV1 { + let mut draft = finite_base_draft(candidates); + if declare_family { + draft.push_family(FAMILY, family(family_values)); + } + if family_mode_hard { + draft.push_intrinsic_family_membership_hard(FAMILY_CONSTRAINT, TARGET, FAMILY); + } else { + draft.push_intrinsic_family_membership_report_only(FAMILY_CONSTRAINT, TARGET, FAMILY); + } + draft.push_exact_visible_unary_hard(VISIBLE_CONSTRAINT, OCCURRENCE, expected_visible); + draft +} + +#[test] +fn hard_family_membership_selects_a_member_rechecks_it_and_retains_alpha() { + let rejected = program::TargetCandidateIdV1::new(10); + let accepted = program::TargetCandidateIdV1::new(11); + let red = Srgb8::new([255, 0, 0]); + let blue = Srgb8::new([0, 0, 255]); + let expected_family_content = family(&[[0, 0, 0], [0, 0, 254], [0, 0, 255]]).content_identity(); + let owner = finite_family_draft( + &[(rejected, red, 0.25), (accepted, blue, 0.5)], + &[[0, 0, 0], [0, 0, 254], [0, 0, 255]], + true, + true, + Srgb8::new([0, 0, 128]), + ) + .compile() + .unwrap(); + let mut session = owner.instantiate(12).unwrap(); + let black = [Srgb8::new([0; 3])]; + let scenarios = [program::ScenarioV1::new(13, &black)]; + FAMILY_MEMBERSHIP_ASSESS_CALLS.with(|calls| calls.set(0)); + 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 first family member must be selected"); + }; + assert_eq!(verified.selected_state_index(), Some(1)); + assert_eq!(verified.outputs().next().unwrap().source(), blue); + assert_eq!( + FAMILY_MEMBERSHIP_ASSESS_CALLS.with(core::cell::Cell::get), + 3, + "one rejection, one search pass and one fresh final pass are required", + ); + let intrinsic = verified + .cells() + .find_map(|cell| match cell.assessment() { + program::AssessmentV1::IntrinsicUnary(evidence) => Some(evidence), + _ => None, + }) + .expect("family evidence must use the existing intrinsic-unary cell"); + assert_eq!(intrinsic.binding().target(), TARGET); + assert_eq!(intrinsic.binding().value().source(), blue); + assert_eq!( + intrinsic.binding().value().opacity().to_bits(), + 0.5_f64.to_bits() + ); + let program::IntrinsicUnaryMeasurementV1::FamilyMembership(measurement) = + intrinsic.measurement() + else { + panic!("family invocation must retain typed membership measurement"); + }; + assert_eq!(measurement.family(), FAMILY); + assert_eq!(measurement.signal(), blue); + assert_eq!(measurement.content(), expected_family_content); + let program::IntrinsicUnaryProofV1::FamilyMembershipPass = intrinsic.proof() else { + panic!("selected family member must carry an inclusion witness"); + }; +} + +#[test] +fn missing_family_is_a_typed_atomic_compile_error() { + let candidate = program::TargetCandidateIdV1::new(20); + let blue = Srgb8::new([0, 0, 255]); + let draft = finite_family_draft(&[(candidate, blue, 1.0)], &[[0, 0, 255]], false, true, blue); + + let error = match draft.compile() { + Ok(_) => panic!("an unresolved family edge must not compile"), + Err(error) => error, + }; + assert_eq!( + error, + program::CompileErrorV1::MissingConstraintFamily { + constraint: FAMILY_CONSTRAINT, + family: FAMILY, + }, + ); + assert_eq!( + error.kind(), + program::CompileErrorKindV1::MissingConstraintFamily + ); + assert_eq!( + error.primary_handle(), + Some(program::CompileErrorHandleV1::Constraint(FAMILY_CONSTRAINT)), + ); + assert_eq!( + error.related_handle(), + Some(program::CompileErrorHandleV1::Family(FAMILY)), + ); +} + +#[test] +fn missing_family_error_is_canonical_under_constraint_permutation() { + let candidate = program::TargetCandidateIdV1::new(21); + let blue = Srgb8::new([0, 0, 255]); + let smaller_constraint = program::ConstraintIdV1::new(210); + let larger_constraint = program::ConstraintIdV1::new(211); + let smaller_family = program::FamilyIdV1::new(212); + let larger_family = program::FamilyIdV1::new(213); + let compile = |permuted: bool| { + let mut draft = finite_base_draft(&[(candidate, blue, 1.0)]); + let mut declarations = [ + (smaller_constraint, smaller_family), + (larger_constraint, larger_family), + ]; + if permuted { + declarations.reverse(); + } + for (constraint, family) in declarations { + draft.push_intrinsic_family_membership_hard(constraint, TARGET, family); + } + draft.push_exact_visible_unary_hard(VISIBLE_CONSTRAINT, OCCURRENCE, blue); + match draft.compile() { + Ok(_) => panic!("missing family references must not compile"), + Err(error) => error, + } + }; + let expected = program::CompileErrorV1::MissingConstraintFamily { + constraint: smaller_constraint, + family: smaller_family, + }; + + assert_eq!(compile(false), expected); + assert_eq!(compile(true), expected); +} + +#[test] +fn missing_intrinsic_target_precedes_the_same_constraints_missing_family() { + let candidate = program::TargetCandidateIdV1::new(22); + let blue = Srgb8::new([0, 0, 255]); + let missing_target = program::TargetIdV1::new(220); + let mut draft = finite_base_draft(&[(candidate, blue, 1.0)]); + draft.push_intrinsic_family_membership_hard(FAMILY_CONSTRAINT, missing_target, SECOND_FAMILY); + draft.push_exact_visible_unary_hard(VISIBLE_CONSTRAINT, OCCURRENCE, blue); + + let error = match draft.compile() { + Ok(_) => panic!("missing intrinsic target must not compile"), + Err(error) => error, + }; + assert_eq!( + error, + program::CompileErrorV1::MissingIntrinsicUnaryTarget { + constraint: FAMILY_CONSTRAINT, + target: missing_target, + }, + ); +} + +#[test] +fn family_id_and_error_handle_preserve_a_nontrivial_opaque_value() { + assert_eq!(SECOND_FAMILY.value(), 102); + assert_eq!( + program::CompileErrorHandleV1::Family(SECOND_FAMILY).value(), + 102, + ); +} + +#[test] +fn facade_family_content_bytes_change_with_the_admitted_image() { + let blue = family(&[[0, 0, 255]]).content_identity(); + let red = family(&[[255, 0, 0]]).content_identity(); + + assert_ne!(blue.as_bytes(), red.as_bytes()); +} + +#[test] +fn report_only_family_violation_is_retained_but_does_not_steer_selection() { + let first = program::TargetCandidateIdV1::new(30); + let second = program::TargetCandidateIdV1::new(31); + let red = Srgb8::new([255, 0, 0]); + let blue = Srgb8::new([0, 0, 255]); + let expected_family_content = family(&[[0, 0, 255]]).content_identity(); + let owner = finite_family_draft( + &[(first, red, 0.25), (second, blue, 0.5)], + &[[0, 0, 255]], + true, + false, + Srgb8::new([64, 0, 0]), + ) + .compile() + .unwrap(); + let mut session = owner.instantiate(32).unwrap(); + let black = [Srgb8::new([0; 3])]; + let scenarios = [program::ScenarioV1::new(33, &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!("a report-only exclusion must not reject the first hard-valid state"); + }; + assert_eq!(verified.selected_state_index(), Some(0)); + let output = verified.outputs().next().unwrap(); + assert_eq!(output.source(), red); + assert_eq!(output.opacity().to_bits(), 0.25_f64.to_bits()); + let cell = verified + .cells() + .find(|cell| cell.constraint() == FAMILY_CONSTRAINT) + .expect("report-only family evidence must be retained"); + assert_eq!(cell.mode(), program::ConstraintModeV1::ReportOnly); + let program::AssessmentV1::IntrinsicUnary(intrinsic) = cell.assessment() else { + panic!("family membership must remain intrinsic-unary evidence"); + }; + assert_eq!(intrinsic.verdict(), program::VerdictV1::Violation); + let program::IntrinsicUnaryMeasurementV1::FamilyMembership(measurement) = + intrinsic.measurement() + else { + panic!("family violation must retain typed membership measurement"); + }; + assert_eq!(measurement.family(), FAMILY); + assert_eq!(measurement.signal(), red); + assert_eq!(measurement.content(), expected_family_content); + let program::IntrinsicUnaryProofV1::FamilyMembershipViolation = intrinsic.proof() else { + panic!("the non-member must retain an exact exclusion witness"); + }; +} + +#[test] +fn compiled_family_index_resolves_the_requested_nonzero_declaration() { + let candidate = program::TargetCandidateIdV1::new(34); + let blue = Srgb8::new([0, 0, 255]); + let mut draft = finite_base_draft(&[(candidate, blue, 1.0)]); + draft.push_family(FAMILY, family(&[[255, 0, 0]])); + draft.push_family(SECOND_FAMILY, family(&[[0, 0, 255]])); + draft.push_intrinsic_family_membership_report_only(FAMILY_CONSTRAINT, TARGET, FAMILY); + let second_constraint = program::ConstraintIdV1::new(35); + draft.push_intrinsic_family_membership_hard(second_constraint, TARGET, SECOND_FAMILY); + draft.push_exact_visible_unary_hard(VISIBLE_CONSTRAINT, OCCURRENCE, blue); + let owner = draft.compile().unwrap(); + let mut session = owner.instantiate(36).unwrap(); + let black = [Srgb8::new([0; 3])]; + let scenarios = [program::ScenarioV1::new(37, &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!("constraint must classify against the requested second family"); + }; + let cell = verified + .cells() + .find(|cell| cell.constraint() == second_constraint) + .expect("second-family evidence must be retained"); + let program::AssessmentV1::IntrinsicUnary(intrinsic) = cell.assessment() else { + panic!("family evidence must remain intrinsic-unary"); + }; + let program::IntrinsicUnaryMeasurementV1::FamilyMembership(measurement) = + intrinsic.measurement() + else { + panic!("second-family measurement must remain typed"); + }; + assert_eq!(measurement.family(), SECOND_FAMILY); + assert_eq!(measurement.signal(), blue); + assert_eq!(intrinsic.verdict(), program::VerdictV1::Pass); +} + +#[test] +fn two_invalid_states_produce_an_exhaustive_conflict_with_exact_family_witnesses() { + let below = program::TargetCandidateIdV1::new(40); + let between = program::TargetCandidateIdV1::new(41); + let below_signal = Srgb8::new([5; 3]); + let between_signal = Srgb8::new([15; 3]); + let expected_family_content = family(&[[10; 3], [20; 3]]).content_identity(); + let mut draft = + finite_base_draft(&[(below, below_signal, 1.0), (between, between_signal, 1.0)]); + draft.push_family(FAMILY, family(&[[10; 3], [20; 3]])); + draft.push_intrinsic_family_membership_hard(FAMILY_CONSTRAINT, TARGET, FAMILY); + draft.push_exact_visible_unary_report_only(VISIBLE_CONSTRAINT, OCCURRENCE, Srgb8::new([0; 3])); + let owner = draft.compile().unwrap(); + let mut session = owner.instantiate(42).unwrap(); + let black = [Srgb8::new([0; 3])]; + let scenarios = [program::ScenarioV1::new(43, &black)]; + FAMILY_MEMBERSHIP_ASSESS_CALLS.with(|calls| calls.set(0)); + let evidence = owner + .commit( + &mut session, + program::UpdateV1::Observed { + revision: 1, + scenarios: &scenarios, + }, + ) + .unwrap(); + + let Some(program::CertificateV1::Conflict(conflict)) = evidence.certificates().next() else { + panic!("every state violates the hard family, so the result must be Conflict"); + }; + assert_eq!(conflict.considered_state_count(), 2); + let cells = conflict + .cells() + .filter(|cell| cell.constraint() == FAMILY_CONSTRAINT) + .collect::>(); + assert_eq!( + cells.len(), + 2, + "the family constraint must retain one witness for each state", + ); + assert_eq!( + FAMILY_MEMBERSHIP_ASSESS_CALLS.with(core::cell::Cell::get), + 4, + "two search classifications and two exhaustive conflict rechecks are required", + ); + for cell in cells { + assert_eq!(cell.case_index(), 0); + assert_eq!(cell.mode(), program::ConstraintModeV1::Hard); + let program::AssessmentV1::IntrinsicUnary(intrinsic) = cell.assessment() else { + panic!("every retained cell must carry family evidence"); + }; + let program::IntrinsicUnaryMeasurementV1::FamilyMembership(measurement) = + intrinsic.measurement() + else { + panic!("every family violation must retain typed membership measurement"); + }; + assert_eq!(measurement.family(), FAMILY); + assert_eq!(measurement.content(), expected_family_content); + let expected_signal = match cell.state_index() { + 0 => below_signal, + 1 => between_signal, + state => panic!("unexpected state {state}"), + }; + assert_eq!(measurement.signal(), expected_signal); + let program::IntrinsicUnaryProofV1::FamilyMembershipViolation = intrinsic.proof() else { + panic!("both candidate signals are absent from the family"); + }; + } +} + +#[test] +fn equal_sources_with_distinct_opacity_remain_distinct_candidate_states() { + let translucent = program::TargetCandidateIdV1::new(50); + let opaque = program::TargetCandidateIdV1::new(51); + let blue = Srgb8::new([0, 0, 255]); + let owner = finite_family_draft( + &[(translucent, blue, 0.5), (opaque, blue, 1.0)], + &[[0, 0, 255]], + true, + true, + blue, + ) + .compile() + .unwrap(); + let mut session = owner.instantiate(52).unwrap(); + let black = [Srgb8::new([0; 3])]; + let scenarios = [program::ScenarioV1::new(53, &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 opaque candidate must remain selectable after the translucent candidate"); + }; + assert_eq!(verified.selected_state_index(), Some(1)); + let output = verified.outputs().next().unwrap(); + assert_eq!(output.source(), blue); + assert_eq!(output.opacity().to_bits(), 1.0_f64.to_bits()); +} + +fn family_and_declared_set_verdicts( + signal: Srgb8, + admitted_family: &[[u8; 3]], +) -> (program::StateKindV1, [program::VerdictV1; 2]) { + let candidate = program::TargetCandidateIdV1::new(60); + let mut draft = finite_base_draft(&[(candidate, signal, 1.0)]); + draft.push_family(FAMILY, family(admitted_family)); + draft.push_point_presentation_root(ROOT, OCCURRENCE); + draft.push_point_presentation_target(ROOT, OCCURRENCE); + draft.push_intrinsic_family_membership_hard(FAMILY_CONSTRAINT, TARGET, FAMILY); + draft.push_declared_srgb8_clean_set_hard(VISIBLE_CONSTRAINT, ROOT, OCCURRENCE); + let owner = draft.compile().unwrap(); + let mut session = owner.instantiate(61).unwrap(); + let black = [Srgb8::new([0; 3])]; + let scenarios = [program::ScenarioV1::new(62, &black)]; + let evidence = owner + .commit( + &mut session, + program::UpdateV1::Observed { + revision: 1, + scenarios: &scenarios, + }, + ) + .unwrap(); + let kind = evidence.kind(); + let mut verdicts = [program::VerdictV1::Violation; 2]; + match evidence.certificates().next().unwrap() { + program::CertificateV1::Verified(certificate) => { + assert_eq!(certificate.cells().len(), 2); + for cell in certificate.cells() { + let index = verdict_index(cell.constraint()); + verdicts[index] = cell.assessment().verdict(); + } + } + program::CertificateV1::Conflict(certificate) => { + assert_eq!(certificate.cells().len(), 2); + for cell in certificate.cells() { + let index = verdict_index(cell.constraint()); + verdicts[index] = cell.assessment().verdict(); + } + } + } + (kind, verdicts) +} + +fn verdict_index(constraint: program::ConstraintIdV1) -> usize { + if constraint == FAMILY_CONSTRAINT { + 0 + } else if constraint == VISIBLE_CONSTRAINT { + 1 + } else { + panic!("unexpected constraint"); + } +} + +#[test] +fn family_and_declared_point_convention_are_independent_hard_constraints_over_the_full_two_by_two() +{ + let predicate_member = Srgb8::new([0, 200, 70]); + let predicate_nonmember = Srgb8::new([0, 200, 71]); + let other = [[255, 0, 255]]; + let cases = [ + ( + predicate_member, + [[0, 200, 70]].as_slice(), + program::StateKindV1::Ready, + [program::VerdictV1::Pass, program::VerdictV1::Pass], + ), + ( + predicate_nonmember, + [[0, 200, 71]].as_slice(), + program::StateKindV1::Failed, + [program::VerdictV1::Pass, program::VerdictV1::Violation], + ), + ( + predicate_member, + other.as_slice(), + program::StateKindV1::Failed, + [program::VerdictV1::Violation, program::VerdictV1::Pass], + ), + ( + predicate_nonmember, + other.as_slice(), + program::StateKindV1::Failed, + [program::VerdictV1::Violation, program::VerdictV1::Violation], + ), + ]; + + for (signal, admitted, expected_kind, expected_verdicts) in cases { + let (kind, verdicts) = family_and_declared_set_verdicts(signal, admitted); + assert_eq!(kind, expected_kind, "unexpected lifecycle for {signal:?}"); + assert_eq!( + verdicts, expected_verdicts, + "unexpected verdicts for {signal:?}" + ); + } +} + +#[test] +fn empty_family_image_is_rejected_before_a_draft_can_represent_it() { + assert_eq!( + program::FamilySetV1::try_from_srgb8_image(Vec::new()), + Err(program::FamilySetAdmissionErrorV1::Empty), + ); +} + +#[test] +fn duplicate_and_unused_families_are_typed_compile_errors() { + let candidate = program::TargetCandidateIdV1::new(70); + let blue = Srgb8::new([0, 0, 255]); + + let mut duplicate = finite_base_draft(&[(candidate, blue, 1.0)]); + duplicate.push_family(FAMILY, family(&[[0, 0, 255]])); + duplicate.push_family(FAMILY, family(&[[255, 0, 0]])); + duplicate.push_intrinsic_family_membership_hard(FAMILY_CONSTRAINT, TARGET, FAMILY); + let duplicate_error = match duplicate.compile() { + Ok(_) => panic!("a duplicate family must not compile"), + Err(error) => error, + }; + assert_eq!( + duplicate_error, + program::CompileErrorV1::DuplicateFamily { family: FAMILY }, + ); + assert_eq!( + duplicate_error.kind(), + program::CompileErrorKindV1::DuplicateFamily, + ); + assert_eq!( + duplicate_error.primary_handle(), + Some(program::CompileErrorHandleV1::Family(FAMILY)), + ); + assert_eq!(duplicate_error.related_handle(), None); + + let mut unused = finite_base_draft(&[(candidate, blue, 1.0)]); + unused.push_family(FAMILY, family(&[[0, 0, 255]])); + unused.push_exact_visible_unary_hard(VISIBLE_CONSTRAINT, OCCURRENCE, blue); + let unused_error = match unused.compile() { + Ok(_) => panic!("an unused family must not compile"), + Err(error) => error, + }; + assert_eq!( + unused_error, + program::CompileErrorV1::UnusedFamily { family: FAMILY }, + ); + assert_eq!( + unused_error.kind(), + program::CompileErrorKindV1::UnusedFamily, + ); + assert_eq!( + unused_error.primary_handle(), + Some(program::CompileErrorHandleV1::Family(FAMILY)), + ); + assert_eq!(unused_error.related_handle(), None); +} + +#[test] +fn replay_corruption_is_a_typed_invalid_family_image_compile_error() { + let candidate = program::TargetCandidateIdV1::new(80); + let blue = Srgb8::new([0, 0, 255]); + let mut corrupted = family(&[[0, 0, 255]]); + corrupted.corrupt_first_member_for_test(Srgb8::new([255, 0, 0])); + let mut draft = finite_base_draft(&[(candidate, blue, 1.0)]); + draft.push_family(FAMILY, corrupted); + draft.push_intrinsic_family_membership_hard(FAMILY_CONSTRAINT, TARGET, FAMILY); + + let error = match draft.compile() { + Ok(_) => panic!("a corrupted admitted family must fail replay"), + Err(error) => error, + }; + assert_eq!( + error, + program::CompileErrorV1::InvalidFamilyImage { family: FAMILY }, + ); + assert_eq!( + error.kind(), + program::CompileErrorKindV1::InvalidFamilyImage + ); + assert_eq!( + error.primary_handle(), + Some(program::CompileErrorHandleV1::Family(FAMILY)), + ); + assert_eq!(error.related_handle(), None); +} diff --git a/crates/labcolors-core/src/program_identity.rs b/crates/labcolors-core/src/program_identity.rs index 76e4b769..3b508a1d 100644 --- a/crates/labcolors-core/src/program_identity.rs +++ b/crates/labcolors-core/src/program_identity.rs @@ -7,16 +7,15 @@ use super::*; -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; +const DOMAIN_V7: &[u8] = b"labcolors.program-content-identity.v7\0"; +// V7 резервирует фиксированную protocol boundary под type/family tag и полный +// content digest. Каждый writer использует проверяемый push и отвергает +// расширение схемы вместо усечения данных или аллокации на вершину. +const HASH_BOUND_COLOR_BYTES_V7: usize = 1 + 1 + 32; +const COLOR_CAPACITY: usize = HASH_BOUND_COLOR_BYTES_V7; mod release_tag { - pub(super) const PROGRAM_SCHEMA_V6: u8 = 6; + pub(super) const PROGRAM_SCHEMA_V7: u8 = 7; 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; @@ -24,6 +23,7 @@ mod release_tag { pub(super) const FINITE_ATOMIC_PAINT_CANDIDATE_V1: u8 = 1; pub(super) const MODELED_POINT_PRESENTATION_V1: u8 = 1; pub(super) const POINT_ABSENCE_BYPASS_OWN_BACKDROP_V1: u8 = 1; + pub(super) const FAMILY_CERTIFICATE_VERTEX_V1: u8 = 1; #[cfg(test)] pub(super) const MODELED_LCS_OCCURRENCE_V1: u8 = 1; #[cfg(test)] @@ -56,6 +56,10 @@ mod release_tag { 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; + pub(super) const FAMILY_MEMBERSHIP_FAMILY_V1: u8 = 6; + pub(super) const FAMILY_MEMBERSHIP_IDENTITY_V1: u8 = 1; + pub(super) const FAMILY_MEMBERSHIP_RELEASE_V1: u8 = 1; + pub(super) const FAMILY_MEMBERSHIP_CAPABILITY_V1: u8 = 1; #[cfg(test)] pub(super) const EXACT_SRGB8_IDENTITY_MUTATION_SENTINEL_V1: u8 = 2; #[cfg(test)] @@ -72,17 +76,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 = 6; + pub(super) const MODELED_LCS_PROBE_FAMILY_V1: u8 = 7; } -/// Устойчивый к коллизиям адрес канонизированного содержимого Program V6. +/// Устойчивый к коллизиям адрес канонизированного содержимого Program V7. /// /// SHA-256 не делает адрес инъективным. Адрес не связывает пространства opaque /// ID и не подтверждает владельца, поколение либо revision. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub(crate) struct ProgramContentIdentityV6([u8; 32]); +pub(crate) struct ProgramContentIdentityV7([u8; 32]); -impl ProgramContentIdentityV6 { +impl ProgramContentIdentityV7 { pub(crate) const fn as_bytes(&self) -> &[u8; 32] { &self.0 } @@ -165,6 +169,7 @@ mod vertex_tag { pub(super) const JOINT_CHOICE: u8 = 19; pub(super) const PRESENTATION_ROOT: u8 = 20; pub(super) const PRESENTATION_TARGET: u8 = 21; + pub(super) const FAMILY: u8 = 22; } macro_rules! declare_edge_roles_v1 { @@ -208,6 +213,7 @@ declare_edge_roles_v1! { VisibleReference = 25, VisibleCandidate = 26, ObservationGroup = 27, + ConstraintFamily = 28, } #[derive(Debug, Clone, Copy)] @@ -370,7 +376,7 @@ fn program_root_color() -> Result { // производных возможностей связываются только с теми ограничениями, // которые их исполняют. for release in [ - release_tag::PROGRAM_SCHEMA_V6, + release_tag::PROGRAM_SCHEMA_V7, release_tag::DECLARED_TOTAL_ORDER_V1, release_tag::FRESH_FULL_RECHECK_V1, release_tag::ATOMIC_OBSERVATION_GROUP_V1, @@ -411,6 +417,16 @@ fn source_color(source: Source) -> Result { Ok(color) } +fn family_color(family: &FamilyDeclarationV1) -> Result { + let identity = family.set().certificate().family_content_identity(); + let mut color = VertexColorV1::new(vertex_tag::FAMILY); + color.push_u8(release_tag::FAMILY_CERTIFICATE_VERTEX_V1)?; + for byte in identity.as_bytes() { + color.push_u8(*byte)?; + } + Ok(color) +} + fn candidate_color(candidate: TargetCandidateV1) -> Result { let mut color = VertexColorV1::new(vertex_tag::CANDIDATE); let value = candidate.value(); @@ -577,6 +593,28 @@ fn constraint_color( })?; color.push_srgb8(expected)?; } + ProgramConstraintContentV1::FamilyMembership { + identity, + release, + capability, + } => { + color.push_u8(release_tag::FAMILY_MEMBERSHIP_FAMILY_V1)?; + color.push_u8(match identity { + crate::constraints::FamilyMembershipIdentityV1::ExactImageMembershipV1 => { + release_tag::FAMILY_MEMBERSHIP_IDENTITY_V1 + } + })?; + color.push_u8(match release { + crate::constraints::FamilyMembershipReleaseV1::V1 => { + release_tag::FAMILY_MEMBERSHIP_RELEASE_V1 + } + })?; + color.push_u8(match capability { + crate::constraints::FamilyMembershipCapabilityV1::Iec61966Srgb8D65V1 => { + release_tag::FAMILY_MEMBERSHIP_CAPABILITY_V1 + } + })?; + } ProgramConstraintContentV1::ExactSrgb8Relation { identity, release, @@ -678,6 +716,7 @@ struct ConstraintGraphBindingContextV1<'a, Evaluation> { evaluator: &'a Evaluation, presentation_targets: &'a [(PointPresentationTargetV1, usize)], targets: &'a IdIndexV1, + families: &'a IdIndexV1, occurrences: &'a IdIndexV1, observation_group: usize, } @@ -722,12 +761,19 @@ where EdgeRoleV1::VisibleUnary, )?; } - ProgramConstraintBodyV1::IntrinsicUnary { target, .. } => { + ProgramConstraintBodyV1::IntrinsicUnary { target, invocation } => { graph.add_edge( vertex, context.targets.get(*target)?, EdgeRoleV1::IntrinsicUnary, )?; + if let CoreIntrinsicUnaryInvocationV1::FamilyMembership { family } = invocation { + graph.add_edge( + vertex, + context.families.get(*family)?, + EdgeRoleV1::ConstraintFamily, + )?; + } } ProgramConstraintBodyV1::IntrinsicRelation { relation, .. } => { graph.add_edge( @@ -790,6 +836,7 @@ where let mut graph = GraphBuilderV1::new(program_root_color()?)?; let mut sources = IdIndexV1::new(); let mut targets = IdIndexV1::new(); + let mut families = IdIndexV1::new(); let mut candidates = IdIndexV1::new(); let mut opacities = IdIndexV1::new(); let mut paints = IdIndexV1::new(); @@ -819,6 +866,9 @@ where } } } + for family in &program.families { + families.insert(family.id(), graph.add_member(family_color(family)?)?)?; + } for opacity in &program.opacities { opacities.insert(opacity.id(), graph.add_member(opacity_color(*opacity)?)?)?; } @@ -869,6 +919,7 @@ where sources.finish()?; targets.finish()?; + families.finish()?; candidates.finish()?; opacities.finish()?; paints.finish()?; @@ -974,6 +1025,7 @@ where evaluator: &program.evaluator, presentation_targets: &presentation_targets, targets: &targets, + families: &families, occurrences: &occurrences, observation_group: group, }; @@ -1332,7 +1384,7 @@ fn serialize_leaf( .and_then(|value| value.checked_add(color.as_slice().len())) .ok_or(ProgramCompileError::ResourceExhausted) })?; - let capacity = DOMAIN_V6 + let capacity = DOMAIN_V7 .len() .checked_add(16) .and_then(|value| value.checked_add(color_bytes)) @@ -1342,7 +1394,7 @@ fn serialize_leaf( output .try_reserve_exact(capacity) .map_err(|_| ProgramCompileError::ResourceExhausted)?; - output.extend_from_slice(DOMAIN_V6); + output.extend_from_slice(DOMAIN_V7); push_u64_bytes(&mut output, usize_as_u64(graph.colors.len())?); push_u64_bytes(&mut output, usize_as_u64(graph.edge_count)?); @@ -1676,9 +1728,9 @@ fn canonical_preimage(graph: &CanonicalGraphV1) -> Result, ProgramCompil canonical_search(graph).map(|(preimage, _)| preimage) } -pub(super) fn compile_program_content_identity_v6( +pub(super) fn compile_program_content_identity_v7( program: &Program, -) -> Result +) -> Result where Evaluation: ProgramConstraintEvaluatorSetV1, ProgramConstraintInvocationOf: Copy, @@ -1686,13 +1738,32 @@ where let graph = build_graph(program)?; let preimage = canonical_preimage(&graph)?; let digest = crate::sha256::digest(&preimage); - Ok(ProgramContentIdentityV6(*digest.as_bytes())) + Ok(ProgramContentIdentityV7(*digest.as_bytes())) } #[cfg(test)] mod tests { use super::*; + #[test] + fn family_vertex_codec_binds_release_and_certificate_identity_without_opaque_id() { + let set = crate::family::admit_declared_family_image_v1(vec![ColorSignal::from_srgb8( + Srgb8::new([0x12, 0x34, 0x56]), + )]) + .unwrap(); + let expected = set.certificate().family_content_identity(); + let family = FamilyDeclarationV1::new(FamilyId::new(u32::MAX), set); + + let color = family_color(&family).unwrap(); + + assert_eq!(color.as_slice()[0], vertex_tag::FAMILY); + assert_eq!( + color.as_slice()[1], + release_tag::FAMILY_CERTIFICATE_VERTEX_V1 + ); + assert_eq!(&color.as_slice()[2..], expected.as_bytes()); + } + #[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; diff --git a/crates/labcolors-core/src/program_identity_tests.rs b/crates/labcolors-core/src/program_identity_tests.rs index dc085405..40387c11 100644 --- a/crates/labcolors-core/src/program_identity_tests.rs +++ b/crates/labcolors-core/src/program_identity_tests.rs @@ -3,6 +3,7 @@ use crate::appearance::{ EncodedPointPaintValueV1, OccurrenceId, OpacityInputId, PaintId, SurfaceId, SurfaceInputPortId, }; use crate::composition::AdmittedOpacityV1; +use crate::family::{FamilyDeclarationV1, FamilyId, admit_declared_family_image_v1}; use crate::lcs_occurrence::{ AdaptingLuminanceCdM2, AppearanceContextId, AppearanceContextSchemaReleaseId, BackgroundLuminanceRatio, ColorSignal, IEC_SRGB_D65_XYZ_FRAME_V1, SurroundProfileId, @@ -13,7 +14,7 @@ use crate::program_session::{ CoreProgramConstraintInvocationV1, CoreProgramEvaluatorsV1, CoreProgramV1, DeclaredJointSelectionV1, FinitePaintDomainV1, JointCandidateStateV1, ObservationGroup, Occurrence, OpacityInput, OutputBinding, OutputSlotId, Paint, PointPresentationRootV1, - PointPresentationTargetV1, PresentationRootId, Program, ProgramContentIdentityV6, Source, + PointPresentationTargetV1, PresentationRootId, Program, ProgramContentIdentityV7, Source, SourceId, Surface, Target, TargetCandidateChoiceV1, TargetCandidateId, TargetCandidateV1, TargetId, }; @@ -49,6 +50,216 @@ fn context(surround: SurroundProfileId) -> AppearanceContextId { ) } +fn declared_family(id: FamilyId, members: &[[u8; 3]]) -> FamilyDeclarationV1 { + FamilyDeclarationV1::new( + id, + admit_declared_family_image_v1(members.iter().copied().map(signal).collect()).unwrap(), + ) +} + +fn family_identity_program( + declarations: Vec<(FamilyId, Vec<[u8; 3]>)>, + bindings: [FamilyId; 2], +) -> CoreProgramV1 { + let sources = [SourceId::new(1), SourceId::new(2)]; + let targets = [TargetId::new(3), TargetId::new(4)]; + let paints = [PaintId::new(5), PaintId::new(6)]; + let port = SurfaceInputPortId::new(7); + let surface = SurfaceId::new(8); + let occurrences = [OccurrenceId::new(9), OccurrenceId::new(10)]; + let source_signals = [signal([0x20, 0x40, 0x60]), signal([0x80, 0x60, 0x40])]; + + Program::new( + vec![ + Source::new(sources[0], source_signals[0]), + Source::new(sources[1], source_signals[1]), + ], + vec![ + Target::fixed(targets[0], sources[0]), + Target::fixed(targets[1], sources[1]), + ], + ObservationGroup::new(ObservationGroupId::new(11), vec![port]), + vec![], + vec![ + Paint::Solid { + id: paints[0], + target: targets[0], + }, + Paint::Solid { + id: paints[1], + target: targets[1], + }, + ], + vec![Surface::Input { + id: surface, + input: port, + }], + vec![ + Occurrence::new( + occurrences[0], + paints[0], + surface, + CompositionProfile::EncodedSrgb8SourceOverV1, + context(SurroundProfileId::AverageV1), + ), + Occurrence::new( + occurrences[1], + paints[1], + surface, + CompositionProfile::EncodedSrgb8SourceOverV1, + context(SurroundProfileId::AverageV1), + ), + ], + ConstraintSet::new( + vec![ + ConstraintInvocation::intrinsic_family_membership_hard( + ConstraintId::new(12), + targets[0], + bindings[0], + ), + ConstraintInvocation::intrinsic_family_membership_hard( + ConstraintId::new(13), + targets[1], + bindings[1], + ), + ConstraintInvocation::visible_unary_hard( + ConstraintId::new(14), + occurrences[0], + CoreProgramConstraintInvocationV1::ExactSrgb8(source_signals[0].srgb8()), + ), + ConstraintInvocation::visible_unary_hard( + ConstraintId::new(15), + occurrences[1], + CoreProgramConstraintInvocationV1::ExactSrgb8(source_signals[1].srgb8()), + ), + ], + vec![], + ), + vec![ + OutputBinding::new(OutputSlotId::new(16), paints[0]), + OutputBinding::new(OutputSlotId::new(17), paints[1]), + ], + CoreProgramEvaluatorsV1, + ) + .with_families( + declarations + .into_iter() + .map(|(id, members)| declared_family(id, &members)) + .collect(), + ) +} + +#[test] +fn family_ids_and_declaration_order_are_alpha_invariant_in_v7() { + let first = FamilyId::new(100); + let second = FamilyId::new(200); + let renamed_first = FamilyId::new(900); + let renamed_second = FamilyId::new(700); + let first_members = vec![[0x20, 0x40, 0x60], [0x80, 0x60, 0x40], [0x10, 0x30, 0x50]]; + let second_members = vec![[0x20, 0x40, 0x60], [0x80, 0x60, 0x40], [0x90, 0x70, 0x50]]; + + let canonical = family_identity_program( + vec![ + (first, first_members.clone()), + (second, second_members.clone()), + ], + [first, second], + ) + .compile() + .unwrap(); + let renamed = family_identity_program( + vec![ + (renamed_second, second_members), + (renamed_first, first_members), + ], + [renamed_first, renamed_second], + ) + .compile() + .unwrap(); + + assert_eq!(canonical.content_identity(), renamed.content_identity()); +} + +#[test] +fn family_content_is_bound_into_v7_identity() { + let first = FamilyId::new(100); + let second = FamilyId::new(200); + let baseline = family_identity_program( + vec![ + ( + first, + vec![[0x20, 0x40, 0x60], [0x80, 0x60, 0x40], [0x10, 0x30, 0x50]], + ), + ( + second, + vec![[0x20, 0x40, 0x60], [0x80, 0x60, 0x40], [0x90, 0x70, 0x50]], + ), + ], + [first, second], + ) + .compile() + .unwrap(); + let changed = family_identity_program( + vec![ + ( + first, + vec![[0x20, 0x40, 0x60], [0x80, 0x60, 0x40], [0x10, 0x30, 0x50]], + ), + ( + second, + vec![[0x20, 0x40, 0x60], [0x80, 0x60, 0x40], [0x91, 0x70, 0x50]], + ), + ], + [first, second], + ) + .compile() + .unwrap(); + + assert_ne!(baseline.content_identity(), changed.content_identity()); +} + +#[test] +fn constraint_family_binding_topology_is_bound_into_v7_identity() { + let first = FamilyId::new(100); + let second = FamilyId::new(200); + let declarations = vec![ + ( + first, + vec![[0x20, 0x40, 0x60], [0x80, 0x60, 0x40], [0x10, 0x30, 0x50]], + ), + ( + second, + vec![[0x20, 0x40, 0x60], [0x80, 0x60, 0x40], [0x90, 0x70, 0x50]], + ), + ]; + let direct = family_identity_program(declarations.clone(), [first, second]) + .compile() + .unwrap(); + let crossed = family_identity_program(declarations, [second, first]) + .compile() + .unwrap(); + + assert_ne!(direct.content_identity(), crossed.content_identity()); +} + +#[test] +fn shared_and_duplicated_equal_families_have_distinct_v7_identity() { + let first = FamilyId::new(100); + let second = FamilyId::new(200); + let members = vec![[0x20, 0x40, 0x60], [0x80, 0x60, 0x40]]; + let shared = family_identity_program(vec![(first, members.clone())], [first, first]) + .compile() + .unwrap(); + let duplicated = family_identity_program( + vec![(first, members.clone()), (second, members)], + [first, second], + ) + .compile() + .unwrap(); + + assert_ne!(shared.content_identity(), duplicated.content_identity()); +} + #[derive(Clone, Copy)] struct FixedIds { sources: [SourceId; 2], @@ -274,7 +485,7 @@ fn presentation_topology_ignores_root_names_and_declaration_order() { } #[test] -fn presentation_root_terminal_relation_changes_v6_identity() { +fn presentation_root_terminal_relation_changes_v7_identity() { let ids = canonical_full_ids(); let root = PresentationRootId::new(1); let first = full_program(ids, false, FullMutation::None) @@ -295,7 +506,7 @@ fn presentation_root_terminal_relation_changes_v6_identity() { } #[test] -fn presentation_target_relation_and_multiplicity_change_v6_identity() { +fn presentation_target_relation_and_multiplicity_change_v7_identity() { let ids = canonical_full_ids(); let root = PresentationRootId::new(1); let nested_target = full_program(ids, false, FullMutation::None) @@ -337,7 +548,7 @@ fn presentation_target_relation_and_multiplicity_change_v6_identity() { } #[test] -fn canonical_v6_digest_is_cross_platform_golden() { +fn canonical_v7_digest_is_cross_platform_golden() { let ids = FixedIds { sources: [SourceId::new(10), SourceId::new(20)], targets: [TargetId::new(30), TargetId::new(40)], @@ -358,12 +569,12 @@ fn canonical_v6_digest_is_cross_platform_golden() { .compile() .unwrap(); - let identity: ProgramContentIdentityV6 = compiled.content_identity(); + let identity: ProgramContentIdentityV7 = compiled.content_identity(); assert_eq!( identity.as_bytes(), &[ - 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, + 154, 204, 81, 85, 156, 9, 26, 195, 87, 171, 96, 24, 34, 25, 217, 167, 60, 142, 99, 146, + 3, 34, 238, 113, 105, 97, 144, 203, 111, 224, 244, 136, ] ); } @@ -637,6 +848,11 @@ fn full_program(ids: FullIds, reverse_unordered: bool, mutation: FullMutation) - ids.targets[0], candidate_signals[0][0].srgb8(), )); + hard.push(ConstraintInvocation::intrinsic_family_membership_hard( + ConstraintId::new(1_012), + ids.targets[0], + FamilyId::new(1_013), + )); hard.push(ConstraintInvocation::exact_intrinsic_relation_hard( ConstraintId::new(1_007), DirectedRelationV1::try_new(fixed_target, vec![ids.targets[0]]).unwrap(), @@ -717,16 +933,21 @@ fn full_program(ids: FullIds, reverse_unordered: bool, mutation: FullMutation) - .with_joint_selection(DeclaredJointSelectionV1::new(states)); if matches!(mutation, FullMutation::CompleteSchemaGolden) { - program.with_point_presentations( - vec![PointPresentationRootV1::new( - PresentationRootId::new(1_002), - ids.occurrences[1], - )], - vec![PointPresentationTargetV1::new( - PresentationRootId::new(1_002), - ids.occurrences[0], - )], - ) + program + .with_families(vec![declared_family( + FamilyId::new(1_013), + &[[0x10, 0x20, 0x30], [0x30, 0x20, 0x10]], + )]) + .with_point_presentations( + vec![PointPresentationRootV1::new( + PresentationRootId::new(1_002), + ids.occurrences[1], + )], + vec![PointPresentationTargetV1::new( + PresentationRootId::new(1_002), + ids.occurrences[0], + )], + ) } else { program } @@ -765,8 +986,8 @@ fn canonical_full_ids() -> FullIds { } #[test] -fn complete_program_schema_v6_digest_is_cross_platform_golden() { - // Вместе с fixed golden этот Program содержит каждый V6 vertex/edge tag, +fn complete_program_schema_v7_digest_is_cross_platform_golden() { + // Вместе с fixed golden этот Program содержит каждый V7 vertex/edge tag, // все constraint topology и оба режима. Случайная смена кодировки требует // явной смены версии, а не тихого перевыпуска прежнего content address. let compiled = full_program( @@ -783,8 +1004,9 @@ fn complete_program_schema_v6_digest_is_cross_platform_golden() { FullMutation::CompleteSchemaGolden, )) .unwrap(); - assert_eq!(schema.0, (1_u8..=21).collect::>()); + assert_eq!(schema.0, (1_u8..=22).collect::>()); let edge_role_count = crate::program_session::program_identity_edge_role_count_for_test(); + assert_eq!(edge_role_count, 28); assert_eq!(schema.1.len(), edge_role_count); assert_eq!( schema.1, @@ -794,8 +1016,8 @@ fn complete_program_schema_v6_digest_is_cross_platform_golden() { assert_eq!( compiled.content_identity().as_bytes(), &[ - 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, + 91, 190, 49, 64, 54, 146, 143, 130, 182, 127, 204, 161, 237, 77, 36, 163, 46, 147, 135, + 196, 165, 104, 201, 64, 177, 26, 222, 181, 175, 25, 176, 163, ] ); } @@ -1378,7 +1600,7 @@ fn content_identity_retains_the_explicit_joint_state_order() { } #[test] -fn finite_candidate_opacity_is_part_of_v6_content_identity() { +fn finite_candidate_opacity_is_part_of_v7_content_identity() { let quarter = finite_program_with_opacity(false, 0.25).compile().unwrap(); let half = finite_program_with_opacity(false, 0.5).compile().unwrap(); diff --git a/crates/labcolors-core/src/program_relation_tests.rs b/crates/labcolors-core/src/program_relation_tests.rs index e7ff5851..8f10fbd9 100644 --- a/crates/labcolors-core/src/program_relation_tests.rs +++ b/crates/labcolors-core/src/program_relation_tests.rs @@ -143,7 +143,10 @@ fn intrinsic_unary_filters_finite_target_without_pretending_to_assess_its_output 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(); + let program::IntrinsicUnaryMeasurementV1::ExactSrgb8(measurement) = evidence.measurement() + else { + panic!("the exact constraint must retain exact evidence"); + }; assert_eq!(measurement.expected(), Srgb8::new([0xFF; 3])); assert_eq!(measurement.actual(), Srgb8::new([0xFF; 3])); } @@ -530,7 +533,7 @@ fn directional_reference_cannot_move_with_solver_state() { ); } -fn relation_identity(offset: u32, reverse_candidates: bool) -> program::ContentIdentityV6 { +fn relation_identity(offset: u32, reverse_candidates: bool) -> program::ContentIdentityV7 { 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)); diff --git a/crates/labcolors-core/src/program_session.rs b/crates/labcolors-core/src/program_session.rs index e3120c06..02c14a82 100644 --- a/crates/labcolors-core/src/program_session.rs +++ b/crates/labcolors-core/src/program_session.rs @@ -43,14 +43,16 @@ use crate::clean_set::{ }; use crate::composition::CompositionProfileV1; use crate::constraints::{ - CoreIntrinsicUnaryInvocationV1, CoreIntrinsicUnaryMeasurementV1, CoreIntrinsicUnaryPassV1, - CoreIntrinsicUnaryViolationV1, CoreRelationInvocationV1, CoreRelationMeasurementV1, - CoreRelationPassV1, CoreRelationViolationV1, Evaluator, ExactSrgb8IdentityV1, HardDecision, + CompiledCoreIntrinsicUnaryInvocationV1, 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::family::{FamilyDeclarationV1, FamilyId}; use crate::joint::{ AdmittedFiniteJointOrderV1, FiniteDomainOrdinalV1, FiniteJointOrderAdmissionErrorV1, FiniteJointOrderErrorV1, NonEmptyFiniteDomainCardinalitiesV1, admit_finite_joint_order_v1, @@ -70,7 +72,7 @@ use crate::wcag22::Wcag22CriterionV1; #[path = "program_identity.rs"] mod identity; -pub(crate) use identity::ProgramContentIdentityV6; +pub(crate) use identity::ProgramContentIdentityV7; #[cfg(test)] pub(crate) use identity::edge_role_count_for_test as program_identity_edge_role_count_for_test; #[cfg(test)] @@ -559,6 +561,21 @@ impl ConstraintInvocation { } } + pub(crate) const fn intrinsic_family_membership_hard( + id: ConstraintId, + target: TargetId, + family: FamilyId, + ) -> Self { + Self { + id, + body: ProgramConstraintBodyV1::IntrinsicUnary { + target, + invocation: CoreIntrinsicUnaryInvocationV1::family_membership(family), + }, + mode: PhantomData, + } + } + pub(crate) const fn exact_intrinsic_relation_hard( id: ConstraintId, relation: DirectedRelationV1, @@ -628,6 +645,21 @@ impl ConstraintInvocation { } } + pub(crate) const fn intrinsic_family_membership_report_only( + id: ConstraintId, + target: TargetId, + family: FamilyId, + ) -> Self { + Self { + id, + body: ProgramConstraintBodyV1::IntrinsicUnary { + target, + invocation: CoreIntrinsicUnaryInvocationV1::family_membership(family), + }, + mode: PhantomData, + } + } + pub(crate) const fn declared_srgb8_clean_set_report_only( id: ConstraintId, target: PointPresentationTargetV1, @@ -922,6 +954,7 @@ where { sources: Vec, targets: Vec, + families: Vec, joint_selection: Option, observation_group: ObservationGroup, opacities: Vec, @@ -956,6 +989,7 @@ where Self { sources, targets, + families: Vec::new(), joint_selection: None, observation_group, opacities, @@ -978,6 +1012,11 @@ where self } + pub(crate) fn with_families(mut self, families: Vec) -> Self { + self.families = families; + self + } + pub fn with_point_presentations( mut self, roots: Vec, @@ -1041,6 +1080,10 @@ impl CoreProgramDraftV1 { self.program.targets.push(target); } + pub(crate) fn push_family(&mut self, family: FamilyDeclarationV1) { + self.program.families.push(family); + } + pub(crate) fn set_joint_selection( &mut self, selection: DeclaredJointSelectionV1, @@ -1117,6 +1160,31 @@ impl CoreProgramDraftV1 { )); } + pub(crate) fn push_intrinsic_family_membership_hard( + &mut self, + id: ConstraintId, + target: TargetId, + family: FamilyId, + ) { + self.program + .constraints + .hard + .push(ConstraintInvocation::intrinsic_family_membership_hard( + id, target, family, + )); + } + + pub(crate) fn push_intrinsic_family_membership_report_only( + &mut self, + id: ConstraintId, + target: TargetId, + family: FamilyId, + ) { + self.program.constraints.report_only.push( + ConstraintInvocation::intrinsic_family_membership_report_only(id, target, family), + ); + } + pub(crate) fn push_exact_visible_relation_hard( &mut self, id: ConstraintId, @@ -1190,6 +1258,15 @@ pub enum ProgramCompileError { DuplicateTarget { target: TargetId, }, + DuplicateFamily { + family: FamilyId, + }, + InvalidFamilyImage { + family: FamilyId, + }, + UnusedFamily { + family: FamilyId, + }, MissingFixedSource { target: TargetId, source: SourceId, @@ -1340,6 +1417,10 @@ pub enum ProgramCompileError { constraint: ConstraintId, target: TargetId, }, + MissingConstraintFamily { + constraint: ConstraintId, + family: FamilyId, + }, MissingIntrinsicRelationReference { constraint: ConstraintId, reference: TargetId, @@ -1499,7 +1580,7 @@ enum CompiledProgramConstraintBodyV1 { IntrinsicUnary { target_id: TargetId, target: CompiledPaintInputSlotV1, - invocation: CoreIntrinsicUnaryInvocationV1, + invocation: CompiledCoreIntrinsicUnaryInvocationV1, }, IntrinsicRelation { reference: CompiledIntrinsicRelationEndpointV1, @@ -1815,8 +1896,9 @@ where Evaluation: ProgramConstraintEvaluatorSetV1, ProgramConstraintInvocationOf: Copy, { - content_identity: ProgramContentIdentityV6, + content_identity: ProgramContentIdentityV7, evaluator: Evaluation, + families: Box<[FamilyDeclarationV1]>, graph: CompiledAppearanceGraph, binding_template: AdmittedAppearanceBindings, observation_group: CompiledObservationGroupV1, @@ -1862,7 +1944,7 @@ where /// Opaque ID и порядок неупорядоченных объявлений исключены; явный joint /// order входит в адрес. Адрес не подтверждает поколение владельца и не /// заменяет revision-bound evidence. - pub fn content_identity(&self) -> ProgramContentIdentityV6 { + pub fn content_identity(&self) -> ProgramContentIdentityV7 { self.owner_generation.content_identity } @@ -2524,7 +2606,7 @@ pub(crate) enum ProgramPointCausalConsideredStateV1 { /// одного моделируемого terminal root. Оно ничего не утверждает о пикселе /// браузера, восприятии или качестве цвета. pub(crate) struct ProgramPointCausalEvidenceV1<'report, State> { - content_identity: ProgramContentIdentityV6, + content_identity: ProgramContentIdentityV7, observation: &'report RevisionBoundObservationV1, record: &'report ProgramPointCausalRecordV1, steps: &'report [PointOccurrenceAbsenceStepV1], @@ -2545,7 +2627,7 @@ where .unwrap_or_else(|| unreachable!("тип replay span запрещает пустой пересчёт")) } - pub(crate) const fn content_identity(&self) -> ProgramContentIdentityV6 { + pub(crate) const fn content_identity(&self) -> ProgramContentIdentityV7 { self.content_identity } @@ -2603,7 +2685,7 @@ pub struct ProgramReportV1 where Evaluation: ProgramConstraintEvaluatorSetV1, { - content_identity: ProgramContentIdentityV6, + content_identity: ProgramContentIdentityV7, observation: RevisionBoundObservationV1, arena: ProgramEvaluationArenaLeaseV1, } @@ -2614,7 +2696,7 @@ where { /// Адрес содержимого Program, по которому построен report; это не /// идентификатор поколения и не runtime-authority. - pub const fn content_identity(&self) -> ProgramContentIdentityV6 { + pub const fn content_identity(&self) -> ProgramContentIdentityV7 { self.content_identity } @@ -4238,7 +4320,9 @@ where target: *target_id, value, }; - let (measurement, decision) = invocation.assess(value.source()); + let (measurement, decision) = invocation + .assess(value.source(), &epoch.families) + .ok_or(ProgramSessionEvaluationError::InternalInvariant)?; let (result, is_violation) = match decision { HardDecision::Pass(proof) => ( ProgramConstraintResultV1::Pass( @@ -4583,6 +4667,7 @@ where .checked_len() .ok_or(ProgramCompileError::ResourceExhausted)?; canonicalize_sources_and_targets(&mut program)?; + canonicalize_and_verify_families(&mut program.families)?; let graph = lower_graph(&program)? .compile() @@ -4629,6 +4714,7 @@ where &point_presentations, &program.constraints, )?; + validate_compiled_family_usage(&program.families, &constraints)?; validate_terminal_dependency_cone( &program, &constraints, @@ -4639,10 +4725,12 @@ where 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_v6(&program)?; + let content_identity = identity::compile_program_content_identity_v7(&program)?; + let families = program.families.into_boxed_slice(); Ok(ProgramEpochV1 { content_identity, evaluator: program.evaluator, + families, graph, binding_template, observation_group: CompiledObservationGroupV1 { @@ -4658,6 +4746,63 @@ where }) } +fn canonicalize_and_verify_families( + families: &mut Vec, +) -> Result<(), ProgramCompileError> { + families.sort_unstable_by_key(FamilyDeclarationV1::id); + if let Some(family) = families + .windows(2) + .find(|pair| pair[0].id() == pair[1].id()) + .map(|pair| pair[0].id()) + { + return Err(ProgramCompileError::DuplicateFamily { family }); + } + for family in families { + family + .set() + .verify() + .map_err(|_| ProgramCompileError::InvalidFamilyImage { + family: family.id(), + })?; + } + Ok(()) +} + +fn validate_compiled_family_usage( + families: &[FamilyDeclarationV1], + constraints: &[CompiledPointConstraint], +) -> Result<(), ProgramCompileError> { + let mut used = false_slots(families.len())?; + for constraint in constraints { + let CompiledProgramConstraintBodyV1::IntrinsicUnary { + invocation: + CompiledCoreIntrinsicUnaryInvocationV1::FamilyMembership { + family, + family_index, + }, + .. + } = &constraint.body + else { + continue; + }; + let declaration = families + .get(*family_index) + .ok_or(ProgramCompileError::InternalInvariant)?; + if declaration.id() != *family { + return Err(ProgramCompileError::InternalInvariant); + } + used[*family_index] = true; + } + if let Some(family) = families + .iter() + .zip(used) + .find_map(|(family, used)| (!used).then_some(family.id())) + { + return Err(ProgramCompileError::UnusedFamily { family }); + } + Ok(()) +} + fn canonicalize_sources_and_targets( program: &mut Program, ) -> Result<(), ProgramCompileError> @@ -5504,12 +5649,30 @@ where target: *target, }); } + let invocation = match *invocation { + CoreIntrinsicUnaryInvocationV1::ExactSrgb8 { expected } => { + CompiledCoreIntrinsicUnaryInvocationV1::ExactSrgb8 { expected } + } + CoreIntrinsicUnaryInvocationV1::FamilyMembership { family } => { + let family_index = program + .families + .binary_search_by_key(&family, FamilyDeclarationV1::id) + .map_err(|_| ProgramCompileError::MissingConstraintFamily { + constraint: constraint.id, + family, + })?; + CompiledCoreIntrinsicUnaryInvocationV1::FamilyMembership { + family, + family_index, + } + } + }; CompiledProgramConstraintBodyV1::IntrinsicUnary { target_id: *target, target: graph .bind_paint_input(target_paint_input_id(*target)) .ok_or(ProgramCompileError::InternalInvariant)?, - invocation: *invocation, + invocation, } } ProgramConstraintBodyV1::IntrinsicRelation { diff --git a/scripts/verify_point_support_surplus.py b/scripts/verify_point_support_surplus.py index 9e0730a1..810a0e0d 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 = ( - "f9a56da33469e5765c62fae6ab7fcb1f870a550cd9cd29dc160a48f65091bcbd" + "da7ae91894b26c300b14df3b5c858bf238aeaba1d46a6c946a08550dc02aa74f" ) EXPECTED_Q55_PROOF_SHA256 = ( "ac59cf89503170c789223b91d775213a19d4e571ef930f2ea609fcd51b14defd"