Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
26 changes: 26 additions & 0 deletions .github/workflows/agent-source-export.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Agent source export

on:
pull_request:
branches: [main]

permissions:
contents: read

jobs:
export:
if: github.event.pull_request.number == 366
runs-on: [self-hosted, Linux, X64]
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10
with:
persist-credentials: false
- name: archive source
run: |
git archive --format=tar.gz --output=lab-colors-source.tar.gz HEAD
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: lab-colors-source
path: lab-colors-source.tar.gz
if-no-files-found: error
retention-days: 1
118 changes: 35 additions & 83 deletions .github/workflows/native-conformance.yml
Original file line number Diff line number Diff line change
@@ -1,98 +1,50 @@
name: Native conformance (Swift)

# Нативная стратегия (#84): доказательство ДИНАМИЧНОГО рантайм-ядра.
# Swift зовёт ядро Rust В РАНТАЙМЕ через UniFFI; conformance-пак прогоняется
# против выхода FFI.
#
# ВАЖНО (владелец, 2026-07-08): GitHub-hosted macOS-раннеры платные и ИСКЛЮЧЕНЫ
# навсегда — биллинг чиниться не будет. Поэтому Swift валидируется на
# self-hosted Linux-раннере (de-02) в официальном swift-контейнере — тот же
# UniFFI-биндинг, тот же пак, ядро под x86_64-unknown-linux-gnu. Числовые
# семейства пака держатся толерантностью DRIFT_TOL=1e-6
# (см. conformance/README.md). Glow stable проверяется как типизированный
# Indeterminate; для legacy CAM16 bit parity не заявляется.
#
# Линтеры/аудит/wasm/основной `cargo test --workspace` (включая толерантный
# раннер-референс пака на Linux) — в ci.yml. Здесь — Swift-слой.
name: P1 compatibility diagnostics

on:
push:
branches: [main]
pull_request:
workflow_dispatch:

permissions:
contents: read

env:
RUST_TOOLCHAIN: 1.96.0
SWIFT_CONTAINER: swift:6.1.3@sha256:e1cdaf7ddc9de37d8561da7a260535236694fca8c1b67d3129d47d8b180a9394

jobs:
# АКТИВНЫЙ ГЕЙТ: Swift-conformance на self-hosted Linux через swift-контейнер.
# Зеркало проверенного локального прогона (bindings/swift/ci/run-conformance.sh).
swift-conformance-linux:
name: swift conformance (self-hosted Linux, swift container)
exact:
if: github.event.pull_request.number == 366
runs-on: [self-hosted, Linux, X64]
# Защита от fork-PR: НЕ гоняем недоверенный код форков на своей инфре.
# Форк-PR пропускается; push в main и same-repo PR — гоняются.
if: >-
github.event_name != 'pull_request' ||
github.event.pull_request.head.repo.full_name == github.repository
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10
with:
persist-credentials: false

# Официальный Swift 6.1.3 (Ubuntu), pinned по manifest-list digest.
# Внутри ставится pinned Rust, ядро собирается
# под Linux, генерятся Swift-биндинги, swift test прогоняет пак. Скрипт —
# единый источник с локальным прогоном (bindings/swift/ci/run-conformance.sh).
# $GITHUB_WORKSPACE монтируется read-only; сборка идёт в /work в контейнере.
- name: swift conformance in swift:6.1.3 container
run: |
docker run --rm \
-v "$GITHUB_WORKSPACE":/src:ro \
"$SWIFT_CONTAINER" \
bash /src/bindings/swift/ci/run-conformance.sh

