From 6a9771b002d417c6532c3e6eb91e9cf2d7650868 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Mon, 3 Aug 2026 09:13:21 +0300 Subject: [PATCH 1/7] =?UTF-8?q?=D0=9E=D0=B1=D0=BE=D0=B1=D1=89=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20exact=20point-=D0=BF=D1=80=D0=B5=D0=B4=D1=81=D1=82?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/labcolors-core/src/alpha.rs | 379 ++++-------------- crates/labcolors-core/src/appearance.rs | 2 +- .../src/appearance_graph_tests.rs | 2 +- crates/labcolors-core/src/config.rs | 6 +- .../src/generic_boundary_tests.rs | 50 ++- crates/labcolors-core/src/lib.rs | 2 +- .../{analog.rs => point_representation.rs} | 309 +++++--------- crates/labcolors-core/src/semantic.rs | 124 +++--- docs/whitepaper.md | 10 +- 9 files changed, 297 insertions(+), 587 deletions(-) rename crates/labcolors-core/src/{analog.rs => point_representation.rs} (52%) diff --git a/crates/labcolors-core/src/alpha.rs b/crates/labcolors-core/src/alpha.rs index c11c2d03..99e49c05 100644 --- a/crates/labcolors-core/src/alpha.rs +++ b/crates/labcolors-core/src/alpha.rs @@ -1,63 +1,21 @@ -//! Альфа-аналог солидного цвета: прямой и обратный ход straight-alpha -//! композита в ГАММА-КОДИРОВАННОМ sRGB. +//! Прямой и обратный ход straight-alpha source-over в кодированном sRGB. //! -//! Кодированный домен заземлён измерением: модель `c = α·t + (1−α)·b` -//! воспроизводит все 12 семантических Figma-якорей движка -//! (`reference/labui-figma-structure.md` §3–§4, воспроизводимо -//! `cargo run -p labcolors-core --example figma_anchor_provenance`). Для -//! эмиссии контракт дополнительно фиксирует **encoded-sRGB8 source-over -//! reference**: арифметика идёт на байтах `0..255` с одним итоговым round. -//! Совпадение конкретного renderer зависит от его color-management профиля и -//! проверяется отдельно; линейный свет здесь только для последующей колориметрии. +//! Модуль предоставляет только физические операции композиции, непрерывной +//! инверсии и strict-binary64 границы разрешимости. Он не знает token, recipe, +//! client ID или назначения результата. //! -//! # Зачем обратный ход +//! Для непрерывных каналов используется +//! `c = b + α·(s−b)`, обратный ход — `s = b + (c−b)/α`. Входы должны быть +//! конечными и лежать в своих закрытых диапазонах; epsilon и silent clamp не +//! применяются. Ошибка round-trip ограничена тестируемой оценкой `8·ε/α`. //! -//! Движок решает роли СОЛИДАМИ (контраст-корректными на данном фоне). Альфа- -//! аналог роли — пара `(tint, α)`, чей композит на том же фоне равен солиду: +//! Это не сертификат эмитируемого sRGB8: финальное округление расширяет +//! достижимое множество. Exact byte-grid proposal, единичная materialization +//! occurrence и postcondition принадлежат приватному `point_representation` и +//! используют тот же `composition` source-over kernel. //! -//! ```text -//! t = (c − (1−α)·b) / α (по каналам, кодированные значения) -//! ``` -//! -//! В вещественной алгебре continuous encoded-sRGB композит инверсии равен -//! солиду тождественно. Реализация на `binary64` честно отделена: ошибка -//! round-trip ограничена выведенной оценкой `8·ε/α` и проверяется тестом -//! `inversion_identity_respects_derived_binary64_error_bound`. После эмиссии тинт уже -//! лежит на sRGB8-сетке; побайтовый контракт ниже доказывается и проверяется -//! отдельно, а не приписывается непрерывной функции. -//! На ином фоне композит другой — это и есть смысл альфы (адаптация к -//! подложке), гарантия формулируется для фона, на котором решён солид. -//! -//! # Разрешимость и границы квантования -//! -//! Тинт обязан лежать в гамуте `[0,1]³`. Поканальная алгебра нижней границы α: -//! -//! ```text -//! t ≥ 0 ⇔ α ≥ (b − c) / b (канал с c < b; при b = 0 недостижимо, если c > 0 — но тогда c > b) -//! t ≤ 1 ⇔ α ≥ (c − b) / (1 − b) (канал с c > b; при b = 1 симметрично) -//! ``` -//! -//! [`min_alpha_encoded`] начинает с максимума этих алгебраических границ, затем -//! возвращает первый `binary64`, на котором строгая binary64-инверсия реально -//! лежит в `[0,1]³`. [`invert_composite_encoded`] может также принять более -//! раннюю граничную пару только если повторный binary64-композит побитно равен -//! входному solid; глобального epsilon и безусловного clamp нет. Это не минимальная α -//! дискретного sRGB8-композита: `#010000` над чёрным уже округляется из белого -//! красного канала при `α=0.5/255`, тогда как strict-binary64 пол равен `1/255`. -//! -//! Эмиссионный путь НЕ использует continuous-пол как суррогат byte-grid пола. -//! После квантования solid/background он сначала исчерпывающе решает три -//! независимых одноканальных диапазона тинта на запрошенной alpha. Если решения -//! нет, lower-bound по упорядоченным битам `f64` находит первый `binary64`, -//! проходящий ТОТ ЖЕ [`composite_over_srgb8`]; predecessor обязан не проходить. -//! Поэтому округлительно разрешимые пары вроде `#FF0000 @ 0.12 → #1F0000` -//! сохраняют запрошенную прозрачность, а `alphaCoerced` не врёт о деградации. -//! Весь одноканальный домен из 65 536 `(S, B)` проверяет и точный композит, и -//! минимальность фактической alpha. -//! -//! Это не обещает восстановить исходный тинт: солид из 8-битного hex несёт -//! ошибку ≤ 0.5/255, которую инверсия масштабирует в `1/α` раз. Граница -//! `0.5/(255·α)` запинена тестом `quantisation_error_bound_is_honoured`. +//! Совпадение с конкретным renderer зависит от его color-management и не +//! следует из reference-арифметики библиотеки. use crate::spaces::srgb::{hex_from_srgb_encoded, srgb_encoded_from_hex}; @@ -372,120 +330,31 @@ pub fn min_alpha_hex(solid_hex: &str, bg_hex: &str) -> Result { .expect("hex-вход всегда в домене byte/255 — None недостижим по построению")) } -/// Непрерывный альфа-аналог солида: тинт + ФАКТИЧЕСКАЯ α. -/// -/// Продуктовый слой поверх строгого закона: потребитель всегда получает -/// пригодный ответ без подмены цели клампом. Вещественная формула сохраняет -/// заданный цвет, а ошибка её binary64-вычисления ограничена доказанной выше -/// оценкой. Побайтовая эмиссия в hex/semantic использует отдельный проверяющий -/// sRGB8-путь с точным постусловием по байтам. -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct AlphaAnalog { - /// Кодированный тинт `[0,1]³`. - pub tint: [f64; 3], - /// Фактическая α: запрошенная, если она разрешима, иначе strict-binary64 - /// пол (не вещественный инфимум и не минимальная byte-grid α). - pub alpha: f64, -} - -/// Продуктовый резолвер: ближайший ПРИЕМЛЕМЫЙ альфа-аналог вместо отказа. -/// -/// «Приблизить» можно двумя способами, и только один честен: кламп тинта при -/// запрошенной α тихо сдвинул бы композит (система соврала бы о цвете — -/// запрещённая подмена), а подъём α до strict-binary64 пола -/// [`min_alpha_encoded`] сохраняет исходную цель вещественной инверсии; в -/// binary64 остаётся только ограниченная ошибка округления, а не произвольный -/// цветовой сдвиг. Двигается прозрачность, и -/// фактическая α возвращается явно ([`AlphaAnalog::alpha`]). Запрошенная α вне -/// `[0,1]` отвергается: clamp скрыл бы ошибку вызывающего кода. `α=0` входит в -/// библиотечный домен и поднимается до strict-binary64 пола, если solid -/// отличается от фона; при `solid == bg` возвращается вырожденная пара -/// `tint=bg, α=0`. -/// -/// `None` — только на входе вне домена (цвет не конечен/не в `[0,1]³` либо -/// запрошенная α не конечна/не в `[0,1]`). Для валидного входа ответ существует -/// всегда (в худшем случае α=1, тинт=солид). -pub fn resolve_alpha_analog( - solid: [f64; 3], - requested_alpha: f64, - bg: [f64; 3], -) -> Option { - if !requested_alpha.is_finite() || !(0.0..=1.0).contains(&requested_alpha) { - return None; - } - let floor = min_alpha_encoded(solid, bg)?; // None только на мусор-входах - // Если запрошенная binary64-пара уже является точной обратной к нашему - // прямому ходу (включая честную gamut-границу), не поднимаем прозрачность. - if requested_alpha > 0.0 { - if let Some(tint) = invert_composite_encoded(solid, requested_alpha, bg) { - return Some(AlphaAnalog { - tint, - alpha: requested_alpha, - }); - } - } - let alpha = requested_alpha.max(floor); - // При α == floor == 0 солид равен фону: любой видимый эффект отсутствует, - // тинт = фон (инверсия при α=0 вырожденна — отвечаем без неё). - if alpha == 0.0 { - return Some(AlphaAnalog { tint: bg, alpha }); - } - let tint = invert_composite_encoded(solid, alpha, bg) - .expect("α ≥ α_min по построению — инверсия разрешима"); - Some(AlphaAnalog { tint, alpha }) -} - -/// Hex-обёртка эмиссионного sRGB8-резолвера: `(tint_hex, фактическая α)`. -/// Возвращённая пара побайтно воспроизводит -/// `solid_hex` через [`composite_over_srgb8`]; постусловие проверено до возврата. -/// -/// # Errors -/// -/// `Err` при невалидном hex, неконечной/внедиапазонной запрошенной α либо при -/// нарушении точного sRGB8-постусловия (защитная ветка против численного дрейфа). -pub fn resolve_alpha_analog_hex( - solid_hex: &str, - requested_alpha: f64, - bg_hex: &str, -) -> Result<(String, f64), String> { - let target = crate::Srgb8::new(crate::srgb8::hex_bytes(solid_hex)?); - let backdrop = crate::Srgb8::new(crate::srgb8::hex_bytes(bg_hex)?); - let verified = crate::analog::resolve_verified( - crate::analog::AuthoredAlphaBindingIdV1::Standalone, - target, - requested_alpha, - backdrop, - ) - .map_err(resolve_verified_error_message)?; - Ok((verified.tint().to_hex(), verified.alpha())) -} - -fn resolve_verified_error_message(error: crate::analog::ResolveVerifiedErrorV1) -> String { - match error { - crate::analog::ResolveVerifiedErrorV1::Proposal(error) => match error { - crate::analog::AlphaAnalogProposalErrorV1::InvalidRequestedAlpha { bits } => { - let requested_alpha = f64::from_bits(bits); - format!("requested_alpha вне конечного [0,1]: {requested_alpha}") - } - crate::analog::AlphaAnalogProposalErrorV1::DerivedAlphaOutsideUnitInterval => { - "выведенная alpha вышла из конечного [0,1]".to_owned() - } - crate::analog::AlphaAnalogProposalErrorV1::MissingTintAtFirstAlpha => { - "первая sRGB8-alpha не дала допустимый byte-тинт".to_owned() - } - }, - crate::analog::ResolveVerifiedErrorV1::ConstraintViolation(witness) => format!( - "alpha-analog не воспроизвёл sRGB8-цель: target={:?}, actual={:?}", - witness.violation().target().bytes(), - witness.violation().actual().bytes() - ), - } -} - #[cfg(test)] mod tests { use super::*; + fn admitted(alpha: f64) -> crate::composition::AdmittedOpacityV1 { + crate::composition::AdmittedOpacityV1::new(alpha) + .expect("test opacity must be finite and inside [0,1]") + } + + fn resolve_exact_hex( + target_hex: &str, + minimum_opacity: f64, + backdrop_hex: &str, + ) -> Result<(String, f64), String> { + let target = crate::Srgb8::new(crate::srgb8::hex_bytes(target_hex)?); + let backdrop = crate::Srgb8::new(crate::srgb8::hex_bytes(backdrop_hex)?); + let opacity = crate::composition::AdmittedOpacityV1::new(minimum_opacity) + .map_err(|_| format!("opacity outside finite [0,1]: {minimum_opacity}"))?; + let verified = crate::point_representation::resolve_exact_point_representation_v1( + target, opacity, backdrop, + ) + .map_err(|error| format!("exact point representation failed: {error:?}"))?; + Ok((verified.source().to_hex(), verified.opacity().value())) + } + /// Живые Figma-пары нейтральной лестницы (`reference/labui-figma-structure.md` /// §2 — альфы и тинт, §4 — композиты; фоны Backgrounds/Neutral/Primary): /// (композит, α, фон). Тинт всех 12 пар — один: `#787880`. @@ -537,7 +406,7 @@ mod tests { #[test] fn source_over_half_seam_matches_the_official_js_operation_order() { assert_eq!(composite_hex("#000505", 0.1, "#050505").unwrap(), "#050505"); - let (tint, actual) = resolve_alpha_analog_hex("#040505", 0.1, "#050505").unwrap(); + let (tint, actual) = resolve_exact_hex("#040505", 0.1, "#050505").unwrap(); assert!( actual > 0.1, "запрошенная пара не воспроизводит красный байт 4" @@ -592,22 +461,23 @@ mod tests { for bg in u8::MIN..=u8::MAX { let target = crate::Srgb8::new([solid; 3]); let backdrop = crate::Srgb8::new([bg; 3]); - let requested_is_feasible = - crate::analog::tint_at_alpha(target, requested_alpha, backdrop).is_some(); - let verified = crate::analog::resolve_verified( - crate::analog::AuthoredAlphaBindingIdV1::Standalone, + let requested_is_feasible = crate::point_representation::source_at_opacity( target, requested_alpha, backdrop, ) - .unwrap_or_else(|error| { - panic!( - "solid={solid}, bg={bg}, requested={requested_alpha}: {}", - resolve_verified_error_message(error) + .is_some(); + let verified = + crate::point_representation::resolve_exact_point_representation_v1( + target, + admitted(requested_alpha), + backdrop, ) - }); - let tint = verified.tint().bytes(); - let actual_alpha = verified.alpha(); + .unwrap_or_else(|error| { + panic!("solid={solid}, bg={bg}, requested={requested_alpha}: {error:?}") + }); + let tint = verified.source().bytes(); + let actual_alpha = verified.opacity().value(); let got = composite_over_srgb8(tint, actual_alpha, [bg; 3]) .expect("резолвер возвращает α в [0,1]"); assert_eq!( @@ -624,7 +494,12 @@ mod tests { assert!(actual_alpha > requested_alpha); let predecessor = f64::from_bits(actual_alpha.to_bits() - 1); assert!( - crate::analog::tint_at_alpha(target, predecessor, backdrop).is_none(), + crate::point_representation::source_at_opacity( + target, + predecessor, + backdrop, + ) + .is_none(), "actual alpha не минимальна: solid={solid}, bg={bg}, actual={actual_alpha}" ); } @@ -639,7 +514,7 @@ mod tests { fn byte_grid_resolver_preserves_every_already_feasible_requested_alpha() { let tiny_red_alpha = 0.5 / 255.0; let (tiny_tint, tiny_actual) = - resolve_alpha_analog_hex("#010000", tiny_red_alpha, "#000000").unwrap(); + resolve_exact_hex("#010000", tiny_red_alpha, "#000000").unwrap(); assert_eq!(tiny_tint, "#FF0000"); assert_eq!(tiny_actual.to_bits(), tiny_red_alpha.to_bits()); assert_eq!( @@ -647,7 +522,7 @@ mod tests { "#010000" ); - let (tint, actual) = resolve_alpha_analog_hex("#1F0000", 0.12, "#000000").unwrap(); + let (tint, actual) = resolve_exact_hex("#1F0000", 0.12, "#000000").unwrap(); assert_eq!(tint, "#FF0000"); assert_eq!(actual.to_bits(), 0.12_f64.to_bits()); assert_eq!(composite_hex(&tint, actual, "#000000").unwrap(), "#1F0000"); @@ -659,12 +534,12 @@ mod tests { #[test] fn byte_grid_resolver_handles_tiny_normal_and_subnormal_alpha() { for requested in [2_f64.powi(-100), f64::from_bits(1)] { - let (tint, actual) = resolve_alpha_analog_hex("#010000", requested, "#000000").unwrap(); + let (tint, actual) = resolve_exact_hex("#010000", requested, "#000000").unwrap(); assert!(actual > requested); assert_eq!(composite_hex(&tint, actual, "#000000").unwrap(), "#010000"); let predecessor = f64::from_bits(actual.to_bits() - 1); assert!( - crate::analog::tint_at_alpha( + crate::point_representation::source_at_opacity( crate::Srgb8::new([1, 0, 0]), predecessor, crate::Srgb8::new([0; 3]), @@ -684,9 +559,10 @@ mod tests { let background = [bg; 3]; let target = crate::Srgb8::new(target); let background = crate::Srgb8::new(background); - let floor = crate::analog::first_alpha(target, background); + let floor = crate::point_representation::first_opacity(target, background); assert!( - crate::analog::tint_at_alpha(target, floor, background).is_some(), + crate::point_representation::source_at_opacity(target, floor, background) + .is_some(), "solid={solid}, bg={bg}, floor={floor} не проходит" ); if solid == bg { @@ -694,7 +570,12 @@ mod tests { } else { let predecessor = f64::from_bits(floor.to_bits() - 1); assert!( - crate::analog::tint_at_alpha(target, predecessor, background).is_none(), + crate::point_representation::source_at_opacity( + target, + predecessor, + background, + ) + .is_none(), "solid={solid}, bg={bg}: predecessor={predecessor} тоже проходит" ); } @@ -711,15 +592,14 @@ mod tests { let off_grid_solid = [0.25 / 255.0; 3]; let target = crate::Srgb8::new(encoded_to_srgb8(off_grid_solid, "solid").unwrap()); let backdrop = crate::Srgb8::new([0; 3]); - let verified = crate::analog::resolve_verified( - crate::analog::AuthoredAlphaBindingIdV1::Standalone, + let verified = crate::point_representation::resolve_exact_point_representation_v1( target, - 0.5, + admitted(0.5), backdrop, ) .expect("валидный домен всегда имеет конечный sRGB8-ответ"); - let tint = verified.tint().bytes(); - let alpha = verified.alpha(); + let tint = verified.source().bytes(); + let alpha = verified.opacity().value(); assert_eq!(tint, [0; 3]); assert_eq!(composite_over_srgb8(tint, alpha, [0; 3]).unwrap(), [0; 3]); } @@ -858,49 +738,38 @@ mod tests { assert!(invert_composite_encoded([0.0; 3], alpha, [1.0; 3]).is_none()); } - /// Непрерывный резолвер не подменяет целевой цвет: при разрешимой - /// запрошенной α возвращает её саму; при неразрешимой — поднимает α ровно - /// до α_min. Ошибка binary64-композита остаётся внутри доказанной оценки - /// (двигается прозрачность, не целевой цвет). Побайтовый постусловный тест живёт отдельно в - /// `srgb8_alpha_analog_is_exact_for_every_channel_pair`. + /// Непрерывная инверсия на своём strict-binary64 полу не подменяет целевой + /// цвет. Побайтовый postcondition проверяется отдельно общим exact point- + /// coordinator-ом. #[test] - fn resolver_moves_alpha_never_the_colour() { + fn strict_floor_and_inversion_preserve_the_continuous_target() { let grid: Vec = (0..=8).map(|i| f64::from(i) / 8.0).collect(); for &s in &grid { for &b in &grid { let solid = [s, 0.4, 0.6]; let bg = [b, 0.4, 0.6]; let floor = min_alpha_encoded(solid, bg).expect("в домене"); - for requested in [0.0, 0.05, 0.3, 0.9, 1.0] { - let requested_is_exact = - requested > 0.0 && invert_composite_encoded(solid, requested, bg).is_some(); - let a = resolve_alpha_analog(solid, requested, bg).expect("в домене"); - // Фактическая α: запрошенная, если строгая или побитно-точная - // граничная инверсия разрешима; иначе ровно conservative floor. - let want = if requested_is_exact { - requested + for minimum in [0.0_f64, 0.05, 0.3, 0.9, 1.0] { + let opacity = minimum.max(floor); + let source = if opacity == 0.0 { + bg } else { - requested.max(floor) + invert_composite_encoded(solid, opacity, bg) + .expect("opacity at or above the exact floor must invert") }; - assert_eq!( - a.alpha.to_bits(), - want.to_bits(), - "solid={s},bg={b},req={requested}: α={} != {want}", - a.alpha - ); // Отклонение — только ограниченная ошибка binary64, а не // результат клампа или смены целевого цвета. - let c = if a.alpha == 0.0 { - bg // вырожденный случай solid==bg + let c = if opacity == 0.0 { + bg } else { - composite_over_encoded_unchecked(a.tint, a.alpha, bg) + composite_over_encoded_unchecked(source, opacity, bg) }; for ch in 0..3 { let error = (c[ch] - solid[ch]).abs(); let bound = 8.0 * f64::EPSILON; assert!( error <= bound, - "solid={s},bg={b},req={requested}: канал {ch}, error={error} > {bound}" + "solid={s},bg={b},minimum={minimum}: канал {ch}, error={error} > {bound}" ); } } @@ -936,9 +805,8 @@ mod tests { } } - /// Все три hex-обёртки публичной поверхности: roundtrip на живой Figma-паре, - /// плюс честный Err (не паника) на невалидном hex — .expect в min_alpha_hex - /// недостижим, парсинг падает раньше через `?`. + /// Публичные физические hex-обёртки: roundtrip на живой Figma-паре и + /// честный Err (не паника) на невалидном hex. #[test] fn hex_wrappers_roundtrip_and_reject_invalid_hex() { // Roundtrip: Fills/Neutral/Primary light (композит #E4E4E6 = #787880@0.20 над #FFFFFF). @@ -949,17 +817,10 @@ mod tests { // min_alpha_hex: для равных цветов пол = 0; для контрастной пары > 0. assert_eq!(min_alpha_hex("#FFFFFF", "#FFFFFF").unwrap(), 0.0); assert!(min_alpha_hex("#101012", "#FFFFFF").unwrap() > 0.9); - // Hex-путь: неразрешимая α поднимается, а итоговый sRGB8-композит - // побайтно равен солиду. - let (tint2, actual) = resolve_alpha_analog_hex("#101012", 0.05, "#FFFFFF") - .expect("валидный домен всегда имеет конечный ответ"); - assert!(actual > 0.05, "α обязана подняться до разрешимой"); - assert_eq!(composite_hex(&tint2, actual, "#FFFFFF").unwrap(), "#101012"); - // Невалидный hex — Err на каждой обёртке (никаких паник). + // Невалидный hex — Err на каждой публичной обёртке (никаких паник). for f in [ invert_composite_hex("ош", 0.5, "#FFFFFF").err(), min_alpha_hex("#12345", "#FFFFFF").err(), - resolve_alpha_analog_hex("#GGGGGG", 0.5, "#FFFFFF").err(), ] { assert!(f.is_some(), "невалидный hex обязан дать Err"); } @@ -985,70 +846,6 @@ mod tests { ); } - /// Резолвер строго отвергает α вне `[0,1]`: ни NaN, ни конечное значение за - /// границей не должны превращаться clamp-ом в правдоподобный ответ. - #[test] - fn resolver_rejects_every_out_of_domain_alpha() { - let ok = [0.5, 0.5, 0.5]; - assert!(resolve_alpha_analog([1.5, 0.0, 0.0], 0.5, ok).is_none()); - for bad in [ - f64::NAN, - f64::NEG_INFINITY, - f64::INFINITY, - -f64::EPSILON, - 1.0 + f64::EPSILON, - -1.0, - 5.0, - ] { - assert!( - resolve_alpha_analog(ok, bad, ok).is_none(), - "requested α={bad} обязана быть отвергнута" - ); - assert!( - resolve_alpha_analog_hex("#808080", bad, "#808080").is_err(), - "hex-граница обязана вернуть Err для α={bad}" - ); - } - } - - #[test] - fn public_hex_boundary_stringifies_typed_proposal_failure() { - let error = resolve_alpha_analog_hex("#000000", -0.25, "#FFFFFF") - .expect_err("public hex boundary must preserve proposal rejection"); - assert!(error.contains("requested_alpha вне конечного [0,1]")); - } - - #[test] - fn public_string_boundary_omits_authored_routing_identity() { - let message_for = |declaration_ordinal| { - let error = crate::analog::ExactAlphaProgramV1::evaluate( - crate::analog::AuthoredAlphaBindingIdV1::Named { - declaration_ordinal, - }, - crate::Srgb8::new([0; 3]), - crate::Srgb8::new([255; 3]), - crate::composition::AdmittedOpacityV1::new(0.5).unwrap(), - crate::Srgb8::new([0; 3]), - ) - .expect_err("control candidate must violate exact identity"); - let witness = error; - resolve_verified_error_message( - crate::analog::ResolveVerifiedErrorV1::ConstraintViolation(witness), - ) - }; - - let first = message_for(2); - let second = message_for(9); - assert_eq!(first, second, "routing identity must remain typed-only"); - assert_eq!( - first, - "alpha-analog не воспроизвёл sRGB8-цель: target=[0, 0, 0], actual=[128, 128, 128]" - ); - for forbidden in ["Standalone", "Named", "ordinal", "declaration_ordinal"] { - assert!(!first.contains(forbidden)); - } - } - /// Домен ядра закреплён: внегамутные и неконечные входы отвергаются /// (молчаливый ответ на мусор был бы ложным обещанием разрешимости). #[test] diff --git a/crates/labcolors-core/src/appearance.rs b/crates/labcolors-core/src/appearance.rs index 2f8aab06..7f9f5f45 100644 --- a/crates/labcolors-core/src/appearance.rs +++ b/crates/labcolors-core/src/appearance.rs @@ -1383,7 +1383,7 @@ impl SourceOverCertificateV1 { self.subject_opacity.bits() } - const fn subject_opacity(&self) -> crate::composition::AdmittedOpacityV1 { + pub(crate) const fn subject_opacity(&self) -> crate::composition::AdmittedOpacityV1 { self.subject_opacity } diff --git a/crates/labcolors-core/src/appearance_graph_tests.rs b/crates/labcolors-core/src/appearance_graph_tests.rs index 5f79920b..e6aaffce 100644 --- a/crates/labcolors-core/src/appearance_graph_tests.rs +++ b/crates/labcolors-core/src/appearance_graph_tests.rs @@ -265,7 +265,7 @@ fn static_exact_program_is_declarative_topology_plus_typed_constraint() { .unwrap(); assert!(crate::appearance::point_program_matches(&compiled)); assert_eq!( - crate::analog::ExactAlphaProgramV1::physical_identity(), + crate::point_representation::ExactPointRepresentationV1::physical_identity(), crate::appearance::PhysicalProgramIdentityV1::InputOpacityOverSurfaceEncodedSrgb8V1 ); assert_eq!( diff --git a/crates/labcolors-core/src/config.rs b/crates/labcolors-core/src/config.rs index ca8ac53b..804b97bf 100644 --- a/crates/labcolors-core/src/config.rs +++ b/crates/labcolors-core/src/config.rs @@ -112,9 +112,9 @@ const HUE_STIFFNESS_MIN_INCLUSIVE: f64 = 0.0; /// Запрошенная альфа альфа-аналога (`roles.*.alpha`) обязана лежать в `(0, 1]`. /// /// `≤ 0` — невидимая роль (вырождение), `> 1` — не альфа. Резолвер поднимает -/// фактическую α до `α_min`, если запрошенная ниже минимально-разрешимой в -/// гамуте ([`crate::alpha::resolve_alpha_analog`]) — но сам запрос должен быть -/// валидной альфой. +/// фактическую α до первого exact sRGB8-представления, если запрос ниже +/// минимально разрешимого в гамуте. Сам запрос всё равно обязан быть валидной +/// альфой. const ALPHA_MIN_EXCLUSIVE: f64 = 0.0; /// Верхний предел запрошенной альфы (включительно; α = 1 = солид). const ALPHA_MAX_INCLUSIVE: f64 = 1.0; diff --git a/crates/labcolors-core/src/generic_boundary_tests.rs b/crates/labcolors-core/src/generic_boundary_tests.rs index 843a6d8d..8d8fe520 100644 --- a/crates/labcolors-core/src/generic_boundary_tests.rs +++ b/crates/labcolors-core/src/generic_boundary_tests.rs @@ -5,6 +5,7 @@ use std::path::PathBuf; mod source_scanner; const APPEARANCE_SOURCE: &str = include_str!("appearance.rs"); +const ALPHA_SOURCE: &str = include_str!("alpha.rs"); const CLEAN_SET_SOURCE: &str = include_str!("clean_set.rs"); const CONSTRAINTS_SOURCE: &str = include_str!("constraints/mod.rs"); const CONTEXTUAL_REGION_SOURCE: &str = include_str!("contextual_region.rs"); @@ -20,13 +21,59 @@ 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 POINT_REPRESENTATION_SOURCE: &str = include_str!("point_representation.rs"); const PROGRAM_IDENTITY_SOURCE: &str = include_str!("program_identity.rs"); const PROGRAM_SESSION_SOURCE: &str = include_str!("program_session.rs"); const RELATION_SOURCE: &str = include_str!("relation.rs"); const SESSION_SOURCE: &str = include_str!("session.rs"); const WCAG22_CONSTRAINT_SOURCE: &str = include_str!("constraints/wcag22.rs"); -const GENERIC_SOURCES: [(&str, &str); 10] = [ +#[test] +fn point_representation_execution_is_generic_and_the_helper_facade_is_gone() { + let representation = normalized_production_code(POINT_REPRESENTATION_SOURCE); + for forbidden in [ + "alphaanalog", + "authoredalphabinding", + "exactalphaprogram", + "crate::analog", + "rolerecipe", + "rolespec", + ] { + assert!( + !representation.contains(forbidden), + "generic point representation still owns recipe vocabulary `{forbidden}`", + ); + } + for required in [ + "pointopacityoversurfacev1", + "assess_visible_point_hard", + "exactidentityreleasev1", + ] { + assert!( + representation.contains(required), + "generic point representation lost physical gate `{required}`", + ); + } + + let alpha = compact_production_syntax(ALPHA_SOURCE).to_ascii_lowercase(); + for removed in [ + "structalphaanalog", + "fnresolve_alpha_analog(", + "fnresolve_alpha_analog_hex(", + "crate::analog", + ] { + assert!( + !alpha.contains(removed), + "public alpha helper facade still exposes removed recipe path `{removed}`", + ); + } + assert!( + !LIB_SOURCE.contains("mod analog;"), + "the recipe-shaped execution module must be deleted", + ); +} + +const GENERIC_SOURCES: [(&str, &str); 11] = [ ("appearance.rs", APPEARANCE_SOURCE), ("constraints/family.rs", FAMILY_CONSTRAINT_SOURCE), ("constraints/relation.rs", RELATION_CONSTRAINT_SOURCE), @@ -36,6 +83,7 @@ const GENERIC_SOURCES: [(&str, &str); 10] = [ ("program/attachment.rs", PROGRAM_ATTACHMENT_SOURCE), ("program_identity.rs", PROGRAM_IDENTITY_SOURCE), ("program_session.rs", PROGRAM_SESSION_SOURCE), + ("point_representation.rs", POINT_REPRESENTATION_SOURCE), ("relation.rs", RELATION_SOURCE), ]; diff --git a/crates/labcolors-core/src/lib.rs b/crates/labcolors-core/src/lib.rs index df65cb19..2fe730e8 100644 --- a/crates/labcolors-core/src/lib.rs +++ b/crates/labcolors-core/src/lib.rs @@ -25,7 +25,6 @@ pub use srgb8::Srgb8; pub(crate) mod accent_balance; pub mod alpha; -pub(crate) mod analog; pub(crate) mod appearance; pub mod config; pub(crate) mod constraints; @@ -47,6 +46,7 @@ pub mod numerical_plan; reason = "the output-profile firewall is intentionally internal to registered profiles" )] pub(crate) mod output_projection; +pub(crate) mod point_representation; #[cfg_attr( not(test), expect( diff --git a/crates/labcolors-core/src/analog.rs b/crates/labcolors-core/src/point_representation.rs similarity index 52% rename from crates/labcolors-core/src/analog.rs rename to crates/labcolors-core/src/point_representation.rs index 5567ce31..857fd2ca 100644 --- a/crates/labcolors-core/src/analog.rs +++ b/crates/labcolors-core/src/point_representation.rs @@ -1,6 +1,6 @@ -//! Приватная exact-программа допуска AlphaAnalog. +//! Exact point representation over one declared backdrop. //! -//! Proposal выбирает `(tint, alpha)`, но не сертифицирует себя. Этот модуль +//! Proposal выбирает `(source, opacity)`, но не сертифицирует себя. Этот модуль //! материализует ровно один финальный occurrence общей point-программой, //! применяет exact identity constraint и только после `Pass` создаёт verified //! value. Никакого результата с частичным evidence при mismatch не существует. @@ -17,18 +17,9 @@ use crate::constraints::{ assess_visible_point_hard, }; -/// Opaque identity authored invocation-а. Standalone helper не притворяется -/// client binding; named compiler назначает ordinal конкретной декларации. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum AuthoredAlphaBindingIdV1 { - Standalone, - Named { declaration_ordinal: usize }, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) struct ExactIdentityEvidenceV1 { +pub(crate) struct PointRepresentationEvidenceV1 { physical: PhysicalProgramIdentityV1, - authored: AuthoredAlphaBindingIdV1, constraint: ExactConstraintIdentityV1, capability: ExactIdentityCapabilityV1, release: ExactIdentityReleaseV1, @@ -39,30 +30,28 @@ pub(crate) struct ExactIdentityEvidenceV1 { } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) struct VerifiedAlphaAnalogV1 { - authored: AuthoredAlphaBindingIdV1, +pub(crate) struct VerifiedPointRepresentationV1 { evidence: ExactPassEvidenceV1, } -impl VerifiedAlphaAnalogV1 { - pub(crate) fn tint(&self) -> Srgb8 { +impl VerifiedPointRepresentationV1 { + pub(crate) fn source(&self) -> Srgb8 { Srgb8::new(self.certificate().subject_rgb()) } - pub(crate) fn alpha(&self) -> f64 { - f64::from_bits(self.certificate().subject_opacity_bits()) + pub(crate) fn opacity(&self) -> AdmittedOpacityV1 { + self.certificate().subject_opacity() } pub(crate) fn certificate(&self) -> &SourceOverCertificateV1 { self.evidence.binding().occurrence_ref() } - pub(crate) fn evidence(&self) -> ExactIdentityEvidenceV1 { + pub(crate) fn evidence(&self) -> PointRepresentationEvidenceV1 { let binding = *self.evidence.binding(); let occurrence = binding.occurrence(); - ExactIdentityEvidenceV1 { - physical: ExactAlphaProgramV1::physical_identity(), - authored: self.authored, + PointRepresentationEvidenceV1 { + physical: ExactPointRepresentationV1::physical_identity(), constraint: *self.evidence.identity(), capability: *self.evidence.capability(), release: *self.evidence.release(), @@ -74,20 +63,21 @@ impl VerifiedAlphaAnalogV1 { } } -/// Существование byte-тинта проверяется по крайнему тинту нужного направления: -/// source-over монотонен по tint, а соседние байты при `alpha <= 1` не могут +/// Существование byte-источника проверяется по крайнему значению нужного +/// направления: source-over монотонен по source, а соседние байты при +/// `opacity <= 1` не могут /// перепрыгнуть целевой байт. -fn target_is_feasible(target: Srgb8, alpha: f64, backdrop: Srgb8) -> bool { +fn target_is_feasible(target: Srgb8, opacity: f64, backdrop: Srgb8) -> bool { let target = target.bytes(); let backdrop = backdrop.bytes(); (0..3).all(|channel| match target[channel].cmp(&backdrop[channel]) { core::cmp::Ordering::Equal => true, core::cmp::Ordering::Greater => { - crate::composition::source_over_channel_srgb8(u8::MAX, alpha, backdrop[channel]) + crate::composition::source_over_channel_srgb8(u8::MAX, opacity, backdrop[channel]) >= target[channel] } core::cmp::Ordering::Less => { - crate::composition::source_over_channel_srgb8(u8::MIN, alpha, backdrop[channel]) + crate::composition::source_over_channel_srgb8(u8::MIN, opacity, backdrop[channel]) <= target[channel] } }) @@ -95,7 +85,7 @@ fn target_is_feasible(target: Srgb8, alpha: f64, backdrop: Srgb8) -> bool { /// Первый `binary64` в `[0,1]`, на котором byte-grid допускает target. /// Неотрицательные `f64` упорядочены битами, поэтому поиск точен и конечен. -pub(crate) fn first_alpha(target: Srgb8, backdrop: Srgb8) -> f64 { +pub(crate) fn first_opacity(target: Srgb8, backdrop: Srgb8) -> f64 { if target == backdrop { return 0.0; } @@ -115,29 +105,30 @@ pub(crate) fn first_alpha(target: Srgb8, backdrop: Srgb8) -> f64 { f64::from_bits(passing) } -/// Канонический byte-тинт при фиксированной alpha. Для каждого канала берётся -/// ближайший к непрерывной инверсии байт из полного проходящего интервала. -pub(crate) fn tint_at_alpha(target: Srgb8, alpha: f64, backdrop: Srgb8) -> Option { - if !target_is_feasible(target, alpha, backdrop) { +/// Канонический byte-источник при фиксированной opacity. Для каждого канала +/// берётся ближайший к непрерывной инверсии байт из полного проходящего +/// интервала. +pub(crate) fn source_at_opacity(target: Srgb8, opacity: f64, backdrop: Srgb8) -> Option { + if !target_is_feasible(target, opacity, backdrop) { return None; } let target = target.bytes(); let backdrop = backdrop.bytes(); - let mut tint = [0_u8; 3]; + let mut source = [0_u8; 3]; for channel in 0..3 { let target_channel = target[channel]; let backdrop_channel = backdrop[channel]; if target_channel == backdrop_channel { - tint[channel] = backdrop_channel; + source[channel] = backdrop_channel; continue; } - if alpha == 0.0 { + if opacity == 0.0 { return None; } let output = |candidate: u8| { - crate::composition::source_over_channel_srgb8(candidate, alpha, backdrop_channel) + crate::composition::source_over_channel_srgb8(candidate, opacity, backdrop_channel) }; let mut lo = 0_u16; let mut hi = 255_u16; @@ -168,15 +159,15 @@ pub(crate) fn tint_at_alpha(target: Srgb8, alpha: f64, backdrop: Srgb8) -> Optio let last = (lo - 1) as u8; let ideal = f64::from(backdrop_channel) - + (f64::from(target_channel) - f64::from(backdrop_channel)) / alpha; + + (f64::from(target_channel) - f64::from(backdrop_channel)) / opacity; let floor = ideal.floor().clamp(f64::from(first), f64::from(last)) as u8; let ceil = ideal.ceil().clamp(f64::from(first), f64::from(last)) as u8; let error = |candidate: u8| { - (crate::composition::source_over_channel_value(candidate, alpha, backdrop_channel) + (crate::composition::source_over_channel_value(candidate, opacity, backdrop_channel) - f64::from(target_channel)) .abs() }; - tint[channel] = if error(floor).total_cmp(&error(ceil)).is_le() { + source[channel] = if error(floor).total_cmp(&error(ceil)).is_le() { floor } else { ceil @@ -184,72 +175,64 @@ pub(crate) fn tint_at_alpha(target: Srgb8, alpha: f64, backdrop: Srgb8) -> Optio } let actual = [ - crate::composition::source_over_channel_srgb8(tint[0], alpha, backdrop[0]), - crate::composition::source_over_channel_srgb8(tint[1], alpha, backdrop[1]), - crate::composition::source_over_channel_srgb8(tint[2], alpha, backdrop[2]), + crate::composition::source_over_channel_srgb8(source[0], opacity, backdrop[0]), + crate::composition::source_over_channel_srgb8(source[1], opacity, backdrop[1]), + crate::composition::source_over_channel_srgb8(source[2], opacity, backdrop[2]), ]; - (actual == target).then_some(Srgb8::new(tint)) + (actual == target).then_some(Srgb8::new(source)) } fn propose( target: Srgb8, - requested_alpha: f64, + minimum_opacity: AdmittedOpacityV1, backdrop: Srgb8, -) -> Result<(Srgb8, AdmittedOpacityV1), AlphaAnalogProposalErrorV1> { - let requested = AdmittedOpacityV1::new(requested_alpha).map_err(|_| { - AlphaAnalogProposalErrorV1::InvalidRequestedAlpha { - bits: requested_alpha.to_bits(), - } - })?; - if let Some(tint) = tint_at_alpha(target, requested.value(), backdrop) { - return Ok((tint, requested)); +) -> Result<(Srgb8, AdmittedOpacityV1), PointRepresentationProposalErrorV1> { + if let Some(source) = source_at_opacity(target, minimum_opacity.value(), backdrop) { + return Ok((source, minimum_opacity)); } - let alpha = AdmittedOpacityV1::new(first_alpha(target, backdrop)) - .map_err(|_| AlphaAnalogProposalErrorV1::DerivedAlphaOutsideUnitInterval)?; - debug_assert!(alpha.value() > requested.value()); - let tint = tint_at_alpha(target, alpha.value(), backdrop) - .ok_or(AlphaAnalogProposalErrorV1::MissingTintAtFirstAlpha)?; - Ok((tint, alpha)) + let opacity = AdmittedOpacityV1::new(first_opacity(target, backdrop)) + .map_err(|_| PointRepresentationProposalErrorV1::DerivedOpacityOutsideUnitInterval)?; + debug_assert!(opacity.value() > minimum_opacity.value()); + let source = source_at_opacity(target, opacity.value(), backdrop) + .ok_or(PointRepresentationProposalErrorV1::MissingSourceAtFirstOpacity)?; + Ok((source, opacity)) } /// Единственный coordinator byte-grid proposal, point occurrence и exact gate. -pub(crate) fn resolve_verified( - authored: AuthoredAlphaBindingIdV1, +pub(crate) fn resolve_exact_point_representation_v1( target: Srgb8, - requested_alpha: f64, + minimum_opacity: AdmittedOpacityV1, backdrop: Srgb8, -) -> Result { - let (tint, alpha) = - propose(target, requested_alpha, backdrop).map_err(ResolveVerifiedErrorV1::Proposal)?; - ExactAlphaProgramV1::evaluate(authored, target, tint, alpha, backdrop) - .map_err(ResolveVerifiedErrorV1::ConstraintViolation) +) -> Result { + let (source, opacity) = propose(target, minimum_opacity, backdrop) + .map_err(ResolvePointRepresentationErrorV1::Proposal)?; + ExactPointRepresentationV1::evaluate(target, source, opacity, backdrop) + .map_err(ResolvePointRepresentationErrorV1::ConstraintViolation) } #[derive(Debug, Clone, PartialEq, Eq)] -pub(crate) enum ResolveVerifiedErrorV1 { - Proposal(AlphaAnalogProposalErrorV1), +pub(crate) enum ResolvePointRepresentationErrorV1 { + Proposal(PointRepresentationProposalErrorV1), // Нынешний exact byte-grid proposal не может попасть сюда, но coordinator - // не имеет права стирать authored witness, если его построитель изменится. - ConstraintViolation(AlphaAnalogViolationV1), + // не имеет права стирать physical witness, если его построитель изменится. + ConstraintViolation(PointRepresentationViolationV1), } -/// Typed отказ proposal до materialization occurrence. Binary64 хранится -/// битами, чтобы transport-строка оставалась обязанностью публичной границы. +/// Typed отказ proposal до materialization occurrence. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum AlphaAnalogProposalErrorV1 { - InvalidRequestedAlpha { bits: u64 }, - DerivedAlphaOutsideUnitInterval, - MissingTintAtFirstAlpha, +pub(crate) enum PointRepresentationProposalErrorV1 { + DerivedOpacityOutsideUnitInterval, + MissingSourceAtFirstOpacity, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) struct AlphaAnalogViolationV1 { - authored: AuthoredAlphaBindingIdV1, +pub(crate) struct PointRepresentationViolationV1 { violation: ExactViolationEvidenceV1, } -impl AlphaAnalogViolationV1 { +impl PointRepresentationViolationV1 { + #[cfg(test)] pub(crate) const fn violation(&self) -> &ExactViolationEvidenceV1 { &self.violation } @@ -257,33 +240,29 @@ impl AlphaAnalogViolationV1 { /// Code-owned compiled invocation: одна физическая topology и один exact /// evaluator. Runtime bindings — только admitted bytes и alpha. -pub(crate) struct ExactAlphaProgramV1; +pub(crate) struct ExactPointRepresentationV1; -impl ExactAlphaProgramV1 { +impl ExactPointRepresentationV1 { pub(crate) const fn physical_identity() -> PhysicalProgramIdentityV1 { PointOpacityOverSurfaceV1::physical_identity() } pub(crate) fn evaluate( - authored: AuthoredAlphaBindingIdV1, target: Srgb8, - tint: Srgb8, - alpha: AdmittedOpacityV1, + source: Srgb8, + opacity: AdmittedOpacityV1, backdrop: Srgb8, - ) -> Result { + ) -> Result { let occurrence = - PointOpacityOverSurfaceV1::evaluate_admitted(tint.bytes(), alpha, backdrop.bytes()); + PointOpacityOverSurfaceV1::evaluate_admitted(source.bytes(), opacity, backdrop.bytes()); let evidence = match assess_visible_point_hard(&occurrence, &ExactSrgb8IdentityV1, target) { Ok(HardDecision::Pass(evidence)) => evidence, Ok(HardDecision::Violation(violation)) => { - return Err(AlphaAnalogViolationV1 { - authored, - violation, - }); + return Err(PointRepresentationViolationV1 { violation }); } Err(error) => match error {}, }; - let verified = VerifiedAlphaAnalogV1 { authored, evidence }; + let verified = VerifiedPointRepresentationV1 { evidence }; debug_assert_eq!( verified.evidence().actual, Srgb8::new(verified.certificate().output_rgb()) @@ -301,34 +280,26 @@ mod tests { } #[test] - fn public_alpha_facade_owns_no_byte_grid_proposal_or_verified_coordinator() { - let facade = include_str!("alpha.rs"); - let (production, _) = facade - .split_once("\n#[cfg(test)]\nmod tests {") - .expect("alpha facade must retain one explicit test-module boundary"); - for forbidden in [ - "fn target_is_feasible", - "fn first_alpha(", - "fn tint_at_alpha", - "fn propose(", - "fn resolve_verified(", - "fn srgb8_target_is_feasible", - "fn first_srgb8_alpha", - "fn srgb8_tint_at_alpha", - "fn propose_alpha_analog_srgb8", - "fn resolve_alpha_analog_srgb8_verified", - ] { - assert!( - !production.contains(forbidden), - "alpha facade still owns private exact machinery: {forbidden}" - ); - } + fn requested_opacity_is_preserved_when_feasible_and_raised_when_required() { + let backdrop = Srgb8::new([0; 3]); + let feasible = + resolve_exact_point_representation_v1(Srgb8::new([128; 3]), admitted(0.5), backdrop) + .unwrap(); + assert_eq!(feasible.opacity().value().to_bits(), 0.5_f64.to_bits()); + + let requested = admitted(0.001); + let raised = + resolve_exact_point_representation_v1(Srgb8::new([128; 3]), requested, backdrop) + .unwrap(); + assert!(raised.opacity().value() > requested.value()); assert_eq!( - production - .matches("crate::analog::resolve_verified") - .count(), - 1, - "the public hex facade must delegate exactly once to the private coordinator" + crate::composition::source_over_srgb8( + raised.source().bytes(), + raised.opacity().value(), + backdrop.bytes(), + ) + .unwrap(), + [128; 3] ); } @@ -354,7 +325,7 @@ mod tests { .total_cmp(&error(*right)) .then_with(|| left.cmp(right)) }); - let actual = tint_at_alpha( + let actual = source_at_opacity( Srgb8::new([target, backdrop, backdrop]), alpha, Srgb8::new([backdrop; 3]), @@ -369,43 +340,19 @@ mod tests { } } - #[test] - fn finite_invalid_alpha_is_rejected_before_any_proposal() { - let target = Srgb8::new([1, 0, 0]); - let backdrop = Srgb8::new([0; 3]); - for requested_alpha in [-0.25, 1.25] { - let error: ResolveVerifiedErrorV1 = resolve_verified( - AuthoredAlphaBindingIdV1::Standalone, - target, - requested_alpha, - backdrop, - ) - .expect_err("invalid alpha must not be silently moved onto the exact frontier"); - let ResolveVerifiedErrorV1::Proposal( - AlphaAnalogProposalErrorV1::InvalidRequestedAlpha { bits }, - ) = error - else { - panic!("invalid requested alpha must fail at the proposal boundary") - }; - assert_eq!(bits, requested_alpha.to_bits()); - } - } - #[test] fn corrupted_candidate_cannot_create_verified_output() { let target = Srgb8::new([0, 0, 0]); crate::composition::reset_source_over_evaluation_count(); - let error = ExactAlphaProgramV1::evaluate( - AuthoredAlphaBindingIdV1::Standalone, + let error = ExactPointRepresentationV1::evaluate( target, Srgb8::new([255, 255, 255]), admitted(0.5), Srgb8::new([0, 0, 0]), ) - .expect_err("wrong final bytes must not mint VerifiedAlphaAnalogV1"); + .expect_err("wrong final bytes must not mint a verified representation"); let witness = error; fn requires_violation(_: ExactViolationEvidenceV1) {} - assert_eq!(witness.authored, AuthoredAlphaBindingIdV1::Standalone); assert_eq!(witness.violation().target(), target); assert_eq!(witness.violation().actual(), Srgb8::new([128; 3])); requires_violation(witness.violation); @@ -413,33 +360,8 @@ mod tests { } #[test] - fn equal_violation_physics_under_distinct_named_bindings_keeps_distinct_witnesses() { - let evaluate = |declaration_ordinal| { - ExactAlphaProgramV1::evaluate( - AuthoredAlphaBindingIdV1::Named { - declaration_ordinal, - }, - Srgb8::new([0, 0, 0]), - Srgb8::new([255, 255, 255]), - admitted(0.5), - Srgb8::new([0, 0, 0]), - ) - .expect_err("control candidate must violate exact identity") - }; - let first = evaluate(2); - let second = evaluate(9); - - assert_eq!(first.violation(), second.violation()); - assert_ne!(first.authored, second.authored); - assert_ne!(first, second); - } - - #[test] - fn exact_evidence_keeps_physics_and_authored_routing_separate() { - let verified = ExactAlphaProgramV1::evaluate( - AuthoredAlphaBindingIdV1::Named { - declaration_ordinal: 7, - }, + fn exact_evidence_contains_only_physics_and_the_executed_constraint() { + let verified = ExactPointRepresentationV1::evaluate( Srgb8::new([128, 128, 128]), Srgb8::new([0, 0, 0]), admitted(0.5), @@ -464,41 +386,6 @@ mod tests { verified.evidence.binding().program_occurrence() ); assert_eq!(evidence.occurrence.output_rgb(), evidence.actual.bytes()); - assert_eq!( - evidence.authored, - AuthoredAlphaBindingIdV1::Named { - declaration_ordinal: 7 - } - ); - } - - #[test] - fn equal_physics_under_distinct_named_bindings_keeps_distinct_evidence() { - let evaluate = |declaration_ordinal| { - ExactAlphaProgramV1::evaluate( - AuthoredAlphaBindingIdV1::Named { - declaration_ordinal, - }, - Srgb8::new([128, 128, 128]), - Srgb8::new([0, 0, 0]), - admitted(0.5), - Srgb8::new([255, 255, 255]), - ) - .unwrap() - }; - let first = evaluate(2).evidence(); - let second = evaluate(9).evidence(); - - assert_eq!(first.physical, second.physical); - assert_eq!(first.constraint, second.constraint); - assert_eq!(first.capability, second.capability); - assert_eq!(first.release, second.release); - assert_eq!(first.program_occurrence, second.program_occurrence); - assert_eq!(first.occurrence, second.occurrence); - assert_eq!(first.target, second.target); - assert_eq!(first.actual, second.actual); - assert_ne!(first.authored, second.authored); - assert_ne!(first, second); } #[test] @@ -508,13 +395,7 @@ mod tests { let backdrop = Srgb8::new([0xFF, 0xFF, 0xFF]); let (result, allocations) = crate::test_support::measured_allocations(|| { - ExactAlphaProgramV1::evaluate( - AuthoredAlphaBindingIdV1::Standalone, - target, - tint, - admitted(0.5), - backdrop, - ) + ExactPointRepresentationV1::evaluate(target, tint, admitted(0.5), backdrop) }); assert!(result.is_ok()); diff --git a/crates/labcolors-core/src/semantic.rs b/crates/labcolors-core/src/semantic.rs index d899d933..e6371283 100644 --- a/crates/labcolors-core/src/semantic.rs +++ b/crates/labcolors-core/src/semantic.rs @@ -569,7 +569,7 @@ pub enum RoleSpec { /// тинт-якорь эмитится напрямую. Даёт `-tinted`-роли labui (fill-*-tinted): /// заливка, чей композит на подложке = соответствующий солид. /// - /// Фактическая α возвращается явно ([`crate::alpha::AlphaAnalog::alpha`]): при + /// Фактическая α возвращается явно ([`TranslucentResolved::alpha`]): при /// неразрешимой запрошенной α поднимается до `α_min` (композит остаётся точно /// равным солиду — двигается прозрачность, не цвет; кламп тинта запрещён). AlphaAnalog { @@ -577,9 +577,9 @@ pub enum RoleSpec { of: LadderTint, /// Запрошенная альфа: `(0, 1]` — контракт РОЛИ (тот же предел, что у /// конфиг-валидатора: α = 0 — невидимая роль, отказ честнее выдумки). - /// Уже: библиотечный [`crate::alpha::resolve_alpha_analog`] принимает - /// и `0.0` (вырожденный ответ tint=фон) — то его домен, не ролевой. - /// Поднимается до `α_min`, если запрошенная ниже разрешимой. + /// Generic point-закон допускает `0.0` как физическую opacity, но этот + /// временный recipe-frontend исключает невидимую роль. Поднимается до + /// первого exact sRGB8-представления, если запрос ниже разрешимого. alpha: f64, }, /// Двухслойный материал (стекло/акрил): опаковая тон-база `02` на целевом @@ -2536,13 +2536,12 @@ fn resolve_solid_with_ui_floor( } } -/// Альфа-аналог: солид-цель `solid` (кодированный, по теме) на фоне резолва -/// инвертируется в `(tint, фактическая α)`. Перед инверсией цель квантуется до -/// эмитируемой sRGB8-сетки; production-композитор обязан побайтно вернуть её. -fn resolve_rgba_inverted_with_binding( - authored: crate::analog::AuthoredAlphaBindingIdV1, +/// Замороженный recipe-frontend понижает solid-цель и локальный фон в общий +/// exact point-закон. Client routing заканчивается здесь и структурно не может +/// попасть в физический coordinator. +fn resolve_rgba_inverted_admitted( solid_encoded: [f64; 3], - requested_alpha: f64, + minimum_opacity: crate::composition::AdmittedOpacityV1, bg: &BgInput, vc: &ViewingConditions, ) -> PendingResolution { @@ -2554,11 +2553,6 @@ fn resolve_rgba_inverted_with_binding( "validated alpha-analog solid left encoded sRGB domain".into(), )); } - if !role_alpha_valid(requested_alpha) { - return Err(SolveFailure::InvalidInput( - "alpha-analog alpha must be finite and inside (0, 1]".into(), - )); - } let target = match crate::alpha::encoded_to_srgb8(solid_encoded, "solid") { Ok(target) => Srgb8::new(target), Err(error) => { @@ -2575,23 +2569,26 @@ fn resolve_rgba_inverted_with_binding( ))); } }; - let verified = - match crate::analog::resolve_verified(authored, target, requested_alpha, backdrop) { - Ok(verified) => verified, - Err(_error) => { - // Здесь вход уже прошёл admission; typed witness остаётся в - // analog boundary, а публичная semantic-ошибка не сериализует - // authored routing или внутреннюю структуру evidence. - return Err(SolveFailure::InternalInvariant( - "validated alpha-analog resolver violated its total-domain contract".into(), - )); - } - }; - let actual_alpha = verified.alpha(); + let verified = match crate::point_representation::resolve_exact_point_representation_v1( + target, + minimum_opacity, + backdrop, + ) { + Ok(verified) => verified, + Err(_error) => { + // Typed admission и полный byte-grid proof делают ветку + // недостижимой, пока не изменился сам физический закон. + return Err(SolveFailure::InternalInvariant( + "validated point representation violated its total-domain contract".into(), + )); + } + }; + let actual_opacity = verified.opacity(); + let actual_alpha = actual_opacity.value(); // Резолвер возвращает тот же binary64 либо строго больший точный пол. - let alpha_coerced = actual_alpha > requested_alpha; + let alpha_coerced = actual_alpha > minimum_opacity.value(); finish_rgba_from_certificate( - verified.tint(), + verified.source(), actual_alpha, verified.certificate(), vc, @@ -2607,13 +2604,13 @@ fn resolve_rgba_inverted( bg: &BgInput, vc: &ViewingConditions, ) -> PendingResolution { - resolve_rgba_inverted_with_binding( - crate::analog::AuthoredAlphaBindingIdV1::Standalone, - solid_encoded, - requested_alpha, - bg, - vc, - ) + let opacity = crate::composition::AdmittedOpacityV1::new(requested_alpha) + .ok() + .filter(|opacity| opacity.value() > 0.0) + .ok_or_else(|| { + SolveFailure::InvalidInput("alpha-analog alpha must be finite and inside (0, 1]".into()) + })?; + resolve_rgba_inverted_admitted(solid_encoded, opacity, bg, vc) } /// Собрать [`Resolved::Translucent`] из эмитируемых тинта и альфы: вывести их @@ -2952,24 +2949,11 @@ pub struct NamedRoleTable { alpha_analog_invocations: Box<[CompiledAlphaAnalogInvocationV1]>, } -#[derive(Debug, Clone, Copy, PartialEq)] -struct AdmittedRequestedAlphaV1(f64); - -impl AdmittedRequestedAlphaV1 { - fn parse(value: f64) -> Option { - role_alpha_valid(value).then_some(Self(value)) - } - - const fn get(self) -> f64 { - self.0 - } -} - #[derive(Debug, Clone, Copy, PartialEq)] struct CompiledAlphaAnalogInvocationV1 { declaration_ordinal: usize, target: LadderTint, - requested_alpha: AdmittedRequestedAlphaV1, + minimum_opacity: crate::composition::AdmittedOpacityV1, } #[cfg(test)] @@ -2991,15 +2975,7 @@ fn alpha_binding_plan_compilation_count() -> usize { impl CompiledAlphaAnalogInvocationV1 { fn resolve(self, bg: &BgInput, vc: &ViewingConditions) -> PendingResolution { - resolve_rgba_inverted_with_binding( - crate::analog::AuthoredAlphaBindingIdV1::Named { - declaration_ordinal: self.declaration_ordinal, - }, - self.target.for_vc(vc), - self.requested_alpha.get(), - bg, - vc, - ) + resolve_rgba_inverted_admitted(self.target.for_vc(vc), self.minimum_opacity, bg, vc) } } @@ -3229,15 +3205,17 @@ impl NamedRoleTable { let RoleSpec::AlphaAnalog { of, alpha } = *spec else { continue; }; - let requested_alpha = - AdmittedRequestedAlphaV1::parse(alpha).ok_or(AlphaAnalogCompileErrorV1 { + let minimum_opacity = crate::composition::AdmittedOpacityV1::new(alpha) + .ok() + .filter(|opacity| opacity.value() > 0.0) + .ok_or(AlphaAnalogCompileErrorV1 { declaration_ordinal, value: alpha, })?; invocations.push(CompiledAlphaAnalogInvocationV1 { declaration_ordinal, target: of, - requested_alpha, + minimum_opacity, }); } debug_assert!( @@ -4151,7 +4129,7 @@ mod tests { #![proptest_config(ProptestConfig::with_cases(64))] #[test] - fn public_helper_and_named_compiled_invocation_share_exact_bytes( + fn compiled_frontend_and_generic_point_law_share_exact_bytes( target_bytes in any::<[u8; 3]>(), backdrop_bytes in any::<[u8; 3]>(), alpha_step in 1_u16..=1024, @@ -4178,18 +4156,22 @@ mod tests { ) .unwrap(); let named = named[0].1.translucent().unwrap(); - let (public_tint, public_alpha) = crate::alpha::resolve_alpha_analog_hex( - &target.to_hex(), - requested_alpha, - &backdrop.to_hex(), + let physical = crate::point_representation::resolve_exact_point_representation_v1( + target, + crate::composition::AdmittedOpacityV1::new(requested_alpha).unwrap(), + backdrop, ) .unwrap(); - prop_assert_eq!(named.tint_hex(), public_tint.as_str()); - prop_assert_eq!(named.alpha().to_bits(), public_alpha.to_bits()); + prop_assert_eq!(named.tint_hex(), physical.source().to_hex()); + prop_assert_eq!(named.alpha().to_bits(), physical.opacity().value().to_bits()); prop_assert_eq!(named.composite_hex(), target.to_hex()); prop_assert_eq!( - crate::alpha::composite_hex(&public_tint, public_alpha, &backdrop.to_hex()).unwrap(), + crate::alpha::composite_hex( + &physical.source().to_hex(), + physical.opacity().value(), + &backdrop.to_hex(), + ).unwrap(), target.to_hex() ); } diff --git a/docs/whitepaper.md b/docs/whitepaper.md index b37412e7..6f33973d 100644 --- a/docs/whitepaper.md +++ b/docs/whitepaper.md @@ -163,11 +163,13 @@ APCA-shaped компонент и H-K brightness correction не реклами ## Точечные композиции -`AlphaAnalog`, `Material` и `Glow` решают разные точечные физические задачи, но -используют общие правила владения источником, локальным фоном и конечной -проверкой. +`AlphaAnalog`, `Material` и `Glow` пока остаются boundary-рецептами, а не +вариантами физического Core. Они должны односторонне понижаться в общие +`Paint`/`Surface`/`Occurrence` и проходить один закон композиции и конечной +проверки; отдельный recipe-specific execution запрещён. -- `AlphaAnalog` выводит tint и достижимую alpha для объявленной solid-цели. +- `AlphaAnalog` — замороженный frontend: объявленная solid-цель и минимум + opacity понижаются в exact point-представление над локальной подложкой. - `Material` возвращает точечные слои материала; он не сертифицирует blur, преломление или пространственное стекло. - `Glow` возвращает точечные screen-слои и numerical outcome; он не From fddad10c5431723db978af3678f1131d13919733 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Mon, 3 Aug 2026 09:19:29 +0300 Subject: [PATCH 2/7] =?UTF-8?q?=D0=A1=D0=B4=D0=B5=D0=BB=D0=B0=D1=82=D1=8C?= =?UTF-8?q?=20compiled=20point=20IR=20=D0=BE=D0=B1=D1=89=D0=B8=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/generic_boundary_tests.rs | 22 ++++++ crates/labcolors-core/src/semantic.rs | 72 ++++++++++--------- 2 files changed, 62 insertions(+), 32 deletions(-) diff --git a/crates/labcolors-core/src/generic_boundary_tests.rs b/crates/labcolors-core/src/generic_boundary_tests.rs index 8d8fe520..86bb447f 100644 --- a/crates/labcolors-core/src/generic_boundary_tests.rs +++ b/crates/labcolors-core/src/generic_boundary_tests.rs @@ -25,6 +25,7 @@ const POINT_REPRESENTATION_SOURCE: &str = include_str!("point_representation.rs" const PROGRAM_IDENTITY_SOURCE: &str = include_str!("program_identity.rs"); const PROGRAM_SESSION_SOURCE: &str = include_str!("program_session.rs"); const RELATION_SOURCE: &str = include_str!("relation.rs"); +const SEMANTIC_SOURCE: &str = include_str!("semantic.rs"); const SESSION_SOURCE: &str = include_str!("session.rs"); const WCAG22_CONSTRAINT_SOURCE: &str = include_str!("constraints/wcag22.rs"); @@ -71,6 +72,27 @@ fn point_representation_execution_is_generic_and_the_helper_facade_is_gone() { !LIB_SOURCE.contains("mod analog;"), "the recipe-shaped execution module must be deleted", ); + + let semantic = compact_production_syntax(SEMANTIC_SOURCE).to_ascii_lowercase(); + for removed in [ + "compiledalphaanaloginvocation", + "alpha_analog_invocations", + "compile_alpha_analog_invocations", + ] { + assert!( + !semantic.contains(removed), + "compiled execution still owns recipe-shaped IR `{removed}`", + ); + } + for required in [ + "compiledpointrepresentationinvocationv1", + "resolve_exact_point_representation_v1", + ] { + assert!( + semantic.contains(required), + "frozen frontend no longer lowers through generic point IR `{required}`", + ); + } } const GENERIC_SOURCES: [(&str, &str); 11] = [ diff --git a/crates/labcolors-core/src/semantic.rs b/crates/labcolors-core/src/semantic.rs index e6371283..79f6c8f2 100644 --- a/crates/labcolors-core/src/semantic.rs +++ b/crates/labcolors-core/src/semantic.rs @@ -2946,11 +2946,11 @@ pub struct NamedRoleTable { entries: Vec<(String, RoleSpec)>, aliases: Vec<(String, String)>, chroma: RoleChroma, - alpha_analog_invocations: Box<[CompiledAlphaAnalogInvocationV1]>, + point_representation_invocations: Box<[CompiledPointRepresentationInvocationV1]>, } #[derive(Debug, Clone, Copy, PartialEq)] -struct CompiledAlphaAnalogInvocationV1 { +struct CompiledPointRepresentationInvocationV1 { declaration_ordinal: usize, target: LadderTint, minimum_opacity: crate::composition::AdmittedOpacityV1, @@ -2958,22 +2958,22 @@ struct CompiledAlphaAnalogInvocationV1 { #[cfg(test)] thread_local! { - static ALPHA_BINDING_PLAN_COMPILATIONS: std::cell::Cell = const { + static POINT_REPRESENTATION_PLAN_COMPILATIONS: std::cell::Cell = const { std::cell::Cell::new(0) }; } #[cfg(test)] -fn reset_alpha_binding_plan_compilation_count() { - ALPHA_BINDING_PLAN_COMPILATIONS.with(|count| count.set(0)); +fn reset_point_representation_plan_compilation_count() { + POINT_REPRESENTATION_PLAN_COMPILATIONS.with(|count| count.set(0)); } #[cfg(test)] -fn alpha_binding_plan_compilation_count() -> usize { - ALPHA_BINDING_PLAN_COMPILATIONS.with(std::cell::Cell::get) +fn point_representation_plan_compilation_count() -> usize { + POINT_REPRESENTATION_PLAN_COMPILATIONS.with(std::cell::Cell::get) } -impl CompiledAlphaAnalogInvocationV1 { +impl CompiledPointRepresentationInvocationV1 { fn resolve(self, bg: &BgInput, vc: &ViewingConditions) -> PendingResolution { resolve_rgba_inverted_admitted(self.target.for_vc(vc), self.minimum_opacity, bg, vc) } @@ -3148,8 +3148,8 @@ impl NamedRoleTable { spec.validate_with_chroma(chroma) .map_err(|message| SolveFailure::InvalidInput(format!("role {name}: {message}")))?; } - let alpha_analog_invocations = - Self::compile_alpha_analog_invocations(&entries).map_err(|error| { + let point_representation_invocations = + Self::compile_point_representation_invocations(&entries).map_err(|error| { SolveFailure::InvalidInput(format!( "role {}: alpha-analog alpha must be finite and inside (0, 1], got {}", entries[error.declaration_ordinal].0, error.value @@ -3159,7 +3159,7 @@ impl NamedRoleTable { entries, aliases, chroma, - alpha_analog_invocations, + point_representation_invocations, )) } @@ -3172,12 +3172,13 @@ impl NamedRoleTable { aliases: Vec<(String, String)>, chroma: RoleChroma, ) -> Result { - let alpha_analog_invocations = Self::compile_alpha_analog_invocations(&entries)?; + let point_representation_invocations = + Self::compile_point_representation_invocations(&entries)?; Ok(Self::from_compiled_parts( entries, aliases, chroma, - alpha_analog_invocations, + point_representation_invocations, )) } @@ -3185,21 +3186,21 @@ impl NamedRoleTable { entries: Vec<(String, RoleSpec)>, aliases: Vec<(String, String)>, chroma: RoleChroma, - alpha_analog_invocations: Box<[CompiledAlphaAnalogInvocationV1]>, + point_representation_invocations: Box<[CompiledPointRepresentationInvocationV1]>, ) -> Self { Self { entries, aliases, chroma, - alpha_analog_invocations, + point_representation_invocations, } } - fn compile_alpha_analog_invocations( + fn compile_point_representation_invocations( entries: &[(String, RoleSpec)], - ) -> Result, AlphaAnalogCompileErrorV1> { + ) -> Result, AlphaAnalogCompileErrorV1> { #[cfg(test)] - ALPHA_BINDING_PLAN_COMPILATIONS.with(|count| count.set(count.get() + 1)); + POINT_REPRESENTATION_PLAN_COMPILATIONS.with(|count| count.set(count.get() + 1)); let mut invocations = Vec::new(); for (declaration_ordinal, (_, spec)) in entries.iter().enumerate() { let RoleSpec::AlphaAnalog { of, alpha } = *spec else { @@ -3212,7 +3213,7 @@ impl NamedRoleTable { declaration_ordinal, value: alpha, })?; - invocations.push(CompiledAlphaAnalogInvocationV1 { + invocations.push(CompiledPointRepresentationInvocationV1 { declaration_ordinal, target: of, minimum_opacity, @@ -3304,9 +3305,13 @@ pub fn resolve_named_set( let _forward_cache = crate::spaces::cam16::ForwardCacheGuard::activate(); let ctx = ResolveContext::new(bg, vc); let mut set = Vec::with_capacity(table.entries.len()); - let mut alpha_invocations = table.alpha_analog_invocations.iter().copied().peekable(); + let mut point_invocations = table + .point_representation_invocations + .iter() + .copied() + .peekable(); for (declaration_ordinal, (name, spec)) in table.entries.iter().enumerate() { - let pending = match alpha_invocations.peek().copied() { + let pending = match point_invocations.peek().copied() { Some(invocation) if invocation.declaration_ordinal < declaration_ordinal => { return Err(ResolveSetError { state: ResolveSetErrorState::Internal(SolveFailure::InternalInvariant( @@ -3315,7 +3320,7 @@ pub fn resolve_named_set( }); } Some(invocation) if invocation.declaration_ordinal == declaration_ordinal => { - alpha_invocations.next(); + point_invocations.next(); invocation.resolve(bg, vc) } _ => resolve_spec_in(bg, spec, table.chroma, vc, &ctx), @@ -3323,7 +3328,7 @@ pub fn resolve_named_set( let resolved = admit_resolution(pending)?; set.push((name.clone(), resolved)); } - if alpha_invocations.next().is_some() { + if point_invocations.next().is_some() { return Err(ResolveSetError { state: ResolveSetErrorState::Internal(SolveFailure::InternalInvariant( "compiled alpha-analog invocation points outside declarations".into(), @@ -4015,8 +4020,11 @@ mod tests { RoleChroma::Neutral, ) .unwrap(); - assert_eq!(table.alpha_analog_invocations.len(), 1); - assert_eq!(table.alpha_analog_invocations[0].declaration_ordinal, 1); + assert_eq!(table.point_representation_invocations.len(), 1); + assert_eq!( + table.point_representation_invocations[0].declaration_ordinal, + 1 + ); crate::composition::reset_source_over_evaluation_count(); let set = resolve_named_set( @@ -4029,7 +4037,7 @@ mod tests { assert_eq!(crate::composition::source_over_evaluation_count(), 1); let mut missing = table.clone(); - missing.alpha_analog_invocations = Box::default(); + missing.point_representation_invocations = Box::default(); assert_eq!( missing, table, "derived execution state is outside public equality" @@ -4072,10 +4080,10 @@ mod tests { } #[test] - fn alpha_binding_plan_is_sparse_compiled_once_and_reused() { + fn point_representation_plan_is_sparse_compiled_once_and_reused() { let first_target = crate::spaces::srgb::srgb_encoded_from_hex("#787880").unwrap(); let second_target = crate::spaces::srgb::srgb_encoded_from_hex("#406080").unwrap(); - reset_alpha_binding_plan_compilation_count(); + reset_point_representation_plan_compilation_count(); let table = NamedRoleTable::new( vec![ ( @@ -4098,10 +4106,10 @@ mod tests { RoleChroma::Neutral, ) .unwrap(); - assert_eq!(alpha_binding_plan_compilation_count(), 1); + assert_eq!(point_representation_plan_compilation_count(), 1); assert_eq!( table - .alpha_analog_invocations + .point_representation_invocations .iter() .map(|invocation| invocation.declaration_ordinal) .collect::>(), @@ -4114,7 +4122,7 @@ mod tests { let set = resolve_named_set(&backdrop, current, &ViewingConditions::srgb()).unwrap(); assert_eq!(set.len(), 3); } - assert_eq!(alpha_binding_plan_compilation_count(), 1); + assert_eq!(point_representation_plan_compilation_count(), 1); let without_analogs = NamedRoleTable::new( vec![("unrelated".into(), RoleSpec::Zero)], @@ -4122,7 +4130,7 @@ mod tests { RoleChroma::Neutral, ) .unwrap(); - assert!(without_analogs.alpha_analog_invocations.is_empty()); + assert!(without_analogs.point_representation_invocations.is_empty()); } proptest! { From cb90dcfc956fe66fd9ad7c708f875bd5d2a61a04 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Mon, 3 Aug 2026 09:22:17 +0300 Subject: [PATCH 3/7] =?UTF-8?q?=D0=A2=D0=B8=D0=BF=D0=B8=D0=B7=D0=B8=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D1=82=D1=8C=20opacity=20point-=D0=B7=D0=B0?= =?UTF-8?q?=D0=BA=D0=BE=D0=BD=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/labcolors-core/src/alpha.rs | 15 ++++++----- crates/labcolors-core/src/composition.rs | 1 + .../src/point_representation.rs | 27 ++++++++++++------- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/crates/labcolors-core/src/alpha.rs b/crates/labcolors-core/src/alpha.rs index 99e49c05..47e9ece2 100644 --- a/crates/labcolors-core/src/alpha.rs +++ b/crates/labcolors-core/src/alpha.rs @@ -463,7 +463,7 @@ mod tests { let backdrop = crate::Srgb8::new([bg; 3]); let requested_is_feasible = crate::point_representation::source_at_opacity( target, - requested_alpha, + admitted(requested_alpha), backdrop, ) .is_some(); @@ -496,7 +496,7 @@ mod tests { assert!( crate::point_representation::source_at_opacity( target, - predecessor, + admitted(predecessor), backdrop, ) .is_none(), @@ -541,7 +541,7 @@ mod tests { assert!( crate::point_representation::source_at_opacity( crate::Srgb8::new([1, 0, 0]), - predecessor, + admitted(predecessor), crate::Srgb8::new([0; 3]), ) .is_none() @@ -563,16 +563,17 @@ mod tests { assert!( crate::point_representation::source_at_opacity(target, floor, background) .is_some(), - "solid={solid}, bg={bg}, floor={floor} не проходит" + "solid={solid}, bg={bg}, floor={} не проходит", + floor.value() ); if solid == bg { - assert_eq!(floor.to_bits(), 0.0_f64.to_bits()); + assert_eq!(floor.bits(), 0.0_f64.to_bits()); } else { - let predecessor = f64::from_bits(floor.to_bits() - 1); + let predecessor = f64::from_bits(floor.bits() - 1); assert!( crate::point_representation::source_at_opacity( target, - predecessor, + admitted(predecessor), background, ) .is_none(), diff --git a/crates/labcolors-core/src/composition.rs b/crates/labcolors-core/src/composition.rs index 3f5469dc..f3c19b3f 100644 --- a/crates/labcolors-core/src/composition.rs +++ b/crates/labcolors-core/src/composition.rs @@ -33,6 +33,7 @@ pub(crate) enum CompositionProfileV1 { pub(crate) struct AdmittedOpacityV1(u64); impl AdmittedOpacityV1 { + pub(crate) const TRANSPARENT: Self = Self(0.0f64.to_bits()); pub(crate) const OPAQUE: Self = Self(1.0f64.to_bits()); pub(crate) fn new(alpha: f64) -> Result { diff --git a/crates/labcolors-core/src/point_representation.rs b/crates/labcolors-core/src/point_representation.rs index 857fd2ca..c6de3ea7 100644 --- a/crates/labcolors-core/src/point_representation.rs +++ b/crates/labcolors-core/src/point_representation.rs @@ -85,9 +85,9 @@ fn target_is_feasible(target: Srgb8, opacity: f64, backdrop: Srgb8) -> bool { /// Первый `binary64` в `[0,1]`, на котором byte-grid допускает target. /// Неотрицательные `f64` упорядочены битами, поэтому поиск точен и конечен. -pub(crate) fn first_opacity(target: Srgb8, backdrop: Srgb8) -> f64 { +pub(crate) fn first_opacity(target: Srgb8, backdrop: Srgb8) -> AdmittedOpacityV1 { if target == backdrop { - return 0.0; + return AdmittedOpacityV1::TRANSPARENT; } let mut failing = 0.0_f64.to_bits(); let mut passing = 1.0_f64.to_bits(); @@ -102,13 +102,22 @@ pub(crate) fn first_opacity(target: Srgb8, backdrop: Srgb8) -> f64 { failing = middle; } } - f64::from_bits(passing) + AdmittedOpacityV1::new(f64::from_bits(passing)) + .expect("binary search is bounded by admitted transparent and opaque endpoints") } /// Канонический byte-источник при фиксированной opacity. Для каждого канала /// берётся ближайший к непрерывной инверсии байт из полного проходящего /// интервала. -pub(crate) fn source_at_opacity(target: Srgb8, opacity: f64, backdrop: Srgb8) -> Option { +pub(crate) fn source_at_opacity( + target: Srgb8, + opacity: AdmittedOpacityV1, + backdrop: Srgb8, +) -> Option { + source_at_opacity_value(target, opacity.value(), backdrop) +} + +fn source_at_opacity_value(target: Srgb8, opacity: f64, backdrop: Srgb8) -> Option { if !target_is_feasible(target, opacity, backdrop) { return None; } @@ -187,14 +196,13 @@ fn propose( minimum_opacity: AdmittedOpacityV1, backdrop: Srgb8, ) -> Result<(Srgb8, AdmittedOpacityV1), PointRepresentationProposalErrorV1> { - if let Some(source) = source_at_opacity(target, minimum_opacity.value(), backdrop) { + if let Some(source) = source_at_opacity(target, minimum_opacity, backdrop) { return Ok((source, minimum_opacity)); } - let opacity = AdmittedOpacityV1::new(first_opacity(target, backdrop)) - .map_err(|_| PointRepresentationProposalErrorV1::DerivedOpacityOutsideUnitInterval)?; + let opacity = first_opacity(target, backdrop); debug_assert!(opacity.value() > minimum_opacity.value()); - let source = source_at_opacity(target, opacity.value(), backdrop) + let source = source_at_opacity(target, opacity, backdrop) .ok_or(PointRepresentationProposalErrorV1::MissingSourceAtFirstOpacity)?; Ok((source, opacity)) } @@ -222,7 +230,6 @@ pub(crate) enum ResolvePointRepresentationErrorV1 { /// Typed отказ proposal до materialization occurrence. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum PointRepresentationProposalErrorV1 { - DerivedOpacityOutsideUnitInterval, MissingSourceAtFirstOpacity, } @@ -327,7 +334,7 @@ mod tests { }); let actual = source_at_opacity( Srgb8::new([target, backdrop, backdrop]), - alpha, + admitted(alpha), Srgb8::new([backdrop; 3]), ) .map(|tint| tint.bytes()[0]); From 52d758819eb17d859c68754bd12a5d35f8612268 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Mon, 3 Aug 2026 09:31:10 +0300 Subject: [PATCH 4/7] =?UTF-8?q?=D0=97=D0=B0=D0=BA=D1=80=D1=8B=D1=82=D1=8C?= =?UTF-8?q?=20=D0=B4=D1=80=D0=B5=D0=B9=D1=84=20=D1=82=D0=B5=D1=80=D0=BC?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2=20point=20IR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/labcolors-core/src/generic_boundary_tests.rs | 1 + crates/labcolors-core/src/point_representation.rs | 2 +- crates/labcolors-core/src/semantic.rs | 5 +++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/labcolors-core/src/generic_boundary_tests.rs b/crates/labcolors-core/src/generic_boundary_tests.rs index 86bb447f..de2831b5 100644 --- a/crates/labcolors-core/src/generic_boundary_tests.rs +++ b/crates/labcolors-core/src/generic_boundary_tests.rs @@ -76,6 +76,7 @@ fn point_representation_execution_is_generic_and_the_helper_facade_is_gone() { let semantic = compact_production_syntax(SEMANTIC_SOURCE).to_ascii_lowercase(); for removed in [ "compiledalphaanaloginvocation", + "compiledalpha-analoginvocation", "alpha_analog_invocations", "compile_alpha_analog_invocations", ] { diff --git a/crates/labcolors-core/src/point_representation.rs b/crates/labcolors-core/src/point_representation.rs index c6de3ea7..e9ba6806 100644 --- a/crates/labcolors-core/src/point_representation.rs +++ b/crates/labcolors-core/src/point_representation.rs @@ -246,7 +246,7 @@ impl PointRepresentationViolationV1 { } /// Code-owned compiled invocation: одна физическая topology и один exact -/// evaluator. Runtime bindings — только admitted bytes и alpha. +/// evaluator. Runtime bindings — только admitted bytes и opacity. pub(crate) struct ExactPointRepresentationV1; impl ExactPointRepresentationV1 { diff --git a/crates/labcolors-core/src/semantic.rs b/crates/labcolors-core/src/semantic.rs index 79f6c8f2..bd578f35 100644 --- a/crates/labcolors-core/src/semantic.rs +++ b/crates/labcolors-core/src/semantic.rs @@ -3315,7 +3315,8 @@ pub fn resolve_named_set( Some(invocation) if invocation.declaration_ordinal < declaration_ordinal => { return Err(ResolveSetError { state: ResolveSetErrorState::Internal(SolveFailure::InternalInvariant( - "compiled alpha-analog invocation order drifted behind declarations".into(), + "compiled point-representation invocation order drifted behind declarations" + .into(), )), }); } @@ -3331,7 +3332,7 @@ pub fn resolve_named_set( if point_invocations.next().is_some() { return Err(ResolveSetError { state: ResolveSetErrorState::Internal(SolveFailure::InternalInvariant( - "compiled alpha-analog invocation points outside declarations".into(), + "compiled point-representation invocation points outside declarations".into(), )), }); } From 02615ab56898c64fab450b11cf928a99274a9ebc Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Mon, 3 Aug 2026 09:39:27 +0300 Subject: [PATCH 5/7] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D0=BF=D1=80=D0=B8?= =?UTF-8?q?=D0=B2=D1=8F=D0=B7=D0=B0=D1=82=D1=8C=20cleanliness=20receipt=20?= =?UTF-8?q?=D0=BA=20source=20cone?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../contracts/clean-set-srgb8-v1/receipt-v1.json | 12 ++++++------ .../contracts/clean-set-srgb8-v1/receipt-v1.sha256 | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) 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 14865105..ee60728a 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": 87722, + "bytes": 87733, "license": "MIT", "path": "crates/labcolors-core/src/appearance.rs", "role": "appearance_executor_source", - "sha256": "66266904fc84afb03a3a774a8aaa0da23fe1e05a8b04abb14d5f18820e17a85d" + "sha256": "fecab7250ec96171daf3d6fe75c663e45b2356a2b0a8bb6f5ee418c9d62654a1" }, { "bytes": 6373, @@ -27,11 +27,11 @@ "sha256": "30c7b3c5ce7b7f876c2cf475a2815b6ac8e53b9986e471864b03de1558978d99" }, { - "bytes": 5981, + "bytes": 6046, "license": "MIT", "path": "crates/labcolors-core/src/composition.rs", "role": "composition_executor_source", - "sha256": "195a67327a3bd86d7816b634481389930bf68577bb1202fad14c2ea152df8625" + "sha256": "0497b90b5e4bf8771604758034bee81655bcd5aedd934e9fef9dfc66429b07b7" }, { "bytes": 7419, @@ -48,11 +48,11 @@ "sha256": "8bf5d5a1c0f00ce245a1ecb18b923aa1631483345962d72b633e466c242a8a1d" }, { - "bytes": 14149, + "bytes": 14163, "license": "MIT", "path": "crates/labcolors-core/src/lib.rs", "role": "module_registration_source", - "sha256": "919b46d9a838b1f5f333af938c2db7bfa20ba444e22c841def0349d93d213c38" + "sha256": "9c5493cc2d4dcca04dc989f61431e126d3f4ae973f284af0bac232059192dfed" }, { "bytes": 39439, 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 2e924cae..aa010403 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 @@ -a1cedccfe9acf5feeb9244e4c9ed3dc7ee1c1703fc6910c665dbd88443899b14 receipt-v1.json +f29c6f4fc5d814843d1f64fdb4d26cb986968a675c67d8350f8f5a0ba9a2fddb receipt-v1.json From 3ebe4027bcca6451d478510fefd22a381b696d5b Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Mon, 3 Aug 2026 09:44:11 +0300 Subject: [PATCH 6/7] =?UTF-8?q?=D0=9E=D0=BF=D1=83=D1=81=D1=82=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20WASM=20=D1=80=D0=B0=D1=82=D1=87=D0=B5=D1=82=20=D0=BF?= =?UTF-8?q?=D0=BE=20=D1=82=D0=BE=D1=87=D0=BD=D0=BE=D0=BC=D1=83=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=BC=D0=B5=D1=80=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/colors/bench/wasm.json | 8 ++++---- scripts/check-wasm-size-budget.mjs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/colors/bench/wasm.json b/packages/colors/bench/wasm.json index 27d2efdb..6ad29cd0 100644 --- a/packages/colors/bench/wasm.json +++ b/packages/colors/bench/wasm.json @@ -27,13 +27,13 @@ ] }, "measurement": { - "source": "github-actions-run-30776537125", + "source": "github-actions-run-30790916413", "platform": "linux-x64", - "rawBytes": 376675 + "rawBytes": 376554 }, "policy": { - "maxRawBytes": 376675, - "basis": "v3a-atomic-paint-input", + "maxRawBytes": 376554, + "basis": "generic-exact-point-representation", "gzip": "diagnostic-only" } } diff --git a/scripts/check-wasm-size-budget.mjs b/scripts/check-wasm-size-budget.mjs index a8ad19a5..d32d4698 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 = - "e9a9bd75a30bb3becbd3d5e0bc2900b5e88a1221aaf8bbc54abcb1c18df1323b"; + "5d61b976f770a5dd46075d7571a9f4368f07c41f6b97c844df9493edda805d0e"; const SCHEMA_VERSION = 2; const CANONICAL_ARTIFACT = "packages/colors/pkg/labcolors_bg.wasm"; From 25ae8aeaea2dc9040b617ec2e09ebb4c06932a25 Mon Sep 17 00:00:00 2001 From: Daniel from Labpics <63733699+lemone112@users.noreply.github.com> Date: Mon, 3 Aug 2026 09:57:43 +0300 Subject: [PATCH 7/7] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D0=BF=D1=80=D0=B8?= =?UTF-8?q?=D0=B2=D1=8F=D0=B7=D0=B0=D1=82=D1=8C=20point-support=20proof=20?= =?UTF-8?q?=D0=BA=20source=20cone?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../point-support-reference-surplus-q55-bps-proof-v1.json | 2 +- scripts/verify_point_support_surplus.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 6dcafa59..482836d6 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":"3cd2e3ee7c6e2a0eac228c6dc0d68c7c9b53161830df15737b254ee3169fbbfb","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":"8c6688559f108204423e9efe364dc8a108d203be07260077912b37a0b8811803","source_files":[{"kind":"compile-time-input","path":"crates/labcolors-core/contracts/wcag22-srgb8-q55-proof-v1.json","sha256":"ac59cf89503170c789223b91d775213a19d4e571ef930f2ea609fcd51b14defd"},{"kind":"compile-time-input","path":"crates/labcolors-core/contracts/wcag22-srgb8-v1.json","sha256":"b4bb7e5f17a99f2c911fdbe3da23a48b049277b796291094950f14680cc3cc7b"},{"kind":"rust-source","path":"crates/labcolors-core/src/appearance.rs","sha256":"66266904fc84afb03a3a774a8aaa0da23fe1e05a8b04abb14d5f18820e17a85d"},{"kind":"rust-source","path":"crates/labcolors-core/src/composition.rs","sha256":"195a67327a3bd86d7816b634481389930bf68577bb1202fad14c2ea152df8625"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/exact.rs","sha256":"892576a8621185352583e63dc0a1aacac32e32a8063b6fe24ae16d4ff9dce7cb"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/mod.rs","sha256":"f18bcafd7f911e84049cf9dae9673d6d6e0ed1fd50a9a7635a0ff597953b7727"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/wcag22.rs","sha256":"856093c91159d8b3faab001f2d6524d33d7b16458a5a4e98ea65f8c62ab2694c"},{"kind":"rust-source","path":"crates/labcolors-core/src/hash.rs","sha256":"f97a0fd7d6ad3162f0f1dfb326fccfb7ed40da9a8fa67a5b8a239a1ae2ae49c3"},{"kind":"rust-source","path":"crates/labcolors-core/src/lcs_occurrence.rs","sha256":"78d37406e9bdc37f126b72987c9c92b452c13b3233c0aeb0a75ed25dadb83a68"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"919b46d9a838b1f5f333af938c2db7bfa20ba444e22c841def0349d93d213c38"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"b0cfec5c9fe798abd5492260aac3caf87685f7e27f4f9628b01f386ef3f6ac7d"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"6f6a376ff036d3d65960c004e6566e1bca580f19f5bd3cd333a80b0da5b5c242"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"38c65b46a3e392b399092e8c4834d7f31acb2b91eff626d6faa1b1aaa90b52f3"},{"kind":"rust-source","path":"crates/labcolors-core/src/srgb8.rs","sha256":"6c95324eb05476f35f75375a9af0b2b4a41b8b2978c46e67d2ce1aea5adde342"},{"kind":"rust-source","path":"crates/labcolors-core/src/wcag22.rs","sha256":"7ba7864eb7e73789bad6c63c64a4dc2dcc08c2da6921375fb9564fca230c2780"},{"kind":"rust-source","path":"crates/labcolors-core/src/wcag22/kernel.rs","sha256":"c97980c1ca2c7ea9cabff9c8d2fb7282773cca180ae15948391c29c9d6196040"},{"kind":"rust-source","path":"crates/labcolors-core/src/wcag22/q55_data.rs","sha256":"af4d23d6b70c45ce6efa839e7dda4bb0a61f6aae43cb805af6fa9b29e6c3bae2"},{"kind":"rust-source","path":"crates/labcolors-core/src/wcag22_evidence.rs","sha256":"3c5a75b07254c6071a64700af208a64987d0f0ea9698eadc54a9e74585ce1f72"}],"source_negative_controls":43,"universal_algebraic_certificate":{"basis_point_scale_instantiation":10000,"domain":"integers; Q55 scale Q>0; anchor L>=D>=0; lighter monotonicity L2>=L1>D>=0; darker monotonicity L>D2>=D1>=0; current/baseline denominators b,q>0; basis-point scale B>0 instantiated as 10000; p>0; a>=0; 0<=drop_bps<=B","identities":["three explicit anchor-surplus formulas after denominator clearing","reference distance is monotone increasing in lighter L","reference distance is monotone decreasing in darker D","positive-baseline retained threshold is p*(B-drop)/(q*B)","a/b >= p*(B-drop)/(q*B) iff a*q*B >= p*(B-drop)*b"],"method":"exact-sparse-integer-polynomial-identities-plus-positive-denominator-order-lemma-v1","nonpositive_baseline_case":"max(baseline,0)=0; retained threshold is exactly zero","symbolic_mutation_controls":{"anchor_coefficients_and_denominator":6,"retained_cross_product":5},"wolfram_language_cross_check":{"query":"FullSimplify[{20 g/d - 0 == 20 g/d, 20 g/d - 2 == (20 g - 2 d)/d, 20 g/d - 7/2 == (40 g - 7 d)/(2 d), Equivalent[a/b >= p (s-x)/(q s), a q s >= p (s-x) b], Max[p/q, 0] (s-x)/s == Piecewise[{{0, p <= 0}}, p (s-x)/(q s)]}, Assumptions -> Element[{a,b,p,q,s,x,g,d}, Integers] && a >= 0 && b > 0 && q > 0 && s > 0 && 0 <= x <= s && d > 0 && g >= 0]","query_sha256":"8cdbb9964583030c8b92498961896cb2a98613f1cb31eb7c54acdf8e16beff10","result":"{True, True, True, True, True}","result_sha256":"13a8f2ee8d0fde335a638e46d7cc8a8427b9a1437c77d22cfcf925bb87fa6303"}},"verifier_sha256":"64a6c702a447c77fb82af5f886fbcd266124c7e2074b92274f84bb68f71faef9"} +{"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":"590718a64dc37c35bc3caf6c4505272f61f7ddb735e81e7e0f622ad7f0f32fb4","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":"0fb5c973261c49103ffe93eb8d1331c928a5b441ef1e7290457af3d88f34cf28","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":"fecab7250ec96171daf3d6fe75c663e45b2356a2b0a8bb6f5ee418c9d62654a1"},{"kind":"rust-source","path":"crates/labcolors-core/src/composition.rs","sha256":"0497b90b5e4bf8771604758034bee81655bcd5aedd934e9fef9dfc66429b07b7"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/exact.rs","sha256":"892576a8621185352583e63dc0a1aacac32e32a8063b6fe24ae16d4ff9dce7cb"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/mod.rs","sha256":"f18bcafd7f911e84049cf9dae9673d6d6e0ed1fd50a9a7635a0ff597953b7727"},{"kind":"rust-source","path":"crates/labcolors-core/src/constraints/wcag22.rs","sha256":"856093c91159d8b3faab001f2d6524d33d7b16458a5a4e98ea65f8c62ab2694c"},{"kind":"rust-source","path":"crates/labcolors-core/src/hash.rs","sha256":"f97a0fd7d6ad3162f0f1dfb326fccfb7ed40da9a8fa67a5b8a239a1ae2ae49c3"},{"kind":"rust-source","path":"crates/labcolors-core/src/lcs_occurrence.rs","sha256":"78d37406e9bdc37f126b72987c9c92b452c13b3233c0aeb0a75ed25dadb83a68"},{"kind":"rust-source","path":"crates/labcolors-core/src/lib.rs","sha256":"9c5493cc2d4dcca04dc989f61431e126d3f4ae973f284af0bac232059192dfed"},{"kind":"rust-source","path":"crates/labcolors-core/src/numerics.rs","sha256":"e73a12136494f2ef9aca4e943ab38302c1439f054cecab36a552d35252c164f9"},{"kind":"rust-source","path":"crates/labcolors-core/src/observation.rs","sha256":"b0cfec5c9fe798abd5492260aac3caf87685f7e27f4f9628b01f386ef3f6ac7d"},{"kind":"rust-source","path":"crates/labcolors-core/src/point_support.rs","sha256":"6f6a376ff036d3d65960c004e6566e1bca580f19f5bd3cd333a80b0da5b5c242"},{"kind":"rust-source","path":"crates/labcolors-core/src/session.rs","sha256":"38c65b46a3e392b399092e8c4834d7f31acb2b91eff626d6faa1b1aaa90b52f3"},{"kind":"rust-source","path":"crates/labcolors-core/src/srgb8.rs","sha256":"6c95324eb05476f35f75375a9af0b2b4a41b8b2978c46e67d2ce1aea5adde342"},{"kind":"rust-source","path":"crates/labcolors-core/src/wcag22.rs","sha256":"7ba7864eb7e73789bad6c63c64a4dc2dcc08c2da6921375fb9564fca230c2780"},{"kind":"rust-source","path":"crates/labcolors-core/src/wcag22/kernel.rs","sha256":"c97980c1ca2c7ea9cabff9c8d2fb7282773cca180ae15948391c29c9d6196040"},{"kind":"rust-source","path":"crates/labcolors-core/src/wcag22/q55_data.rs","sha256":"af4d23d6b70c45ce6efa839e7dda4bb0a61f6aae43cb805af6fa9b29e6c3bae2"},{"kind":"rust-source","path":"crates/labcolors-core/src/wcag22_evidence.rs","sha256":"3c5a75b07254c6071a64700af208a64987d0f0ea9698eadc54a9e74585ce1f72"}],"source_negative_controls":43,"universal_algebraic_certificate":{"basis_point_scale_instantiation":10000,"domain":"integers; Q55 scale Q>0; anchor L>=D>=0; lighter monotonicity L2>=L1>D>=0; darker monotonicity L>D2>=D1>=0; current/baseline denominators b,q>0; basis-point scale B>0 instantiated as 10000; p>0; a>=0; 0<=drop_bps<=B","identities":["three explicit anchor-surplus formulas after denominator clearing","reference distance is monotone increasing in lighter L","reference distance is monotone decreasing in darker D","positive-baseline retained threshold is p*(B-drop)/(q*B)","a/b >= p*(B-drop)/(q*B) iff a*q*B >= p*(B-drop)*b"],"method":"exact-sparse-integer-polynomial-identities-plus-positive-denominator-order-lemma-v1","nonpositive_baseline_case":"max(baseline,0)=0; retained threshold is exactly zero","symbolic_mutation_controls":{"anchor_coefficients_and_denominator":6,"retained_cross_product":5},"wolfram_language_cross_check":{"query":"FullSimplify[{20 g/d - 0 == 20 g/d, 20 g/d - 2 == (20 g - 2 d)/d, 20 g/d - 7/2 == (40 g - 7 d)/(2 d), Equivalent[a/b >= p (s-x)/(q s), a q s >= p (s-x) b], Max[p/q, 0] (s-x)/s == Piecewise[{{0, p <= 0}}, p (s-x)/(q s)]}, Assumptions -> Element[{a,b,p,q,s,x,g,d}, Integers] && a >= 0 && b > 0 && q > 0 && s > 0 && 0 <= x <= s && d > 0 && g >= 0]","query_sha256":"8cdbb9964583030c8b92498961896cb2a98613f1cb31eb7c54acdf8e16beff10","result":"{True, True, True, True, True}","result_sha256":"13a8f2ee8d0fde335a638e46d7cc8a8427b9a1437c77d22cfcf925bb87fa6303"}},"verifier_sha256":"4918fbe25cd0c5d86f5b2da92164b31dcd2655215fa9df150254d35ac1fff1c3"} diff --git a/scripts/verify_point_support_surplus.py b/scripts/verify_point_support_surplus.py index 8408275b..98667eaa 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 = ( - "8c6688559f108204423e9efe364dc8a108d203be07260077912b37a0b8811803" + "0fb5c973261c49103ffe93eb8d1331c928a5b441ef1e7290457af3d88f34cf28" ) EXPECTED_Q55_PROOF_SHA256 = ( "ac59cf89503170c789223b91d775213a19d4e571ef930f2ea609fcd51b14defd"