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 f791b4c0..9876ba9a 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": 13830, + "bytes": 13860, "license": "MIT", "path": "crates/labcolors-core/src/lib.rs", "role": "module_registration_source", - "sha256": "1fdb8645a1f05c61232e362b89ef583198c131c493c3fab20c5ec545d02c6539" + "sha256": "55441efda3b9d8f8534cc015a15d4bf1390d74435738a1196c3bd44b6fb55120" }, { "bytes": 39439, @@ -69,11 +69,11 @@ "sha256": "ec6585496208972e183d3a5bcd1f67bae4d89cc8e6a797d9c6443d8e0754502b" }, { - "bytes": 46204, + "bytes": 51180, "license": "MIT", "path": "crates/labcolors-core/src/program/attachment.rs", "role": "point_attachment_source", - "sha256": "172fa95ca1a4508c149c1ec0905baa7d1f93195e63a7574d4ed4b133e50099cc" + "sha256": "4caa55a5a72fbc9aa682c8a0fee5fd5490b2fd64bd4ce13c6d941c5bc6eecc7a" }, { "bytes": 35835, @@ -83,32 +83,32 @@ "sha256": "0c8a7bb59464e1ddcace07b01ba24a64f8c9d3aeff547651dc38c276a0fc3322" }, { - "bytes": 70163, + "bytes": 84322, "license": "MIT", "path": "crates/labcolors-core/src/program/attachment/tests.rs", "role": "point_attachment_tests", - "sha256": "ac8c390a74c8aaea9e04d1006f3604d72f94a980ce26645d08593f025c4fc011" + "sha256": "ec01bb517beca62783c9b0a5335a6ef73fea420afcbf53e315deb28938de3382" }, { - "bytes": 195880, + "bytes": 195771, "license": "MIT", "path": "crates/labcolors-core/src/program.rs", "role": "program_facade_source", - "sha256": "86e372337ad2f21779596c8e608c6c18465577d336a2fc0a5dfcff0aaead5ec7" + "sha256": "6e187abc464884db41da7ab1fbec49abf97eb1a83a111f1c62e3a24ec0c069f9" }, { - "bytes": 82393, + "bytes": 82656, "license": "MIT", "path": "crates/labcolors-core/src/program_identity.rs", "role": "program_identity_source", - "sha256": "c6ae7d7e24240aa4249df1bf74f0e95711b6b889bd3650acdf11e1dd82f34123" + "sha256": "a1b7a592ad5046a686920102fd212319f000ee550ba179d1e20e846960fa781b" }, { - "bytes": 220403, + "bytes": 224928, "license": "MIT", "path": "crates/labcolors-core/src/program_session.rs", "role": "program_source", - "sha256": "db522c68cc992d71f806997123ead19fe5d7064d13b47580ea3fc33f95f02d00" + "sha256": "74ed68dfcffd487425eaca04917dd3c55921658209ee2394c539ee30ecbc8442" }, { "bytes": 21872, @@ -125,11 +125,11 @@ "sha256": "aa6aa7c0b630437f1c1ba8c2ceafb0dadf6551c42331559504076a6cd44e6331" }, { - "bytes": 26829, + "bytes": 27055, "license": "MIT", "path": "crates/labcolors-core/src/session.rs", "role": "session_runtime_source", - "sha256": "383b163f9e715e3f5b313a3c29f962968dec3df2414cc8172e2def6b43236a83" + "sha256": "38c65b46a3e392b399092e8c4834d7f31acb2b91eff626d6faa1b1aaa90b52f3" }, { "bytes": 34105, 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 12b026a0..e52fa872 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 @@ -682da61a4becf096e83fef3052e12ee247b0a12359f4583ddf8a2ed04178175d receipt-v1.json +7faaf2132ca3eb0f3686c81953ecc3e3327e80d1d036f92c5f3227fc99cbb613 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 42fe0425..3a643248 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":"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"} +{"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":"bff7d4c424a9acb7be8e91c32768f585ae2a43e43a87734f6913aaf835468b71","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":"faef3711ffbfeb823e7187996d2f2a06171dea273fab2cd0aa89a56d6fefc95d","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":"f18bcafd7f911e84049cf9dae9673d6d6e0ed1fd50a9a7635a0ff597953b7727"},{"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":"55441efda3b9d8f8534cc015a15d4bf1390d74435738a1196c3bd44b6fb55120"},{"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":"38c65b46a3e392b399092e8c4834d7f31acb2b91eff626d6faa1b1aaa90b52f3"},{"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":"ac10b71590385532f4989112fe47887b6275d780db79dfa75a435e7a2148fccb"} diff --git a/crates/labcolors-core/src/constraints/family.rs b/crates/labcolors-core/src/constraints/family.rs index 5ec1509f..d5504229 100644 --- a/crates/labcolors-core/src/constraints/family.rs +++ b/crates/labcolors-core/src/constraints/family.rs @@ -1,9 +1,9 @@ //! Метаданные code-owned evaluator-а принадлежности точному образу family. use crate::family::{ - AdmittedFamilySetV1, FamilyMembershipMeasurementV1, FamilyMembershipPassV1, - FamilyMembershipViolationV1, + FamilyMembershipMeasurementV2, FamilyMembershipPassV1, FamilyMembershipViolationV1, }; +use crate::family_artifact::AdmittedFamilyArtifactV2; use crate::lcs_occurrence::ColorSignal; use super::HardDecision; @@ -14,8 +14,8 @@ pub(crate) enum FamilyMembershipIdentityV1 { } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum FamilyMembershipReleaseV1 { - V1, +pub(crate) enum FamilyMembershipReleaseV2 { + V2, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -24,15 +24,15 @@ pub(crate) enum FamilyMembershipCapabilityV1 { } #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] -pub(crate) struct FamilyMembershipV1; +pub(crate) struct FamilyMembershipV2; -impl FamilyMembershipV1 { +impl FamilyMembershipV2 { pub(crate) const fn identity(self) -> FamilyMembershipIdentityV1 { FamilyMembershipIdentityV1::ExactImageMembershipV1 } - pub(crate) const fn release(self) -> FamilyMembershipReleaseV1 { - FamilyMembershipReleaseV1::V1 + pub(crate) const fn release(self) -> FamilyMembershipReleaseV2 { + FamilyMembershipReleaseV2::V2 } pub(crate) const fn capability(self) -> FamilyMembershipCapabilityV1 { @@ -41,10 +41,10 @@ impl FamilyMembershipV1 { pub(crate) fn assess( self, - family: &AdmittedFamilySetV1, + family: &AdmittedFamilyArtifactV2, signal: ColorSignal, ) -> ( - FamilyMembershipMeasurementV1, + FamilyMembershipMeasurementV2, HardDecision, ) { family.assess(signal) diff --git a/crates/labcolors-core/src/constraints/mod.rs b/crates/labcolors-core/src/constraints/mod.rs index 33c3be28..ae171fe1 100644 --- a/crates/labcolors-core/src/constraints/mod.rs +++ b/crates/labcolors-core/src/constraints/mod.rs @@ -22,8 +22,8 @@ pub(crate) use exact::{ mod family; pub(crate) use family::{ - FamilyMembershipCapabilityV1, FamilyMembershipIdentityV1, FamilyMembershipReleaseV1, - FamilyMembershipV1, + FamilyMembershipCapabilityV1, FamilyMembershipIdentityV1, FamilyMembershipReleaseV2, + FamilyMembershipV2, }; #[cfg(test)] @@ -520,7 +520,7 @@ pub(crate) enum ProgramConstraintContentV1 { }, FamilyMembership { identity: FamilyMembershipIdentityV1, - release: FamilyMembershipReleaseV1, + release: FamilyMembershipReleaseV2, capability: FamilyMembershipCapabilityV1, }, ExactSrgb8Relation { diff --git a/crates/labcolors-core/src/constraints/relation.rs b/crates/labcolors-core/src/constraints/relation.rs index 57de3309..8a1a488b 100644 --- a/crates/labcolors-core/src/constraints/relation.rs +++ b/crates/labcolors-core/src/constraints/relation.rs @@ -1,9 +1,9 @@ use crate::Srgb8; -use crate::constraints::{FamilyMembershipV1, HardDecision, ProgramConstraintContentV1}; +use crate::constraints::{FamilyMembershipV2, HardDecision, ProgramConstraintContentV1}; use crate::family::{ - FamilyDeclarationV1, FamilyId, FamilyMembershipMeasurementV1, FamilyMembershipPassV1, - FamilyMembershipViolationV1, + FamilyId, FamilyMembershipMeasurementV2, FamilyMembershipPassV1, FamilyMembershipViolationV1, }; +use crate::family_artifact::BoundFamilyArtifactBundleV2; use crate::lcs_occurrence::ColorSignal; fn exact_srgb8_equal(left: Srgb8, right: Srgb8) -> bool { @@ -188,7 +188,7 @@ pub(crate) enum CoreIntrinsicUnaryMeasurementV1 { ExactSrgb8(ExactSrgb8IntrinsicUnaryMeasurementV1), FamilyMembership { family: FamilyId, - measurement: FamilyMembershipMeasurementV1, + measurement: FamilyMembershipMeasurementV2, }, } @@ -225,7 +225,7 @@ impl CoreIntrinsicUnaryInvocationV1 { } } Self::FamilyMembership { .. } => { - let profile = FamilyMembershipV1; + let profile = FamilyMembershipV2; ProgramConstraintContentV1::FamilyMembership { identity: profile.identity(), release: profile.release(), @@ -240,7 +240,7 @@ impl CompiledCoreIntrinsicUnaryInvocationV1 { pub(crate) fn assess( self, actual: Srgb8, - families: &[FamilyDeclarationV1], + families: &BoundFamilyArtifactBundleV2, ) -> Option<( CoreIntrinsicUnaryMeasurementV1, HardDecision, @@ -265,16 +265,12 @@ impl CompiledCoreIntrinsicUnaryInvocationV1 { 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(); + // Compile-time ordinal и Session projection независимо + // связываются через semantic release; runtime lookup не читает + // opaque FamilyId. `None` поэтому означает порчу compiled state. + let artifact = families.artifact(family_index)?; let (measurement, decision) = - FamilyMembershipV1.assess(set, ColorSignal::from_srgb8(actual)); + FamilyMembershipV2.assess(artifact, ColorSignal::from_srgb8(actual)); let decision = match decision { HardDecision::Pass(proof) => { HardDecision::Pass(CoreIntrinsicUnaryPassV1::FamilyMembership(proof)) diff --git a/crates/labcolors-core/src/family.rs b/crates/labcolors-core/src/family.rs index 089ecf9c..9e231135 100644 --- a/crates/labcolors-core/src/family.rs +++ b/crates/labcolors-core/src/family.rs @@ -1,13 +1,10 @@ -//! Точный конечный образ versioned family-generator-а. +//! Semantic contract точного конечного family image. //! -//! Family здесь — множество физических [`ColorSignal`], а не роль, оттенок или -//! обещание визуальной чистоты. Объявленное множество полно по определению; -//! независимо вычисленный образ допускается только после исчерпывающего -//! равенства канонических множеств. SHA-256 адресует весь проверенный объект. +//! Opaque [`FamilyId`] маршрутизирует клиентский граф, representation-independent +//! semantic release связывает definition и канонический image, а membership +//! measurement называет проверенный [`ColorSignal`]. Transport, proof и verifier +//! admission принадлежат только `family_artifact` и не входят в Program identity. -#[cfg(test)] -use crate::Srgb8; -use crate::constraints::HardDecision; use crate::lcs_occurrence::{ColorSignal, OutputProfileId}; use crate::sha256::Hasher; @@ -22,23 +19,33 @@ mod assess_counter { } } -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; +#[cfg(test)] +const FAMILY_DEFINITION_DOMAIN_V2: &[u8] = b"labcolors.family-definition.v2\0"; +const FAMILY_IMAGE_DOMAIN_V2: &[u8] = b"labcolors.family-canonical-image.v2\0"; +const FAMILY_SEMANTIC_RELEASE_DOMAIN_V2: &[u8] = b"labcolors.family-semantic-release.v2\0"; +// Image и semantic preimage развиваются независимо; общий literal мог бы +// незаметно переиспользовать старый content address при изменении только одного. +const FAMILY_IMAGE_ENCODING_RELEASE_V2: u8 = 2; +const FAMILY_SEMANTIC_ENCODING_RELEASE_V2: u8 = 2; +// Канонические кодеки пишут длины как u64. Этот закон платформы делает каждое +// usize → u64 преобразование точным вместо ложной runtime-ветки ошибки. +const _: () = assert!(usize::BITS <= u64::BITS); #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] #[repr(u8)] -enum OutputProfileTagV1 { - // Дискриминант принадлежит canonical codec family V1; новый output profile +enum OutputProfileTag { + // Дискриминант принадлежит canonical codec family V2; новый output profile // требует нового tag, sensitivity vectors и явного решения, может ли один // family-set вообще содержать сигналы разных профилей. Iec61966Srgb8D65V1 = 1, } +const fn output_profile_tag(profile: OutputProfileId) -> OutputProfileTag { + match profile { + OutputProfileId::Iec61966Srgb8D65V1 => OutputProfileTag::Iec61966Srgb8D65V1, + } +} + /// Непрозрачный клиентский ключ одного объявленного family-set. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub(crate) struct FamilyId(u32); @@ -53,812 +60,163 @@ impl FamilyId { } } +/// Контентный адрес определения provider-а до его точного конечного образа. +/// +/// Context, transform и параметры будущего provider-а входят в этот digest; +/// storage codec и opaque [`FamilyId`] не входят. #[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]); +pub(crate) struct FamilyDefinitionDigestV2([u8; 32]); -impl FamilyImageContentIdentityV1 { - pub(crate) const fn as_bytes(&self) -> &[u8; 32] { - &self.0 +impl FamilyDefinitionDigestV2 { + pub(crate) const fn from_digest(bytes: [u8; 32]) -> Self { + Self(bytes) } -} - -#[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 }) + pub(crate) fn from_fixture_bytes_v2(bytes: &[u8]) -> Self { + let mut hasher = Hasher::new(); + hasher.update(FAMILY_DEFINITION_DOMAIN_V2); + hasher.update(bytes); + Self(*hasher.finalize().as_bytes()) } - - #[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 - } +/// Representation-independent адрес канонического точного множества сигналов. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct CanonicalFamilyImageDigestV2([u8; 32]); - #[cfg(test)] - pub(crate) const fn preimage_count(self) -> u64 { - self.preimage_count +impl CanonicalFamilyImageDigestV2 { + pub(crate) const fn from_digest(bytes: [u8; 32]) -> Self { + Self(bytes) } - #[cfg(test)] - pub(crate) const fn member_count(self) -> u64 { - self.member_count + pub(crate) const fn as_bytes(&self) -> &[u8; 32] { + &self.0 } } -#[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; - } - } +/// Семантический release family: provider definition + его точный образ. +/// +/// Два lossless artifact codec-а одного release имеют этот же ID, но разные +/// artifact receipts. Opaque client ID также не участвует в адресе. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct SemanticFamilyReleaseIdV2([u8; 32]); - #[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"); +impl SemanticFamilyReleaseIdV2 { + pub(crate) const fn from_digest(bytes: [u8; 32]) -> Self { + Self(bytes) } - #[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"); + pub(crate) const fn as_bytes(&self) -> &[u8; 32] { + &self.0 } } -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) struct FamilyDeclarationV1 { +/// Program-декларация связывает opaque ID только с semantic release, не с bytes. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct FamilyDeclarationV2 { id: FamilyId, - set: AdmittedFamilySetV1, + semantic: SemanticFamilyReleaseIdV2, } -impl FamilyDeclarationV1 { - pub(crate) const fn new(id: FamilyId, set: AdmittedFamilySetV1) -> Self { - Self { id, set } +impl FamilyDeclarationV2 { + pub(crate) const fn new(id: FamilyId, semantic: SemanticFamilyReleaseIdV2) -> Self { + Self { id, semantic } } - pub(crate) const fn id(&self) -> FamilyId { + pub(crate) const fn id(self) -> FamilyId { self.id } - pub(crate) const fn set(&self) -> &AdmittedFamilySetV1 { - &self.set + pub(crate) const fn semantic(self) -> SemanticFamilyReleaseIdV2 { + self.semantic } } +/// Membership evidence связывает semantic release и проверенный сигнал. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) struct FamilyMembershipMeasurementV1 { - family: FamilyContentIdentityV1, +pub(crate) struct FamilyMembershipMeasurementV2 { + semantic: SemanticFamilyReleaseIdV2, signal: ColorSignal, } -impl FamilyMembershipMeasurementV1 { - pub(crate) const fn family(self) -> FamilyContentIdentityV1 { - self.family - } - - pub(crate) const fn signal(self) -> ColorSignal { - self.signal +impl FamilyMembershipMeasurementV2 { + pub(crate) const fn new(semantic: SemanticFamilyReleaseIdV2, signal: ColorSignal) -> Self { + Self { semantic, 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); + pub(crate) const fn semantic(self) -> SemanticFamilyReleaseIdV2 { + self.semantic } - 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; - } + pub(crate) const fn signal(self) -> ColorSignal { + self.signal } - (missing, extraneous) } -fn certificate_for( - generator_release: FamilyGeneratorReleaseV1, - generator_content_identity: FamilyGeneratorContentIdentityV1, +pub(crate) fn canonical_family_image_digest_v2( + output_profile: OutputProfileId, members: &[ColorSignal], - preimage_count: u64, - proof_release: FamilyImageProofReleaseV1, -) -> Result { - let first = members - .first() +) -> Result { + if members + .iter() .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, - )); + .any(|member| member.output_profile() != output_profile) + || members + .windows(2) + .any(|pair| family_image_member_key(pair[0]) >= family_image_member_key(pair[1])) + { + return Err(CanonicalFamilyImageErrorV2::NonCanonicalAdmittedImage); + } + let count = members.len() as u64; 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(FAMILY_IMAGE_DOMAIN_V2); hasher.update(&[ + FAMILY_IMAGE_ENCODING_RELEASE_V2, 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() + Ok(CanonicalFamilyImageDigestV2(*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 family_image_member_key(member: ColorSignal) -> (u8, [u8; 3]) { + ( + output_profile_tag(member.output_profile()) as u8, + member.srgb8().bytes(), + ) } -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)?; +pub(crate) fn semantic_family_release_id_v2( + definition: FamilyDefinitionDigestV2, + image: CanonicalFamilyImageDigestV2, + member_count: u64, +) -> SemanticFamilyReleaseIdV2 { 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), + hasher.update(FAMILY_SEMANTIC_RELEASE_DOMAIN_V2); + hasher.update(&[FAMILY_SEMANTIC_ENCODING_RELEASE_V2]); + hasher.update(definition.as_bytes()); + hasher.update(image.as_bytes()); + hasher.update(&member_count.to_be_bytes()); + SemanticFamilyReleaseIdV2(*hasher.finalize().as_bytes()) } -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 - } - } - } +/// Membership proof is intentionally empty: semantic release and queried +/// signal live in measurement. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct FamilyMembershipPassV1; - 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]); - } - } - } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct FamilyMembershipViolationV1; - 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, - )), - } - } +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum CanonicalFamilyImageErrorV2 { + NonCanonicalAdmittedImage, } diff --git a/crates/labcolors-core/src/family_artifact.rs b/crates/labcolors-core/src/family_artifact.rs new file mode 100644 index 00000000..29c81d9e --- /dev/null +++ b/crates/labcolors-core/src/family_artifact.rs @@ -0,0 +1,1161 @@ +//! Content-bound transport boundary одного точного family image. +//! +//! Semantic release описывает семейство независимо от хранения. Artifact +//! receipt отдельно связывает envelope, codec и payload. Loader допускает bytes +//! до появления executable view; V5b2p намеренно не содержит production codec. + +#![cfg_attr( + not(test), + expect( + dead_code, + reason = "V5b2p is a private loader precursor; V5b2a supplies its first production codec and caller" + ) +)] + +use core::fmt; + +use crate::family::{ + CanonicalFamilyImageDigestV2, CanonicalFamilyImageErrorV2, FamilyDeclarationV2, + FamilyDefinitionDigestV2, FamilyMembershipMeasurementV2, FamilyMembershipPassV1, + FamilyMembershipViolationV1, SemanticFamilyReleaseIdV2, canonical_family_image_digest_v2, + semantic_family_release_id_v2, +}; +use crate::lcs_occurrence::{ColorSignal, OutputProfileId}; +use crate::sha256::Hasher; + +const MAGIC_V2: &[u8; 8] = b"LCFAM2\0\0"; +const ENVELOPE_RELEASE_V2: u8 = 2; +const SIGNAL_DOMAIN_SRGB8_D65_V1: u8 = 1; +// Полная кардинальность sRGB8: большее exact-множество обязано повторить сигнал. +const MAX_SRGB8_MEMBER_COUNT_V1: u64 = 1 << 24; +const SIGNAL_ORDINAL_RGB_BIG_ENDIAN_V1: u8 = 1; +const PROOF_RELEASE_FIXTURE_EXACT_IMAGE_V1: u8 = 1; +const VERIFIER_RELEASE_FIXTURE_REPLAY_V1: u8 = 1; +// magic + 6 release/domain tags + 6 SHA-256 identities + 2 u64 lengths + receipt. +// Любое изменение certificate layout обязано синхронно менять этот размер. +const HEADER_LEN_V2: usize = 254; +const PAYLOAD_DIGEST_DOMAIN_V2: &[u8] = b"labcolors.family-artifact-payload.v2\0"; +const RECEIPT_DOMAIN_V2: &[u8] = b"labcolors.family-artifact-receipt.v2\0"; +const FIXTURE_PROOF_ARTIFACT_DOMAIN_V2: &[u8] = b"labcolors.family-artifact-fixture-proof.v2\0"; +const FIXTURE_VERIFIER_IDENTITY_DOMAIN_V2: &[u8] = + b"labcolors.family-artifact-fixture-verifier.v2\0"; + +#[cfg(test)] +pub(crate) use decoder_counter::FAMILY_ARTIFACT_DECODER_CALLS; +#[cfg(test)] +pub(crate) use decoder_counter::FAMILY_ARTIFACT_PAYLOAD_DIGEST_CALLS; + +#[cfg(test)] +mod decoder_counter { + std::thread_local! { + pub(crate) static FAMILY_ARTIFACT_DECODER_CALLS: core::cell::Cell = + const { core::cell::Cell::new(0) }; + pub(crate) static FAMILY_ARTIFACT_PAYLOAD_DIGEST_CALLS: core::cell::Cell = + const { core::cell::Cell::new(0) }; + } +} + +/// Content address конкретного artifact representation. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct FamilyArtifactReceiptIdV2([u8; 32]); + +impl FamilyArtifactReceiptIdV2 { + pub(crate) const fn from_digest(bytes: [u8; 32]) -> Self { + Self(bytes) + } + + pub(crate) const fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } +} + +/// Content identity exact proof artifact, отдельно от proof algorithm release. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct FamilyProofArtifactIdV2([u8; 32]); + +impl FamilyProofArtifactIdV2 { + pub(crate) const fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } +} + +/// Content identity независимо поставленного verifier implementation. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct FamilyVerifierIdentityV2([u8; 32]); + +impl FamilyVerifierIdentityV2 { + pub(crate) const fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } +} + +/// Отдельный certificate semantic release и его transport representation. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct FamilyImageCertificateV2 { + envelope_release: u8, + codec_release: u8, + signal_domain: u8, + signal_ordinal: u8, + proof_release: u8, + verifier_release: u8, + proof_artifact: FamilyProofArtifactIdV2, + verifier_identity: FamilyVerifierIdentityV2, + definition_digest: FamilyDefinitionDigestV2, + image_digest: CanonicalFamilyImageDigestV2, + semantic_release: SemanticFamilyReleaseIdV2, + payload_digest: [u8; 32], + member_count: u64, + payload_len: u64, + artifact_receipt: FamilyArtifactReceiptIdV2, +} + +impl FamilyImageCertificateV2 { + pub(crate) const fn semantic_release(self) -> SemanticFamilyReleaseIdV2 { + self.semantic_release + } + + pub(crate) const fn image_digest(self) -> CanonicalFamilyImageDigestV2 { + self.image_digest + } + + pub(crate) const fn artifact_receipt(self) -> FamilyArtifactReceiptIdV2 { + self.artifact_receipt + } + + pub(crate) const fn proof_artifact(self) -> FamilyProofArtifactIdV2 { + self.proof_artifact + } + + pub(crate) const fn verifier_identity(self) -> FamilyVerifierIdentityV2 { + self.verifier_identity + } + + #[cfg(test)] + pub(crate) fn identity_mutants_for_test(self) -> Vec { + let mut mutants = Vec::with_capacity(11); + + let mut codec = self; + codec.codec_release = match codec.codec_release { + 0xF1 => 0xF2, + _ => 0xF1, + }; + mutants.push(codec); + + let mut definition = self; + let mut definition_bytes = *definition.definition_digest.as_bytes(); + definition_bytes[0] ^= 1; + definition.definition_digest = FamilyDefinitionDigestV2::from_digest(definition_bytes); + mutants.push(definition); + + let mut image = self; + let mut image_bytes = *image.image_digest.as_bytes(); + image_bytes[0] ^= 1; + image.image_digest = CanonicalFamilyImageDigestV2::from_digest(image_bytes); + mutants.push(image); + + let mut semantic = self; + let mut semantic_bytes = *semantic.semantic_release.as_bytes(); + semantic_bytes[0] ^= 1; + semantic.semantic_release = SemanticFamilyReleaseIdV2::from_digest(semantic_bytes); + mutants.push(semantic); + + let mut payload = self; + payload.payload_digest[0] ^= 1; + mutants.push(payload); + + let mut member_count = self; + member_count.member_count ^= 1; + mutants.push(member_count); + + let mut payload_len = self; + payload_len.payload_len ^= 1; + mutants.push(payload_len); + + let mut proof = self; + proof.proof_release ^= 1; + mutants.push(proof); + + let mut proof_artifact = self; + proof_artifact.proof_artifact.0[0] ^= 1; + mutants.push(proof_artifact); + + let mut verifier_identity = self; + verifier_identity.verifier_identity.0[0] ^= 1; + mutants.push(verifier_identity); + + let mut receipt = self; + receipt.artifact_receipt.0[0] ^= 1; + mutants.push(receipt); + mutants + } + + #[cfg(test)] + pub(crate) fn receipt_mismatch_for_test(mut self) -> Self { + self.artifact_receipt.0[0] ^= 1; + self + } + + #[cfg(test)] + pub(crate) fn semantic_mismatch_with_valid_receipt_for_test(mut self) -> Self { + let mut bytes = *self.semantic_release.as_bytes(); + bytes[0] ^= 1; + self.semantic_release = SemanticFamilyReleaseIdV2::from_digest(bytes); + self.artifact_receipt = artifact_receipt(self); + self + } + + #[cfg(test)] + pub(crate) fn image_mismatch_with_coherent_certificate_for_test(mut self) -> Self { + let mut bytes = *self.image_digest.as_bytes(); + bytes[0] ^= 1; + self.image_digest = CanonicalFamilyImageDigestV2::from_digest(bytes); + self.semantic_release = semantic_family_release_id_v2( + self.definition_digest, + self.image_digest, + self.member_count, + ); + self.artifact_receipt = artifact_receipt(self); + self + } + + #[cfg(test)] + pub(crate) fn codec_with_valid_receipt_for_test(mut self, codec_release: u8) -> Self { + self.codec_release = codec_release; + self.artifact_receipt = artifact_receipt(self); + self + } + + #[cfg(test)] + pub(crate) fn member_count_with_coherent_certificate_for_test( + mut self, + member_count: u64, + ) -> Self { + self.member_count = member_count; + self.semantic_release = semantic_family_release_id_v2( + self.definition_digest, + self.image_digest, + self.member_count, + ); + self.artifact_receipt = artifact_receipt(self); + self + } +} + +/// Owned transport bytes. Admission never borrows host storage. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct EncodedFamilyArtifactV2(Box<[u8]>); + +impl EncodedFamilyArtifactV2 { + /// Принимает exact owned bytes от transport adapter без заимствования. + pub(crate) const fn from_owned_bytes(bytes: Box<[u8]>) -> Self { + Self(bytes) + } + + /// Возвращает те же bytes для исправления, хранения или другой admission. + pub(crate) fn into_bytes(self) -> Box<[u8]> { + self.0 + } + + #[cfg(test)] + pub(crate) fn from_raw_bytes_for_test(bytes: Vec) -> Self { + Self(bytes.into_boxed_slice()) + } + + #[cfg(test)] + pub(crate) fn flip_first_payload_bit_for_test(&mut self) { + self.0[HEADER_LEN_V2] ^= 1; + } + + #[cfg(test)] + pub(crate) fn truncate_one_byte_for_test(self) -> Self { + let mut bytes = self.0.into_vec(); + bytes.pop(); + Self(bytes.into_boxed_slice()) + } + + #[cfg(test)] + pub(crate) fn extend_one_byte_for_test(self) -> Self { + let mut bytes = self.0.into_vec(); + bytes.push(0); + Self(bytes.into_boxed_slice()) + } + + #[cfg(test)] + pub(crate) fn with_certificate_for_test( + mut self, + certificate: FamilyImageCertificateV2, + ) -> Self { + let mut encoded = Vec::with_capacity(HEADER_LEN_V2 - MAGIC_V2.len()); + encode_certificate(&mut encoded, certificate); + self.0[MAGIC_V2.len()..HEADER_LEN_V2].copy_from_slice(&encoded); + self + } + + #[cfg(test)] + pub(crate) fn corrupt_magic_for_test(mut self) -> Self { + self.0[0] ^= 1; + self + } + + #[cfg(test)] + pub(crate) fn corrupt_envelope_field_for_test(mut self, field: FixtureEnvelopeFieldV1) -> Self { + let offset = match field { + FixtureEnvelopeFieldV1::EnvelopeRelease => 0, + FixtureEnvelopeFieldV1::SignalDomain => 2, + FixtureEnvelopeFieldV1::SignalOrdinal => 3, + FixtureEnvelopeFieldV1::ProofRelease => 4, + FixtureEnvelopeFieldV1::VerifierRelease => 5, + }; + self.0[MAGIC_V2.len() + offset] ^= 1; + self + } + + #[cfg(test)] + pub(crate) fn truncate_inside_header_for_test(self) -> Self { + Self(self.0[..HEADER_LEN_V2 - 1].into()) + } +} + +#[cfg(test)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum FixtureEnvelopeFieldV1 { + EnvelopeRelease, + SignalDomain, + SignalOrdinal, + ProofRelease, + VerifierRelease, +} + +/// Executable storage after all envelope, digest and semantic checks. +#[derive(Debug)] +pub(crate) struct AdmittedFamilyArtifactV2 { + certificate: FamilyImageCertificateV2, + members: Box<[ColorSignal]>, + #[cfg(test)] + drop_probe: Option, +} + +#[cfg(test)] +#[derive(Debug)] +struct ArtifactDropProbeV1(std::rc::Rc>); + +#[cfg(test)] +impl Drop for ArtifactDropProbeV1 { + fn drop(&mut self) { + self.0.set(self.0.get() + 1); + } +} + +impl AdmittedFamilyArtifactV2 { + pub(crate) const fn semantic_release(&self) -> SemanticFamilyReleaseIdV2 { + self.certificate.semantic_release + } + + pub(crate) const fn artifact_receipt(&self) -> FamilyArtifactReceiptIdV2 { + self.certificate.artifact_receipt + } + + pub(crate) fn contains(&self, signal: ColorSignal) -> bool { + self.members + .binary_search_by_key(&signal_key(signal), |member| signal_key(*member)) + .is_ok() + } + + pub(crate) fn assess( + &self, + signal: ColorSignal, + ) -> ( + FamilyMembershipMeasurementV2, + crate::constraints::HardDecision, + ) { + #[cfg(test)] + crate::family::FAMILY_MEMBERSHIP_ASSESS_CALLS.with(|calls| calls.set(calls.get() + 1)); + let measurement = FamilyMembershipMeasurementV2::new(self.semantic_release(), signal); + let decision = if self.contains(signal) { + crate::constraints::HardDecision::Pass(FamilyMembershipPassV1) + } else { + crate::constraints::HardDecision::Violation(FamilyMembershipViolationV1) + }; + (measurement, decision) + } + + #[cfg(test)] + pub(crate) fn with_drop_counter_for_test( + mut self, + counter: std::rc::Rc>, + ) -> Self { + self.drop_probe = Some(ArtifactDropProbeV1(counter)); + self + } +} + +/// Loader отказывается до decoder-а при любом transport/certificate mismatch. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum FamilyArtifactLoadErrorV1 { + HeaderTooShort, + InvalidMagic, + UnsupportedEnvelope, + UnsupportedSignalDomain, + UnsupportedSignalOrdinal, + UnsupportedProofRelease, + UnsupportedVerifierRelease, + InvalidMemberCount, + ExactLengthMismatch { expected: usize, actual: usize }, + ForeignCertificate, + PayloadDigestMismatch, + ArtifactReceiptMismatch, + SemanticReleaseMismatch, + UnsupportedCodec, + InvalidCodecPayload, + DecodedMemberCountMismatch { expected: u64, actual: u64 }, + ImageDigestMismatch, + ResourceExhausted, +} + +/// Неуспешный admission возвращает исходные owned bytes для исправления, +/// повторной попытки или точной диагностики без refetch/clone. +#[derive(PartialEq, Eq)] +pub(crate) struct FamilyArtifactLoadFailureV1 { + cause: FamilyArtifactLoadErrorV1, + encoded: EncodedFamilyArtifactV2, +} + +impl fmt::Debug for FamilyArtifactLoadFailureV1 { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("FamilyArtifactLoadFailureV1") + .field("cause", &self.cause) + .finish_non_exhaustive() + } +} + +impl FamilyArtifactLoadFailureV1 { + pub(crate) const fn cause(&self) -> FamilyArtifactLoadErrorV1 { + self.cause + } + + pub(crate) fn into_parts(self) -> (FamilyArtifactLoadErrorV1, EncodedFamilyArtifactV2) { + (self.cause, self.encoded) + } +} + +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub(crate) struct FamilyArtifactLoaderV1; + +impl FamilyArtifactLoaderV1 { + pub(crate) fn load( + expected: FamilyImageCertificateV2, + encoded: EncodedFamilyArtifactV2, + ) -> Result { + Self::load_with_codec( + expected, + encoded, + reject_unreleased_codec, + decode_unreachable_codec, + ) + } + + #[cfg(test)] + pub(crate) fn load_fixture( + expected: FamilyImageCertificateV2, + encoded: EncodedFamilyArtifactV2, + ) -> Result { + Self::load_with_codec( + expected, + encoded, + preflight_fixture_payload, + decode_fixture_payload, + ) + } + + #[cfg(test)] + pub(crate) fn load_with_decoder_for_test( + expected: FamilyImageCertificateV2, + encoded: EncodedFamilyArtifactV2, + decoder: impl FnOnce(u8, u64, &[u8]) -> Result, FamilyArtifactLoadErrorV1>, + ) -> Result { + Self::load_with_codec(expected, encoded, preflight_fixture_payload, decoder) + } + + #[cfg(test)] + pub(crate) fn load_with_codec_for_test( + expected: FamilyImageCertificateV2, + encoded: EncodedFamilyArtifactV2, + preflight: impl FnOnce(u8, u64, u64) -> Result<(), FamilyArtifactLoadErrorV1>, + decoder: impl FnOnce(u8, u64, &[u8]) -> Result, FamilyArtifactLoadErrorV1>, + ) -> Result { + Self::load_with_codec(expected, encoded, preflight, decoder) + } + + fn load_with_codec( + expected: FamilyImageCertificateV2, + encoded: EncodedFamilyArtifactV2, + preflight: impl FnOnce(u8, u64, u64) -> Result<(), FamilyArtifactLoadErrorV1>, + decoder: impl FnOnce(u8, u64, &[u8]) -> Result, FamilyArtifactLoadErrorV1>, + ) -> Result { + match Self::try_load_with_codec(expected, &encoded, preflight, decoder) { + Ok(admitted) => Ok(admitted), + Err(cause) => Err(FamilyArtifactLoadFailureV1 { cause, encoded }), + } + } + + fn try_load_with_codec( + expected: FamilyImageCertificateV2, + encoded: &EncodedFamilyArtifactV2, + preflight: impl FnOnce(u8, u64, u64) -> Result<(), FamilyArtifactLoadErrorV1>, + decoder: impl FnOnce(u8, u64, &[u8]) -> Result, FamilyArtifactLoadErrorV1>, + ) -> Result { + let parsed = ParsedFamilyArtifactEnvelopeV2::parse(&encoded.0)?; + if parsed.certificate != expected { + return Err(FamilyArtifactLoadErrorV1::ForeignCertificate); + } + preflight( + parsed.certificate.codec_release, + parsed.certificate.member_count, + parsed.certificate.payload_len, + )?; + let payload = &encoded.0[HEADER_LEN_V2..]; + if payload_digest(payload) != parsed.certificate.payload_digest { + return Err(FamilyArtifactLoadErrorV1::PayloadDigestMismatch); + } + if artifact_receipt(parsed.certificate) != parsed.certificate.artifact_receipt { + return Err(FamilyArtifactLoadErrorV1::ArtifactReceiptMismatch); + } + let semantic = semantic_family_release_id_v2( + parsed.certificate.definition_digest, + parsed.certificate.image_digest, + parsed.certificate.member_count, + ); + if semantic != parsed.certificate.semantic_release { + return Err(FamilyArtifactLoadErrorV1::SemanticReleaseMismatch); + } + let verified = VerifiedFamilyArtifactEnvelopeV2::decode(parsed, payload, decoder)?; + Ok(AdmittedFamilyArtifactV2 { + certificate: verified.certificate, + members: verified.members, + #[cfg(test)] + drop_probe: None, + }) + } +} + +struct ParsedFamilyArtifactEnvelopeV2 { + certificate: FamilyImageCertificateV2, +} + +impl ParsedFamilyArtifactEnvelopeV2 { + fn parse(bytes: &[u8]) -> Result { + if bytes.len() < HEADER_LEN_V2 { + return Err(FamilyArtifactLoadErrorV1::HeaderTooShort); + } + if bytes.get(..8) != Some(MAGIC_V2) { + return Err(FamilyArtifactLoadErrorV1::InvalidMagic); + } + let certificate = decode_certificate(&bytes[8..HEADER_LEN_V2]); + if certificate.envelope_release != ENVELOPE_RELEASE_V2 { + return Err(FamilyArtifactLoadErrorV1::UnsupportedEnvelope); + } + if certificate.signal_domain != SIGNAL_DOMAIN_SRGB8_D65_V1 { + return Err(FamilyArtifactLoadErrorV1::UnsupportedSignalDomain); + } + if certificate.signal_ordinal != SIGNAL_ORDINAL_RGB_BIG_ENDIAN_V1 { + return Err(FamilyArtifactLoadErrorV1::UnsupportedSignalOrdinal); + } + if certificate.proof_release != PROOF_RELEASE_FIXTURE_EXACT_IMAGE_V1 { + return Err(FamilyArtifactLoadErrorV1::UnsupportedProofRelease); + } + if certificate.verifier_release != VERIFIER_RELEASE_FIXTURE_REPLAY_V1 { + return Err(FamilyArtifactLoadErrorV1::UnsupportedVerifierRelease); + } + if certificate.member_count > MAX_SRGB8_MEMBER_COUNT_V1 { + return Err(FamilyArtifactLoadErrorV1::InvalidMemberCount); + } + let payload_len = usize::try_from(certificate.payload_len) + .map_err(|_| FamilyArtifactLoadErrorV1::ResourceExhausted)?; + let expected_len = HEADER_LEN_V2 + .checked_add(payload_len) + .ok_or(FamilyArtifactLoadErrorV1::ResourceExhausted)?; + if bytes.len() != expected_len { + return Err(FamilyArtifactLoadErrorV1::ExactLengthMismatch { + expected: expected_len, + actual: bytes.len(), + }); + } + Ok(Self { certificate }) + } +} + +struct VerifiedFamilyArtifactEnvelopeV2 { + certificate: FamilyImageCertificateV2, + members: Box<[ColorSignal]>, +} + +impl VerifiedFamilyArtifactEnvelopeV2 { + fn decode( + parsed: ParsedFamilyArtifactEnvelopeV2, + payload: &[u8], + decoder: impl FnOnce(u8, u64, &[u8]) -> Result, FamilyArtifactLoadErrorV1>, + ) -> Result { + let members = decoder( + parsed.certificate.codec_release, + parsed.certificate.member_count, + payload, + )?; + let actual_count = members.len() as u64; + if actual_count != parsed.certificate.member_count { + return Err(FamilyArtifactLoadErrorV1::DecodedMemberCountMismatch { + expected: parsed.certificate.member_count, + actual: actual_count, + }); + } + let image = canonical_family_image_digest_v2(OutputProfileId::Iec61966Srgb8D65V1, &members) + .map_err(|CanonicalFamilyImageErrorV2::NonCanonicalAdmittedImage| { + FamilyArtifactLoadErrorV1::InvalidCodecPayload + })?; + if image != parsed.certificate.image_digest { + return Err(FamilyArtifactLoadErrorV1::ImageDigestMismatch); + } + Ok(Self { + certificate: parsed.certificate, + members, + }) + } +} + +#[cfg(test)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub(crate) enum FixtureFamilyArtifactCodecV1 { + CanonicalMembersV1 = 0xF1, + ReversedMembersV1 = 0xF2, +} + +fn reject_unreleased_codec( + _codec: u8, + _member_count: u64, + _payload_len: u64, +) -> Result<(), FamilyArtifactLoadErrorV1> { + Err(FamilyArtifactLoadErrorV1::UnsupportedCodec) +} + +fn decode_unreachable_codec( + _codec: u8, + _member_count: u64, + _payload: &[u8], +) -> Result, FamilyArtifactLoadErrorV1> { + Err(FamilyArtifactLoadErrorV1::UnsupportedCodec) +} + +#[cfg(test)] +fn preflight_fixture_payload( + codec: u8, + member_count: u64, + payload_len: u64, +) -> Result<(), FamilyArtifactLoadErrorV1> { + match codec { + value + if value == FixtureFamilyArtifactCodecV1::CanonicalMembersV1 as u8 + || value == FixtureFamilyArtifactCodecV1::ReversedMembersV1 as u8 => {} + _ => return Err(FamilyArtifactLoadErrorV1::UnsupportedCodec), + } + let expected_len = member_count + .checked_mul(3) + .ok_or(FamilyArtifactLoadErrorV1::ResourceExhausted)?; + if payload_len != expected_len { + return Err(FamilyArtifactLoadErrorV1::InvalidCodecPayload); + } + Ok(()) +} + +#[cfg(test)] +fn decode_fixture_payload( + codec: u8, + member_count: u64, + payload: &[u8], +) -> Result, FamilyArtifactLoadErrorV1> { + let codec = match codec { + value if value == FixtureFamilyArtifactCodecV1::CanonicalMembersV1 as u8 => { + FixtureFamilyArtifactCodecV1::CanonicalMembersV1 + } + value if value == FixtureFamilyArtifactCodecV1::ReversedMembersV1 as u8 => { + FixtureFamilyArtifactCodecV1::ReversedMembersV1 + } + _ => return Err(FamilyArtifactLoadErrorV1::UnsupportedCodec), + }; + FAMILY_ARTIFACT_DECODER_CALLS.with(|calls| calls.set(calls.get() + 1)); + let count = + usize::try_from(member_count).map_err(|_| FamilyArtifactLoadErrorV1::ResourceExhausted)?; + let expected_len = count + .checked_mul(3) + .ok_or(FamilyArtifactLoadErrorV1::ResourceExhausted)?; + if payload.len() != expected_len { + return Err(FamilyArtifactLoadErrorV1::InvalidCodecPayload); + } + let mut members = Vec::new(); + members + .try_reserve_exact(count) + .map_err(|_| FamilyArtifactLoadErrorV1::ResourceExhausted)?; + members.extend( + payload.chunks_exact(3).map(|bytes| { + ColorSignal::from_srgb8(crate::Srgb8::new([bytes[0], bytes[1], bytes[2]])) + }), + ); + let canonical = members + .windows(2) + .all(|pair| signal_key(pair[0]) < signal_key(pair[1])); + match codec { + FixtureFamilyArtifactCodecV1::CanonicalMembersV1 if !canonical => { + return Err(FamilyArtifactLoadErrorV1::InvalidCodecPayload); + } + FixtureFamilyArtifactCodecV1::CanonicalMembersV1 => {} + FixtureFamilyArtifactCodecV1::ReversedMembersV1 => { + let reversed = members + .windows(2) + .all(|pair| signal_key(pair[0]) > signal_key(pair[1])); + if !reversed { + return Err(FamilyArtifactLoadErrorV1::InvalidCodecPayload); + } + members.reverse(); + } + } + Ok(members.into_boxed_slice()) +} + +#[cfg(test)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum FamilyArtifactBuildErrorV1 { + ResourceExhausted, + NonCanonicalFixture, +} + +#[cfg(test)] +pub(crate) fn encode_fixture_family_artifact_v2( + definition: FamilyDefinitionDigestV2, + members: &[ColorSignal], + codec: FixtureFamilyArtifactCodecV1, +) -> Result<(FamilyImageCertificateV2, EncodedFamilyArtifactV2), FamilyArtifactBuildErrorV1> { + let mut canonical = Vec::new(); + canonical + .try_reserve_exact(members.len()) + .map_err(|_| FamilyArtifactBuildErrorV1::ResourceExhausted)?; + canonical.extend_from_slice(members); + canonical.sort_unstable_by_key(|member| signal_key(*member)); + canonical.dedup_by_key(|member| signal_key(*member)); + let member_count = canonical.len() as u64; + let image_digest = + canonical_family_image_digest_v2(OutputProfileId::Iec61966Srgb8D65V1, &canonical).map_err( + |CanonicalFamilyImageErrorV2::NonCanonicalAdmittedImage| { + FamilyArtifactBuildErrorV1::NonCanonicalFixture + }, + )?; + let semantic_release = semantic_family_release_id_v2(definition, image_digest, member_count); + let proof_artifact = + fixture_proof_artifact_id(definition, image_digest, semantic_release, member_count); + let verifier_identity = fixture_verifier_identity(); + let encoded_order: Box> = match codec { + FixtureFamilyArtifactCodecV1::CanonicalMembersV1 => Box::new(canonical.iter().copied()), + FixtureFamilyArtifactCodecV1::ReversedMembersV1 => { + Box::new(canonical.iter().rev().copied()) + } + }; + let payload_capacity = canonical + .len() + .checked_mul(3) + .ok_or(FamilyArtifactBuildErrorV1::ResourceExhausted)?; + let mut payload = Vec::new(); + payload + .try_reserve_exact(payload_capacity) + .map_err(|_| FamilyArtifactBuildErrorV1::ResourceExhausted)?; + for member in encoded_order { + payload.extend_from_slice(&member.srgb8().bytes()); + } + let payload_len = payload.len() as u64; + let mut certificate = FamilyImageCertificateV2 { + envelope_release: ENVELOPE_RELEASE_V2, + codec_release: codec as u8, + signal_domain: SIGNAL_DOMAIN_SRGB8_D65_V1, + signal_ordinal: SIGNAL_ORDINAL_RGB_BIG_ENDIAN_V1, + proof_release: PROOF_RELEASE_FIXTURE_EXACT_IMAGE_V1, + verifier_release: VERIFIER_RELEASE_FIXTURE_REPLAY_V1, + proof_artifact, + verifier_identity, + definition_digest: definition, + image_digest, + semantic_release, + payload_digest: payload_digest(&payload), + member_count, + payload_len, + artifact_receipt: FamilyArtifactReceiptIdV2([0; 32]), + }; + certificate.artifact_receipt = artifact_receipt(certificate); + let total_len = HEADER_LEN_V2 + .checked_add(payload.len()) + .ok_or(FamilyArtifactBuildErrorV1::ResourceExhausted)?; + let mut encoded = Vec::new(); + encoded + .try_reserve_exact(total_len) + .map_err(|_| FamilyArtifactBuildErrorV1::ResourceExhausted)?; + encoded.extend_from_slice(MAGIC_V2); + encode_certificate(&mut encoded, certificate); + encoded.extend_from_slice(&payload); + debug_assert_eq!(encoded.len(), total_len); + Ok(( + certificate, + EncodedFamilyArtifactV2(encoded.into_boxed_slice()), + )) +} + +fn signal_key(signal: ColorSignal) -> [u8; 3] { + signal.srgb8().bytes() +} + +fn payload_digest(payload: &[u8]) -> [u8; 32] { + #[cfg(test)] + FAMILY_ARTIFACT_PAYLOAD_DIGEST_CALLS.with(|calls| calls.set(calls.get() + 1)); + let mut hasher = Hasher::new(); + hasher.update(PAYLOAD_DIGEST_DOMAIN_V2); + hasher.update(&(payload.len() as u64).to_be_bytes()); + hasher.update(payload); + *hasher.finalize().as_bytes() +} + +#[cfg(test)] +fn fixture_proof_artifact_id( + definition: FamilyDefinitionDigestV2, + image: CanonicalFamilyImageDigestV2, + semantic: SemanticFamilyReleaseIdV2, + member_count: u64, +) -> FamilyProofArtifactIdV2 { + let mut hasher = Hasher::new(); + hasher.update(FIXTURE_PROOF_ARTIFACT_DOMAIN_V2); + hasher.update(definition.as_bytes()); + hasher.update(image.as_bytes()); + hasher.update(semantic.as_bytes()); + hasher.update(&member_count.to_be_bytes()); + FamilyProofArtifactIdV2(*hasher.finalize().as_bytes()) +} + +#[cfg(test)] +fn fixture_verifier_identity() -> FamilyVerifierIdentityV2 { + let mut hasher = Hasher::new(); + hasher.update(FIXTURE_VERIFIER_IDENTITY_DOMAIN_V2); + hasher.update(&[ + PROOF_RELEASE_FIXTURE_EXACT_IMAGE_V1, + VERIFIER_RELEASE_FIXTURE_REPLAY_V1, + ]); + FamilyVerifierIdentityV2(*hasher.finalize().as_bytes()) +} + +fn artifact_receipt(certificate: FamilyImageCertificateV2) -> FamilyArtifactReceiptIdV2 { + let mut hasher = Hasher::new(); + hasher.update(RECEIPT_DOMAIN_V2); + update_receipt_preimage(&mut hasher, certificate); + FamilyArtifactReceiptIdV2(*hasher.finalize().as_bytes()) +} + +/// Receipt-slot исключён намеренно: иначе certificate должен содержать хеш +/// самого себя и admission станет циклическим, а не content-addressed. +fn update_receipt_preimage(hasher: &mut Hasher, certificate: FamilyImageCertificateV2) { + hasher.update(&[ + certificate.envelope_release, + certificate.codec_release, + certificate.signal_domain, + certificate.signal_ordinal, + certificate.proof_release, + certificate.verifier_release, + ]); + hasher.update(&certificate.proof_artifact.0); + hasher.update(&certificate.verifier_identity.0); + hasher.update(certificate.definition_digest.as_bytes()); + hasher.update(certificate.image_digest.as_bytes()); + hasher.update(certificate.semantic_release.as_bytes()); + hasher.update(&certificate.payload_digest); + hasher.update(&certificate.member_count.to_be_bytes()); + hasher.update(&certificate.payload_len.to_be_bytes()); +} + +fn encode_certificate(output: &mut Vec, certificate: FamilyImageCertificateV2) { + output.extend_from_slice(&[ + certificate.envelope_release, + certificate.codec_release, + certificate.signal_domain, + certificate.signal_ordinal, + certificate.proof_release, + certificate.verifier_release, + ]); + output.extend_from_slice(&certificate.proof_artifact.0); + output.extend_from_slice(&certificate.verifier_identity.0); + output.extend_from_slice(certificate.definition_digest.as_bytes()); + output.extend_from_slice(certificate.image_digest.as_bytes()); + output.extend_from_slice(certificate.semantic_release.as_bytes()); + output.extend_from_slice(&certificate.payload_digest); + output.extend_from_slice(&certificate.member_count.to_be_bytes()); + output.extend_from_slice(&certificate.payload_len.to_be_bytes()); + output.extend_from_slice(certificate.artifact_receipt.as_bytes()); +} + +fn decode_certificate(bytes: &[u8]) -> FamilyImageCertificateV2 { + debug_assert_eq!(bytes.len(), HEADER_LEN_V2 - MAGIC_V2.len()); + fn take_32(bytes: &[u8], cursor: &mut usize) -> [u8; 32] { + let start = *cursor; + *cursor += 32; + let mut value = [0; 32]; + value.copy_from_slice(&bytes[start..*cursor]); + value + } + let mut cursor = 0; + let envelope_release = bytes[cursor]; + cursor += 1; + let codec_release = bytes[cursor]; + cursor += 1; + let signal_domain = bytes[cursor]; + cursor += 1; + let signal_ordinal = bytes[cursor]; + cursor += 1; + let proof_release = bytes[cursor]; + cursor += 1; + let verifier_release = bytes[cursor]; + cursor += 1; + let proof_artifact = FamilyProofArtifactIdV2(take_32(bytes, &mut cursor)); + let verifier_identity = FamilyVerifierIdentityV2(take_32(bytes, &mut cursor)); + let definition_digest = FamilyDefinitionDigestV2::from_digest(take_32(bytes, &mut cursor)); + let image_digest = CanonicalFamilyImageDigestV2::from_digest(take_32(bytes, &mut cursor)); + let semantic_release = SemanticFamilyReleaseIdV2::from_digest(take_32(bytes, &mut cursor)); + let payload_digest = take_32(bytes, &mut cursor); + let member_count = u64::from_be_bytes(bytes[cursor..cursor + 8].try_into().unwrap()); + cursor += 8; + let payload_len = u64::from_be_bytes(bytes[cursor..cursor + 8].try_into().unwrap()); + cursor += 8; + let artifact_receipt = FamilyArtifactReceiptIdV2::from_digest(take_32(bytes, &mut cursor)); + debug_assert_eq!(cursor, bytes.len()); + FamilyImageCertificateV2 { + envelope_release, + codec_release, + signal_domain, + signal_ordinal, + proof_release, + verifier_release, + proof_artifact, + verifier_identity, + definition_digest, + image_digest, + semantic_release, + payload_digest, + member_count, + payload_len, + artifact_receipt, + } +} + +/// Loaded semantic artifacts ещё не сопоставлены ordinal-слотам Program. +/// +/// Transport не повторяет client-owned `FamilyId → semantic`: один artifact +/// обслуживает все opaque aliases одного semantic release. +#[derive(Debug)] +pub(crate) struct FamilyArtifactBundleV2 { + artifacts: Box<[AdmittedFamilyArtifactV2]>, +} + +impl FamilyArtifactBundleV2 { + pub(crate) fn empty() -> Self { + Self { + artifacts: Box::new([]), + } + } + + pub(crate) fn from_artifacts(artifacts: Vec) -> Self { + Self { + artifacts: artifacts.into_boxed_slice(), + } + } + + /// Возвращает уже допущенный semantic pool для add/remove/replace без + /// повторного payload decode. + pub(crate) fn into_artifacts(self) -> Vec { + self.artifacts.into_vec() + } + + pub(crate) fn bind( + self, + declarations: &[FamilyDeclarationV2], + ) -> Result { + let mut artifacts = self.artifacts.into_vec(); + artifacts.sort_unstable_by_key(AdmittedFamilyArtifactV2::semantic_release); + if let Some(semantic) = artifacts + .windows(2) + .find(|pair| pair[0].semantic_release() == pair[1].semantic_release()) + .map(|pair| pair[0].semantic_release()) + { + return Err(bind_failure( + FamilyArtifactBindErrorV2::Contract(FamilyArtifactContractErrorV2::Duplicate { + semantic, + }), + artifacts, + )); + } + let mut slots = Vec::new(); + if slots.try_reserve_exact(declarations.len()).is_err() { + return Err(bind_failure( + FamilyArtifactBindErrorV2::ResourceExhausted, + artifacts, + )); + } + let mut missing: Option = None; + for declaration in declarations.iter().copied() { + let semantic = declaration.semantic(); + match artifacts + .binary_search_by_key(&semantic, AdmittedFamilyArtifactV2::semantic_release) + { + Ok(index) => slots.push(index), + Err(_) => { + missing = Some(missing.map_or(semantic, |current| current.min(semantic))); + } + } + } + if let Some(semantic) = missing { + return Err(bind_failure( + FamilyArtifactBindErrorV2::Contract(FamilyArtifactContractErrorV2::Missing { + semantic, + }), + artifacts, + )); + } + + let mut used = Vec::new(); + if used.try_reserve_exact(artifacts.len()).is_err() { + return Err(bind_failure( + FamilyArtifactBindErrorV2::ResourceExhausted, + artifacts, + )); + } + used.resize(artifacts.len(), false); + for artifact_index in slots.iter().copied() { + used[artifact_index] = true; + } + if let Some((artifact_index, _)) = used.iter().enumerate().find(|(_, used)| !**used) { + return Err(bind_failure( + FamilyArtifactBindErrorV2::Contract(FamilyArtifactContractErrorV2::Extra { + semantic: artifacts[artifact_index].semantic_release(), + }), + artifacts, + )); + } + Ok(BoundFamilyArtifactBundleV2 { + artifacts: artifacts.into_boxed_slice(), + family_artifact_indices: slots.into_boxed_slice(), + }) + } +} + +fn bind_failure( + cause: FamilyArtifactBindErrorV2, + artifacts: Vec, +) -> FamilyArtifactBindFailureV2 { + FamilyArtifactBindFailureV2 { + cause, + bundle: FamilyArtifactBundleV2::from_artifacts(artifacts), + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum FamilyArtifactContractErrorV2 { + Missing { semantic: SemanticFamilyReleaseIdV2 }, + Extra { semantic: SemanticFamilyReleaseIdV2 }, + Duplicate { semantic: SemanticFamilyReleaseIdV2 }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum FamilyArtifactBindErrorV2 { + Contract(FamilyArtifactContractErrorV2), + ResourceExhausted, +} + +/// Failed binding returns the same loaded storage without reload or decode. +pub(crate) struct FamilyArtifactBindFailureV2 { + cause: FamilyArtifactBindErrorV2, + bundle: FamilyArtifactBundleV2, +} + +impl fmt::Debug for FamilyArtifactBindFailureV2 { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("FamilyArtifactBindFailureV2") + .field("cause", &self.cause) + .finish_non_exhaustive() + } +} + +impl FamilyArtifactBindFailureV2 { + pub(crate) const fn cause(&self) -> FamilyArtifactBindErrorV2 { + self.cause + } + + pub(crate) fn into_parts(self) -> (FamilyArtifactBindErrorV2, FamilyArtifactBundleV2) { + (self.cause, self.bundle) + } +} + +/// Canonical family-index aligned executable storage of one Session generation. +#[derive(Debug)] +pub(crate) struct BoundFamilyArtifactBundleV2 { + artifacts: Box<[AdmittedFamilyArtifactV2]>, + family_artifact_indices: Box<[usize]>, +} + +impl BoundFamilyArtifactBundleV2 { + pub(crate) fn artifact(&self, family_index: usize) -> Option<&AdmittedFamilyArtifactV2> { + let artifact_index = *self.family_artifact_indices.get(family_index)?; + self.artifacts.get(artifact_index) + } + + pub(crate) fn execution_bindings(&self) -> FamilyExecutionBindingsV2<'_> { + FamilyExecutionBindingsV2 { + artifacts: self.artifacts.iter(), + } + } + + pub(crate) fn into_unbound(self) -> FamilyArtifactBundleV2 { + FamilyArtifactBundleV2 { + artifacts: self.artifacts, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct FamilyExecutionBindingV2 { + semantic: SemanticFamilyReleaseIdV2, + receipt: FamilyArtifactReceiptIdV2, +} + +impl FamilyExecutionBindingV2 { + pub(crate) const fn semantic(self) -> SemanticFamilyReleaseIdV2 { + self.semantic + } + + pub(crate) const fn receipt(self) -> FamilyArtifactReceiptIdV2 { + self.receipt + } +} + +pub(crate) struct FamilyExecutionBindingsV2<'a> { + artifacts: core::slice::Iter<'a, AdmittedFamilyArtifactV2>, +} + +impl Iterator for FamilyExecutionBindingsV2<'_> { + type Item = FamilyExecutionBindingV2; + + fn next(&mut self) -> Option { + let artifact = self.artifacts.next()?; + Some(FamilyExecutionBindingV2 { + semantic: artifact.semantic_release(), + receipt: artifact.artifact_receipt(), + }) + } + + fn size_hint(&self) -> (usize, Option) { + self.artifacts.size_hint() + } +} + +impl ExactSizeIterator for FamilyExecutionBindingsV2<'_> {} +impl core::iter::FusedIterator for FamilyExecutionBindingsV2<'_> {} diff --git a/crates/labcolors-core/src/family_artifact_tests.rs b/crates/labcolors-core/src/family_artifact_tests.rs new file mode 100644 index 00000000..c33986cc --- /dev/null +++ b/crates/labcolors-core/src/family_artifact_tests.rs @@ -0,0 +1,713 @@ +//! Hostile-контракт transport artifact семейства до первого production codec. + +use proptest::prelude::*; + +use crate::Srgb8; +use crate::family::{ + CanonicalFamilyImageErrorV2, FamilyDeclarationV2, FamilyDefinitionDigestV2, FamilyId, + canonical_family_image_digest_v2, +}; +use crate::family_artifact::{ + AdmittedFamilyArtifactV2, EncodedFamilyArtifactV2, FAMILY_ARTIFACT_DECODER_CALLS, + FAMILY_ARTIFACT_PAYLOAD_DIGEST_CALLS, FamilyArtifactBindErrorV2, FamilyArtifactBundleV2, + FamilyArtifactContractErrorV2, FamilyArtifactLoadErrorV1, FamilyArtifactLoaderV1, + FamilyImageCertificateV2, FixtureEnvelopeFieldV1, FixtureFamilyArtifactCodecV1, + encode_fixture_family_artifact_v2, +}; +use crate::lcs_occurrence::ColorSignal; +use crate::lcs_occurrence::OutputProfileId; + +fn signals(values: &[[u8; 3]]) -> Vec { + values + .iter() + .copied() + .map(Srgb8::new) + .map(ColorSignal::from_srgb8) + .collect() +} + +fn definition() -> FamilyDefinitionDigestV2 { + FamilyDefinitionDigestV2::from_fixture_bytes_v2(b"family-artifact-tests/blue-axis") +} + +fn load_fixture( + expected: FamilyImageCertificateV2, + encoded: EncodedFamilyArtifactV2, +) -> Result { + FamilyArtifactLoaderV1::load_fixture(expected, encoded).map_err(|failure| failure.cause()) +} + +#[test] +fn owned_transport_round_trips_without_copy_or_private_constructor() { + let bytes = vec![1, 2, 3, 4].into_boxed_slice(); + let pointer = bytes.as_ptr(); + let encoded = EncodedFamilyArtifactV2::from_owned_bytes(bytes); + let returned = encoded.into_bytes(); + + assert_eq!(returned.as_ptr(), pointer); + assert_eq!(&*returned, &[1, 2, 3, 4]); +} + +#[test] +fn canonical_image_digest_rejects_permutations_and_duplicates() { + let first = ColorSignal::from_srgb8(Srgb8::new([0, 0, 1])); + let second = ColorSignal::from_srgb8(Srgb8::new([0, 0, 2])); + + assert_eq!( + canonical_family_image_digest_v2(OutputProfileId::Iec61966Srgb8D65V1, &[second, first],), + Err(CanonicalFamilyImageErrorV2::NonCanonicalAdmittedImage), + ); + assert_eq!( + canonical_family_image_digest_v2(OutputProfileId::Iec61966Srgb8D65V1, &[first, first],), + Err(CanonicalFamilyImageErrorV2::NonCanonicalAdmittedImage), + ); +} + +#[test] +fn empty_exact_set_has_a_domain_bound_semantic_release_and_total_lookup() { + let members = Vec::new(); + let image = + canonical_family_image_digest_v2(OutputProfileId::Iec61966Srgb8D65V1, &members).unwrap(); + let (certificate, encoded) = encode_fixture_family_artifact_v2( + definition(), + &members, + FixtureFamilyArtifactCodecV1::CanonicalMembersV1, + ) + .unwrap(); + + assert_eq!(certificate.image_digest(), image); + let artifact = load_fixture(certificate, encoded).unwrap(); + assert!(!artifact.contains(ColorSignal::from_srgb8(Srgb8::new([0; 3])))); +} + +#[test] +fn two_encodings_keep_semantic_release_and_change_artifact_receipt() { + let members = signals(&[[0, 0, 1], [0, 0, 2], [0, 0, 255]]); + let (canonical_certificate, canonical_bytes) = encode_fixture_family_artifact_v2( + definition(), + &members, + FixtureFamilyArtifactCodecV1::CanonicalMembersV1, + ) + .unwrap(); + let (reversed_certificate, reversed_bytes) = encode_fixture_family_artifact_v2( + definition(), + &members, + FixtureFamilyArtifactCodecV1::ReversedMembersV1, + ) + .unwrap(); + + assert_eq!( + canonical_certificate.semantic_release(), + reversed_certificate.semantic_release(), + ); + assert_eq!( + canonical_certificate.image_digest(), + reversed_certificate.image_digest(), + ); + assert_ne!( + canonical_certificate.artifact_receipt(), + reversed_certificate.artifact_receipt(), + ); + + let canonical = load_fixture(canonical_certificate, canonical_bytes).unwrap(); + let reversed = load_fixture(reversed_certificate, reversed_bytes).unwrap(); + for member in members { + assert!(canonical.contains(member)); + assert!(reversed.contains(member)); + } +} + +#[test] +fn semantic_and_receipt_codecs_have_independent_sha256_goldens() { + let members = signals(&[[0, 0, 1], [0, 0, 2], [0, 0, 255]]); + let (certificate, _) = encode_fixture_family_artifact_v2( + definition(), + &members, + FixtureFamilyArtifactCodecV1::CanonicalMembersV1, + ) + .unwrap(); + + // Эти значения вычисляются независимым Python hashlib oracle над явно + // специфицированными preimage; self-consistent Rust hash не считается proof. + assert_eq!( + certificate.semantic_release().as_bytes(), + &[ + 0x36, 0xfa, 0xda, 0x04, 0xed, 0xeb, 0x34, 0x08, 0x6a, 0x56, 0x83, 0xc8, 0xdb, 0xfe, + 0x4c, 0xe0, 0xb7, 0xba, 0xec, 0x8d, 0x99, 0x81, 0xd7, 0xc8, 0xba, 0xf8, 0x04, 0x16, + 0x5f, 0x9c, 0x5f, 0x49, + ], + ); + assert_eq!( + certificate.proof_artifact().as_bytes(), + &[ + 0x09, 0x6d, 0x54, 0x67, 0x6e, 0xc3, 0xfd, 0x1e, 0x5e, 0x53, 0x83, 0x73, 0x02, 0xb9, + 0xd7, 0x07, 0xcb, 0x99, 0xbf, 0x07, 0x40, 0xe5, 0xc2, 0x48, 0x3d, 0x9e, 0x7d, 0xa0, + 0xa5, 0xb1, 0xbc, 0xb4, + ], + ); + assert_eq!( + certificate.verifier_identity().as_bytes(), + &[ + 0xb1, 0x0b, 0xc6, 0xb7, 0x97, 0xc6, 0xc2, 0x0c, 0xcb, 0xe3, 0xed, 0x99, 0x79, 0xcb, + 0xd6, 0xa2, 0xdd, 0x98, 0xd1, 0xe4, 0xa2, 0xb3, 0x99, 0x9a, 0x07, 0x1b, 0x2f, 0x54, + 0x22, 0xbe, 0xed, 0x87, + ], + ); + assert_eq!( + certificate.artifact_receipt().as_bytes(), + &[ + 0x4c, 0xef, 0xb1, 0xc4, 0xd6, 0xd0, 0x47, 0x6a, 0xb1, 0xc3, 0x89, 0x2e, 0x30, 0xd1, + 0x8f, 0xc0, 0x9e, 0x05, 0x25, 0xc7, 0x71, 0x9b, 0x19, 0xdd, 0x64, 0x1e, 0xe7, 0xdb, + 0xfa, 0xed, 0xc3, 0x45, + ], + ); +} + +#[test] +fn payload_corruption_is_rejected_before_decoder_dispatch() { + let members = signals(&[[0, 0, 1], [0, 0, 2]]); + let (certificate, mut encoded) = encode_fixture_family_artifact_v2( + definition(), + &members, + FixtureFamilyArtifactCodecV1::CanonicalMembersV1, + ) + .unwrap(); + encoded.flip_first_payload_bit_for_test(); + FAMILY_ARTIFACT_DECODER_CALLS.with(|calls| calls.set(0)); + + assert_eq!( + load_fixture(certificate, encoded).unwrap_err(), + FamilyArtifactLoadErrorV1::PayloadDigestMismatch, + ); + assert_eq!(FAMILY_ARTIFACT_DECODER_CALLS.with(core::cell::Cell::get), 0); +} + +#[test] +fn envelope_discriminants_are_rejected_before_certificate_or_decoder_admission() { + let members = signals(&[[0, 0, 1], [0, 0, 2]]); + let (certificate, encoded) = encode_fixture_family_artifact_v2( + definition(), + &members, + FixtureFamilyArtifactCodecV1::CanonicalMembersV1, + ) + .unwrap(); + let cases = [ + ( + encoded.clone().truncate_inside_header_for_test(), + FamilyArtifactLoadErrorV1::HeaderTooShort, + ), + ( + encoded.clone().corrupt_magic_for_test(), + FamilyArtifactLoadErrorV1::InvalidMagic, + ), + ( + encoded + .clone() + .corrupt_envelope_field_for_test(FixtureEnvelopeFieldV1::EnvelopeRelease), + FamilyArtifactLoadErrorV1::UnsupportedEnvelope, + ), + ( + encoded + .clone() + .corrupt_envelope_field_for_test(FixtureEnvelopeFieldV1::SignalDomain), + FamilyArtifactLoadErrorV1::UnsupportedSignalDomain, + ), + ( + encoded + .clone() + .corrupt_envelope_field_for_test(FixtureEnvelopeFieldV1::SignalOrdinal), + FamilyArtifactLoadErrorV1::UnsupportedSignalOrdinal, + ), + ( + encoded + .clone() + .corrupt_envelope_field_for_test(FixtureEnvelopeFieldV1::ProofRelease), + FamilyArtifactLoadErrorV1::UnsupportedProofRelease, + ), + ( + encoded.corrupt_envelope_field_for_test(FixtureEnvelopeFieldV1::VerifierRelease), + FamilyArtifactLoadErrorV1::UnsupportedVerifierRelease, + ), + ]; + + for (malformed, expected) in cases { + FAMILY_ARTIFACT_DECODER_CALLS.with(|calls| calls.set(0)); + assert_eq!(load_fixture(certificate, malformed).unwrap_err(), expected); + assert_eq!(FAMILY_ARTIFACT_DECODER_CALLS.with(core::cell::Cell::get), 0); + } +} + +#[test] +fn receipt_semantic_codec_and_image_checks_reach_their_own_branches() { + let members = signals(&[[0, 0, 1], [0, 0, 2]]); + let (certificate, encoded) = encode_fixture_family_artifact_v2( + definition(), + &members, + FixtureFamilyArtifactCodecV1::CanonicalMembersV1, + ) + .unwrap(); + + let invalid_receipt = certificate.receipt_mismatch_for_test(); + FAMILY_ARTIFACT_DECODER_CALLS.with(|calls| calls.set(0)); + assert_eq!( + load_fixture( + invalid_receipt, + encoded.clone().with_certificate_for_test(invalid_receipt), + ) + .unwrap_err(), + FamilyArtifactLoadErrorV1::ArtifactReceiptMismatch, + ); + assert_eq!(FAMILY_ARTIFACT_DECODER_CALLS.with(core::cell::Cell::get), 0); + + let invalid_semantic = certificate.semantic_mismatch_with_valid_receipt_for_test(); + FAMILY_ARTIFACT_DECODER_CALLS.with(|calls| calls.set(0)); + assert_eq!( + load_fixture( + invalid_semantic, + encoded.clone().with_certificate_for_test(invalid_semantic), + ) + .unwrap_err(), + FamilyArtifactLoadErrorV1::SemanticReleaseMismatch, + ); + assert_eq!(FAMILY_ARTIFACT_DECODER_CALLS.with(core::cell::Cell::get), 0); + + let unsupported_codec = certificate.codec_with_valid_receipt_for_test(0x7f); + FAMILY_ARTIFACT_DECODER_CALLS.with(|calls| calls.set(0)); + assert_eq!( + load_fixture( + unsupported_codec, + encoded.clone().with_certificate_for_test(unsupported_codec), + ) + .unwrap_err(), + FamilyArtifactLoadErrorV1::UnsupportedCodec, + ); + assert_eq!(FAMILY_ARTIFACT_DECODER_CALLS.with(core::cell::Cell::get), 0); + + let invalid_image = certificate.image_mismatch_with_coherent_certificate_for_test(); + FAMILY_ARTIFACT_DECODER_CALLS.with(|calls| calls.set(0)); + assert_eq!( + load_fixture( + invalid_image, + encoded.with_certificate_for_test(invalid_image), + ) + .unwrap_err(), + FamilyArtifactLoadErrorV1::ImageDigestMismatch, + ); + assert_eq!(FAMILY_ARTIFACT_DECODER_CALLS.with(core::cell::Cell::get), 1); + + let (reversed_certificate, reversed) = encode_fixture_family_artifact_v2( + definition(), + &members, + FixtureFamilyArtifactCodecV1::ReversedMembersV1, + ) + .unwrap(); + let wrong_decoder = reversed_certificate + .codec_with_valid_receipt_for_test(FixtureFamilyArtifactCodecV1::CanonicalMembersV1 as u8); + FAMILY_ARTIFACT_DECODER_CALLS.with(|calls| calls.set(0)); + assert_eq!( + load_fixture( + wrong_decoder, + reversed.with_certificate_for_test(wrong_decoder), + ) + .unwrap_err(), + FamilyArtifactLoadErrorV1::InvalidCodecPayload, + ); + assert_eq!(FAMILY_ARTIFACT_DECODER_CALLS.with(core::cell::Cell::get), 1); +} + +#[test] +fn impossible_srgb8_set_cardinality_is_rejected_before_decoder_dispatch() { + let members = signals(&[[0, 0, 1], [0, 0, 2]]); + let (certificate, encoded) = encode_fixture_family_artifact_v2( + definition(), + &members, + FixtureFamilyArtifactCodecV1::CanonicalMembersV1, + ) + .unwrap(); + + let impossible = certificate.member_count_with_coherent_certificate_for_test((1_u64 << 24) + 1); + FAMILY_ARTIFACT_DECODER_CALLS.with(|calls| calls.set(0)); + assert_eq!( + load_fixture(impossible, encoded.with_certificate_for_test(impossible),).unwrap_err(), + FamilyArtifactLoadErrorV1::InvalidMemberCount, + ); + assert_eq!(FAMILY_ARTIFACT_DECODER_CALLS.with(core::cell::Cell::get), 0); +} + +#[test] +fn full_srgb8_set_cardinality_reaches_codec_admission() { + let members = signals(&[[0, 0, 1], [0, 0, 2]]); + let (certificate, encoded) = encode_fixture_family_artifact_v2( + definition(), + &members, + FixtureFamilyArtifactCodecV1::CanonicalMembersV1, + ) + .unwrap(); + let full = certificate.member_count_with_coherent_certificate_for_test(1_u64 << 24); + let preflight_reached = core::cell::Cell::new(false); + let decoder_reached = core::cell::Cell::new(false); + + let failure = FamilyArtifactLoaderV1::load_with_codec_for_test( + full, + encoded.with_certificate_for_test(full), + |_codec, _count, _payload_len| { + preflight_reached.set(true); + Ok(()) + }, + |_codec, _count, _payload| { + decoder_reached.set(true); + Ok(Box::new([])) + }, + ) + .unwrap_err(); + + assert!(preflight_reached.get()); + assert!(decoder_reached.get()); + assert_eq!( + failure.cause(), + FamilyArtifactLoadErrorV1::DecodedMemberCountMismatch { + expected: 1_u64 << 24, + actual: 0, + }, + ); +} + +#[test] +fn central_loader_rejects_a_decoder_that_lies_about_member_count() { + let members = signals(&[[0, 0, 1], [0, 0, 2]]); + let (certificate, encoded) = encode_fixture_family_artifact_v2( + definition(), + &members, + FixtureFamilyArtifactCodecV1::CanonicalMembersV1, + ) + .unwrap(); + + let failure = FamilyArtifactLoaderV1::load_with_decoder_for_test( + certificate, + encoded, + |_codec, _declared_count, _payload| Ok(signals(&[[0, 0, 1]]).into_boxed_slice()), + ) + .unwrap_err(); + + assert_eq!( + failure.cause(), + FamilyArtifactLoadErrorV1::DecodedMemberCountMismatch { + expected: 2, + actual: 1, + }, + ); +} + +#[test] +fn truncation_and_extension_fail_exact_length_before_decoder_dispatch() { + let members = signals(&[[0, 0, 1], [0, 0, 2]]); + let (certificate, encoded) = encode_fixture_family_artifact_v2( + definition(), + &members, + FixtureFamilyArtifactCodecV1::CanonicalMembersV1, + ) + .unwrap(); + + for malformed in [ + encoded.clone().truncate_one_byte_for_test(), + encoded.extend_one_byte_for_test(), + ] { + FAMILY_ARTIFACT_DECODER_CALLS.with(|calls| calls.set(0)); + assert!(matches!( + load_fixture(certificate, malformed), + Err(FamilyArtifactLoadErrorV1::ExactLengthMismatch { .. }), + )); + assert_eq!(FAMILY_ARTIFACT_DECODER_CALLS.with(core::cell::Cell::get), 0); + } +} + +#[test] +fn a_structurally_valid_foreign_certificate_is_not_a_generic_digest_error() { + let members = signals(&[[0, 0, 1], [0, 0, 2]]); + let (expected, _) = encode_fixture_family_artifact_v2( + definition(), + &members, + FixtureFamilyArtifactCodecV1::CanonicalMembersV1, + ) + .unwrap(); + let (foreign, encoded) = encode_fixture_family_artifact_v2( + FamilyDefinitionDigestV2::from_fixture_bytes_v2(b"foreign-definition"), + &members, + FixtureFamilyArtifactCodecV1::CanonicalMembersV1, + ) + .unwrap(); + assert_ne!(expected, foreign); + FAMILY_ARTIFACT_DECODER_CALLS.with(|calls| calls.set(0)); + + assert_eq!( + load_fixture(expected, encoded).unwrap_err(), + FamilyArtifactLoadErrorV1::ForeignCertificate, + ); + assert_eq!(FAMILY_ARTIFACT_DECODER_CALLS.with(core::cell::Cell::get), 0); +} + +#[test] +fn admitted_storage_does_not_borrow_transport_bytes() { + let member = ColorSignal::from_srgb8(Srgb8::new([0, 0, 255])); + let (certificate, encoded) = encode_fixture_family_artifact_v2( + definition(), + &[member], + FixtureFamilyArtifactCodecV1::CanonicalMembersV1, + ) + .unwrap(); + + let admitted = load_fixture(certificate, encoded).unwrap(); + + assert!(admitted.contains(member)); +} + +#[test] +fn every_certificate_identity_field_is_bound_before_decode() { + let members = signals(&[[0, 0, 1], [0, 0, 2]]); + let (certificate, encoded) = encode_fixture_family_artifact_v2( + definition(), + &members, + FixtureFamilyArtifactCodecV1::CanonicalMembersV1, + ) + .unwrap(); + + for mutant in certificate.identity_mutants_for_test() { + FAMILY_ARTIFACT_DECODER_CALLS.with(|calls| calls.set(0)); + assert_eq!( + load_fixture(mutant, encoded.clone()).unwrap_err(), + FamilyArtifactLoadErrorV1::ForeignCertificate, + ); + assert_eq!(FAMILY_ARTIFACT_DECODER_CALLS.with(core::cell::Cell::get), 0); + } +} + +fn loaded_artifact(definition_suffix: &[u8]) -> AdmittedFamilyArtifactV2 { + let definition = FamilyDefinitionDigestV2::from_fixture_bytes_v2(definition_suffix); + let (certificate, encoded) = encode_fixture_family_artifact_v2( + definition, + &signals(&[[0, 0, 1], [0, 0, 2]]), + FixtureFamilyArtifactCodecV1::CanonicalMembersV1, + ) + .unwrap(); + load_fixture(certificate, encoded).unwrap() +} + +#[test] +fn exact_pool_binding_is_repairable_without_redecoding_retained_artifacts() { + FAMILY_ARTIFACT_DECODER_CALLS.with(|calls| calls.set(0)); + let first = FamilyId::new(10); + let second = FamilyId::new(20); + let first_artifact = loaded_artifact(b"first"); + let first_semantic = first_artifact.semantic_release(); + let second_artifact = loaded_artifact(b"second"); + let second_semantic = second_artifact.semantic_release(); + let declarations = [ + FamilyDeclarationV2::new(first, first_semantic), + FamilyDeclarationV2::new(second, second_semantic), + ]; + + let failure = FamilyArtifactBundleV2::from_artifacts(vec![first_artifact]) + .bind(&declarations) + .unwrap_err(); + assert_eq!( + failure.cause(), + FamilyArtifactBindErrorV2::Contract(FamilyArtifactContractErrorV2::Missing { + semantic: second_semantic, + }), + ); + assert_eq!( + format!("{failure:?}"), + format!( + "FamilyArtifactBindFailureV2 {{ cause: {:?}, .. }}", + failure.cause(), + ), + "owning bind failures must expose only their typed cause", + ); + let (_, returned) = failure.into_parts(); + let mut retained = returned.into_artifacts(); + retained.push(second_artifact); + let bound = FamilyArtifactBundleV2::from_artifacts(retained) + .bind(&declarations) + .unwrap(); + let mut execution = bound.execution_bindings(); + assert_eq!(execution.len(), 2); + let first = execution.next().unwrap(); + assert_eq!(execution.len(), 1); + let second = execution.next().unwrap(); + assert_ne!(first.semantic(), second.semantic()); + assert_eq!(execution.len(), 0); + assert_eq!(execution.next(), None); + assert_eq!(execution.next(), None, "the iterator must stay fused"); + assert_eq!( + FAMILY_ARTIFACT_DECODER_CALLS.with(core::cell::Cell::get), + 2, + "bundle repair must reuse both already decoded artifacts", + ); +} + +#[test] +fn exact_pool_binding_rejects_extra_duplicate_and_wrong_semantic() { + let first = FamilyId::new(10); + let second = FamilyId::new(20); + let first_artifact = loaded_artifact(b"first"); + let first_semantic = first_artifact.semantic_release(); + let second_artifact = loaded_artifact(b"second"); + let second_semantic = second_artifact.semantic_release(); + let declarations = [ + FamilyDeclarationV2::new(first, first_semantic), + FamilyDeclarationV2::new(second, second_semantic), + ]; + + let extra_artifact = loaded_artifact(b"extra"); + let extra_semantic = extra_artifact.semantic_release(); + let failure = FamilyArtifactBundleV2::from_artifacts(vec![ + first_artifact, + second_artifact, + extra_artifact, + ]) + .bind(&declarations) + .unwrap_err(); + assert_eq!( + failure.cause(), + FamilyArtifactBindErrorV2::Contract(FamilyArtifactContractErrorV2::Extra { + semantic: extra_semantic, + }), + ); + + let duplicate_semantic = first_semantic; + let failure = FamilyArtifactBundleV2::from_artifacts(vec![ + loaded_artifact(b"first"), + loaded_artifact(b"first"), + loaded_artifact(b"second"), + ]) + .bind(&declarations) + .unwrap_err(); + assert_eq!( + failure.cause(), + FamilyArtifactBindErrorV2::Contract(FamilyArtifactContractErrorV2::Duplicate { + semantic: duplicate_semantic, + }), + ); + + let failure = FamilyArtifactBundleV2::from_artifacts(vec![ + loaded_artifact(b"wrong"), + loaded_artifact(b"second"), + ]) + .bind(&declarations) + .unwrap_err(); + assert_eq!( + failure.cause(), + FamilyArtifactBindErrorV2::Contract(FamilyArtifactContractErrorV2::Missing { + semantic: first_semantic, + }), + ); +} + +#[test] +fn missing_error_order_is_invariant_under_opaque_id_rename_and_permutation() { + let first_semantic = loaded_artifact(b"missing-first").semantic_release(); + let second_semantic = loaded_artifact(b"missing-second").semantic_release(); + let expected = first_semantic.min(second_semantic); + let first = [ + FamilyDeclarationV2::new(FamilyId::new(1), first_semantic), + FamilyDeclarationV2::new(FamilyId::new(2), second_semantic), + ]; + let renamed_and_permuted = [ + FamilyDeclarationV2::new(FamilyId::new(900), second_semantic), + FamilyDeclarationV2::new(FamilyId::new(3), first_semantic), + ]; + + for declarations in [&first[..], &renamed_and_permuted[..]] { + let failure = FamilyArtifactBundleV2::empty() + .bind(declarations) + .unwrap_err(); + assert_eq!( + failure.cause(), + FamilyArtifactBindErrorV2::Contract(FamilyArtifactContractErrorV2::Missing { + semantic: expected, + }), + ); + } +} + +#[test] +fn one_semantic_artifact_serves_two_opaque_family_aliases() { + let first = FamilyId::new(10); + let alias = FamilyId::new(20); + let artifact = loaded_artifact(b"shared"); + let semantic = artifact.semantic_release(); + let receipt = artifact.artifact_receipt(); + let declarations = [ + FamilyDeclarationV2::new(first, semantic), + FamilyDeclarationV2::new(alias, semantic), + ]; + let bound = FamilyArtifactBundleV2::from_artifacts(vec![artifact]) + .bind(&declarations) + .unwrap(); + + assert!(core::ptr::eq( + bound.artifact(0).unwrap(), + bound.artifact(1).unwrap(), + )); + let execution = bound.execution_bindings().collect::>(); + assert_eq!(execution.len(), 1); + assert!( + execution + .iter() + .all(|binding| binding.semantic() == semantic && binding.receipt() == receipt) + ); + assert_ne!(semantic.as_bytes(), receipt.as_bytes()); +} + +#[test] +fn production_loader_rejects_unreleased_codec_and_returns_exact_transport() { + let members = signals(&[[0, 0, 1], [0, 0, 2]]); + let (certificate, encoded) = encode_fixture_family_artifact_v2( + definition(), + &members, + FixtureFamilyArtifactCodecV1::CanonicalMembersV1, + ) + .unwrap(); + let exact_transport = encoded.clone(); + FAMILY_ARTIFACT_PAYLOAD_DIGEST_CALLS.with(|calls| calls.set(0)); + + let failure = FamilyArtifactLoaderV1::load(certificate, encoded).unwrap_err(); + + assert_eq!(failure.cause(), FamilyArtifactLoadErrorV1::UnsupportedCodec,); + assert_eq!( + format!("{failure:?}"), + "FamilyArtifactLoadFailureV1 { cause: UnsupportedCodec, .. }", + "owning failures must not dump transport bytes", + ); + let (cause, returned) = failure.into_parts(); + assert_eq!(cause, FamilyArtifactLoadErrorV1::UnsupportedCodec); + assert_eq!(returned, exact_transport); + assert_eq!( + FAMILY_ARTIFACT_PAYLOAD_DIGEST_CALLS.with(core::cell::Cell::get), + 0, + "unsupported codecs must fail before O(payload) hashing", + ); +} + +proptest! { + #[test] + fn arbitrary_owned_transport_never_panics( + header in proptest::collection::vec(any::(), 0..=256), + payload in proptest::collection::vec(any::(), 0..=256), + ) { + let members = signals(&[[0, 0, 1]]); + let (certificate, _) = encode_fixture_family_artifact_v2( + definition(), + &members, + FixtureFamilyArtifactCodecV1::CanonicalMembersV1, + ) + .unwrap(); + let mut bytes = header; + bytes.extend_from_slice(&payload); + + let _ = FamilyArtifactLoaderV1::load( + certificate, + EncodedFamilyArtifactV2::from_raw_bytes_for_test(bytes), + ); + } +} diff --git a/crates/labcolors-core/src/family_tests.rs b/crates/labcolors-core/src/family_tests.rs deleted file mode 100644 index c5f16a88..00000000 --- a/crates/labcolors-core/src/family_tests.rs +++ /dev/null @@ -1,527 +0,0 @@ -//! Контракт точного образа генератора 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/lib.rs b/crates/labcolors-core/src/lib.rs index 90c7b7a5..5eaf6b09 100644 --- a/crates/labcolors-core/src/lib.rs +++ b/crates/labcolors-core/src/lib.rs @@ -10,6 +10,7 @@ pub mod wcag22_evidence; pub(crate) mod clean_set; pub(crate) mod composition; mod family; +mod family_artifact; pub(crate) mod spaces; pub use srgb8::Srgb8; @@ -171,7 +172,7 @@ pub(crate) mod joint; mod constraint_tests; #[cfg(test)] -mod family_tests; +mod family_artifact_tests; #[cfg(test)] mod clean_set_tests; diff --git a/crates/labcolors-core/src/program.rs b/crates/labcolors-core/src/program.rs index e7522146..16b46b63 100644 --- a/crates/labcolors-core/src/program.rs +++ b/crates/labcolors-core/src/program.rs @@ -32,7 +32,7 @@ /// Транзакционный point-output attachment и его линейный sink-контракт. pub(crate) mod attachment; -use core::iter::FusedIterator; +use core::{fmt, iter::FusedIterator}; use crate::Srgb8; use crate::appearance::{ @@ -46,11 +46,11 @@ use crate::constraints::{ ProgramVisiblePointPassEvidence, ProgramVisiblePointViolationEvidence, Wcag22Srgb8V1, }; use crate::family::{ - AdmittedFamilySetV1, FamilyContentIdentityV1 as CoreFamilyContentIdentityV1, - FamilyDeclarationV1, FamilyId, FamilyImageErrorV1, - FamilyMembershipMeasurementV1 as CoreFamilyMembershipMeasurementV1, - admit_declared_family_image_v1, + FamilyDeclarationV2, FamilyId, + FamilyMembershipMeasurementV2 as CoreFamilyMembershipMeasurementV2, + SemanticFamilyReleaseIdV2 as CoreSemanticFamilyReleaseIdV2, }; +use crate::family_artifact::{FamilyArtifactBundleV2, FamilyArtifactContractErrorV2}; use crate::joint::FiniteJointOrderErrorV1; use crate::lcs_occurrence::{ AdaptingLuminanceCdM2, AppearanceContextDomainErrorV1, @@ -226,52 +226,22 @@ projected_id!( ObservationStreamId ); -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) struct FamilySetV1(AdmittedFamilySetV1); - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum FamilySetAdmissionErrorV1 { - Empty, - ResourceExhausted, - InternalInvariant, -} +/// Representation-independent semantic release одной family declaration. +#[repr(transparent)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct FamilySemanticReleaseV2(CoreSemanticFamilyReleaseIdV2); -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 - } - }) +impl FamilySemanticReleaseV2 { + pub(crate) const fn from_core(value: CoreSemanticFamilyReleaseIdV2) -> Self { + Self(value) } - /// Возвращает адрес неизменяемого сертификата допущенного family-set. - pub(crate) const fn content_identity(&self) -> FamilyContentIdentityV1 { - FamilyContentIdentityV1::from_core(self.0.certificate().family_content_identity()) + pub(crate) const fn into_core(self) -> CoreSemanticFamilyReleaseIdV2 { + self.0 } - #[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)); + pub(crate) const fn as_bytes(&self) -> &[u8; 32] { + self.0.as_bytes() } } projected_id!( @@ -564,8 +534,6 @@ pub(crate) enum CompileErrorKindV1 { DuplicateTarget, /// Повторно объявлен family-set. DuplicateFamily, - /// Сертификат family-set не прошёл полный replay. - InvalidFamilyImage, /// Family-set объявлен, но не связан ни с одним ограничением. UnusedFamily, /// В одной цели повторно объявлен ID кандидата. @@ -832,11 +800,6 @@ pub(crate) enum CompileErrorV1 { /// Повторный family ID. family: FamilyIdV1, }, - /// Допущенный образ family не прошёл повторную верификацию. - InvalidFamilyImage { - /// Ошибочный family ID. - family: FamilyIdV1, - }, /// Объявленный family-set не используется ни одним constraint. UnusedFamily { /// Неиспользуемый family ID. @@ -1180,7 +1143,6 @@ impl CompileErrorV1 { 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, @@ -1275,9 +1237,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::DuplicateFamily { family } | Self::UnusedFamily { family } => { + Some(Handle::Family(*family)) + } Self::DuplicateOpacityInput { input } | Self::OpacityOutOfDomain { input } => { Some(Handle::OpacityInput(*input)) } @@ -1399,7 +1361,6 @@ impl CompileErrorV1 { Self::DuplicateSource { .. } | Self::DuplicateTarget { .. } | Self::DuplicateFamily { .. } - | Self::InvalidFamilyImage { .. } | Self::UnusedFamily { .. } | Self::DuplicateOpacityInput { .. } | Self::DuplicateSurfaceInputPort { .. } @@ -1480,9 +1441,15 @@ impl DraftV1 { } /// Объявляет одно точное допущенное множество без клиентской семантики. - pub(crate) fn push_family(&mut self, id: FamilyIdV1, set: FamilySetV1) -> &mut Self { - self.inner - .push_family(FamilyDeclarationV1::new(id.into_core(), set.0)); + pub(crate) fn push_family( + &mut self, + id: FamilyIdV1, + semantic: FamilySemanticReleaseV2, + ) -> &mut Self { + self.inner.push_family(FamilyDeclarationV2::new( + id.into_core(), + semantic.into_core(), + )); self } @@ -1926,6 +1893,18 @@ impl OwnerV1 { .map(|(slot, _paint)| OutputSlotIdV1::from_core(slot)) } + /// Unique semantic releases that the host must resolve through its trusted + /// artifact manifest before constructing a Session or Attachment. + pub(crate) fn required_family_releases( + &self, + ) -> impl ExactSizeIterator + '_ { + self.compiled + .required_family_releases() + .iter() + .copied() + .map(FamilySemanticReleaseV2::from_core) + } + /// Допускает update без изменения зафиксированных raw head и lifecycle. /// /// Несовпадение Owner проверяется до admission, аллокаций и вычисления. @@ -1955,6 +1934,55 @@ impl OwnerV1 { session, }) } + + pub(crate) fn instantiate_with_family_artifacts( + &self, + stream_id: u32, + family_artifacts: FamilyArtifactBundleV2, + ) -> Result { + let stream = ObservationStreamId::new(stream_id); + match self + .compiled + .instantiate_with_family_artifacts(stream, family_artifacts) + { + Ok(session) => Ok(SessionV1 { + scenario_order_scratch: Vec::new(), + session, + }), + Err(failure) => { + let (cause, family_artifacts) = failure.into_parts(); + Err(InstantiateFailureV2 { + cause: InstantiateErrorV1::from_core(cause), + family_artifacts, + }) + } + } + } +} + +/// Cold Session failure retains the same loaded family storage for retry. +pub(crate) struct InstantiateFailureV2 { + cause: InstantiateErrorV1, + family_artifacts: FamilyArtifactBundleV2, +} + +impl fmt::Debug for InstantiateFailureV2 { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("InstantiateFailureV2") + .field("cause", &self.cause) + .finish_non_exhaustive() + } +} + +impl InstantiateFailureV2 { + pub(crate) const fn cause(&self) -> InstantiateErrorV1 { + self.cause + } + + pub(crate) fn into_parts(self) -> (InstantiateErrorV1, FamilyArtifactBundleV2) { + (self.cause, self.family_artifacts) + } } /// Один заимствованный физический сценарий в скомпилированном schema order. @@ -2720,7 +2748,7 @@ impl IntrinsicUnaryEvidenceV1<'_> { family, measurement, } => IntrinsicUnaryMeasurementV1::FamilyMembership( - FamilyMembershipMeasurementV1::from_core(family, measurement), + FamilyMembershipMeasurementV2::from_core(family, measurement), ), } } @@ -2789,38 +2817,22 @@ 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 - } + FamilyMembership(FamilyMembershipMeasurementV2), } -/// Точный сигнал и сертификат family, проверенные одним вызовом. +/// Точный сигнал и semantic family release, проверенные одним вызовом. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) struct FamilyMembershipMeasurementV1 { +pub(crate) struct FamilyMembershipMeasurementV2 { family: FamilyIdV1, - content: FamilyContentIdentityV1, + semantic: FamilySemanticReleaseV2, signal: Srgb8, } -impl FamilyMembershipMeasurementV1 { - const fn from_core(family: FamilyId, value: CoreFamilyMembershipMeasurementV1) -> Self { +impl FamilyMembershipMeasurementV2 { + const fn from_core(family: FamilyId, value: CoreFamilyMembershipMeasurementV2) -> Self { Self { family: FamilyIdV1::from_core(family), - content: FamilyContentIdentityV1::from_core(value.family()), + semantic: FamilySemanticReleaseV2::from_core(value.semantic()), signal: value.signal().srgb8(), } } @@ -2830,9 +2842,9 @@ impl FamilyMembershipMeasurementV1 { self.family } - /// Возвращает адрес полного допущенного сертификата family. - pub(crate) const fn content(self) -> FamilyContentIdentityV1 { - self.content + /// Возвращает representation-independent semantic release family. + pub(crate) const fn semantic(self) -> FamilySemanticReleaseV2 { + self.semantic } /// Возвращает классифицированный точный исходный сигнал. @@ -3370,47 +3382,47 @@ impl<'a> Iterator for CertificatesV1<'a> { impl ExactSizeIterator for CertificatesV1<'_> {} impl FusedIterator for CertificatesV1<'_> {} -/// Закрытая классификация ошибки создания Session. +/// Точное несовпадение semantic artifact pool с требованиями Program. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum InstantiateErrorKindV1 { - /// Для создания Session недостаточно ресурсов. - ResourceExhausted, - /// Нарушен внутренний инвариант скомпилированной Program. - InternalInvariant, +pub(crate) enum FamilyArtifactErrorV2 { + Missing { semantic: FamilySemanticReleaseV2 }, + Extra { semantic: FamilySemanticReleaseV2 }, + Duplicate { semantic: FamilySemanticReleaseV2 }, } -/// Непрозрачная ошибка создания Session. +impl FamilyArtifactErrorV2 { + const fn from_core(error: FamilyArtifactContractErrorV2) -> Self { + match error { + FamilyArtifactContractErrorV2::Missing { semantic } => Self::Missing { + semantic: FamilySemanticReleaseV2::from_core(semantic), + }, + FamilyArtifactContractErrorV2::Extra { semantic } => Self::Extra { + semantic: FamilySemanticReleaseV2::from_core(semantic), + }, + FamilyArtifactContractErrorV2::Duplicate { semantic } => Self::Duplicate { + semantic: FamilySemanticReleaseV2::from_core(semantic), + }, + } + } +} + +/// Ошибочные состояния создания Session представлены закрытой суммой. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) struct InstantiateErrorV1 { - kind: InstantiateErrorKindV1, +pub(crate) enum InstantiateErrorV1 { + ResourceExhausted, + InternalInvariant, + FamilyArtifacts(FamilyArtifactErrorV2), } impl InstantiateErrorV1 { - const fn new(kind: InstantiateErrorKindV1) -> Self { - Self { kind } - } - fn from_core(error: ProgramSessionInstantiateError) -> Self { - let kind = match error { - ProgramSessionInstantiateError::ResourceExhausted => { - InstantiateErrorKindV1::ResourceExhausted - } - ProgramSessionInstantiateError::InternalInvariant => { - InstantiateErrorKindV1::InternalInvariant + match error { + ProgramSessionInstantiateError::ResourceExhausted => Self::ResourceExhausted, + ProgramSessionInstantiateError::InternalInvariant => Self::InternalInvariant, + ProgramSessionInstantiateError::FamilyArtifacts(cause) => { + Self::FamilyArtifacts(FamilyArtifactErrorV2::from_core(cause)) } - }; - Self::new(kind) - } - - /// Возвращает стабильный класс ошибки. - pub(crate) const fn kind(self) -> InstantiateErrorKindV1 { - self.kind - } -} - -impl From for InstantiateErrorV1 { - fn from(kind: InstantiateErrorKindV1) -> Self { - Self::new(kind) + } } } @@ -3754,9 +3766,6 @@ fn map_program_compile_error(error: ProgramCompileError) -> CompileErrorV1 { 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), }, diff --git a/crates/labcolors-core/src/program/attachment.rs b/crates/labcolors-core/src/program/attachment.rs index c71ef96b..6d018370 100644 --- a/crates/labcolors-core/src/program/attachment.rs +++ b/crates/labcolors-core/src/program/attachment.rs @@ -7,6 +7,7 @@ use core::{fmt, iter::FusedIterator, mem, num::NonZeroU64}; use crate::appearance::EncodedPointPaintV1; +use crate::family_artifact::{FamilyArtifactBundleV2, FamilyExecutionBindingsV2}; use crate::program_session::{ CompiledPointOutputPresentationV1, CoreProgramEvaluatorsV1, PointOutputPresentationBindErrorV1, ProgramOwnerLeaseV1, ProgramPaintOutputV1, @@ -427,65 +428,123 @@ pub(crate) enum AttachmentCreateErrorV1 { InternalInvariant, } -/// Точная причина cold attach failure; lease хранится один раз во внешнем -/// owning-контейнере и не дублируется по вариантам. -#[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) enum AttachmentCreateCauseV1 { - Contract(AttachmentCreateErrorV1), - SinkAdmission(SinkAdmissionError), +/// Contract failure ещё не построил Session и возвращает оба linear input-а. +pub(crate) struct UnpreparedAttachmentRetryV2 +where + L: UnboundPointSinkLeaseV1, +{ + sink: L, + family_artifacts: FamilyArtifactBundleV2, } -/// Cold failure сохраняет тот же unbound lease для исправления и retry. -pub(crate) struct AttachmentCreateFailureV1 +impl UnpreparedAttachmentRetryV2 +where + L: UnboundPointSinkLeaseV1, +{ + pub(crate) fn into_parts(self) -> (L, FamilyArtifactBundleV2) { + (self.sink, self.family_artifacts) + } +} + +/// Sink admission failure сохраняет всю уже подготовленную Session. +/// +/// Retry повторяет только host admission: artifact loader, semantic binding и +/// все cold allocation уже завершены и не запускаются снова. +pub(crate) struct PreparedAttachmentRetryV2 where L: UnboundPointSinkLeaseV1, { - cause: AttachmentCreateCauseV1, sink: L, + prepared: PreparedAttachmentColdV1, } -impl fmt::Debug for AttachmentCreateFailureV1 +impl PreparedAttachmentRetryV2 where L: UnboundPointSinkLeaseV1, - L::OutputId: fmt::Debug, - L::AdmissionError: fmt::Debug, + L::OutputId: Copy + Eq, { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter - .debug_struct("AttachmentCreateFailureV1") - .field("cause", &self.cause) - .finish_non_exhaustive() + #[expect( + clippy::result_large_err, + reason = "the cold retry must return the exact prepared Session without a compensating heap allocation" + )] + pub(crate) fn retry( + self, + ) -> Result, PreparedAttachmentAdmissionFailureV2> { + admit_prepared_attachment(self.prepared, self.sink) } } -impl AttachmentCreateFailureV1 +/// Повторный отказ уже подготовленного объекта остаётся только sink-отказом. +pub(crate) struct PreparedAttachmentAdmissionFailureV2 where L: UnboundPointSinkLeaseV1, { - const fn contract(cause: AttachmentCreateErrorV1, sink: L) -> Self { - Self { - cause: AttachmentCreateCauseV1::Contract(cause), - sink, - } - } + cause: L::AdmissionError, + retry: PreparedAttachmentRetryV2, +} - const fn sink_admission(cause: L::AdmissionError, sink: L) -> Self { - Self { - cause: AttachmentCreateCauseV1::SinkAdmission(cause), - sink, - } +impl PreparedAttachmentAdmissionFailureV2 +where + L: UnboundPointSinkLeaseV1, +{ + pub(crate) const fn cause(&self) -> &L::AdmissionError { + &self.cause } - pub(crate) const fn cause(&self) -> &AttachmentCreateCauseV1 { - &self.cause + pub(crate) fn into_parts(self) -> (L::AdmissionError, PreparedAttachmentRetryV2) { + (self.cause, self.retry) } +} - pub(crate) fn into_sink(self) -> L { - self.sink +impl fmt::Debug for PreparedAttachmentAdmissionFailureV2 +where + L: UnboundPointSinkLeaseV1, + L::AdmissionError: fmt::Debug, +{ + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("PreparedAttachmentAdmissionFailureV2") + .field("cause", &self.cause) + .finish_non_exhaustive() } +} - pub(crate) fn into_parts(self) -> (AttachmentCreateCauseV1, L) { - (self.cause, self.sink) +/// Тип failure не допускает pairing contract cause с уже построенной Session. +#[expect( + clippy::large_enum_variant, + reason = "the owning cold failure preserves the exact prepared Session; boxing would add an allocation to every attach" +)] +pub(crate) enum AttachmentCreateFailureV2 +where + L: UnboundPointSinkLeaseV1, +{ + Contract { + cause: AttachmentCreateErrorV1, + retry: UnpreparedAttachmentRetryV2, + }, + SinkAdmission { + cause: L::AdmissionError, + retry: PreparedAttachmentRetryV2, + }, +} + +impl fmt::Debug for AttachmentCreateFailureV2 +where + L: UnboundPointSinkLeaseV1, + L::OutputId: fmt::Debug, + L::AdmissionError: fmt::Debug, +{ + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Contract { cause, .. } => formatter + .debug_struct("AttachmentCreateFailureV2") + .field("contract", cause) + .finish_non_exhaustive(), + Self::SinkAdmission { cause, .. } => formatter + .debug_struct("AttachmentCreateFailureV2") + .field("sink_admission", cause) + .finish_non_exhaustive(), + } } } @@ -636,6 +695,7 @@ impl<'a, SinkOutputId: Copy> AttachedRenderOutputV1<'a, SinkOutputId> { /// Точный post-commit view; historical evidence и render authority не смешаны. pub(crate) struct AttachmentCommitV1<'a, SinkOutputId> { evidence: EvidenceViewV1<'a>, + session: &'a SessionV1, committed_render_patch: &'a [AttachedRenderPatchEntryV1], committed_revision: u64, committed_sink_stamp: &'a PointSinkStampV1, @@ -654,6 +714,12 @@ impl<'a, SinkOutputId: Copy> AttachmentCommitV1<'a, SinkOutputId> { self.evidence } + /// Exact semantic release и artifact receipt исполняемой Session. + /// Historical evidence по-прежнему не получает storage identity. + pub(crate) fn family_execution_bindings(self) -> FamilyExecutionBindingsV2<'a> { + self.session.session.family_execution_bindings() + } + pub(crate) fn render_outputs(self) -> AttachedRenderOutputsV1<'a, SinkOutputId> { let certificate = match self.evidence.state() { SessionState::Ready { current } => Some(VerifiedCertificateV1 { inner: current }), @@ -730,6 +796,16 @@ where } /// Все fallible Core-части cold attach, завершённые до host admission. +struct PreparedAttachmentBindingsV1 { + emissions: Vec>, + presentations: Vec>, + committed_sink_patch: Vec>, + scratch_sink_patch: Vec>, + committed_render_patch: Vec>, + scratch_render_patch: Vec>, +} + +/// Полностью подготовленная Session и её terminal host bindings. struct PreparedAttachmentColdV1 { session: SessionV1, emissions: Vec>, @@ -742,41 +818,90 @@ struct PreparedAttachmentColdV1 { } impl OwnerV1 { - /// Создаёт один terminal attachment этой exact compiled generation. + /// Создаёт terminal attachment с exact family artifact generation. + /// + /// Contract failure возвращает исходные sink и bundle. После полной cold + /// подготовки sink failure сохраняет сам prepared объект, поэтому retry не + /// повторяет loader, semantic binding, Session allocation или generation. + #[expect( + clippy::result_large_err, + reason = "the cold failure owns retryable linear resources instead of allocating an error box" + )] pub(crate) fn attach( &self, stream_id: u32, authored_emissions: &[AuthoredPointEmissionBindingV1], authored_presentations: &[AuthoredPointPresentationBindingV1], + family_artifacts: FamilyArtifactBundleV2, sink: L, - ) -> Result, AttachmentCreateFailureV1> + ) -> Result, AttachmentCreateFailureV2> where L: UnboundPointSinkLeaseV1, { - let prepared = match PreparedAttachmentColdV1::try_new( + let bindings = match PreparedAttachmentBindingsV1::try_new( self, - stream_id, authored_emissions, authored_presentations, &sink, ) { - Ok(prepared) => prepared, - Err(cause) => return Err(AttachmentCreateFailureV1::contract(cause, sink)), - }; - let permit = BoundPointSinkScopePermitV1 { - _owner: &prepared.owner_pin, - emissions: &prepared.emissions, - _presentations: &prepared.presentations, + Ok(bindings) => bindings, + Err(cause) => { + return Err(AttachmentCreateFailureV2::Contract { + cause, + retry: UnpreparedAttachmentRetryV2 { + sink, + family_artifacts, + }, + }); + } }; - let admission = match sink.try_admit_closed(permit) { - Ok(admission) => admission, + let session = match self.instantiate_with_family_artifacts(stream_id, family_artifacts) { + Ok(session) => session, Err(failure) => { - let (cause, sink) = failure.into_parts(); - return Err(AttachmentCreateFailureV1::sink_admission(cause, sink)); + let (cause, family_artifacts) = failure.into_parts(); + return Err(AttachmentCreateFailureV2::Contract { + cause: AttachmentCreateErrorV1::Instantiate(cause), + retry: UnpreparedAttachmentRetryV2 { + sink, + family_artifacts, + }, + }); } }; - // Возвращаемый `Self`, а не `Result`, типом закрывает fallible-границу. - Ok(Attachment::from_closed_admission(prepared, admission)) + let prepared = bindings.finish(session, self.compiled.pin_owner()); + admit_prepared_attachment(prepared, sink).map_err( + |PreparedAttachmentAdmissionFailureV2 { cause, retry }| { + AttachmentCreateFailureV2::SinkAdmission { cause, retry } + }, + ) + } +} + +#[expect( + clippy::result_large_err, + reason = "host rejection returns the exact prepared Session without another cold-path allocation" +)] +fn admit_prepared_attachment( + prepared: PreparedAttachmentColdV1, + sink: L, +) -> Result, PreparedAttachmentAdmissionFailureV2> +where + L: UnboundPointSinkLeaseV1, +{ + let permit = BoundPointSinkScopePermitV1 { + _owner: &prepared.owner_pin, + emissions: &prepared.emissions, + _presentations: &prepared.presentations, + }; + match sink.try_admit_closed(permit) { + Ok(admission) => Ok(Attachment::from_closed_admission(prepared, admission)), + Err(failure) => { + let (cause, sink) = failure.into_parts(); + Err(PreparedAttachmentAdmissionFailureV2 { + cause, + retry: PreparedAttachmentRetryV2 { sink, prepared }, + }) + } } } @@ -807,14 +932,13 @@ where } } -impl PreparedAttachmentColdV1 +impl PreparedAttachmentBindingsV1 where SinkOutputId: Copy + Eq, { - /// Связывает authored IDs и pin той же exact compiled generation до host admission. + /// Проверяет authored terminal bindings и завершает все их allocation. fn try_new( owner: &OwnerV1, - stream_id: u32, authored_emissions: &[AuthoredPointEmissionBindingV1], authored_presentations: &[AuthoredPointPresentationBindingV1], sink: &L, @@ -1007,21 +1131,32 @@ where .try_reserve_exact(presentations.len()) .map_err(|_| AttachmentCreateErrorV1::ResourceExhausted)?; - let session = owner - .instantiate(stream_id) - .map_err(AttachmentCreateErrorV1::Instantiate)?; - let owner_pin = owner.compiled.pin_owner(); Ok(Self { - session, emissions, presentations, committed_sink_patch, scratch_sink_patch, committed_render_patch, scratch_render_patch, - owner_pin, }) } + + fn finish( + self, + session: SessionV1, + owner_pin: ProgramOwnerLeaseV1, + ) -> PreparedAttachmentColdV1 { + PreparedAttachmentColdV1 { + session, + emissions: self.emissions, + presentations: self.presentations, + committed_sink_patch: self.committed_sink_patch, + scratch_sink_patch: self.scratch_sink_patch, + committed_render_patch: self.committed_render_patch, + scratch_render_patch: self.scratch_render_patch, + owner_pin, + } + } } impl Attachment @@ -1139,6 +1274,7 @@ where Ok(AttachmentCommitV1 { evidence: self.session.evidence(), + session: &self.session, committed_render_patch: &self.committed_render_patch, committed_revision: action.revision(), committed_sink_stamp: &self.expected_sink_stamp, diff --git a/crates/labcolors-core/src/program/attachment/tests.rs b/crates/labcolors-core/src/program/attachment/tests.rs index f9fdeb66..84e12895 100644 --- a/crates/labcolors-core/src/program/attachment/tests.rs +++ b/crates/labcolors-core/src/program/attachment/tests.rs @@ -4,10 +4,17 @@ use super::support::{ }; use super::*; use crate::Srgb8; +use crate::family::FamilyDefinitionDigestV2; +use crate::family_artifact::{ + FAMILY_ARTIFACT_DECODER_CALLS, FamilyArtifactBundleV2, FamilyArtifactLoaderV1, + FamilyArtifactReceiptIdV2, FixtureFamilyArtifactCodecV1, encode_fixture_family_artifact_v2, +}; +use crate::lcs_occurrence::ColorSignal; use crate::program::{ - AppearanceContextV1, ConstraintIdV1, DraftV1, FinitePaintDomainV1, JointChoiceV1, JointStateV1, - PaintIdV1, PaintValueV1, ScenarioV1, SourceIdV1, StateKindV1, SurfaceIdV1, - SurfaceInputPortIdV1, SurroundV1, TargetCandidateIdV1, TargetCandidateV1, TargetIdV1, + AppearanceContextV1, ConstraintIdV1, DraftV1, FamilyIdV1, FamilySemanticReleaseV2, + FinitePaintDomainV1, JointChoiceV1, JointStateV1, PaintIdV1, PaintValueV1, ScenarioV1, + SourceIdV1, StateKindV1, SurfaceIdV1, SurfaceInputPortIdV1, SurroundV1, TargetCandidateIdV1, + TargetCandidateV1, TargetIdV1, }; use crate::wcag22::Wcag22CriterionV1; use proptest::prelude::*; @@ -23,6 +30,7 @@ const MIDDLE: OccurrenceIdV1 = OccurrenceIdV1::new(15); const ROOT: PresentationRootIdV1 = PresentationRootIdV1::new(51); const OUTPUT_A: OutputSlotIdV1 = OutputSlotIdV1::new(12); const OUTPUT_B: OutputSlotIdV1 = OutputSlotIdV1::new(13); +const FAMILY: FamilyIdV1 = FamilyIdV1::new(21); fn finite_domain(candidates: Vec) -> FinitePaintDomainV1 { FinitePaintDomainV1::try_new(candidates).unwrap() @@ -58,13 +66,29 @@ fn a_stale_copy_stamp_cannot_cross_a_sequential_sink_epoch() { }; let (first, first_probe) = in_memory_point_sink(&[900]); - let mut first = owner.attach(1, &emissions, &presentations, first).unwrap(); + let mut first = owner + .attach( + 1, + &emissions, + &presentations, + FamilyArtifactBundleV2::empty(), + first, + ) + .unwrap(); first.update(unknown).unwrap(); let stale = first_probe.stamp(); drop(first); let (second, second_probe) = in_memory_point_sink(&[900]); - let mut second = owner.attach(1, &emissions, &presentations, second).unwrap(); + let mut second = owner + .attach( + 1, + &emissions, + &presentations, + FamilyArtifactBundleV2::empty(), + second, + ) + .unwrap(); second.update(unknown).unwrap(); assert_ne!(second_probe.stamp(), stale); assert!(matches!( @@ -92,19 +116,26 @@ fn cold_attach_failure_preserves_the_same_unbound_lease_for_retry() { INNER.value(), )]; - let failure = match owner.attach(1, &emissions, &[], sink) { + let failure = match owner.attach(1, &emissions, &[], FamilyArtifactBundleV2::empty(), sink) { Ok(_) => panic!("incomplete presentation binding must fail"), Err(failure) => failure, }; - assert!(matches!( - failure.cause(), - &AttachmentCreateCauseV1::Contract(AttachmentCreateErrorV1::EmptyPresentations) - )); + let retry = match failure { + AttachmentCreateFailureV2::Contract { cause, retry } => { + assert_eq!(cause, AttachmentCreateErrorV1::EmptyPresentations); + retry + } + AttachmentCreateFailureV2::SinkAdmission { .. } => { + panic!("invalid bindings must not reach host admission") + } + }; assert!(probe.ambient_fallback_is_exposed()); assert!(!probe.lease_was_dropped()); - let sink = failure.into_sink(); - let attachment = owner.attach(1, &emissions, &presentations, sink).unwrap(); + let (sink, family_artifacts) = retry.into_parts(); + let attachment = owner + .attach(1, &emissions, &presentations, family_artifacts, sink) + .unwrap(); assert!(probe.is_closed()); assert!(!probe.ambient_fallback_is_exposed()); @@ -113,6 +144,183 @@ fn cold_attach_failure_preserves_the_same_unbound_lease_for_retry() { assert!(!probe.ambient_fallback_is_exposed()); } +#[test] +fn family_contract_failure_returns_the_same_sink_and_loaded_bundle_for_retry() { + FAMILY_ARTIFACT_DECODER_CALLS.with(|calls| calls.set(0)); + let FamilyAttachmentFixtureV2 { + owner, + artifacts, + semantic, + receipt, + } = family_attachment_fixture(); + assert_eq!(FAMILY_ARTIFACT_DECODER_CALLS.with(core::cell::Cell::get), 1,); + let (sink, probe) = in_memory_point_sink(&[900]); + let emissions = [authored_emission(OUTPUT_A.value(), 900)]; + let presentations = [authored_presentation( + OUTPUT_A.value(), + ROOT.value(), + INNER.value(), + )]; + + let failure = match owner.attach(31, &emissions, &[], artifacts, sink) { + Ok(_) => panic!("incomplete presentation binding must fail"), + Err(failure) => failure, + }; + let retry = match failure { + AttachmentCreateFailureV2::Contract { cause, retry } => { + assert_eq!(cause, AttachmentCreateErrorV1::EmptyPresentations); + retry + } + AttachmentCreateFailureV2::SinkAdmission { .. } => { + panic!("contract failure must not reach host admission") + } + }; + assert!(probe.ambient_fallback_is_exposed()); + assert!(!probe.lease_was_dropped()); + assert_eq!( + FAMILY_ARTIFACT_DECODER_CALLS.with(core::cell::Cell::get), + 1, + "cold contract retry must retain the loaded generation", + ); + + let (sink, artifacts) = retry.into_parts(); + let mut attachment = owner + .attach(31, &emissions, &presentations, artifacts, sink) + .unwrap(); + assert!(probe.is_closed()); + assert_eq!( + FAMILY_ARTIFACT_DECODER_CALLS.with(core::cell::Cell::get), + 1, + "corrected attach must consume the returned bundle without reload", + ); + + let surface = [Srgb8::new([0, 0, 0])]; + let scenarios = [ScenarioV1::new(1, &surface)]; + let committed = attachment.update(observed(1, &scenarios)).unwrap(); + let execution = committed.family_execution_bindings().next().unwrap(); + assert_eq!(execution.semantic(), semantic.into_core()); + assert_eq!(execution.receipt(), receipt); +} + +#[test] +fn family_sink_admission_retry_reuses_the_prepared_session_without_allocator_or_decode() { + FAMILY_ARTIFACT_DECODER_CALLS.with(|calls| calls.set(0)); + let FamilyAttachmentFixtureV2 { + owner, + artifacts, + semantic, + receipt, + } = family_attachment_fixture(); + let (sink, probe) = in_memory_point_sink(&[900]); + let emissions = [authored_emission(OUTPUT_A.value(), 900)]; + let presentations = [authored_presentation( + OUTPUT_A.value(), + ROOT.value(), + INNER.value(), + )]; + + probe.reject_next_admission(); + let failure = match owner.attach(32, &emissions, &presentations, artifacts, sink) { + Ok(_) => panic!("sink admission rejection must retain cold preparation"), + Err(failure) => failure, + }; + let retry = match failure { + AttachmentCreateFailureV2::SinkAdmission { cause, retry } => { + assert_eq!( + cause, + InMemoryPointSinkAdmissionErrorV1::RejectedBeforeInstall, + ); + retry + } + AttachmentCreateFailureV2::Contract { .. } => { + panic!("valid family attachment must reach host admission") + } + }; + assert!(probe.ambient_fallback_is_exposed()); + assert_eq!(probe.admitted_stamp(), None); + assert_eq!(FAMILY_ARTIFACT_DECODER_CALLS.with(core::cell::Cell::get), 1,); + + probe.reject_next_admission_after_install(); + let (failure, events) = + crate::test_support::measured_allocator_events(|| match retry.retry() { + Ok(_) => panic!("second sink admission rejection must retain cold preparation"), + Err(failure) => failure, + }); + assert_eq!(events, crate::test_support::AllocatorEvents::default()); + assert_eq!( + failure.cause(), + &InMemoryPointSinkAdmissionErrorV1::RejectedAfterInstall, + ); + let (cause, retry) = failure.into_parts(); + assert_eq!( + cause, + InMemoryPointSinkAdmissionErrorV1::RejectedAfterInstall, + ); + assert_eq!(probe.admitted_stamp(), None); + assert_eq!( + FAMILY_ARTIFACT_DECODER_CALLS.with(core::cell::Cell::get), + 1, + "rejected prepared retry must not reconstruct the artifact generation", + ); + + let (mut attachment, events) = + crate::test_support::measured_allocator_events(|| match retry.retry() { + Ok(attachment) => attachment, + Err(_) => panic!("third sink admission must accept the retained preparation"), + }); + assert_eq!(events, crate::test_support::AllocatorEvents::default()); + assert_eq!( + FAMILY_ARTIFACT_DECODER_CALLS.with(core::cell::Cell::get), + 1, + "successful prepared retry must not reconstruct the artifact generation", + ); + assert!(probe.is_closed()); + + let surface = [Srgb8::new([0, 0, 0])]; + let scenarios = [ScenarioV1::new(1, &surface)]; + let committed = attachment.update(observed(1, &scenarios)).unwrap(); + let execution = committed.family_execution_bindings().next().unwrap(); + assert_eq!(execution.semantic(), semantic.into_core()); + assert_eq!(execution.receipt(), receipt); +} + +#[test] +fn attachment_pins_owner_and_artifact_until_explicit_dispose() { + let drops = std::rc::Rc::new(core::cell::Cell::new(0)); + let FamilyAttachmentFixtureV2 { + owner, + artifacts, + semantic: _, + receipt: _, + } = family_attachment_fixture_with_drop_counter(drops.clone()); + let (sink, _) = in_memory_point_sink(&[900]); + let emissions = [authored_emission(OUTPUT_A.value(), 900)]; + let presentations = [authored_presentation( + OUTPUT_A.value(), + ROOT.value(), + INNER.value(), + )]; + let mut attachment = owner + .attach(40, &emissions, &presentations, artifacts, sink) + .unwrap(); + + assert_eq!(drops.get(), 0); + drop(owner); + let surface = [Srgb8::new([0, 0, 0])]; + let scenarios = [ScenarioV1::new(1, &surface)]; + assert_eq!( + attachment + .update(observed(1, &scenarios)) + .unwrap() + .evidence() + .kind(), + StateKindV1::Ready, + ); + assert_eq!(drops.get(), 0); + attachment.dispose(); + assert_eq!(drops.get(), 1); +} + #[test] fn create_failure_debug_reports_the_typed_cause_without_sink_internals() { let owner = owner( @@ -129,25 +337,31 @@ fn create_failure_debug_reports_the_typed_cause_without_sink_internals() { )]; let (sink, _) = in_memory_point_sink(&[900]); - let contract = match owner.attach(1, &emissions, &[], sink) { + let contract = match owner.attach(1, &emissions, &[], FamilyArtifactBundleV2::empty(), sink) { Ok(_) => panic!("contract failure was expected"), Err(failure) => failure, }; let contract_debug = format!("{contract:?}"); - assert!(contract_debug.contains("AttachmentCreateFailureV1")); - assert!(contract_debug.contains("Contract(EmptyPresentations)")); + assert!(contract_debug.contains("AttachmentCreateFailureV2")); + assert!(contract_debug.contains("contract: EmptyPresentations")); assert!(!contract_debug.contains("owned_scope")); assert!(!contract_debug.contains("TestSinkSharedV1")); let (sink, probe) = in_memory_point_sink(&[900]); probe.reject_next_admission(); - let admission = match owner.attach(2, &emissions, &presentations, sink) { + let admission = match owner.attach( + 2, + &emissions, + &presentations, + FamilyArtifactBundleV2::empty(), + sink, + ) { Ok(_) => panic!("admission failure was expected"), Err(failure) => failure, }; let admission_debug = format!("{admission:?}"); - assert!(admission_debug.contains("AttachmentCreateFailureV1")); - assert!(admission_debug.contains("SinkAdmission(RejectedBeforeInstall)")); + assert!(admission_debug.contains("AttachmentCreateFailureV2")); + assert!(admission_debug.contains("sink_admission: RejectedBeforeInstall")); assert!(!admission_debug.contains("owned_scope")); assert!(!admission_debug.contains("TestSinkSharedV1")); } @@ -169,37 +383,53 @@ fn failed_closed_admission_is_atomic_and_mints_epoch_only_after_install() { )]; probe.reject_next_admission(); - let failure = match owner.attach(2, &emissions, &presentations, sink) { + let failure = match owner.attach( + 2, + &emissions, + &presentations, + FamilyArtifactBundleV2::empty(), + sink, + ) { Ok(_) => panic!("pre-install admission fault must return the lease"), Err(failure) => failure, }; - assert!(matches!( - failure.cause(), - &AttachmentCreateCauseV1::SinkAdmission( - InMemoryPointSinkAdmissionErrorV1::RejectedBeforeInstall - ) - )); + let retry = match failure { + AttachmentCreateFailureV2::SinkAdmission { cause, retry } => { + assert_eq!( + cause, + InMemoryPointSinkAdmissionErrorV1::RejectedBeforeInstall + ); + retry + } + AttachmentCreateFailureV2::Contract { .. } => { + panic!("valid bindings must reach host admission") + } + }; assert!(probe.ambient_fallback_is_exposed()); assert_eq!(probe.admitted_stamp(), None); - let sink = failure.into_sink(); probe.reject_next_admission_after_install(); - let failure = match owner.attach(2, &emissions, &presentations, sink) { + let failure = match retry.retry() { Ok(_) => panic!("post-install fault must roll the tombstone back"), Err(failure) => failure, }; - assert!(matches!( + assert_eq!( failure.cause(), - &AttachmentCreateCauseV1::SinkAdmission( - InMemoryPointSinkAdmissionErrorV1::RejectedAfterInstall - ) - )); + &InMemoryPointSinkAdmissionErrorV1::RejectedAfterInstall + ); + assert_eq!( + format!("{failure:?}"), + format!( + "PreparedAttachmentAdmissionFailureV2 {{ cause: {:?}, .. }}", + failure.cause(), + ), + "prepared retry failures must not expose the retained Session or sink", + ); assert!(probe.ambient_fallback_is_exposed()); assert_eq!(probe.admitted_stamp(), None); - let attachment = owner - .attach(2, &emissions, &presentations, failure.into_sink()) - .unwrap(); + let (_, retry) = failure.into_parts(); + let attachment = retry.retry().unwrap(); let admitted = probe.stamp(); assert_eq!(admitted.sequence(), 0); assert!(probe.is_closed()); @@ -228,7 +458,13 @@ fn initial_unknown_and_violation_keep_the_host_scope_closed() { ); let (sink, unknown_probe) = in_memory_point_sink(&[900]); let mut attachment = pass_owner - .attach(3, &emissions, &presentations, sink) + .attach( + 3, + &emissions, + &presentations, + FamilyArtifactBundleV2::empty(), + sink, + ) .unwrap(); assert!(unknown_probe.is_closed()); attachment.update(unknown).unwrap(); @@ -243,7 +479,13 @@ fn initial_unknown_and_violation_keep_the_host_scope_closed() { ); let (sink, violation_probe) = in_memory_point_sink(&[900]); let mut conflict = conflict_owner - .attach(4, &emissions, &presentations, sink) + .attach( + 4, + &emissions, + &presentations, + FamilyArtifactBundleV2::empty(), + sink, + ) .unwrap(); let values = [Srgb8::new([0, 0, 0])]; let scenarios = [ScenarioV1::new(1, &values)]; @@ -279,7 +521,13 @@ fn every_host_binding_axis_is_checked_before_sink_mutation() { { let (sink, probe) = in_memory_point_sink(&[900]); let mut attachment = owner - .attach(5 + index as u32, &emissions, &presentations, sink) + .attach( + 5 + index as u32, + &emissions, + &presentations, + FamilyArtifactBundleV2::empty(), + sink, + ) .unwrap(); let initial_stamp = probe.stamp(); probe.drift_host_binding(axis); @@ -338,7 +586,15 @@ fn every_sink_intent_cas_checks_the_same_current_binding_stamp() { INNER.value(), )]; let (sink, probe) = in_memory_point_sink(&[900]); - let mut attachment = owner.attach(6, &emissions, &presentations, sink).unwrap(); + let mut attachment = owner + .attach( + 6, + &emissions, + &presentations, + FamilyArtifactBundleV2::empty(), + sink, + ) + .unwrap(); let values = [Srgb8::new([0, 0, 0])]; let scenarios = [ScenarioV1::new(1, &values)]; attachment.update(observed(1, &scenarios)).unwrap(); @@ -348,7 +604,13 @@ fn every_sink_intent_cas_checks_the_same_current_binding_stamp() { let baseline_counts = probe.intent_counts(); let (foreign, foreign_probe) = in_memory_point_sink(&[900]); let foreign = owner - .attach(7, &emissions, &presentations, foreign) + .attach( + 7, + &emissions, + &presentations, + FamilyArtifactBundleV2::empty(), + foreign, + ) .unwrap(); let foreign_stamp = foreign_probe.stamp(); let foreign_transition = PointSinkMutationStampV1::new(foreign_stamp).unwrap(); @@ -403,7 +665,15 @@ fn core_mints_the_exact_successor_for_every_mutating_intent() { INNER.value(), )]; let (sink, probe) = in_memory_point_sink(&[900]); - let mut attachment = owner.attach(8, &emissions, &presentations, sink).unwrap(); + let mut attachment = owner + .attach( + 8, + &emissions, + &presentations, + FamilyArtifactBundleV2::empty(), + sink, + ) + .unwrap(); let values = [Srgb8::new([0, 0, 0])]; let scenarios = [ScenarioV1::new(1, &values)]; @@ -459,7 +729,15 @@ fn exhausted_stamp_fails_before_sink_prepare_or_session_commit() { INNER.value(), )]; let (sink, probe) = in_memory_point_sink(&[900]); - let mut attachment = owner.attach(8, &emissions, &presentations, sink).unwrap(); + let mut attachment = owner + .attach( + 8, + &emissions, + &presentations, + FamilyArtifactBundleV2::empty(), + sink, + ) + .unwrap(); let exhausted = PointSinkStampV1::new(u64::MAX, probe.stamp().binding_epoch()); probe.force_stamp_sequence(u64::MAX); attachment.expected_sink_stamp = exhausted; @@ -496,7 +774,15 @@ fn closed_revoke_is_confirmable_from_one_expected_stamp_source_of_truth() { ROOT.value(), INNER.value(), )]; - let mut attachment = owner.attach(8, &emissions, &presentations, sink).unwrap(); + let mut attachment = owner + .attach( + 8, + &emissions, + &presentations, + FamilyArtifactBundleV2::empty(), + sink, + ) + .unwrap(); let admission_stamp = attachment.expected_sink_stamp; assert_eq!(attachment.committed_revision, None); @@ -551,7 +837,7 @@ proptest! { INNER.value(), )]; let mut attachment = owner - .attach(9, &emissions, &presentations, sink) + .attach(9, &emissions, &presentations, FamilyArtifactBundleV2::empty(), sink) .unwrap(); let values = [Srgb8::new([0, 0, 0])]; let scenarios = [ScenarioV1::new(1, &values)]; @@ -701,7 +987,15 @@ fn attachment_terminal_tail_has_no_allocator_events() { ROOT.value(), INNER.value(), )]; - let mut attachment = owner.attach(1, &emissions, &presentations, sink).unwrap(); + let mut attachment = owner + .attach( + 1, + &emissions, + &presentations, + FamilyArtifactBundleV2::empty(), + sink, + ) + .unwrap(); let values = [Srgb8::new([0, 0, 0])]; let scenarios = [ScenarioV1::new(44, &values)]; @@ -741,7 +1035,15 @@ fn warmed_attachment_complete_lifecycle_has_no_allocator_events() { ROOT.value(), INNER.value(), )]; - let mut attachment = owner.attach(1, &emissions, &presentations, sink).unwrap(); + let mut attachment = owner + .attach( + 1, + &emissions, + &presentations, + FamilyArtifactBundleV2::empty(), + sink, + ) + .unwrap(); let white = [Srgb8::new([0xFF; 3])]; let light = [Srgb8::new([0xF0; 3])]; let black = [Srgb8::new([0; 3])]; @@ -939,7 +1241,15 @@ fn allocator_sink_missing_stamp_is_a_typed_prepare_failure() { ROOT.value(), INNER.value(), )]; - let mut attachment = owner.attach(1, &emissions, &presentations, sink).unwrap(); + let mut attachment = owner + .attach( + 1, + &emissions, + &presentations, + FamilyArtifactBundleV2::empty(), + sink, + ) + .unwrap(); probe.clear_stamp_for_test(); assert!(matches!( @@ -1041,6 +1351,70 @@ fn owner( owner_with_terminal_presentation(source, expected, extra_topology, outputs, false) } +struct FamilyAttachmentFixtureV2 { + owner: OwnerV1, + artifacts: FamilyArtifactBundleV2, + semantic: FamilySemanticReleaseV2, + receipt: FamilyArtifactReceiptIdV2, +} + +fn family_attachment_fixture() -> FamilyAttachmentFixtureV2 { + family_attachment_fixture_inner(None) +} + +fn family_attachment_fixture_with_drop_counter( + counter: std::rc::Rc>, +) -> FamilyAttachmentFixtureV2 { + family_attachment_fixture_inner(Some(counter)) +} + +fn family_attachment_fixture_inner( + drop_counter: Option>>, +) -> FamilyAttachmentFixtureV2 { + let member = Srgb8::new([12, 34, 56]); + let definition = + FamilyDefinitionDigestV2::from_fixture_bytes_v2(b"attachment-tests/exact-family"); + let (certificate, encoded) = encode_fixture_family_artifact_v2( + definition, + &[ColorSignal::from_srgb8(member)], + FixtureFamilyArtifactCodecV1::CanonicalMembersV1, + ) + .unwrap(); + let semantic = FamilySemanticReleaseV2::from_core(certificate.semantic_release()); + let receipt = certificate.artifact_receipt(); + let artifact = FamilyArtifactLoaderV1::load_fixture(certificate, encoded).unwrap(); + let artifact = match drop_counter { + Some(counter) => artifact.with_drop_counter_for_test(counter), + None => artifact, + }; + let artifacts = FamilyArtifactBundleV2::from_artifacts(vec![artifact]); + + let context = AppearanceContextV1::try_new(64.0, 0.2, SurroundV1::Dim).unwrap(); + let inner_surface = SurfaceIdV1::new(7); + let mut draft = DraftV1::new(); + draft.push_source(SOURCE, member); + draft.push_fixed_target(TARGET, SOURCE); + draft.push_family(FAMILY, semantic); + draft.push_surface_input_port(INPUT); + draft.push_solid_paint(PAINT, TARGET); + draft.push_input_surface(INPUT_SURFACE, INPUT); + draft.push_source_over_occurrence(INNER, PAINT, INPUT_SURFACE, context); + draft.push_occurrence_surface(inner_surface, INNER); + draft.push_source_over_occurrence(TERMINAL, PAINT, inner_surface, context); + draft.push_point_presentation_root(ROOT, TERMINAL); + draft.push_point_presentation_target(ROOT, INNER); + draft.push_intrinsic_family_membership_hard(ConstraintIdV1::new(10), TARGET, FAMILY); + draft.push_exact_visible_unary_hard(ConstraintIdV1::new(11), INNER, member); + draft.push_output(OUTPUT_A, PAINT); + + FamilyAttachmentFixtureV2 { + owner: draft.compile().unwrap(), + artifacts, + semantic, + receipt, + } +} + fn owner_with_terminal_presentation( source: Srgb8, expected: Srgb8, @@ -1090,7 +1464,7 @@ fn observed<'a>(revision: u64, scenarios: &'a [ScenarioV1<'a>]) -> UpdateV1<'a> } fn contract_error( - result: Result, AttachmentCreateFailureV1>, + result: Result, AttachmentCreateFailureV2>, ) -> AttachmentCreateErrorV1 where L: UnboundPointSinkLeaseV1, @@ -1099,9 +1473,9 @@ where Ok(_) => panic!("cold contract error was expected"), Err(failure) => failure, }; - match failure.into_parts().0 { - AttachmentCreateCauseV1::Contract(cause) => cause, - AttachmentCreateCauseV1::SinkAdmission(_) => { + match failure { + AttachmentCreateFailureV2::Contract { cause, .. } => cause, + AttachmentCreateFailureV2::SinkAdmission { .. } => { panic!("contract test reached host admission") } } @@ -1126,7 +1500,13 @@ fn attach_rejects_missing_extra_duplicate_and_accepts_reordered_sink_scope() { let (missing, missing_probe) = in_memory_point_sink(&[900]); assert!(matches!( - contract_error(owner.attach(1, &emissions, &presentations, missing)), + contract_error(owner.attach( + 1, + &emissions, + &presentations, + FamilyArtifactBundleV2::empty(), + missing + )), AttachmentCreateErrorV1::SinkScopeCount { expected: 2, actual: 1 @@ -1137,7 +1517,13 @@ fn attach_rejects_missing_extra_duplicate_and_accepts_reordered_sink_scope() { let (extra, _) = in_memory_point_sink(&[900, 901, 902]); assert!(matches!( - contract_error(owner.attach(1, &emissions, &presentations, extra)), + contract_error(owner.attach( + 1, + &emissions, + &presentations, + FamilyArtifactBundleV2::empty(), + extra + )), AttachmentCreateErrorV1::SinkScopeCount { expected: 2, actual: 3 @@ -1146,13 +1532,25 @@ fn attach_rejects_missing_extra_duplicate_and_accepts_reordered_sink_scope() { let (duplicate, _) = in_memory_point_sink(&[900, 900]); assert!(matches!( - contract_error(owner.attach(1, &emissions, &presentations, duplicate)), + contract_error(owner.attach( + 1, + &emissions, + &presentations, + FamilyArtifactBundleV2::empty(), + duplicate + )), AttachmentCreateErrorV1::DuplicateSinkScopeOutput { .. } )); let (reordered, reordered_probe) = in_memory_point_sink(&[901, 900]); let reordered = owner - .attach(1, &emissions, &presentations, reordered) + .attach( + 1, + &emissions, + &presentations, + FamilyArtifactBundleV2::empty(), + reordered, + ) .unwrap(); assert!(reordered_probe.is_closed()); reordered.dispose(); @@ -1163,7 +1561,13 @@ fn attach_rejects_missing_extra_duplicate_and_accepts_reordered_sink_scope() { ]; let (sink, _) = in_memory_point_sink(&[900, 901]); assert!(matches!( - contract_error(owner.attach(1, &duplicate_emission, &presentations, sink)), + contract_error(owner.attach( + 1, + &duplicate_emission, + &presentations, + FamilyArtifactBundleV2::empty(), + sink + )), AttachmentCreateErrorV1::SinkOutputAliased { .. } )); } @@ -1187,7 +1591,13 @@ fn attach_requires_exact_bijection_over_compiled_presentations() { ]; let (sink, probe) = in_memory_point_sink(&[900, 901]); assert!(matches!( - contract_error(owner.attach(2, &emissions, &omitted_terminal, sink)), + contract_error(owner.attach( + 2, + &emissions, + &omitted_terminal, + FamilyArtifactBundleV2::empty(), + sink + )), AttachmentCreateErrorV1::PresentationCount { expected: 3, actual: 2 @@ -1215,7 +1625,13 @@ fn alias_outputs_cannot_claim_the_same_compiled_presentation() { ]; let (sink, _) = in_memory_point_sink(&[900, 901]); assert!(matches!( - contract_error(owner.attach(3, &emissions, &duplicate_actual_target, sink)), + contract_error(owner.attach( + 3, + &emissions, + &duplicate_actual_target, + FamilyArtifactBundleV2::empty(), + sink + )), AttachmentCreateErrorV1::DuplicatePresentation { root: ROOT, occurrence: INNER, @@ -1244,7 +1660,13 @@ fn every_emission_requires_at_least_one_distinct_compiled_presentation() { let (sink, _) = in_memory_point_sink(&[900, 901]); assert!(matches!( - contract_error(owner.attach(4, &emissions, &only_output_a, sink)), + contract_error(owner.attach( + 4, + &emissions, + &only_output_a, + FamilyArtifactBundleV2::empty(), + sink + )), AttachmentCreateErrorV1::MissingOutputPresentation { output: OUTPUT_B } )); } @@ -1268,7 +1690,13 @@ fn duplicate_emission_output_has_its_exact_typed_error() { let (sink, _) = in_memory_point_sink(&[900, 901]); assert!(matches!( - contract_error(owner.attach(5, &duplicate_output, &presentations, sink)), + contract_error(owner.attach( + 5, + &duplicate_output, + &presentations, + FamilyArtifactBundleV2::empty(), + sink + )), AttachmentCreateErrorV1::DuplicateEmissionOutput { output: OUTPUT_A } )); } @@ -1290,7 +1718,15 @@ fn verified_snapshot_mints_attached_render_output_and_exact_confirm_only_for_ide authored_presentation(OUTPUT_B.value(), ROOT.value(), MIDDLE.value()), authored_presentation(OUTPUT_A.value(), ROOT.value(), INNER.value()), ]; - let mut attachment = owner.attach(7, &emissions, &presentations, sink).unwrap(); + let mut attachment = owner + .attach( + 7, + &emissions, + &presentations, + FamilyArtifactBundleV2::empty(), + sink, + ) + .unwrap(); let values = [Srgb8::new([0, 0, 0])]; let scenarios = [ScenarioV1::new(44, &values)]; @@ -1363,7 +1799,15 @@ fn selected_nonopaque_finite_paint_reaches_sink_and_render_authority_atomically( ROOT.value(), INNER.value(), )]; - let mut attachment = owner.attach(71, &emissions, &presentations, sink).unwrap(); + let mut attachment = owner + .attach( + 71, + &emissions, + &presentations, + FamilyArtifactBundleV2::empty(), + sink, + ) + .unwrap(); let black = [Srgb8::new([0; 3])]; let scenarios = [ScenarioV1::new(44, &black)]; @@ -1391,7 +1835,15 @@ fn one_emission_fans_out_to_every_distinct_attached_presentation() { authored_presentation(OUTPUT_A.value(), ROOT.value(), MIDDLE.value()), authored_presentation(OUTPUT_A.value(), ROOT.value(), INNER.value()), ]; - let mut attachment = owner.attach(70, &emissions, &presentations, sink).unwrap(); + let mut attachment = owner + .attach( + 70, + &emissions, + &presentations, + FamilyArtifactBundleV2::empty(), + sink, + ) + .unwrap(); let values = [Srgb8::new([0, 0, 0])]; let scenarios = [ScenarioV1::new(44, &values)]; @@ -1425,7 +1877,13 @@ fn unknown_and_known_violation_revoke_the_complete_snapshot() { INNER.value(), )]; let mut attachment = pass_owner - .attach(8, &emissions, &presentations, sink) + .attach( + 8, + &emissions, + &presentations, + FamilyArtifactBundleV2::empty(), + sink, + ) .unwrap(); let values = [Srgb8::new([0, 0, 0])]; let scenarios = [ScenarioV1::new(1, &values)]; @@ -1454,7 +1912,13 @@ fn unknown_and_known_violation_revoke_the_complete_snapshot() { ); let (sink, conflict_probe) = in_memory_point_sink(&[900]); let mut conflict = conflict_owner - .attach(9, &emissions, &presentations, sink) + .attach( + 9, + &emissions, + &presentations, + FamilyArtifactBundleV2::empty(), + sink, + ) .unwrap(); let committed = conflict.update(observed(1, &scenarios)).unwrap(); assert_eq!(committed.evidence().kind(), StateKindV1::Failed); @@ -1480,7 +1944,15 @@ fn rejected_install_keeps_session_snapshot_and_releases_busy() { ROOT.value(), INNER.value(), )]; - let mut attachment = owner.attach(10, &emissions, &presentations, sink).unwrap(); + let mut attachment = owner + .attach( + 10, + &emissions, + &presentations, + FamilyArtifactBundleV2::empty(), + sink, + ) + .unwrap(); let values = [Srgb8::new([0, 0, 0])]; let scenarios = [ScenarioV1::new(1, &values)]; attachment.update(observed(1, &scenarios)).unwrap(); @@ -1523,7 +1995,15 @@ fn every_fallible_sink_boundary_is_all_or_nothing() { ROOT.value(), INNER.value(), )]; - let mut attachment = owner.attach(73, &emissions, &presentations, sink).unwrap(); + let mut attachment = owner + .attach( + 73, + &emissions, + &presentations, + FamilyArtifactBundleV2::empty(), + sink, + ) + .unwrap(); let values = [Srgb8::new([0, 0, 0])]; let scenarios = [ScenarioV1::new(1, &values)]; let initial_stamp = probe.stamp(); @@ -1604,7 +2084,15 @@ fn installed_retirement_waits_for_the_next_preinstall_drain_and_retry_is_clean() ROOT.value(), INNER.value(), )]; - let mut attachment = owner.attach(71, &emissions, &presentations, sink).unwrap(); + let mut attachment = owner + .attach( + 71, + &emissions, + &presentations, + FamilyArtifactBundleV2::empty(), + sink, + ) + .unwrap(); let values = [Srgb8::new([0, 0, 0])]; let scenarios = [ScenarioV1::new(1, &values)]; @@ -1649,8 +2137,17 @@ fn source_guards_keep_the_post_install_tail_destructor_free() { let session_source = include_str!("../../session.rs"); let support_source = include_str!("support.rs"); + assert_eq!( + attachment_source + .matches("pub(crate) fn attach(") + .count(), + 1, + "one canonical attach path must own both family and family-free sessions", + ); + assert!(!attachment_source.contains("attach_with_family_artifacts")); + let cold_prepare = attachment_source - .find("PreparedAttachmentColdV1::try_new(") + .find("PreparedAttachmentBindingsV1::try_new(") .expect("all fallible Core preparation must precede host admission"); let admission = attachment_source .find("sink.try_admit_closed(permit)") @@ -1823,7 +2320,15 @@ fn dispose_revokes_before_hostile_retirement_destructor_runs() { ROOT.value(), INNER.value(), )]; - let mut attachment = owner.attach(72, &emissions, &presentations, sink).unwrap(); + let mut attachment = owner + .attach( + 72, + &emissions, + &presentations, + FamilyArtifactBundleV2::empty(), + sink, + ) + .unwrap(); let values = [Srgb8::new([0, 0, 0])]; let scenarios = [ScenarioV1::new(1, &values)]; @@ -1887,7 +2392,13 @@ fn same_ids_and_ordinals_cannot_pair_a_foreign_generation_token_with_the_pin() { authored_presentation(OUTPUT_A.value(), ROOT.value(), MIDDLE.value()), ]; let mut attachment = owner_b - .attach(11, &emissions, &presentations, sink) + .attach( + 11, + &emissions, + &presentations, + FamilyArtifactBundleV2::empty(), + sink, + ) .unwrap(); drop(owner_a); drop(owner_b); @@ -1916,7 +2427,15 @@ fn dispose_revokes_before_lease_session_and_owner_pin_release() { ROOT.value(), INNER.value(), )]; - let mut attachment = owner.attach(12, &emissions, &presentations, sink).unwrap(); + let mut attachment = owner + .attach( + 12, + &emissions, + &presentations, + FamilyArtifactBundleV2::empty(), + sink, + ) + .unwrap(); drop(owner); let values = [Srgb8::new([0, 0, 0])]; let scenarios = [ScenarioV1::new(1, &values)]; diff --git a/crates/labcolors-core/src/program_family_tests.rs b/crates/labcolors-core/src/program_family_tests.rs index bbc1b2fe..1173a0e2 100644 --- a/crates/labcolors-core/src/program_family_tests.rs +++ b/crates/labcolors-core/src/program_family_tests.rs @@ -1,6 +1,12 @@ //! Интеграция точного допущенного образа family в Program. use crate::family::FAMILY_MEMBERSHIP_ASSESS_CALLS; +use crate::family::FamilyDefinitionDigestV2; +use crate::family_artifact::{ + AdmittedFamilyArtifactV2, FamilyArtifactBundleV2, FamilyArtifactLoaderV1, + FixtureFamilyArtifactCodecV1, encode_fixture_family_artifact_v2, +}; +use crate::lcs_occurrence::ColorSignal; use crate::program_boundary_tests::CommitProgramUpdateForTest as _; use crate::{Srgb8, program}; @@ -20,11 +26,49 @@ 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() +struct LoadedFamilyFixtureV2 { + semantic: program::FamilySemanticReleaseV2, + artifact: AdmittedFamilyArtifactV2, +} + +fn family(values: &[[u8; 3]]) -> LoadedFamilyFixtureV2 { + family_with_codec(values, FixtureFamilyArtifactCodecV1::CanonicalMembersV1) +} + +fn family_with_codec( + values: &[[u8; 3]], + codec: FixtureFamilyArtifactCodecV1, +) -> LoadedFamilyFixtureV2 { + let members = values + .iter() + .copied() + .map(Srgb8::new) + .map(ColorSignal::from_srgb8) + .collect::>(); + let definition = + FamilyDefinitionDigestV2::from_fixture_bytes_v2(b"program-family-tests/declared-set"); + let (certificate, encoded) = + encode_fixture_family_artifact_v2(definition, &members, codec).unwrap(); + let semantic = program::FamilySemanticReleaseV2::from_core(certificate.semantic_release()); + let artifact = FamilyArtifactLoaderV1::load_fixture(certificate, encoded).unwrap(); + LoadedFamilyFixtureV2 { semantic, artifact } +} + +struct FamilyDraftFixtureV2 { + draft: program::DraftV1, + artifacts: FamilyArtifactBundleV2, + semantic: Option, +} + +fn instantiate_with_family_artifacts( + owner: &program::OwnerV1, + stream_id: u32, + artifacts: FamilyArtifactBundleV2, +) -> program::SessionV1 { + match owner.instantiate_with_family_artifacts(stream_id, artifacts) { + Ok(session) => session, + Err(failure) => panic!("family artifact admission failed: {:?}", failure.cause()), + } } fn finite_base_draft( @@ -70,10 +114,15 @@ fn finite_family_draft( declare_family: bool, family_mode_hard: bool, expected_visible: Srgb8, -) -> program::DraftV1 { +) -> FamilyDraftFixtureV2 { let mut draft = finite_base_draft(candidates); + let mut artifacts = Vec::new(); + let mut semantic = None; if declare_family { - draft.push_family(FAMILY, family(family_values)); + let fixture = family(family_values); + semantic = Some(fixture.semantic); + draft.push_family(FAMILY, fixture.semantic); + artifacts.push(fixture.artifact); } if family_mode_hard { draft.push_intrinsic_family_membership_hard(FAMILY_CONSTRAINT, TARGET, FAMILY); @@ -81,7 +130,11 @@ fn finite_family_draft( draft.push_intrinsic_family_membership_report_only(FAMILY_CONSTRAINT, TARGET, FAMILY); } draft.push_exact_visible_unary_hard(VISIBLE_CONSTRAINT, OCCURRENCE, expected_visible); - draft + FamilyDraftFixtureV2 { + draft, + artifacts: FamilyArtifactBundleV2::from_artifacts(artifacts), + semantic, + } } #[test] @@ -90,17 +143,16 @@ fn hard_family_membership_selects_a_member_rechecks_it_and_retains_alpha() { 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( + let fixture = 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 expected_semantic = fixture.semantic.unwrap(); + let owner = fixture.draft.compile().unwrap(); + let mut session = instantiate_with_family_artifacts(&owner, 12, fixture.artifacts); let black = [Srgb8::new([0; 3])]; let scenarios = [program::ScenarioV1::new(13, &black)]; FAMILY_MEMBERSHIP_ASSESS_CALLS.with(|calls| calls.set(0)); @@ -144,7 +196,7 @@ fn hard_family_membership_selects_a_member_rechecks_it_and_retains_alpha() { }; assert_eq!(measurement.family(), FAMILY); assert_eq!(measurement.signal(), blue); - assert_eq!(measurement.content(), expected_family_content); + assert_eq!(measurement.semantic(), expected_semantic); let program::IntrinsicUnaryProofV1::FamilyMembershipPass = intrinsic.proof() else { panic!("selected family member must carry an inclusion witness"); }; @@ -156,7 +208,7 @@ fn missing_family_is_a_typed_atomic_compile_error() { 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() { + let error = match draft.draft.compile() { Ok(_) => panic!("an unresolved family edge must not compile"), Err(error) => error, }; @@ -248,30 +300,110 @@ fn family_id_and_error_handle_preserve_a_nontrivial_opaque_value() { } #[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(); +fn semantic_release_bytes_change_with_the_exact_image() { + let blue = family(&[[0, 0, 255]]).semantic; + let red = family(&[[255, 0, 0]]).semantic; assert_ne!(blue.as_bytes(), red.as_bytes()); } +#[test] +fn representation_receipt_is_session_provenance_not_program_semantics() { + let candidate = program::TargetCandidateIdV1::new(23); + let blue = Srgb8::new([0, 0, 255]); + let canonical = family_with_codec( + &[[0, 0, 1], [0, 0, 255]], + FixtureFamilyArtifactCodecV1::CanonicalMembersV1, + ); + let reversed = family_with_codec( + &[[0, 0, 1], [0, 0, 255]], + FixtureFamilyArtifactCodecV1::ReversedMembersV1, + ); + assert_eq!(canonical.semantic, reversed.semantic); + let canonical_receipt = canonical.artifact.artifact_receipt(); + let reversed_receipt = reversed.artifact.artifact_receipt(); + assert_ne!(canonical_receipt, reversed_receipt); + + let compile = || { + let mut draft = finite_base_draft(&[(candidate, blue, 1.0)]); + draft.push_family(FAMILY, canonical.semantic); + draft.push_intrinsic_family_membership_hard(FAMILY_CONSTRAINT, TARGET, FAMILY); + draft.push_exact_visible_unary_hard(VISIBLE_CONSTRAINT, OCCURRENCE, blue); + draft.compile().unwrap() + }; + let canonical_owner = compile(); + let reversed_owner = compile(); + assert_eq!( + canonical_owner.content_identity(), + reversed_owner.content_identity(), + ); + + let mut canonical_session = instantiate_with_family_artifacts( + &canonical_owner, + 24, + FamilyArtifactBundleV2::from_artifacts(vec![canonical.artifact]), + ); + let mut reversed_session = instantiate_with_family_artifacts( + &reversed_owner, + 25, + FamilyArtifactBundleV2::from_artifacts(vec![reversed.artifact]), + ); + + let black = [Srgb8::new([0; 3])]; + let scenarios = [program::ScenarioV1::new(26, &black)]; + for (owner, session) in [ + (&canonical_owner, &mut canonical_session), + (&reversed_owner, &mut reversed_session), + ] { + let evidence = owner + .commit( + session, + program::UpdateV1::Observed { + revision: 1, + scenarios: &scenarios, + }, + ) + .unwrap(); + let Some(program::CertificateV1::Verified(verified)) = evidence.certificates().next() + else { + panic!("both transport representations must execute the same semantic family"); + }; + let measurement = verified + .cells() + .find_map(|cell| match cell.assessment() { + program::AssessmentV1::IntrinsicUnary(intrinsic) => match intrinsic.measurement() { + program::IntrinsicUnaryMeasurementV1::FamilyMembership(measurement) => { + Some(measurement) + } + _ => None, + }, + _ => None, + }) + .unwrap(); + assert_eq!( + measurement.semantic().as_bytes(), + canonical.semantic.as_bytes(), + ); + assert_eq!(measurement.signal(), blue); + } +} + #[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( + let fixture = 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 expected_semantic = fixture.semantic.unwrap(); + let owner = fixture.draft.compile().unwrap(); + let mut session = instantiate_with_family_artifacts(&owner, 32, fixture.artifacts); let black = [Srgb8::new([0; 3])]; let scenarios = [program::ScenarioV1::new(33, &black)]; let evidence = owner @@ -307,7 +439,7 @@ fn report_only_family_violation_is_retained_but_does_not_steer_selection() { }; assert_eq!(measurement.family(), FAMILY); assert_eq!(measurement.signal(), red); - assert_eq!(measurement.content(), expected_family_content); + assert_eq!(measurement.semantic(), expected_semantic); let program::IntrinsicUnaryProofV1::FamilyMembershipViolation = intrinsic.proof() else { panic!("the non-member must retain an exact exclusion witness"); }; @@ -318,14 +450,20 @@ 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]])); + let first_family = family(&[[255, 0, 0]]); + let second_family = family(&[[0, 0, 255]]); + draft.push_family(FAMILY, first_family.semantic); + draft.push_family(SECOND_FAMILY, second_family.semantic); 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 mut session = instantiate_with_family_artifacts( + &owner, + 36, + FamilyArtifactBundleV2::from_artifacts(vec![first_family.artifact, second_family.artifact]), + ); let black = [Srgb8::new([0; 3])]; let scenarios = [program::ScenarioV1::new(37, &black)]; let evidence = owner @@ -364,14 +502,19 @@ fn two_invalid_states_produce_an_exhaustive_conflict_with_exact_family_witnesses 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 family = family(&[[10; 3], [20; 3]]); + let expected_semantic = family.semantic; 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_family(FAMILY, family.semantic); 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 mut session = instantiate_with_family_artifacts( + &owner, + 42, + FamilyArtifactBundleV2::from_artifacts(vec![family.artifact]), + ); let black = [Srgb8::new([0; 3])]; let scenarios = [program::ScenarioV1::new(43, &black)]; FAMILY_MEMBERSHIP_ASSESS_CALLS.with(|calls| calls.set(0)); @@ -415,7 +558,7 @@ fn two_invalid_states_produce_an_exhaustive_conflict_with_exact_family_witnesses panic!("every family violation must retain typed membership measurement"); }; assert_eq!(measurement.family(), FAMILY); - assert_eq!(measurement.content(), expected_family_content); + assert_eq!(measurement.semantic(), expected_semantic); let expected_signal = match cell.state_index() { 0 => below_signal, 1 => between_signal, @@ -433,16 +576,15 @@ 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( + let fixture = 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 owner = fixture.draft.compile().unwrap(); + let mut session = instantiate_with_family_artifacts(&owner, 52, fixture.artifacts); let black = [Srgb8::new([0; 3])]; let scenarios = [program::ScenarioV1::new(53, &black)]; let evidence = owner @@ -470,13 +612,18 @@ fn family_and_declared_set_verdicts( ) -> (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)); + let family = family(admitted_family); + draft.push_family(FAMILY, family.semantic); 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 mut session = instantiate_with_family_artifacts( + &owner, + 61, + FamilyArtifactBundleV2::from_artifacts(vec![family.artifact]), + ); let black = [Srgb8::new([0; 3])]; let scenarios = [program::ScenarioV1::new(62, &black)]; let evidence = owner @@ -562,22 +709,14 @@ fn family_and_declared_point_convention_are_independent_hard_constraints_over_th } } -#[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_family(FAMILY, family(&[[0, 0, 255]]).semantic); + duplicate.push_family(FAMILY, family(&[[255, 0, 0]]).semantic); duplicate.push_intrinsic_family_membership_hard(FAMILY_CONSTRAINT, TARGET, FAMILY); let duplicate_error = match duplicate.compile() { Ok(_) => panic!("a duplicate family must not compile"), @@ -598,7 +737,7 @@ fn duplicate_and_unused_families_are_typed_compile_errors() { 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_family(FAMILY, family(&[[0, 0, 255]]).semantic); unused.push_exact_visible_unary_hard(VISIBLE_CONSTRAINT, OCCURRENCE, blue); let unused_error = match unused.compile() { Ok(_) => panic!("an unused family must not compile"), @@ -620,30 +759,99 @@ fn duplicate_and_unused_families_are_typed_compile_errors() { } #[test] -fn replay_corruption_is_a_typed_invalid_family_image_compile_error() { +fn missing_loaded_artifact_is_rejected_before_a_session_exists() { + FAMILY_MEMBERSHIP_ASSESS_CALLS.with(|calls| calls.set(0)); 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 family = family(&[[0, 0, 255]]); let mut draft = finite_base_draft(&[(candidate, blue, 1.0)]); - draft.push_family(FAMILY, corrupted); + draft.push_family(FAMILY, family.semantic); draft.push_intrinsic_family_membership_hard(FAMILY_CONSTRAINT, TARGET, FAMILY); + draft.push_exact_visible_unary_hard(VISIBLE_CONSTRAINT, OCCURRENCE, blue); + let owner = draft.compile().unwrap(); - let error = match draft.compile() { - Ok(_) => panic!("a corrupted admitted family must fail replay"), - Err(error) => error, + let failure = match owner.instantiate_with_family_artifacts(81, FamilyArtifactBundleV2::empty()) + { + Ok(_) => panic!("semantic declarations require the exact loaded artifact bundle"), + Err(failure) => failure, }; + assert_eq!( - error, - program::CompileErrorV1::InvalidFamilyImage { family: FAMILY }, + failure.cause(), + program::InstantiateErrorV1::FamilyArtifacts(program::FamilyArtifactErrorV2::Missing { + semantic: family.semantic, + }), ); assert_eq!( - error.kind(), - program::CompileErrorKindV1::InvalidFamilyImage + format!("{failure:?}"), + format!( + "InstantiateFailureV2 {{ cause: {:?}, .. }}", + failure.cause(), + ), + "owning instantiate failures must expose only their typed cause", ); assert_eq!( - error.primary_handle(), - Some(program::CompileErrorHandleV1::Family(FAMILY)), + FAMILY_MEMBERSHIP_ASSESS_CALLS.with(core::cell::Cell::get), + 0, + "rejected admission must not reach membership assessment", + ); + let (_, returned) = failure.into_parts(); + let mut artifacts = returned.into_artifacts(); + artifacts.push(family.artifact); + let retry = owner + .instantiate_with_family_artifacts(81, FamilyArtifactBundleV2::from_artifacts(artifacts)); + assert!( + retry.is_ok(), + "adding the missing semantic must repair retry" ); - assert_eq!(error.related_handle(), None); +} + +#[test] +fn required_family_releases_are_unique_semantic_keys_not_opaque_aliases() { + let candidate = program::TargetCandidateIdV1::new(90); + let blue = Srgb8::new([0, 0, 255]); + let shared = family(&[[0, 0, 255]]); + let mut draft = finite_base_draft(&[(candidate, blue, 1.0)]); + draft.push_family(FAMILY, shared.semantic); + draft.push_family(SECOND_FAMILY, shared.semantic); + draft.push_intrinsic_family_membership_hard(FAMILY_CONSTRAINT, TARGET, FAMILY); + draft.push_intrinsic_family_membership_hard( + program::ConstraintIdV1::new(107), + TARGET, + SECOND_FAMILY, + ); + draft.push_exact_visible_unary_hard(VISIBLE_CONSTRAINT, OCCURRENCE, blue); + let owner = draft.compile().unwrap(); + + assert_eq!( + owner.required_family_releases().collect::>(), + vec![shared.semantic], + ); +} + +#[test] +fn session_owns_artifact_generation_until_session_drop_after_owner_release() { + let candidate = program::TargetCandidateIdV1::new(91); + let blue = Srgb8::new([0, 0, 255]); + let loaded = family(&[[0, 0, 255]]); + let semantic = loaded.semantic; + let drops = std::rc::Rc::new(core::cell::Cell::new(0)); + let artifact = loaded.artifact.with_drop_counter_for_test(drops.clone()); + let mut draft = finite_base_draft(&[(candidate, blue, 1.0)]); + draft.push_family(FAMILY, semantic); + draft.push_intrinsic_family_membership_hard(FAMILY_CONSTRAINT, TARGET, FAMILY); + draft.push_exact_visible_unary_hard(VISIBLE_CONSTRAINT, OCCURRENCE, blue); + let owner = draft.compile().unwrap(); + let session = instantiate_with_family_artifacts( + &owner, + 92, + FamilyArtifactBundleV2::from_artifacts(vec![artifact]), + ); + + assert_eq!(drops.get(), 0); + drop(owner); + assert_eq!(session.evidence().kind(), program::StateKindV1::Waiting); + assert_eq!(drops.get(), 0); + drop(session); + assert_eq!(drops.get(), 1); } diff --git a/crates/labcolors-core/src/program_identity.rs b/crates/labcolors-core/src/program_identity.rs index 3b508a1d..f0791788 100644 --- a/crates/labcolors-core/src/program_identity.rs +++ b/crates/labcolors-core/src/program_identity.rs @@ -23,7 +23,9 @@ 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; + #[cfg(test)] pub(super) const FAMILY_CERTIFICATE_VERTEX_V1: u8 = 1; + pub(super) const FAMILY_SEMANTIC_RELEASE_VERTEX_V2: u8 = 2; #[cfg(test)] pub(super) const MODELED_LCS_OCCURRENCE_V1: u8 = 1; #[cfg(test)] @@ -58,7 +60,9 @@ mod release_tag { 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; + #[cfg(test)] pub(super) const FAMILY_MEMBERSHIP_RELEASE_V1: u8 = 1; + pub(super) const FAMILY_MEMBERSHIP_RELEASE_V2: u8 = 2; pub(super) const FAMILY_MEMBERSHIP_CAPABILITY_V1: u8 = 1; #[cfg(test)] pub(super) const EXACT_SRGB8_IDENTITY_MUTATION_SENTINEL_V1: u8 = 2; @@ -417,11 +421,11 @@ fn source_color(source: Source) -> Result { Ok(color) } -fn family_color(family: &FamilyDeclarationV1) -> Result { - let identity = family.set().certificate().family_content_identity(); +fn family_color(family: &FamilyDeclarationV2) -> Result { + let semantic = family.semantic(); 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(release_tag::FAMILY_SEMANTIC_RELEASE_VERTEX_V2)?; + for byte in semantic.as_bytes() { color.push_u8(*byte)?; } Ok(color) @@ -605,8 +609,8 @@ fn constraint_color( } })?; color.push_u8(match release { - crate::constraints::FamilyMembershipReleaseV1::V1 => { - release_tag::FAMILY_MEMBERSHIP_RELEASE_V1 + crate::constraints::FamilyMembershipReleaseV2::V2 => { + release_tag::FAMILY_MEMBERSHIP_RELEASE_V2 } })?; color.push_u8(match capability { @@ -1746,20 +1750,20 @@ 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); + fn family_vertex_codec_binds_semantic_release_without_opaque_id_or_artifact_receipt() { + assert_eq!(release_tag::FAMILY_CERTIFICATE_VERTEX_V1, 1); + assert_eq!(release_tag::FAMILY_MEMBERSHIP_RELEASE_V1, 1); + assert_eq!(release_tag::FAMILY_SEMANTIC_RELEASE_VERTEX_V2, 2); + assert_eq!(release_tag::FAMILY_MEMBERSHIP_RELEASE_V2, 2); + let expected = crate::family::SemanticFamilyReleaseIdV2::from_digest([0xA5; 32]); + let family = FamilyDeclarationV2::new(FamilyId::new(u32::MAX), expected); 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 + release_tag::FAMILY_SEMANTIC_RELEASE_VERTEX_V2 ); assert_eq!(&color.as_slice()[2..], expected.as_bytes()); } diff --git a/crates/labcolors-core/src/program_identity_tests.rs b/crates/labcolors-core/src/program_identity_tests.rs index 40387c11..c67f0314 100644 --- a/crates/labcolors-core/src/program_identity_tests.rs +++ b/crates/labcolors-core/src/program_identity_tests.rs @@ -3,7 +3,10 @@ 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::family::{ + FamilyDeclarationV2, FamilyDefinitionDigestV2, FamilyId, canonical_family_image_digest_v2, + semantic_family_release_id_v2, +}; use crate::lcs_occurrence::{ AdaptingLuminanceCdM2, AppearanceContextId, AppearanceContextSchemaReleaseId, BackgroundLuminanceRatio, ColorSignal, IEC_SRGB_D65_XYZ_FRAME_V1, SurroundProfileId, @@ -50,11 +53,18 @@ 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 declared_family(id: FamilyId, members: &[[u8; 3]]) -> FamilyDeclarationV2 { + let mut members = members.iter().copied().map(signal).collect::>(); + members.sort_unstable_by_key(|member| member.srgb8().bytes()); + members.dedup_by_key(|member| member.srgb8().bytes()); + let image = canonical_family_image_digest_v2( + crate::lcs_occurrence::OutputProfileId::Iec61966Srgb8D65V1, + &members, ) + .unwrap(); + let count = u64::try_from(members.len()).unwrap(); + let definition = FamilyDefinitionDigestV2::from_fixture_bytes_v2(b"identity-test-family"); + FamilyDeclarationV2::new(id, semantic_family_release_id_v2(definition, image, count)) } fn family_identity_program( @@ -1016,8 +1026,8 @@ fn complete_program_schema_v7_digest_is_cross_platform_golden() { assert_eq!( compiled.content_identity().as_bytes(), &[ - 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, + 7, 48, 234, 107, 255, 142, 19, 44, 100, 188, 151, 23, 132, 32, 125, 8, 223, 146, 164, + 87, 8, 134, 38, 127, 130, 32, 197, 17, 118, 227, 166, 95, ] ); } diff --git a/crates/labcolors-core/src/program_session.rs b/crates/labcolors-core/src/program_session.rs index 02c14a82..c09bdaca 100644 --- a/crates/labcolors-core/src/program_session.rs +++ b/crates/labcolors-core/src/program_session.rs @@ -52,7 +52,11 @@ use crate::constraints::{ ProgramPointTargetV1, ProgramVisiblePointBindingV1, ProgramVisiblePointPassEvidence, ProgramVisiblePointViolationEvidence, Wcag22Srgb8V1, assess_program_point_hard, }; -use crate::family::{FamilyDeclarationV1, FamilyId}; +use crate::family::{FamilyDeclarationV2, FamilyId}; +use crate::family_artifact::{ + BoundFamilyArtifactBundleV2, FamilyArtifactBindErrorV2, FamilyArtifactBundleV2, + FamilyArtifactContractErrorV2, FamilyExecutionBindingsV2, +}; use crate::joint::{ AdmittedFiniteJointOrderV1, FiniteDomainOrdinalV1, FiniteJointOrderAdmissionErrorV1, FiniteJointOrderErrorV1, NonEmptyFiniteDomainCardinalitiesV1, admit_finite_joint_order_v1, @@ -954,7 +958,7 @@ where { sources: Vec, targets: Vec, - families: Vec, + families: Vec, joint_selection: Option, observation_group: ObservationGroup, opacities: Vec, @@ -1012,7 +1016,7 @@ where self } - pub(crate) fn with_families(mut self, families: Vec) -> Self { + pub(crate) fn with_families(mut self, families: Vec) -> Self { self.families = families; self } @@ -1080,7 +1084,7 @@ impl CoreProgramDraftV1 { self.program.targets.push(target); } - pub(crate) fn push_family(&mut self, family: FamilyDeclarationV1) { + pub(crate) fn push_family(&mut self, family: FamilyDeclarationV2) { self.program.families.push(family); } @@ -1261,9 +1265,6 @@ pub enum ProgramCompileError { DuplicateFamily { family: FamilyId, }, - InvalidFamilyImage { - family: FamilyId, - }, UnusedFamily { family: FamilyId, }, @@ -1898,7 +1899,8 @@ where { content_identity: ProgramContentIdentityV7, evaluator: Evaluation, - families: Box<[FamilyDeclarationV1]>, + families: Box<[FamilyDeclarationV2]>, + required_family_releases: Box<[crate::family::SemanticFamilyReleaseIdV2]>, graph: CompiledAppearanceGraph, binding_template: AdmittedAppearanceBindings, observation_group: CompiledObservationGroupV1, @@ -2054,6 +2056,13 @@ where self.owner_generation.outputs.len() } + /// Unique semantic artifacts required by this Program, in canonical order. + /// A trusted host registry supplies the full certificate for each release; + /// client-owned FamilyId never participates in that transport lookup. + pub(crate) fn required_family_releases(&self) -> &[crate::family::SemanticFamilyReleaseIdV2] { + &self.owner_generation.required_family_releases + } + pub(crate) fn evidence_cell_bounds(&self, scenario_count: usize) -> Option<(usize, usize)> { checked_program_epoch_evaluation_cell_counts(&self.owner_generation, scenario_count) .map(|counts| (counts.selected, counts.exhaustive_conflict)) @@ -2094,19 +2103,76 @@ where &self, stream: ObservationStreamId, ) -> Result>, ProgramSessionInstantiateError> { + self.instantiate_with_family_artifacts(stream, FamilyArtifactBundleV2::empty()) + .map_err(|failure| failure.cause) + } + + /// Создаёт Session и перемещает в неё exact artifact generation. + /// + /// Program остаётся semantic-only; при любом cold failure тот же loaded + /// bundle возвращается вызывающему коду без reload/decode. + pub(crate) fn instantiate_with_family_artifacts( + &self, + stream: ObservationStreamId, + family_artifacts: FamilyArtifactBundleV2, + ) -> Result>, ProgramSessionInstantiateFailureV2> { + let family_artifacts = match family_artifacts.bind(&self.owner_generation.families) { + Ok(bound) => bound, + Err(failure) => { + let (cause, family_artifacts) = failure.into_parts(); + let cause = match cause { + FamilyArtifactBindErrorV2::Contract(cause) => { + ProgramSessionInstantiateError::FamilyArtifacts(cause) + } + FamilyArtifactBindErrorV2::ResourceExhausted => { + ProgramSessionInstantiateError::ResourceExhausted + } + }; + return Err(ProgramSessionInstantiateFailureV2 { + cause, + family_artifacts, + }); + } + }; let bindings = self .owner_generation .binding_template .try_clone_v1() - .map_err(map_session_instantiate_error)?; + .map_err(map_session_instantiate_error); + let bindings = match bindings { + Ok(bindings) => bindings, + Err(cause) => { + return Err(ProgramSessionInstantiateFailureV2 { + cause, + family_artifacts: family_artifacts.into_unbound(), + }); + } + }; let workspace = self .owner_generation .graph .new_workspace() - .map_err(map_session_instantiate_error)?; + .map_err(map_session_instantiate_error); + let workspace = match workspace { + Ok(workspace) => workspace, + Err(cause) => { + return Err(ProgramSessionInstantiateFailureV2 { + cause, + family_artifacts: family_artifacts.into_unbound(), + }); + } + }; let presentation_cache = - ProgramPresentationCacheV1::try_new(&self.owner_generation.point_presentations) - .map_err(|()| ProgramSessionInstantiateError::ResourceExhausted)?; + ProgramPresentationCacheV1::try_new(&self.owner_generation.point_presentations); + let presentation_cache = match presentation_cache { + Ok(cache) => cache, + Err(()) => { + return Err(ProgramSessionInstantiateFailureV2 { + cause: ProgramSessionInstantiateError::ResourceExhausted, + family_artifacts: family_artifacts.into_unbound(), + }); + } + }; Ok(Session::new( stream, ProgramSessionPlan { @@ -2115,6 +2181,7 @@ where workspace, presentation_cache, evaluation_arenas: ProgramEvaluationArenaPoolV1::new(), + family_artifacts, }, )) } @@ -2125,6 +2192,19 @@ where pub enum ProgramSessionInstantiateError { ResourceExhausted, InternalInvariant, + FamilyArtifacts(FamilyArtifactContractErrorV2), +} + +/// Failed Session construction returns the same admitted artifact storage. +pub(crate) struct ProgramSessionInstantiateFailureV2 { + cause: ProgramSessionInstantiateError, + family_artifacts: FamilyArtifactBundleV2, +} + +impl ProgramSessionInstantiateFailureV2 { + pub(crate) fn into_parts(self) -> (ProgramSessionInstantiateError, FamilyArtifactBundleV2) { + (self.cause, self.family_artifacts) + } } fn map_session_instantiate_error(error: BindingError) -> ProgramSessionInstantiateError { @@ -3629,6 +3709,19 @@ where workspace: AppearanceWorkspace, presentation_cache: ProgramPresentationCacheV1, evaluation_arenas: ProgramEvaluationArenaPoolV1, + // Последнее owning-поле Plan: Session сначала уничтожает evidence/arenas, + // затем executable artifact storage этой generation. + family_artifacts: BoundFamilyArtifactBundleV2, +} + +impl Session> +where + Evaluation: ProgramConstraintEvaluatorSetV1, + ProgramConstraintInvocationOf: Copy, +{ + pub(crate) fn family_execution_bindings(&self) -> FamilyExecutionBindingsV2<'_> { + self.plan().family_artifacts.execution_bindings() + } } /// Mutable execution state не владеет arena: guard удерживает непересекающееся @@ -3637,6 +3730,7 @@ struct ProgramEvaluationRuntimeV1<'plan> { bindings: &'plan mut AdmittedAppearanceBindings, workspace: &'plan mut AppearanceWorkspace, presentation_cache: &'plan mut ProgramPresentationCacheV1, + family_artifacts: &'plan BoundFamilyArtifactBundleV2, } impl session_private::PlanSealed for ProgramSessionPlan @@ -3706,6 +3800,7 @@ where workspace, presentation_cache, evaluation_arenas, + family_artifacts, } = plan; let mut arena = evaluation_arenas .guard(slot) @@ -3714,6 +3809,7 @@ where bindings, workspace, presentation_cache, + family_artifacts, }; let scenario_set = NonEmptyScenarioSetV1::from_admitted(&observation) .ok_or(ProgramSessionEvaluationError::InternalInvariant)?; @@ -4321,7 +4417,7 @@ where value, }; let (measurement, decision) = invocation - .assess(value.source(), &epoch.families) + .assess(value.source(), runtime.family_artifacts) .ok_or(ProgramSessionEvaluationError::InternalInvariant)?; let (result, is_violation) = match decision { HardDecision::Pass(proof) => ( @@ -4667,7 +4763,8 @@ where .checked_len() .ok_or(ProgramCompileError::ResourceExhausted)?; canonicalize_sources_and_targets(&mut program)?; - canonicalize_and_verify_families(&mut program.families)?; + canonicalize_families(&mut program.families)?; + let required_family_releases = canonical_required_family_releases(&program.families)?; let graph = lower_graph(&program)? .compile() @@ -4731,6 +4828,7 @@ where content_identity, evaluator: program.evaluator, families, + required_family_releases, graph, binding_template, observation_group: CompiledObservationGroupV1 { @@ -4746,10 +4844,21 @@ where }) } -fn canonicalize_and_verify_families( - families: &mut Vec, -) -> Result<(), ProgramCompileError> { - families.sort_unstable_by_key(FamilyDeclarationV1::id); +fn canonical_required_family_releases( + families: &[FamilyDeclarationV2], +) -> Result, ProgramCompileError> { + let mut releases = Vec::new(); + releases + .try_reserve_exact(families.len()) + .map_err(|_| ProgramCompileError::ResourceExhausted)?; + releases.extend(families.iter().map(|family| family.semantic())); + releases.sort_unstable(); + releases.dedup(); + Ok(releases.into_boxed_slice()) +} + +fn canonicalize_families(families: &mut [FamilyDeclarationV2]) -> Result<(), ProgramCompileError> { + families.sort_unstable_by_key(|family| family.id()); if let Some(family) = families .windows(2) .find(|pair| pair[0].id() == pair[1].id()) @@ -4757,19 +4866,11 @@ fn canonicalize_and_verify_families( { 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], + families: &[FamilyDeclarationV2], constraints: &[CompiledPointConstraint], ) -> Result<(), ProgramCompileError> { let mut used = false_slots(families.len())?; @@ -5656,7 +5757,7 @@ where CoreIntrinsicUnaryInvocationV1::FamilyMembership { family } => { let family_index = program .families - .binary_search_by_key(&family, FamilyDeclarationV1::id) + .binary_search_by_key(&family, |declaration| declaration.id()) .map_err(|_| ProgramCompileError::MissingConstraintFamily { constraint: constraint.id, family, diff --git a/crates/labcolors-core/src/session.rs b/crates/labcolors-core/src/session.rs index 61b25a5f..2f6235a1 100644 --- a/crates/labcolors-core/src/session.rs +++ b/crates/labcolors-core/src/session.rs @@ -451,11 +451,13 @@ fn publish_session_transition<'session, Plan: SessionPlanV1>( /// after commit or rollback. pub(crate) struct Session { stream: ObservationStreamId, - plan: Plan, observation_arenas: ObservationArenaPoolV1, raw_head: SessionObservationHeadV1, state: SessionState, deferred_retirement: Option>, + // Plan может владеть большим executable artifact storage. Evidence и + // retirement должны освободиться раньше него, поэтому Plan всегда последний. + plan: Plan, } impl Session { @@ -467,11 +469,11 @@ impl Session { drop(owner); Self { stream, - plan, observation_arenas, raw_head: SessionObservationHeadV1::Empty, state: SessionState::Waiting, deferred_retirement: None, + plan, } } diff --git a/scripts/verify_point_support_surplus.py b/scripts/verify_point_support_surplus.py index 810a0e0d..61d3e1d6 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 = ( - "da7ae91894b26c300b14df3b5c858bf238aeaba1d46a6c946a08550dc02aa74f" + "faef3711ffbfeb823e7187996d2f2a06171dea273fab2cd0aa89a56d6fefc95d" ) EXPECTED_Q55_PROOF_SHA256 = ( "ac59cf89503170c789223b91d775213a19d4e571ef930f2ea609fcd51b14defd"