Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 0 additions & 21 deletions crates/labcolors-core/src/appearance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,18 +87,6 @@ pub(crate) enum CompositionProfileV1 {
EncodedSrgb8SourceOverV1,
}

/// Класс доказательства результата: точная операция объявленного профиля либо
/// охарактеризованное legacy-совместимое поведение. Классы не смешиваются:
/// occurrence, решаемый legacy-солвером, не наследует exact-статус композита.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum EvidenceClass {
/// Точный результат reference-операции в её объявленном конечном домене.
ReferenceExact,
/// Охарактеризованное текущее поведение (см. §5.2 ТЗ #307): сохраняется
/// байт-в-байт, но не объявляется новой научной истиной.
LegacyCompatibility,
}

/// Декларация поверхности: input-слой (цвет из bindings как есть) либо
/// source-over композит поверх другой поверхности.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
Expand Down Expand Up @@ -145,8 +133,6 @@ pub(crate) struct ForegroundOccurrenceSpec {
pub(crate) identity_source: ColorInputId,
/// Поверхность, против которой foreground реально стоит.
pub(crate) against: SurfaceId,
/// Класс доказательства решателя, потребляющего occurrence.
pub(crate) evidence: EvidenceClass,
}

/// Типизированные ошибки compile/evaluate. Публичный (в пределах crate) вход
Expand Down Expand Up @@ -467,9 +453,6 @@ pub(crate) struct SourceOverCertificateV1 {
pub(crate) opacity_bits: u64,
/// Финальные байты результата.
pub(crate) output_rgb: [u8; 3],
/// Класс доказательства: всегда [`EvidenceClass::ReferenceExact`] —
/// сертификат существует только для exact-профиля.
pub(crate) evidence: EvidenceClass,
}

impl SourceOverCertificateV1 {
Expand Down Expand Up @@ -509,8 +492,6 @@ pub(crate) struct ResolvedOccurrence {
pub(crate) against: SurfaceId,
/// Финальные вычисленные байты этой поверхности.
pub(crate) backdrop: [u8; 3],
/// Класс доказательства решателя-потребителя.
pub(crate) evidence: EvidenceClass,
}

/// Результат одного evaluate: байты каждой поверхности, occurrences,
Expand Down Expand Up @@ -688,7 +669,6 @@ impl CompiledAppearanceGraph {
opacity_input: opacity,
opacity_bits: alpha.to_bits(),
output_rgb,
evidence: EvidenceClass::ReferenceExact,
});
resolved[index] = Some(output_rgb);
}
Expand Down Expand Up @@ -723,7 +703,6 @@ impl CompiledAppearanceGraph {
source: color_value(spec.identity_source),
against: spec.against,
backdrop,
evidence: spec.evidence,
}
})
.collect();
Expand Down
84 changes: 63 additions & 21 deletions crates/labcolors-core/src/appearance_graph_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
use proptest::prelude::*;

use crate::appearance::{
AppearanceBindings, AppearanceGraphSpec, ColorInputId, CompositionProfileV1, EvidenceClass,
ForegroundOccurrenceSpec, GraphError, OccurrenceId, OpacityInputId, SurfaceId, SurfaceSpec,
AppearanceBindings, AppearanceGraphSpec, ColorInputId, CompositionProfileV1,
ForegroundOccurrenceSpec, GraphError, OccurrenceId, OpacityInputId, ResolvedOccurrence,
SourceOverCertificateV1, SurfaceId, SurfaceSpec,
};
use crate::solve::Floor;

Expand All @@ -22,6 +23,45 @@ const CONTEXT_SURFACE: SurfaceId = SurfaceId::new(0);
const DERIVED_SURFACE: SurfaceId = SurfaceId::new(1);
const FOREGROUND: OccurrenceId = OccurrenceId::new(0);

#[test]
fn occurrence_contract_contains_only_physical_facts() {
let graph = AppearanceGraphSpec::new(
vec![SOURCE, CONTEXT],
vec![],
vec![SurfaceSpec::Input {
id: CONTEXT_SURFACE,
color: CONTEXT,
}],
vec![ForegroundOccurrenceSpec {
id: FOREGROUND,
identity_source: SOURCE,
against: CONTEXT_SURFACE,
}],
)
.compile()
.unwrap();

let rendered = graph
.evaluate(&AppearanceBindings::new(
vec![(SOURCE, [1, 2, 3]), (CONTEXT, [4, 5, 6])],
vec![],
))
.unwrap();

let ResolvedOccurrence {
id,
identity_source,
source,
against,
backdrop,
} = *rendered.occurrence(FOREGROUND).unwrap();

assert_eq!(
(id, identity_source, source, against, backdrop),
(FOREGROUND, SOURCE, [1, 2, 3], CONTEXT_SURFACE, [4, 5, 6],)
);
}

