Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
4 changes: 4 additions & 0 deletions .cargo/mutants.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ examine_globs = [
"crates/labcolors-core/src/numerical_plan.rs",
"crates/labcolors-core/src/numerics.rs",
"crates/labcolors-core/src/pair.rs",
"crates/labcolors-core/src/srgb8.rs",
"crates/labcolors-core/src/wcag22.rs",
"crates/labcolors-core/src/wcag22_evidence.rs",
"crates/labcolors-core/src/wcag22/kernel.rs",
]

# Запас времени на мутант: legacy proxy coordinate считается быстро, но набор в целом не
Expand Down
21 changes: 12 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,8 @@ jobs:
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
- name: cargo test
run: cargo test --workspace --locked
- name: prove WCAG22 sRGB8 Q55 artifact over the full finite domain
run: python3 scripts/verify_wcag22_q55.py

audit:
name: cargo audit (rustsec)
Expand Down Expand Up @@ -315,12 +317,21 @@ jobs:
- name: wasm-pack build (release)
# The release bundle: panic=abort, no panic hook, wasm-opt -Oz. This is
# the artifact @labpics/colors ships, so the size measured below is real.
run: wasm-pack build crates/labcolors-wasm --release --target web --out-dir ../../packages/colors/pkg --out-name labcolors --locked
# Rust error locations otherwise embed the self-hosted runner's mutable
# workspace/CARGO_HOME roots and make identical source hash differently.
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
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: ${{ env.NODE_TOOLCHAIN }}
cache: npm
cache-dependency-path: packages/colors/package-lock.json
- name: enforce measured WASM raw-byte budget
# This Linux x64 release-equivalent build is the canonical Issue #284
# artifact: the checker binds its exact SHA-256 and raw-byte ceiling,
# without invented headroom. gzip is transport diagnostics only.
run: node scripts/check-wasm-size-budget.mjs
- name: "@labpics/colors: typecheck + runtime tests"
# Now that pkg/ exists (built above), the package's public types resolve
# against the *real* wasm-bindgen declarations — so the type-level smoke
Expand Down Expand Up @@ -459,14 +470,6 @@ jobs:
# Chrome is provisioned above; the parity smoke runs against native
# resolve_set inside the same wasm runtime. D1 default from the chapter.
run: wasm-pack test --headless --chrome --chromedriver "$CHROMEDRIVER_PATH" crates/labcolors-wasm --locked
- name: report bundle size (gzip)
# Size is part of the perf story (perf-bench). Not a hard gate yet — a
# budget gate lands with perf-bench; here we surface the number in logs.
run: |
wasm="packages/colors/pkg/labcolors_bg.wasm"
glue="packages/colors/pkg/labcolors.js"
echo "wasm: raw=$(wc -c <"$wasm")B gzip=$(gzip -9 -c "$wasm" | wc -c)B"
echo "glue: raw=$(wc -c <"$glue")B gzip=$(gzip -9 -c "$glue" | wc -c)B"
docs-drift:
name: docs-drift (нейминг-канон)
runs-on: [self-hosted, Linux, X64]
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ target/
packages/colors/.release/
packages/colors/LICENSE
packages/colors/build-metadata.json
packages/colors/evidence/

# Психофизический харнесс: сгенерированные манифесты/раннеры/экспорты сессий —
# это артефакты запуска (детерминированы из seed), в гит не коммитятся.
Expand Down
40 changes: 32 additions & 8 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,23 @@ Rust различаются, потому что это разные delivery su

## [Unreleased]

