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 76a85c86..fe9aa3a5 100644 --- a/crates/labcolors-core/contracts/clean-set-srgb8-v1/receipt-v1.json +++ b/crates/labcolors-core/contracts/clean-set-srgb8-v1/receipt-v1.json @@ -6,11 +6,11 @@ }, "artifacts": [ { - "bytes": 84737, + "bytes": 85430, "license": "MIT", "path": "crates/labcolors-core/src/appearance.rs", "role": "appearance_executor_source", - "sha256": "2302258dec70a76acbb4ac5c3a1a472997716c3e58fc7bc1b9aaeb105526a709" + "sha256": "019899f04cf91a29e4710609cd8d8c334784833a0972204cfedb179517a1e732" }, { "bytes": 6373, @@ -62,11 +62,32 @@ "sha256": "8c9838107077775c51d80638ba0b59f9672d14347ca404dfd4c63e2fb62d1c45" }, { - "bytes": 173158, + "bytes": 38023, + "license": "MIT", + "path": "crates/labcolors-core/src/program/attachment.rs", + "role": "point_attachment_source", + "sha256": "ea0774ed2719c4a5642ddf053b8fcb60ad10ee056b102b26e5884ab57febbe71" + }, + { + "bytes": 17072, + "license": "MIT", + "path": "crates/labcolors-core/src/program/attachment/support.rs", + "role": "point_attachment_test_support", + "sha256": "21afe869e4b0e9abacb266ee92dea893a98c48f7da4f58a352ab5b8caca8f231" + }, + { + "bytes": 28964, + "license": "MIT", + "path": "crates/labcolors-core/src/program/attachment/tests.rs", + "role": "point_attachment_tests", + "sha256": "d52f10afab0765e3cfced403b32e9c1294bed152b8f3f6b831c972da90b4f2e2" + }, + { + "bytes": 162073, "license": "MIT", "path": "crates/labcolors-core/src/program.rs", "role": "program_facade_source", - "sha256": "b93e1d066d03d28abc49bdfd492025da7c8fd8531fd19d8ab29526aeeb29a7ae" + "sha256": "1f8e732bf70876cbc69ddfaf2503a682bdf1e2881f3655961386c2472d875976" }, { "bytes": 71522, @@ -76,18 +97,18 @@ "sha256": "8f0366079e6fa0006360ab19b0449e622add48fdb08431a02258236e1371d78a" }, { - "bytes": 154789, + "bytes": 159952, "license": "MIT", "path": "crates/labcolors-core/src/program_session.rs", "role": "program_source", - "sha256": "a236381dcf80760e1ea39743b3a20d10de938fe1a9d6c64e9a9075f6265b6eb3" + "sha256": "4768f1dbfd564b1f1b21bbb02d20cc0213a2f12ebca9a9939129cf62fdd9fd3b" }, { - "bytes": 21649, + "bytes": 21546, "license": "MIT", "path": "crates/labcolors-core/src/program_clean_set_tests.rs", "role": "program_tests", - "sha256": "30bbb97208b428d2ca0e5a230bffa0e2a8e1ede5b0b071979c7a7b2590f57d93" + "sha256": "44c30c5e10494dec4d6865e4597cf9f5e616955eff8279bb606d1f5a9f3ce35f" }, { "bytes": 11370, @@ -97,11 +118,11 @@ "sha256": "aa6aa7c0b630437f1c1ba8c2ceafb0dadf6551c42331559504076a6cd44e6331" }, { - "bytes": 15043, + "bytes": 21061, "license": "MIT", "path": "crates/labcolors-core/src/session.rs", "role": "session_runtime_source", - "sha256": "4c24ffb02ac5909d8436410ba233aec8abd99156e55ac9d65232ca614de92905" + "sha256": "faf5bdffa7bc82a17d4ead99d34f1a22c24b0b511e253e8280ded86629c24e2e" }, { "bytes": 34105, @@ -118,18 +139,18 @@ "sha256": "6c95324eb05476f35f75375a9af0b2b4a41b8b2978c46e67d2ce1aea5adde342" }, { - "bytes": 39302, + "bytes": 39567, "license": "MIT", "path": "scripts/verify_clean_set_receipt.py", "role": "verifier_source", - "sha256": "c6d9025235c1c53b54ce4c7b231fb02a412d5505740f82cdbba42f80ea375472" + "sha256": "6e07ec6d9d72e5cfe084773b27e45cf6ea7df8c98eca860d19f8646a893afd62" }, { - "bytes": 31194, + "bytes": 31977, "license": "MIT", "path": "scripts/test_verify_clean_set_receipt.py", "role": "verifier_tests", - "sha256": "2831e67431294c28ba60753a892d0ad5e3ff972144d483500eeb10c47bb9d760" + "sha256": "0c54a269c5412baf24f393c6d38a695ad0143d7d1e62fdf0ce8da2a15132dbcd" } ], "excluded_claims": [ 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 0cf5f9a5..e7fd80b6 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 @@ -e55be72b39800c75b3009378734089b2474041e8598557e3ac3f82dbe67dc985 receipt-v1.json +d561d07320fd62fc7287aff30c630f1ee2cd05745cf15c1421951137975e42c7 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 8b7d8d3c..c1b22c4e 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":"ac1d1b9432192035bf8dd4c10e5696b374f07fa1a4a5f8a99a91c2495791ddbd","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":"3cc4aad62ebd85681e4cef6a15fb52ddf4fae9af06b8a35715d3fe6cf9fec173","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":"2302258dec70a76acbb4ac5c3a1a472997716c3e58fc7bc1b9aaeb105526a709"},{"kind":"rust-source","path":"crates/labcolors-core/src/composition.rs","sha256":"195a67327a3bd86d7816b634481389930bf68577bb1202fad14c2ea152df8625"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/exact.rs","sha256":"892576a8621185352583e63dc0a1aacac32e32a8063b6fe24ae16d4ff9dce7cb"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/mod.rs","sha256":"aba84c05a203af12ef2e445334409d9bd385a854c9058f0e30bbf8542addddbc"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/wcag22.rs","sha256":"856093c91159d8b3faab001f2d6524d33d7b16458a5a4e98ea65f8c62ab2694c"},{"kind":"rust-source","path":"crates/labcolors-core/src/hash.rs","sha256":"f97a0fd7d6ad3162f0f1dfb326fccfb7ed40da9a8fa67a5b8a239a1ae2ae49c3"},{"kind":"rust-source","path":"crates/labcolors-core/src/lcs_occurrence.rs","sha256":"78d37406e9bdc37f126b72987c9c92b452c13b3233c0aeb0a75ed25dadb83a68"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"b30300edd3910e3d9da1e56896ce14c3db508b1a6fc5608e01032a5ad95777b1"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"8c9838107077775c51d80638ba0b59f9672d14347ca404dfd4c63e2fb62d1c45"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"6f6a376ff036d3d65960c004e6566e1bca580f19f5bd3cd333a80b0da5b5c242"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"4c24ffb02ac5909d8436410ba233aec8abd99156e55ac9d65232ca614de92905"},{"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":"47534251babef1241b48509f11de74c492a571f16153e0685460a14bbb25e16a"} +{"artifact_id":"wcag22-srgb8-luminance-q55-v1","basis_point_proof":{"checks":30,"drop_all_semantics":"zero required surplus; current must still meet the anchor","drop_domain_inclusive":[0,10000],"nonpositive_baseline_semantics":"zero required surplus; current must meet the anchor"},"bound_id":"point-support-reference-surplus-q55-bps-v1","certified_claim":"for every successfully evaluated enabled stability cell, decision is Retained iff current_lower_surplus >= (10000-drop_bps)/10000 * max(baseline_lower_surplus,0); the declared anchor remains a separate hard floor","comparator_proof":{"algorithm":"euclidean-continued-fraction-ordering-v1","dense_denominator_inclusive":[1,31],"dense_numerator_inclusive":[0,31],"dense_small_cases":984064,"invariant":"equal integer parts; reciprocal proper fractions reverse order","largest_fibonacci_index":186,"oracle":"unbounded-integer-cross-product","random_cases":250000,"random_corpus_sha256":"97c4af7b452b31a4ab92645f70c17acb38bf57ca55484e32ad9d7d79d97a333d","random_seed":210583930,"termination":"each nonterminal denominator becomes a strictly smaller remainder","u128_adversarial_cases":190},"declared_operation_law":"q55-lower-reference-distance-explicit-anchor-bps-retention-v1","excluded_claim":"does not certify retention against the unknown exact baseline surplus, renderer equivalence outside encoded-sRGB8 source-over, or a successful result when evaluation fails","integer_replay_envelope":{"assumption":"every Q55 luminance upper <= scale + 3","i128_max":170141183460469231731687303715884105727,"offset_cleared_denominator_max":756604737398243388,"positive_baseline_numerator_max":1188950301625811064,"rational_denominator_max":1513209474796486776,"required_denominator_max":15132094747964867760000,"required_numerator_max":11889503016258110640000,"signed_anchor_abs_coarse_max":5296233161787703716,"u128_max":340282366920938463463374607431768211455,"u64_max":18446744073709551615},"profile_id":"srgb8-q55-retained-reference-surplus-bps-v1","proof_id":"point-support-reference-surplus-integer-v1","proof_payload_sha256":"ebcc08779d73839d6a194ec35c772071ea999d68f64a2a31efe8d81811b82c98","q55_dependency":{"artifact_id":"wcag22-srgb8-luminance-q55-v1","artifact_sha256":"7ff239d9052b346f3c50da01ca65ca2330892ed1a3ff30e190797fcef6f03604","maximum_luminance_upper":36028797018963971,"outward_interval_width_bound":3,"proof_id":"wcag22-srgb8-full-domain-q55-v1","proof_payload_sha256":"3c639a7c875046c46b56b51ecdd67d5ecaf14a1134490c88a222e7037b63c0f2","proof_sha256":"ac59cf89503170c789223b91d775213a19d4e571ef930f2ea609fcd51b14defd","q55_scale":36028797018963968},"reference_and_anchor_proof":{"anchor_identity_checks":75,"orientation_law":"distance-magnitude-symmetric-orientation-reported-separately","overlap_lower_distance":"0/1","separated_endpoint_checks":504},"schema_version":2,"site_id":"point-support-retained-reference-surplus-v1","source_binding_exclusions":["whole-crate compilation or compiler/toolchain attestation","binary, package, FFI, renderer, or browser transport attestation","unrelated Lab Colors modules outside the declared point-support semantic cone"],"source_binding_law":"point-support-rust-whole-file-semantic-cone-v2","source_binding_schema_version":2,"source_binding_scope":"exact bytes of the private point-support Rust semantic cone and its two WCAG include_str inputs; comments and cfg(test) text are intentionally significant","source_closure_sha256":"838315cee50b75ff823cdb76c2ed5a72667a4b6a688a11dfc29b9eb8a5bc459c","source_files":[{"kind":"compile-time-input","path":"crates/labcolors-core/contracts/wcag22-srgb8-q55-proof-v1.json","sha256":"ac59cf89503170c789223b91d775213a19d4e571ef930f2ea609fcd51b14defd"},{"kind":"compile-time-input","path":"crates/labcolors-core/contracts/wcag22-srgb8-v1.json","sha256":"b4bb7e5f17a99f2c911fdbe3da23a48b049277b796291094950f14680cc3cc7b"},{"kind":"rust-source","path":"crates/labcolors-core/src/appearance.rs","sha256":"019899f04cf91a29e4710609cd8d8c334784833a0972204cfedb179517a1e732"},{"kind":"rust-source","path":"crates/labcolors-core/src/composition.rs","sha256":"195a67327a3bd86d7816b634481389930bf68577bb1202fad14c2ea152df8625"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/exact.rs","sha256":"892576a8621185352583e63dc0a1aacac32e32a8063b6fe24ae16d4ff9dce7cb"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/mod.rs","sha256":"aba84c05a203af12ef2e445334409d9bd385a854c9058f0e30bbf8542addddbc"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/wcag22.rs","sha256":"856093c91159d8b3faab001f2d6524d33d7b16458a5a4e98ea65f8c62ab2694c"},{"kind":"rust-source","path":"crates/labcolors-core/src/hash.rs","sha256":"f97a0fd7d6ad3162f0f1dfb326fccfb7ed40da9a8fa67a5b8a239a1ae2ae49c3"},{"kind":"rust-source","path":"crates/labcolors-core/src/lcs_occurrence.rs","sha256":"78d37406e9bdc37f126b72987c9c92b452c13b3233c0aeb0a75ed25dadb83a68"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"b30300edd3910e3d9da1e56896ce14c3db508b1a6fc5608e01032a5ad95777b1"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"8c9838107077775c51d80638ba0b59f9672d14347ca404dfd4c63e2fb62d1c45"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"6f6a376ff036d3d65960c004e6566e1bca580f19f5bd3cd333a80b0da5b5c242"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"faf5bdffa7bc82a17d4ead99d34f1a22c24b0b511e253e8280ded86629c24e2e"},{"kind":"rust-source","path":"crates/labcolors-core/src/srgb8.rs","sha256":"6c95324eb05476f35f75375a9af0b2b4a41b8b2978c46e67d2ce1aea5adde342"},{"kind":"rust-source","path":"crates/labcolors-core/src/wcag22.rs","sha256":"7ba7864eb7e73789bad6c63c64a4dc2dcc08c2da6921375fb9564fca230c2780"},{"kind":"rust-source","path":"crates/labcolors-core/src/wcag22/kernel.rs","sha256":"c97980c1ca2c7ea9cabff9c8d2fb7282773cca180ae15948391c29c9d6196040"},{"kind":"rust-source","path":"crates/labcolors-core/src/wcag22/q55_data.rs","sha256":"af4d23d6b70c45ce6efa839e7dda4bb0a61f6aae43cb805af6fa9b29e6c3bae2"},{"kind":"rust-source","path":"crates/labcolors-core/src/wcag22_evidence.rs","sha256":"3c5a75b07254c6071a64700af208a64987d0f0ea9698eadc54a9e74585ce1f72"}],"source_negative_controls":43,"universal_algebraic_certificate":{"basis_point_scale_instantiation":10000,"domain":"integers; Q55 scale Q>0; anchor L>=D>=0; lighter monotonicity L2>=L1>D>=0; darker monotonicity L>D2>=D1>=0; current/baseline denominators b,q>0; basis-point scale B>0 instantiated as 10000; p>0; a>=0; 0<=drop_bps<=B","identities":["three explicit anchor-surplus formulas after denominator clearing","reference distance is monotone increasing in lighter L","reference distance is monotone decreasing in darker D","positive-baseline retained threshold is p*(B-drop)/(q*B)","a/b >= p*(B-drop)/(q*B) iff a*q*B >= p*(B-drop)*b"],"method":"exact-sparse-integer-polynomial-identities-plus-positive-denominator-order-lemma-v1","nonpositive_baseline_case":"max(baseline,0)=0; retained threshold is exactly zero","symbolic_mutation_controls":{"anchor_coefficients_and_denominator":6,"retained_cross_product":5},"wolfram_language_cross_check":{"query":"FullSimplify[{20 g/d - 0 == 20 g/d, 20 g/d - 2 == (20 g - 2 d)/d, 20 g/d - 7/2 == (40 g - 7 d)/(2 d), Equivalent[a/b >= p (s-x)/(q s), a q s >= p (s-x) b], Max[p/q, 0] (s-x)/s == Piecewise[{{0, p <= 0}}, p (s-x)/(q s)]}, Assumptions -> Element[{a,b,p,q,s,x,g,d}, Integers] && a >= 0 && b > 0 && q > 0 && s > 0 && 0 <= x <= s && d > 0 && g >= 0]","query_sha256":"8cdbb9964583030c8b92498961896cb2a98613f1cb31eb7c54acdf8e16beff10","result":"{True, True, True, True, True}","result_sha256":"13a8f2ee8d0fde335a638e46d7cc8a8427b9a1437c77d22cfcf925bb87fa6303"}},"verifier_sha256":"eb6b342c6a60a6ef38680f7145928bef2ca80b1049438946f1c6936b885499b0"} diff --git a/crates/labcolors-core/src/appearance.rs b/crates/labcolors-core/src/appearance.rs index c2816d1d..4602cb35 100644 --- a/crates/labcolors-core/src/appearance.rs +++ b/crates/labcolors-core/src/appearance.rs @@ -1935,6 +1935,19 @@ impl CompiledAppearanceGraph { Some(CompiledOccurrenceSlotV1 { index, id }) } + /// Находит объявленный subject Paint одного канонического Occurrence. + /// + /// Cold compiler lookup не читает вычисленные значения и не создаёт + /// runtime-состояние. Hot path сохраняет полученный Paint ID в закрытом + /// compiled binding вместо повторного поиска. + pub(crate) fn occurrence_subject(&self, id: OccurrenceId) -> Option { + let index = self + .occurrences + .binary_search_by_key(&id, |occurrence| occurrence.id) + .ok()?; + Some(self.occurrences[index].subject_id) + } + /// Создаёт полномочие только для `Occurrence`, который не потребляется /// другим `Occurrence` этого точечного графа: промежуточный слой нельзя /// принять за финальный моделируемый результат. diff --git a/crates/labcolors-core/src/appearance_graph_tests.rs b/crates/labcolors-core/src/appearance_graph_tests.rs index f8fba1ac..afb9af03 100644 --- a/crates/labcolors-core/src/appearance_graph_tests.rs +++ b/crates/labcolors-core/src/appearance_graph_tests.rs @@ -351,6 +351,24 @@ fn compiled_slots_have_canonical_ordinals_across_declaration_permutations() { } } +#[test] +fn occurrence_subject_is_an_exact_cold_lookup_across_declaration_permutations() { + let canonical = slot_component(false).compile().unwrap(); + let reversed = slot_component(true).compile().unwrap(); + + for (occurrence, subject) in [ + (FILL_OCCURRENCE, FILL_PAINT), + (OTHER_OCCURRENCE, SOLID_PAINT), + ] { + assert_eq!(canonical.occurrence_subject(occurrence), Some(subject)); + assert_eq!(reversed.occurrence_subject(occurrence), Some(subject)); + } + assert_eq!( + canonical.occurrence_subject(OccurrenceId::new(u32::MAX)), + None + ); +} + #[test] fn evaluation_view_rejects_same_ordinal_slots_with_different_nominal_ids() { let compile_single = |paint, occurrence| { diff --git a/crates/labcolors-core/src/generic_boundary_tests.rs b/crates/labcolors-core/src/generic_boundary_tests.rs index 4cc4fd53..0c7093a2 100644 --- a/crates/labcolors-core/src/generic_boundary_tests.rs +++ b/crates/labcolors-core/src/generic_boundary_tests.rs @@ -17,6 +17,7 @@ const LIB_SOURCE: &str = include_str!("lib.rs"); const LCS_OCCURRENCE_SOURCE: &str = include_str!("lcs_occurrence.rs"); const OBSERVATION_SOURCE: &str = include_str!("observation.rs"); const OUTPUT_PROJECTION_SOURCE: &str = include_str!("output_projection.rs"); +const PROGRAM_ATTACHMENT_SOURCE: &str = include_str!("program/attachment.rs"); const PROGRAM_SOURCE: &str = include_str!("program.rs"); const POINT_SUPPORT_SOURCE: &str = include_str!("point_support.rs"); const PROGRAM_IDENTITY_SOURCE: &str = include_str!("program_identity.rs"); @@ -24,15 +25,17 @@ const PROGRAM_SESSION_SOURCE: &str = include_str!("program_session.rs"); const SESSION_SOURCE: &str = include_str!("session.rs"); const WCAG22_CONSTRAINT_SOURCE: &str = include_str!("constraints/wcag22.rs"); -const GENERIC_SOURCES: [(&str, &str); 4] = [ +const GENERIC_SOURCES: [(&str, &str); 5] = [ ("appearance.rs", APPEARANCE_SOURCE), ("lcs_occurrence.rs", LCS_OCCURRENCE_SOURCE), + ("program/attachment.rs", PROGRAM_ATTACHMENT_SOURCE), ("program_identity.rs", PROGRAM_IDENTITY_SOURCE), ("program_session.rs", PROGRAM_SESSION_SOURCE), ]; const CLEAN_SET_PROGRAM_SOURCES: &[(&str, &str)] = &[ ("clean_set.rs", CLEAN_SET_SOURCE), + ("program/attachment.rs", PROGRAM_ATTACHMENT_SOURCE), ("program.rs", PROGRAM_SOURCE), ("program_session.rs", PROGRAM_SESSION_SOURCE), ("program_identity.rs", PROGRAM_IDENTITY_SOURCE), @@ -138,6 +141,7 @@ fn clean_set_program_guard_covers_the_complete_classifier_and_program_path() { [ "clean_set.rs", "program.rs", + "program/attachment.rs", "program_identity.rs", "program_session.rs", ], @@ -355,7 +359,7 @@ fn staged_program_draft_wraps_the_single_canonical_core_graph() { } #[test] -fn staged_session_keeps_evidence_but_owner_alone_grants_updates_and_operations() { +fn staged_session_is_evidence_only_and_retired_operation_authority_cannot_return() { assert_eq!( normalized_source_scope( PROGRAM_SOURCE, @@ -402,7 +406,7 @@ fn staged_session_keeps_evidence_but_owner_alone_grants_updates_and_operations() let evidence_api = source_scope( PROGRAM_SOURCE, "impl<'a> EvidenceViewV1<'a> {", - "struct BorrowScopeV1<'owner, 'session>", + "/// Полностью вычисленный, но ещё не опубликованный переход одной Session.", ); for forbidden in [ "fn revision(", @@ -422,18 +426,18 @@ fn staged_session_keeps_evidence_but_owner_alone_grants_updates_and_operations() "pub(crate) struct ScenarioV1<'a> {", ); for required in [ - "pub(crate) fn project<'owner, 'session>(", - "pub(crate) fn prepare_update<'owner, 'session>(", + "pub(crate) fn prepare_update<'session>(", ".owns_session(&session.session)", + "pub(crate) fn instantiate(", ] { assert!( owner_api.contains(required), - "the exact owner must remain the only operation authority; missing `{required}`", + "the evidence-only owner/session seam is incomplete; missing `{required}`", ); } let prepare = source_scope( owner_api, - "pub(crate) fn prepare_update<'owner, 'session>(", + "pub(crate) fn prepare_update<'session>(", "pub(crate) fn instantiate(", ); assert!( @@ -445,10 +449,24 @@ fn staged_session_keeps_evidence_but_owner_alone_grants_updates_and_operations() .expect("owner prepare must delegate one prepared Session transition"), "owner mismatch must be rejected before admission, allocation, or evaluation", ); - assert!( - !owner_api.contains("pub(crate) fn update<'owner, 'session>("), - "Owner must not retain an immediate prepare-and-commit authority", - ); + for forbidden in ["pub(crate) fn project(", "pub(crate) fn update("] { + assert!( + !PROGRAM_SOURCE.contains(forbidden), + "evidence must not regain retired sink authority `{forbidden}`", + ); + } + for retired in [ + "OperationV1", + "SetV1", + "RemoveV1", + "HoldV1", + "BorrowScopeV1", + ] { + assert!( + !contains_rust_identifier(PROGRAM_SOURCE, retired), + "evidence must not regain retired sink authority `{retired}`", + ); + } let session_code = normalized_production_code(SESSION_SOURCE); let program_code = normalized_production_code(PROGRAM_SOURCE); @@ -519,29 +537,50 @@ fn staged_session_keeps_evidence_but_owner_alone_grants_updates_and_operations() "commit must remain infallible move-only publication; found `{forbidden}`", ); } - for required in ["drop(owner);", "*raw_head =", "*state = next_state;"] { + for required in [ + "let (view, retirement) = self.commit_deferred();", + "drop(retirement);", + "mem::replace(raw_head,", + "mem::replace(state, next_state)", + "_owner: owner,", + ] { assert!( core_commit.contains(required), "commit must publish under the pinned owner; missing `{required}`", ); } - let owner_release = core_commit - .find("drop(owner);") - .expect("commit must release its exact owner explicitly"); - for publication in ["*raw_head =", "*state = next_state;"] { + let owner_retirement = core_commit + .find("_owner: owner,") + .expect("deferred commit must park its exact owner"); + for publication in ["mem::replace(raw_head,", "mem::replace(state, next_state)"] { let last_publication = core_commit .rfind(publication) .unwrap_or_else(|| panic!("commit must contain `{publication}`")); assert!( - last_publication < owner_release, - "every `{publication}` path must publish before releasing the exact owner", + last_publication < owner_retirement, + "every `{publication}` path must publish before parking the exact owner", ); } + let deferred_retirement = source_scope( + SESSION_SOURCE, + "pub(crate) struct DeferredSessionRetirement", + "/// Линейный, полностью вычисленный", + ); + let retired_evidence = deferred_retirement + .find("_retired_verified: Option,") + .expect("retirement must own displaced verified evidence"); + let retired_owner = deferred_retirement + .find("_owner: Plan::OwnerLease,") + .expect("retirement must retain the exact owner"); + assert!( + retired_evidence < retired_owner, + "retired evidence must drop before its exact owner", + ); let concrete_prepared = source_scope( PROGRAM_SOURCE, "/// Полностью вычисленный, но ещё не опубликованный переход одной Session.", - "impl<'owner, 'session> PreparedSessionTransitionV1<'owner, 'session>", + "impl<'session> PreparedSessionTransitionV1<'session>", ); let concrete_must_use = "#[must_use = \"commit the prepared transition or drop it intentionally\"]"; @@ -562,25 +601,14 @@ fn staged_session_keeps_evidence_but_owner_alone_grants_updates_and_operations() let concrete_commit = source_scope( PROGRAM_SOURCE, - "impl<'owner, 'session> PreparedSessionTransitionV1<'owner, 'session>", - "/// Проверенная Owner-and-snapshot проекция", + "impl<'session> PreparedSessionTransitionV1<'session>", + "/// Collision-resistant адрес канонического физического содержания Program.", ); assert!( - concrete_commit.contains("session: transition.commit(),") + concrete_commit.contains("session: self.transition.commit(),") && !concrete_commit.contains("Result<") && !concrete_commit.contains("?;"), - "Program commit must only project the already committed Session view", - ); - - let staged_access_errors = source_scope( - PROGRAM_SOURCE, - "pub(crate) enum AccessErrorV1 {", - "impl OwnerV1", - ); - assert!( - staged_access_errors.contains("OwnerMismatch,") - && !staged_access_errors.contains("OwnerExpired"), - "operation projection must distinguish foreign ownership, not expose internal expiry", + "evidence-only commit must only project the already committed Session view", ); let staged_update_errors = source_scope( PROGRAM_SOURCE, @@ -601,27 +629,6 @@ fn staged_session_keeps_evidence_but_owner_alone_grants_updates_and_operations() .contains("fn map_plan_error(error: CoreProgramPlanErrorV1) -> UpdateErrorV1",), "update errors must retain payloads in the authoritative enum before kind projection", ); - - for (payload, end) in [ - ( - "pub(crate) struct SetV1<'owner, 'session> {", - "impl<'session> SetV1<'_, 'session>", - ), - ( - "pub(crate) struct RemoveV1<'owner, 'session> {", - "impl RemoveV1<'_, '_>", - ), - ] { - assert!( - source_scope(PROGRAM_SOURCE, payload, end) - .contains("_scope: BorrowScopeV1<'owner, 'session>,"), - "{payload} must retain both owner and immutable Session borrows", - ); - } - assert!( - !PROGRAM_SOURCE.contains("HoldV1") && !PROGRAM_SOURCE.contains("OperationV1::Hold"), - "past evidence must not become a current emission authority", - ); } #[test] diff --git a/crates/labcolors-core/src/program.rs b/crates/labcolors-core/src/program.rs index 6f967b1d..b09229a0 100644 --- a/crates/labcolors-core/src/program.rs +++ b/crates/labcolors-core/src/program.rs @@ -10,31 +10,16 @@ //! [`DraftV1::compile`] проверяет граф целиком и возвращает [`OwnerV1`] — //! единственного владельца конкретной скомпилированной эпохи. //! -//! [`OwnerV1::instantiate`] создаёт потоковую [`SessionV1`]. На горячем пути -//! [`OwnerV1::prepare_update`] принимает физические сценарии в каноническом -//! порядке входов и возвращает линейный [`PreparedSessionTransitionV1`]. Его -//! Drop не меняет зафиксированные raw head, lifecycle и previous evidence, а -//! consuming commit публикует только уже вычисленные raw head и lifecycle и -//! возвращает [`ProjectionV1`]. Внутренние scratch-буферы могут быть полностью -//! переинициализированы во время prepare; они не являются состоянием Session. -//! Это ещё не commit внешнего sink. Исторические доказательства принадлежат -//! Session, но только тот же Owner разрешает подготовку обновлений и операции. +//! [`OwnerV1::instantiate`] создаёт evidence-only [`SessionV1`] для lint и +//! мониторинга. [`OwnerV1::prepare_update`] возвращает линейный +//! [`PreparedSessionTransitionV1`]: Drop ничего не публикует, а consuming commit +//! меняет только raw head и lifecycle Session. Этот путь не выдаёт sink-authority. //! -//! Состояния проецируются однозначно: -//! -//! | Состояние | Операции | -//! |---|---| -//! | `Waiting` + `Empty` | нет | -//! | `Waiting` + допущенный `Unknown` | `Remove` для каждого выхода | -//! | `Ready` | `Set` для каждого выхода | -//! | `Stale` | `Remove` для каждого выхода | -//! | `Failed` | `Remove` для каждого выхода | -//! -//! Прошлый Verified-сертификат остаётся в evidence для диагностики, но не -//! разрешает эмиссию: он относится к прошлому наблюдению, а не к текущему -//! неизвестному или нарушающему контексту. Непустая сырая голова без текущего -//! Verified-сертификата также отзывает выходы: это закрывает передачу sink от -//! одной Session другой Session того же Owner. +//! Terminal runtime принадлежит [`attachment`]: один Attachment структурно +//! связывает точную compiled generation, Session, полные output→sink и +//! output→presentation bindings и линейный writer lease. Только он может +//! атомарно материализовать или отозвать весь снимок; историческое evidence +//! само по себе такого права не даёт. //! //! [`CertificateV1::Verified`] хранит выбранное состояние, все клетки //! доказательства и сертифицированные Paint outputs. [`CertificateV1::Conflict`] @@ -44,9 +29,10 @@ #![forbid(unreachable_pub)] +/// Транзакционный point-output attachment и его линейный sink-контракт. +pub(crate) mod attachment; + use core::iter::FusedIterator; -use core::marker::PhantomData; -use core::slice; use crate::Srgb8; use crate::appearance::{OccurrenceId, OpacityInputId, PaintId, SurfaceId, SurfaceInputPortId}; @@ -84,7 +70,8 @@ use crate::program_session::{ TargetCandidateV1 as CoreTargetCandidateV1, TargetId, }; use crate::session::{ - PreparedSessionTransition, Session, SessionState, SessionUpdateError, SessionView, + DeferredSessionRetirement, PreparedSessionTransition, Session, SessionState, + SessionUpdateError, SessionView, }; use crate::wcag22::{ Wcag22ClientDeclaredNotApplicableV1, Wcag22CriterionV1, Wcag22EvaluationErrorV1, @@ -98,6 +85,7 @@ type CoreProgramSessionV1 = Session; type CoreProgramStateV1 = SessionState; type CoreProgramSessionViewV1<'a> = SessionView<'a, CoreProgramPlanV1>; type CorePreparedSessionTransitionV1<'a> = PreparedSessionTransition<'a, CoreProgramPlanV1>; +type CoreDeferredSessionRetirementV1 = DeferredSessionRetirement; type CoreProgramPlanErrorV1 = ProgramSessionEvaluationError; type CoreProgramConstraintCellV1 = ProgramConstraintCellV1; type CoreExactPassEvidenceV1 = ProgramVisiblePointPassEvidence; @@ -1472,9 +1460,10 @@ impl Default for DraftV1 { /// Непрозрачный сильный владелец одной точной скомпилированной Program. /// -/// Созданные им Session изменяются только через эту же аллокацию. Уничтожение -/// Owner отзывает обновления и операции, но исторические evidence остаются в -/// Session. +/// Созданные им standalone Session изменяются только через эту же аллокацию. +/// Attachment атомарно удерживает собственный strong pin той же эпохи, поэтому +/// уничтожение внешнего Owner не отзывает уже присоединённый runtime. Без такого +/// pin исторические evidence остаются читаемыми, но новые обновления недоступны. pub(crate) struct OwnerV1 { compiled: CompiledCoreProgramV1, } @@ -1510,13 +1499,6 @@ pub(crate) enum EvidenceBoundsErrorV1 { CardinalityOverflow, } -/// Отказ доступа из-за несовпадения точной owner-эпохи. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum AccessErrorV1 { - /// Session была создана другой аллокацией Owner. - OwnerMismatch, -} - impl OwnerV1 { /// Внутренняя передача из канонического компилятора. pub(crate) const fn from_compiled(compiled: CompiledCoreProgramV1) -> Self { @@ -1588,42 +1570,21 @@ impl OwnerV1 { .map(|(slot, _paint)| OutputSlotIdV1::from_core(slot)) } - /// Проецирует операции только для Session этой точной owner-эпохи. - /// - /// Равенство [`ContentIdentityV3`] не даёт полномочий. - pub(crate) fn project<'owner, 'session>( - &'owner self, - session: &'session SessionV1, - ) -> Result, AccessErrorV1> { - if !self.compiled.owns_session(&session.session) { - return Err(AccessErrorV1::OwnerMismatch); - } - Ok(ProjectionV1 { - evidence: session.evidence(), - owner: self, - scope: BorrowScopeV1::new(self, session), - }) - } - /// Допускает update без изменения зафиксированных raw head и lifecycle. /// /// Несовпадение Owner проверяется до admission, аллокаций и вычисления. /// Raw head, lifecycle и previous evidence меняются только в consuming /// commit; внутренние scratch-буферы могут быть переинициализированы здесь. - pub(crate) fn prepare_update<'owner, 'session>( - &'owner self, + pub(crate) fn prepare_update<'session>( + &self, session: &'session mut SessionV1, update: UpdateV1<'_>, - ) -> Result, UpdateErrorV1> { + ) -> Result, UpdateErrorV1> { if !self.compiled.owns_session(&session.session) { return Err(UpdateErrorV1::OwnerMismatch); } let transition = session.prepare_update(update)?; - Ok(PreparedSessionTransitionV1 { - owner: self, - transition, - scope: BorrowScopeV1::prepared(), - }) + Ok(PreparedSessionTransitionV1 { transition }) } /// Создаёт Session, привязанную к одному непрозрачному stream ID. @@ -1843,31 +1804,6 @@ impl<'a> EvidenceViewV1<'a> { } } -/// Нулевой lifetime-маркер точной пары Owner и неизменяемого снимка Session. -#[derive(Clone, Copy)] -struct BorrowScopeV1<'owner, 'session> { - _scope: PhantomData<(&'owner OwnerV1, &'session SessionV1)>, -} - -impl<'owner, 'session> BorrowScopeV1<'owner, 'session> { - const fn new(_owner: &'owner OwnerV1, _session: &'session SessionV1) -> Self { - Self { - _scope: PhantomData, - } - } - - const fn prepared() -> Self { - // Core transition already owns the sole mutable Session borrow, so - // accepting owner/session here would duplicate or conflict with it. - // The containing PreparedSessionTransitionV1 stores that transition - // and the exact Owner reference; its type ties this marker to both - // concrete `'owner` and `'session` lifetimes. - Self { - _scope: PhantomData, - } - } -} - /// Полностью вычисленный, но ещё не опубликованный переход одной Session. /// /// Тип линейный: он не реализует Clone/Copy. Drop сохраняет зафиксированные raw @@ -1875,90 +1811,20 @@ impl<'owner, 'session> BorrowScopeV1<'owner, 'session> { /// откатывается и не является наблюдаемым состоянием Session. [`Self::commit`] /// не выполняет fallible work и не утверждает запись в sink. #[must_use = "commit the prepared transition or drop it intentionally"] -pub(crate) struct PreparedSessionTransitionV1<'owner, 'session> { - owner: &'owner OwnerV1, +pub(crate) struct PreparedSessionTransitionV1<'session> { transition: CorePreparedSessionTransitionV1<'session>, - scope: BorrowScopeV1<'owner, 'session>, } -impl<'owner, 'session> PreparedSessionTransitionV1<'owner, 'session> { +impl<'session> PreparedSessionTransitionV1<'session> { /// Публикует только уже подготовленные raw head и lifecycle Session и - /// возвращает их точную проекцию. - pub(crate) fn commit(self) -> ProjectionV1<'owner, 'session> { - let Self { - owner, - transition, - scope, - } = self; - ProjectionV1 { - evidence: EvidenceViewV1 { - session: transition.commit(), - }, - owner, - scope, + /// возвращает exact evidence-only snapshot без sink-authority. + pub(crate) fn commit(self) -> EvidenceViewV1<'session> { + EvidenceViewV1 { + session: self.transition.commit(), } } } -/// Проверенная Owner-and-snapshot проекция evidence и операций. -#[derive(Clone, Copy)] -pub(crate) struct ProjectionV1<'owner, 'session> { - evidence: EvidenceViewV1<'session>, - owner: &'owner OwnerV1, - scope: BorrowScopeV1<'owner, 'session>, -} - -impl<'owner, 'session> ProjectionV1<'owner, 'session> { - /// Возвращает историческое evidence этого снимка. - pub(crate) const fn evidence(self) -> EvidenceViewV1<'session> { - self.evidence - } - - /// Возвращает полную каноническую последовательность операций состояния. - pub(crate) fn operations( - self, - ) -> impl ExactSizeIterator> + FusedIterator { - let inner = match self.evidence.state() { - SessionState::Waiting - if matches!( - self.evidence.session.raw_head(), - ObservationHeadViewV1::Empty - ) => - { - OperationSourceV1::Empty - } - SessionState::Ready { current } => { - debug_assert_eq!(current.outputs().len(), self.owner.compiled.output_count()); - debug_assert!( - current - .outputs() - .iter() - .enumerate() - .all(|(index, output)| self.owner.compiled.output_slot_at(index) - == Some(output.output())) - ); - OperationSourceV1::Set { - outputs: current.outputs().iter(), - certificate: VerifiedCertificateV1 { inner: current }, - scope: self.scope, - } - } - // `Waiting + Empty` — единственное состояние без действия и без - // полномочий на sink. После admission сырой головы любое состояние - // без текущего Verified-доказательства подчиняется одному закону - // отзыва. Так же fail-closed обрабатывается внутренне недостижимое - // сегодня сочетание `Waiting + Observed`. - SessionState::Waiting | SessionState::Stale { .. } | SessionState::Failed { .. } => { - OperationSourceV1::Remove { - slots: OwnerOutputSlotsV1::new(&self.owner.compiled), - scope: self.scope, - } - } - }; - OperationsV1 { inner } - } -} - /// Collision-resistant адрес канонического физического содержания Program. /// /// Identity не идентифицирует owner-эпоху и не даёт runtime-полномочий. @@ -2675,63 +2541,6 @@ impl<'a> CertifiedPaintOutputV1<'a> { } } -/// Операция установки, структурно связанная с точным Verified-сертификатом. -#[derive(Clone, Copy)] -pub(crate) struct SetV1<'owner, 'session> { - output: &'session ProgramPaintOutputV1, - certificate: VerifiedCertificateV1<'session>, - _scope: BorrowScopeV1<'owner, 'session>, -} - -impl<'session> SetV1<'_, 'session> { - /// Возвращает изменяемый клиентский выходной слот. - pub(crate) const fn output_slot(self) -> OutputSlotIdV1 { - OutputSlotIdV1::from_core((*self.output).output()) - } - - /// Возвращает исходный encoded sRGB8 сигнал выходного Paint. - pub(crate) const fn source(self) -> Srgb8 { - (*self.output).paint().source() - } - - /// Возвращает прозрачность выходного Paint. - pub(crate) const fn opacity(self) -> f64 { - (*self.output).paint().opacity().value() - } - - /// Возвращает сертификат, разрешивший эту операцию. - pub(crate) const fn certificate(self) -> VerifiedCertificateV1<'session> { - self.certificate - } -} - -/// Операция удаления результата без сертификата для текущего контекста. -#[derive(Clone, Copy)] -pub(crate) struct RemoveV1<'owner, 'session> { - output_slot: OutputSlotIdV1, - _scope: BorrowScopeV1<'owner, 'session>, -} - -impl RemoveV1<'_, '_> { - /// Возвращает удаляемый клиентский выходной слот. - pub(crate) const fn output_slot(self) -> OutputSlotIdV1 { - self.output_slot - } -} - -/// Полное закрытое множество операций над непрозрачными выходными слотами. -/// -/// Каждый payload заимствует точные Owner и снимок Session. Скопированные -/// slot/source/opacity — только данные: runtime обязан перепроверить живую -/// пару непосредственно перед одним атомарным sink commit. -#[derive(Clone, Copy)] -pub(crate) enum OperationV1<'owner, 'session> { - /// Установить сертифицированный результат. - Set(SetV1<'owner, 'session>), - /// Удалить результат, когда текущий контекст не сертифицирован. - Remove(RemoveV1<'owner, 'session>), -} - struct CertificatesV1<'a> { values: [Option>; 2], index: usize, @@ -2772,98 +2581,6 @@ impl<'a> Iterator for CertificatesV1<'a> { impl ExactSizeIterator for CertificatesV1<'_> {} impl FusedIterator for CertificatesV1<'_> {} -struct OwnerOutputSlotsV1<'owner> { - compiled: &'owner CompiledCoreProgramV1, - index: usize, - len: usize, -} - -impl<'owner> OwnerOutputSlotsV1<'owner> { - fn new(compiled: &'owner CompiledCoreProgramV1) -> Self { - Self { - compiled, - index: 0, - len: compiled.output_count(), - } - } -} - -impl Iterator for OwnerOutputSlotsV1<'_> { - type Item = OutputSlotIdV1; - - fn next(&mut self) -> Option { - if self.index == self.len { - return None; - } - let output = self.compiled.output_slot_at(self.index)?; - self.index += 1; - Some(OutputSlotIdV1::from_core(output)) - } - - fn size_hint(&self) -> (usize, Option) { - let remaining = self.len - self.index; - (remaining, Some(remaining)) - } -} - -impl ExactSizeIterator for OwnerOutputSlotsV1<'_> {} -impl FusedIterator for OwnerOutputSlotsV1<'_> {} - -enum OperationSourceV1<'owner, 'session> { - Empty, - Set { - outputs: slice::Iter<'session, ProgramPaintOutputV1>, - certificate: VerifiedCertificateV1<'session>, - scope: BorrowScopeV1<'owner, 'session>, - }, - Remove { - slots: OwnerOutputSlotsV1<'owner>, - scope: BorrowScopeV1<'owner, 'session>, - }, -} - -struct OperationsV1<'owner, 'session> { - inner: OperationSourceV1<'owner, 'session>, -} - -impl<'owner, 'session> Iterator for OperationsV1<'owner, 'session> { - type Item = OperationV1<'owner, 'session>; - - fn next(&mut self) -> Option { - match &mut self.inner { - OperationSourceV1::Empty => None, - OperationSourceV1::Set { - outputs, - certificate, - scope, - } => { - let output = outputs.next()?; - Some(OperationV1::Set(SetV1 { - output, - certificate: *certificate, - _scope: *scope, - })) - } - OperationSourceV1::Remove { slots, scope } => Some(OperationV1::Remove(RemoveV1 { - output_slot: slots.next()?, - _scope: *scope, - })), - } - } - - fn size_hint(&self) -> (usize, Option) { - let remaining = match &self.inner { - OperationSourceV1::Empty => 0, - OperationSourceV1::Set { outputs, .. } => outputs.len(), - OperationSourceV1::Remove { slots, .. } => slots.len(), - }; - (remaining, Some(remaining)) - } -} - -impl ExactSizeIterator for OperationsV1<'_, '_> {} -impl FusedIterator for OperationsV1<'_, '_> {} - /// Закрытая классификация ошибки создания Session. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum InstantiateErrorKindV1 { @@ -4039,16 +3756,6 @@ mod update_error_projection_tests { } } -#[cfg(test)] -mod operation_scope_tests { - use super::*; - - #[test] - fn operation_scope_is_a_zero_sized_borrow_marker() { - assert_eq!(core::mem::size_of::>(), 0); - } -} - #[cfg(test)] mod compile_error_projection_tests { use super::*; diff --git a/crates/labcolors-core/src/program/attachment.rs b/crates/labcolors-core/src/program/attachment.rs new file mode 100644 index 00000000..a9fe90e6 --- /dev/null +++ b/crates/labcolors-core/src/program/attachment.rs @@ -0,0 +1,1025 @@ +//! Терминальный point-attachment с одной revision-bound Session и одним writer. +//! +//! Конструктор минтит presentation-корреляции и strong Program pin из одного +//! compiled owner. Поэтому runtime-update не принимает независимые owner, +//! Session, stamp или sink handle, которые клиент мог бы перепутать. + +use core::{iter::FusedIterator, mem}; + +use crate::appearance::EncodedPointPaintV1; +use crate::program_session::{ + CompiledPointOutputPresentationV1, CoreProgramEvaluatorsV1, PointOutputPresentationBindErrorV1, + ProgramOwnerLeaseV1, ProgramPaintOutputV1, +}; +use crate::session::PreparedSessionDispositionV1; + +use super::{ + CoreDeferredSessionRetirementV1, CorePreparedSessionTransitionV1, EvidenceViewV1, + InstantiateErrorV1, OccurrenceIdV1, OutputSlotIdV1, OwnerV1, PresentationRootIdV1, + SessionState, SessionV1, UpdateErrorV1, UpdateV1, VerifiedCertificateV1, +}; + +/// Sealing оставляет реализации физического writer внутри пакета. +pub(crate) mod sink_private { + pub(crate) trait Sealed {} +} + +/// Одна authored и ещё не доверенная корреляция output→sink для emission. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct AuthoredPointEmissionBindingV1 { + output: OutputSlotIdV1, + sink_output: SinkOutputId, +} + +impl AuthoredPointEmissionBindingV1 { + pub(crate) const fn new(output: OutputSlotIdV1, sink_output: SinkOutputId) -> Self { + Self { + output, + sink_output, + } + } +} + +/// Одна authored relation output→presentation; fan-out разрешён явно. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct AuthoredPointPresentationBindingV1 { + output: OutputSlotIdV1, + root: PresentationRootIdV1, + occurrence: OccurrenceIdV1, +} + +impl AuthoredPointPresentationBindingV1 { + pub(crate) const fn new( + output: OutputSlotIdV1, + root: PresentationRootIdV1, + occurrence: OccurrenceIdV1, + ) -> Self { + Self { + output, + root, + occurrence, + } + } +} + +/// Сминченная компилятором unique emission-корреляция в output order. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct AttachedPointEmissionV1 { + output_ordinal: usize, + output: OutputSlotIdV1, + sink_output: SinkOutputId, +} + +impl AttachedPointEmissionV1 { + pub(crate) const fn output(self) -> OutputSlotIdV1 { + self.output + } + + pub(crate) const fn sink_output(self) -> SinkOutputId { + self.sink_output + } +} + +/// Сминченная compiler relation; output и sink могут законно повторяться. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct AttachedPointPresentationV1 { + compiled: CompiledPointOutputPresentationV1, + sink_output: SinkOutputId, +} + +impl AttachedPointPresentationV1 { + pub(crate) const fn output(self) -> OutputSlotIdV1 { + OutputSlotIdV1::from_core(self.compiled.output()) + } + + pub(crate) const fn root(self) -> PresentationRootIdV1 { + PresentationRootIdV1::from_core(self.compiled.root()) + } + + pub(crate) const fn occurrence(self) -> OccurrenceIdV1 { + OccurrenceIdV1::from_core(self.compiled.occurrence()) + } + + pub(crate) const fn sink_output(self) -> SinkOutputId { + self.sink_output + } +} + +/// Один элемент полного сертифицированного point-снимка для sink. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct PointSinkPatchEntryV1 { + emission: AttachedPointEmissionV1, + paint: EncodedPointPaintV1, +} + +impl PointSinkPatchEntryV1 { + pub(crate) const fn output(self) -> OutputSlotIdV1 { + self.emission.output() + } + + pub(crate) const fn sink_output(self) -> SinkOutputId { + self.emission.sink_output() + } + + pub(crate) const fn paint(self) -> EncodedPointPaintV1 { + self.paint + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct AttachedRenderPatchEntryV1 { + presentation: AttachedPointPresentationV1, + paint: EncodedPointPaintV1, +} + +/// Единственные три intent, принимаемые терминальным point sink. +pub(crate) enum PointSinkIntentV1<'a, SinkOutputId, Stamp> { + SetAll { + revision: u64, + patch: &'a [PointSinkPatchEntryV1], + }, + RevokeAll { + revision: u64, + }, + ConfirmExact { + revision: u64, + published_stamp: &'a Stamp, + }, +} + +/// Подготовленная sink-local транзакция целого снимка, удерживающая Busy lease. +/// +/// Все реализации подчиняются hard-law all-or-nothing: внешний наблюдатель +/// никогда не видит часть нового scope. Успех заменяет весь принадлежащий lease +/// scope одной атомарной публикацией. Любой отказ сохраняет прежние наблюдаемые +/// scope→value snapshot, revision и равный по [`Eq`] Stamp. +pub(crate) trait PreparedPointSinkWriteV1 { + type Stamp: Clone + Eq; + type Error; + + /// Stamp точного снимка, который опубликует успешный install. + fn proposed_stamp(&self) -> &Self::Stamp; + + /// Единственная fallible-операция после parsing, allocations и CAS setup. + /// + /// `Ok` означает, что весь scope уже опубликован атомарно. `Err` означает, + /// что прежние snapshot, revision и Stamp наблюдаемо не изменились. Пока + /// Prepared жив, Busy может оставаться занятым; его Drop обязан освободить + /// Busy, поэтому [`Attachment::update`] возвращает ошибку уже после release. + fn try_install(&mut self) -> Result<(), Self::Error>; + + /// Освобождает Busy после переноса Session и состояния Attachment. + /// + /// Реализация обязана быть infallible и allocation-free. Она только + /// переносит все owning-значения в заранее очищенный lease retirement-slot + /// и снимает Busy; Drop/deallocation в этой фазе запрещены. + fn finish_after_session(self); +} + +/// Линейное владение одним точным физическим point-sink scope. +pub(crate) trait LinearPointSinkLeaseV1: sink_private::Sealed { + type OutputId: Copy + Eq; + type Stamp: Clone + Eq; + type Error; + type Prepared<'lease>: PreparedPointSinkWriteV1 + where + Self: 'lease; + + /// Точный scope, которым эксклюзивно владеет lease, в каноническом порядке + /// выходов скомпилированной Program. + fn owned_output_scope(&self) -> &[Self::OutputId]; + + /// Готовит полный снимок, не сохраняя borrowed-данные patch. + /// + /// `Err` сохраняет прежние наблюдаемые snapshot, revision и Stamp и + /// возвращает lease с уже свободным Busy. Подготовка не публикует даже + /// допустимую часть нового снимка. + fn prepare<'lease>( + &'lease mut self, + intent: PointSinkIntentV1<'_, Self::OutputId, Self::Stamp>, + ) -> Result, Self::Error>; + + /// Атомарно отзывает полный scope lease перед его освобождением. + /// + /// Реализация обязана быть infallible и allocation-free, даже если ни один + /// снимок ещё не публиковался. + fn revoke_all_before_release(&mut self, published_stamp: Option<&Self::Stamp>); +} + +/// Cold-ошибка создания Attachment; sink ещё ничего не опубликовал. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum AttachmentCreateErrorV1 { + ResourceExhausted, + Instantiate(InstantiateErrorV1), + EmissionBindingCount { + expected: usize, + actual: usize, + }, + DuplicateEmissionOutput { + output: OutputSlotIdV1, + }, + EmissionOutputMismatch { + ordinal: usize, + expected: OutputSlotIdV1, + authored: OutputSlotIdV1, + }, + SinkOutputAliased { + sink_output: SinkOutputId, + first_output: OutputSlotIdV1, + second_output: OutputSlotIdV1, + }, + EmptyPresentations, + PresentationCount { + expected: usize, + actual: usize, + }, + MissingOutputPresentation { + output: OutputSlotIdV1, + }, + DuplicatePresentation { + root: PresentationRootIdV1, + occurrence: OccurrenceIdV1, + first_output: OutputSlotIdV1, + second_output: OutputSlotIdV1, + }, + MissingCompiledPresentation { + presentation_ordinal: usize, + }, + SinkScopeCount { + expected: usize, + actual: usize, + }, + DuplicateSinkScopeOutput { + sink_output: SinkOutputId, + }, + SinkScopeMismatch { + ordinal: usize, + binding: SinkOutputId, + owned: SinkOutputId, + }, + InvalidPointBinding { + authored_index: usize, + cause: PointOutputPresentationBindErrorV1, + }, + InternalInvariant, +} + +/// Закрытый отказ уже скомпилированной терминальной транзакции. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum AttachmentInvariantV1 { + EmptyIdempotentHead, + MissingPublishedStamp, + PublishedRevisionMismatch, + OutputCountMismatch, + OutputIdentityMismatch, + PaintIdentityMismatch, + ScratchCapacityLost, + ConfirmStampMismatch, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum AttachmentUpdateErrorV1 { + Update(UpdateErrorV1), + SinkPrepare(SinkError), + SinkInstall(SinkError), + InternalInvariant(AttachmentInvariantV1), +} + +type AttachmentUpdateResultV1<'a, L> = Result< + AttachmentCommitV1< + 'a, + ::OutputId, + ::Stamp, + >, + AttachmentUpdateErrorV1<::Error>, +>; + +/// Prospective sink-смысл одного полностью вычисленного перехода Session. +pub(super) enum PreparedDispositionV1<'a> { + ConfirmExact { + revision: u64, + }, + RevokeAll { + revision: u64, + }, + SetAll { + revision: u64, + outputs: &'a [ProgramPaintOutputV1], + }, +} + +fn prepared_disposition<'prepared>( + transition: &'prepared CorePreparedSessionTransitionV1<'_>, +) -> Result, AttachmentInvariantV1> { + match transition.disposition() { + PreparedSessionDispositionV1::Idempotent { raw_head, .. } => raw_head + .revision() + .map(|revision| PreparedDispositionV1::ConfirmExact { + revision: revision.value(), + }) + .ok_or(AttachmentInvariantV1::EmptyIdempotentHead), + PreparedSessionDispositionV1::Unknown(unknown) => Ok(PreparedDispositionV1::RevokeAll { + revision: unknown.revision().value(), + }), + PreparedSessionDispositionV1::Verified(verified) => Ok(PreparedDispositionV1::SetAll { + revision: verified.report().observation().revision().value(), + outputs: verified.outputs(), + }), + PreparedSessionDispositionV1::Violation(violation) => { + Ok(PreparedDispositionV1::RevokeAll { + revision: violation.report().observation().revision().value(), + }) + } + } +} + +struct PublishedAttachmentStampV1 { + revision: u64, + sink: Stamp, +} + +enum PreparedPatchActionV1 { + SetAll { revision: u64 }, + RevokeAll { revision: u64 }, + ConfirmExact { revision: u64 }, +} + +impl PreparedPatchActionV1 { + const fn revision(&self) -> u64 { + match self { + Self::SetAll { revision } + | Self::RevokeAll { revision } + | Self::ConfirmExact { revision, .. } => *revision, + } + } +} + +/// Borrowed exact stamp снимка, принадлежащего одному Attachment. +pub(crate) struct AttachedPublishedStampV1<'a, Stamp> { + inner: &'a PublishedAttachmentStampV1, +} + +impl Copy for AttachedPublishedStampV1<'_, Stamp> {} + +impl Clone for AttachedPublishedStampV1<'_, Stamp> { + fn clone(&self) -> Self { + *self + } +} + +impl AttachedPublishedStampV1<'_, Stamp> { + pub(crate) const fn revision(self) -> u64 { + self.inner.revision + } +} + +/// Один элемент final render-authority после commit sink и Session. +pub(crate) struct AttachedRenderOutputV1<'a, SinkOutputId, Stamp> { + certificate: VerifiedCertificateV1<'a>, + patch: AttachedRenderPatchEntryV1, + published_stamp: AttachedPublishedStampV1<'a, Stamp>, +} + +impl Copy for AttachedRenderOutputV1<'_, SinkOutputId, Stamp> {} + +impl Clone for AttachedRenderOutputV1<'_, SinkOutputId, Stamp> { + fn clone(&self) -> Self { + *self + } +} + +impl<'a, SinkOutputId: Copy, Stamp> AttachedRenderOutputV1<'a, SinkOutputId, Stamp> { + pub(crate) const fn certificate(self) -> VerifiedCertificateV1<'a> { + self.certificate + } + + pub(crate) const fn output(self) -> OutputSlotIdV1 { + self.patch.presentation.output() + } + + pub(crate) const fn paint(self) -> EncodedPointPaintV1 { + self.patch.paint + } + + pub(crate) const fn root(self) -> PresentationRootIdV1 { + self.patch.presentation.root() + } + + pub(crate) const fn occurrence(self) -> OccurrenceIdV1 { + self.patch.presentation.occurrence() + } + + pub(crate) const fn sink_output(self) -> SinkOutputId { + self.patch.presentation.sink_output() + } + + pub(crate) const fn published_stamp(self) -> AttachedPublishedStampV1<'a, Stamp> { + self.published_stamp + } +} + +/// Точный post-commit view; historical evidence и render authority не смешаны. +pub(crate) struct AttachmentCommitV1<'a, SinkOutputId, Stamp> { + evidence: EvidenceViewV1<'a>, + committed_render_patch: &'a [AttachedRenderPatchEntryV1], + published_stamp: &'a PublishedAttachmentStampV1, +} + +impl Copy for AttachmentCommitV1<'_, SinkOutputId, Stamp> {} + +impl Clone for AttachmentCommitV1<'_, SinkOutputId, Stamp> { + fn clone(&self) -> Self { + *self + } +} + +impl<'a, SinkOutputId: Copy, Stamp> AttachmentCommitV1<'a, SinkOutputId, Stamp> { + pub(crate) const fn evidence(self) -> EvidenceViewV1<'a> { + self.evidence + } + + pub(crate) fn render_outputs(self) -> AttachedRenderOutputsV1<'a, SinkOutputId, Stamp> { + let certificate = match self.evidence.state() { + SessionState::Ready { current } => Some(VerifiedCertificateV1 { inner: current }), + SessionState::Waiting | SessionState::Stale { .. } | SessionState::Failed { .. } => { + None + } + }; + AttachedRenderOutputsV1 { + certificate, + committed_render_patch: self.committed_render_patch, + published_stamp: AttachedPublishedStampV1 { + inner: self.published_stamp, + }, + index: 0, + } + } +} + +pub(crate) struct AttachedRenderOutputsV1<'a, SinkOutputId, Stamp> { + certificate: Option>, + committed_render_patch: &'a [AttachedRenderPatchEntryV1], + published_stamp: AttachedPublishedStampV1<'a, Stamp>, + index: usize, +} + +impl<'a, SinkOutputId: Copy, Stamp> Iterator for AttachedRenderOutputsV1<'a, SinkOutputId, Stamp> { + type Item = AttachedRenderOutputV1<'a, SinkOutputId, Stamp>; + + fn next(&mut self) -> Option { + let certificate = self.certificate?; + let patch = *self.committed_render_patch.get(self.index)?; + self.index += 1; + Some(AttachedRenderOutputV1 { + certificate, + patch, + published_stamp: self.published_stamp, + }) + } + + fn size_hint(&self) -> (usize, Option) { + let remaining = if self.certificate.is_some() { + self.committed_render_patch.len().saturating_sub(self.index) + } else { + 0 + }; + (remaining, Some(remaining)) + } +} + +impl ExactSizeIterator + for AttachedRenderOutputsV1<'_, SinkOutputId, Stamp> +{ +} +impl FusedIterator for AttachedRenderOutputsV1<'_, SinkOutputId, Stamp> {} + +/// Владеет одной Session, одним exact Program pin и одним linear writer. +pub(crate) struct Attachment +where + L: LinearPointSinkLeaseV1, +{ + // Порядок полей задаёт освобождение после `Drop::drop`: writer, Session, + // инертные снимки и последней — точная Program generation. + sink: L, + session: SessionV1, + emissions: Vec>, + presentations: Vec>, + // Published patch остаётся reusable backing: swap/clear меняют длину, + // но сохраняют заранее зарезервированную capacity для следующей ревизии. + committed_sink_patch: Vec>, + scratch_sink_patch: Vec>, + committed_render_patch: Vec>, + scratch_render_patch: Vec>, + published_stamp: Option>, + // Прошлый control block переносится сюда после install и освобождается до + // следующей транзакции, но не в infallible commit tail. + retired_stamp: Option>, + // Вытеснённые Session evidence и transaction owner после install только + // переносятся сюда и освобождаются до следующего prepare. + retired_session: Option, + _owner_pin: ProgramOwnerLeaseV1, +} + +struct UnpublishedSinkGuardV1<'a, L: LinearPointSinkLeaseV1> { + sink: &'a mut L, + armed: bool, +} + +impl<'a, L: LinearPointSinkLeaseV1> UnpublishedSinkGuardV1<'a, L> { + const fn new(sink: &'a mut L) -> Self { + Self { sink, armed: true } + } + + fn sink(&self) -> &L { + self.sink + } + + fn disarm(mut self) { + self.armed = false; + } +} + +impl Drop for UnpublishedSinkGuardV1<'_, L> { + fn drop(&mut self) { + if self.armed { + self.sink.revoke_all_before_release(None); + } + } +} + +impl OwnerV1 { + /// Создаёт один terminal attachment этой exact compiled generation. + pub(crate) fn attach( + &self, + stream_id: u32, + authored_emissions: &[AuthoredPointEmissionBindingV1], + authored_presentations: &[AuthoredPointPresentationBindingV1], + sink: L, + ) -> Result, AttachmentCreateErrorV1> + where + L: LinearPointSinkLeaseV1, + { + Attachment::try_new( + self, + stream_id, + authored_emissions, + authored_presentations, + sink, + ) + } +} + +impl Attachment +where + L: LinearPointSinkLeaseV1, +{ + /// Атомарно связывает authored IDs и pin той же exact compiled generation. + fn try_new( + owner: &OwnerV1, + stream_id: u32, + authored_emissions: &[AuthoredPointEmissionBindingV1], + authored_presentations: &[AuthoredPointPresentationBindingV1], + sink: L, + ) -> Result> { + let mut sink = sink; + let sink_guard = UnpublishedSinkGuardV1::new(&mut sink); + let expected_outputs = owner.compiled.output_count(); + if authored_emissions.len() != expected_outputs { + return Err(AttachmentCreateErrorV1::EmissionBindingCount { + expected: expected_outputs, + actual: authored_emissions.len(), + }); + } + + let mut emissions: Vec> = Vec::new(); + emissions + .try_reserve_exact(expected_outputs) + .map_err(|_| AttachmentCreateErrorV1::ResourceExhausted)?; + emissions.extend(authored_emissions.iter().copied().map(|binding| { + AttachedPointEmissionV1 { + output_ordinal: 0, + output: binding.output, + sink_output: binding.sink_output, + } + })); + emissions.sort_unstable_by_key(|binding| binding.output); + for adjacent in emissions.windows(2) { + if adjacent[0].output == adjacent[1].output { + return Err(AttachmentCreateErrorV1::DuplicateEmissionOutput { + output: adjacent[0].output, + }); + } + } + for (output_ordinal, (binding, expected)) in + emissions.iter_mut().zip(owner.output_slots()).enumerate() + { + if binding.output != expected { + return Err(AttachmentCreateErrorV1::EmissionOutputMismatch { + ordinal: output_ordinal, + expected, + authored: binding.output, + }); + } + binding.output_ordinal = output_ordinal; + } + for emission_index in 0..emissions.len() { + let emission = emissions[emission_index]; + if let Some(previous) = emissions[..emission_index] + .iter() + .find(|previous| previous.sink_output == emission.sink_output) + { + return Err(AttachmentCreateErrorV1::SinkOutputAliased { + sink_output: emission.sink_output, + first_output: previous.output, + second_output: emission.output, + }); + } + } + + let owned_scope = sink_guard.sink().owned_output_scope(); + if owned_scope.len() != emissions.len() { + return Err(AttachmentCreateErrorV1::SinkScopeCount { + expected: emissions.len(), + actual: owned_scope.len(), + }); + } + for (ordinal, owned) in owned_scope.iter().copied().enumerate() { + if owned_scope[..ordinal].contains(&owned) { + return Err(AttachmentCreateErrorV1::DuplicateSinkScopeOutput { + sink_output: owned, + }); + } + } + + if authored_presentations.is_empty() { + return Err(AttachmentCreateErrorV1::EmptyPresentations); + } + let expected_presentations = owner.compiled.point_presentation_count(); + if authored_presentations.len() != expected_presentations { + return Err(AttachmentCreateErrorV1::PresentationCount { + expected: expected_presentations, + actual: authored_presentations.len(), + }); + } + + let mut presentations = Vec::new(); + presentations + .try_reserve_exact(authored_presentations.len()) + .map_err(|_| AttachmentCreateErrorV1::ResourceExhausted)?; + for (authored_index, binding) in authored_presentations.iter().copied().enumerate() { + let compiled = owner + .compiled + .bind_point_output_presentation( + binding.output.into_core(), + binding.root.into_core(), + binding.occurrence.into_core(), + ) + .map_err(|cause| AttachmentCreateErrorV1::InvalidPointBinding { + authored_index, + cause, + })?; + let emission = emissions + .get(compiled.output_ordinal()) + .copied() + .ok_or(AttachmentCreateErrorV1::InternalInvariant)?; + if emission.output.into_core() != compiled.output() { + return Err(AttachmentCreateErrorV1::InternalInvariant); + } + presentations.push(AttachedPointPresentationV1 { + compiled, + sink_output: emission.sink_output, + }); + } + presentations.sort_unstable_by_key(|binding| binding.compiled.presentation_ordinal()); + for adjacent in presentations.windows(2) { + if adjacent[0].compiled.presentation_ordinal() + == adjacent[1].compiled.presentation_ordinal() + { + let previous = adjacent[0]; + let relation = adjacent[1]; + return Err(AttachmentCreateErrorV1::DuplicatePresentation { + root: relation.root(), + occurrence: relation.occurrence(), + first_output: previous.output(), + second_output: relation.output(), + }); + } + } + for (presentation_ordinal, presentation) in presentations.iter().enumerate() { + if presentation.compiled.presentation_ordinal() != presentation_ordinal { + return Err(AttachmentCreateErrorV1::MissingCompiledPresentation { + presentation_ordinal, + }); + } + } + presentations.sort_unstable_by_key(|binding| { + ( + binding.compiled.output_ordinal(), + binding.compiled.presentation_ordinal(), + ) + }); + let mut presentation_index = 0; + for emission in &emissions { + if presentations + .get(presentation_index) + .is_none_or(|presentation| { + presentation.compiled.output_ordinal() != emission.output_ordinal + }) + { + return Err(AttachmentCreateErrorV1::MissingOutputPresentation { + output: emission.output, + }); + } + while presentations + .get(presentation_index) + .is_some_and(|presentation| { + presentation.compiled.output_ordinal() == emission.output_ordinal + }) + { + presentation_index += 1; + } + } + if presentation_index != presentations.len() { + return Err(AttachmentCreateErrorV1::InternalInvariant); + } + for (ordinal, (binding, owned)) in emissions + .iter() + .zip(owned_scope.iter().copied()) + .enumerate() + { + if binding.sink_output != owned { + return Err(AttachmentCreateErrorV1::SinkScopeMismatch { + ordinal, + binding: binding.sink_output, + owned, + }); + } + } + + let mut committed_sink_patch = Vec::new(); + committed_sink_patch + .try_reserve_exact(expected_outputs) + .map_err(|_| AttachmentCreateErrorV1::ResourceExhausted)?; + let mut scratch_sink_patch = Vec::new(); + scratch_sink_patch + .try_reserve_exact(expected_outputs) + .map_err(|_| AttachmentCreateErrorV1::ResourceExhausted)?; + let mut committed_render_patch = Vec::new(); + committed_render_patch + .try_reserve_exact(presentations.len()) + .map_err(|_| AttachmentCreateErrorV1::ResourceExhausted)?; + let mut scratch_render_patch = Vec::new(); + scratch_render_patch + .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(); + sink_guard.disarm(); + Ok(Self { + sink, + session, + emissions, + presentations, + committed_sink_patch, + scratch_sink_patch, + committed_render_patch, + scratch_render_patch, + published_stamp: None, + retired_stamp: None, + retired_session: None, + _owner_pin: owner_pin, + }) + } + + /// Готовит, атомарно устанавливает и infallibly публикует целый update. + pub(crate) fn update(&mut self, update: UpdateV1<'_>) -> AttachmentUpdateResultV1<'_, L> { + drop(self.retired_session.take()); + drop(self.retired_stamp.take()); + let transition = self + .session + .prepare_update(update) + .map_err(AttachmentUpdateErrorV1::Update)?; + let disposition = prepared_disposition(&transition) + .map_err(AttachmentUpdateErrorV1::InternalInvariant)?; + + let confirmed_stamp = match &disposition { + PreparedDispositionV1::ConfirmExact { revision } => { + let published = self.published_stamp.as_ref().ok_or( + AttachmentUpdateErrorV1::InternalInvariant( + AttachmentInvariantV1::MissingPublishedStamp, + ), + )?; + if published.revision != *revision { + return Err(AttachmentUpdateErrorV1::InternalInvariant( + AttachmentInvariantV1::PublishedRevisionMismatch, + )); + } + Some(&published.sink) + } + PreparedDispositionV1::SetAll { .. } | PreparedDispositionV1::RevokeAll { .. } => None, + }; + + let action = match disposition { + PreparedDispositionV1::SetAll { revision, outputs } => { + stage_complete_patches( + &self.emissions, + &self.presentations, + outputs, + &mut self.scratch_sink_patch, + &mut self.scratch_render_patch, + ) + .map_err(AttachmentUpdateErrorV1::InternalInvariant)?; + PreparedPatchActionV1::SetAll { revision } + } + PreparedDispositionV1::RevokeAll { revision } => { + self.scratch_sink_patch.clear(); + self.scratch_render_patch.clear(); + PreparedPatchActionV1::RevokeAll { revision } + } + PreparedDispositionV1::ConfirmExact { revision } => { + PreparedPatchActionV1::ConfirmExact { revision } + } + }; + + let intent = match &action { + PreparedPatchActionV1::SetAll { revision } => PointSinkIntentV1::SetAll { + revision: *revision, + patch: &self.scratch_sink_patch, + }, + PreparedPatchActionV1::RevokeAll { revision } => PointSinkIntentV1::RevokeAll { + revision: *revision, + }, + PreparedPatchActionV1::ConfirmExact { revision } => PointSinkIntentV1::ConfirmExact { + revision: *revision, + published_stamp: confirmed_stamp.ok_or( + AttachmentUpdateErrorV1::InternalInvariant( + AttachmentInvariantV1::MissingPublishedStamp, + ), + )?, + }, + }; + let sink_prepared = self + .sink + .prepare(intent) + .map_err(AttachmentUpdateErrorV1::SinkPrepare)?; + let mut prepared: PreparedAttachmentUpdateV1<'_, '_, L> = + PreparedAttachmentUpdateV1::new(transition, sink_prepared); + let next_stamp = PublishedAttachmentStampV1 { + revision: action.revision(), + sink: prepared.proposed_stamp().clone(), + }; + if matches!(&action, PreparedPatchActionV1::ConfirmExact { .. }) { + let expected = confirmed_stamp.ok_or(AttachmentUpdateErrorV1::InternalInvariant( + AttachmentInvariantV1::MissingPublishedStamp, + ))?; + if &next_stamp.sink != expected { + return Err(AttachmentUpdateErrorV1::InternalInvariant( + AttachmentInvariantV1::ConfirmStampMismatch, + )); + } + } + + prepared + .try_install() + .map_err(AttachmentUpdateErrorV1::SinkInstall)?; + + // Ниже только moves/swaps/writes в заранее пустые retirement-слоты. + let (installed_sink, retired_session) = prepared.commit_session(); + self.retired_session = Some(retired_session); + match &action { + PreparedPatchActionV1::SetAll { .. } | PreparedPatchActionV1::RevokeAll { .. } => { + mem::swap(&mut self.committed_sink_patch, &mut self.scratch_sink_patch); + mem::swap( + &mut self.committed_render_patch, + &mut self.scratch_render_patch, + ); + } + PreparedPatchActionV1::ConfirmExact { .. } => {} + } + let previous_stamp = self.published_stamp.take(); + self.retired_stamp = previous_stamp; + let published_stamp = self.published_stamp.insert(next_stamp); + installed_sink.finish_after_session(); + + Ok(AttachmentCommitV1 { + evidence: self.session.evidence(), + committed_render_patch: &self.committed_render_patch, + published_stamp, + }) + } + + /// Детерминированное consuming-освобождение; revoke выполняет `Drop`. + pub(crate) fn dispose(self) { + drop(self); + } +} + +impl Drop for Attachment +where + L: LinearPointSinkLeaseV1, +{ + fn drop(&mut self) { + self.sink + .revoke_all_before_release(self.published_stamp.as_ref().map(|stamp| &stamp.sink)); + self.published_stamp = None; + self.committed_sink_patch.clear(); + self.committed_render_patch.clear(); + } +} + +fn stage_complete_patches( + emissions: &[AttachedPointEmissionV1], + presentations: &[AttachedPointPresentationV1], + outputs: &[ProgramPaintOutputV1], + sink_scratch: &mut Vec>, + render_scratch: &mut Vec>, +) -> Result<(), AttachmentInvariantV1> { + if outputs.len() != emissions.len() { + return Err(AttachmentInvariantV1::OutputCountMismatch); + } + if sink_scratch.capacity() < emissions.len() || render_scratch.capacity() < presentations.len() + { + return Err(AttachmentInvariantV1::ScratchCapacityLost); + } + + sink_scratch.clear(); + render_scratch.clear(); + for (output_ordinal, (emission, output)) in emissions + .iter() + .copied() + .zip(outputs.iter().copied()) + .enumerate() + { + if emission.output_ordinal != output_ordinal + || emission.output.into_core() != output.output() + { + return Err(AttachmentInvariantV1::OutputIdentityMismatch); + } + sink_scratch.push(PointSinkPatchEntryV1 { + emission, + paint: output.paint(), + }); + } + for presentation in presentations.iter().copied() { + let output = outputs + .get(presentation.compiled.output_ordinal()) + .copied() + .ok_or(AttachmentInvariantV1::OutputCountMismatch)?; + if presentation.compiled.output() != output.output() { + return Err(AttachmentInvariantV1::OutputIdentityMismatch); + } + if presentation.compiled.paint() != output.paint().id() { + return Err(AttachmentInvariantV1::PaintIdentityMismatch); + } + render_scratch.push(AttachedRenderPatchEntryV1 { + presentation, + paint: output.paint(), + }); + } + Ok(()) +} + +/// Общий token: abort всегда уничтожает evidence до освобождения Busy. +struct PreparedAttachmentUpdateV1<'session, 'sink, L> +where + L: LinearPointSinkLeaseV1 + 'sink, +{ + // Порядок объявления и есть abort-протокол: prospective evidence + // уничтожается, пока sink ещё удерживает Busy. + transition: CorePreparedSessionTransitionV1<'session>, + sink: L::Prepared<'sink>, +} + +impl<'session, 'sink, L> PreparedAttachmentUpdateV1<'session, 'sink, L> +where + L: LinearPointSinkLeaseV1 + 'sink, +{ + fn new( + transition: CorePreparedSessionTransitionV1<'session>, + sink: L::Prepared<'sink>, + ) -> Self { + Self { transition, sink } + } + + fn proposed_stamp(&self) -> &L::Stamp { + self.sink.proposed_stamp() + } + + fn try_install(&mut self) -> Result<(), L::Error> { + self.sink.try_install() + } + + fn commit_session(self) -> (L::Prepared<'sink>, CoreDeferredSessionRetirementV1) { + let Self { transition, sink } = self; + let (_view, retirement) = transition.commit_deferred(); + (sink, retirement) + } +} + +#[cfg(test)] +pub(crate) mod support; +#[cfg(test)] +mod tests; diff --git a/crates/labcolors-core/src/program/attachment/support.rs b/crates/labcolors-core/src/program/attachment/support.rs new file mode 100644 index 00000000..e4af7f7e --- /dev/null +++ b/crates/labcolors-core/src/program/attachment/support.rs @@ -0,0 +1,534 @@ +use std::{ + cell::{Cell, RefCell}, + rc::Rc, +}; + +use super::*; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct TestSinkOutputIdV1(u32); + +impl TestSinkOutputIdV1 { + pub(crate) const fn new(value: u32) -> Self { + Self(value) + } + + pub(crate) const fn value(self) -> u32 { + self.0 + } +} + +#[derive(Debug, Clone)] +pub(crate) struct TestPublishedStampV1 { + sequence: u64, + epoch: Rc<()>, +} + +impl PartialEq for TestPublishedStampV1 { + fn eq(&self, other: &Self) -> bool { + self.sequence == other.sequence && Rc::ptr_eq(&self.epoch, &other.epoch) + } +} + +impl Eq for TestPublishedStampV1 {} + +impl TestPublishedStampV1 { + fn next(&self) -> Result { + Ok(Self { + sequence: self + .sequence + .checked_add(1) + .ok_or(InMemoryPointSinkErrorV1::StampExhausted)?, + epoch: Rc::clone(&self.epoch), + }) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum InMemoryPointSinkErrorV1 { + Busy, + StampMismatch, + RejectedPrepare, + RejectedInstall, + RejectedInstallAfterSwap, + StampExhausted, + ResourceExhausted, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct TestSnapshotEntryV1 { + output: OutputSlotIdV1, + sink_output: TestSinkOutputIdV1, + paint: EncodedPointPaintV1, +} + +impl TestSnapshotEntryV1 { + pub(crate) const fn output(self) -> OutputSlotIdV1 { + self.output + } + + pub(crate) const fn sink_output(self) -> TestSinkOutputIdV1 { + self.sink_output + } + + pub(crate) const fn paint(self) -> EncodedPointPaintV1 { + self.paint + } +} + +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub(crate) struct TestIntentCountsV1 { + pub(crate) set_all: usize, + pub(crate) revoke_all: usize, + pub(crate) confirm_exact: usize, +} + +struct TestSinkStateV1 { + snapshot: Vec, + revision: Option, + stamp: TestPublishedStampV1, + reject_next_install: bool, + counts: TestIntentCountsV1, + revoke_count: usize, + sequence: u64, + revoke_sequence: Option, + lease_drop_sequence: Option, + revoke_saw_exact_stamp: bool, +} + +struct TestSinkSharedV1 { + state: RefCell, + busy: Cell, + reject_next_prepare: Cell, + rejected_prepare_saw_busy: Cell, + reject_next_install_after_swap: Cell, + misreport_next_confirm_proposed_stamp: Cell, + panic_on_retirement_drop: Cell, + retirement_drop_count: Cell, +} + +pub(crate) struct InMemoryPointSinkLeaseV1 { + owned_scope: Vec, + shared: Rc, + retired: Option, +} + +#[derive(Clone)] +pub(crate) struct InMemoryPointSinkProbeV1 { + shared: Rc, +} + +impl InMemoryPointSinkProbeV1 { + pub(crate) fn snapshot(&self) -> Vec { + self.shared.state.borrow().snapshot.clone() + } + + pub(crate) fn revision(&self) -> Option { + self.shared.state.borrow().revision + } + + pub(crate) fn stamp(&self) -> TestPublishedStampV1 { + self.shared.state.borrow().stamp.clone() + } + + pub(crate) fn intent_counts(&self) -> TestIntentCountsV1 { + self.shared.state.borrow().counts + } + + pub(crate) fn is_busy(&self) -> bool { + self.shared.busy.get() + } + + pub(crate) fn reject_next_install(&self) { + self.shared.state.borrow_mut().reject_next_install = true; + } + + pub(crate) fn reject_next_prepare(&self) { + self.shared.reject_next_prepare.set(true); + } + + pub(crate) fn reject_next_install_after_swap(&self) { + self.shared.reject_next_install_after_swap.set(true); + } + + pub(crate) fn rejected_prepare_saw_busy(&self) -> bool { + self.shared.rejected_prepare_saw_busy.get() + } + + pub(crate) fn misreport_next_confirm_proposed_stamp(&self) { + self.shared.misreport_next_confirm_proposed_stamp.set(true); + } + + pub(crate) fn revoke_count(&self) -> usize { + self.shared.state.borrow().revoke_count + } + + pub(crate) fn revoked_before_lease_drop(&self) -> bool { + let state = self.shared.state.borrow(); + matches!( + (state.revoke_sequence, state.lease_drop_sequence), + (Some(revoke), Some(release)) if revoke < release + ) + } + + pub(crate) fn revoke_saw_exact_stamp(&self) -> bool { + self.shared.state.borrow().revoke_saw_exact_stamp + } + + pub(crate) fn panic_on_next_retirement_drop(&self) { + self.shared.panic_on_retirement_drop.set(true); + } + + pub(crate) fn retirement_drop_count(&self) -> usize { + self.shared.retirement_drop_count.get() + } +} + +pub(crate) fn in_memory_point_sink( + owned_scope: &[u32], +) -> (InMemoryPointSinkLeaseV1, InMemoryPointSinkProbeV1) { + let epoch = Rc::new(()); + let shared = Rc::new(TestSinkSharedV1 { + state: RefCell::new(TestSinkStateV1 { + snapshot: Vec::new(), + revision: None, + stamp: TestPublishedStampV1 { sequence: 0, epoch }, + reject_next_install: false, + counts: TestIntentCountsV1::default(), + revoke_count: 0, + sequence: 0, + revoke_sequence: None, + lease_drop_sequence: None, + revoke_saw_exact_stamp: false, + }), + busy: Cell::new(false), + reject_next_prepare: Cell::new(false), + rejected_prepare_saw_busy: Cell::new(false), + reject_next_install_after_swap: Cell::new(false), + misreport_next_confirm_proposed_stamp: Cell::new(false), + panic_on_retirement_drop: Cell::new(false), + retirement_drop_count: Cell::new(0), + }); + ( + InMemoryPointSinkLeaseV1 { + owned_scope: owned_scope + .iter() + .copied() + .map(TestSinkOutputIdV1::new) + .collect(), + shared: Rc::clone(&shared), + retired: None, + }, + InMemoryPointSinkProbeV1 { shared }, + ) +} + +pub(crate) const fn authored_emission( + output: u32, + sink_output: u32, +) -> AuthoredPointEmissionBindingV1 { + AuthoredPointEmissionBindingV1::new( + OutputSlotIdV1::new(output), + TestSinkOutputIdV1::new(sink_output), + ) +} + +pub(crate) const fn authored_presentation( + output: u32, + root: u32, + occurrence: u32, +) -> AuthoredPointPresentationBindingV1 { + AuthoredPointPresentationBindingV1::new( + OutputSlotIdV1::new(output), + PresentationRootIdV1::new(root), + OccurrenceIdV1::new(occurrence), + ) +} + +impl sink_private::Sealed for InMemoryPointSinkLeaseV1 {} + +impl LinearPointSinkLeaseV1 for InMemoryPointSinkLeaseV1 { + type OutputId = TestSinkOutputIdV1; + type Stamp = TestPublishedStampV1; + type Error = InMemoryPointSinkErrorV1; + type Prepared<'lease> = InMemoryPreparedPointSinkWriteV1<'lease>; + + fn owned_output_scope(&self) -> &[Self::OutputId] { + &self.owned_scope + } + + fn prepare<'lease>( + &'lease mut self, + intent: PointSinkIntentV1<'_, Self::OutputId, Self::Stamp>, + ) -> Result, Self::Error> { + // Retirement предыдущего install завершается до Busy и до любых + // изменений нового физического снимка. + drop(self.retired.take()); + let (base_stamp, current_revision, busy) = { + let state = self.shared.state.borrow(); + (state.stamp.clone(), state.revision, self.shared.busy.get()) + }; + if busy { + return Err(InMemoryPointSinkErrorV1::Busy); + } + + let (staging, proposed, intent_kind) = match intent { + PointSinkIntentV1::SetAll { revision, patch } => { + let mut snapshot = Vec::new(); + snapshot + .try_reserve_exact(patch.len()) + .map_err(|_| InMemoryPointSinkErrorV1::ResourceExhausted)?; + snapshot.extend(patch.iter().copied().map(|entry| TestSnapshotEntryV1 { + output: entry.output(), + sink_output: entry.sink_output(), + paint: entry.paint(), + })); + ( + TestStagingV1::SetAll { revision, snapshot }, + base_stamp.next()?, + TestIntentKindV1::SetAll, + ) + } + PointSinkIntentV1::RevokeAll { revision } => ( + TestStagingV1::RevokeAll { + revision, + retired: Vec::new(), + }, + base_stamp.next()?, + TestIntentKindV1::RevokeAll, + ), + PointSinkIntentV1::ConfirmExact { + revision, + published_stamp, + } => { + if published_stamp != &base_stamp || current_revision != Some(revision) { + return Err(InMemoryPointSinkErrorV1::StampMismatch); + } + let proposed = if self + .shared + .misreport_next_confirm_proposed_stamp + .replace(false) + { + published_stamp.next()? + } else { + published_stamp.clone() + }; + ( + TestStagingV1::ConfirmExact { revision }, + proposed, + TestIntentKindV1::ConfirmExact, + ) + } + }; + + { + let mut state = self.shared.state.borrow_mut(); + if self.shared.busy.get() || state.stamp != base_stamp { + return Err(InMemoryPointSinkErrorV1::Busy); + } + self.shared.busy.set(true); + // Счётчики фиксируют каждый intent, получивший linear Busy; + // test-only fault injection после этого тоже считается попыткой. + match intent_kind { + TestIntentKindV1::SetAll => state.counts.set_all += 1, + TestIntentKindV1::RevokeAll => state.counts.revoke_all += 1, + TestIntentKindV1::ConfirmExact => state.counts.confirm_exact += 1, + } + } + if self.shared.reject_next_prepare.replace(false) { + self.shared + .rejected_prepare_saw_busy + .set(self.shared.busy.get()); + self.shared.busy.set(false); + return Err(InMemoryPointSinkErrorV1::RejectedPrepare); + } + + let retirement_probe = Some(Rc::clone(&self.shared)); + Ok(InMemoryPreparedPointSinkWriteV1 { + lease: self, + base_stamp: Some(base_stamp), + proposed: Some(proposed), + staging: Some(staging), + retired_stamp: None, + retirement_probe, + finished: false, + }) + } + + fn revoke_all_before_release(&mut self, published_stamp: Option<&Self::Stamp>) { + let mut state = self.shared.state.borrow_mut(); + state.revoke_saw_exact_stamp = published_stamp.is_none_or(|stamp| stamp == &state.stamp); + state.snapshot.clear(); + state.revision = None; + state.revoke_count += 1; + state.sequence = state.sequence.saturating_add(1); + state.revoke_sequence = Some(state.sequence); + } +} + +impl Drop for InMemoryPointSinkLeaseV1 { + fn drop(&mut self) { + let mut state = self.shared.state.borrow_mut(); + state.sequence = state.sequence.saturating_add(1); + state.lease_drop_sequence = Some(state.sequence); + } +} + +#[derive(Clone, Copy)] +enum TestIntentKindV1 { + SetAll, + RevokeAll, + ConfirmExact, +} + +enum TestStagingV1 { + SetAll { + revision: u64, + snapshot: Vec, + }, + RevokeAll { + revision: u64, + retired: Vec, + }, + ConfirmExact { + revision: u64, + }, +} + +struct TestSinkRetirementV1 { + _staging: Option, + _retired_stamp: Option, + _proposed: Option, + _base_stamp: Option, + probe: Option>, +} + +impl Drop for TestSinkRetirementV1 { + fn drop(&mut self) { + if let Some(probe) = &self.probe { + probe + .retirement_drop_count + .set(probe.retirement_drop_count.get() + 1); + if probe.panic_on_retirement_drop.replace(false) { + panic!("sink retirement crossed the physical install boundary"); + } + } + } +} + +pub(crate) struct InMemoryPreparedPointSinkWriteV1<'lease> { + lease: &'lease mut InMemoryPointSinkLeaseV1, + base_stamp: Option, + proposed: Option, + staging: Option, + retired_stamp: Option, + retirement_probe: Option>, + finished: bool, +} + +impl PreparedPointSinkWriteV1 for InMemoryPreparedPointSinkWriteV1<'_> { + type Stamp = TestPublishedStampV1; + type Error = InMemoryPointSinkErrorV1; + + fn proposed_stamp(&self) -> &Self::Stamp { + self.proposed + .as_ref() + .unwrap_or_else(|| unreachable!("proposed stamp читается до install")) + } + + fn try_install(&mut self) -> Result<(), Self::Error> { + let proposed = match self.proposed.take() { + Some(proposed) => proposed, + None => return Err(InMemoryPointSinkErrorV1::StampMismatch), + }; + if self.retired_stamp.is_some() { + return Err(InMemoryPointSinkErrorV1::StampMismatch); + } + let staging = match self.staging.as_mut() { + Some(staging) => staging, + None => return Err(InMemoryPointSinkErrorV1::StampMismatch), + }; + let mut state = self.lease.shared.state.borrow_mut(); + if state.reject_next_install { + state.reject_next_install = false; + return Err(InMemoryPointSinkErrorV1::RejectedInstall); + } + if self.base_stamp.as_ref() != Some(&state.stamp) { + return Err(InMemoryPointSinkErrorV1::StampMismatch); + } + + let prior_revision = state.revision; + let revision = match staging { + TestStagingV1::SetAll { revision, .. } | TestStagingV1::RevokeAll { revision, .. } => { + *revision + } + TestStagingV1::ConfirmExact { revision } => { + if state.revision != Some(*revision) { + return Err(InMemoryPointSinkErrorV1::StampMismatch); + } + *revision + } + }; + match staging { + TestStagingV1::SetAll { snapshot, .. } => { + mem::swap(&mut state.snapshot, snapshot); + } + TestStagingV1::RevokeAll { retired, .. } => { + mem::swap(&mut state.snapshot, retired); + } + TestStagingV1::ConfirmExact { .. } => {} + } + self.retired_stamp = Some(mem::replace(&mut state.stamp, proposed)); + state.revision = Some(revision); + if self + .lease + .shared + .reject_next_install_after_swap + .replace(false) + { + match staging { + TestStagingV1::SetAll { snapshot, .. } => { + mem::swap(&mut state.snapshot, snapshot); + } + TestStagingV1::RevokeAll { retired, .. } => { + mem::swap(&mut state.snapshot, retired); + } + TestStagingV1::ConfirmExact { .. } => {} + } + let retired_stamp = self + .retired_stamp + .take() + .unwrap_or_else(|| unreachable!("install уже перенёс прежний stamp")); + self.proposed = Some(mem::replace(&mut state.stamp, retired_stamp)); + state.revision = prior_revision; + return Err(InMemoryPointSinkErrorV1::RejectedInstallAfterSwap); + } + Ok(()) + } + + fn finish_after_session(mut self) { + let retirement = TestSinkRetirementV1 { + _staging: self.staging.take(), + _retired_stamp: self.retired_stamp.take(), + _proposed: self.proposed.take(), + _base_stamp: self.base_stamp.take(), + probe: self.retirement_probe.take(), + }; + self.lease.retired = Some(retirement); + self.lease.shared.busy.set(false); + self.finished = true; + } +} + +impl Drop for InMemoryPreparedPointSinkWriteV1<'_> { + fn drop(&mut self) { + if !self.finished { + drop(self.staging.take()); + drop(self.retired_stamp.take()); + drop(self.proposed.take()); + drop(self.base_stamp.take()); + drop(self.retirement_probe.take()); + self.lease.shared.busy.set(false); + } + } +} diff --git a/crates/labcolors-core/src/program/attachment/tests.rs b/crates/labcolors-core/src/program/attachment/tests.rs new file mode 100644 index 00000000..3715db55 --- /dev/null +++ b/crates/labcolors-core/src/program/attachment/tests.rs @@ -0,0 +1,814 @@ +use super::support::{ + InMemoryPointSinkErrorV1, authored_emission, authored_presentation, in_memory_point_sink, +}; +use super::*; +use crate::Srgb8; +use crate::program::{ + AppearanceContextV1, ConstraintIdV1, DraftV1, PaintIdV1, ScenarioV1, SourceIdV1, StateKindV1, + SurfaceIdV1, SurfaceInputPortIdV1, SurroundV1, TargetIdV1, +}; + +const SOURCE: SourceIdV1 = SourceIdV1::new(1); +const TARGET: TargetIdV1 = TargetIdV1::new(2); +const PAINT: PaintIdV1 = PaintIdV1::new(4); +const INPUT: SurfaceInputPortIdV1 = SurfaceInputPortIdV1::new(5); +const INPUT_SURFACE: SurfaceIdV1 = SurfaceIdV1::new(6); +const INNER: OccurrenceIdV1 = OccurrenceIdV1::new(8); +const TERMINAL: OccurrenceIdV1 = OccurrenceIdV1::new(9); +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); + +fn owner( + source: Srgb8, + expected: Srgb8, + extra_topology: bool, + outputs: &[OutputSlotIdV1], +) -> OwnerV1 { + owner_with_terminal_presentation(source, expected, extra_topology, outputs, false) +} + +fn owner_with_terminal_presentation( + source: Srgb8, + expected: Srgb8, + extra_topology: bool, + outputs: &[OutputSlotIdV1], + include_terminal: bool, +) -> OwnerV1 { + let context = AppearanceContextV1::try_new(64.0, 0.2, SurroundV1::Dim).unwrap(); + let inner_surface = SurfaceIdV1::new(7); + let middle_surface = SurfaceIdV1::new(16); + let has_middle = extra_topology || outputs.len() > 1; + let mut draft = DraftV1::new(); + draft.push_source(SOURCE, source); + draft.push_fixed_target(TARGET, SOURCE); + 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); + if has_middle { + draft.push_source_over_occurrence(MIDDLE, PAINT, inner_surface, context); + draft.push_occurrence_surface(middle_surface, MIDDLE); + draft.push_source_over_occurrence(TERMINAL, PAINT, middle_surface, context); + } else { + draft.push_source_over_occurrence(TERMINAL, PAINT, inner_surface, context); + } + draft.push_point_presentation_root(ROOT, TERMINAL); + draft.push_point_presentation_target(ROOT, INNER); + if has_middle { + draft.push_point_presentation_target(ROOT, MIDDLE); + } + if include_terminal { + draft.push_point_presentation_target(ROOT, TERMINAL); + } + draft.push_exact_hard(ConstraintIdV1::new(10), INNER, expected); + for output in outputs { + draft.push_output(*output, PAINT); + } + draft.compile().unwrap() +} + +fn observed<'a>(revision: u64, scenarios: &'a [ScenarioV1<'a>]) -> UpdateV1<'a> { + UpdateV1::Observed { + revision, + scenarios, + } +} + +#[test] +fn attach_rejects_missing_extra_duplicate_and_reordered_sink_scope() { + let owner = owner( + Srgb8::new([0, 0, 0]), + Srgb8::new([0, 0, 0]), + false, + &[OUTPUT_A, OUTPUT_B], + ); + let emissions = [ + authored_emission(OUTPUT_B.value(), 901), + authored_emission(OUTPUT_A.value(), 900), + ]; + let presentations = [ + authored_presentation(OUTPUT_B.value(), ROOT.value(), MIDDLE.value()), + authored_presentation(OUTPUT_A.value(), ROOT.value(), INNER.value()), + ]; + + let (missing, missing_probe) = in_memory_point_sink(&[900]); + assert!(matches!( + owner.attach(1, &emissions, &presentations, missing), + Err(AttachmentCreateErrorV1::SinkScopeCount { + expected: 2, + actual: 1 + }) + )); + assert_eq!(missing_probe.revoke_count(), 1); + assert!(missing_probe.revoke_saw_exact_stamp()); + assert!(missing_probe.revoked_before_lease_drop()); + + let (extra, _) = in_memory_point_sink(&[900, 901, 902]); + assert!(matches!( + owner.attach(1, &emissions, &presentations, extra), + Err(AttachmentCreateErrorV1::SinkScopeCount { + expected: 2, + actual: 3 + }) + )); + + let (duplicate, _) = in_memory_point_sink(&[900, 900]); + assert!(matches!( + owner.attach(1, &emissions, &presentations, duplicate), + Err(AttachmentCreateErrorV1::DuplicateSinkScopeOutput { .. }) + )); + + let (reordered, _) = in_memory_point_sink(&[901, 900]); + assert!(matches!( + owner.attach(1, &emissions, &presentations, reordered), + Err(AttachmentCreateErrorV1::SinkScopeMismatch { ordinal: 0, .. }) + )); + + let duplicate_emission = [ + authored_emission(OUTPUT_A.value(), 900), + authored_emission(OUTPUT_B.value(), 900), + ]; + let (sink, _) = in_memory_point_sink(&[900, 901]); + assert!(matches!( + owner.attach(1, &duplicate_emission, &presentations, sink), + Err(AttachmentCreateErrorV1::SinkOutputAliased { .. }) + )); +} + +#[test] +fn attach_requires_exact_bijection_over_compiled_presentations() { + let owner = owner_with_terminal_presentation( + Srgb8::new([0, 0, 0]), + Srgb8::new([0, 0, 0]), + false, + &[OUTPUT_A, OUTPUT_B], + true, + ); + let emissions = [ + authored_emission(OUTPUT_A.value(), 900), + authored_emission(OUTPUT_B.value(), 901), + ]; + let omitted_terminal = [ + authored_presentation(OUTPUT_A.value(), ROOT.value(), INNER.value()), + authored_presentation(OUTPUT_B.value(), ROOT.value(), MIDDLE.value()), + ]; + let (sink, probe) = in_memory_point_sink(&[900, 901]); + assert!(matches!( + owner.attach(2, &emissions, &omitted_terminal, sink), + Err(AttachmentCreateErrorV1::PresentationCount { + expected: 3, + actual: 2 + }) + )); + assert_eq!(probe.revoke_count(), 1); + assert!(probe.revoked_before_lease_drop()); +} + +#[test] +fn alias_outputs_cannot_claim_the_same_compiled_presentation() { + let owner = owner( + Srgb8::new([0, 0, 0]), + Srgb8::new([0, 0, 0]), + false, + &[OUTPUT_A, OUTPUT_B], + ); + let emissions = [ + authored_emission(OUTPUT_A.value(), 900), + authored_emission(OUTPUT_B.value(), 901), + ]; + let duplicate_actual_target = [ + authored_presentation(OUTPUT_A.value(), ROOT.value(), INNER.value()), + authored_presentation(OUTPUT_B.value(), ROOT.value(), INNER.value()), + ]; + let (sink, _) = in_memory_point_sink(&[900, 901]); + assert!(matches!( + owner.attach(3, &emissions, &duplicate_actual_target, sink), + Err(AttachmentCreateErrorV1::DuplicatePresentation { + root: ROOT, + occurrence: INNER, + first_output: OUTPUT_A, + second_output: OUTPUT_B, + }) + )); +} + +#[test] +fn every_emission_requires_at_least_one_distinct_compiled_presentation() { + let owner = owner( + Srgb8::new([0, 0, 0]), + Srgb8::new([0, 0, 0]), + false, + &[OUTPUT_A, OUTPUT_B], + ); + let emissions = [ + authored_emission(OUTPUT_A.value(), 900), + authored_emission(OUTPUT_B.value(), 901), + ]; + let only_output_a = [ + authored_presentation(OUTPUT_A.value(), ROOT.value(), INNER.value()), + authored_presentation(OUTPUT_A.value(), ROOT.value(), MIDDLE.value()), + ]; + let (sink, _) = in_memory_point_sink(&[900, 901]); + + assert!(matches!( + owner.attach(4, &emissions, &only_output_a, sink), + Err(AttachmentCreateErrorV1::MissingOutputPresentation { output: OUTPUT_B }) + )); +} + +#[test] +fn duplicate_emission_output_has_its_exact_typed_error() { + let owner = owner( + Srgb8::new([0, 0, 0]), + Srgb8::new([0, 0, 0]), + false, + &[OUTPUT_A, OUTPUT_B], + ); + let duplicate_output = [ + authored_emission(OUTPUT_A.value(), 900), + authored_emission(OUTPUT_A.value(), 901), + ]; + let presentations = [ + authored_presentation(OUTPUT_A.value(), ROOT.value(), INNER.value()), + authored_presentation(OUTPUT_B.value(), ROOT.value(), MIDDLE.value()), + ]; + let (sink, _) = in_memory_point_sink(&[900, 901]); + + assert!(matches!( + owner.attach(5, &duplicate_output, &presentations, sink), + Err(AttachmentCreateErrorV1::DuplicateEmissionOutput { output: OUTPUT_A }) + )); +} + +#[test] +fn verified_snapshot_mints_attached_render_output_and_exact_confirm_only_for_idempotence() { + let owner = owner( + Srgb8::new([12, 34, 56]), + Srgb8::new([12, 34, 56]), + false, + &[OUTPUT_A, OUTPUT_B], + ); + let (sink, probe) = in_memory_point_sink(&[900, 901]); + let emissions = [ + authored_emission(OUTPUT_B.value(), 901), + authored_emission(OUTPUT_A.value(), 900), + ]; + let presentations = [ + 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 values = [Srgb8::new([0, 0, 0])]; + let scenarios = [ScenarioV1::new(44, &values)]; + + { + let committed = attachment.update(observed(1, &scenarios)).unwrap(); + assert_eq!(committed.evidence().kind(), StateKindV1::Ready); + let outputs: Vec<_> = committed.render_outputs().collect(); + assert_eq!(outputs.len(), 2); + let output = outputs[0]; + assert_eq!(output.certificate().observation().revision(), 1); + assert_eq!(output.output(), OUTPUT_A); + assert_eq!(output.paint().source(), Srgb8::new([12, 34, 56])); + assert_eq!(output.root(), ROOT); + assert_eq!(output.occurrence(), INNER); + assert_eq!(output.sink_output().value(), 900); + assert_eq!(output.published_stamp().revision(), 1); + assert_eq!(outputs[1].output(), OUTPUT_B); + assert_eq!(outputs[1].occurrence(), MIDDLE); + assert_eq!(outputs[1].sink_output().value(), 901); + } + assert_eq!(probe.intent_counts().set_all, 1); + assert_eq!(probe.intent_counts().confirm_exact, 0); + + { + let committed = attachment.update(observed(1, &scenarios)).unwrap(); + assert_eq!(committed.render_outputs().len(), 2); + } + assert_eq!(probe.intent_counts().set_all, 1); + assert_eq!(probe.intent_counts().confirm_exact, 1); + + attachment.update(observed(2, &scenarios)).unwrap(); + assert_eq!(probe.intent_counts().set_all, 2); + assert_eq!(probe.intent_counts().confirm_exact, 1); + assert_eq!(probe.revision(), Some(2)); +} + +#[test] +fn confirm_exact_rejects_a_sink_that_misreports_its_proposed_stamp() { + let owner = owner( + Srgb8::new([12, 34, 56]), + Srgb8::new([12, 34, 56]), + false, + &[OUTPUT_A], + ); + 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 mut attachment = owner.attach(6, &emissions, &presentations, sink).unwrap(); + let values = [Srgb8::new([0, 0, 0])]; + let scenarios = [ScenarioV1::new(44, &values)]; + attachment.update(observed(1, &scenarios)).unwrap(); + let prior_snapshot = probe.snapshot(); + + probe.misreport_next_confirm_proposed_stamp(); + assert!(matches!( + attachment.update(observed(1, &scenarios)), + Err(AttachmentUpdateErrorV1::InternalInvariant( + AttachmentInvariantV1::ConfirmStampMismatch + )) + )); + assert_eq!(probe.snapshot(), prior_snapshot); + assert_eq!(probe.revision(), Some(1)); + assert!(!probe.is_busy()); + match attachment.session.evidence().observation_head() { + super::super::ObservationHeadV1::Observed { stream, revision } => { + assert_eq!(stream.value(), 6); + assert_eq!(revision, 1); + } + _ => panic!("rejected confirm must preserve the prior observed head"), + } + + let committed = attachment.update(observed(1, &scenarios)).unwrap(); + assert_eq!(committed.render_outputs().len(), 1); + assert_eq!(probe.intent_counts().confirm_exact, 2); +} + +#[test] +fn one_emission_fans_out_to_every_distinct_attached_presentation() { + let owner = owner( + Srgb8::new([21, 22, 23]), + Srgb8::new([21, 22, 23]), + true, + &[OUTPUT_A], + ); + 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(), MIDDLE.value()), + authored_presentation(OUTPUT_A.value(), ROOT.value(), INNER.value()), + ]; + let mut attachment = owner.attach(70, &emissions, &presentations, sink).unwrap(); + let values = [Srgb8::new([0, 0, 0])]; + let scenarios = [ScenarioV1::new(44, &values)]; + + let committed = attachment.update(observed(1, &scenarios)).unwrap(); + assert_eq!(probe.snapshot().len(), 1); + assert_eq!(probe.snapshot()[0].output(), OUTPUT_A); + let outputs: Vec<_> = committed.render_outputs().collect(); + assert_eq!(outputs.len(), 2); + assert_eq!(outputs[0].occurrence(), INNER); + assert_eq!(outputs[1].occurrence(), MIDDLE); + assert!( + outputs + .iter() + .all(|output| output.output() == OUTPUT_A && output.sink_output().value() == 900) + ); +} + +#[test] +fn unknown_and_known_violation_revoke_the_complete_snapshot() { + let pass_owner = owner( + Srgb8::new([0, 0, 0]), + Srgb8::new([0, 0, 0]), + false, + &[OUTPUT_A], + ); + 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 mut attachment = pass_owner + .attach(8, &emissions, &presentations, sink) + .unwrap(); + let values = [Srgb8::new([0, 0, 0])]; + let scenarios = [ScenarioV1::new(1, &values)]; + attachment.update(observed(1, &scenarios)).unwrap(); + assert_eq!(probe.snapshot().len(), 1); + + let unknown = UpdateV1::Unknown { + revision: 2, + reason_id: 77, + }; + let committed = attachment.update(unknown).unwrap(); + assert_eq!(committed.evidence().kind(), StateKindV1::Stale); + assert_eq!(committed.render_outputs().len(), 0); + assert!(probe.snapshot().is_empty()); + assert_eq!(probe.intent_counts().revoke_all, 1); + attachment.update(unknown).unwrap(); + assert_eq!(probe.intent_counts().confirm_exact, 1); + + let conflict_owner = owner( + Srgb8::new([255, 0, 0]), + Srgb8::new([0, 0, 0]), + false, + &[OUTPUT_A], + ); + let (sink, conflict_probe) = in_memory_point_sink(&[900]); + let mut conflict = conflict_owner + .attach(9, &emissions, &presentations, sink) + .unwrap(); + let committed = conflict.update(observed(1, &scenarios)).unwrap(); + assert_eq!(committed.evidence().kind(), StateKindV1::Failed); + assert_eq!(committed.render_outputs().len(), 0); + assert!(conflict_probe.snapshot().is_empty()); + assert_eq!(conflict_probe.intent_counts().revoke_all, 1); +} + +#[test] +fn rejected_install_keeps_session_snapshot_and_releases_busy() { + let owner = owner( + Srgb8::new([4, 5, 6]), + Srgb8::new([4, 5, 6]), + false, + &[OUTPUT_A], + ); + 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 mut attachment = owner.attach(10, &emissions, &presentations, sink).unwrap(); + let values = [Srgb8::new([0, 0, 0])]; + let scenarios = [ScenarioV1::new(1, &values)]; + attachment.update(observed(1, &scenarios)).unwrap(); + let snapshot = probe.snapshot(); + + probe.reject_next_install(); + assert!(matches!( + attachment.update(observed(2, &scenarios)), + Err(AttachmentUpdateErrorV1::SinkInstall( + InMemoryPointSinkErrorV1::RejectedInstall + )) + )); + assert!(!probe.is_busy()); + assert_eq!(probe.revision(), Some(1)); + assert_eq!(probe.snapshot(), snapshot); + match attachment.session.evidence().observation_head() { + super::super::ObservationHeadV1::Observed { stream, revision } => { + assert_eq!(stream.value(), 10); + assert_eq!(revision, 1); + } + _ => panic!("rejected install must preserve the prior observed head"), + } + + attachment.update(observed(1, &scenarios)).unwrap(); + assert_eq!(probe.intent_counts().confirm_exact, 1); +} + +#[test] +fn every_fallible_sink_boundary_is_all_or_nothing() { + let owner = owner( + Srgb8::new([4, 5, 6]), + Srgb8::new([4, 5, 6]), + false, + &[OUTPUT_A], + ); + 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 mut attachment = owner.attach(73, &emissions, &presentations, sink).unwrap(); + let values = [Srgb8::new([0, 0, 0])]; + let scenarios = [ScenarioV1::new(1, &values)]; + let initial_stamp = probe.stamp(); + + probe.reject_next_install_after_swap(); + assert!(matches!( + attachment.update(observed(1, &scenarios)), + Err(AttachmentUpdateErrorV1::SinkInstall( + InMemoryPointSinkErrorV1::RejectedInstallAfterSwap + )) + )); + assert!(probe.snapshot().is_empty()); + assert_eq!(probe.revision(), None); + assert_eq!(probe.stamp(), initial_stamp); + assert!(!probe.is_busy()); + assert!(matches!( + attachment.session.evidence().observation_head(), + super::super::ObservationHeadV1::Empty + )); + + attachment.update(observed(1, &scenarios)).unwrap(); + let snapshot = probe.snapshot(); + let stamp = probe.stamp(); + + probe.reject_next_prepare(); + assert!(matches!( + attachment.update(observed(2, &scenarios)), + Err(AttachmentUpdateErrorV1::SinkPrepare( + InMemoryPointSinkErrorV1::RejectedPrepare + )) + )); + assert_eq!(probe.snapshot(), snapshot); + assert_eq!(probe.revision(), Some(1)); + assert_eq!(probe.stamp(), stamp); + assert!(!probe.is_busy()); + assert!(probe.rejected_prepare_saw_busy()); + + probe.reject_next_install_after_swap(); + let unknown = UpdateV1::Unknown { + revision: 2, + reason_id: 99, + }; + assert!(matches!( + attachment.update(unknown), + Err(AttachmentUpdateErrorV1::SinkInstall( + InMemoryPointSinkErrorV1::RejectedInstallAfterSwap + )) + )); + assert_eq!(probe.snapshot(), snapshot); + assert_eq!(probe.revision(), Some(1)); + assert_eq!(probe.stamp(), stamp); + assert!(!probe.is_busy()); + match attachment.session.evidence().observation_head() { + super::super::ObservationHeadV1::Observed { stream, revision } => { + assert_eq!(stream.value(), 73); + assert_eq!(revision, 1); + } + _ => panic!("fallible sink boundary must preserve the prior observed head"), + } + + let committed = attachment.update(observed(1, &scenarios)).unwrap(); + assert_eq!(committed.render_outputs().len(), 1); + assert_eq!(probe.intent_counts().confirm_exact, 1); +} + +#[test] +fn installed_retirement_waits_for_the_next_preinstall_drain_and_retry_is_clean() { + let owner = owner( + Srgb8::new([4, 5, 6]), + Srgb8::new([4, 5, 6]), + false, + &[OUTPUT_A], + ); + 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 mut attachment = owner.attach(71, &emissions, &presentations, sink).unwrap(); + let values = [Srgb8::new([0, 0, 0])]; + let scenarios = [ScenarioV1::new(1, &values)]; + + probe.panic_on_next_retirement_drop(); + let first = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let committed = attachment.update(observed(1, &scenarios)).unwrap(); + assert_eq!(committed.evidence().kind(), StateKindV1::Ready); + })); + assert!( + first.is_ok(), + "post-install finish must only park retirement" + ); + assert_eq!(probe.retirement_drop_count(), 0); + assert_eq!(probe.revision(), Some(1)); + assert!(!probe.is_busy()); + let installed_snapshot = probe.snapshot(); + + let drain = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let _ = attachment.update(observed(2, &scenarios)); + })); + assert!(drain.is_err(), "hostile retirement destructor must run"); + assert_eq!(probe.retirement_drop_count(), 1); + assert_eq!(probe.revision(), Some(1)); + assert_eq!(probe.snapshot(), installed_snapshot); + assert!(!probe.is_busy()); + match attachment.session.evidence().observation_head() { + super::super::ObservationHeadV1::Observed { stream, revision } => { + assert_eq!(stream.value(), 71); + assert_eq!(revision, 1); + } + _ => panic!("pre-install retirement panic must preserve Session"), + } + + attachment.update(observed(2, &scenarios)).unwrap(); + assert_eq!(probe.revision(), Some(2)); + assert!(!probe.is_busy()); +} + +#[test] +fn source_guards_keep_the_post_install_tail_destructor_free() { + let attachment_source = include_str!("../attachment.rs"); + let support_source = include_str!("support.rs"); + + assert!( + attachment_source.contains("transition.commit_deferred()"), + "Attachment must publish through the deferred Session commit seam", + ); + assert!( + !attachment_source.contains("let _ = transition.commit();"), + "eager Session commit must not return to the post-install tail", + ); + + let session_drain = attachment_source + .find("drop(self.retired_session.take())") + .expect("Attachment must drain deferred Session retirement"); + let stamp_drain = attachment_source + .find("drop(self.retired_stamp.take())") + .expect("Attachment must drain deferred stamp retirement"); + let prepare = attachment_source + .find(".prepare_update(update)") + .expect("Attachment must prepare one Session transition"); + let install = attachment_source + .find(".try_install()") + .expect("Attachment must install the prepared sink transaction"); + assert!( + session_drain < prepare && prepare < install, + "Session retirement must drain before prepare and install", + ); + assert!( + stamp_drain < prepare && prepare < install, + "stamp retirement must drain before prepare and install", + ); + + let sink_prepare = support_source + .split("fn prepare<'lease>(") + .nth(1) + .expect("test sink must implement prepare") + .split("fn revoke_all_before_release") + .next() + .expect("prepare body must precede revoke implementation"); + assert!( + sink_prepare + .find("drop(self.retired.take())") + .expect("prepare must drain retired sink state") + < sink_prepare + .find("self.shared.busy.set(true)") + .expect("prepare must acquire Busy"), + "retired sink state must drain before Busy is acquired", + ); + + let finish = support_source + .split("fn finish_after_session(mut self)") + .nth(1) + .expect("prepared sink must implement finish_after_session") + .split("impl Drop for InMemoryPreparedPointSinkWriteV1") + .next() + .expect("finish body must precede prepared-sink Drop"); + assert!( + !finish.contains("drop("), + "post-install finish must not run a destructor", + ); + assert!( + !finish.contains("borrow_mut"), + "post-install finish must not enter a fallible RefCell borrow", + ); + for owning_take in [ + "_staging: self.staging.take()", + "_retired_stamp: self.retired_stamp.take()", + "_proposed: self.proposed.take()", + "_base_stamp: self.base_stamp.take()", + "probe: self.retirement_probe.take()", + ] { + assert!( + finish.contains(owning_take), + "finish must park every owning field: {owning_take}" + ); + } + assert!( + finish + .find("self.lease.retired = Some(retirement)") + .expect("finish must park retirement") + < finish + .find("self.lease.shared.busy.set(false)") + .expect("finish must release Busy"), + "finish must park every retired owner before releasing Busy", + ); +} + +#[test] +fn dispose_revokes_before_hostile_retirement_destructor_runs() { + let owner = owner( + Srgb8::new([4, 5, 6]), + Srgb8::new([4, 5, 6]), + false, + &[OUTPUT_A], + ); + 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 mut attachment = owner.attach(72, &emissions, &presentations, sink).unwrap(); + let values = [Srgb8::new([0, 0, 0])]; + let scenarios = [ScenarioV1::new(1, &values)]; + + probe.panic_on_next_retirement_drop(); + attachment.update(observed(1, &scenarios)).unwrap(); + assert_eq!(probe.retirement_drop_count(), 0); + + let disposed = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + attachment.dispose(); + })); + assert!(disposed.is_err(), "hostile retirement destructor must run"); + assert_eq!(probe.retirement_drop_count(), 1); + assert_eq!(probe.revoke_count(), 1); + assert!(probe.snapshot().is_empty()); + assert!(probe.revoked_before_lease_drop()); +} + +#[test] +fn same_ids_and_ordinals_cannot_pair_a_foreign_generation_token_with_the_pin() { + let owner_a = owner( + Srgb8::new([0, 0, 0]), + Srgb8::new([0, 0, 0]), + false, + &[OUTPUT_A], + ); + let owner_b = owner( + Srgb8::new([255, 0, 0]), + Srgb8::new([255, 0, 0]), + true, + &[OUTPUT_A], + ); + let token_a = owner_a + .compiled + .bind_point_output_presentation(OUTPUT_A.into_core(), ROOT.into_core(), INNER.into_core()) + .unwrap(); + let token_b = owner_b + .compiled + .bind_point_output_presentation(OUTPUT_A.into_core(), ROOT.into_core(), INNER.into_core()) + .unwrap(); + assert_eq!(token_a.output(), token_b.output()); + assert_eq!(token_a.root(), token_b.root()); + assert_eq!(token_a.occurrence(), token_b.occurrence()); + assert_eq!(token_a.output_ordinal(), token_b.output_ordinal()); + assert_eq!( + token_a.presentation_ordinal(), + token_b.presentation_ordinal() + ); + assert_ne!( + owner_a.content_identity().as_bytes(), + owner_b.content_identity().as_bytes() + ); + + // `OwnerV1::attach` принимает только authored IDs и вместе минтит token_b + // с его pin, поэтому для token_a в API нет позиции. + 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()), + authored_presentation(OUTPUT_A.value(), ROOT.value(), MIDDLE.value()), + ]; + let mut attachment = owner_b + .attach(11, &emissions, &presentations, sink) + .unwrap(); + drop(owner_a); + drop(owner_b); + + let values = [Srgb8::new([0, 0, 0])]; + let scenarios = [ScenarioV1::new(1, &values)]; + attachment.update(observed(1, &scenarios)).unwrap(); + assert_eq!( + probe.snapshot()[0].paint().source(), + Srgb8::new([255, 0, 0]) + ); +} + +#[test] +fn dispose_revokes_before_lease_session_and_owner_pin_release() { + let owner = owner( + Srgb8::new([1, 2, 3]), + Srgb8::new([1, 2, 3]), + false, + &[OUTPUT_A], + ); + 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 mut attachment = owner.attach(12, &emissions, &presentations, sink).unwrap(); + drop(owner); + let values = [Srgb8::new([0, 0, 0])]; + let scenarios = [ScenarioV1::new(1, &values)]; + attachment.update(observed(1, &scenarios)).unwrap(); + assert_eq!(probe.snapshot().len(), 1); + + attachment.dispose(); + assert!(probe.snapshot().is_empty()); + assert_eq!(probe.revoke_count(), 1); + assert!(probe.revoke_saw_exact_stamp()); + assert!(probe.revoked_before_lease_drop()); +} diff --git a/crates/labcolors-core/src/program_api_tests.rs b/crates/labcolors-core/src/program_api_tests.rs index 39dea0cb..e4882286 100644 --- a/crates/labcolors-core/src/program_api_tests.rs +++ b/crates/labcolors-core/src/program_api_tests.rs @@ -43,11 +43,18 @@ fn staged_program_api_is_module_qualified_without_transport_prefixes() { }, ) .unwrap(); - let Some(program::OperationV1::Set(set)) = projection.operations().next() else { - panic!("the exact program must emit one certified Set"); + let mut certificates = projection.certificates(); + let Some(program::CertificateV1::Verified(certificate)) = certificates.next() else { + panic!("the exact program must retain one Verified certificate"); }; - assert_eq!(set.output_slot(), output); - assert_eq!(set.source(), Srgb8::new([0, 0, 0])); + assert!(certificates.next().is_none()); + let mut outputs = certificate.outputs(); + let Some(result) = outputs.next() else { + panic!("the exact program must retain one certified Paint output"); + }; + assert!(outputs.next().is_none()); + assert_eq!(result.output_slot(), output); + assert_eq!(result.source(), Srgb8::new([0, 0, 0])); } #[test] diff --git a/crates/labcolors-core/src/program_boundary_tests.rs b/crates/labcolors-core/src/program_boundary_tests.rs index 4efee661..0188aee4 100644 --- a/crates/labcolors-core/src/program_boundary_tests.rs +++ b/crates/labcolors-core/src/program_boundary_tests.rs @@ -10,31 +10,30 @@ use crate::program::{ AppearanceContextErrorKindV1, AppearanceContextFieldV1, AppearanceContextV1, AssessmentV1, CertificateV1, CompileErrorHandleV1, CompileErrorKindV1, CompileErrorV1, ConstraintIdV1, ConstraintSubjectV1, ContentIdentityV3, DraftErrorV1, DraftV1, EvidenceBoundsErrorV1, - InstantiateErrorV1, JointChoiceV1, JointOrderErrorV1, JointStateV1, NumericDomainErrorV1, - ObservationHeadV1, OccurrenceIdV1, OpacityInputIdV1, OperationV1, OutputSlotIdV1, OwnerV1, - PaintIdV1, PhysicalPointV1, PresentationRootIdV1, ProjectionV1, ScenarioV1, SessionV1, - SignalV1, SourceIdV1, StateKindV1, SurfaceIdV1, SurfaceInputPortIdV1, SurroundV1, - TargetCandidateIdV1, TargetCandidateV1, TargetIdV1, UpdateErrorKindV1, UpdateErrorV1, UpdateV1, - VerdictV1, + EvidenceViewV1, InstantiateErrorV1, JointChoiceV1, JointOrderErrorV1, JointStateV1, + NumericDomainErrorV1, ObservationHeadV1, OccurrenceIdV1, OpacityInputIdV1, OutputSlotIdV1, + OwnerV1, PaintIdV1, PhysicalPointV1, PresentationRootIdV1, ScenarioV1, SessionV1, SignalV1, + SourceIdV1, StateKindV1, SurfaceIdV1, SurfaceInputPortIdV1, SurroundV1, TargetCandidateIdV1, + TargetCandidateV1, TargetIdV1, UpdateErrorKindV1, UpdateErrorV1, UpdateV1, VerdictV1, }; use crate::wcag22::Wcag22CriterionV1; /// Existing Program characterizations commit the prepared lifecycle through /// this test-only extension; production exposes no immediate update method. pub(crate) trait CommitProgramUpdateForTest { - fn commit<'owner, 'session>( - &'owner self, + fn commit<'session>( + &self, session: &'session mut SessionV1, update: UpdateV1<'_>, - ) -> Result, UpdateErrorV1>; + ) -> Result, UpdateErrorV1>; } impl CommitProgramUpdateForTest for OwnerV1 { - fn commit<'owner, 'session>( - &'owner self, + fn commit<'session>( + &self, session: &'session mut SessionV1, update: UpdateV1<'_>, - ) -> Result, UpdateErrorV1> { + ) -> Result, UpdateErrorV1> { self.prepare_update(session, update) .map(|prepared| prepared.commit()) } @@ -65,12 +64,11 @@ fn wasm_can_use_only_the_concrete_owner_and_session( let view = owner .commit(session, update) .expect("well-formed owner-bound update"); - assert_projection_is_owner_bound(view); + assert_evidence_snapshot(view); Ok(()) } -fn assert_projection_is_owner_bound(projection: ProjectionV1<'_, '_>) { - let view = projection.evidence(); +fn assert_evidence_snapshot(view: EvidenceViewV1<'_>) { let _kind: StateKindV1 = view.kind(); match view.observation_head() { ObservationHeadV1::Empty => {} @@ -89,7 +87,6 @@ fn assert_projection_is_owner_bound(projection: ProjectionV1<'_, '_>) { } } let certificates = exact_size(view.certificates()); - let certificate_count = certificates.len(); for certificate in certificates { let _: &[u8; 32] = certificate.content_identity().as_bytes(); let observation = certificate.observation(); @@ -190,20 +187,6 @@ fn assert_projection_is_owner_bound(projection: ProjectionV1<'_, '_>) { } } } - for operation in exact_size(projection.operations()) { - match operation { - OperationV1::Set(set) => { - let _: OutputSlotIdV1 = set.output_slot(); - let _: Srgb8 = set.source(); - assert!(set.opacity().is_finite() && (0.0..=1.0).contains(&set.opacity())); - let _ = set.certificate().content_identity(); - } - OperationV1::Remove(remove) => { - let _: OutputSlotIdV1 = remove.output_slot(); - } - } - } - let _ = certificate_count; } #[allow(dead_code)] @@ -582,8 +565,7 @@ fn evidence_cell_bounds_cover_actual_joint_conflict_and_duplicate_case_reduction }, ) .unwrap(); - let Some(CertificateV1::Conflict(certificate)) = projection.evidence().certificates().next() - else { + let Some(CertificateV1::Conflict(certificate)) = projection.certificates().next() else { panic!("both authored joint states must violate the hard exact constraint"); }; assert_eq!(certificate.cells().len(), joint_bounds.conflict_cells()); @@ -607,8 +589,7 @@ fn evidence_cell_bounds_cover_actual_joint_conflict_and_duplicate_case_reduction }, ) .unwrap(); - let Some(CertificateV1::Verified(certificate)) = projection.evidence().certificates().next() - else { + let Some(CertificateV1::Verified(certificate)) = projection.certificates().next() else { panic!("duplicate physical scenarios must preserve a valid certificate"); }; assert!(certificate.cells().len() < fixed_bounds.verified_cells()); @@ -647,9 +628,7 @@ fn evidence_cell_bounds_query_is_pure_across_session_updates() { }, ) .unwrap(); - let Some(CertificateV1::Verified(certificate)) = - projection.evidence().certificates().next() - else { + let Some(CertificateV1::Verified(certificate)) = projection.certificates().next() else { panic!("the fixed admissible program must produce Verified evidence"); }; assert_eq!(certificate.cells().len(), expected.verified_cells()); @@ -667,7 +646,7 @@ fn evidence_cell_bounds_query_is_pure_across_session_updates() { }, ) .unwrap(); - assert_eq!(projection.evidence().kind(), StateKindV1::Ready); + assert_eq!(projection.kind(), StateKindV1::Ready); } #[test] @@ -744,16 +723,16 @@ fn staged_authoring_lowers_the_actual_closed_program_and_returns_canonical_input }, ) .unwrap(); - assert_eq!(ready.evidence().kind(), StateKindV1::Ready); - let mut operations = ready.operations(); - let Some(OperationV1::Set(set)) = operations.next() else { - panic!("Ready must emit one Set operation"); + assert_eq!(ready.kind(), StateKindV1::Ready); + let Some(CertificateV1::Verified(certificate)) = ready.certificates().next() else { + panic!("Ready must retain one Verified certificate"); }; - assert_eq!(set.output_slot(), output); - assert_eq!(set.source(), Srgb8::new([0; 3])); - assert_eq!(set.opacity(), 1.0); - assert_eq!(set.certificate().observation().revision(), 1); - assert!(operations.next().is_none()); + let outputs = certificate.outputs().collect::>(); + assert_eq!(outputs.len(), 1); + assert_eq!(outputs[0].output_slot(), output); + assert_eq!(outputs[0].source(), Srgb8::new([0; 3])); + assert_eq!(outputs[0].opacity(), 1.0); + assert_eq!(certificate.observation().revision(), 1); } #[test] @@ -794,26 +773,22 @@ fn every_physical_constructor_and_both_remaining_constraint_modes_execute() { }, ) .unwrap(); - assert_eq!(state.evidence().kind(), StateKindV1::Ready); - let Some(CertificateV1::Verified(certificate)) = state.evidence().certificates().next() else { + assert_eq!(state.kind(), StateKindV1::Ready); + let Some(CertificateV1::Verified(certificate)) = state.certificates().next() else { panic!("a fixed target must produce one Verified certificate"); }; assert_eq!(certificate.selected_state_index(), None); - let mut operations = state.operations(); - let Some(OperationV1::Set(set)) = operations.next() else { - panic!("Ready must emit one Set operation"); - }; - assert_eq!(set.output_slot(), OutputSlotIdV1::new(12)); - assert_eq!(set.source(), Srgb8::new([0; 3])); - assert_eq!(set.opacity(), 1.0); - assert_eq!(set.certificate().observation().revision(), 1); - assert!(operations.next().is_none()); + let outputs = certificate.outputs().collect::>(); + assert_eq!(outputs.len(), 1); + assert_eq!(outputs[0].output_slot(), OutputSlotIdV1::new(12)); + assert_eq!(outputs[0].source(), Srgb8::new([0; 3])); + assert_eq!(outputs[0].opacity(), 1.0); + assert_eq!(certificate.observation().revision(), 1); } #[test] -fn observed_violation_removes_outputs_but_retains_previous_certificate_as_evidence() { +fn observed_violation_retains_previous_certificate_only_as_evidence() { let input = SurfaceInputPortIdV1::new(50); - let output = OutputSlotIdV1::new(12); let owner = fixed_nested_draft(0.5, SourceIdV1::new(1), input, input) .compile() .unwrap(); @@ -830,10 +805,7 @@ fn observed_violation_removes_outputs_but_retains_previous_certificate_as_eviden }, ) .unwrap(); - assert!(matches!( - ready.operations().next(), - Some(OperationV1::Set(_)) - )); + assert_eq!(ready.kind(), StateKindV1::Ready); let white = [Srgb8::new([0xFF; 3])]; let white_scenarios = [ScenarioV1::new(2, &white)]; @@ -846,25 +818,18 @@ fn observed_violation_removes_outputs_but_retains_previous_certificate_as_eviden }, ) .unwrap(); - assert_eq!(failed.evidence().kind(), StateKindV1::Failed); - let certificates = failed.evidence().certificates().collect::>(); + assert_eq!(failed.kind(), StateKindV1::Failed); + let certificates = failed.certificates().collect::>(); assert_eq!(certificates.len(), 2); assert!(matches!(certificates[0], CertificateV1::Conflict(_))); let CertificateV1::Verified(previous) = certificates[1] else { panic!("the previous certificate must remain available as diagnostics"); }; assert_eq!(previous.observation().revision(), 1); - - let mut operations = failed.operations(); - let Some(OperationV1::Remove(remove)) = operations.next() else { - panic!("a known violation of the current context must remove the old output"); - }; - assert_eq!(remove.output_slot(), output); - assert!(operations.next().is_none()); } #[test] -fn program_prepare_drop_is_invisible_and_commit_returns_the_new_projection() { +fn program_prepare_drop_is_invisible_and_commit_returns_the_new_evidence() { let input = SurfaceInputPortIdV1::new(50); let owner = fixed_nested_draft(0.5, SourceIdV1::new(1), input, input) .compile() @@ -893,10 +858,10 @@ fn program_prepare_drop_is_invisible_and_commit_returns_the_new_projection() { .unwrap(); drop(prepared); - let unchanged = owner.project(&session).unwrap(); - assert_eq!(unchanged.evidence().kind(), StateKindV1::Ready); + let unchanged = session.evidence(); + assert_eq!(unchanged.kind(), StateKindV1::Ready); assert!(matches!( - unchanged.evidence().observation_head(), + unchanged.observation_head(), ObservationHeadV1::Observed { revision: 1, .. } )); @@ -910,17 +875,16 @@ fn program_prepare_drop_is_invisible_and_commit_returns_the_new_projection() { ) .unwrap() .commit(); - assert_eq!(committed.evidence().kind(), StateKindV1::Stale); + assert_eq!(committed.kind(), StateKindV1::Stale); assert!(matches!( - committed.evidence().observation_head(), + committed.observation_head(), ObservationHeadV1::Unknown { revision: 2, .. } )); } #[test] -fn unknown_context_removes_outputs_but_retains_previous_certificate_as_evidence() { +fn unknown_context_retains_previous_certificate_only_as_evidence() { let input = SurfaceInputPortIdV1::new(50); - let output = OutputSlotIdV1::new(12); let owner = fixed_nested_draft(0.5, SourceIdV1::new(1), input, input) .compile() .unwrap(); @@ -937,10 +901,7 @@ fn unknown_context_removes_outputs_but_retains_previous_certificate_as_evidence( }, ) .unwrap(); - assert!(matches!( - ready.operations().next(), - Some(OperationV1::Set(_)) - )); + assert_eq!(ready.kind(), StateKindV1::Ready); let stale = owner .commit( @@ -951,24 +912,17 @@ fn unknown_context_removes_outputs_but_retains_previous_certificate_as_evidence( }, ) .unwrap(); - assert_eq!(stale.evidence().kind(), StateKindV1::Stale); - let certificates = stale.evidence().certificates().collect::>(); + assert_eq!(stale.kind(), StateKindV1::Stale); + let certificates = stale.certificates().collect::>(); assert_eq!(certificates.len(), 1); let CertificateV1::Verified(previous) = certificates[0] else { panic!("the previous certificate must remain available as diagnostics"); }; assert_eq!(previous.observation().revision(), 1); assert!(matches!( - stale.evidence().observation_head(), + stale.observation_head(), ObservationHeadV1::Unknown { revision: 2, .. } )); - - let mut operations = stale.operations(); - let Some(OperationV1::Remove(remove)) = operations.next() else { - panic!("unknown current context cannot authorize the old output"); - }; - assert_eq!(remove.output_slot(), output); - assert!(operations.next().is_none()); } #[test] @@ -1052,7 +1006,7 @@ fn owner_and_update_errors_preserve_content_and_input_identity() { }, ) .unwrap(); - let certificate = projection.evidence().certificates().next().unwrap(); + let certificate = projection.certificates().next().unwrap(); assert_eq!(owner_identity, certificate.content_identity()); let error = match owner.commit( @@ -1150,7 +1104,7 @@ fn certificate_and_set_retain_the_same_nonunit_opacity() { }, ) .unwrap(); - let Some(CertificateV1::Verified(certificate)) = state.evidence().certificates().next() else { + let Some(CertificateV1::Verified(certificate)) = state.certificates().next() else { panic!("the exact emitted midpoint must be verified"); }; let AssessmentV1::ExactSrgb8(assessment) = certificate.cells().next().unwrap().assessment() @@ -1164,11 +1118,6 @@ fn certificate_and_set_retain_the_same_nonunit_opacity() { certificate.outputs().next().unwrap().opacity().to_bits(), physical.opacity().to_bits() ); - - let Some(OperationV1::Set(set)) = state.operations().next() else { - panic!("the verified output must emit one Set"); - }; - assert_eq!(set.opacity().to_bits(), physical.opacity().to_bits()); } #[test] diff --git a/crates/labcolors-core/src/program_clean_set_tests.rs b/crates/labcolors-core/src/program_clean_set_tests.rs index f5c23931..62294aba 100644 --- a/crates/labcolors-core/src/program_clean_set_tests.rs +++ b/crates/labcolors-core/src/program_clean_set_tests.rs @@ -77,9 +77,8 @@ fn report_only_dirty_terminal_is_retained_as_a_typed_rejected_violation() { ) .unwrap(); - assert_eq!(projection.evidence().kind(), program::StateKindV1::Ready); - let Some(program::CertificateV1::Verified(certificate)) = - projection.evidence().certificates().next() + assert_eq!(projection.kind(), program::StateKindV1::Ready); + let Some(program::CertificateV1::Verified(certificate)) = projection.certificates().next() else { panic!("report-only rejection must retain a Verified certificate"); }; @@ -122,9 +121,8 @@ fn hard_absent_final_owned_domain_is_a_violation_not_a_pass() { ) .unwrap(); - assert_eq!(projection.evidence().kind(), program::StateKindV1::Failed); - let Some(program::CertificateV1::Conflict(certificate)) = - projection.evidence().certificates().next() + assert_eq!(projection.kind(), program::StateKindV1::Failed); + let Some(program::CertificateV1::Conflict(certificate)) = projection.certificates().next() else { panic!("absent final-owned domain must reject a hard fixed candidate"); }; @@ -185,13 +183,14 @@ fn finite_search_skips_dirty_and_freshly_rechecks_the_first_clean_state() { ) .unwrap(); - let Some(program::OperationV1::Set(set)) = projection.operations().next() else { + let Some(program::CertificateV1::Verified(certificate)) = projection.certificates().next() + else { panic!("the first clean finite state must be selected"); }; - assert_eq!(set.source(), clean); - assert_eq!(set.certificate().selected_state_index(), Some(1)); + assert_eq!(certificate.outputs().next().unwrap().source(), clean); + assert_eq!(certificate.selected_state_index(), Some(1)); let program::AssessmentV1::DeclaredSrgb8CleanSet(evidence) = - set.certificate().cells().next().unwrap().assessment() + certificate.cells().next().unwrap().assessment() else { panic!("final recheck must retain clean-set evidence"); }; @@ -227,8 +226,7 @@ fn two_clean_constraints_and_causal_reporting_share_one_phase_materialization() ) .unwrap(); - let Some(program::CertificateV1::Verified(certificate)) = - projection.evidence().certificates().next() + let Some(program::CertificateV1::Verified(certificate)) = projection.certificates().next() else { panic!("report-only constraints must retain a Verified certificate"); }; @@ -306,8 +304,7 @@ fn downstream_occlusion_is_absent_even_when_the_inner_nominal_color_is_rejected( ) .unwrap(); - let Some(program::CertificateV1::Conflict(certificate)) = - projection.evidence().certificates().next() + let Some(program::CertificateV1::Conflict(certificate)) = projection.certificates().next() else { panic!("opaque downstream replacement must erase the inner final-owned domain"); }; @@ -557,7 +554,6 @@ fn rejected_clean_search_states_do_not_add_hot_path_allocations() { }, ) .unwrap() - .evidence() .kind() }); let (_, rejected_allocations) = crate::test_support::measured_allocations(|| { @@ -570,7 +566,6 @@ fn rejected_clean_search_states_do_not_add_hot_path_allocations() { }, ) .unwrap() - .evidence() .kind() }); diff --git a/crates/labcolors-core/src/program_mixed_evaluator_tests.rs b/crates/labcolors-core/src/program_mixed_evaluator_tests.rs index cb1c8bda..434dd881 100644 --- a/crates/labcolors-core/src/program_mixed_evaluator_tests.rs +++ b/crates/labcolors-core/src/program_mixed_evaluator_tests.rs @@ -19,12 +19,11 @@ use crate::observation::{ ObservedScenarioSetInput, Revision, ScenarioId, ScenarioInput, SurfaceInputBinding, }; use crate::program::{ - AccessErrorV1, AssessmentV1, CertificateV1, ConflictCellV1, ConstraintModeV1, - ConstraintSubjectV1, DeclaredSrgb8CleanSetViolationKindV1, EvidenceViewV1, - ExactSrgb8EvidenceV1, ObservationHeadV1, ObservationV1, OperationV1, OutputSlotIdV1, OwnerV1, - PhysicalPointV1, PreparedSessionTransitionV1, ProjectionV1, ScenarioV1, SessionV1, SignalV1, - StateKindV1, SurroundV1, UpdateErrorKindV1, UpdateErrorV1, UpdateV1, VerdictV1, VerifiedCellV1, - Wcag22Srgb8EvidenceV1, + AssessmentV1, CertificateV1, ConflictCellV1, ConstraintModeV1, ConstraintSubjectV1, + DeclaredSrgb8CleanSetViolationKindV1, EvidenceViewV1, ExactSrgb8EvidenceV1, ObservationHeadV1, + ObservationV1, OutputSlotIdV1, OwnerV1, PhysicalPointV1, PreparedSessionTransitionV1, + ScenarioV1, SessionV1, SignalV1, StateKindV1, SurroundV1, UpdateErrorKindV1, UpdateErrorV1, + UpdateV1, VerdictV1, VerifiedCellV1, Wcag22Srgb8EvidenceV1, }; use crate::program_boundary_tests::CommitProgramUpdateForTest as _; use crate::program_session::{ @@ -548,7 +547,7 @@ fn assert_conflict_cell_matches_core( } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -struct ProjectionProbe { +struct EvidenceProbe { iterators: usize, certificates: usize, cases: usize, @@ -556,14 +555,13 @@ struct ProjectionProbe { provenance: usize, cells: usize, outputs: usize, - operations: usize, exact_assessments: usize, wcag_assessments: usize, iterator_laws_hold: bool, checksum: u64, } -impl ProjectionProbe { +impl EvidenceProbe { const fn new() -> Self { Self { iterators: 0, @@ -573,7 +571,6 @@ impl ProjectionProbe { provenance: 0, cells: 0, outputs: 0, - operations: 0, exact_assessments: 0, wcag_assessments: 0, iterator_laws_hold: true, @@ -598,8 +595,8 @@ impl ProjectionProbe { fn consume_exact_fused( mut iterator: I, - probe: &mut ProjectionProbe, - mut consume: impl FnMut(I::Item, &mut ProjectionProbe), + probe: &mut EvidenceProbe, + mut consume: impl FnMut(I::Item, &mut EvidenceProbe), ) where I: ExactSizeIterator + FusedIterator, { @@ -623,7 +620,7 @@ fn consume_exact_fused( probe.iterator_laws_hold &= iterator.next().is_none(); } -fn consume_public_assessment(assessment: AssessmentV1<'_>, probe: &mut ProjectionProbe) { +fn consume_public_assessment(assessment: AssessmentV1<'_>, probe: &mut EvidenceProbe) { probe.mix(match assessment.verdict() { VerdictV1::Pass => 1, VerdictV1::Violation => 2, @@ -687,7 +684,7 @@ fn consume_public_assessment(assessment: AssessmentV1<'_>, probe: &mut Projectio }); } -fn clean_set_projection_probe(source: [u8; 3]) -> ProjectionProbe { +fn clean_set_projection_probe(source: [u8; 3]) -> EvidenceProbe { let owner = OwnerV1::from_compiled(fixed_clean_set_program(source)); let mut session = owner.instantiate(STREAM.value()).unwrap(); let backdrop = [Srgb8::new([0; 3])]; @@ -701,14 +698,13 @@ fn clean_set_projection_probe(source: [u8; 3]) -> ProjectionProbe { }, ) .unwrap(); - let Some(CertificateV1::Verified(certificate)) = projection.evidence().certificates().next() - else { + let Some(CertificateV1::Verified(certificate)) = projection.certificates().next() else { panic!("report-only clean-set outcome must retain a verified certificate"); }; assert_eq!(certificate.cells().len(), 1); let assessment = certificate.cells().next().unwrap().assessment(); - let mut probe = ProjectionProbe::new(); + let mut probe = EvidenceProbe::new(); consume_public_assessment(assessment, &mut probe); probe } @@ -725,7 +721,7 @@ fn clean_set_projection_probe_binds_pass_absence_rejection_and_interval() { ), ] { let actual = clean_set_projection_probe(source); - let mut expected = ProjectionProbe::new(); + let mut expected = EvidenceProbe::new(); for component in components { expected.mix(component); } @@ -733,9 +729,8 @@ fn clean_set_projection_probe_binds_pass_absence_rejection_and_interval() { } } -fn consume_public_projection(projection: ProjectionV1<'_, '_>) -> ProjectionProbe { - let view = projection.evidence(); - let mut probe = ProjectionProbe::new(); +fn consume_public_evidence(view: EvidenceViewV1<'_>) -> EvidenceProbe { + let mut probe = EvidenceProbe::new(); probe.mix(match view.kind() { StateKindV1::Waiting => 1, StateKindV1::Ready => 2, @@ -855,23 +850,6 @@ fn consume_public_projection(projection: ProjectionV1<'_, '_>) -> ProjectionProb } } }); - consume_exact_fused(projection.operations(), &mut probe, |operation, probe| { - probe.operations += 1; - match operation { - OperationV1::Set(set) => { - probe.mix(1); - probe.mix(u64::from(set.output_slot().value())); - probe.mix_srgb8(set.source()); - probe.mix(set.opacity().to_bits()); - probe.mix_bytes(set.certificate().content_identity().as_bytes()); - probe.mix(set.certificate().observation().revision()); - } - OperationV1::Remove(remove) => { - probe.mix(2); - probe.mix(u64::from(remove.output_slot().value())); - } - } - }); std::hint::black_box(probe) } @@ -1001,8 +979,7 @@ fn fixed_public_certificate_retains_none_selection_and_nonunit_output_opacity() }, ) .unwrap(); - let Some(CertificateV1::Verified(certificate)) = projection.evidence().certificates().next() - else { + let Some(CertificateV1::Verified(certificate)) = projection.certificates().next() else { panic!("the exact translucent midpoint must be verified"); }; assert_eq!(certificate.selected_state_index(), None); @@ -1017,10 +994,6 @@ fn fixed_public_certificate_retains_none_selection_and_nonunit_output_opacity() certificate.outputs().next().unwrap().opacity().to_bits(), physical.opacity().to_bits() ); - let Some(OperationV1::Set(set)) = projection.operations().next() else { - panic!("Verified must emit one Set"); - }; - assert_eq!(set.opacity().to_bits(), physical.opacity().to_bits()); } #[test] @@ -1145,7 +1118,7 @@ fn public_projection_preserves_every_exposed_ready_and_conflict_field_against_co }, ) .unwrap(); - let mut public_certificates = public_ready.evidence().certificates(); + let mut public_certificates = public_ready.certificates(); assert_eq!(public_certificates.len(), 1); let Some(CertificateV1::Verified(public_verified)) = public_certificates.next() else { panic!("Ready must retain exactly one Verified certificate"); @@ -1192,30 +1165,6 @@ fn public_projection_preserves_every_exposed_ready_and_conflict_field_against_co assert!(public_outputs.next().is_none()); assert!(public_outputs.next().is_none()); assert!(core_outputs.next().is_none()); - let mut ready_operations = public_ready.operations(); - assert_eq!(ready_operations.len(), core_verified.outputs().len()); - for core_output in core_verified.outputs() { - let Some(OperationV1::Set(set)) = ready_operations.next() else { - panic!("every certified output must become exactly one Set"); - }; - assert_eq!(set.output_slot().value(), core_output.output().value()); - assert_eq!(set.source(), core_output.paint().source()); - assert_eq!( - set.opacity().to_bits(), - core_output.paint().opacity().value().to_bits() - ); - assert_eq!( - set.certificate().content_identity(), - public_verified.content_identity() - ); - assert_eq!( - set.certificate().observation().revision(), - public_verified.observation().revision() - ); - } - assert!(ready_operations.next().is_none()); - assert!(ready_operations.next().is_none()); - let conflict_core_owner = finite_program([[0; 3], [0xFF; 3]]); let conflict_identity = conflict_core_owner.content_identity(); let conflict_public_owner = OwnerV1::from_compiled(finite_program([[0; 3], [0xFF; 3]])); @@ -1246,7 +1195,7 @@ fn public_projection_preserves_every_exposed_ready_and_conflict_field_against_co }, ) .unwrap(); - let mut public_certificates = public_failed.evidence().certificates(); + let mut public_certificates = public_failed.certificates(); assert_eq!(public_certificates.len(), 1); let Some(CertificateV1::Conflict(public_conflict)) = public_certificates.next() else { panic!("Failed without previous state must retain one Conflict certificate"); @@ -1283,18 +1232,10 @@ fn public_projection_preserves_every_exposed_ready_and_conflict_field_against_co assert!(public_cells.next().is_none()); assert!(public_cells.next().is_none()); assert!(core_cells.next().is_none()); - let mut failed_operations = public_failed.operations(); - assert_eq!(failed_operations.len(), 1); - assert!(matches!( - failed_operations.next(), - Some(OperationV1::Remove(_)) - )); - assert!(failed_operations.next().is_none()); - assert!(failed_operations.next().is_none()); } #[test] -fn committed_projection_is_zero_alloc_and_repeats_no_composite_transform_or_evaluator_dispatch() { +fn committed_evidence_is_zero_alloc_and_repeats_no_composite_transform_or_evaluator_dispatch() { crate::composition::reset_source_over_evaluation_count(); MODELED_TRISTIMULUS_DERIVATION_CALLS.with(|calls| calls.set(0)); CORE_PROGRAM_ASSESSMENT_CALLS.with(|calls| calls.set(0)); @@ -1328,7 +1269,7 @@ fn committed_projection_is_zero_alloc_and_repeats_no_composite_transform_or_eval ); assert!(ready_assessments > 0); let (ready_probe, ready_allocations) = crate::test_support::measured_allocations(|| { - consume_public_projection(std::hint::black_box(owner.project(&session).unwrap())) + consume_public_evidence(std::hint::black_box(session.evidence())) }); assert_eq!(ready_allocations, 0); assert!(ready_probe.iterator_laws_hold); @@ -1338,7 +1279,6 @@ fn committed_projection_is_zero_alloc_and_repeats_no_composite_transform_or_eval assert_eq!(ready_probe.provenance, 1); assert_eq!(ready_probe.cells, 2); assert_eq!(ready_probe.outputs, 1); - assert_eq!(ready_probe.operations, 1); assert_eq!(ready_probe.exact_assessments, 1); assert_eq!(ready_probe.wcag_assessments, 1); assert_ne!(ready_probe.checksum, 0); @@ -1368,14 +1308,13 @@ fn committed_projection_is_zero_alloc_and_repeats_no_composite_transform_or_eval let stale_derivations = MODELED_TRISTIMULUS_DERIVATION_CALLS.with(core::cell::Cell::get); let stale_assessments = CORE_PROGRAM_ASSESSMENT_CALLS.with(core::cell::Cell::get); let (stale_probe, stale_allocations) = crate::test_support::measured_allocations(|| { - consume_public_projection(std::hint::black_box(owner.project(&session).unwrap())) + consume_public_evidence(std::hint::black_box(session.evidence())) }); assert_eq!(stale_allocations, 0); assert!(stale_probe.iterator_laws_hold); assert_eq!(stale_probe.certificates, 1); assert_eq!(stale_probe.cells, 2); assert_eq!(stale_probe.outputs, 1); - assert_eq!(stale_probe.operations, 1); assert_ne!(stale_probe.checksum, ready_probe.checksum); assert_eq!( crate::composition::source_over_evaluation_count(), @@ -1405,7 +1344,7 @@ fn committed_projection_is_zero_alloc_and_repeats_no_composite_transform_or_eval let failed_derivations = MODELED_TRISTIMULUS_DERIVATION_CALLS.with(core::cell::Cell::get); let failed_assessments = CORE_PROGRAM_ASSESSMENT_CALLS.with(core::cell::Cell::get); let (failed_probe, failed_allocations) = crate::test_support::measured_allocations(|| { - consume_public_projection(std::hint::black_box(owner.project(&session).unwrap())) + consume_public_evidence(std::hint::black_box(session.evidence())) }); assert_eq!(failed_allocations, 0); assert!(failed_probe.iterator_laws_hold); @@ -1415,7 +1354,6 @@ fn committed_projection_is_zero_alloc_and_repeats_no_composite_transform_or_eval assert_eq!(failed_probe.provenance, 3); assert_eq!(failed_probe.cells, 10); assert_eq!(failed_probe.outputs, 1); - assert_eq!(failed_probe.operations, 1); assert_eq!(failed_probe.exact_assessments, 5); assert_eq!(failed_probe.wcag_assessments, 5); assert_ne!(failed_probe.checksum, stale_probe.checksum); @@ -1493,8 +1431,7 @@ fn observation_projection_is_invariant_under_every_scenario_permutation_and_keep }, ) .unwrap(); - let Some(CertificateV1::Verified(public_verified)) = - public_state.evidence().certificates().next() + let Some(CertificateV1::Verified(public_verified)) = public_state.certificates().next() else { panic!("the canonical observation must keep one Verified certificate"); }; @@ -1558,7 +1495,7 @@ fn observation_projection_is_invariant_under_every_scenario_permutation_and_keep } #[test] -fn concrete_program_projects_ready_and_fail_closed_stale_operations() { +fn concrete_program_retains_ready_and_fail_closed_stale_evidence() { let owner = OwnerV1::from_compiled(finite_program([[0x80; 3], [0; 3]])); assert_eq!(owner.surface_input_port_count(), 1); assert_eq!( @@ -1571,7 +1508,6 @@ fn concrete_program_projects_ready_and_fail_closed_stale_operations() { assert_eq!(initial.kind(), StateKindV1::Waiting); assert_eq!(initial.observation_head(), ObservationHeadV1::Empty); assert_eq!(initial.certificates().len(), 0); - assert_eq!(owner.project(&session).unwrap().operations().len(), 0); let white = [Srgb8::new([0xFF; 3])]; let gray = [Srgb8::new([0x80; 3])]; @@ -1585,36 +1521,14 @@ fn concrete_program_projects_ready_and_fail_closed_stale_operations() { }, ) .unwrap(); - let ready_evidence = ready.evidence(); - assert_eq!(ready_evidence.kind(), StateKindV1::Ready); - assert_observed_head(ready_evidence.observation_head(), STREAM.value(), 1); - assert_eq!(ready_evidence.cause_certificate_index(), None); - let certificates = ready_evidence.certificates().collect::>(); + assert_eq!(ready.kind(), StateKindV1::Ready); + assert_observed_head(ready.observation_head(), STREAM.value(), 1); + assert_eq!(ready.cause_certificate_index(), None); + let certificates = ready.certificates().collect::>(); assert_eq!(certificates.len(), 1); assert!(matches!(certificates[0], CertificateV1::Verified(_))); assert_eq!(certificates[0].observation().revision(), 1); let ready_backing = certificates[0].observation_backing_ptr_for_test(); - let mut operations = ready.operations(); - let Some(OperationV1::Set(set)) = operations.next() else { - panic!("Ready must emit one Set operation"); - }; - assert_eq!(set.output_slot(), OutputSlotIdV1::new(OUTPUT.value())); - assert_eq!(set.source(), Srgb8::new([0; 3])); - assert_eq!(set.opacity(), 1.0); - assert_eq!( - set.certificate().observation().revision(), - certificates[0].observation().revision() - ); - assert_eq!( - set.certificate().content_identity(), - certificates[0].content_identity() - ); - assert_eq!( - CertificateV1::Verified(set.certificate()).observation_backing_ptr_for_test(), - ready_backing - ); - assert!(operations.next().is_none()); - drop(operations); drop(certificates); let reordered = [ScenarioV1::new(1, &white), ScenarioV1::new(2, &gray)]; @@ -1627,7 +1541,7 @@ fn concrete_program_projects_ready_and_fail_closed_stale_operations() { }, ) .unwrap(); - let replay_certificate = replay.evidence().certificates().next().unwrap(); + let replay_certificate = replay.certificates().next().unwrap(); assert_eq!( replay_certificate.observation_backing_ptr_for_test(), ready_backing, @@ -1658,10 +1572,9 @@ fn concrete_program_projects_ready_and_fail_closed_stale_operations() { }, ) .unwrap(); - let stale_evidence = stale.evidence(); - assert_eq!(stale_evidence.kind(), StateKindV1::Stale); - assert_unknown_head(stale_evidence.observation_head(), STREAM.value(), 2, 7); - let certificates = stale_evidence.certificates().collect::>(); + assert_eq!(stale.kind(), StateKindV1::Stale); + assert_unknown_head(stale.observation_head(), STREAM.value(), 2, 7); + let certificates = stale.certificates().collect::>(); assert_eq!(certificates.len(), 1); assert!(matches!(certificates[0], CertificateV1::Verified(_))); assert_eq!(certificates[0].observation().revision(), 1); @@ -1669,72 +1582,10 @@ fn concrete_program_projects_ready_and_fail_closed_stale_operations() { certificates[0].observation_backing_ptr_for_test(), ready_backing ); - let mut operations = stale.operations(); - let Some(OperationV1::Remove(remove)) = operations.next() else { - panic!("Stale must remove an output that lacks current evidence"); - }; - assert_eq!(remove.output_slot(), OutputSlotIdV1::new(OUTPUT.value())); - assert!(operations.next().is_none()); } #[test] -fn unknown_replacement_session_revokes_an_existing_owner_output() { - let owner = OwnerV1::from_compiled(finite_program([[0x80; 3], [0; 3]])); - let white = [Srgb8::new([0xFF; 3])]; - let scenarios = [ScenarioV1::new(1, &white)]; - - let mut first_session = owner.instantiate(11).unwrap(); - let ready = owner - .commit( - &mut first_session, - UpdateV1::Observed { - revision: 1, - scenarios: &scenarios, - }, - ) - .unwrap(); - let mut sink = None; - for operation in ready.operations() { - match operation { - OperationV1::Set(set) => sink = Some((set.source(), set.opacity())), - OperationV1::Remove(_) => sink = None, - } - } - assert!( - sink.is_some(), - "the first Session must populate the shared sink" - ); - drop(first_session); - - let mut replacement_session = owner.instantiate(12).unwrap(); - let unknown = owner - .commit( - &mut replacement_session, - UpdateV1::Unknown { - revision: 1, - reason_id: 7, - }, - ) - .unwrap(); - assert_eq!(unknown.evidence().kind(), StateKindV1::Waiting); - assert_unknown_head(unknown.evidence().observation_head(), 12, 1, 7); - assert_eq!(unknown.evidence().certificates().len(), 0); - - let mut operations = unknown.operations(); - let Some(OperationV1::Remove(remove)) = operations.next() else { - panic!("an explicit Unknown must revoke an existing owner output during handoff"); - }; - assert_eq!(remove.output_slot(), OutputSlotIdV1::new(OUTPUT.value())); - sink = None; - assert!(operations.next().is_none()); - assert!( - sink.is_none(), - "the replacement Session must not leave stale paint" - ); -} - -#[test] -fn concrete_program_failed_always_removes_but_retains_previous_evidence() { +fn concrete_program_failed_retains_only_current_conflict_and_previous_evidence() { let white = [Srgb8::new([0xFF; 3])]; let black = [Srgb8::new([0; 3])]; let white_only = [ScenarioV1::new(1, &white)]; @@ -1750,19 +1601,12 @@ fn concrete_program_failed_always_removes_but_retains_previous_evidence() { }, ) .unwrap(); - let failed_evidence = failed.evidence(); - assert_eq!(failed_evidence.kind(), StateKindV1::Failed); - assert_eq!(failed_evidence.cause_certificate_index(), Some(0)); - let certificates = failed_evidence.certificates().collect::>(); + assert_eq!(failed.kind(), StateKindV1::Failed); + assert_eq!(failed.cause_certificate_index(), Some(0)); + let certificates = failed.certificates().collect::>(); assert_eq!(certificates.len(), 1); assert!(matches!(certificates[0], CertificateV1::Conflict(_))); assert_eq!(certificates[0].observation().revision(), 1); - let mut operations = failed.operations(); - let Some(OperationV1::Remove(remove)) = operations.next() else { - panic!("Failed without previous evidence must emit one Remove operation"); - }; - assert_eq!(remove.output_slot(), OutputSlotIdV1::new(OUTPUT.value())); - assert!(operations.next().is_none()); let owner = OwnerV1::from_compiled(finite_program([[0; 3], [0xFF; 3]])); let mut session = owner.instantiate(12).unwrap(); @@ -1776,7 +1620,6 @@ fn concrete_program_failed_always_removes_but_retains_previous_evidence() { ) .unwrap(); let previous_backing = previous - .evidence() .certificates() .next() .unwrap() @@ -1791,10 +1634,9 @@ fn concrete_program_failed_always_removes_but_retains_previous_evidence() { }, ) .unwrap(); - let failed_evidence = failed.evidence(); - assert_eq!(failed_evidence.kind(), StateKindV1::Failed); - assert_eq!(failed_evidence.cause_certificate_index(), Some(0)); - let certificates = failed_evidence.certificates().collect::>(); + assert_eq!(failed.kind(), StateKindV1::Failed); + assert_eq!(failed.cause_certificate_index(), Some(0)); + let certificates = failed.certificates().collect::>(); assert_eq!( certificates .iter() @@ -1813,12 +1655,6 @@ fn concrete_program_failed_always_removes_but_retains_previous_evidence() { certificates[1].observation_backing_ptr_for_test(), previous_backing ); - let mut operations = failed.operations(); - let Some(OperationV1::Remove(remove)) = operations.next() else { - panic!("Failed must remove an output that violates the current context"); - }; - assert_eq!(remove.output_slot(), OutputSlotIdV1::new(OUTPUT.value())); - assert!(operations.next().is_none()); } #[test] @@ -1908,15 +1744,6 @@ fn same_content_foreign_owner_is_rejected_before_admission_without_work() { CORE_PROGRAM_ASSESSMENT_CALLS.with(core::cell::Cell::get), ); - let (project_mismatch, project_allocations) = crate::test_support::measured_allocations(|| { - matches!( - owner_b.project(std::hint::black_box(&session)), - Err(AccessErrorV1::OwnerMismatch) - ) - }); - assert!(project_mismatch); - assert_eq!(project_allocations, 0); - let empty = []; let malformed = [ScenarioV1::new(2, &empty)]; let (update_mismatch, update_allocations) = crate::test_support::measured_allocations(|| { @@ -1953,7 +1780,7 @@ fn same_content_foreign_owner_is_rejected_before_admission_without_work() { .observation_backing_ptr_for_test(), before_backing ); - assert!(owner_a.project(&session).is_ok()); + assert_eq!(session.evidence().kind(), StateKindV1::Ready); } #[test] @@ -2088,10 +1915,6 @@ fn expired_owner_preserves_historical_evidence_but_equivalent_owner_has_no_autho "historical evidence is Session-owned rather than owner-authorized" ); - assert!(matches!( - owner_b.project(&session), - Err(AccessErrorV1::OwnerMismatch) - )); let mismatch = match owner_b.commit( &mut session, UpdateV1::Unknown { @@ -2212,108 +2035,6 @@ fn raw_head_and_evaluator_lifecycle_form_the_exact_reachable_product() { assert_eq!(waiting.certificates().len(), 0); } -#[test] -fn failed_without_previous_removes_every_output_in_canonical_exact_order() { - let owner = OwnerV1::from_compiled(finite_program_with_outputs( - [[0x80; 3], [0xFF; 3]], - vec![ - OutputBinding::new(SECOND_OUTPUT, PAINT), - OutputBinding::new(OUTPUT, PAINT), - ], - )); - let mut session = owner.instantiate(STREAM.value()).unwrap(); - let white = [Srgb8::new([0xFF; 3])]; - let scenarios = [ScenarioV1::new(1, &white)]; - let failed = owner - .commit( - &mut session, - UpdateV1::Observed { - revision: 1, - scenarios: &scenarios, - }, - ) - .unwrap(); - assert_eq!(failed.evidence().kind(), StateKindV1::Failed); - assert_eq!(failed.evidence().certificates().len(), 1); - - let mut operations = failed.operations(); - assert_eq!(operations.len(), 2); - assert_eq!(operations.size_hint(), (2, Some(2))); - let Some(OperationV1::Remove(first)) = operations.next() else { - panic!("Failed without previous evidence must remove the first output"); - }; - assert_eq!(first.output_slot(), OutputSlotIdV1::new(OUTPUT.value())); - assert_eq!(operations.len(), 1); - assert_eq!(operations.size_hint(), (1, Some(1))); - - let Some(OperationV1::Remove(second)) = operations.next() else { - panic!("Failed without previous evidence must remove the second output"); - }; - assert_eq!( - second.output_slot(), - OutputSlotIdV1::new(SECOND_OUTPUT.value()) - ); - assert_eq!(operations.len(), 0); - assert_eq!(operations.size_hint(), (0, Some(0))); - assert!(operations.next().is_none()); - assert!(operations.next().is_none()); -} - -#[test] -fn ready_sets_and_stale_removes_every_output_in_the_same_canonical_order() { - let owner = OwnerV1::from_compiled(finite_program_with_outputs( - [[0x80; 3], [0; 3]], - vec![ - OutputBinding::new(SECOND_OUTPUT, PAINT), - OutputBinding::new(OUTPUT, PAINT), - ], - )); - let mut session = owner.instantiate(STREAM.value()).unwrap(); - let white = [Srgb8::new([0xFF; 3])]; - let scenarios = [ScenarioV1::new(1, &white)]; - - { - let ready = owner - .commit( - &mut session, - UpdateV1::Observed { - revision: 1, - scenarios: &scenarios, - }, - ) - .unwrap(); - let mut operations = ready.operations(); - assert_eq!(operations.len(), 2); - for expected in [OUTPUT, SECOND_OUTPUT] { - let Some(OperationV1::Set(set)) = operations.next() else { - panic!("Ready must set every compiled output"); - }; - assert_eq!(set.output_slot().value(), expected.value()); - assert_eq!(set.certificate().observation().revision(), 1); - } - assert!(operations.next().is_none()); - } - - let stale = owner - .commit( - &mut session, - UpdateV1::Unknown { - revision: 2, - reason_id: 7, - }, - ) - .unwrap(); - let mut operations = stale.operations(); - assert_eq!(operations.len(), 2); - for expected in [OUTPUT, SECOND_OUTPUT] { - let Some(OperationV1::Remove(remove)) = operations.next() else { - panic!("Stale must remove every output that lacks current evidence"); - }; - assert_eq!(remove.output_slot().value(), expected.value()); - } - assert!(operations.next().is_none()); -} - #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum ModeledPayload { ReadyOnWhite, @@ -2432,21 +2153,21 @@ impl ModeledSession { } } -fn apply_modeled_payload<'owner, 'session>( - owner: &'owner OwnerV1, +fn apply_modeled_payload<'session>( + owner: &OwnerV1, session: &'session mut SessionV1, revision: u64, payload: ModeledPayload, -) -> Result, UpdateErrorV1> { +) -> Result, UpdateErrorV1> { prepare_modeled_payload(owner, session, revision, payload).map(|prepared| prepared.commit()) } -fn prepare_modeled_payload<'owner, 'session>( - owner: &'owner OwnerV1, +fn prepare_modeled_payload<'session>( + owner: &OwnerV1, session: &'session mut SessionV1, revision: u64, payload: ModeledPayload, -) -> Result, UpdateErrorV1> { +) -> Result, UpdateErrorV1> { let white = [Srgb8::new([0xFF; 3])]; let black = [Srgb8::new([0; 3])]; match payload { @@ -2506,10 +2227,9 @@ fn assert_verified_matches_model( } fn assert_projection_matches_model( - projection: ProjectionV1<'_, '_>, + evidence: EvidenceViewV1<'_>, model: ModeledSession, ) -> Result<(), TestCaseError> { - let evidence = projection.evidence(); let expected_kind = match model.lifecycle { ModeledLifecycle::Waiting => StateKindV1::Waiting, ModeledLifecycle::Ready(_) => StateKindV1::Ready, @@ -2594,37 +2314,6 @@ fn assert_projection_matches_model( } } - let operations = projection.operations().collect::>(); - match (model.head, model.lifecycle) { - (None, ModeledLifecycle::Waiting) => prop_assert_eq!(operations.len(), 0), - (_, ModeledLifecycle::Ready(expected)) => { - prop_assert_eq!(operations.len(), 2); - for (operation, expected_slot) in operations.into_iter().zip([OUTPUT, SECOND_OUTPUT]) { - let OperationV1::Set(set) = operation else { - return Err(TestCaseError::fail("Ready must emit Set for every output")); - }; - prop_assert_eq!(set.output_slot().value(), expected_slot.value()); - prop_assert_eq!(set.source(), Srgb8::new(expected.source)); - prop_assert_eq!(set.opacity(), 1.0); - prop_assert_eq!( - set.certificate().observation().revision(), - expected.revision - ); - } - } - (Some(_), _) => { - prop_assert_eq!(operations.len(), 2); - for (operation, expected_slot) in operations.into_iter().zip([OUTPUT, SECOND_OUTPUT]) { - let OperationV1::Remove(remove) = operation else { - return Err(TestCaseError::fail( - "a non-Ready admitted head must not authorize Set", - )); - }; - prop_assert_eq!(remove.output_slot().value(), expected_slot.value()); - } - } - (None, _) => prop_assert!(false, "only Waiting may have an empty raw head"), - } Ok(()) } @@ -2674,8 +2363,8 @@ fn historical_evidence_identity_probe_rejects_recreated_equal_certificates() { ) .unwrap(); assert_ne!( - historical_evidence_identities(first_projection.evidence()), - historical_evidence_identities(recreated_projection.evidence()), + historical_evidence_identities(first_projection), + historical_evidence_identities(recreated_projection), "the probe must reject value-equivalent evidence recreated in another Session", ); } @@ -2850,12 +2539,7 @@ fn exercise_modeled_action( "replay, Unknown and rejected inputs must not dispatch evaluators", ); } - assert_projection_matches_model( - owner - .project(session) - .map_err(|error| TestCaseError::fail(format!("matching owner rejected: {error:?}")))?, - *model, - ) + assert_projection_matches_model(session.evidence(), *model) } fn exercise_modeled_sequence( @@ -2873,9 +2557,7 @@ fn exercise_modeled_sequence( .instantiate(STREAM.value()) .map_err(|error| TestCaseError::fail(format!("fixture stream was rejected: {error:?}")))?; let mut model = ModeledSession::new(); - let projection = owner - .project(&session) - .map_err(|error| TestCaseError::fail(format!("fixture epoch mismatch: {error:?}")))?; + let projection = session.evidence(); assert_projection_matches_model(projection, model)?; for action in actions { exercise_modeled_action(&owner, &mut session, &mut model, action)?; diff --git a/crates/labcolors-core/src/program_session.rs b/crates/labcolors-core/src/program_session.rs index aaaeb18c..5fce49a3 100644 --- a/crates/labcolors-core/src/program_session.rs +++ b/crates/labcolors-core/src/program_session.rs @@ -1352,6 +1352,81 @@ struct CompiledOutputBinding { paint: CompiledPaintSlotV1, } +/// Сминченная компилятором точная корреляция одного выхода Program и одной +/// моделируемой point-presentation цели. +/// +/// Закрытые поля не дают подделать ordinal. Номинальные ID сохранены рядом, +/// чтобы hot path повторно проверял их без поиска. Само значение не доказывает +/// owner generation: enclosing owner обязан атомарно сминтить его и удержать +/// [`ProgramOwnerLeaseV1`] из той же [`CompiledProgram`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct CompiledPointOutputPresentationV1 { + output_ordinal: usize, + output: OutputSlotId, + paint: PaintId, + presentation_ordinal: usize, + root: PresentationRootId, + occurrence: OccurrenceId, +} + +impl CompiledPointOutputPresentationV1 { + pub(crate) const fn output_ordinal(self) -> usize { + self.output_ordinal + } + + pub(crate) const fn output(self) -> OutputSlotId { + self.output + } + + pub(crate) const fn paint(self) -> PaintId { + self.paint + } + + pub(crate) const fn presentation_ordinal(self) -> usize { + self.presentation_ordinal + } + + pub(crate) const fn root(self) -> PresentationRootId { + self.root + } + + pub(crate) const fn occurrence(self) -> OccurrenceId { + self.occurrence + } +} + +/// Cold-ошибка binding до допуска point output в hot path. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum PointOutputPresentationBindErrorV1 { + /// Авторский output отсутствует в compiled output table. + MissingOutput { + /// Неизвестный output ID. + output: OutputSlotId, + }, + /// Авторская point-presentation цель отсутствует в compiled graph. + MissingPresentationTarget { + /// Root, в котором ожидалась цель. + root: PresentationRootId, + /// Occurrence, который должен быть доступен из root. + occurrence: OccurrenceId, + }, + /// Output и point-presentation цель ссылаются на разные Paint. + SubjectPaintMismatch { + /// Авторский output ID. + output: OutputSlotId, + /// Paint, связанный с output. + output_paint: PaintId, + /// Авторский presentation root. + root: PresentationRootId, + /// Авторский presentation occurrence. + occurrence: OccurrenceId, + /// Paint, физически представленный occurrence. + subject_paint: PaintId, + }, + /// Нарушен закрытый compiled-инвариант после успешной валидации Draft. + InternalInvariant, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] struct CompiledOccurrenceContextV1 { occurrence: OccurrenceId, @@ -1419,9 +1494,10 @@ where joint_selection: Option, } -/// Transaction-local strong pin for one exact compiled Program generation. -/// Construction is possible only by upgrading a Session plan's weak binding; -/// the contained epoch never becomes an independently shareable API. +/// Strong pin одной точной compiled generation Program. Транзакция получает +/// его через upgrade слабой связи Session plan; enclosing cold owner также +/// может клонировать его прямо из [`CompiledProgram`]. Вложенная эпоха никогда +/// не становится независимо распространяемым API. pub(crate) struct ProgramOwnerLeaseV1(Rc>) where Evaluation: ProgramConstraintEvaluatorSetV1, @@ -1474,6 +1550,63 @@ where .map(|output| (output.output, output.paint_id)) } + /// Минтит закрытую ordinal-backed корреляцию, только если Paint выхода + /// точно совпадает с объявленным subject выбранной presentation target. + pub(crate) fn bind_point_output_presentation( + &self, + output: OutputSlotId, + root: PresentationRootId, + occurrence: OccurrenceId, + ) -> Result { + let output_ordinal = self + .owner_generation + .outputs + .binary_search_by_key(&output, |candidate| candidate.output) + .map_err(|_| PointOutputPresentationBindErrorV1::MissingOutput { output })?; + let compiled_output = &self.owner_generation.outputs[output_ordinal]; + + let target = (root, occurrence); + let presentation_ordinal = self + .owner_generation + .point_presentations + .entries + .binary_search_by_key(&target, |candidate| (candidate.root, candidate.target)) + .map_err( + |_| PointOutputPresentationBindErrorV1::MissingPresentationTarget { + root, + occurrence, + }, + )?; + let subject_paint = self + .owner_generation + .graph + .occurrence_subject(occurrence) + .ok_or(PointOutputPresentationBindErrorV1::InternalInvariant)?; + if compiled_output.paint_id != subject_paint { + return Err(PointOutputPresentationBindErrorV1::SubjectPaintMismatch { + output, + output_paint: compiled_output.paint_id, + root, + occurrence, + subject_paint, + }); + } + + Ok(CompiledPointOutputPresentationV1 { + output_ordinal, + output, + paint: compiled_output.paint_id, + presentation_ordinal, + root, + occurrence, + }) + } + + /// Удерживает точную owner generation независимо от `CompiledProgram`. + pub(crate) fn pin_owner(&self) -> ProgramOwnerLeaseV1 { + ProgramOwnerLeaseV1(Rc::clone(&self.owner_generation)) + } + pub(crate) fn point_presentation_count(&self) -> usize { debug_assert!( self.owner_generation @@ -1510,13 +1643,6 @@ where .map(|counts| (counts.selected, counts.exhaustive_conflict)) } - pub(crate) fn output_slot_at(&self, index: usize) -> Option { - self.owner_generation - .outputs - .get(index) - .map(|output| output.output) - } - #[cfg(test)] pub(crate) fn observation_schema_strong_count_for_test(&self) -> usize { self.owner_generation diff --git a/crates/labcolors-core/src/program_session_tests.rs b/crates/labcolors-core/src/program_session_tests.rs index 75f16681..d0fc1497 100644 --- a/crates/labcolors-core/src/program_session_tests.rs +++ b/crates/labcolors-core/src/program_session_tests.rs @@ -12,9 +12,11 @@ use crate::observation::{ }; use crate::program_session::{ CompositionProfile, ConstraintId, ConstraintInvocation, ConstraintSet, ObservationGroup, - Occurrence, OpacityInput, OutputBinding, OutputSlotId, Paint, Program, ProgramCompileError, - ProgramConstraintBodyV1, ProgramConstraintSubjectV1, Source, SourceId, Surface, Target, - TargetId, canonical_surface_input_port_sequence_matches, check_render_node_count, + Occurrence, OpacityInput, OutputBinding, OutputSlotId, Paint, + PointOutputPresentationBindErrorV1, PointPresentationRootV1, PointPresentationTargetV1, + PresentationRootId, Program, ProgramCompileError, ProgramConstraintBodyV1, + ProgramConstraintSubjectV1, Source, SourceId, Surface, Target, TargetId, + canonical_surface_input_port_sequence_matches, check_render_node_count, }; use crate::session::{SessionPlanV1, SessionState, SessionUpdateError}; use crate::session_tests::CommitSessionUpdateForTest as _; @@ -29,7 +31,10 @@ const BACKDROP: SurfaceId = SurfaceId::new(20); const VISIBLE_SURFACE: SurfaceId = SurfaceId::new(21); const OCCURRENCE: OccurrenceId = OccurrenceId::new(30); const OUTPUT: OutputSlotId = OutputSlotId::new(40); +const EARLIER_OUTPUT: OutputSlotId = OutputSlotId::new(39); const REQUIRED: ConstraintId = ConstraintId::new(50); +const PRESENTATION_ROOT: PresentationRootId = PresentationRootId::new(91); +const EARLIER_PRESENTATION_ROOT: PresentationRootId = PresentationRootId::new(90); const GROUP: ObservationGroupId = ObservationGroupId::new(60); const STREAM_A: ObservationStreamId = ObservationStreamId::new(70); const STREAM_B: ObservationStreamId = ObservationStreamId::new(71); @@ -164,6 +169,37 @@ fn exact_compiled( .unwrap() } +fn exact_compiled_with_point_presentations( + outputs: Vec, +) -> crate::program_session::CompiledProgram { + base_program( + 0.5, + BACKDROP, + ConstraintSet::new( + vec![ConstraintInvocation::hard( + REQUIRED, + OCCURRENCE, + Srgb8::new([0x80; 3]), + )], + vec![], + ), + outputs, + ExactSrgb8IdentityV1, + ) + .with_point_presentations( + vec![ + PointPresentationRootV1::new(PRESENTATION_ROOT, OCCURRENCE), + PointPresentationRootV1::new(EARLIER_PRESENTATION_ROOT, OCCURRENCE), + ], + vec![ + PointPresentationTargetV1::new(PRESENTATION_ROOT, OCCURRENCE), + PointPresentationTargetV1::new(EARLIER_PRESENTATION_ROOT, OCCURRENCE), + ], + ) + .compile() + .unwrap() +} + #[test] fn authored_modes_are_marker_typed_and_values_preserve_exact_ids() { let hard = ConstraintInvocation::hard(REQUIRED, OCCURRENCE, Srgb8::new([0x80; 3])); @@ -504,6 +540,201 @@ fn compile_canonicalizes_constraints_and_outputs_independent_of_mode_lists() { ); } +#[test] +fn point_output_presentation_binding_retains_exact_ids_and_canonical_ordinals() { + let compiled = exact_compiled_with_point_presentations(vec![ + OutputBinding::new(OUTPUT, TRANSLUCENT), + OutputBinding::new(EARLIER_OUTPUT, TRANSLUCENT), + ]); + + let binding = compiled + .bind_point_output_presentation(OUTPUT, PRESENTATION_ROOT, OCCURRENCE) + .unwrap(); + + assert_eq!(binding.output(), OUTPUT); + assert_eq!(binding.paint(), TRANSLUCENT); + assert_eq!(binding.root(), PRESENTATION_ROOT); + assert_eq!(binding.occurrence(), OCCURRENCE); + // Canonical ordinals следуют возрастающим numeric IDs: оба EARLIER_ ID + // меньше выбранных OUTPUT/PRESENTATION_ROOT и потому занимают ordinal 0. + assert_eq!(binding.output_ordinal(), 1); + assert_eq!(binding.presentation_ordinal(), 1); +} + +#[test] +fn point_output_presentation_uses_the_selected_target_subject_not_the_terminal_subject() { + let lower_source = SourceId::new(101); + let terminal_source = SourceId::new(102); + let lower_target = TargetId::new(103); + let terminal_target = TargetId::new(104); + let lower_paint = PaintId::new(105); + let terminal_paint = PaintId::new(106); + let root_surface = SurfaceId::new(107); + let derived_surface = SurfaceId::new(108); + let selected_occurrence = OccurrenceId::new(109); + let terminal_occurrence = OccurrenceId::new(110); + let selected_output = OutputSlotId::new(111); + let presentation_root = PresentationRootId::new(112); + + let compiled = Program::new( + vec![ + Source::new( + lower_source, + ColorSignal::from_srgb8(Srgb8::new([10, 20, 30])), + ), + Source::new( + terminal_source, + ColorSignal::from_srgb8(Srgb8::new([40, 50, 60])), + ), + ], + vec![ + Target::fixed(lower_target, lower_source), + Target::fixed(terminal_target, terminal_source), + ], + observation_group(vec![SURFACE_PORT]), + vec![], + vec![ + Paint::Solid { + id: lower_paint, + target: lower_target, + }, + Paint::Solid { + id: terminal_paint, + target: terminal_target, + }, + ], + vec![ + Surface::Input { + id: root_surface, + input: SURFACE_PORT, + }, + Surface::FromOccurrence { + id: derived_surface, + occurrence: selected_occurrence, + }, + ], + vec![ + Occurrence::new( + selected_occurrence, + lower_paint, + root_surface, + CompositionProfile::EncodedSrgb8SourceOverV1, + appearance_context(), + ), + Occurrence::new( + terminal_occurrence, + terminal_paint, + derived_surface, + CompositionProfile::EncodedSrgb8SourceOverV1, + appearance_context(), + ), + ], + ConstraintSet::new( + vec![ConstraintInvocation::hard( + REQUIRED, + terminal_occurrence, + Srgb8::new([40, 50, 60]), + )], + vec![], + ), + vec![OutputBinding::new(selected_output, lower_paint)], + ExactSrgb8IdentityV1, + ) + .with_point_presentations( + vec![PointPresentationRootV1::new( + presentation_root, + terminal_occurrence, + )], + vec![PointPresentationTargetV1::new( + presentation_root, + selected_occurrence, + )], + ) + .compile() + .unwrap(); + + let binding = compiled + .bind_point_output_presentation(selected_output, presentation_root, selected_occurrence) + .unwrap(); + assert_eq!(binding.paint(), lower_paint); + assert_eq!(binding.occurrence(), selected_occurrence); + assert_ne!(binding.paint(), terminal_paint); +} + +#[test] +fn point_output_presentation_binding_reports_each_exact_failure() { + let compiled = + exact_compiled_with_point_presentations(vec![OutputBinding::new(OUTPUT, TRANSLUCENT)]); + let missing_output = OutputSlotId::new(u32::MAX); + let missing_root = PresentationRootId::new(u32::MAX); + let missing_occurrence = OccurrenceId::new(u32::MAX); + + assert_eq!( + compiled.bind_point_output_presentation(missing_output, missing_root, missing_occurrence,), + Err(PointOutputPresentationBindErrorV1::MissingOutput { + output: missing_output, + }) + ); + assert_eq!( + compiled.bind_point_output_presentation(OUTPUT, missing_root, OCCURRENCE), + Err( + PointOutputPresentationBindErrorV1::MissingPresentationTarget { + root: missing_root, + occurrence: OCCURRENCE, + } + ) + ); + assert_eq!( + compiled.bind_point_output_presentation(OUTPUT, PRESENTATION_ROOT, missing_occurrence), + Err( + PointOutputPresentationBindErrorV1::MissingPresentationTarget { + root: PRESENTATION_ROOT, + occurrence: missing_occurrence, + } + ) + ); + + let mismatch = exact_compiled_with_point_presentations(vec![OutputBinding::new(OUTPUT, SOLID)]); + assert_eq!( + mismatch.bind_point_output_presentation(OUTPUT, PRESENTATION_ROOT, OCCURRENCE), + Err(PointOutputPresentationBindErrorV1::SubjectPaintMismatch { + output: OUTPUT, + output_paint: SOLID, + root: PRESENTATION_ROOT, + occurrence: OCCURRENCE, + subject_paint: TRANSLUCENT, + }) + ); +} + +#[test] +fn compiled_program_owner_pin_keeps_the_exact_generation_alive_until_drop() { + let compiled = exact_compiled(ConstraintSet::new( + vec![ConstraintInvocation::hard( + REQUIRED, + OCCURRENCE, + Srgb8::new([0x80; 3]), + )], + vec![], + )); + let owner_pin = compiled.pin_owner(); + let mut session = compiled.instantiate(STREAM_A).unwrap(); + drop(compiled); + + assert!(matches!( + session + .commit(observed_update(STREAM_A, 1, &[(1, [0xFF; 3])])) + .unwrap(), + SessionState::Ready { .. } + )); + + drop(owner_pin); + assert!(matches!( + session.commit(observed_update(STREAM_A, 2, &[(1, [0xFF; 3])])), + Err(SessionUpdateError::OwnerExpired) + )); +} + #[test] fn canonical_helpers_and_checked_cardinality_fail_closed() { assert_eq!(check_render_node_count(usize::MAX - 1, 1), Ok(())); diff --git a/crates/labcolors-core/src/session.rs b/crates/labcolors-core/src/session.rs index c313ccf0..19eba6cf 100644 --- a/crates/labcolors-core/src/session.rs +++ b/crates/labcolors-core/src/session.rs @@ -190,28 +190,96 @@ enum PendingSessionTransition { }, } -/// Linear, fully evaluated Session transition that has not been published yet. +/// Заимствованный prospective lifecycle для imperative shell до commit. /// -/// Dropping this value discards only prospective data. Committing consumes the -/// sole mutable borrow and publishes raw head and lifecycle with moves after -/// every fallible operation has completed. +/// Generic Session открывает только evidence и provenance ревизии; она не +/// знает, будет ли вызывающий код render-ить, lint-ить, сохранять или +/// отбрасывать результат. +pub(crate) enum PreparedSessionDispositionV1<'a, Plan: SessionPlanV1> { + Idempotent { + raw_head: ObservationHeadViewV1<'a>, + state: &'a SessionState, + }, + Unknown(&'a RevisionBoundUnknownV1), + Verified(&'a Plan::Verified), + Violation(&'a Plan::Violation), +} + +struct DisplacedSessionState { + last_verified: Option, + discarded_violation: Option, +} + +/// Вытесненные значения уже опубликованного перехода, чьё уничтожение +/// вызывающий shell обязан отложить за границу физического commit. +/// +/// Поля намеренно закрыты. Значение служит только линейным retirement-bundle; +/// owner объявлен последним и потому переживает raw/evidence при уничтожении. +pub(crate) struct DeferredSessionRetirement { + _retired_raw_head: Option, + _retired_verified: Option, + _retired_violation: Option, + _displaced_placeholder: SessionState, + _owner: Plan::OwnerLease, +} + +/// Линейный, полностью вычисленный и ещё не опубликованный переход Session. +/// +/// Drop отбрасывает только prospective data. Commit поглощает единственное +/// mutable-заимствование и публикует raw head и lifecycle перемещениями после +/// завершения всех fallible-операций. #[must_use = "commit the prepared transition or drop it intentionally"] pub(crate) struct PreparedSessionTransition<'session, Plan: SessionPlanV1> { raw_head: &'session mut SessionObservationHeadV1, state: &'session mut SessionState, pending: PendingSessionTransition, - // Rust drops fields in declaration order. Keep the lease last so abort - // destroys every prospective evidence value while its generation is live. + // Rust уничтожает поля в порядке объявления. Lease остаётся последним, + // чтобы abort уничтожил все prospective evidence при живой generation. owner: Plan::OwnerLease, } impl<'session, Plan: SessionPlanV1> PreparedSessionTransition<'session, Plan> { - /// Publish one already admitted and evaluated lifecycle transition. + /// Возвращает fully evaluated prospective disposition без публикации. + pub(crate) fn disposition(&self) -> PreparedSessionDispositionV1<'_, Plan> { + match &self.pending { + PendingSessionTransition::Idempotent => PreparedSessionDispositionV1::Idempotent { + raw_head: self.raw_head.observation_head(), + state: self.state, + }, + PendingSessionTransition::Unknown(unknown) => { + PreparedSessionDispositionV1::Unknown(unknown) + } + PendingSessionTransition::Observed { decision, .. } => match decision { + SessionDecision::Verified(verified) => { + PreparedSessionDispositionV1::Verified(verified) + } + SessionDecision::Violation(violation) => { + PreparedSessionDispositionV1::Violation(violation) + } + }, + } + } + + /// Публикует один уже допущенный и вычисленный lifecycle-переход. /// - /// This function has no failure return and performs no admission, - /// evaluation or allocation. It does not claim that an external sink has - /// accepted any output. + /// Функция не возвращает ошибку и не выполняет admission, evaluation или + /// allocation. Она не утверждает, что внешний sink принял output. pub(crate) fn commit(self) -> SessionView<'session, Plan> { + let (view, retirement) = self.commit_deferred(); + drop(retirement); + view + } + + /// Публикует пару raw-head/lifecycle, но возвращает всё вытесненное без + /// запуска пользовательских деструкторов. + /// + /// После входа в эту функцию выполняются только перемещения и записи в + /// уже существующие слоты. Это вариант для imperative shell, который уже + /// установил внешний снимок и обязан отложить retirement до следующего + /// pre-install участка. + pub(crate) fn commit_deferred( + self, + ) -> (SessionView<'session, Plan>, DeferredSessionRetirement) { let Self { raw_head, state, @@ -219,36 +287,74 @@ impl<'session, Plan: SessionPlanV1> PreparedSessionTransition<'session, Plan> { owner, } = self; - match pending { - PendingSessionTransition::Idempotent => {} - PendingSessionTransition::Unknown(unknown) => { - let next_state = match take_last_verified(state) { - Some(previous) => SessionState::Stale { previous }, - None => SessionState::Waiting, - }; - *raw_head = SessionObservationHeadV1::Unknown(unknown); - *state = next_state; - } - PendingSessionTransition::Observed { - raw_observation, - decision, - } => { - let previous = take_last_verified(state); - let next_state = match decision { - SessionDecision::Verified(current) => SessionState::Ready { current }, - SessionDecision::Violation(cause) => SessionState::Failed { cause, previous }, - }; - *raw_head = SessionObservationHeadV1::Observed(raw_observation); - *state = next_state; - } - } - - // The exact generation remains pinned through both lifecycle moves. - drop(owner); - SessionView { + let (retired_raw_head, retired_verified, retired_violation, displaced_placeholder) = + match pending { + PendingSessionTransition::Idempotent => (None, None, None, SessionState::Waiting), + PendingSessionTransition::Unknown(unknown) => { + let DisplacedSessionState { + last_verified, + discarded_violation, + } = displace_session_state(state); + let next_state = match last_verified { + Some(previous) => SessionState::Stale { previous }, + None => SessionState::Waiting, + }; + let retired_raw_head = + mem::replace(raw_head, SessionObservationHeadV1::Unknown(unknown)); + let displaced_placeholder = mem::replace(state, next_state); + ( + Some(retired_raw_head), + None, + discarded_violation, + displaced_placeholder, + ) + } + PendingSessionTransition::Observed { + raw_observation, + decision, + } => { + let DisplacedSessionState { + last_verified, + discarded_violation, + } = displace_session_state(state); + let (next_state, retired_verified) = match decision { + SessionDecision::Verified(current) => { + (SessionState::Ready { current }, last_verified) + } + SessionDecision::Violation(cause) => ( + SessionState::Failed { + cause, + previous: last_verified, + }, + None, + ), + }; + let retired_raw_head = mem::replace( + raw_head, + SessionObservationHeadV1::Observed(raw_observation), + ); + let displaced_placeholder = mem::replace(state, next_state); + ( + Some(retired_raw_head), + retired_verified, + discarded_violation, + displaced_placeholder, + ) + } + }; + + let view = SessionView { raw_head: raw_head.observation_head(), state, - } + }; + let retirement = DeferredSessionRetirement { + _retired_raw_head: retired_raw_head, + _retired_verified: retired_verified, + _retired_violation: retired_violation, + _displaced_placeholder: displaced_placeholder, + _owner: owner, + }; + (view, retirement) } } @@ -405,14 +511,27 @@ fn prepare_session_transition<'session, Plan: SessionPlanV1>( }) } -/// Move exactly one retained verified witness out of the old closed owner. -fn take_last_verified( +/// Вытесняет старый lifecycle, не уничтожая evidence до установки следующей +/// пары raw-head/state. +fn displace_session_state( state: &mut SessionState, -) -> Option { +) -> DisplacedSessionState { match mem::replace(state, SessionState::Waiting) { - SessionState::Waiting => None, - SessionState::Ready { current } => Some(current), - SessionState::Stale { previous } => Some(previous), - SessionState::Failed { previous, .. } => previous, + SessionState::Waiting => DisplacedSessionState { + last_verified: None, + discarded_violation: None, + }, + SessionState::Ready { current } => DisplacedSessionState { + last_verified: Some(current), + discarded_violation: None, + }, + SessionState::Stale { previous } => DisplacedSessionState { + last_verified: Some(previous), + discarded_violation: None, + }, + SessionState::Failed { cause, previous } => DisplacedSessionState { + last_verified: previous, + discarded_violation: Some(cause), + }, } } diff --git a/crates/labcolors-core/src/session_tests.rs b/crates/labcolors-core/src/session_tests.rs index 18b6b3bc..196ef2ff 100644 --- a/crates/labcolors-core/src/session_tests.rs +++ b/crates/labcolors-core/src/session_tests.rs @@ -11,8 +11,9 @@ use crate::observation::{ SurfaceInputBinding, UnknownReasonId, canonicalize_observation_schema, }; use crate::session::{ - Session, SessionDecision, SessionEvidenceV1, SessionObservationBindingPermitV1, SessionPlanV1, - SessionState, SessionUpdateError, private as session_private, + PreparedSessionDispositionV1, Session, SessionDecision, SessionEvidenceV1, + SessionObservationBindingPermitV1, SessionPlanV1, SessionState, SessionUpdateError, + private as session_private, }; /// Characterization tests spell an immediate commit explicitly through this @@ -268,6 +269,84 @@ impl SessionEvidenceV1 for DropOrderEvidence { } } +#[derive(Debug)] +struct RetirementEvidence { + observation: RevisionBoundObservationV1, + panic_on_next_drop: Rc>, + drops: Rc>, +} + +impl Drop for RetirementEvidence { + fn drop(&mut self) { + self.drops.set(self.drops.get() + 1); + if self.panic_on_next_drop.replace(false) { + panic!("retired evidence observed the commit boundary"); + } + } +} + +impl session_private::EvidenceSealed for RetirementEvidence {} + +impl SessionEvidenceV1 for RetirementEvidence { + fn observation(&self) -> &RevisionBoundObservationV1 { + &self.observation + } +} + +#[derive(Debug)] +struct RetirementPlan { + schema: CanonicalObservationSchemaV1, + panic_on_next_drop: Rc>, + drops: Rc>, +} + +impl session_private::PlanSealed for RetirementPlan {} + +impl SessionPlanV1 for RetirementPlan { + type OwnerLease = (); + type Verified = RetirementEvidence; + type Violation = RetirementEvidence; + type Error = SentinelError; + + fn try_acquire_owner(&self) -> Option { + Some(()) + } + + fn observation_schema<'a>( + &'a self, + _owner: &'a Self::OwnerLease, + ) -> &'a CanonicalObservationSchemaV1 { + &self.schema + } + + fn evaluate( + &mut self, + _owner: &Self::OwnerLease, + observation: RevisionBoundObservationV1, + _permit: SessionObservationBindingPermitV1, + ) -> Result, Self::Error> { + if !observation.shares_schema_backing_with(&self.schema) { + return Err(SentinelError::SchemaBackingMismatch); + } + let first = observation + .physical_values(0) + .and_then(|values| values.first()) + .copied() + .map(ColorSignal::srgb8) + .ok_or(SentinelError::EmptyObservation)?; + let evidence = RetirementEvidence { + observation, + panic_on_next_drop: Rc::clone(&self.panic_on_next_drop), + drops: Rc::clone(&self.drops), + }; + if first == Srgb8::new([255; 3]) { + Ok(SessionDecision::Verified(evidence)) + } else { + Ok(SessionDecision::Violation(evidence)) + } + } +} + #[derive(Debug)] struct DropOrderPlan { generation: std::rc::Weak, @@ -365,6 +444,23 @@ fn session() -> ( ) } +fn retirement_session() -> (Session, Rc>, Rc>) { + let panic_on_next_drop = Rc::new(Cell::new(false)); + let drops = Rc::new(Cell::new(0)); + ( + Session::new( + STREAM, + RetirementPlan { + schema: canonicalize_observation_schema(vec![SURFACE]).unwrap(), + panic_on_next_drop: Rc::clone(&panic_on_next_drop), + drops: Rc::clone(&drops), + }, + ), + panic_on_next_drop, + drops, + ) +} + fn observed_update(revision: u64, value: [u8; 3]) -> ObservationUpdateInput { ObservationUpdateInput { stream: STREAM, @@ -672,6 +768,160 @@ fn detached_plan_evidence_is_rejected_before_raw_or_lifecycle_commit() { assert_shared_observation(raw_observed(&session), ¤t_observation); } +#[test] +fn successful_commit_publishes_new_pair_before_retiring_old_evidence() { + let (mut session, panic_on_next_drop, drops) = retirement_session(); + session.commit(observed_update(1, [0; 3])).unwrap(); + assert!(matches!(session.state(), SessionState::Failed { .. })); + assert_eq!(drops.get(), 0); + + let prepared = session + .prepare_update(observed_update(2, [255; 3])) + .unwrap(); + panic_on_next_drop.set(true); + let retirement = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let _ = prepared.commit(); + })); + + assert!(retirement.is_err(), "the hostile retirement probe must run"); + assert_eq!(session.raw_head().revision(), Some(Revision::new(2))); + let SessionState::Ready { current } = session.state() else { + panic!("retirement must begin only after publishing the new state"); + }; + assert_eq!(current.observation.revision(), Revision::new(2)); + assert_eq!(drops.get(), 1); +} + +#[test] +fn deferred_commit_returns_before_hostile_retirement_destructor_runs() { + let (mut session, panic_on_next_drop, drops) = retirement_session(); + session.commit(observed_update(1, [0; 3])).unwrap(); + assert!(matches!(session.state(), SessionState::Failed { .. })); + + let prepared = session + .prepare_update(observed_update(2, [255; 3])) + .unwrap(); + panic_on_next_drop.set(true); + let committed = + std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| prepared.commit_deferred())); + let (view, retirement) = committed.expect("deferred commit must only move retirement"); + assert_eq!(view.raw_head().revision(), Some(Revision::new(2))); + assert!(matches!(view.state(), SessionState::Ready { .. })); + assert_eq!(drops.get(), 0); + + let retirement = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + drop(retirement); + })); + assert!( + retirement.is_err(), + "the hostile destructor must be deferred" + ); + assert_eq!(drops.get(), 1); +} + +#[test] +fn deferred_retirement_keeps_its_exact_owner_alive_through_old_evidence_drop() { + let alive = Rc::new(Cell::new(true)); + let events = Rc::new(RefCell::new(Vec::new())); + let generation = Rc::new(DropOrderOwnerGeneration { + schema: canonicalize_observation_schema(vec![SURFACE]).unwrap(), + alive: Rc::clone(&alive), + events: Rc::clone(&events), + }); + let generation_weak = Rc::downgrade(&generation); + let owner_slot = Rc::new(RefCell::new(Some(Rc::clone(&generation)))); + let mut session = Session::new( + STREAM, + DropOrderPlan { + generation: generation_weak.clone(), + owner_slot: Rc::clone(&owner_slot), + }, + ); + + session + .prepare_update(observed_update(1, [255; 3])) + .unwrap() + .commit(); + assert!(events.borrow().is_empty()); + + *owner_slot.borrow_mut() = Some(Rc::clone(&generation)); + let prepared = session + .prepare_update(observed_update(2, [255; 3])) + .unwrap(); + drop(generation); + assert!(alive.get()); + + let (_view, retirement) = prepared.commit_deferred(); + assert!(events.borrow().is_empty()); + assert!(alive.get()); + drop(retirement); + + assert!(!alive.get()); + assert!(generation_weak.upgrade().is_none()); + assert_eq!(&*events.borrow(), &["evidence", "owner"]); +} + +#[test] +fn dropping_prepared_transition_retires_only_pending_evidence() { + let (mut session, _, drops) = retirement_session(); + session.commit(observed_update(1, [0; 3])).unwrap(); + + let prepared = session + .prepare_update(observed_update(2, [255; 3])) + .unwrap(); + drop(prepared); + + assert_eq!(drops.get(), 1); + assert_eq!(session.raw_head().revision(), Some(Revision::new(1))); + let SessionState::Failed { cause, previous } = session.state() else { + panic!("aborting prepare must preserve the committed violation"); + }; + assert_eq!(cause.observation.revision(), Revision::new(1)); + assert!(previous.is_none()); +} + +#[test] +fn prepared_disposition_borrows_the_exact_uncommitted_outcome() { + let (mut session, _, _) = session(); + session.commit(observed_update(1, [255; 3])).unwrap(); + + let idempotent = session + .prepare_update(observed_update(1, [255; 3])) + .unwrap(); + let PreparedSessionDispositionV1::Idempotent { raw_head, state } = idempotent.disposition() + else { + panic!("same observation must prepare an idempotent disposition"); + }; + assert_eq!(raw_head.revision(), Some(Revision::new(1))); + assert!(matches!(state, SessionState::Ready { .. })); + drop(idempotent); + + let unknown = session + .prepare_unknown(Revision::new(2), UnknownReasonId::new(7)) + .unwrap(); + let PreparedSessionDispositionV1::Unknown(value) = unknown.disposition() else { + panic!("unknown payload must stay typed before commit"); + }; + assert_eq!(value.revision(), Revision::new(2)); + drop(unknown); + + let verified = session + .prepare_update(observed_update(2, [255; 3])) + .unwrap(); + let PreparedSessionDispositionV1::Verified(value) = verified.disposition() else { + panic!("verified outcome must stay typed before commit"); + }; + assert_eq!(value.observation().revision(), Revision::new(2)); + drop(verified); + + let violation = session.prepare_update(observed_update(2, [0; 3])).unwrap(); + let PreparedSessionDispositionV1::Violation(value) = violation.disposition() else { + panic!("violation outcome must stay typed before commit"); + }; + assert_eq!(value.observation().revision(), Revision::new(2)); + drop(violation); +} + #[test] fn dropping_prepared_observed_transition_preserves_committed_state_and_retry() { let (mut session, control, _) = session(); diff --git a/scripts/test_program_public_surface.py b/scripts/test_program_public_surface.py index 9bfe50c4..6b1b423c 100755 --- a/scripts/test_program_public_surface.py +++ b/scripts/test_program_public_surface.py @@ -62,6 +62,18 @@ def test_nested_alias_is_rejected_without_a_name_allowlist(self) -> None: _, leaks = program_public_surface(self.crate) self.assertEqual(len(leaks), 1) + def test_nested_program_source_file_is_rejected_by_origin(self) -> None: + self.write_all("struct.AttachmentAlias.html") + self.write_item( + "struct.AttachmentAlias.html", + ( + 'Source' + ), + ) + _, leaks = program_public_surface(self.crate) + self.assertEqual(len(leaks), 1) + def test_public_type_alias_route_to_program_is_rejected(self) -> None: self.write_all("type.Alias.html") self.write_item( diff --git a/scripts/test_verify_clean_set_receipt.py b/scripts/test_verify_clean_set_receipt.py index 1bc4c627..4cd4d86b 100644 --- a/scripts/test_verify_clean_set_receipt.py +++ b/scripts/test_verify_clean_set_receipt.py @@ -40,9 +40,14 @@ "content_digest_source", "joint_selection_source", "observation_runtime_source", + "point_attachment_source", "session_runtime_source", "signal_transport_source", ) +ATTACHMENT_PROOF_ROLES = ( + "point_attachment_test_support", + "point_attachment_tests", +) def _sha256(data: bytes) -> str: @@ -414,27 +419,11 @@ def _fixture(root: Path) -> ReceiptFixture: class ReceiptHostileTests(unittest.TestCase): - def test_valid_receipt_binds_committed_research_and_product_codec(self) -> None: - with tempfile.TemporaryDirectory() as temporary: - _fixture(Path(temporary)).verify() - - def test_product_only_mode_detects_stale_product_source_without_research_repo(self) -> None: + def _assert_product_only_rejects_missing_roles(self, roles: tuple[str, ...]) -> None: with tempfile.TemporaryDirectory() as temporary: fixture = _fixture(Path(temporary)) fixture.write_pin() - result = verify_product_receipt(fixture.product, policy=fixture.policy) - self.assertFalse(result.research_replayed) - - source = fixture.product / PRODUCT_ARTIFACT_PATHS["classifier_source"] - source.write_bytes(source.read_bytes() + b"stale\n") - with self.assertRaisesRegex(VerificationError, "receipt metadata"): - verify_product_receipt(fixture.product, policy=fixture.policy) - - def test_product_only_mode_rejects_each_missing_transitive_executor(self) -> None: - with tempfile.TemporaryDirectory() as temporary: - fixture = _fixture(Path(temporary)) - fixture.write_pin() - for role in TRANSITIVE_EXECUTOR_ROLES: + for role in roles: with self.subTest(role=role): path = fixture.product / PRODUCT_ARTIFACT_PATHS[role] original = path.read_bytes() @@ -443,11 +432,11 @@ def test_product_only_mode_rejects_each_missing_transitive_executor(self) -> Non verify_product_receipt(fixture.product, policy=fixture.policy) path.write_bytes(original) - def test_product_only_mode_rejects_each_mutated_transitive_executor(self) -> None: + def _assert_product_only_rejects_mutated_roles(self, roles: tuple[str, ...]) -> None: with tempfile.TemporaryDirectory() as temporary: fixture = _fixture(Path(temporary)) fixture.write_pin() - for role in TRANSITIVE_EXECUTOR_ROLES: + for role in roles: with self.subTest(role=role): path = fixture.product / PRODUCT_ARTIFACT_PATHS[role] original = path.read_bytes() @@ -456,6 +445,34 @@ def test_product_only_mode_rejects_each_mutated_transitive_executor(self) -> Non verify_product_receipt(fixture.product, policy=fixture.policy) path.write_bytes(original) + def test_valid_receipt_binds_committed_research_and_product_codec(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + _fixture(Path(temporary)).verify() + + def test_product_only_mode_detects_stale_product_source_without_research_repo(self) -> None: + with tempfile.TemporaryDirectory() as temporary: + fixture = _fixture(Path(temporary)) + fixture.write_pin() + result = verify_product_receipt(fixture.product, policy=fixture.policy) + self.assertFalse(result.research_replayed) + + source = fixture.product / PRODUCT_ARTIFACT_PATHS["classifier_source"] + source.write_bytes(source.read_bytes() + b"stale\n") + with self.assertRaisesRegex(VerificationError, "receipt metadata"): + verify_product_receipt(fixture.product, policy=fixture.policy) + + def test_product_only_mode_rejects_each_missing_transitive_executor(self) -> None: + self._assert_product_only_rejects_missing_roles(TRANSITIVE_EXECUTOR_ROLES) + + def test_product_only_mode_rejects_each_mutated_transitive_executor(self) -> None: + self._assert_product_only_rejects_mutated_roles(TRANSITIVE_EXECUTOR_ROLES) + + def test_product_only_mode_rejects_each_missing_attachment_proof_artifact(self) -> None: + self._assert_product_only_rejects_missing_roles(ATTACHMENT_PROOF_ROLES) + + def test_product_only_mode_rejects_each_mutated_attachment_proof_artifact(self) -> None: + self._assert_product_only_rejects_mutated_roles(ATTACHMENT_PROOF_ROLES) + def test_product_only_mode_rejects_a_receipt_changed_without_external_pin(self) -> None: with tempfile.TemporaryDirectory() as temporary: fixture = _fixture(Path(temporary)) diff --git a/scripts/verify_clean_set_receipt.py b/scripts/verify_clean_set_receipt.py index c7d377a4..f17b4241 100644 --- a/scripts/verify_clean_set_receipt.py +++ b/scripts/verify_clean_set_receipt.py @@ -77,6 +77,9 @@ "joint_selection_source": "crates/labcolors-core/src/joint.rs", "module_registration_source": "crates/labcolors-core/src/lib.rs", "observation_runtime_source": "crates/labcolors-core/src/observation.rs", + "point_attachment_source": "crates/labcolors-core/src/program/attachment.rs", + "point_attachment_test_support": "crates/labcolors-core/src/program/attachment/support.rs", + "point_attachment_tests": "crates/labcolors-core/src/program/attachment/tests.rs", "program_facade_source": "crates/labcolors-core/src/program.rs", "program_identity_source": "crates/labcolors-core/src/program_identity.rs", "program_source": "crates/labcolors-core/src/program_session.rs", diff --git a/scripts/verify_point_support_surplus.py b/scripts/verify_point_support_surplus.py index d0b6f9f8..85514bee 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 = ( - "3cc4aad62ebd85681e4cef6a15fb52ddf4fae9af06b8a35715d3fe6cf9fec173" + "838315cee50b75ff823cdb76c2ed5a72667a4b6a688a11dfc29b9eb8a5bc459c" ) EXPECTED_Q55_PROOF_SHA256 = ( "ac59cf89503170c789223b91d775213a19d4e571ef930f2ea609fcd51b14defd" @@ -209,11 +209,11 @@ def verify_source_binding() -> tuple[str, int]: (LCS_OCCURRENCE_SOURCE, b" pub(crate) const fn srgb8(self) -> Srgb8 {\n self.srgb8\n }", b" pub(crate) const fn srgb8(self) -> Srgb8 {\n Srgb8::new([0, 0, 0])\n }"), (SESSION_SOURCE, b" Self::Observed(observation) => ObservationHeadViewV1::Observed(observation),\n", b" Self::Observed(_) => ObservationHeadViewV1::Empty,\n"), (SESSION_SOURCE, b" let (raw_head, observation) = prepared.into_parts();\n let raw_observation = observation.clone();\n", b" let (raw_head, observation) = prepared.into_parts();\n *raw_head = SessionObservationHeadV1::Observed(observation.clone());\n let raw_observation = observation.clone();\n"), - (SESSION_SOURCE, b" *raw_head = SessionObservationHeadV1::Observed(raw_observation);\n", b" *raw_head = SessionObservationHeadV1::Empty;\n"), + (SESSION_SOURCE, b" let retired_raw_head = mem::replace(\n raw_head,\n SessionObservationHeadV1::Observed(raw_observation),\n );\n", b" let retired_raw_head = mem::replace(\n raw_head,\n SessionObservationHeadV1::Empty,\n );\n"), (SESSION_SOURCE, b" Some(previous) => SessionState::Stale { previous },", b" Some(_) => SessionState::Waiting,"), (SESSION_SOURCE, b" SessionObservationBindingPermitV1::mint(),", b" SessionObservationBindingPermitV1::for_test(),"), - (SESSION_SOURCE, b" SessionDecision::Verified(current) => SessionState::Ready { current },", b" SessionDecision::Verified(current) => SessionState::Stale { previous: current },"), - (SESSION_SOURCE, b" SessionDecision::Violation(cause) => SessionState::Failed { cause, previous },", b" SessionDecision::Violation(_) => SessionState::Waiting,"), + (SESSION_SOURCE, b" SessionDecision::Verified(current) => {\n (SessionState::Ready { current }, last_verified)\n }\n", b" SessionDecision::Verified(current) => {\n (SessionState::Stale { previous: current }, last_verified)\n }\n"), + (SESSION_SOURCE, b" SessionDecision::Violation(cause) => (\n SessionState::Failed {\n cause,\n previous: last_verified,\n },\n None,\n ),\n", b" SessionDecision::Violation(_) => (\n SessionState::Waiting,\n last_verified,\n ),\n"), (SESSION_SOURCE, b" return Err(SessionUpdateError::EvidenceBindingInvariant);\n", b" unreachable!();\n"), (NUMERICS_SOURCE, b"proof_ids: [NumericalProofIdV2::PointSupportReferenceSurplusIntegerV1],\n bound_status: Available", b"proof_ids: [NumericalProofIdV2::PointSupportReferenceSurplusIntegerV1],\n bound_status: Unavailable"), (COMPOSITION_SOURCE, b"f64::from(backdrop) + alpha * (f64::from(tint) - f64::from(backdrop))", b"f64::from(tint)"), diff --git a/scripts/verify_program_public_surface.py b/scripts/verify_program_public_surface.py index 36279310..6a9c78f6 100755 --- a/scripts/verify_program_public_surface.py +++ b/scripts/verify_program_public_surface.py @@ -130,6 +130,7 @@ def program_public_surface(crate_doc_root: Path) -> tuple[int, list[ProgramLeak] crate_doc_root = crate_doc_root.resolve() docs_root = crate_doc_root.parent forbidden_source = (docs_root / "src/labcolors_core/program.rs.html").resolve() + forbidden_source_dir = (docs_root / "src/labcolors_core/program").resolve() forbidden_module = (crate_doc_root / "program").resolve() pages = public_item_pages(crate_doc_root) leaks: list[ProgramLeak] = [] @@ -149,7 +150,11 @@ def program_public_surface(crate_doc_root: Path) -> tuple[int, list[ProgramLeak] continue if "src" in classes: source_links += 1 - if route == forbidden_source or _inside(route, forbidden_module): + if ( + route == forbidden_source + or _inside(route, forbidden_source_dir) + or _inside(route, forbidden_module) + ): leaks.append(ProgramLeak(public_item, href)) break if source_links == 0: