Skip to content
Merged
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
1 change: 1 addition & 0 deletions .cargo/mutants.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ examine_globs = [
"crates/labcolors-core/src/cleanliness.rs",
"crates/labcolors-core/src/glow.rs",
"crates/labcolors-core/src/material.rs",
"crates/labcolors-core/src/numerical_plan.rs",
"crates/labcolors-core/src/numerics.rs",
"crates/labcolors-core/src/pair.rs",
]
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ jobs:

const manifest = JSON.parse(readFileSync(manifests[0], "utf8"));
const expectedVersion = expectedTag.slice("colors-v".length);
if (manifest.schemaVersion !== 1) fail(`manifest schema ${manifest.schemaVersion}`);
if (manifest.schemaVersion !== 2) fail(`manifest schema ${manifest.schemaVersion}`);
if (manifest.sourceSha !== expectedSha) {
fail(`manifest sourceSha ${manifest.sourceSha} != ${expectedSha}`);
}
Expand Down
39 changes: 39 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,45 @@
Все существенные изменения Lab Colors фиксируются в этом файле. Версии npm и
Rust различаются, потому что это разные delivery surfaces одного контракта.

## [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.

### Breaking (Rust API)

- Удалены `classify_at_least_v1`, `AtLeastDecisionV1` и `DecisionGuaranteeV1`:
сравнительная «сила гарантии» как данные допускала lossy-схлопывание
взаимоисключающих исходов.
- `NumericalDecisionV1<T>` стал атомарным: `Determinate { evidence }` |
`Compatibility { release_id, provenance }` | `Indeterminate { evidence }`.
Legacy-исход — отдельный `Compatibility` с registered release
`glow-cam16-ucs-jprime-target-or-max-v1`, а не determinate со слабой
гарантией; stable outcome с legacy provenance непредставим в типе.
- `NumericalDecisionEvidenceV1::BitExact` запечатан (приватное поле-печать):
внешний код матчит только с `..`, минт выполняет registry-owned конструктор
(закреплено compile-fail тестом).
- `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.

### 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` не изменились.
- Release manifest schema v2: секция `numericalSites` заменена на
`numericalCapabilities`; release verifier и Swift conformance-тесты
пересчитывают capability checksum независимо от Rust-кода.
- Добавлен компилируемый numerical plan (`compile_numerical_plan_v1`) с
канонической invocation identity и checksum — типизированная проекция того,
какие site/mode заявляет сборка.

## [@labpics/colors 0.10.0 / Rust 0.2.0] - 2026-07-11

Breaking release относительно `@labpics/colors` 0.9.1 / Rust 0.1.0. Пошаговый
Expand Down
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,15 +165,17 @@ CSS-строка, CSSOM и обход DOM сами по себе не повыш
|---|---|
| **Поддерживаемый точечный путь sRGB** | encoded sRGB input/output, клиентский конфиг, resolve всей таблицы и специализированные рецепты в заявленной версии пакета |
| **Exact encoded-sRGB8 операции** | конечные source-over/screen композиторы, выбранная binary64 alpha и её канонический CSS round-trip; сертификат относится к point-reference, не к renderer/display |
| **Stable Glow decision** | exact point-no-op даёт `Determinate` / `bit-exact` без CAM16-профиля; нетривиальный target/max без sound bound даёт typed `Indeterminate`, не platform-selected fallback |
| **Stable Glow decision** | exact point-no-op даёт `Determinate` с sealed `bit-exact` evidence без CAM16-профиля; нетривиальный target/max без sound bound даёт typed `Indeterminate`, не platform-selected fallback |
| **Explicit compatibility (legacy)** | явный legacy execution mode даёт атомарный `Compatibility`-результат с registered release (`glow-cam16-ucs-jprime-target-or-max-v1`) и provenance-классом `legacy-platform-dependent-v1`; это НЕ determinate: результат идентифицирует воспроизводимый АЛГОРИТМ, а не cross-runtime bit-exact значение |
| **Унаследованное платформенно охарактеризованное поведение** | target-driven CAM16/CAM16-UCS/Oklab/LPC, neutral/accent/sentiment policies и связанные поиски по `f64`; они поддерживают совместимость, но не дают cross-runtime bit-exact guarantee |
| **Точность в отдельном эталонном профиле** | только операции, для которых конкретный release объявляет эталонный профиль и проверяемый конечный контракт |
| **Явно не поддержано как стабильная гарантия** | Display-P3 solving, HDR/PQ/HLG, пространственное поле Glow/Material, индивидуальное восприятие, неизвестный browser/display pipeline |