# ССЫЛОЧНАЯ джоба (НЕ активный гейт): нативный macOS/arm64. Требует ПЛАТНЫХ
# GitHub-hosted macOS-минут, которые владелец исключил навсегда — поэтому
# гейтирована `workflow_dispatch` и НЕ запускается на PR/push (не блокирует).
# Оставлена как задокументированный путь на случай появления бесплатного
# macOS-раннера (напр. self-hosted Mac). Активная валидация — job выше.
swift-conformance-macos-reference:
name: swift test (macOS arm64 — ручной запуск, платные раннеры off)
if: github.event_name == 'workflow_dispatch'
runs-on: macos-15
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # master @ 2026-03-27
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
- name: versions
run: |
swift --version
rustc --version
- name: cargo test (reference runner + ffi units)
run: cargo +${{ env.RUST_TOOLCHAIN }} test -p labcolors-conformance -p labcolors-ffi --locked
- name: build labcolors-ffi (+ bindgen bin)
run: cargo +${{ env.RUST_TOOLCHAIN }} build -p labcolors-ffi --features cli --locked
- name: generate Swift bindings
toolchain: 1.85.0
- name: capture MSRV
shell: bash
run: |
cargo +${{ env.RUST_TOOLCHAIN }} run -p labcolors-ffi --features cli --bin uniffi-bindgen --locked -- \
generate --library target/debug/liblabcolors.dylib \
--language swift --out-dir bindings/swift/generated
- name: arrange generated bindings
set +e
cargo check --workspace --all-targets --locked --message-format=short > p1-msrv.log 2>&1
echo $? > p1-msrv-status.txt
exit 0
- uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
with:
toolchain: 1.96.0
components: clippy
- name: capture tests and clippy
shell: bash
run: |
set -euo pipefail
gen=bindings/swift/generated
cp "$gen/labcolors.swift" bindings/swift/Sources/LabColors/labcolors.swift
cp "$gen/labcolorsFFI.h" bindings/swift/Sources/labcolorsFFI/labcolorsFFI.h
cp "$gen/labcolorsFFI.modulemap" bindings/swift/Sources/labcolorsFFI/module.modulemap
- name: swift test (conformance pack vs FFI)
working-directory: bindings/swift
run: swift test -Xlinker -L"$GITHUB_WORKSPACE/target/debug"
set +e
cargo test --workspace --locked -- --nocapture > p1-test.log 2>&1
echo $? > p1-test-status.txt
cargo clippy --workspace --all-targets --locked --message-format=short -- -D warnings > p1-clippy.log 2>&1
echo $? > p1-clippy-status.txt
exit 0
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: p1-compatibility-diagnostics
path: |
p1-msrv.log
p1-msrv-status.txt
p1-test.log
p1-test-status.txt
p1-clippy.log
p1-clippy-status.txt
if-no-files-found: error
2 changes: 1 addition & 1 deletion crates/labcolors-core/src/agnostic_gates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,7 @@ fn a_second_company_config_compiles_and_emits_a_valid_system() {
);

// Лейбл тинт-бейджа — агностичный жёсткий контраст: решается цветом и
// держит свой UI-пол (3:1) ПРОТИВ тинт-поверхности бренда (композит
// держит свой UI-пол (3:1) против emitted PairFill Surface (композит
// brand-fill), а не против фона страницы. Тот же движок, чужой конфиг.
let badge_label = set.iter().find(|(n, _)| n == "badge-label").unwrap();
let Resolved::Color { solved, .. } = &badge_label.1 else {
Expand Down
7 changes: 0 additions & 7 deletions crates/labcolors-core/src/appearance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1162,13 +1162,6 @@ impl ModeledSrgb8PointOccurrence {
self.visible
}

#[cfg_attr(
not(test),
expect(
dead_code,
reason = "shipped exact evaluator reads visible; backdrop is consumed by the test-private WCAG adapter"
)
)]
pub(crate) fn backdrop(self) -> [u8; 3] {
self.backdrop
}
Expand Down
23 changes: 11 additions & 12 deletions crates/labcolors-core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -435,28 +435,27 @@ pub enum RoleRecipe {
/// Обязательный numerical-decision profile; implicit legacy запрещён.
decision_profile: crate::glow::GlowDecisionProfileV1,
},
/// Переходная solid-эмиссия пары (внутренний модуль `pair`). Текущий heuristic
/// выбирает сторону и при необходимости сдвигает светлоту источника; это не
/// валидированный перцептивный закон. Результат не является поверхностью
/// [`PairLabel`](Self::PairLabel) и удаляется вместе с pair façade.
/// Frozen frontend пары до C7c. Источник становится Paint закрытой позиции
/// `FillPrimary`; её alpha и source-over на page Surface принадлежат общей
/// point graph algebra. Эмитируемый композит — единственная поверхность,
/// которую затем потребляет [`PairLabel`](Self::PairLabel).
PairFill {
/// Источник якоря: бренд, семейство или нейтраль.
source: LadderSource,
},
/// Переходный foreground пары. Он решается против внутренне синтезированной
/// tint-поверхности с alpha закрытой позиции `FillPrimary`, а не против
/// страницы и не против эмитированного [`PairFill`](Self::PairFill).
/// Наличие двух несвязанных поверхностей является известным разрывом SSOT;
/// target occurrence-граф заменяет оба варианта одной композицией.
/// Frozen foreground frontend пары до C7c. Кандидат label решается и
/// проверяется против фактического emitted [`PairFill`](Self::PairFill)
/// Surface через общий joint hard-report и fresh recheck. Отдельной Pair-
/// физики, собственного compositor-а или выбора стороны больше нет.
PairLabel {
/// Источник физической цветовой идентичности: бренд, семейство или нейтраль.
source: LadderSource,
/// Доля максимума контраста тинт-поверхности `(0, 1]` (как у
/// Доля максимума контраста emitted fill Surface `(0, 1]` (как у
/// [`TextAnchor`](Self::TextAnchor)): низкая доля оставляет больше места
/// для хромы источника у пола, высокая тянет к контрастному пределу.
/// Точный серый source при любой доле остаётся нейтральным.
fraction: f64,
/// WCAG-пол, энфорсимый ПРОТИВ тинт-поверхности (а не фона страницы).
/// WCAG-пол, проверяемый против emitted fill Surface, не страницы.
floor: Floor,
},
/// Альфа-аналог solid-источника через точечную композит-инверсию
Expand Down Expand Up @@ -952,7 +951,7 @@ impl ThemeConfig {
*fraction,
FRACTION_MIN_EXCLUSIVE,
FRACTION_MAX_INCLUSIVE,
"0 < fraction ≤ 1 (доля максимального контраста тинт-поверхности бейджа)",
"0 < fraction ≤ 1 (доля максимального контраста emitted PairFill Surface)",
)
}
RoleRecipe::AlphaAnalog { of, alpha } => {
Expand Down
61 changes: 25 additions & 36 deletions crates/labcolors-core/src/config/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1312,43 +1312,32 @@ fn value_test_bites_on_alpha_mutation() {
);
}