Атомарная numerical-decision граница (#292). Wire/npm JSON, эмитируемые цвета,
config fingerprint и `packDigest` conformance-векторов НЕ изменились; breaking
только Rust API. Migration-note: [exact alpha / typed
Glow](docs/migrations/exact-alpha-glow.md), дополнение ADR-0004 от 2026-07-12.
Атомарная numerical-decision граница (#292) и exact WCAG 2.2 evaluator для
финальной sRGB8-пары (#284). Существующая цветовая эмиссия, config fingerprint
и adaptive runtime не меняются, но Rust/npm capability API и conformance pack
изменены; следующий release обязан получить согласованный 0.x version bump.
Migration-note: [exact alpha / typed Glow](docs/migrations/exact-alpha-glow.md),
дополнение ADR-0004 от 2026-07-12.

### Added

- Versioned `evaluate_wcag22_srgb8` / `evaluate_wcag22_hex` и эквивалентные
WASM/TypeScript/UniFFI границы. Criterion всегда объявляет клиент; verdict
возвращает точное terminal evidence без epsilon или округлённого ratio.
- Canonical Q55 artifact и независимый verifier: Decimal directed-rounding +
integer tightness для 768 строк, полный scan всех `16 777 216` sRGB8-цветов,
source bindings и SHA-256 live typed registry admission-row.
- npm package несёт byte-exact profile/table/proof в `evidence/`; release
verifier и clean-install gate повторно проверяют их хэши и содержимое.

### Breaking (Rust API)

Expand All @@ -23,18 +36,29 @@ Glow](docs/migrations/exact-alpha-glow.md), дополнение ADR-0004 от 2
- `NumericalDecisionEvidenceV1::BitExact` запечатан (приватное поле-печать):
внешний код матчит только с `..`, минт выполняет registry-owned конструктор
(закреплено compile-fail тестом).
- Все terminal-варианты `NumericalDecisionV1<T>` и
`GlowDecisionOutcomeV1` запечатаны variant-level `#[non_exhaustive]`:
подлинное evidence одного site нельзя переупаковать как результат другого;
внешний match обязан использовать `..`.
- Временный capability V1 заменён единственным public
`NumericalCapabilityManifestV2`; WCAG site несёт artifact/bound/proof IDs.
Published `@labpics/colors` 0.10.0 остаётся неизменным, а следующий npm
release должен мигрировать на V2 явно.
- `RoleSpec::Glow` несёт typed execution mode
(`NumericalExecutionModeV1::StableOnly` |
`ExplicitCompatibility { release_id }`); строковый `GlowDecisionProfileV1`
остался boundary-адаптером, прежние wire keys (`stable-v1`,
`legacy-platform-dependent-v1`, `bit-exact`) сохранены byte-for-byte.
- Raw WCAG profile/proof JSON больше не поля runtime-профиля: используйте
`Wcag22ProfileV1::source_json()` / `proof_json()`. Это позволяет linker-у не
включать отдельно поставляемые evidence-документы в WASM.

### Changed

- Conformance pack 3.0.0: `manifest.numericalSites` заменён typed
`numericalCapabilities` — capability manifest ядра (schema v1, coverage
`migrated-sites-only-v1`, FNV-1a-32 drift-checksum над canonical
length-prefixed preimage). Векторные семейства и `packDigest` не изменились.
- Conformance pack 4.0.0 добавляет `wcag22.json`; `packDigest` закономерно
изменён. `manifest.numericalCapabilities` зеркалит single public V2 core
manifest (coverage `migrated-sites-only-v1`, FNV-1a-32 drift-checksum над
canonical length-prefixed preimage).
- Release manifest schema v2: секция `numericalSites` заменена на
`numericalCapabilities`; release verifier и Swift conformance-тесты
пересчитывают capability checksum независимо от Rust-кода.
Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,9 @@ CSS-строка, CSSOM и обход DOM сами по себе не повыш
registry с независимо пересчитываемым drift-checksum — входит в conformance
manifest. Он описывает возможности сборки и не повышает незарегистрированный
или explicit `Compatibility`-результат до determinate только потому, что тесты
на одной платформе зелёные.
на одной платформе зелёные. До появления внешних клиентов capability-контракт
исправлен атомарно: единственный `numericalCapabilityManifest()` возвращает
proof-capable schema V2; промежуточный public V1 и второй V2-entrypoint удалены.

## Источники и производные значения

Expand Down Expand Up @@ -216,7 +218,8 @@ anchors
- Нормативный floor применяется только там, где его требует контракт клиента или компонента.
- Core не определяет размер текста, essentialness, disabled/decorative status по имени роли.
- Экспериментальный LPC/APCA-shaped или appearance-результат не меняет WCAG pass/fail.
- До миграции на единый versioned WCAG 2.2 profile старое поле `wcagRatio` нельзя автоматически рекламировать как `Wcag22`.
- Для финальной пары sRGB8 новый `wcag22-srgb8-contrast-v1` принимает явно объявленный критерий и возвращает строгий `Pass | Fail`; профиль, Q55-артефакт и full-domain proof входят в релиз.
- Старое поле `wcagRatio` остаётся compatibility-диагностикой текущего resolver/runtime и не может автоматически рекламироваться как результат нового evaluator-а.
- Цвет не должен быть единственным носителем смысла; текст, иконка и форма принадлежат компоненту.

## Runtime и фон
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ final class ConformanceTests: XCTestCase {
}
}

func wcag22Criterion(_ key: String) -> Wcag22Criterion {
switch key {
case "sc-1.4.3-text-default": return .sc143TextDefault
case "sc-1.4.3-text-large-scale": return .sc143TextLargeScale
case "sc-1.4.11-ui-component-or-state": return .sc1411UiComponentOrState
case "sc-1.4.11-graphical-object": return .sc1411GraphicalObject
default: fatalError("unknown WCAG22 criterion in pack: \(key)")
}
}

func channels(_ hex: String) -> [Int] {
let s = hex.hasPrefix("#") ? String(hex.dropFirst()) : hex
precondition(s.count == 6, "ожидался #RRGGBB, получено \(hex)")
Expand Down Expand Up @@ -118,16 +128,16 @@ final class ConformanceTests: XCTestCase {
/// preimage ядра (labcolors-core/src/numerics.rs): length-prefixed (u32 LE
/// длина + байты) домен-сепаратор, u32 LE schema version, coverage key,
/// u32 LE счётчик sites (сортировка по сырым UTF-8 байтам siteId), на site —
/// siteId и шесть списков ключей; каждый список: u32 LE count (явный и для
/// siteId и семь списков ключей; каждый список: u32 LE count (явный и для
/// пустого) + отсортированные length-prefixed ключи. Кодирование повторено
/// здесь НАМЕРЕННО: тест — оракул, он не должен переиспользовать encoder,
/// который проверяет.
func testCapabilityManifestChecksumRecomputes() throws {
let manifest = try load("manifest.json", as: Manifest.self)
let caps = manifest.numericalCapabilities
// Оракул реализует canonical preimage V1: другая версия схемы обязана
// Оракул реализует canonical preimage V2: другая версия схемы обязана
// падать здесь, а не молча проходить с пересчитанным checksum.
XCTAssertEqual(caps.schemaVersion, 1, "неподдерживаемая версия capability-схемы")
XCTAssertEqual(caps.schemaVersion, 2, "неподдерживаемая версия capability-схемы")
XCTAssertEqual(caps.coverage, "migrated-sites-only-v1", "coverage capability manifest")
XCTAssertFalse(caps.sites.isEmpty, "capability manifest без единого migrated site пуст")
for site in caps.sites {
Expand Down Expand Up @@ -161,7 +171,7 @@ final class ConformanceTests: XCTestCase {
for key in sorted { pushLenPrefixed(key) }
}

pushLenPrefixed("labcolors.numerical-capability.v1")
pushLenPrefixed("labcolors.numerical-capability.v2")
pushU32LE(caps.schemaVersion)
pushLenPrefixed(caps.coverage)
let sites = caps.sites.sorted {
Expand All @@ -175,6 +185,7 @@ final class ConformanceTests: XCTestCase {
pushSortedKeyList(site.evidenceClasses)
pushSortedKeyList(site.artifactIds)
pushSortedKeyList(site.boundIds)
pushSortedKeyList(site.proofIds)
pushSortedKeyList(site.runtimeAttestations)
}

Expand Down Expand Up @@ -422,6 +433,41 @@ final class ConformanceTests: XCTestCase {
XCTAssertEqual(got, v.score, accuracy: Self.driftTol, "muddiness \(v.hex)")
}
}


// MARK: - Exact WCAG 2.2 final-sRGB8 assessment

func testWcag22() throws {
let vectors = try load("wcag22.json", as: [Wcag22Vec].self)
XCTAssertFalse(vectors.isEmpty)
for vector in vectors {
let got = try evaluateWcag22(
foreground: vector.foreground,
background: vector.background,
criterion: wcag22Criterion(vector.criterion))
XCTAssertEqual(got.profileId, vector.profileId)
XCTAssertEqual(got.criterion, wcag22Criterion(vector.criterion))
XCTAssertEqual(got.foreground, vector.foreground)
XCTAssertEqual(got.background, vector.background)
XCTAssertEqual(got.decision == .pass ? "pass" : "fail", vector.decision)
XCTAssertEqual(got.foregroundLuminance.lower, UInt64(vector.foregroundLowerQ55))
XCTAssertEqual(got.foregroundLuminance.upper, UInt64(vector.foregroundUpperQ55))
XCTAssertEqual(got.backgroundLuminance.lower, UInt64(vector.backgroundLowerQ55))
XCTAssertEqual(got.backgroundLuminance.upper, UInt64(vector.backgroundUpperQ55))
XCTAssertEqual(got.q55Scale, UInt64(vector.q55Scale))
XCTAssertEqual(got.evidence.kind, vector.evidenceKind)
XCTAssertEqual(got.evidence.artifactId, vector.artifactId)
XCTAssertEqual(got.evidence.artifactSha256, vector.artifactSha256)
XCTAssertEqual(got.evidence.boundId, vector.boundId)
XCTAssertEqual(got.evidence.proofId, vector.proofId)
XCTAssertEqual(got.evidence.proofSha256, vector.proofSha256)
XCTAssertEqual(got.evidence.proofPayloadSha256, vector.proofPayloadSha256)
XCTAssertEqual(got.evidence.generatorSha256, vector.generatorSha256)
XCTAssertEqual(got.evidence.verifierSha256, vector.verifierSha256)
XCTAssertEqual(got.evidence.profileChecksum, vector.profileChecksum)
XCTAssertEqual(got.evidence.profileSha256, vector.profileSha256)
}
}
}

// MARK: - Codable-зеркала схемы векторов
Expand All @@ -433,7 +479,7 @@ struct Manifest: Codable {
let numericalCapabilities: CapabilityManifest
}

/// Зеркало capability manifest (pack 3.0.0): typed-проекция core registry
/// Зеркало proof-capable capability manifest (pack 4.0.0): typed-проекция core registry
/// численных решений. Заменяет прозаический `numericalSites` из pack 2.x —
/// биндинг сверяет typed rows и drift-checksum, а не research-тексты.
struct CapabilityManifest: Codable {
Expand All @@ -452,9 +498,34 @@ struct CapabilitySite: Codable {
let evidenceClasses: [String]
let artifactIds: [String]
let boundIds: [String]
let proofIds: [String]
let runtimeAttestations: [String]
}

struct Wcag22Vec: Codable {
let foreground: String
let background: String
let criterion: String
let profileId: String
let decision: String
let foregroundLowerQ55: String
let foregroundUpperQ55: String
let backgroundLowerQ55: String
let backgroundUpperQ55: String
let q55Scale: String
let evidenceKind: String
let artifactId: String
let artifactSha256: String
let boundId: String
let proofId: String
let proofSha256: String
let proofPayloadSha256: String
let generatorSha256: String
let verifierSha256: String
let profileChecksum: String
let profileSha256: String
}

struct ContrastVec: Codable {
let fg: String
let bg: String
Expand Down
Loading
Loading