Машинно читаемый реестр численных sites и их runtime-матрица входят в
conformance manifest. Он не повышает незарегистрированный или explicit legacy
результат до `BitExact`, `ProvenOptimal` или `ProvenInfeasible` только потому,
что тесты на одной платформе зелёные.
Машинно читаемый capability manifest численных sites — typed-проекция core
registry с независимо пересчитываемым drift-checksum — входит в conformance
manifest. Он описывает возможности сборки и не повышает незарегистрированный
или explicit `Compatibility`-результат до determinate только потому, что тесты
на одной платформе зелёные.

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

Expand Down
106 changes: 106 additions & 0 deletions bindings/swift/Tests/LabColorsConformanceTests/ConformanceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,89 @@ final class ConformanceTests: XCTestCase {
"версия ядра биндинга разошлась с манифестом пака")
}

// MARK: - Capability manifest (численные решения)

/// FNV-1a-32 (как `packDigest`) — независимая Swift-копия примитива ядра,
/// чтобы пересчёт checksum не опирался на проверяемый Rust-код.
func fnv1a32(_ bytes: [UInt8]) -> UInt32 {
var hash: UInt32 = 0x811c_9dc5
for byte in bytes {
hash ^= UInt32(byte)
hash = hash &* 0x0100_0193
}
return hash
}

/// Независимый пересчёт drift-checksum capability manifest по canonical
/// 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 (явный и для
/// пустого) + отсортированные length-prefixed ключи. Кодирование повторено
/// здесь НАМЕРЕННО: тест — оракул, он не должен переиспользовать encoder,
/// который проверяет.
func testCapabilityManifestChecksumRecomputes() throws {
let manifest = try load("manifest.json", as: Manifest.self)
let caps = manifest.numericalCapabilities
// Оракул реализует canonical preimage V1: другая версия схемы обязана
// падать здесь, а не молча проходить с пересчитанным checksum.
XCTAssertEqual(caps.schemaVersion, 1, "неподдерживаемая версия capability-схемы")
XCTAssertEqual(caps.coverage, "migrated-sites-only-v1", "coverage capability manifest")
XCTAssertFalse(caps.sites.isEmpty, "capability manifest без единого migrated site пуст")
Comment thread
coderabbitai[bot] marked this conversation as resolved.
for site in caps.sites {
XCTAssertFalse(site.siteId.isEmpty, "siteId обязан быть непустым")
XCTAssertFalse(
site.stableOutcomes.isEmpty,
"site \(site.siteId) обязан объявлять lawful stable outcome")
}

var preimage: [UInt8] = []
func pushU32LE(_ value: UInt32) {
preimage.append(contentsOf: [
UInt8(truncatingIfNeeded: value),
UInt8(truncatingIfNeeded: value >> 8),
UInt8(truncatingIfNeeded: value >> 16),
UInt8(truncatingIfNeeded: value >> 24),
])
}
func pushLenPrefixed(_ key: String) {
let bytes = Array(key.utf8)
pushU32LE(UInt32(bytes.count))
preimage.append(contentsOf: bytes)
}
// Сортировка по сырым UTF-8 байтам (эквивалент sort_unstable по &[u8]
// в ядре), а не по Unicode-коллации String.
func pushSortedKeyList(_ keys: [String]) {
let sorted = keys.sorted {
Array($0.utf8).lexicographicallyPrecedes(Array($1.utf8))
}
pushU32LE(UInt32(sorted.count))
for key in sorted { pushLenPrefixed(key) }
}

pushLenPrefixed("labcolors.numerical-capability.v1")
pushU32LE(caps.schemaVersion)
pushLenPrefixed(caps.coverage)
let sites = caps.sites.sorted {
Array($0.siteId.utf8).lexicographicallyPrecedes(Array($1.siteId.utf8))
}
pushU32LE(UInt32(sites.count))
for site in sites {
pushLenPrefixed(site.siteId)
pushSortedKeyList(site.stableOutcomes)
pushSortedKeyList(site.compatibilityReleases)
pushSortedKeyList(site.evidenceClasses)
pushSortedKeyList(site.artifactIds)
pushSortedKeyList(site.boundIds)
pushSortedKeyList(site.runtimeAttestations)
}

let recomputed = String(format: "%08x", fnv1a32(preimage))
XCTAssertEqual(
recomputed, caps.checksum,
"checksum capability manifest не сходится с независимым Swift-пересчётом")
}