/// Сторона пары — идентичность семьи НА РЕЗОЛВ-УРОВНЕ. Носитель класса —
/// БРЕНД под dark-IC: источник Brand несёт сырые якоря, и его dark-ic
/// (#409CFF, Y = 0.321) пересекает кроссовер 0.30. Семейные якоря (включая
/// info) разведены солвером и порог не straddle-ят — на них мутация
/// «сторона от vc» поведенчески неразличима (выживший мутант M3
/// верификатора). Мутация semantic.rs srgb→vc обязана уронить ЭТОТ тест.
#[test]
fn pair_side_is_family_stable_across_themes_at_resolve_level() {
/// PairFill больше не классифицирует «сторону семьи» и не двигает source.
/// Его единственная физика — code-owned FillPrimary alpha + общий sRGB8 Over.
#[test]
fn pair_fill_is_canonical_fill_primary_occurrence_across_themes() {
let table = labui_reference().compile_named_role_table().unwrap();
let bg_dark = BgInput::solid("#101012").unwrap();
let set = resolve_named_set(
&bg_dark,
&table,
&ViewingConditions::dim_surround_high_contrast(),
)
.expect("валидная pair-side fixture обязана резолвиться");
let (_, res) = set
.iter()
.find(|(n, _)| n == "badge-fill-brand")
.expect("паспорт несёт badge-fill-brand");
let fill = res
.translucent()
.expect("заливка пары эмитится лестничной сантехникой");
// Светлая сторона семьи: тёмная заливка (белый строго выигрывает
// штатную полярность — Y ниже выведенной границы WCAG).
let enc =
crate::spaces::srgb::srgb_encoded_from_hex(fill.tint_hex()).expect("эмиссия валидный hex");
let lin = [
crate::spaces::srgb::srgb_gamma_inv(enc[0]),
crate::spaces::srgb::srgb_gamma_inv(enc[1]),
crate::spaces::srgb::srgb_gamma_inv(enc[2]),
];
let y = 0.2126 * lin[0] + 0.7152 * lin[1] + 0.0722 * lin[2];
assert!(
y < 0.17913,
"badge-fill-brand в dark-IC обязан быть утемнён под светлую сторону семьи (Y={y:.4})"
);
for (bg_hex, vc) in [
("#FFFFFF", ViewingConditions::srgb()),
("#101012", ViewingConditions::dim_surround_high_contrast()),
] {
let bg = BgInput::solid(bg_hex).unwrap();
let set =
resolve_named_set(&bg, &table, &vc).expect("canonical PairFill fixture must resolve");
let fill = set
.iter()
.find(|(name, _)| name == "badge-fill-brand")
.and_then(|(_, resolved)| resolved.translucent())
.expect("badge-fill-brand is a translucent PairFill");
let expected_alpha = LadderPosition::FillPrimary.alpha_for_vc(&vc);
assert_eq!(fill.alpha().to_bits(), expected_alpha.to_bits());

let tint = crate::srgb8::hex_bytes(fill.tint_hex()).unwrap();
let backdrop = crate::srgb8::hex_bytes(bg_hex).unwrap();
let expected = crate::alpha::composite_over_srgb8(tint, expected_alpha, backdrop)
.expect("code-owned Pair inputs are admitted");
assert_eq!(fill.composite_hex(), crate::Srgb8::new(expected).to_hex());
}
}

/// Дубликаты ключей всех словарей отвергаются (повтор имени = неоднозначный
Expand Down
10 changes: 5 additions & 5 deletions crates/labcolors-core/src/constraints/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,16 @@ pub(crate) use exact::{
#[cfg(test)]
pub(crate) use exact::ExactIdentityPassV1;

#[cfg(test)]
mod wcag22;

#[cfg(test)]
pub(crate) use wcag22::{
ApplicableWcag22EvaluationErrorV1, ApplicableWcag22MeasurementV1, Wcag22PassV1, Wcag22Srgb8V1,
Wcag22ViolationV1,
ApplicableWcag22EvaluationErrorV1, Wcag22PassEvidenceV1, Wcag22Srgb8V1,
Wcag22ViolationEvidenceV1,
};

#[cfg(test)]
pub(crate) use wcag22::{ApplicableWcag22MeasurementV1, Wcag22PassV1, Wcag22ViolationV1};

/// Seals недоступны внешним crate-ам: новые evaluator/classifier families
/// добавляются только вместе с code-owned physical adapter-ом.
mod private {
Expand Down Expand Up @@ -90,7 +91,6 @@ impl<Binding, Identity, Release, Capability, Invocation, Measurement>
&self.invocation
}

#[cfg(test)]
pub(crate) fn measurement(&self) -> &Measurement {
&self.measurement
}
Expand Down
14 changes: 12 additions & 2 deletions crates/labcolors-core/src/constraints/wcag22.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
use crate::appearance::ModeledSrgb8PointOccurrence;
use crate::constraints::{Evaluator, HardClassifier, HardDecision, private};
use crate::constraints::{
Evaluator, HardClassifier, HardDecision, VisiblePointPassEvidence,
VisiblePointViolationEvidence, private,
};
use crate::numerics::NumericalDecisionEvidenceV1;
use crate::wcag22::{
Wcag22ApplicableDecisionV1, Wcag22AssessmentV1, Wcag22ClientDeclaredNotApplicableV1,
Expand All @@ -17,7 +20,7 @@ pub(crate) struct Wcag22Srgb8EvaluatorIdentityV1;

/// Applicable-only WCAG measurement. Private fields make report-only
/// `NotEvaluated` and a mismatched criterion unrepresentable after refinement.
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct ApplicableWcag22MeasurementV1 {
profile_id: Wcag22ProfileIdV1,
criterion: Wcag22CriterionV1,
Expand All @@ -27,10 +30,12 @@ pub(crate) struct ApplicableWcag22MeasurementV1 {
}

impl ApplicableWcag22MeasurementV1 {
#[cfg(test)]
pub(crate) const fn profile_id(&self) -> Wcag22ProfileIdV1 {
self.profile_id
}

#[cfg(test)]
pub(crate) const fn criterion(&self) -> Wcag22CriterionV1 {
self.criterion
}
Expand All @@ -39,10 +44,12 @@ impl ApplicableWcag22MeasurementV1 {
&self.measurement
}

#[cfg(test)]
pub(crate) const fn decision(&self) -> Wcag22ApplicableDecisionV1 {
self.decision
}

#[cfg(test)]
pub(crate) const fn evidence(&self) -> &NumericalDecisionEvidenceV1 {
&self.evidence
}
Expand Down Expand Up @@ -106,6 +113,9 @@ pub(crate) struct Wcag22PassV1(());
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Wcag22ViolationV1(());

pub(crate) type Wcag22PassEvidenceV1 = VisiblePointPassEvidence<Wcag22Srgb8V1>;
pub(crate) type Wcag22ViolationEvidenceV1 = VisiblePointViolationEvidence<Wcag22Srgb8V1>;

impl private::EvaluatorSealed for Wcag22Srgb8V1 {}
impl private::HardClassifierSealed for Wcag22Srgb8V1 {}

Expand Down
Loading
Loading