Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
39 changes: 36 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -466,15 +466,48 @@ jobs:
run: |
cargo install wasm-pack --version 0.13.1 --locked
echo "$CARGO_HOME/bin" >> "$GITHUB_PATH"
- name: wasm-pack build (runtime + compiler release roles)
- name: reproduce WASM roles from the same source
# Each execution role is a separate Cargo root and physical artifact;
# building them in separate invocations prevents feature unification.
# Rust error locations otherwise embed the self-hosted runner's mutable
# workspace/CARGO_HOME roots and make identical source hash differently.
# The second build follows `cargo clean`, so equality proves same-source
# reproduction instead of comparing unrelated source commits by SHA.
run: |
export CARGO_ENCODED_RUSTFLAGS="--remap-path-prefix=$GITHUB_WORKSPACE=/workspace/lab-colors"$'\x1f'"--remap-path-prefix=$CARGO_HOME=/cargo-home"
wasm-pack build crates/labcolors-wasm --release --target web --out-dir ../../packages/colors/pkg --out-name labcolors --locked
wasm-pack build crates/labcolors-compiler --release --target web --out-dir ../../packages/colors/compiler --out-name labcolors_compiler --locked
build_roles() {
wasm-pack build crates/labcolors-wasm --release --target web --out-dir ../../packages/colors/pkg --out-name labcolors --locked
wasm-pack build crates/labcolors-compiler --release --target web --out-dir ../../packages/colors/compiler --out-name labcolors_compiler --locked
}

rm -rf packages/colors/pkg packages/colors/compiler
build_roles

first="$RUNNER_TEMP/wasm-first-$GITHUB_JOB"
rm -rf "$first"
mkdir -p "$first"
cp -a packages/colors/pkg "$first/pkg"
cp -a packages/colors/compiler "$first/compiler"

cargo clean
rm -rf packages/colors/pkg packages/colors/compiler
build_roles

diff --no-dereference --recursive "$first/pkg" packages/colors/pkg
diff --no-dereference --recursive "$first/compiler" packages/colors/compiler

for wasm in \
packages/colors/pkg/labcolors_bg.wasm \
packages/colors/compiler/labcolors_compiler_bg.wasm
do
if LC_ALL=C grep -a -q -E '/(Users|home)/[^[:cntrl:]]*/\.cargo/registry/src/|/opt/actions-runner/[^[:cntrl:]]*/cargo-wasm/registry/src/' "$wasm"; then
echo "unmapped build path in $wasm" >&2
exit 1
fi
done
sha256sum \
packages/colors/pkg/labcolors_bg.wasm \
packages/colors/compiler/labcolors_compiler_bg.wasm
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: ${{ env.NODE_TOOLCHAIN }}
Expand Down
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
Loading
Loading