fn atomic_component(surface_declarations_reversed: bool) -> AppearanceGraphSpec {
let context = SurfaceSpec::Input {
id: CONTEXT_SURFACE,
Expand All @@ -48,7 +88,6 @@ fn atomic_component(surface_declarations_reversed: bool) -> AppearanceGraphSpec
id: FOREGROUND,
identity_source: SOURCE,
against: DERIVED_SURFACE,
evidence: EvidenceClass::LegacyCompatibility,
}],
)
}
Expand Down Expand Up @@ -130,7 +169,6 @@ fn unrelated_opaque_handles_do_not_change_the_physics() {
id: other_occurrence,
identity_source: other_source,
against: other_derived_surface,
evidence: EvidenceClass::LegacyCompatibility,
}],
)
.compile()
Expand Down Expand Up @@ -171,7 +209,6 @@ fn graph_rejects_missing_occurrence_backdrop_and_cycles() {
id: FOREGROUND,
identity_source: SOURCE,
against: DERIVED_SURFACE,
evidence: EvidenceClass::LegacyCompatibility,
}],
)
.compile();
Expand Down Expand Up @@ -240,17 +277,27 @@ proptest! {
let certificates = rendered.certificates();
prop_assert_eq!(certificates.len(), 1);
let certificate = &certificates[0];
prop_assert_eq!(certificate.profile, CompositionProfileV1::EncodedSrgb8SourceOverV1);
prop_assert_eq!(certificate.evidence, EvidenceClass::ReferenceExact);
prop_assert_eq!(certificate.surface, DERIVED_SURFACE);
prop_assert_eq!(certificate.source_input, SOURCE);
prop_assert_eq!(certificate.source_rgb, source);
prop_assert_eq!(certificate.backdrop_surface, CONTEXT_SURFACE);
prop_assert_eq!(certificate.backdrop_rgb, context);
prop_assert_eq!(certificate.opacity_input, OPACITY);
prop_assert_eq!(certificate.opacity_bits, opacity.to_bits());
prop_assert_eq!(certificate.output_rgb, rendered.surface_rgb(DERIVED_SURFACE).unwrap());
prop_assert_eq!(certificate.replay(), Ok(certificate.output_rgb));
let SourceOverCertificateV1 {
profile,
surface,
source_input,
source_rgb,
backdrop_surface,
backdrop_rgb,
opacity_input,
opacity_bits,
output_rgb,
} = certificate;
prop_assert_eq!(*profile, CompositionProfileV1::EncodedSrgb8SourceOverV1);
prop_assert_eq!(*surface, DERIVED_SURFACE);
prop_assert_eq!(*source_input, SOURCE);
prop_assert_eq!(*source_rgb, source);
prop_assert_eq!(*backdrop_surface, CONTEXT_SURFACE);
prop_assert_eq!(*backdrop_rgb, context);
prop_assert_eq!(*opacity_input, OPACITY);
prop_assert_eq!(*opacity_bits, opacity.to_bits());
prop_assert_eq!(*output_rgb, rendered.surface_rgb(DERIVED_SURFACE).unwrap());
prop_assert_eq!(certificate.replay(), Ok(*output_rgb));
}
}