// MARK: - Семейство: контрасты

func testContrasts() throws {
Expand Down Expand Up @@ -347,6 +430,29 @@ struct Manifest: Codable {
let packVersion: String
let coreVersion: String
let packDigest: String
let numericalCapabilities: CapabilityManifest
}

/// Зеркало capability manifest (pack 3.0.0): typed-проекция core registry
/// численных решений. Заменяет прозаический `numericalSites` из pack 2.x —
/// биндинг сверяет typed rows и drift-checksum, а не research-тексты.
struct CapabilityManifest: Codable {
let schemaVersion: UInt32
let coverage: String
let sites: [CapabilitySite]
let checksum: String
}

/// Одна capability-строка site. Пустой список — явная часть контракта
/// («evidence отсутствует»), а не пропуск поля.
struct CapabilitySite: Codable {
let siteId: String
let stableOutcomes: [String]
let compatibilityReleases: [String]
let evidenceClasses: [String]
let artifactIds: [String]
let boundIds: [String]
let runtimeAttestations: [String]
}

struct ContrastVec: Codable {
Expand Down
33 changes: 23 additions & 10 deletions conformance/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@

## Версионирование

- **Версия пака** (`manifest.packVersion`, сейчас `2.0.0`) — семантическая
версия СХЕМЫ и состава векторов.
- **Версия пака** (`manifest.packVersion`, сейчас `3.0.0`) — семантическая
версия СХЕМЫ и состава векторов. Bump 2.0.0 → 3.0.0 менял только схему
манифеста (`numericalSites` → `numericalCapabilities`); векторные семейства
и `packDigest` не изменились.
- **Версия ядра** (`manifest.coreVersion`, для этого пака `0.2.0`) — версия
`labcolors-core`, из канона которой сгенерированы значения. Пак действителен
ровно для этой версии ядра; при легитимной смене канона (значения
Expand All @@ -35,7 +37,7 @@
| `alpha.json` | подложка→α | `{tint, alpha, bg, composite, minAlpha}` |
| `solve.json` | резолв контракта | `{bg, contract, theme, outcome}` |
| `muddiness.json` | замороженная legacy-координата `muddiness` | `{hex, score}` |
| `manifest.json` | метаданные и реестр мигрированных numerical sites | `{packVersion, coreVersion, packDigest, counts, numericalSites}` |
| `manifest.json` | метаданные и capability manifest численных решений | `{packVersion, coreVersion, packDigest, counts, numericalCapabilities}` |

`muddiness.json` — это `experimental compatibility proxy`: corpus доказывает
воспроизводимость исторического числового API, но не валидированный на
Expand All @@ -50,14 +52,25 @@ decision. Legacy-идентификаторы сохранены только д
- `code` недостижимости — стабильный словарь, общий для всех биндингов:
`below_contrast_floor`, `exceeds_range`, `quantization_gap`,
`floor_unreachable`, `polarity_mismatch`, `gamut_unsupported`, `invalid_input`.
- `alpha.json` в pack `2.0.0` обязательно содержит точный byte-reference
- `alpha.json` начиная с pack `2.0.0` обязательно содержит точный byte-reference
half-tie `#C0B2FA @ 0.122` над `#000000` → `#17161F`. Это mutation-killer
старого пути `(byte/255) · alpha · 255`, который выбирал соседний LSB.
- `manifest.numericalSites` генерируется из core-owned
`numerical_registry_v1()`. Это реестр **уже мигрированных** branch-sensitive
sites, а не утверждение, что проаудированы все исторические `f64`-ветвления.
Сейчас в нём только `glow-target-or-maximum-v1`; полнота аудита остаётся в
scope #291.
- `manifest.numericalCapabilities` (схема пака 3.0.0) генерируется из
core-owned `numerical_capability_manifest_v1()` и заменяет прозаический
`numericalSites` пака 2.x. Форма:
`{schemaVersion, coverage, sites[], checksum}`, где `schemaVersion` —
независимый version domain capability-схемы (сейчас `1`); `coverage` —
`migrated-sites-only-v1` (перечислены только **уже мигрированные**
branch-sensitive sites, не утверждение полного аудита исторических
`f64`-ветвлений — он остаётся в scope #291); каждая строка `sites[]` несёт
`siteId` и шесть списков стабильных ключей (`stableOutcomes`,
`compatibilityReleases`, `evidenceClasses`, `artifactIds`, `boundIds`,
`runtimeAttestations`; пустой список — явное «evidence отсутствует», не
пропуск); `checksum` — FNV-1a-32 (8 lowercase hex) над canonical
length-prefixed preimage с домен-сепаратором
`labcolors.numerical-capability.v1`. Release verifier и Swift-тесты
пересчитывают checksum НЕЗАВИСИМО от Rust-кода. Сейчас в manifest только
`glow-target-or-maximum-v1`.

Словарь **позиций лестницы** (не ролей): `label-*`, `fill-*`, `border-*`,
`focus-ring`, `glow`, `skeleton-*`, `neutral-fill-*`, `neutral-border-*`,
Expand Down Expand Up @@ -108,7 +121,7 @@ solve-векторами: `Pack::generate()` возвращает `PackGeneratio
через публичный JS API. Поэтому полная conformance именно JS-поверхности текущего
пака пока не заявляется. В
`native-conformance.yml` сохранён ручной macOS/arm64 reference path, но он не
запускается на PR/push и не считается достигнутой аттестацией pack 2.0.0.
запускается на PR/push и не считается достигнутой аттестацией текущего пака.
Полная runtime-матрица остаётся scope #258; допуск `DRIFT_TOL` задаёт правило
сравнения и не заменяет отсутствующий прогон.

Expand Down
42 changes: 24 additions & 18 deletions conformance/vectors/manifest.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"packVersion": "2.0.0",
"packVersion": "3.0.0",
"coreVersion": "0.2.0",
"packDigest": "64a68cbd",
"counts": {
Expand All @@ -10,21 +10,27 @@
"muddiness": 4,
"total": 82
},
"numericalSites": [
{
"siteId": "glow-target-or-maximum-v1",
"operations": "CAM16 forward powf; CAM16-UCS J-prime; abs; target >=; maximum ordering",
"domain": "encoded sRGB8 point screen states -> diagnostic CAM16-UCS delta J-prime",
"branchEffect": "first reached state versus global maximum and reached/unreachable status",
"stableOutcomes": [
"bit-exact",
"indeterminate"
],
"boundStatus": "unavailable",
"boundaryCorpus": "glow stable-indeterminate; exact no-op; finite-state compositor; half-tie alpha",
"runtimeMatrix": "active: native x86_64 + wasm32; native arm64 required before any cross-runtime CAM16 decision claim; exact bytes only for compositor",
"fallbackStatus": "none",
"legacyProfile": "legacy-platform-dependent-v1"
}
]
"numericalCapabilities": {
"schemaVersion": 1,
"coverage": "migrated-sites-only-v1",
"sites": [
{
"siteId": "glow-target-or-maximum-v1",
"stableOutcomes": [
"bit-exact",
"indeterminate"
],
"compatibilityReleases": [
"glow-cam16-ucs-jprime-target-or-max-v1"
],
"evidenceClasses": [
"bit-exact"
],
"artifactIds": [],
"boundIds": [],
"runtimeAttestations": []
}
],
"checksum": "5a3f6cbc"
}
}
Loading
Loading