diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 2acc34b9..9a1c5264 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -790,7 +790,6 @@ jobs: "stable-cam16-glow-target-or-maximum-selection", "renderer-or-output-pipeline-equivalence", "spatial-glow-field", - "display-p3", ]; if ( JSON.stringify(manifest.supported) !== JSON.stringify(expectedSupported) || diff --git a/bindings/swift/README.md b/bindings/swift/README.md index 59d9279f..f240d726 100644 --- a/bindings/swift/README.md +++ b/bindings/swift/README.md @@ -53,7 +53,7 @@ Swift/UniFFI evidence сейчас ограничено описанным вы runtime. Solve-hex — квантование трансцендентного резолва, ±1 LSB на канал. Неуспешный solve возвращает `ColorError.Failure(category, code)`: category — закрытый enum `FailureCategory`, а не произвольная строка. Он -отделяет доказанную `unreachable` от `unresolved`, `rejected` и `unsupported`, +отделяет доказанную `unreachable` от `unresolved` и `rejected`, а code задаёт конкретную машинную причину. Оба поля приходят из одного core-owned descriptor и проверяются conformance-паком. diff --git a/bindings/swift/Tests/LabColorsConformanceTests/ConformanceTests.swift b/bindings/swift/Tests/LabColorsConformanceTests/ConformanceTests.swift index e812e566..10ba110b 100644 --- a/bindings/swift/Tests/LabColorsConformanceTests/ConformanceTests.swift +++ b/bindings/swift/Tests/LabColorsConformanceTests/ConformanceTests.swift @@ -53,7 +53,6 @@ final class ConformanceTests: XCTestCase { case "unreachable": return .unreachable case "unresolved": return .unresolved case "rejected": return .rejected - case "unsupported": return .unsupported default: fatalError("неизвестная failure category в pack: \(key)") } } diff --git a/conformance/README.md b/conformance/README.md index 5fba329a..74a50287 100644 --- a/conformance/README.md +++ b/conformance/README.md @@ -76,7 +76,7 @@ adjacent bytes или нормативного отношения пересчи - `(category, code)` — атомарная core-owned классификация, общая для всех биндингов: `unreachable/exceeds_range`, `unreachable/floor_unreachable`, `unresolved/bounded_search_exhausted`, `rejected/invalid_input`, - `unreachable/below_contrast_floor` и `unsupported/gamut_unsupported`. Только + `unreachable/below_contrast_floor`. Только `unreachable` доказывает отсутствие решения в объявленном полном domain; `unresolved` не делает утверждения о непроверенных кандидатах. diff --git a/crates/labcolors-conformance/src/lib.rs b/crates/labcolors-conformance/src/lib.rs index 19690e9f..fe3be626 100644 --- a/crates/labcolors-conformance/src/lib.rs +++ b/crates/labcolors-conformance/src/lib.rs @@ -296,7 +296,7 @@ pub enum SolveOutcome { floor_override: bool, }, /// Resolver не вернул цвет; category отделяет доказанную недостижимость от - /// unresolved, rejected и unsupported исходов. + /// unresolved и rejected исходов. Failure { /// Стабильная семантическая категория core failure. category: String, @@ -883,7 +883,6 @@ mod tests { "unreachable", "floor_unreachable", ), - (F::GamutUnsupported, "unsupported", "gamut_unsupported"), ( F::InvalidInput("fixture".into()), "rejected", 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 0e1b54c4..1e529242 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":"16f1efb88a9224b4507264f2a560eaa6b174a24020a208266c07a2f47309312a","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":"6b81edc2d56bdca78a4d7c579c79e2791dc59e9bf2626fd22ddc7aa164dfa5c8","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":"e361cf144e0630a4f2ff52261e6515c9394188fc1351c97b98ea9c0ec87ec39d"},{"kind":"rust-source","path":"crates/labcolors-core/src/composition.rs","sha256":"195a67327a3bd86d7816b634481389930bf68577bb1202fad14c2ea152df8625"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/exact.rs","sha256":"33b959f11366415143b5b03fcfe370d1fb7e61e46ed05349ed17560d10663ff7"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/mod.rs","sha256":"37cff33755c5a700853ccbc08bd539d2235325b9eb208be696444799d4cf819e"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/wcag22.rs","sha256":"7e10638e8da68dc1279f078e0a0daa5caf10af403eb00c1cbbd5506190e74d9d"},{"kind":"rust-source","path":"crates/labcolors-core/src/hash.rs","sha256":"f97a0fd7d6ad3162f0f1dfb326fccfb7ed40da9a8fa67a5b8a239a1ae2ae49c3"},{"kind":"rust-source","path":"crates/labcolors-core/src/lcs_occurrence.rs","sha256":"a58acbae5b06a8cd9a45adf93ddfbdfb569c5916b60140bd4046182aab2a9518"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"1c75a1bc5f33c497aff148fbe5d943716b5d88f680623e8cbf028545c08958c7"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"abed15a542b5e8031f0ebfe36138232920c7898c9eae1a2037c6455033ecd91e"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"945755f2eb91eb21aeee9b7a7b0e67b093d7e766af35a121480003669d88a053"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"0a1fdf10c9ecae497cfcd447a6af937f2b07d43b81b2cd28a50d998e11197fe9"},{"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":"e70ce1fadbb64b044772d9f18f7c16ee76cf6aa33b1d1c0e642e0437fd5ed4a3"} +{"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":"fd8cc51bc850523f600cc850b10af56afb450dccaeb928c795d2c45c421df8e4","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":"2e63424882231ec2f00ae79911bf74cfb935bb96f11368bce53240468f7f2dd4","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":"e361cf144e0630a4f2ff52261e6515c9394188fc1351c97b98ea9c0ec87ec39d"},{"kind":"rust-source","path":"crates/labcolors-core/src/composition.rs","sha256":"195a67327a3bd86d7816b634481389930bf68577bb1202fad14c2ea152df8625"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/exact.rs","sha256":"33b959f11366415143b5b03fcfe370d1fb7e61e46ed05349ed17560d10663ff7"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/mod.rs","sha256":"37cff33755c5a700853ccbc08bd539d2235325b9eb208be696444799d4cf819e"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/wcag22.rs","sha256":"7e10638e8da68dc1279f078e0a0daa5caf10af403eb00c1cbbd5506190e74d9d"},{"kind":"rust-source","path":"crates/labcolors-core/src/hash.rs","sha256":"f97a0fd7d6ad3162f0f1dfb326fccfb7ed40da9a8fa67a5b8a239a1ae2ae49c3"},{"kind":"rust-source","path":"crates/labcolors-core/src/lcs_occurrence.rs","sha256":"a58acbae5b06a8cd9a45adf93ddfbdfb569c5916b60140bd4046182aab2a9518"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"0f0e0ed726aba4defc750b793a76435a3f3825e91002c2cd90d982be7260f180"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"abed15a542b5e8031f0ebfe36138232920c7898c9eae1a2037c6455033ecd91e"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"945755f2eb91eb21aeee9b7a7b0e67b093d7e766af35a121480003669d88a053"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"0a1fdf10c9ecae497cfcd447a6af937f2b07d43b81b2cd28a50d998e11197fe9"},{"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":"a8f1b4f65a45bf18ad8c2ae21cc88e5cd270a5ee438d625153b66d26660a7e90"} diff --git a/crates/labcolors-core/contracts/solve-characterization-v1-linux-x64.json b/crates/labcolors-core/contracts/solve-characterization-v1-linux-x64.json index f778ca88..b9947c14 100644 --- a/crates/labcolors-core/contracts/solve-characterization-v1-linux-x64.json +++ b/crates/labcolors-core/contracts/solve-characterization-v1-linux-x64.json @@ -62,7 +62,6 @@ "bg=#FFFFFF contract=text(150) floor=default hue=264 chroma=neutral": "err exceeds_range target_bits=4062c00000000000 max_achievable_bits=405a829a490ad002", "bg=#FFFFFF contract=text(3) floor=none hue=0 chroma=neutral": "err below_contrast_floor target_bits=4008000000000000", "bg=#FFFFFF contract=text(30) floor=default hue=264 chroma=neutral": "ok hex=#767676 lc_bits=40510fca61d5b9ee wcag_ratio_bits=40122b3d05125164 floor_override=true jp_bits=404a3678a8f0c874 h_ok_bits=0000000000000000 s_bits=3f92a6b801503a78", - "bg=#FFFFFF contract=text(60) floor=default hue=0 chroma=neutral gamut=display-p3": "err gamut_unsupported", "bg=#FFFFFF contract=text(60) floor=default hue=264 chroma=neutral": "ok hex=#767676 lc_bits=40510fca61d5b9ee wcag_ratio_bits=40122b3d05125164 floor_override=true jp_bits=404a3678a8f0c874 h_ok_bits=0000000000000000 s_bits=3f92a6b801503a78", "bg=#FFFFFF contract=text(7.3) floor=none hue=0 chroma=neutral": "ok hex=#EDEDED lc_bits=401e6aa8625b8742 wcag_ratio_bits=3ff2bb539763a677 floor_override=false jp_bits=4057afba8603b8da h_ok_bits=0000000000000000 s_bits=3f8f614c4436cac4", "bg=#FFFFFF contract=text(7.35) floor=none hue=0 chroma=neutral": "ok hex=#EDEDED lc_bits=401e6aa8625b8742 wcag_ratio_bits=3ff2bb539763a677 floor_override=false jp_bits=4057afba8603b8da h_ok_bits=0000000000000000 s_bits=3f8f614c4436cac4", diff --git a/crates/labcolors-core/contracts/solve-characterization-v1-macos-aarch64.json b/crates/labcolors-core/contracts/solve-characterization-v1-macos-aarch64.json index e3d55f3f..115971b1 100644 --- a/crates/labcolors-core/contracts/solve-characterization-v1-macos-aarch64.json +++ b/crates/labcolors-core/contracts/solve-characterization-v1-macos-aarch64.json @@ -62,7 +62,6 @@ "bg=#FFFFFF contract=text(150) floor=default hue=264 chroma=neutral": "err exceeds_range target_bits=4062c00000000000 max_achievable_bits=405a829a490ad002", "bg=#FFFFFF contract=text(3) floor=none hue=0 chroma=neutral": "err below_contrast_floor target_bits=4008000000000000", "bg=#FFFFFF contract=text(30) floor=default hue=264 chroma=neutral": "ok hex=#767676 lc_bits=40510fca61d5b9ee wcag_ratio_bits=40122b3d05125164 floor_override=true jp_bits=404a3678a8f0c874 h_ok_bits=0000000000000000 s_bits=3f92a6b801503a78", - "bg=#FFFFFF contract=text(60) floor=default hue=0 chroma=neutral gamut=display-p3": "err gamut_unsupported", "bg=#FFFFFF contract=text(60) floor=default hue=264 chroma=neutral": "ok hex=#767676 lc_bits=40510fca61d5b9ee wcag_ratio_bits=40122b3d05125164 floor_override=true jp_bits=404a3678a8f0c874 h_ok_bits=0000000000000000 s_bits=3f92a6b801503a78", "bg=#FFFFFF contract=text(7.3) floor=none hue=0 chroma=neutral": "ok hex=#EDEDED lc_bits=401e6aa8625b8742 wcag_ratio_bits=3ff2bb539763a677 floor_override=false jp_bits=4057afba8603b8da h_ok_bits=0000000000000000 s_bits=3f8f614c4436cac4", "bg=#FFFFFF contract=text(7.35) floor=none hue=0 chroma=neutral": "ok hex=#EDEDED lc_bits=401e6aa8625b8742 wcag_ratio_bits=3ff2bb539763a677 floor_override=false jp_bits=4057afba8603b8da h_ok_bits=0000000000000000 s_bits=3f8f614c4436cac4", diff --git a/crates/labcolors-core/src/lib.rs b/crates/labcolors-core/src/lib.rs index 09e418be..3734dc54 100644 --- a/crates/labcolors-core/src/lib.rs +++ b/crates/labcolors-core/src/lib.rs @@ -237,7 +237,6 @@ pub use solve::{ SolveFailureCategory, SolveJob, Solved, solve, solve_many, }; pub use spaces::oklch::{css_alpha_value, oklch_css_from_hex, oklch_from_hex}; -pub use spaces::p3::{p3_css_from_hex, p3_from_hex}; pub use spaces::srgb::srgb_encoded_from_hex; pub use spaces::vc::ViewingConditions; diff --git a/crates/labcolors-core/src/scale.rs b/crates/labcolors-core/src/scale.rs index 6e2b3fb6..db7f042c 100644 --- a/crates/labcolors-core/src/scale.rs +++ b/crates/labcolors-core/src/scale.rs @@ -752,16 +752,10 @@ fn quadratic_roots(d: f64, c: f64, b: f64) -> ([f64; 3], usize) { (roots, 2) } -/// Стена гамута **Display P3** при `(L, h)` — та же бисекция, что -/// [`max_chroma_bisect`], но валидность кандидата проверяется в ЛИНЕЙНОМ P3 -/// (Oklab → линейный sRGB → XYZ → линейный P3; первые два шага — линейная -/// алгебра, корректная и за пределами sRGB-куба). -/// -/// Этап 1 gamut-aware солвера (2026-07-03): геометрия стен и решётка эмиссии; -/// перевод `Solved`/эмиссии на P3-кандидаты — этап 2. Чистая гамут-геометрия -/// CSS Color 4 матриц — нуля подгонки (класс M-13 инвентаря). -// Прод-потребитель — этап 2 (P3-кандидаты солвера); до него читается тестами. -#[cfg_attr(not(test), allow(dead_code))] +/// Test-only physical Display P3 gamut boundary. This is geometry, not a +/// released output capability: no production selector, encoder, or verifier +/// consumes it yet. +#[cfg(test)] pub(crate) fn max_chroma_p3_bisect(l_ok: f64, h_ok_deg: f64) -> f64 { let h_ok = h_ok_deg.to_radians(); let cos_h = h_ok.cos(); diff --git a/crates/labcolors-core/src/semantic.rs b/crates/labcolors-core/src/semantic.rs index 033f1768..d899d933 100644 --- a/crates/labcolors-core/src/semantic.rs +++ b/crates/labcolors-core/src/semantic.rs @@ -8,8 +8,8 @@ //! текущего фона и viewing conditions; сериализация принадлежит биндингу. //! //! Граница набора атомарна. Доказанная недостижимость и незавершённый bounded -//! search остаются типизированными исходами отдельных ролей; rejected, -//! unsupported и internal закрывают вызов через [`ResolveSetError`] без +//! search остаются типизированными исходами отдельных ролей; rejected и +//! internal закрывают вызов через [`ResolveSetError`] без //! частичного успешного вектора. Нулевое значение представлено явно через //! [`Resolved::None`]. //! @@ -1335,8 +1335,6 @@ impl std::error::Error for RoleFailure { pub enum ResolveSetErrorKind { /// Значение запроса вышло за объявленный домен. Rejected, - /// Запрос требует capability, которую этот резолвер не реализует. - Unsupported, /// Состояние, произведённое ядром, нарушило внутренний постинвариант. Internal, } @@ -1346,7 +1344,6 @@ impl ResolveSetErrorKind { pub const fn as_str(self) -> &'static str { match self { Self::Rejected => SolveFailureCategory::Rejected.as_str(), - Self::Unsupported => SolveFailureCategory::Unsupported.as_str(), Self::Internal => "internal", } } @@ -1355,16 +1352,15 @@ impl ResolveSetErrorKind { #[derive(Debug, Clone, PartialEq)] enum ResolveSetErrorState { Rejected(BoundaryFailure), - Unsupported(BoundaryFailure), Internal(SolveFailure), } /// Отказ всего набора из [`resolve_named_set`]. /// -/// Rejected-запросы, неподдержанные capability и внутренний дрейф закрывают -/// весь вызов. Конструкторы приватны; допуск делит [`SolveFailure::boundary`] -/// с [`RoleFailure`], а [`Self::kind`] и [`Self::code`] остаются -/// авторитетной whole-call-классификацией. +/// Rejected-запросы и внутренний дрейф закрывают весь вызов. Конструкторы +/// приватны; допуск делит [`SolveFailure::boundary`] с [`RoleFailure`], а +/// [`Self::kind`] и [`Self::code`] остаются авторитетной whole-call- +/// классификацией. #[derive(Debug, Clone, PartialEq)] pub struct ResolveSetError { state: ResolveSetErrorState, @@ -1375,17 +1371,15 @@ impl ResolveSetError { pub const fn kind(&self) -> ResolveSetErrorKind { match &self.state { ResolveSetErrorState::Rejected(_) => ResolveSetErrorKind::Rejected, - ResolveSetErrorState::Unsupported(_) => ResolveSetErrorKind::Unsupported, ResolveSetErrorState::Internal(_) => ResolveSetErrorKind::Internal, } } - /// Стабильный машинный код ядра для rejected/unsupported-отказов. + /// Стабильный машинный код ядра для rejected-отказов. /// У внутреннего дрейфа намеренно нет публичного solver-кода. pub const fn code(&self) -> Option<&'static str> { match &self.state { - ResolveSetErrorState::Rejected(evidence) - | ResolveSetErrorState::Unsupported(evidence) => Some(evidence.boundary.code()), + ResolveSetErrorState::Rejected(evidence) => Some(evidence.boundary.code()), ResolveSetErrorState::Internal(_) => None, } } @@ -1393,8 +1387,7 @@ impl ResolveSetError { /// Структурированный исходный отказ — диагностика и точные evidence-поля. pub const fn reason(&self) -> &SolveFailure { match &self.state { - ResolveSetErrorState::Rejected(evidence) - | ResolveSetErrorState::Unsupported(evidence) => &evidence.reason, + ResolveSetErrorState::Rejected(evidence) => &evidence.reason, ResolveSetErrorState::Internal(reason) => reason, } } @@ -1418,10 +1411,9 @@ type PendingResolution = Result; /// Исход резолва одной допущенной роли: решённый цвет, честный ноль, /// типизированная численная неопределённость или локальный отказ роли. /// -/// Доказанная недостижимость и незавершённый bounded search отдаются -/// пер-ролью и не маскируются. Rejected/unsupported/internal провенанс в этом -/// типе жить не может: он закрывает [`resolve_named_set`] через -/// [`ResolveSetError`]. +/// Доказанная недостижимость и незавершённый bounded search отдаются пер-ролью +/// и не маскируются. Rejected/internal провенанс в этом типе жить не может: он +/// закрывает [`resolve_named_set`] через [`ResolveSetError`]. #[derive(Debug, Clone, PartialEq)] #[non_exhaustive] pub enum Resolved { @@ -1479,9 +1471,6 @@ fn classify_role_failure(reason: SolveFailure) -> Result Err(ResolveSetError { state: ResolveSetErrorState::Rejected(BoundaryFailure { reason, boundary }), }), - SolveFailureCategory::Unsupported => Err(ResolveSetError { - state: ResolveSetErrorState::Unsupported(BoundaryFailure { reason, boundary }), - }), }, None => Err(ResolveSetError { state: ResolveSetErrorState::Internal(reason), @@ -3766,11 +3755,9 @@ fn demotion_outcome( Err(failure) => match failure.boundary().map(|boundary| boundary.category()) { Some(SolveFailureCategory::Unreachable) => Ok(None), Some(SolveFailureCategory::Unresolved) | None => Err(failure), - Some(SolveFailureCategory::Rejected | SolveFailureCategory::Unsupported) => { - Err(SolveFailure::InternalInvariant(format!( - "validated sRGB hierarchy demotion produced {failure}" - ))) - } + Some(SolveFailureCategory::Rejected) => Err(SolveFailure::InternalInvariant(format!( + "validated sRGB hierarchy demotion produced {failure}" + ))), }, } } @@ -3990,11 +3977,6 @@ mod tests { ResolveSetErrorKind::Rejected, Some("invalid_input"), ), - ( - SolveFailure::GamutUnsupported, - ResolveSetErrorKind::Unsupported, - Some("gamut_unsupported"), - ), ( SolveFailure::InternalInvariant("injected drift".into()), ResolveSetErrorKind::Internal, @@ -4258,7 +4240,6 @@ mod tests { floor: 4.5, max_ratio: 3.0, }, - SolveFailure::GamutUnsupported, SolveFailure::InvalidInput("generated probe".into()), ] { assert!( @@ -4319,15 +4300,11 @@ mod tests { ] { assert_eq!(demotion_outcome(Err(failure), 20.0), Ok(None)); } - for failure in [ - SolveFailure::GamutUnsupported, - SolveFailure::InvalidInput("generated request".into()), - ] { - assert!(matches!( - demotion_outcome(Err(failure), 20.0), - Err(SolveFailure::InternalInvariant(_)) - )); - } + let failure = SolveFailure::InvalidInput("generated request".into()); + assert!(matches!( + demotion_outcome(Err(failure), 20.0), + Err(SolveFailure::InternalInvariant(_)) + )); } #[test] diff --git a/crates/labcolors-core/src/solve.rs b/crates/labcolors-core/src/solve.rs index ceaf0b2f..66472972 100644 --- a/crates/labcolors-core/src/solve.rs +++ b/crates/labcolors-core/src/solve.rs @@ -73,16 +73,29 @@ pub enum ChromaPolicy { Relative(f64), } -/// Output colour gamut. The solver produces colours inside this gamut. +/// Output colour gamut. The public surface lists only executable output paths. +/// +/// A reserved Display P3 selector is intentionally not a public capability: +/// +/// ```compile_fail +/// let _ = labcolors_core::Gamut::DisplayP3; +/// ``` +/// +/// Re-encoding an already sRGB-bounded hex value is not a P3 output path, so +/// those former helpers are intentionally absent too: +/// +/// ```compile_fail +/// use labcolors_core::p3_from_hex; +/// ``` +/// +/// ```compile_fail +/// use labcolors_core::p3_css_from_hex; +/// ``` #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[non_exhaustive] pub enum Gamut { /// Standard sRGB. Srgb, - /// Display P3. Reserved: the wider-gamut chroma boundary lands in a later - /// chapter, so v1 returns [`SolveFailure::GamutUnsupported`] rather than - /// silently solving in sRGB. - DisplayP3, } /// The WCAG 2.1 AA legal contrast floor a contract must clear. @@ -339,8 +352,8 @@ impl Solved { /// Why a solve did not return a colour. The variant and its /// [`SolveFailureCategory`] distinguish proof of unreachability from an -/// exhausted algorithm, a rejected request, an unsupported capability, and an -/// internal invariant. Bindings must fail closed on [`Self::InternalInvariant`]. +/// exhausted algorithm, a rejected request, and an internal invariant. Bindings +/// must fail closed on [`Self::InternalInvariant`]. #[derive(Debug, Clone, PartialEq)] #[non_exhaustive] pub enum SolveFailure { @@ -363,8 +376,6 @@ pub enum SolveFailure { /// light-on-dark). `max_ratio` is the most contrast this background can /// supply in that polarity; `floor` is the ratio the contract required. FloorUnreachable { floor: f64, max_ratio: f64 }, - /// The requested gamut is not supported yet (Display P3 arrives later). - GamutUnsupported, /// Malformed input, such as an invalid hex colour or a non-finite target. InvalidInput(String), /// A value produced and validated by the core later violated an internal @@ -386,8 +397,6 @@ pub enum SolveFailureCategory { Unresolved, /// The request is malformed or inconsistent with the declared domain. Rejected, - /// The request is valid, but the requested capability is not implemented. - Unsupported, } impl SolveFailureCategory { @@ -397,7 +406,6 @@ impl SolveFailureCategory { Self::Unreachable => "unreachable", Self::Unresolved => "unresolved", Self::Rejected => "rejected", - Self::Unsupported => "unsupported", } } } @@ -439,7 +447,6 @@ impl SolveFailure { Self::FloorUnreachable { .. } => { (SolveFailureCategory::Unreachable, "floor_unreachable") } - Self::GamutUnsupported => (SolveFailureCategory::Unsupported, "gamut_unsupported"), Self::InvalidInput(_) => (SolveFailureCategory::Rejected, "invalid_input"), Self::InternalInvariant(_) => return None, }; @@ -472,12 +479,6 @@ impl core::fmt::Display for SolveFailure { f, "WCAG floor {floor:.1}:1 is unreachable on this background (max {max_ratio:.2}:1)" ), - Self::GamutUnsupported => { - write!( - f, - "requested gamut is not supported yet (Display P3 is future work)" - ) - } Self::InvalidInput(msg) => write!(f, "invalid input: {msg}"), Self::InternalInvariant(msg) => write!(f, "internal invariant failure: {msg}"), } @@ -507,10 +508,7 @@ pub fn solve( vc: &ViewingConditions, gamut: Gamut, ) -> Result { - // The Display P3 chroma boundary is future work (chapter 5); fail loudly. - if gamut != Gamut::Srgb { - return Err(SolveFailure::GamutUnsupported); - } + let Gamut::Srgb = gamut; validate_job(contract, hue, chroma_policy)?; // Compute the background's quantised display-luminance interval once and // hand it to [`solve_in`]; batch/set entry points reuse the same value for @@ -539,17 +537,14 @@ pub struct SolveJob { /// quantised display-luminance interval is computed once for the whole slice. /// The returned vector is positional: entry `i` is the result for `jobs[i]`, /// each carrying its own `Result` so one failed request never fails the batch. -/// A whole-batch failure (unsupported gamut, or a background that cannot be -/// reduced) is the outer `Err`. +/// A background that cannot be reduced is the outer `Err`. pub fn solve_many( bg: BgInput, jobs: &[SolveJob], vc: &ViewingConditions, gamut: Gamut, ) -> Result>, SolveFailure> { - if gamut != Gamut::Srgb { - return Err(SolveFailure::GamutUnsupported); - } + let Gamut::Srgb = gamut; // Background side: one forward for the whole batch (see [`solve`]). let interval = bg.luma_interval(vc)?; Ok(jobs @@ -1423,11 +1418,6 @@ mod tests { SolveFailureCategory::Unreachable, "floor_unreachable", ), - ( - SolveFailure::GamutUnsupported, - SolveFailureCategory::Unsupported, - "gamut_unsupported", - ), ( SolveFailure::InvalidInput("x".into()), SolveFailureCategory::Rejected, @@ -1468,7 +1458,6 @@ mod tests { floor: 4.5, max_ratio: 2.0, }, - SolveFailure::GamutUnsupported, SolveFailure::InvalidInput("x".to_string()), SolveFailure::InternalInvariant("x".to_string()), ]; @@ -1481,7 +1470,6 @@ mod tests { | SolveFailure::ExceedsRange { .. } | SolveFailure::BoundedSearchExhausted { .. } | SolveFailure::FloorUnreachable { .. } - | SolveFailure::GamutUnsupported | SolveFailure::InvalidInput(_) | SolveFailure::InternalInvariant(_) => {} } @@ -2040,24 +2028,6 @@ mod tests { ); } - #[test] - fn display_p3_gamut_is_reserved_not_implemented() { - // SEAM (c): the P3 variant exists in the type but returns a real error, - // never a panic and never a silent sRGB fallback. - let vc = ViewingConditions::srgb(); - let bg = BgInput::solid("#FFFFFF").unwrap(); - let err = solve( - bg, - Contract::text(60.0), - Hue::deg(0.0), - ChromaPolicy::Neutral, - &vc, - Gamut::DisplayP3, - ) - .unwrap_err(); - assert_eq!(err, SolveFailure::GamutUnsupported); - } - #[test] fn degenerate_range_matches_explicit_target() { // SEAM (b): a degenerate range [t, t] solves identically to text(t). diff --git a/crates/labcolors-core/src/spaces/mod.rs b/crates/labcolors-core/src/spaces/mod.rs index f3c2e20d..2445d3cd 100644 --- a/crates/labcolors-core/src/spaces/mod.rs +++ b/crates/labcolors-core/src/spaces/mod.rs @@ -2,6 +2,7 @@ pub(crate) mod cam16; pub(crate) mod cat16; pub(crate) mod oklab; pub(crate) mod oklch; +#[cfg(test)] pub(crate) mod p3; pub(crate) mod srgb; pub(crate) mod vc; diff --git a/crates/labcolors-core/src/spaces/oklch.rs b/crates/labcolors-core/src/spaces/oklch.rs index 83874159..e7e7fe29 100644 --- a/crates/labcolors-core/src/spaces/oklch.rs +++ b/crates/labcolors-core/src/spaces/oklch.rs @@ -4,7 +4,7 @@ //! …в идеале бы выводить окончательно oklch». Солид и полупрозрачная роль //! отличаются ФИЗИКОЙ (α), но не синтаксисом: `oklch(L% C H)` и //! `oklch(L% C H / A)` — один парсер, одна форма, явно именованные -//! компоненты, готовность к широкому гамуту (P3 — этап gamut-aware солвера). +//! компоненты. //! //! Значения остаются решёнными в sRGB-гамуте: oklch здесь — система координат //! записи, не расширение гамута. Точность цифр подобрана под БАЙТ-ТОЧНЫЙ @@ -55,7 +55,7 @@ pub fn oklch_css_from_hex(hex: &str, alpha: Option) -> Result, значения -//! эталонной реализации colorjs.io). Передаточная функция Display P3 идентична -//! sRGB (IEC 61966-2-1 § 6.4) — переиспользуются [`super::srgb::srgb_gamma`] / -//! [`super::srgb::srgb_gamma_inv`], вторых копий кривой нет. -//! -//! Точность цифр подобрана под БАЙТ-ТОЧНЫЙ round-trip: `p3_css_from_hex` → -//! парс → XYZ → sRGB даёт исходные 8-битные байты на решётке с шагом 5 по -//! каждому каналу (около 140 тысяч цветов, включая края; тест -//! `round_trip_is_byte_exact_on_lattice`). Это не полный перебор куба. - -use super::srgb::{srgb_from_hex, srgb_gamma, srgb_to_xyz}; +//! This module is test-only until a complete output-profile release supplies +//! its own candidate domain, encoder, and final encoded verifier. The sole +//! retained operation is the physical XYZ(D65) to linear Display P3 transform +//! used by the private gamut-boundary regression. It is not an output +//! capability and exposes no public selector or CSS serializer. -// ------------------------------------------------------------------ -// linear Display P3 → XYZ(D65) -// ------------------------------------------------------------------ -#[rustfmt::skip] -const P3_TO_XYZ_D65: [[f64; 3]; 3] = [ - [ 0.486_570_948_648_216_15, 0.265_667_693_169_093_06, 0.198_217_285_234_362_5 ], - [ 0.228_974_564_069_748_78, 0.691_738_521_836_506_4, 0.079_286_914_093_745 ], - [ 0.0, 0.045_113_381_858_902_64, 1.043_944_368_900_976 ], -]; - -// ------------------------------------------------------------------ -// XYZ(D65) → linear Display P3 -// ------------------------------------------------------------------ #[rustfmt::skip] const XYZ_D65_TO_P3: [[f64; 3]; 3] = [ [ 2.493_496_911_941_425, -0.931_383_617_919_123_9, -0.402_710_784_450_716_84 ], @@ -54,367 +13,36 @@ const XYZ_D65_TO_P3: [[f64; 3]; 3] = [ [ 0.035_845_830_243_784_47, -0.076_172_389_268_041_82, 0.956_884_524_007_687_2 ], ]; -fn mat_vec_mul(m: [[f64; 3]; 3], v: [f64; 3]) -> [f64; 3] { - [ - m[0][0] * v[0] + m[0][1] * v[1] + m[0][2] * v[2], - m[1][0] * v[0] + m[1][1] * v[1] + m[1][2] * v[2], - m[2][0] * v[0] + m[2][1] * v[1] + m[2][2] * v[2], - ] -} - -/// XYZ(D65, Y∈[0,1]) → линейный Display P3. +/// XYZ(D65, Y in `[0, 1]`) to linear Display P3. pub(crate) fn xyz_to_p3_linear(xyz: [f64; 3]) -> [f64; 3] { - mat_vec_mul(XYZ_D65_TO_P3, xyz) -} - -/// Линейный Display P3 → XYZ(D65, Y∈[0,1]). -#[cfg_attr(not(test), allow(dead_code))] -pub(crate) fn p3_linear_to_xyz(rgb: [f64; 3]) -> [f64; 3] { - mat_vec_mul(P3_TO_XYZ_D65, rgb) -} - -/// Гашение вычислительного шума у краёв [0, 1]. -/// -/// Для sRGB-входа компоненты P3 математически лежат в [0, 1] (sRGB ⊂ P3); -/// за края может выйти только f64-шум цепочки матриц (≲1e-12, у белой точки — -/// две независимые деривации D65). Шум гасится, реальный выход за гамут — -/// честная ошибка вызывающего (сюда такие значения не приходят, пока солвер -/// работает в sRGB-гамуте; гард — на будущий gamut-aware этап). -const GAMUT_NOISE: f64 = 1e-9; - -fn clamp_gamut_noise(v: f64) -> Result { - if !(-GAMUT_NOISE..=1.0 + GAMUT_NOISE).contains(&v) { - return Err(format!("компонента P3 вне гамута: {v}")); - } - Ok(v.clamp(0.0, 1.0)) -} - -/// Гамма-кодированные компоненты Display P3 `[r, g, b]` (каждая в [0, 1]) -/// из sRGB-hex-солида. -/// -/// Путь: hex → линейный sRGB → XYZ(D65) → линейный P3 → передаточная кривая -/// (общая с sRGB, IEC 61966-2-1 § 6.4). -/// -/// # Errors -/// -/// `Err` — невалидный hex (пробрасывается из парсера) либо компонента вне -/// гамута сверх шумового эпсилона (недостижимо для валидного sRGB-входа). -pub fn p3_from_hex(hex: &str) -> Result<[f64; 3], String> { - let xyz = srgb_to_xyz(srgb_from_hex(hex)?); - let lin = xyz_to_p3_linear(xyz); - Ok([ - srgb_gamma(clamp_gamut_noise(lin[0])?), - srgb_gamma(clamp_gamut_noise(lin[1])?), - srgb_gamma(clamp_gamut_noise(lin[2])?), - ]) -} - -/// CSS-строка `color(display-p3 R G B)` / `color(display-p3 R G B / A)` из -/// sRGB-hex-солида и опциональной альфы. -/// -/// Точность: 6 знаков на компоненту — ошибка квантования печати ≤ 5·10⁻⁷ при -/// полушаге 8-битного канала ≈ 2·10⁻³, запас > 3 порядков; байт-точность -/// round-trip проверена тестом на решётке с шагом 5. Политика альфы — единая -/// (`super::oklch::css_alpha_suffix`): та же, что у oklch-эмиссии. -/// -/// # Errors -/// -/// `Err` — невалидный hex, неконечная альфа либо альфа вне `[0, 1]`. -pub fn p3_css_from_hex(hex: &str, alpha: Option) -> Result { - let [r, g, b] = p3_from_hex(hex)?; - let suffix = super::oklch::css_alpha_suffix(alpha)?; - Ok(format!("color(display-p3 {r:.6} {g:.6} {b:.6}{suffix})")) -} - -// ------------------------------------------------------------------ -// 8-битная решётка эмиссии P3 (этап 1 gamut-aware солвера, 2026-07-03). -// МЁРТВАЯ В ПРОД — только тесты до подключения этапом 2 (см. модульный -// раздел «Потребление»); каждая функция несёт allow(dead_code) для non-test. -// ------------------------------------------------------------------ - -/// 8-битное квантование линейного P3: передаточная кривая (общая с sRGB) → -/// байты. Решётка кандидатов будущего P3-солвера — зеркало sRGB-пути -/// (quantise + измерение на отданном значении). -/// -/// # Errors -/// -/// `Err` — компонента вне гамута P3 сверх шумового эпсилона: квантовать -/// такое значение как цвет нельзя. -#[cfg_attr(not(test), allow(dead_code))] // прод-потребитель — этап 2 -pub(crate) fn p3_bytes_from_linear(lin: [f64; 3]) -> Result<[u8; 3], String> { - let mut out = [0_u8; 3]; - for (i, &v) in lin.iter().enumerate() { - let encoded = srgb_gamma(clamp_gamut_noise(v)?); - out[i] = (encoded * 255.0).round() as u8; - } - Ok(out) -} - -/// Линейный P3 из 8-битных байтов решётки (обратный путь квантования). -#[cfg_attr(not(test), allow(dead_code))] // прод-потребитель — этап 2 -pub(crate) fn p3_linear_from_bytes(bytes: [u8; 3]) -> [f64; 3] { [ - super::srgb::srgb_gamma_inv(f64::from(bytes[0]) / 255.0), - super::srgb::srgb_gamma_inv(f64::from(bytes[1]) / 255.0), - super::srgb::srgb_gamma_inv(f64::from(bytes[2]) / 255.0), + XYZ_D65_TO_P3[0][0] * xyz[0] + XYZ_D65_TO_P3[0][1] * xyz[1] + XYZ_D65_TO_P3[0][2] * xyz[2], + XYZ_D65_TO_P3[1][0] * xyz[0] + XYZ_D65_TO_P3[1][1] * xyz[1] + XYZ_D65_TO_P3[1][2] * xyz[2], + XYZ_D65_TO_P3[2][0] * xyz[0] + XYZ_D65_TO_P3[2][1] * xyz[1] + XYZ_D65_TO_P3[2][2] * xyz[2], ] } -/// CSS-строка `color(display-p3 R G B [/ A])` из 8-битных байтов решётки. -/// Точность печати и политика альфы — те же, что у [`p3_css_from_hex`] -/// (6 знаков: полушаг канала ≈ 2·10⁻³, запас > 3 порядков; байт-точность -/// round-trip доказана тестом на решётке). -#[cfg_attr(not(test), allow(dead_code))] // прод-потребитель — этап 2 -pub(crate) fn p3_css_from_bytes(bytes: [u8; 3], alpha: Option) -> Result { - let r = f64::from(bytes[0]) / 255.0; - let g = f64::from(bytes[1]) / 255.0; - let b = f64::from(bytes[2]) / 255.0; - let suffix = super::oklch::css_alpha_suffix(alpha)?; - Ok(format!("color(display-p3 {r:.6} {g:.6} {b:.6}{suffix})")) -} - #[cfg(test)] mod tests { - use super::*; - use crate::spaces::srgb::{hex_from_srgb, srgb_gamma_inv, xyz_to_srgb}; - - /// Парсер эмитированной строки — эталонная реконструкция потребителя: - /// браузер декодирует компоненты той же передаточной кривой и тем же - /// матричным путём P3 → XYZ → sRGB. - fn parse_emitted(css: &str) -> (String, Option) { - let inner = css - .strip_prefix("color(display-p3 ") - .and_then(|s| s.strip_suffix(')')) - .expect("форма color(display-p3 ...)"); - let (rgb_str, alpha) = match inner.split_once(" / ") { - Some((rgb, a)) => (rgb, Some(a.parse::().expect("альфа — число"))), - None => (inner, None), - }; - let parts: Vec = rgb_str - .split_whitespace() - .map(|p| { - p.parse::() - .unwrap_or_else(|_| panic!("компонента не число: {p} в {css}")) - }) - .collect(); - assert_eq!(parts.len(), 3, "ровно R G B: {css}"); - let lin_p3 = [ - srgb_gamma_inv(parts[0]), - srgb_gamma_inv(parts[1]), - srgb_gamma_inv(parts[2]), - ]; - let xyz = p3_linear_to_xyz(lin_p3); - (hex_from_srgb(xyz_to_srgb(xyz)), alpha) - } - - /// Матрицы — взаимные обратные: P3 → XYZ → P3 тождественно до f64-шума. - #[test] - fn matrices_are_mutual_inverses() { - for rgb in [ - [1.0, 0.0, 0.0], - [0.0, 1.0, 0.0], - [0.0, 0.0, 1.0], - [1.0, 1.0, 1.0], - [0.25, 0.5, 0.75], - ] { - let back = xyz_to_p3_linear(p3_linear_to_xyz(rgb)); - for i in 0..3 { - assert!( - (back[i] - rgb[i]).abs() < 1e-12, - "P3 roundtrip канал {i}: {} vs {}", - back[i], - rgb[i] - ); - } - } - } - - /// Белые точки согласованы: sRGB-белый → P3 (1, 1, 1) до шума двух - /// независимых D65-дериваций (обе цепочки CSS Color 4). #[test] - fn srgb_white_maps_to_p3_white() { - let [r, g, b] = p3_from_hex("#FFFFFF").unwrap(); - for (ch, v) in [("r", r), ("g", g), ("b", b)] { - assert!((v - 1.0).abs() < 1e-6, "белый канал {ch}: {v}"); - } - } - - /// sRGB ⊂ P3: каждая точка решётки куба конвертируется без выхода за - /// гамут (кламп только шумового эпсилона — иначе p3_from_hex вернул бы Err). - /// Шаг 17 взаимно прост с 255 — решётка не выровнена по «удобным» байтам. - #[test] - fn srgb_cube_is_inside_p3_gamut() { - let steps: Vec = (0u16..=255).step_by(17).map(|v| v as u8).collect(); - for &r in &steps { - for &g in &steps { - for &b in &steps { - let hex = format!("#{r:02X}{g:02X}{b:02X}"); - p3_from_hex(&hex).unwrap_or_else(|e| panic!("{hex} вне P3: {e}")); - } - } - } - } - - /// Байт-точность round-trip на решётке 8-битного куба с шагом 5 (включая - /// края 0 и 255): формат → парс → P3 → XYZ → sRGB → те же байты. - #[test] - fn round_trip_is_byte_exact_on_lattice() { - let steps: Vec = (0u16..=255).step_by(5).map(|v| v as u8).collect(); - assert!(steps.contains(&0) && steps.contains(&255)); - for &r in &steps { - for &g in &steps { - for &b in &steps { - let hex = format!("#{r:02X}{g:02X}{b:02X}"); - let css = p3_css_from_hex(&hex, None).unwrap(); - let (back, alpha) = parse_emitted(&css); - assert_eq!(back, hex, "round-trip разошёлся: {css}"); - assert_eq!(alpha, None); - } - } - } - } - - /// Серые с альфой: полный грей-рамп байт-точен, альфа проходит как данные. - #[test] - fn round_trip_is_byte_exact_on_greys_with_alpha() { - for v in 0u16..=255 { - let v = v as u8; - let hex = format!("#{v:02X}{v:02X}{v:02X}"); - let css = p3_css_from_hex(&hex, Some(0.361)).unwrap(); - let (back, alpha) = parse_emitted(&css); - assert_eq!(back, hex, "grey round-trip разошёлся: {css}"); - assert_eq!(alpha, Some(0.361)); - } - } - - /// Политика alpha едина с oklch-эмиссией: любое недоменное значение — - /// ошибка. Даже малый clamp изменил бы публичное число и мог бы выдать - /// соседний конечный композит за сертифицированный. - #[test] - fn alpha_guard_shared_with_oklch() { - assert!(p3_css_from_hex("#101012", Some(f64::NAN)).is_err()); - assert!(p3_css_from_hex("#101012", Some(-10.0)).is_err()); - assert!(p3_css_from_hex("#101012", Some(2.0)).is_err()); - assert!(p3_css_from_hex("#101012", Some(-1e-7)).is_err()); - assert!(p3_css_from_hex("#101012", Some(1.0 + 1e-9)).is_err()); - assert!( - p3_css_from_hex("#101012", Some(0.0)) - .unwrap() - .ends_with(" / 0)") - ); - assert!( - p3_css_from_hex("#101012", Some(1.0)) - .unwrap() - .ends_with(" / 1)") - ); - } - - /// Форма строки — контракт потребителя: `color(display-p3 R G B [/ A])`, - /// компоненты в [0, 1], без знакового нуля. - #[test] - fn css_shape_is_the_contract() { - let solid = p3_css_from_hex("#3E87FF", None).unwrap(); - assert!(solid.starts_with("color(display-p3 ") && solid.ends_with(')')); - assert!(!solid.contains('/')); - assert!(!solid.contains("-0."), "signed zero запрещён: {solid}"); - let translucent = p3_css_from_hex("#101012", Some(0.122)).unwrap(); - assert!(translucent.contains(" / 0.122)")); - // Чистый sRGB-красный внутри P3 — менее насыщен, чем P3-красный: - // r < 1, g/b > 0 (иначе матрицы перепутаны). - let [r, g, b] = p3_from_hex("#FF0000").unwrap(); - assert!(r > 0.9 && r < 1.0, "P3 r красного: {r}"); - assert!(g > 0.0 && b > 0.0, "P3 g/b красного: {g}/{b}"); - } - - /// Этап 1 gamut-aware: стена P3 не уже sRGB-стены НИГДЕ (sRGB ⊂ P3) и - /// СТРОГО шире на насыщенных срединных светлотах (зелёная зона P3 — - /// самое сильное расширение). Сетка L × h покрывает обе ветки бисекции. - #[test] - fn p3_wall_dominates_srgb_wall() { + fn physical_p3_wall_contains_the_srgb_wall() { let mut strictly_wider_somewhere = false; for l10 in 2..=9 { - let l = f64::from(l10) / 10.0; - for h in (0..360).step_by(15) { - let h = f64::from(h); - let srgb = crate::scale::max_chroma_bisect(l, h); - let p3 = crate::scale::max_chroma_p3_bisect(l, h); + let lightness = f64::from(l10) / 10.0; + for hue in (0..360).step_by(15) { + let hue = f64::from(hue); + let srgb = crate::scale::max_chroma_bisect(lightness, hue); + let p3 = crate::scale::max_chroma_p3_bisect(lightness, hue); assert!( p3 >= srgb - 1e-9, - "P3-стена уже sRGB при L={l}, h={h}: {p3} < {srgb}" + "P3 wall is narrower than sRGB at L={lightness}, h={hue}: {p3} < {srgb}" ); - if p3 > srgb * 1.05 { - strictly_wider_somewhere = true; - } + strictly_wider_somewhere |= p3 > srgb * 1.05; } } assert!( strictly_wider_somewhere, - "P3 обязан быть строго шире sRGB хоть где-то (иначе матрицы выродились)" - ); - } - - /// Достижимость за sRGB-стеной: цвет с хромой между стенами (вне sRGB, - /// внутри P3) представим на 8-битной P3-решётке И ПЕРЕЖИВАЕТ квантование — - /// перечитанный с решётки цвет остаётся за sRGB-стеной. Это ровно то, - /// что этап 2 отдаст наружу. - #[test] - fn beyond_srgb_chroma_survives_the_p3_lattice() { - use crate::spaces::oklab::{oklab_to_srgb_linear, srgb_linear_to_oklab}; - // Зелёная срединная зона — максимальный разрыв стен. - let (l, h) = (0.75, 145.0); - let srgb_wall = crate::scale::max_chroma_bisect(l, h); - let p3_wall = crate::scale::max_chroma_p3_bisect(l, h); - assert!( - p3_wall > srgb_wall * 1.1, - "в зелёной зоне разрыв стен обязан быть ощутимым: {p3_wall} vs {srgb_wall}" + "the P3 matrix must produce a genuinely wider gamut somewhere" ); - let c = (srgb_wall + p3_wall) / 2.0; - let h_rad = h.to_radians(); - let lab = [l, c * h_rad.cos(), c * h_rad.sin()]; - let lin_p3 = xyz_to_p3_linear(srgb_to_xyz(oklab_to_srgb_linear(lab))); - let bytes = p3_bytes_from_linear(lin_p3).expect("между стенами — внутри P3"); - // Перечитываем с решётки и меряем хрому честно (на отданном значении). - let back = p3_linear_to_xyz(p3_linear_from_bytes(bytes)); - let back_lab = srgb_linear_to_oklab(crate::spaces::srgb::xyz_to_srgb(back)); - let back_c = (back_lab[1] * back_lab[1] + back_lab[2] * back_lab[2]).sqrt(); - assert!( - back_c > srgb_wall, - "квантование не должно ронять хрому обратно в sRGB: {back_c} <= {srgb_wall}" - ); - } - - /// Байт-точный round-trip решётки: байты → css-строка → парс компонент → - /// байты. Шаг 7 взаимно прост с 255 — решётка пробегает все классы вычетов. - #[test] - fn p3_lattice_css_round_trip_is_byte_exact() { - for r in (0..=255).step_by(7) { - for g in (0..=255).step_by(51) { - for b in (0..=255).step_by(51) { - let bytes = [r as u8, g as u8, b as u8]; - let css = p3_css_from_bytes(bytes, None).expect("байты валидны"); - let inner = css - .strip_prefix("color(display-p3 ") - .and_then(|s| s.strip_suffix(')')) - .expect("форма color(display-p3 ...)"); - let parts: Vec = inner - .split_whitespace() - .map(|p| p.parse::().expect("компонента — число")) - .collect(); - let parsed = [ - (parts[0] * 255.0).round() as u8, - (parts[1] * 255.0).round() as u8, - (parts[2] * 255.0).round() as u8, - ]; - assert_eq!(parsed, bytes, "byte round-trip сломан: {css}"); - } - } - } - } - - /// Гард решётки: не-цвет (за гамутом P3) не квантуется молча. - #[test] - fn out_of_gamut_linear_is_an_error_not_a_clamp() { - assert!(p3_bytes_from_linear([1.2, 0.5, 0.5]).is_err()); - assert!(p3_bytes_from_linear([-0.2, 0.5, 0.5]).is_err()); } } diff --git a/crates/labcolors-core/tests/property_invariants.rs b/crates/labcolors-core/tests/property_invariants.rs index 9a7b13e7..8b19c7f8 100644 --- a/crates/labcolors-core/tests/property_invariants.rs +++ b/crates/labcolors-core/tests/property_invariants.rs @@ -23,7 +23,7 @@ use labcolors_core::{ BgInput, Brand, Floor, GlowDecisionProfileV1, LadderPosition, LadderSource, NeutralAnchors, NeutralConfig, NeutralPick, NeutralTint, PaletteFamily, Resolved, RoleFailure, RoleRecipe, ThemeAnchors, ThemeConfig, ThemesConfig, VcPreset, ViewingConditions, oklch_from_hex, - p3_from_hex, resolve_named_set, srgb_encoded_from_hex, + resolve_named_set, srgb_encoded_from_hex, }; use proptest::prelude::*; use proptest::test_runner::{Config, RngAlgorithm, TestRng, TestRunner}; @@ -593,7 +593,7 @@ fn hued_brand_label_stays_in_family_coordinate_band_when_chromatic() { // СВОЙСТВО 6 — fuzz-устойчивость парсеров цвета (ноль паник на мусоре) // // КЛАСС: «парсер hex падает/паникует на враждебном/битом входе». Любая строка на -// входе `oklch_from_hex` / `srgb_encoded_from_hex` / `p3_from_hex` / `BgInput::solid` +// входе `oklch_from_hex` / `srgb_encoded_from_hex` / `BgInput::solid` // → аккуратный `Result` (Ok или Err), НИКОГДА не паника (паника развернула бы стек // и уронила бы тест). Класс-закрыватель для парсеров, читающих внешний ввод. // БЬЁТ НА МУТАЦИИ: замена валидации на `unwrap`/индексацию без границ → паника → RED. @@ -615,10 +615,9 @@ fn hex_like() -> impl Strategy { #[test] fn color_parsers_never_panic_on_arbitrary_input() { check(2000, hex_like(), |s| { - // Все четыре парсера обязаны вернуть Result, не паниковать. + // Все три парсера обязаны вернуть Result, не паниковать. let _ = oklch_from_hex(&s); let _ = srgb_encoded_from_hex(&s); - let _ = p3_from_hex(&s); let _ = BgInput::solid(&s); Ok(()) }); diff --git a/crates/labcolors-core/tests/solve_characterization.rs b/crates/labcolors-core/tests/solve_characterization.rs index ba2a2f34..ef7e8d30 100644 --- a/crates/labcolors-core/tests/solve_characterization.rs +++ b/crates/labcolors-core/tests/solve_characterization.rs @@ -262,7 +262,6 @@ fn outcome_line(result: &Result) -> String { bits(*floor), bits(*max_ratio) ), - Err(SolveFailure::GamutUnsupported) => "err gamut_unsupported".to_string(), Err(SolveFailure::InvalidInput(message)) => { format!("err invalid_input message={message:?}") } @@ -287,20 +286,6 @@ fn run_case(case: &CaseSpec) -> Result { fn observed_map() -> BTreeMap { let mut observed = BTreeMap::new(); - // Одна GamutUnsupported-строка поверх матрицы (у solve это внешний гейт). - let gamut_case = solve( - BgInput::solid("#FFFFFF").expect("literal background"), - Contract::text(60.0), - Hue::deg(0.0), - ChromaPolicy::Neutral, - &ViewingConditions::srgb(), - Gamut::DisplayP3, - ); - observed.insert( - "bg=#FFFFFF contract=text(60) floor=default hue=0 chroma=neutral gamut=display-p3" - .to_string(), - outcome_line(&gamut_case), - ); for case in matrix() { let previous = observed.insert(case_key(&case), outcome_line(&run_case(&case))); assert!( @@ -410,7 +395,6 @@ fn characterization_counters_are_non_vacuous() { "exceeds_range" => "exceeds_range", "bounded_search_exhausted" => "bounded_search_exhausted", "floor_unreachable" => "floor_unreachable", - "gamut_unsupported" => "gamut_unsupported", "invalid_input" => "invalid_input", "internal_invariant" => "internal_invariant", other => panic!("unknown error class {other}"), @@ -426,7 +410,6 @@ fn characterization_counters_are_non_vacuous() { "below_contrast_floor", "exceeds_range", "floor_unreachable", - "gamut_unsupported", "invalid_input", ] { assert!( @@ -441,10 +424,9 @@ fn characterization_counters_are_non_vacuous() { // квантования; walk в 2 distinct-шага пересекает всё остальное (фикс #44). // Эмпирически: сканы публичного API на миллионы вызовов (solid-фоны обеих // полярностей, серые и хроматические, hue-сетка, Neutral/Relative вплоть до - // 1.0, Floor::None/AaText/AaUi, |Lc| 7.3..112, srgb и dim surround; wide - // gamut не участвует — DisplayP3 умирает на внешнем гейте) не производят - // ни одного. Правда самого варианта (`closest_examined` локален, не глобален) - // запинена на его собственном шве: + // 1.0, Floor::None/AaText/AaUi, |Lc| 7.3..112, srgb и dim surround) не + // производят ни одного. Правда самого варианта (`closest_examined` локален, + // не глобален) запинена на его собственном шве: // `solve::tests::bounded_search_exhausted_is_local_not_global_counterexample`. // Появление исхода из этой матрицы = изменение поведения поиска, не «новый кейс». assert_eq!( @@ -464,7 +446,7 @@ fn characterization_counters_are_non_vacuous() { /// `solve_many(bg, jobs) == jobs.map(solve)` позиционно: успехи, каждый класс /// per-job ошибки, пустой вход, дубликаты и смешанные валидные/невалидные -/// задания; внешняя gamut-ошибка остаётся внешней и не сдвигает позиции. +/// задания. #[test] fn solve_many_is_positionally_identical_to_sequential_solve() { let vc = ViewingConditions::srgb(); @@ -600,13 +582,6 @@ fn solve_many_is_positionally_identical_to_sequential_solve() { .expect("empty batch") .is_empty() ); - - // Внешняя ошибка гамута — внешняя: Err всей партии, позиций нет. - let bg = BgInput::solid("#FFFFFF").expect("literal background"); - assert!(matches!( - solve_many(bg, &jobs, &vc, Gamut::DisplayP3), - Err(SolveFailure::GamutUnsupported) - )); } /// Позитивная характеризация JND-полосы против `recheck_against` — публичного @@ -701,7 +676,7 @@ fn jnd_band_resolves_within_budget_with_tolerant_acceptance() { /// Linux-x64 расходится РОВНО хвост ulp одного поля — Oklab-hue коррелята /// `h_ok` — в двух хроматических кейсах матрицы. Всё остальное (hex-байты, `lc`, /// `wcag_ratio`, `floor_override`, `jp`, `s`, все payload'ы ошибок) — -/// бит-идентично на всех 77 кейсах. Оба кейса — libm-разница +/// бит-идентично на всех 76 кейсах. Оба кейса — libm-разница /// (atan2/cbrt, 5 ulp на хроматике). У точного sRGB-серого `h_ok = 0` по /// определению, поэтому его прежний atan2-шум больше не является частью /// платформенного контракта. Рост этого множества — изменение численного @@ -728,7 +703,7 @@ fn platform_fixtures_agree_except_documented_hue_ulp_drift() { }; let mac = load(FIXTURE_MACOS_AARCH64); let linux = load(FIXTURE_LINUX_X64); - assert_eq!(mac.len(), 77, "macOS fixture cardinality"); + assert_eq!(mac.len(), 76, "macOS fixture cardinality"); assert_eq!( mac.keys().collect::>(), linux.keys().collect::>(), diff --git a/crates/labcolors-ffi/src/lib.rs b/crates/labcolors-ffi/src/lib.rs index 76a2c33d..ae5136f1 100644 --- a/crates/labcolors-ffi/src/lib.rs +++ b/crates/labcolors-ffi/src/lib.rs @@ -352,8 +352,6 @@ pub enum FailureCategory { Unresolved, /// Запрос не принадлежит объявленному domain. Rejected, - /// Запрос валиден, но capability не реализована. - Unsupported, } impl FailureCategory { @@ -362,7 +360,6 @@ impl FailureCategory { labcolors_core::SolveFailureCategory::Unreachable => Self::Unreachable, labcolors_core::SolveFailureCategory::Unresolved => Self::Unresolved, labcolors_core::SolveFailureCategory::Rejected => Self::Rejected, - labcolors_core::SolveFailureCategory::Unsupported => Self::Unsupported, } } } @@ -389,7 +386,7 @@ pub enum ColorError { key: String, }, /// Resolver не вернул цвет. Категория отделяет доказанную недостижимость - /// от unresolved, rejected и unsupported исходов. + /// от unresolved и rejected исходов. #[error("solve failure {category:?}/{code}")] Failure { /// Стабильная семантическая категория core failure. @@ -997,11 +994,6 @@ mod tests { FailureCategory::Unreachable, "floor_unreachable", ), - ( - U::GamutUnsupported, - FailureCategory::Unsupported, - "gamut_unsupported", - ), ( U::InvalidInput("fixture".into()), FailureCategory::Rejected, diff --git a/crates/labcolors-wasm/src/lib.rs b/crates/labcolors-wasm/src/lib.rs index 10722928..38b0a313 100644 --- a/crates/labcolors-wasm/src/lib.rs +++ b/crates/labcolors-wasm/src/lib.rs @@ -657,7 +657,7 @@ impl LabColors { /// /// Возвращает полный `ResolvedTheme`. Локальный `unresolved` остаётся /// типизированным исходом роли; ordinary `unreachable` отклоняет весь вызов - /// как `OutputConflictError`. Rejected/unsupported/internal также + /// как `OutputConflictError`. Rejected/internal также /// отклоняются атомарно: частичной темы или CSS не бывает. /// Ошибки границы — структурная форма `": "`, Rust-паника /// в JavaScript не разматывается. diff --git a/packages/colors/bench/wasm.json b/packages/colors/bench/wasm.json index 212f1cba..528207e5 100644 --- a/packages/colors/bench/wasm.json +++ b/packages/colors/bench/wasm.json @@ -19,13 +19,13 @@ "command": "CARGO_ENCODED_RUSTFLAGS= wasm-pack build crates/labcolors-wasm --release --target web --out-dir ../../packages/colors/pkg --out-name labcolors --locked" }, "measurement": { - "source": "github-actions-run-29955098154", + "source": "github-actions-run-29962821215", "platform": "linux-x64", - "rawBytes": 376977 + "rawBytes": 376707 }, "policy": { - "maxRawBytes": 376977, - "basis": "pair-taxonomy-hard-delete", + "maxRawBytes": 376707, + "basis": "p3-promise-hard-delete", "gzip": "diagnostic-only" } } diff --git a/packages/colors/smoke.consumer.ts b/packages/colors/smoke.consumer.ts index 64f6c003..59e591f7 100644 --- a/packages/colors/smoke.consumer.ts +++ b/packages/colors/smoke.consumer.ts @@ -66,15 +66,6 @@ requireFailure({ code: "invalid_input", message: "x", }); -requireFailure({ - kind: "failure", - cssVar: "--lab-example", - // @ts-expect-error unsupported closes the whole resolve and cannot be role data. - category: "unsupported", - code: "gamut_unsupported", - message: "x", -}); - const admittedFailureCategory: FailureCategory = "unresolved"; void admittedFailureCategory; diff --git a/packages/colors/test/release-contract.test.mjs b/packages/colors/test/release-contract.test.mjs index b2ff182b..b8c22103 100644 --- a/packages/colors/test/release-contract.test.mjs +++ b/packages/colors/test/release-contract.test.mjs @@ -1012,7 +1012,6 @@ test("publish artifact validator executes and rejects identity or byte drift", ( "stable-cam16-glow-target-or-maximum-selection", "renderer-or-output-pipeline-equivalence", "spatial-glow-field", - "display-p3", ], artifacts: { tarball: { @@ -1228,10 +1227,9 @@ test("release checker rejects solve failure wire drift", () => { ["unreachable", "exceeds_range"], ["unresolved", "bounded_search_exhausted"], ["unreachable", "floor_unreachable"], - ["unsupported", "gamut_unsupported"], ["rejected", "invalid_input"], ]; - assert.equal(boundaryRows.length, 6, "public core failure dictionary changed"); + assert.equal(boundaryRows.length, 5, "public core failure dictionary changed"); for (const [category, code] of boundaryRows) { assert.doesNotThrow(() => validateSolveFailurePair(category, code)); const wrongCategory = category === "unreachable" ? "rejected" : "unreachable"; diff --git a/scripts/check-wasm-size-budget.mjs b/scripts/check-wasm-size-budget.mjs index 33b83e7c..a24ddff4 100644 --- a/scripts/check-wasm-size-budget.mjs +++ b/scripts/check-wasm-size-budget.mjs @@ -14,7 +14,7 @@ export const DEFAULT_BUDGET = resolve( "packages/colors/bench/wasm.json", ); export const WASM_BUDGET_FILE_SHA256 = - "224d92604e5ee861da0c09e219da91f0807c4d5cb3c8733f23dff72066ecdb08"; + "035cece04afa7ea37e819c2432c2238b1e902399c31db01c39fbd67e1e299018"; const SCHEMA_VERSION = 1; const CANONICAL_ARTIFACT = "packages/colors/pkg/labcolors_bg.wasm"; diff --git a/scripts/verify-package-release.mjs b/scripts/verify-package-release.mjs index 5b64dc1b..3824ae6f 100644 --- a/scripts/verify-package-release.mjs +++ b/scripts/verify-package-release.mjs @@ -700,7 +700,6 @@ const SOLVE_FAILURE_CATEGORY_BY_CODE = new Map([ ["exceeds_range", "unreachable"], ["bounded_search_exhausted", "unresolved"], ["floor_unreachable", "unreachable"], - ["gamut_unsupported", "unsupported"], ["invalid_input", "rejected"], ]); @@ -1722,7 +1721,6 @@ export async function verifyPackageRelease() { "stable-cam16-glow-target-or-maximum-selection", "renderer-or-output-pipeline-equivalence", "spatial-glow-field", - "display-p3", ], artifacts: { tarball, diff --git a/scripts/verify_point_support_surplus.py b/scripts/verify_point_support_surplus.py index 69816a19..feb34d3f 100644 --- 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 = ( - "6b81edc2d56bdca78a4d7c579c79e2791dc59e9bf2626fd22ddc7aa164dfa5c8" + "2e63424882231ec2f00ae79911bf74cfb935bb96f11368bce53240468f7f2dd4" ) EXPECTED_Q55_PROOF_SHA256 = ( "ac59cf89503170c789223b91d775213a19d4e571ef930f2ea609fcd51b14defd"