Expand Down Expand Up @@ -308,13 +355,11 @@ fn compile_rejects_duplicate_declarations_with_typed_errors() {
id: FOREGROUND,
identity_source: SOURCE,
against: CONTEXT_SURFACE,
evidence: EvidenceClass::LegacyCompatibility,
},
ForegroundOccurrenceSpec {
id: FOREGROUND,
identity_source: CONTEXT,
against: CONTEXT_SURFACE,
evidence: EvidenceClass::LegacyCompatibility,
},
],
)
Expand Down Expand Up @@ -433,7 +478,6 @@ fn compile_rejects_every_missing_reference_with_typed_errors() {
id: FOREGROUND,
identity_source: SOURCE,
against: CONTEXT_SURFACE,
evidence: EvidenceClass::LegacyCompatibility,
}],
)
.compile();
Expand Down Expand Up @@ -567,7 +611,6 @@ fn occurrence_source_follows_the_declared_identity_edge_not_the_composite_source
id: FOREGROUND,
identity_source: identity,
against: DERIVED_SURFACE,
evidence: EvidenceClass::LegacyCompatibility,
}],
)
.compile()
Expand All @@ -587,5 +630,4 @@ fn occurrence_source_follows_the_declared_identity_edge_not_the_composite_source
assert_eq!(occurrence.identity_source, identity);
assert_eq!(occurrence.source, [111, 112, 113]);
assert_ne!(occurrence.source, [10, 20, 30]);
assert_eq!(occurrence.evidence, EvidenceClass::LegacyCompatibility);
}
30 changes: 17 additions & 13 deletions crates/labcolors-core/src/semantic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -688,13 +688,13 @@ pub enum RoleSpec {
/// позиции `fill-*-primary` над фоном резолва), а НЕ против фона страницы
/// и НЕ против эмитированного [`PairFill`](Self::PairFill) — у того своя,
/// отдельно сдвинутая солид-эмиссия; ребра `PairFill → PairLabel` не
/// существует. Резолв — compatibility-адаптер над одним generic-компонентом
/// appearance-графа (#307): скомпилированный граф точно собирает
/// поверхность и возвращает foreground occurrence против неё, затем
/// оттеночный foreground решается прежним законом (`resolve_hued_anchor…`,
/// статус LegacyCompatibility) — пол поверхности гарантирован по
/// построению, тон клампится (флаг `compressed`) при недостижимости на
/// кривой семьи.
/// существует. Резолв использует один generic-компонент appearance-графа:
/// скомпилированный граф собирает поверхность и возвращает физические факты
/// foreground occurrence против неё. Доказательный статус последующего
/// резолвера граф не назначает. Дифференциальный тест закрепляет
/// эквивалентность миграционного подключения и результатов на проверяемом
/// домене, но не является независимым эталоном математики самого резолвера.
/// Тон клампится (флаг `compressed`) при недостижимости на кривой семьи.
PairLabel {
/// Пер-темный кодированный тинт-якорь семьи (как у лестницы).
tint: LadderTint,
Expand Down Expand Up @@ -2774,7 +2774,7 @@ fn nested_foreground_component() -> Result<
&'static crate::appearance::GraphError,
> {
use crate::appearance::{
AppearanceGraphSpec, CompiledAppearanceGraph, CompositionProfileV1, EvidenceClass,
AppearanceGraphSpec, CompiledAppearanceGraph, CompositionProfileV1,
ForegroundOccurrenceSpec, GraphError, SurfaceSpec,
};
static COMPONENT: std::sync::OnceLock<Result<CompiledAppearanceGraph, GraphError>> =
Expand All @@ -2801,7 +2801,6 @@ fn nested_foreground_component() -> Result<
id: NESTED_FOREGROUND,
identity_source: NESTED_SOURCE,
against: NESTED_DERIVED_SURFACE,
evidence: EvidenceClass::LegacyCompatibility,
}],
)
.compile()
Expand Down Expand Up @@ -2829,10 +2828,15 @@ fn pair_label_surface_domain_error(error: &str) -> Resolved {
/// Поверхность НЕ является эмитированным [`RoleSpec::PairFill`] — у того своя,
/// отдельно сдвинутая солид-эмиссия; никакого ребра `PairFill → PairLabel` нет.
///
/// Оттеночный foreground решается ПРЕЖНИМ законом
/// ([`resolve_hued_anchor_from_encoded_source`], статус LegacyCompatibility —
/// не новая научная истина) НА ЭТОЙ ПОВЕРХНОСТИ: её собственный
/// [`ResolveContext`] задаёт полярность/макс-контраст, поэтому WCAG-пол лейбла
/// Оттеночный foreground решается текущим
/// [`resolve_hued_anchor_from_encoded_source`] НА ЭТОЙ ПОВЕРХНОСТИ. Appearance-
/// граф не присваивает этому последующему решению доказательный статус: он
/// возвращает только source/against/backdrop. Дифференциальный тест закрепляет
/// подключение и результаты миграции на проверяемом домене, но оба пути
/// используют один резолвер и потому не образуют независимый эталон его
/// математики. Собственный [`ResolveContext`] поверхности задаёт
/// полярность/макс-контраст, поэтому
/// WCAG-пол лейбла
/// гарантирован против той подложки, на которой foreground реально стоит
/// (обычные `label-*` роли решаются против страницы, и на тинт-подложке их
/// контраст проседает — класс, который закрывает эта роль). Недостижимость пола
Expand Down
17 changes: 10 additions & 7 deletions docs/whitepaper.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,14 +209,17 @@ task #29). `RoleRecipe::PairLabel` решает лейбл штатным зак
→ прежний foreground-резолвер
```

Граф не знает клиентских имён (`PairLabel`, `Warning` и т. п.): роль
foreground/фон задаётся только топологией typed handles. Точной (Reference
exact) здесь является **только** композиция поверхности в объявленном
encoded-sRGB8 профиле; сам foreground-solve (LPC/якорная доля/семейная кривая)
остаётся статусом **LegacyCompatibility** — охарактеризованное текущее
поведение, не новая научная истина. Лейбл не несёт typography-фактов, поэтому
Граф не знает клиентских имён (`PairLabel`, `Warning` и т. п.): роль переднего
плана и фона задаётся только топологией типизированных handles. Точность
композиции определяется объявленным encoded-sRGB8 профилем и независимо
проверяемым replay-сертификатом. Вхождение переднего плана содержит только
физические факты source/against/backdrop; доказательный статус последующего
решения граф не назначает. Дифференциальный тест закрепляет подключение и
результаты миграции на проверяемом домене; оба пути используют один последующий
резолвер и не являются независимым эталоном его
математики. Лейбл не несёт typography-фактов, поэтому
никакой размер/вес текста здесь не учитывается и не обещается. Старая ручная
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
композиция заморожена как test-only differential oracle; матрица
композиция заморожена как тестовый эталон для дифференциального сравнения; матрица
5 семей × 4 режима × 6 фонов + property-тесты доказывают байт-идентичность
production-пути ей (`migration_*`,
[`pair_label_tests.rs`](../crates/labcolors-core/src/pair_label_tests.rs)).
Expand Down
7 changes: 4 additions & 3 deletions packages/colors/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -621,9 +621,10 @@ WASM и полным вызовом.
## Размер бандла

Raw-размер WASM — hard gate с append-only историей. Текущий
`bench/wasm-size-budget-v6.json` содержит exact Linux-x64 size/SHA-бюджеты с
нулевым headroom для `runtime` и `compiler`; V1–V5 остаются
неизменяемой историей прежнего единого артефакта. Release-equivalent CI требует
`bench/wasm-size-budget-v7.json` содержит exact Linux-x64 size/SHA-бюджеты с
нулевым headroom для `runtime` и `compiler`; checker выбирает текущую версию, а
все предыдущие versioned-файлы остаются неизменяемой историей.
Release-equivalent CI требует
точного совпадения обеих ролей и их рецептов сборки. На других host-платформах
checker сообщает только raw/gzip/SHA-диагностику и не выдаёт локальные байты за
канонический release artifact.
Expand Down
54 changes: 54 additions & 0 deletions packages/colors/bench/wasm-size-budget-v7.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"schemaVersion": 5,
"budgetId": "labcolors-wasm-roles-issue-307-c7a-v7",
"predecessor": {
"path": "packages/colors/bench/wasm-size-budget-v6.json",
"fileSha256": "761af6050031169dac7eafdfadb2db9bbb2023b96ed5ba9d3c5dc966ffeafb32"
},
"toolchainSource": {
"path": "packages/colors/bench/wasm-size-budget-v1.json",
"fileSha256": "4f7340fc8cfd0ccb97377c385f2f8d8e7a9ef2c5ba96177f518c5d07de2825e1"
},
"buildRecipes": {
"runtime": {
"command": "CARGO_ENCODED_RUSTFLAGS=<rustPathRemap> wasm-pack build crates/labcolors-wasm --release --target web --out-dir ../../packages/colors/pkg --out-name labcolors --locked",
"recipeSha256": "0ea74cb070e0a5facb7280f6124930a0bb673ee4dcee9c99fff110db6c9389d4"
},
"compiler": {
"command": "CARGO_ENCODED_RUSTFLAGS=<rustPathRemap> wasm-pack build crates/labcolors-compiler --release --target web --out-dir ../../packages/colors/compiler --out-name labcolors_compiler --locked",
"recipeSha256": "ce53cea5f579c512a6d2f0c3348f250ac0a5e03206de55e7979c8eae1403be8f"
}
},
"roles": {
"runtime": {
"artifact": "packages/colors/pkg/labcolors_bg.wasm",
"measurement": {
"issue": 307,
"slice": "C7a",
"measurementPlatform": "linux-x64",
"rawBytes": 454334,
"sha256": "e052ff2413d6da57e745342b45abe9af3719994deface73db19faeba84af91b5"
},
"policy": {
"maxRawBytes": 454334,
"derivation": "exact-accepted-issue-307-slice-c7a-runtime-measurement",
"gzip": "diagnostic-only"
}
},
"compiler": {
"artifact": "packages/colors/compiler/labcolors_compiler_bg.wasm",
"measurement": {
"issue": 296,
"slice": "C3",
"measurementPlatform": "linux-x64",
"rawBytes": 229658,
"sha256": "34e2a561862ee06d52d1104f8ba60ccf9967e2e4fd09803d4e75e1966074bc8d"
},
"policy": {
"maxRawBytes": 229658,
"derivation": "exact-accepted-issue-296-slice-c3-compiler-measurement",
"gzip": "diagnostic-only"
}
}
}
}
Loading
Loading