diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0dd47827..94b338f5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -249,10 +249,7 @@ jobs: "labcolors-conformance", ) core = packages["labcolors-core"] - if core["features"].get("default") != [ - "wcag22-feasibility", - "wcag22-explicit-feasibility", - ]: + if core["features"].get("default") != ["wcag22-feasibility"]: raise SystemExit("labcolors-core default capability set drifted") protocol_core = [ @@ -303,12 +300,11 @@ jobs: raise SystemExit( f"{consumer} did not resolve protocol-owned wcag22-feasibility" ) - # C4a: атомарная explicit-selection операция вырезана. Explicit-домен - # остаётся внутрикорной способностью (до C4b) и заморожен: ни один - # потребитель не смеет резолвить его заново. - if 'labcolors-core feature "wcag22-explicit-feasibility"' in feature_tree: + # Explicit-домен вырезан целиком: анти-воскрешение — сама фича + # больше не существует, потребительские деревья обязаны быть чисты. + if "wcag22-explicit" in feature_tree: raise SystemExit( - f"{consumer} resolved the frozen explicit-domain capability" + f"{consumer} resolved a deleted explicit-domain capability" ) runtime_dependencies = packages["labcolors-wasm"]["dependencies"] @@ -332,7 +328,7 @@ jobs: for forbidden in ( "labcolors-protocol", 'labcolors-core feature "wcag22-feasibility"', - 'labcolors-core feature "wcag22-explicit-feasibility"', + "wcag22-explicit", ): if forbidden in runtime_tree: raise SystemExit(f"runtime role resolved forbidden capability: {forbidden}") @@ -349,8 +345,8 @@ jobs: raise SystemExit("compiler role resolved the theme/runtime engine") if 'labcolors-core feature "wcag22-feasibility"' not in compiler_tree: raise SystemExit("compiler role lacks protocol-owned wcag22-feasibility") - if 'labcolors-core feature "wcag22-explicit-feasibility"' in compiler_tree: - raise SystemExit("compiler role resolved the frozen explicit capability") + if "wcag22-explicit" in compiler_tree: + raise SystemExit("compiler role resolved a deleted explicit capability") print("core capability projection: PASS") PY @@ -362,8 +358,6 @@ jobs: run: python3 scripts/verify_wcag22_neutral_axis.py - name: verify WCAG22 feasibility identity byte grammar run: python3 scripts/verify_wcag22_feasibility_identity.py - - name: verify explicit WCAG22 feasibility identity and anti-vacuum mutations - run: python3 scripts/verify_wcag22_explicit_feasibility_identity.py --self-test audit: name: cargo audit (rustsec) diff --git a/crates/labcolors-core/Cargo.toml b/crates/labcolors-core/Cargo.toml index 26285e57..af96d9d6 100644 --- a/crates/labcolors-core/Cargo.toml +++ b/crates/labcolors-core/Cargo.toml @@ -14,9 +14,8 @@ rust-version.workspace = true [features] # Direct core consumers receive the stable default capability set. A transport # adapter may disable defaults and then enable only capabilities it projects. -default = ["wcag22-feasibility", "wcag22-explicit-feasibility"] +default = ["wcag22-feasibility"] wcag22-feasibility = [] -wcag22-explicit-feasibility = ["wcag22-feasibility"] [dev-dependencies] pretty_assertions = "1.4" @@ -58,7 +57,3 @@ harness = false name = "wcag22_feasibility" required-features = ["wcag22-feasibility"] -[[test]] -name = "wcag22_explicit_feasibility" -required-features = ["wcag22-explicit-feasibility"] - diff --git a/crates/labcolors-core/README.md b/crates/labcolors-core/README.md index c3567995..f2735e93 100644 --- a/crates/labcolors-core/README.md +++ b/crates/labcolors-core/README.md @@ -42,36 +42,32 @@ boundary details. `NotEvaluated`. `Infeasible` означает отсутствие решения только в проверенном конечном домене; это не утверждение об отсутствии цвета вне него. -Прямой Core по умолчанию включает `wcag22-feasibility` и зависящую от неё -возможность `wcag22-explicit-feasibility`. Protocol и адаптеры публикуют одну -offline-операцию — complete feasibility; все поверхности используют один -математический компилятор и не входят в runtime WASM. - -В V1 доступны две формы одного компилятора. Совместимый вход `evaluate` -перечисляет зарегистрированную нейтральную ось: ровно 256 кодов `[v, v, v]`, -где `v` принимает каждое целое значение от 0 до 255. Вход -`explicit::evaluate` принимает непустой клиентский набор пар «непрозрачный ID + -неизменяемый финальный `Srgb8`». Core сортирует точные UTF-8-байты ID, отклоняет -их повторы и сам выводит мощность, digest, матрицу и partition. Разные ID с -одинаковыми физическими байтами остаются разными кандидатами. Ни один из входов -не выводит размер текста, компонентную семантику, применимость или предпочтение -из ID. Feasibility сам ничего не ранжирует и ничего не выбирает: он доказывает -допустимость каждого кандидата в объявленном конечном домене, и только. +Прямой Core по умолчанию включает возможность `wcag22-feasibility`. Protocol +и адаптеры публикуют одну offline-операцию — complete feasibility; все +поверхности используют один математический компилятор и не входят в runtime +WASM. + +Совместимый вход `evaluate` перечисляет зарегистрированную нейтральную ось: +ровно 256 кодов `[v, v, v]`, где `v` принимает каждое целое значение от 0 +до 255. Вход не выводит размер текста, компонентную семантику, применимость +или предпочтение из ID. Feasibility сам ничего не ранжирует и ничего не +выбирает: он доказывает допустимость каждого кандидата в объявленном конечном +домене, и только. Для `C` кандидатов и `E` канонических применимых рёбер выполняется ровно `W=C×E` атомарных проверок. При `E>0` единственный упакованный буфер имеет `B=ceil(C×E/8)+ceil(C/8)` байт; при `E=0` он пуст. Эти величины выводятся и проверяются до выделения памяти. Остальная стоимость не маскируется формулой -`C×E`: отдельно выполняются линейный просмотр деклараций, сортировка кандидатов -и связей сравнением точных байтов ID, сортировка соседей внутри каждой связи и -линейное хеширование канонического результата. Превышение ресурсов, -противоречивая декларация, ошибка выделения памяти и нарушение инварианта -вычислителя или компилятора возвращаются типизированными ошибками; частичного -или запасного результата нет. +`C×E`: отдельно выполняются линейный просмотр деклараций, сортировка связей +сравнением точных байтов ID, сортировка соседей внутри каждой связи и линейное +хеширование канонического результата. Превышение ресурсов, противоречивая +декларация, ошибка выделения памяти и нарушение инварианта вычислителя или +компилятора возвращаются типизированными ошибками; частичного или запасного +результата нет. -Формулы `C×E`, случай `E=0`, повторы ID, ресурсные отказы и запрет частичного -результата исполняются непосредственно в `src/wcag22_feasibility_tests.rs`, -`tests/wcag22_feasibility.rs` и `tests/wcag22_explicit_feasibility.rs`. +Формулы `C×E`, случай `E=0`, ресурсные отказы и запрет частичного результата +исполняются непосредственно в `src/wcag22_feasibility_tests.rs` и +`tests/wcag22_feasibility.rs`. ```rust # #[cfg(feature = "wcag22-feasibility")] @@ -111,42 +107,6 @@ if let Some(evaluated) = result.evaluated() { # fn main() {} ``` -Клиентский конечный набор использует те же `RelationV1` и атомарный WCAG-путь: - -```rust -# #[cfg(feature = "wcag22-explicit-feasibility")] -# fn explicit_feasibility_example() -> Result<(), Box> { -use labcolors_core::{ - Srgb8, - wcag22::Wcag22CriterionV1, - wcag22_feasibility::{ - OccurrenceId, RelationId, RelationV1, ResourceProfileIdV1, - explicit::{CandidateId, CandidateV1, DomainRequestV1, RequestV1, evaluate}, - }, -}; - -let domain = DomainRequestV1::try_new(vec![ - CandidateV1::new(CandidateId::try_new("brand/ink")?, Srgb8::new([18, 52, 86])), - CandidateV1::new(CandidateId::try_new("brand/paper")?, Srgb8::new([245, 247, 250])), -])?; -let relation = RelationV1::applicable( - RelationId::try_new("content-on-canvas")?, - OccurrenceId::try_new("article/body")?, - Wcag22CriterionV1::Sc143TextDefault, - vec![Srgb8::new([255; 3])], -)?; -let result = evaluate(RequestV1::try_new( - domain, - vec![relation], - ResourceProfileIdV1::Compile, -)?)?; - -assert_eq!(result.evaluated().map(|value| value.candidates().len()), Some(2)); -# Ok(()) -# } -# fn main() {} -``` - В примере `Sc143TextDefault` означает явно объявленный клиентом критерий SC 1.4.3 для обычного текста с отношением 4.5:1; Core не угадывает его по ID или типографике. `0x75` и `0x76` — вычисленные граничные результаты именно для двух diff --git a/crates/labcolors-core/contracts/wcag22-explicit-feasibility-identity-v1.json b/crates/labcolors-core/contracts/wcag22-explicit-feasibility-identity-v1.json deleted file mode 100644 index cc61fcac..00000000 --- a/crates/labcolors-core/contracts/wcag22-explicit-feasibility-identity-v1.json +++ /dev/null @@ -1 +0,0 @@ -{"artifactId":"wcag22-explicit-feasibility-identity-v1","encoding":{"byteString":"u64-length-then-exact-bytes","candidateOrder":"lexicographic-exact-utf8-bytes-no-normalization","candidateRecord":"length-prefixed-id-then-three-srgb8-octets","evaluationLayoutOrder":["canonicalRelations","applicableRelations","notApplicableRelations","applicableEdges","candidateCount","logicalAssessments","failureMatrixBytes","partitionBytes","packedResultBytes"],"integer":"u64-big-endian","matrixBitOrder":"candidate-major-contiguous-lsb0","partitionBitOrder":"canonical-candidate-index-lsb0","partitionInEvaluation":"u64-length-then-exact-bytes","relationGrammar":"wcag22-feasibility-relations-v1"},"expected":{"domainDigestSha256":"71960b339a5af0421a5562e02aea28217b3f985c88a53f3244ea73f6c19258f4","evaluationIdSha256":"59e69b867d8feb8afae4d28708bd353d0f3a0e89c12b0f34952f2e9a5e8be700","relationSetDigestSha256":"990dbc58252dc518ccf63b2f4b63ef5ae227a2bed48dda9e5e5959f3e2477132"},"fixture":{"canonicalCandidates":[{"candidateId":"é","candidateIdUtf8Hex":"65cc81","emitted":[18,52,86]},{"candidateId":"é","candidateIdUtf8Hex":"c3a9","emitted":[18,52,86]},{"candidateId":"海","candidateIdUtf8Hex":"e6b5b7","emitted":[0,0,0]},{"candidateId":"🎨","candidateIdUtf8Hex":"f09f8ea8","emitted":[255,128,1]}],"canonicalRelations":[{"adjacent":[[0,0,0],[118,118,118],[255,255,255]],"criterion":"sc-1.4.3-text-default","kind":"applicable","occurrenceId":"hover/🎨","relationId":"alpha"},{"kind":"notApplicable","occurrenceId":"ornament","reasonId":"client/не-применимо","relationId":"zeta"}],"declaredCandidates":[{"candidateId":"🎨","emitted":[255,128,1]},{"candidateId":"é","emitted":[18,52,86]},{"candidateId":"海","emitted":[0,0,0]},{"candidateId":"é","emitted":[18,52,86]}],"domainKind":"explicit-srgb8-set-v1","failureMatrixHex":"5b0c","layout":{"applicableEdges":3,"applicableRelations":1,"candidateCount":4,"canonicalRelations":2,"failureMatrixBytes":2,"logicalAssessments":12,"notApplicableRelations":1,"packedResultBytes":3,"partitionBytes":1},"matrixSha256":"f414937d1b17276054be72790c34aef5a4eb5b6dc2132122599d47297bba5507","partitionHex":"00"},"schemaVersion":1} diff --git a/crates/labcolors-core/src/wcag22_feasibility.rs b/crates/labcolors-core/src/wcag22_feasibility.rs index c3f2a5ca..92eebf42 100644 --- a/crates/labcolors-core/src/wcag22_feasibility.rs +++ b/crates/labcolors-core/src/wcag22_feasibility.rs @@ -17,10 +17,6 @@ use crate::wcag22::{ wcag22_profile_v1, }; -#[path = "wcag22_feasibility/explicit.rs"] -#[cfg(feature = "wcag22-explicit-feasibility")] -pub mod explicit; - const CANDIDATE_COUNT: u64 = 256; const PARTITION_BYTES: u64 = CANDIDATE_COUNT / 8; @@ -127,7 +123,7 @@ pub enum ResourceDimensionV1 { /// /// Counting happens before canonicalization, deduplication or lookup and /// excludes Core-owned keys, framing, escaped transport bytes and total - /// memory. Feasibility counts every raw explicit-candidate, relation and + /// memory. Feasibility counts every raw relation and /// occurrence ID plus every `NotApplicable` reason ID; a registered domain /// contributes no candidate bytes. Selection counts its policy ID and every /// raw ordered candidate ID without recounting IDs retained by the completed @@ -327,15 +323,6 @@ pub enum InvalidRequestV1 { EmptyOccurrenceId, /// No relations were declared. EmptyRelations, - /// An explicit candidate ID was empty. - #[cfg(feature = "wcag22-explicit-feasibility")] - EmptyCandidateId, - /// No explicit candidates were declared. - #[cfg(feature = "wcag22-explicit-feasibility")] - EmptyCandidates, - /// The same explicit candidate ID occurred more than once. - #[cfg(feature = "wcag22-explicit-feasibility")] - DuplicateCandidateId { candidate_id: explicit::CandidateId }, /// An applicable relation had no adjacent colour. EmptyAdjacentSet { relation_id: RelationId }, /// The same relation ID described different canonical declarations. @@ -350,14 +337,6 @@ impl fmt::Display for InvalidRequestV1 { Self::EmptyRelationId => formatter.write_str("relation ID must be non-empty"), Self::EmptyOccurrenceId => formatter.write_str("occurrence ID must be non-empty"), Self::EmptyRelations => formatter.write_str("at least one relation is required"), - #[cfg(feature = "wcag22-explicit-feasibility")] - Self::EmptyCandidateId => formatter.write_str("candidate ID must be non-empty"), - #[cfg(feature = "wcag22-explicit-feasibility")] - Self::EmptyCandidates => formatter.write_str("at least one candidate is required"), - #[cfg(feature = "wcag22-explicit-feasibility")] - Self::DuplicateCandidateId { candidate_id } => { - write!(formatter, "candidate ID {candidate_id} is duplicated") - } Self::EmptyAdjacentSet { relation_id } => { write!( formatter, @@ -1102,12 +1081,6 @@ trait DecisionStorage { logical_index: u64, decision: Wcag22ApplicableDecisionV1, ) -> Result<(), ()>; - #[cfg(feature = "wcag22-explicit-feasibility")] - fn write_feasible_candidate( - &mut self, - matrix_bytes: u64, - candidate_index: u64, - ) -> Result<(), ()>; fn finish(&mut self, partition: &[u8]) -> Result<(), ()>; } @@ -1116,13 +1089,6 @@ struct PackedDecisionStorage { bytes: Vec, } -impl PackedDecisionStorage { - #[cfg(feature = "wcag22-explicit-feasibility")] - fn into_bytes(self) -> Vec { - self.bytes - } -} - impl DecisionStorage for PackedDecisionStorage { fn try_reserve_exact(&mut self, requested_bytes: usize) -> Result<(), ()> { self.bytes @@ -1146,22 +1112,6 @@ impl DecisionStorage for PackedDecisionStorage { Ok(()) } - #[cfg(feature = "wcag22-explicit-feasibility")] - fn write_feasible_candidate( - &mut self, - matrix_bytes: u64, - candidate_index: u64, - ) -> Result<(), ()> { - let byte_index = matrix_bytes - .checked_add(candidate_index / 8) - .and_then(|value| usize::try_from(value).ok()) - .ok_or(())?; - let bit = (candidate_index % 8) as u8; - let byte = self.bytes.get_mut(byte_index).ok_or(())?; - *byte |= 1_u8 << bit; - Ok(()) - } - fn finish(&mut self, partition: &[u8]) -> Result<(), ()> { let start = self.bytes.len().checked_sub(partition.len()).ok_or(())?; let destination = self.bytes.get_mut(start..).ok_or(())?; @@ -1211,32 +1161,6 @@ impl PackedDomainV1 for NeutralAxisPackingV1 { } } -#[derive(Debug)] -#[cfg(feature = "wcag22-explicit-feasibility")] -struct VariablePackingV1; - -#[cfg(feature = "wcag22-explicit-feasibility")] -impl PackedDomainV1 for VariablePackingV1 { - type Partition = (); - - fn record_feasible( - _partition: &mut Self::Partition, - storage: &mut S, - matrix_bytes: u64, - candidate_index: usize, - ) -> Result<(), ()> { - let candidate_index = u64::try_from(candidate_index).map_err(|_| ())?; - storage.write_feasible_candidate(matrix_bytes, candidate_index) - } - - fn finish( - _partition: &Self::Partition, - _storage: &mut S, - ) -> Result<(), ()> { - Ok(()) - } -} - #[derive(Debug)] struct KernelEvaluatedV1 { domain: D, @@ -1468,19 +1392,6 @@ fn packed_bit(bytes: &[u8], logical_index: u64) -> bool { bytes[byte_index] & (1_u8 << bit) != 0 } -#[cfg(feature = "wcag22-explicit-feasibility")] -fn unused_tail_bits_are_zero(bytes: &[u8], used_bits: u64) -> bool { - let remainder = (used_bits % 8) as u8; - if remainder == 0 { - return true; - } - let Some(last) = bytes.last() else { - return false; - }; - let used_mask = ((1_u16 << remainder) - 1) as u8; - last & !used_mask == 0 -} - fn validate_neutral_complete_result_v1( layout: WorkLayoutV1, matrix: &[u8], @@ -1526,57 +1437,6 @@ fn validate_neutral_complete_result_v1( Ok(()) } -#[cfg(feature = "wcag22-explicit-feasibility")] -fn validate_variable_complete_result_v1( - layout: WorkLayoutV1, - matrix: &[u8], - partition: &[u8], - counters: EvaluationProofCountersV1, -) -> Result<(), CompilerInvariantV1> { - let expected_matrix_bytes = usize::try_from(layout.failure_matrix_bytes) - .map_err(|_| CompilerInvariantV1::CompleteResultMismatch)?; - let expected_partition_bytes = usize::try_from(layout.partition_bytes) - .map_err(|_| CompilerInvariantV1::CompleteResultMismatch)?; - if matrix.len() != expected_matrix_bytes - || partition.len() != expected_partition_bytes - || !unused_tail_bits_are_zero(matrix, layout.logical_assessments) - || !unused_tail_bits_are_zero(partition, layout.candidate_count) - { - return Err(CompilerInvariantV1::CompleteResultMismatch); - } - if counters.logical_assessments != layout.logical_assessments { - return Err(CompilerInvariantV1::CompleteResultMismatch); - } - let mut passing = 0_u64; - for candidate in 0_u64..layout.candidate_count { - let row_start = candidate - .checked_mul(layout.applicable_edges) - .ok_or(CompilerInvariantV1::CompleteResultMismatch)?; - let mut row_passes = true; - for edge in 0..layout.applicable_edges { - let failed = packed_bit(matrix, row_start + edge); - row_passes &= !failed; - } - if row_passes != packed_bit(partition, candidate) { - return Err(CompilerInvariantV1::CompleteResultMismatch); - } - if row_passes { - passing += 1; - } - } - if counters.passing_candidates != passing { - return Err(CompilerInvariantV1::CompleteResultMismatch); - } - if counters - .passing_candidates - .checked_add(counters.failing_candidates) - != Some(layout.candidate_count) - { - return Err(CompilerInvariantV1::CompleteResultMismatch); - } - Ok(()) -} - // Identity encoders target this minimal sink so production SHA-256 and bounded // byte-work probes execute the same byte grammar rather than parallel copies. trait CanonicalByteSink { @@ -1671,70 +1531,6 @@ fn evaluation_id( EvaluationIdV1(*hasher.finalize().as_bytes()) } -#[cfg(feature = "wcag22-explicit-feasibility")] -struct SealedPackedV1 { - packed: Vec, - matrix_digest: [u8; 32], - matrix_bytes: usize, -} - -#[cfg(feature = "wcag22-explicit-feasibility")] -impl SealedPackedV1 { - fn partition(&self) -> &[u8] { - &self.packed[self.matrix_bytes..] - } -} - -#[cfg(feature = "wcag22-explicit-feasibility")] -fn seal_evaluated_v1( - result: &KernelEvaluatedV1, - packed: Vec, -) -> Result { - let matrix_bytes = usize::try_from(result.layout.failure_matrix_bytes).map_err(|_| { - ErrorV1::ResourceLimitExceeded { - profile_id: result.resource_profile_id, - dimension: ResourceDimensionV1::PackedResultBytes, - requested: result.layout.failure_matrix_bytes, - limit: usize::MAX as u64, - } - })?; - let packed_bytes = usize::try_from(result.layout.packed_result_bytes).map_err(|_| { - ErrorV1::ResourceLimitExceeded { - profile_id: result.resource_profile_id, - dimension: ResourceDimensionV1::PackedResultBytes, - requested: result.layout.packed_result_bytes, - limit: usize::MAX as u64, - } - })?; - if packed.len() != packed_bytes || matrix_bytes > packed_bytes { - return Err(compiler_result_error()); - } - let (matrix, partition) = packed.split_at(matrix_bytes); - let Some(failing_candidates) = result - .layout - .candidate_count - .checked_sub(result.passing_candidates) - else { - return Err(compiler_result_error()); - }; - validate_variable_complete_result_v1( - result.layout, - matrix, - partition, - EvaluationProofCountersV1 { - logical_assessments: result.observed_assessments, - passing_candidates: result.passing_candidates, - failing_candidates, - }, - ) - .map_err(ErrorV1::CompilerInvariantViolation)?; - Ok(SealedPackedV1 { - matrix_digest: *sha256::digest(matrix).as_bytes(), - packed, - matrix_bytes, - }) -} - /// Sealed evidence for one complete evaluated terminal. #[derive(Debug, Clone, PartialEq, Eq)] pub struct EvaluationProofV1 { diff --git a/crates/labcolors-core/src/wcag22_feasibility/explicit.rs b/crates/labcolors-core/src/wcag22_feasibility/explicit.rs deleted file mode 100644 index 66ae402c..00000000 --- a/crates/labcolors-core/src/wcag22_feasibility/explicit.rs +++ /dev/null @@ -1,719 +0,0 @@ -//! Client-declared finite sRGB8 feasibility (#296-A). - -use core::fmt; -use std::sync::Arc; - -use crate::Srgb8; -use crate::numerics::{NumericalArtifactIdV2, NumericalErrorBoundIdV2, NumericalProofIdV2}; -use crate::sha256::Hasher; -use crate::wcag22::{Wcag22ApplicableDecisionV1, Wcag22ProfileIdV1}; - -use super::{ - AssessmentCellV1, AssessmentCursorV1, AtomicEvidenceBindingV1, AtomicPairEvaluator, - DomainDigestV1, ErrorV1, EvaluationIdV1, FiniteSrgb8DomainV1, InvalidRequestV1, - KernelRequestV1, KernelResultV1, PackedDecisionStorage, RelationId, RelationSetDigestV1, - RelationV1, ResourceProfileIdV1, VariablePackingV1, WorkLayoutV1, evaluate_domain_with, - hash_len_prefixed, hash_u64, packed_bit, relation_set_digest, seal_evaluated_v1, -}; - -const DOMAIN_SEPARATOR: &[u8] = b"labcolors/wcag22-feasibility/domain/explicit-srgb8-set/v1\0"; -const EVALUATION_SEPARATOR: &[u8] = - b"labcolors/wcag22-feasibility/evaluation/explicit-srgb8-set/v1\0"; - -/// Opaque client-owned candidate identity. -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct CandidateId(Arc); - -impl CandidateId { - /// Construct a non-empty opaque ID. Core uses the exact UTF-8 bytes and - /// never normalizes or interprets the text. - pub fn try_new(value: impl Into) -> Result { - let value = value.into(); - if value.is_empty() { - return Err(InvalidRequestV1::EmptyCandidateId); - } - Ok(Self(value.into())) - } - - /// Exact client bytes used for canonical ordering and identity. - pub fn as_str(&self) -> &str { - self.0.as_ref() - } -} - -impl fmt::Display for CandidateId { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str(self.as_str()) - } -} - -/// One explicit physical candidate. The ID remains opaque to Core. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct CandidateV1 { - candidate_id: CandidateId, - emitted: Srgb8, -} - -impl CandidateV1 { - /// Bind an opaque client ID to one exact final encoded-sRGB8 value. - pub const fn new(candidate_id: CandidateId, emitted: Srgb8) -> Self { - Self { - candidate_id, - emitted, - } - } - - /// Opaque client-owned identity. - pub const fn candidate_id(&self) -> &CandidateId { - &self.candidate_id - } - - /// Exact final physical bytes evaluated by Core. - pub const fn emitted(&self) -> Srgb8 { - self.emitted - } -} - -/// Owned client-declared finite domain before Core canonicalization. -#[derive(Debug)] -pub struct DomainRequestV1 { - candidates: Vec, -} - -impl DomainRequestV1 { - /// Construct a non-empty declared set. Duplicate IDs are rejected at the - /// compiler boundary after raw resource preflight and before evaluation. - pub fn try_new(candidates: Vec) -> Result { - if candidates.is_empty() { - return Err(InvalidRequestV1::EmptyCandidates); - } - Ok(Self { candidates }) - } - - fn into_candidates(self) -> Vec { - self.candidates - } -} - -impl FiniteSrgb8DomainV1 for DomainRequestV1 { - type Packing = VariablePackingV1; - - fn raw_opaque_utf8_bytes(&self) -> Result { - let mut bytes = 0_u64; - for candidate in &self.candidates { - let length = u64::try_from(candidate.candidate_id.as_str().len()) - .map_err(|_| InvalidRequestV1::ArithmeticOverflow)?; - bytes = bytes - .checked_add(length) - .ok_or(InvalidRequestV1::ArithmeticOverflow)?; - } - Ok(bytes) - } - - fn canonicalize(&mut self) -> Result<(), InvalidRequestV1> { - self.candidates.sort_unstable_by(|left, right| { - left.candidate_id - .as_str() - .as_bytes() - .cmp(right.candidate_id.as_str().as_bytes()) - }); - if let Some(pair) = self - .candidates - .windows(2) - .find(|pair| pair[0].candidate_id == pair[1].candidate_id) - { - return Err(InvalidRequestV1::DuplicateCandidateId { - candidate_id: pair[0].candidate_id.clone(), - }); - } - Ok(()) - } - - fn candidates(&self) -> impl ExactSizeIterator + '_ { - self.candidates.iter().map(CandidateV1::emitted) - } -} - -/// Core-owned versioned kind of declared finite domain. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -#[non_exhaustive] -pub enum DomainKindV1 { - /// Canonical set of opaque IDs bound to exact final sRGB8 values. - ExplicitSrgb8Set, -} - -impl DomainKindV1 { - /// Stable semantic key. - pub const fn key(self) -> &'static str { - match self { - Self::ExplicitSrgb8Set => "explicit-srgb8-set-v1", - } - } -} - -/// Owned bounded-compilation request for one explicit finite domain. -#[derive(Debug)] -pub struct RequestV1 { - domain: DomainRequestV1, - relations: Vec, - resource_profile_id: ResourceProfileIdV1, -} - -impl RequestV1 { - /// Construct a locally well-formed request. Aggregate resource bounds and - /// duplicate candidate IDs are checked by [`evaluate`] before any pair is - /// evaluated. - pub fn try_new( - domain: DomainRequestV1, - relations: Vec, - resource_profile_id: ResourceProfileIdV1, - ) -> Result { - if relations.is_empty() { - return Err(InvalidRequestV1::EmptyRelations); - } - Ok(Self { - domain, - relations, - resource_profile_id, - }) - } -} - -/// Domain-neutral evidence descriptor: kind, canonical content and exact -/// finite cardinality. It deliberately has no neutral-only first/last fields. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct DomainDescriptorV1 { - kind: DomainKindV1, - digest: DomainDigestV1, - candidate_count: u64, -} - -impl DomainDescriptorV1 { - /// Versioned domain kind. - pub const fn kind(&self) -> DomainKindV1 { - self.kind - } - - /// Canonical `(candidate ID, emitted sRGB8)` content digest. - pub const fn digest(&self) -> DomainDigestV1 { - self.digest - } - - /// Exact number of exhaustively evaluated declared candidates. - pub const fn candidate_count(&self) -> u64 { - self.candidate_count - } -} - -fn domain_digest(domain: &DomainRequestV1, candidate_count: u64) -> DomainDigestV1 { - let mut hasher = Hasher::new(); - hasher.update(DOMAIN_SEPARATOR); - hash_len_prefixed(&mut hasher, DomainKindV1::ExplicitSrgb8Set.key().as_bytes()); - hash_u64(&mut hasher, candidate_count); - for candidate in &domain.candidates { - hash_len_prefixed(&mut hasher, candidate.candidate_id.as_str().as_bytes()); - hasher.update(&candidate.emitted.bytes()); - } - DomainDigestV1(*hasher.finalize().as_bytes()) -} - -fn evaluation_id( - domain_digest: DomainDigestV1, - relation_digest: RelationSetDigestV1, - binding: &AtomicEvidenceBindingV1, - layout: WorkLayoutV1, - matrix_digest: &[u8; 32], - partition: &[u8], -) -> EvaluationIdV1 { - let mut hasher = Hasher::new(); - hasher.update(EVALUATION_SEPARATOR); - hasher.update(domain_digest.as_bytes()); - hasher.update(relation_digest.as_bytes()); - hash_len_prefixed(&mut hasher, binding.profile_id.key().as_bytes()); - hash_len_prefixed(&mut hasher, binding.artifact_id.key().as_bytes()); - hash_len_prefixed(&mut hasher, binding.bound_id.key().as_bytes()); - hash_len_prefixed(&mut hasher, binding.proof_id.key().as_bytes()); - hasher.update(&binding.proof_sha256); - for value in [ - layout.canonical_relations, - layout.applicable_relations, - layout.not_evaluated_relations, - layout.applicable_edges, - layout.candidate_count, - layout.logical_assessments, - layout.failure_matrix_bytes, - layout.partition_bytes, - layout.packed_result_bytes, - ] { - hash_u64(&mut hasher, value); - } - hasher.update(matrix_digest); - hash_len_prefixed(&mut hasher, partition); - EvaluationIdV1(*hasher.finalize().as_bytes()) -} - -#[derive(Debug)] -struct EvaluationProofDataV1 { - resource_profile_id: ResourceProfileIdV1, - canonical_relations: u64, - applicable_relations: u64, - not_applicable_relations: u64, - applicable_edges: u64, - logical_assessments: u64, - matrix_digest: [u8; 32], - atomic_evidence: AtomicEvidenceBindingV1, -} - -/// Borrowed sealed view of one complete explicit-domain proof. -#[derive(Debug, Clone, Copy)] -pub struct EvaluationProofV1<'a> { - record: &'a EvaluatedV1, -} - -impl EvaluationProofV1<'_> { - /// Semantic result identity; resource policy is excluded from it. - pub const fn evaluation_id(&self) -> EvaluationIdV1 { - self.record.evaluation_id - } - - /// Operational policy that admitted this computation. - pub const fn resource_profile_id(&self) -> ResourceProfileIdV1 { - self.record.proof.resource_profile_id - } - - /// Domain-neutral explicit-set descriptor. - pub const fn domain(&self) -> &DomainDescriptorV1 { - &self.record.domain - } - - /// Canonical declared-relation digest. - pub const fn relation_set_digest(&self) -> RelationSetDigestV1 { - self.record.relation_set_digest - } - - /// Exact canonical relation count. - pub const fn canonical_relations(&self) -> u64 { - self.record.proof.canonical_relations - } - - /// Exact applicable relation count. - pub const fn applicable_relations(&self) -> u64 { - self.record.proof.applicable_relations - } - - /// Exact client-declared NotApplicable relation count. - pub const fn not_applicable_relations(&self) -> u64 { - self.record.proof.not_applicable_relations - } - - /// Exact flattened canonical edge count. - pub const fn applicable_edges(&self) -> u64 { - self.record.proof.applicable_edges - } - - /// Exact number of evaluated candidate-edge cells. - pub const fn logical_assessments(&self) -> u64 { - self.record.proof.logical_assessments - } - - /// SHA-256 of the exact packed candidate-major failure matrix. - pub const fn matrix_digest(&self) -> &[u8; 32] { - &self.record.proof.matrix_digest - } - - /// Exact variable-width feasible-candidate partition, LSB0 by canonical - /// candidate index. - pub fn partition(&self) -> &[u8] { - self.record.partition() - } - - /// Exact WCAG evaluator profile bound into every atomic assessment. - pub const fn profile_id(&self) -> Wcag22ProfileIdV1 { - self.record.proof.atomic_evidence.profile_id - } - - /// Exact finite numerical artifact used by the atomic evaluator. - pub const fn artifact_id(&self) -> NumericalArtifactIdV2 { - self.record.proof.atomic_evidence.artifact_id - } - - /// Exact numerical error-bound law used by the atomic evaluator. - pub const fn bound_id(&self) -> NumericalErrorBoundIdV2 { - self.record.proof.atomic_evidence.bound_id - } - - /// Exact complete-domain numerical proof used by the atomic evaluator. - pub const fn proof_id(&self) -> NumericalProofIdV2 { - self.record.proof.atomic_evidence.proof_id - } - - /// SHA-256 of the exact #284 proof-file bytes. - pub const fn proof_sha256(&self) -> &[u8; 32] { - &self.record.proof.atomic_evidence.proof_sha256 - } -} - -/// Zero-allocation view of one explicit candidate-major cell. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct AssessmentV1<'a> { - candidate: &'a CandidateV1, - relation_id: &'a RelationId, - adjacent: Srgb8, - decision: Wcag22ApplicableDecisionV1, -} - -impl<'a> AssessmentV1<'a> { - /// Opaque canonical candidate identity. - pub const fn candidate_id(self) -> &'a CandidateId { - &self.candidate.candidate_id - } - - /// Exact final physical bytes. - pub const fn emitted(self) -> Srgb8 { - self.candidate.emitted - } - - /// Opaque canonical relation identity. - pub const fn relation_id(self) -> &'a RelationId { - self.relation_id - } - - /// Exact declared adjacent colour. - pub const fn adjacent(self) -> Srgb8 { - self.adjacent - } - - /// Atomic #284 decision. - pub const fn decision(self) -> Wcag22ApplicableDecisionV1 { - self.decision - } -} - -struct AssessmentIter<'a> { - candidates: &'a [CandidateV1], - cursor: AssessmentCursorV1<'a>, -} - -impl<'a> Iterator for AssessmentIter<'a> { - type Item = AssessmentV1<'a>; - - fn next(&mut self) -> Option { - let AssessmentCellV1 { - candidate_index, - relation, - adjacent, - decision, - } = self.cursor.next()?; - let candidate = usize::try_from(candidate_index) - .ok() - .and_then(|index| self.candidates.get(index)) - .expect("sealed explicit candidate cardinality must match the assessment cursor"); - Some(AssessmentV1 { - candidate, - relation_id: &relation.relation_id, - adjacent, - decision, - }) - } - - fn size_hint(&self) -> (usize, Option) { - self.cursor.size_hint() - } -} - -impl ExactSizeIterator for AssessmentIter<'_> {} - -/// Complete evaluated explicit-domain terminal. It has no public constructor. -#[derive(Debug)] -pub struct EvaluatedV1 { - candidates: Vec, - relations: Vec, - layout: WorkLayoutV1, - packed: Vec, - domain: DomainDescriptorV1, - relation_set_digest: RelationSetDigestV1, - evaluation_id: EvaluationIdV1, - proof: EvaluationProofDataV1, -} - -impl EvaluatedV1 { - /// Exact candidate-major packed failure matrix. For candidate `c` and edge - /// `e`, bit index is `cE + e`; one means Fail. - pub fn failure_matrix(&self) -> &[u8] { - let length = self.layout.failure_matrix_bytes as usize; - &self.packed[..length] - } - - fn partition(&self) -> &[u8] { - let length = self.layout.failure_matrix_bytes as usize; - &self.packed[length..] - } - - /// Canonical explicit domain descriptor. - pub const fn domain(&self) -> &DomainDescriptorV1 { - &self.domain - } - - /// Canonical explicit domain digest. - pub const fn domain_digest(&self) -> DomainDigestV1 { - self.domain.digest - } - - /// Canonical relation-set digest. - pub const fn relation_set_digest(&self) -> RelationSetDigestV1 { - self.relation_set_digest - } - - /// Semantic evaluated-result identity. - pub const fn evaluation_id(&self) -> EvaluationIdV1 { - self.evaluation_id - } - - /// Borrow the sealed complete-enumeration proof without copying its - /// variable partition. - pub const fn proof(&self) -> EvaluationProofV1<'_> { - EvaluationProofV1 { record: self } - } - - /// Canonical candidates in exact candidate-ID byte order. - pub fn candidates(&self) -> &[CandidateV1] { - &self.candidates - } - - /// Canonical declared graph retained exactly once by the result. - pub fn relations(&self) -> &[RelationV1] { - &self.relations - } - - /// Every feasible candidate in canonical candidate-ID byte order. - pub fn feasible_candidates(&self) -> impl Iterator { - (0_u64..) - .zip(self.candidates.iter()) - .filter(move |(index, _)| packed_bit(self.partition(), *index)) - .map(|(_, candidate)| candidate) - } - - /// Every infeasible candidate in canonical candidate-ID byte order. - pub fn infeasible_candidates(&self) -> impl Iterator { - (0_u64..) - .zip(self.candidates.iter()) - .filter(move |(index, _)| !packed_bit(self.partition(), *index)) - .map(|(_, candidate)| candidate) - } - - /// Full candidate-major `C×E` matrix without per-cell allocation. - pub fn assessments(&self) -> impl ExactSizeIterator> + '_ { - AssessmentIter { - candidates: &self.candidates, - cursor: AssessmentCursorV1 { - relations: &self.relations, - matrix: self.failure_matrix(), - candidate_index: 0, - candidate_count: self.layout.candidate_count, - relation_index: 0, - adjacent_index: 0, - logical_index: 0, - remaining: self.layout.logical_assessments as usize, - }, - } - } -} - -/// Canonical declaration-only explicit-domain terminal. -#[derive(Debug)] -pub struct NotEvaluatedV1 { - candidates: Vec, - relations: Vec, - resource_profile_id: ResourceProfileIdV1, - domain: DomainDescriptorV1, - relation_set_digest: RelationSetDigestV1, -} - -impl NotEvaluatedV1 { - /// Canonical explicit domain descriptor. - pub const fn domain(&self) -> &DomainDescriptorV1 { - &self.domain - } - - /// Canonical explicit candidates. - pub fn candidates(&self) -> &[CandidateV1] { - &self.candidates - } - - /// Canonical relation-set digest. - pub const fn relation_set_digest(&self) -> RelationSetDigestV1 { - self.relation_set_digest - } - - /// Operational policy that admitted canonicalization. - pub const fn resource_profile_id(&self) -> ResourceProfileIdV1 { - self.resource_profile_id - } - - /// Canonical declaration-only graph. - pub fn relations(&self) -> &[RelationV1] { - &self.relations - } -} - -/// Exhaustive explicit-domain terminal algebra. -#[derive(Debug)] -#[non_exhaustive] -pub enum FeasibilityV1 { - /// Complete evaluation with a non-empty feasible partition. - #[non_exhaustive] - Feasible(EvaluatedV1), - /// Complete evaluation with an empty feasible partition. - #[non_exhaustive] - Infeasible(EvaluatedV1), - /// Canonical request contained no applicable relation. - #[non_exhaustive] - NotEvaluated(NotEvaluatedV1), -} - -impl FeasibilityV1 { - /// Whether complete evaluation found at least one feasible candidate. - pub const fn is_feasible(&self) -> bool { - matches!(self, Self::Feasible(..)) - } - - /// Whether complete evaluation found no feasible candidate. - pub const fn is_infeasible(&self) -> bool { - matches!(self, Self::Infeasible(..)) - } - - /// Whether no relation was applicable and therefore no pair was evaluated. - pub const fn is_not_evaluated(&self) -> bool { - matches!(self, Self::NotEvaluated(..)) - } - - /// Borrow the complete evaluated record from either evaluated terminal. - pub const fn evaluated(&self) -> Option<&EvaluatedV1> { - match self { - Self::Feasible(value) | Self::Infeasible(value) => Some(value), - Self::NotEvaluated(..) => None, - } - } - - /// Borrow the declaration-only record when no relation was applicable. - pub const fn not_evaluated(&self) -> Option<&NotEvaluatedV1> { - match self { - Self::NotEvaluated(value) => Some(value), - Self::Feasible(..) | Self::Infeasible(..) => None, - } - } -} - -/// Canonicalize and exhaustively evaluate one client-declared finite set. -pub fn evaluate(request: RequestV1) -> Result { - let mut evaluator = AtomicPairEvaluator::new(); - let mut storage = PackedDecisionStorage::default(); - let request = KernelRequestV1 { - domain: request.domain, - relations: request.relations, - resource_profile_id: request.resource_profile_id, - }; - match evaluate_domain_with(request, &mut evaluator, &mut storage)? { - KernelResultV1::NotEvaluated(result) => { - let candidate_count = result.layout.candidate_count; - let digest = domain_digest(&result.domain, candidate_count); - let domain = DomainDescriptorV1 { - kind: DomainKindV1::ExplicitSrgb8Set, - digest, - candidate_count, - }; - let relation_set_digest = relation_set_digest(&result.relations); - Ok(FeasibilityV1::NotEvaluated(NotEvaluatedV1 { - candidates: result.domain.into_candidates(), - relations: result.relations, - resource_profile_id: result.resource_profile_id, - domain, - relation_set_digest, - })) - } - KernelResultV1::Evaluated(result) => { - let sealed = seal_evaluated_v1(&result, storage.into_bytes())?; - let digest = domain_digest(&result.domain, result.observed_candidates); - let domain = DomainDescriptorV1 { - kind: DomainKindV1::ExplicitSrgb8Set, - digest, - candidate_count: result.observed_candidates, - }; - let relation_set_digest = relation_set_digest(&result.relations); - let evaluation_id = evaluation_id( - digest, - relation_set_digest, - &result.atomic_evidence, - result.layout, - &sealed.matrix_digest, - sealed.partition(), - ); - let proof = EvaluationProofDataV1 { - resource_profile_id: result.resource_profile_id, - canonical_relations: result.layout.canonical_relations, - applicable_relations: result.layout.applicable_relations, - not_applicable_relations: result.layout.not_evaluated_relations, - applicable_edges: result.layout.applicable_edges, - logical_assessments: result.observed_assessments, - matrix_digest: sealed.matrix_digest, - atomic_evidence: result.atomic_evidence, - }; - let passing_candidates = result.passing_candidates; - let evaluated = EvaluatedV1 { - candidates: result.domain.into_candidates(), - relations: result.relations, - layout: result.layout, - packed: sealed.packed, - domain, - relation_set_digest, - evaluation_id, - proof, - }; - if passing_candidates == 0 { - Ok(FeasibilityV1::Infeasible(evaluated)) - } else { - Ok(FeasibilityV1::Feasible(evaluated)) - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::wcag22::Wcag22CriterionV1; - use crate::wcag22_feasibility::OccurrenceId; - - #[test] - #[should_panic(expected = "sealed explicit candidate cardinality")] - fn assessment_iterator_cannot_hide_a_broken_sealed_cardinality() { - let candidates = [CandidateV1::new( - CandidateId::try_new("only-candidate").unwrap(), - Srgb8::new([0; 3]), - )]; - let relations = [RelationV1::applicable( - RelationId::try_new("relation").unwrap(), - OccurrenceId::try_new("occurrence").unwrap(), - Wcag22CriterionV1::Sc143TextDefault, - vec![Srgb8::new([255; 3])], - ) - .unwrap()]; - let matrix = [0_u8]; - let mut iterator = AssessmentIter { - candidates: &candidates, - cursor: AssessmentCursorV1 { - relations: &relations, - matrix: &matrix, - candidate_index: 0, - candidate_count: 2, - relation_index: 0, - adjacent_index: 0, - logical_index: 0, - remaining: 2, - }, - }; - - assert!(iterator.next().is_some()); - let _ = iterator.next(); - } -} diff --git a/crates/labcolors-core/src/wcag22_feasibility_tests.rs b/crates/labcolors-core/src/wcag22_feasibility_tests.rs index 835d90be..a04e5fa3 100644 --- a/crates/labcolors-core/src/wcag22_feasibility_tests.rs +++ b/crates/labcolors-core/src/wcag22_feasibility_tests.rs @@ -380,177 +380,6 @@ fn variable_domain_layout_is_contiguous_ceil_bit_arithmetic() { )); } -#[cfg(feature = "wcag22-explicit-feasibility")] -fn explicit_domain(count: u16) -> explicit::DomainRequestV1 { - explicit::DomainRequestV1::try_new( - (0..count) - .map(|index| { - explicit::CandidateV1::new( - explicit::CandidateId::try_new(format!("candidate/{index:03}")).unwrap(), - Srgb8::new([index as u8; 3]), - ) - }) - .collect(), - ) - .unwrap() -} - -#[test] -#[cfg(feature = "wcag22-explicit-feasibility")] -fn explicit_kernel_executes_exact_c_times_e_and_reserves_one_exact_buffer() { - let request = KernelRequestV1 { - domain: explicit_domain(3), - relations: vec![applicable_relation( - "three-edges", - vec![grey(0), grey(118), grey(255)], - Wcag22CriterionV1::Sc143TextDefault, - )], - resource_profile_id: PROFILE, - }; - let (mut evaluator, calls) = ProbeEvaluator::new(EvaluatorMode::AllPass); - let mut storage = ProbeStorage::default(); - let result = evaluate_domain_with(request, &mut evaluator, &mut storage).unwrap(); - let result = match result { - KernelResultV1::Evaluated(value) => value, - KernelResultV1::NotEvaluated(_) => panic!("three applicable edges must be evaluated"), - }; - - assert_eq!(result.layout.candidate_count, 3); - assert_eq!(result.layout.logical_assessments, 9); - assert_eq!(result.layout.failure_matrix_bytes, 2); - assert_eq!(result.layout.partition_bytes, 1); - assert_eq!(result.layout.packed_result_bytes, 3); - assert_eq!(calls.get(), 9); - assert_eq!(storage.reserve_calls, 1); - assert_eq!(storage.reserved_bytes, Some(3)); - assert_eq!(storage.writes, 9); - assert_eq!(storage.partition_writes, 3); - assert_eq!( - storage.finish_calls, 0, - "variable packing writes its partition in-place and has no finalization step", - ); -} - -#[test] -#[cfg(feature = "wcag22-explicit-feasibility")] -fn explicit_allocation_failure_precedes_the_first_atomic_call() { - let request = KernelRequestV1 { - domain: explicit_domain(3), - relations: vec![applicable_relation( - "allocation", - vec![grey(255)], - Wcag22CriterionV1::Sc143TextDefault, - )], - resource_profile_id: PROFILE, - }; - let (mut evaluator, calls) = ProbeEvaluator::new(EvaluatorMode::AllPass); - let mut storage = ProbeStorage::allocation_failure(); - let error = evaluate_domain_with(request, &mut evaluator, &mut storage).unwrap_err(); - - assert!(matches!( - error, - ErrorV1::AllocationFailed { - profile_id: PROFILE, - requested_bytes: 2, - } - )); - assert_eq!(storage.reserve_calls, 1); - assert_eq!(storage.reserved_bytes, Some(2)); - assert_eq!(calls.get(), 0); - assert_eq!(storage.writes, 0); - assert_eq!(storage.partition_writes, 0); - assert_eq!(storage.finish_calls, 0); -} - -#[test] -#[cfg(feature = "wcag22-explicit-feasibility")] -fn explicit_kernel_reuses_the_single_atomic_invariant_error_algebra() { - let request = KernelRequestV1 { - domain: explicit_domain(1), - relations: vec![applicable_relation( - "shared-error", - vec![grey(255)], - Wcag22CriterionV1::Sc143TextDefault, - )], - resource_profile_id: PROFILE, - }; - let (mut evaluator, calls) = ProbeEvaluator::new(EvaluatorMode::ForegroundMismatch); - let mut storage = ProbeStorage::default(); - let error = evaluate_domain_with(request, &mut evaluator, &mut storage).unwrap_err(); - - assert!(matches!( - error, - ErrorV1::EvaluatorInvariantViolation { - candidate, - adjacent, - violation: EvaluatorInvariantV1::InputMismatch, - .. - } if candidate == Srgb8::new([0; 3]) && adjacent == grey(255) - )); - assert_eq!(calls.get(), 1); - assert_eq!(storage.writes, 0); - assert_eq!(storage.finish_calls, 0); -} - -#[test] -#[cfg(feature = "wcag22-explicit-feasibility")] -fn explicit_compile_profile_maximum_completes_and_plus_one_fails_before_evaluation() { - let edges = PROFILE.limit(ResourceDimensionV1::ApplicableEdges); - let work_limit = PROFILE.limit(ResourceDimensionV1::LogicalAssessments); - let packed_limit = PROFILE.limit(ResourceDimensionV1::PackedResultBytes); - let candidates = work_limit / edges; - assert_eq!((candidates, edges), (256, 2_047)); - assert_eq!(work_limit % edges, 0); - - let adjacent = (0..edges) - .map(|code| Srgb8::new([0, (code >> 8) as u8, code as u8])) - .collect::>(); - let relation = || { - applicable_relation( - "maximum-envelope", - adjacent.clone(), - Wcag22CriterionV1::Sc143TextDefault, - ) - }; - - let request = KernelRequestV1 { - domain: explicit_domain(candidates as u16), - relations: vec![relation()], - resource_profile_id: PROFILE, - }; - let (mut evaluator, calls) = ProbeEvaluator::new(EvaluatorMode::AllPass); - let mut storage = ProbeStorage::default(); - let result = evaluate_domain_with(request, &mut evaluator, &mut storage).unwrap(); - let result = match result { - KernelResultV1::Evaluated(value) => value, - KernelResultV1::NotEvaluated(_) => panic!("the maximum applicable shape must evaluate"), - }; - assert_eq!(result.layout.logical_assessments, work_limit); - assert_eq!(result.layout.packed_result_bytes, packed_limit); - assert_eq!(storage.reserved_bytes, Some(packed_limit)); - assert_eq!(calls.get(), work_limit); - - let oversized = KernelRequestV1 { - domain: explicit_domain((candidates + 1) as u16), - relations: vec![relation()], - resource_profile_id: PROFILE, - }; - let (mut evaluator, calls) = ProbeEvaluator::new(EvaluatorMode::AllPass); - let mut storage = ProbeStorage::default(); - let error = evaluate_domain_with(oversized, &mut evaluator, &mut storage).unwrap_err(); - assert!(matches!( - error, - ErrorV1::ResourceLimitExceeded { - profile_id: PROFILE, - dimension: ResourceDimensionV1::LogicalAssessments, - requested, - limit, - } if requested == (candidates + 1) * edges && limit == work_limit - )); - assert_eq!(calls.get(), 0); - assert_eq!(storage.reserve_calls, 0); -} - #[test] fn all_not_applicable_has_zero_work_and_zero_packed_result_bytes() { let layout = checked_layout_v1( @@ -713,8 +542,6 @@ struct ProbeStorage { reserve_calls: u64, reserved_bytes: Option, writes: u64, - #[cfg(feature = "wcag22-explicit-feasibility")] - partition_writes: u64, finish_calls: u64, } @@ -758,16 +585,6 @@ impl DecisionStorage for ProbeStorage { if self.fail_write { Err(()) } else { Ok(()) } } - #[cfg(feature = "wcag22-explicit-feasibility")] - fn write_feasible_candidate( - &mut self, - _matrix_bytes: u64, - _candidate_index: u64, - ) -> Result<(), ()> { - self.partition_writes += 1; - Ok(()) - } - fn finish(&mut self, _partition: &[u8]) -> Result<(), ()> { self.finish_calls += 1; if self.fail_finish { Err(()) } else { Ok(()) } @@ -1158,57 +975,6 @@ fn matrix_partition_and_proof_invariants_reject_single_field_mutations() { ); } -#[test] -#[cfg(feature = "wcag22-explicit-feasibility")] -fn variable_matrix_and_partition_tail_bits_are_part_of_the_proof() { - let layout = checked_layout_for_domain_v1( - PROFILE, - 3, - RawInputCountsV1 { - raw_relations: 1, - raw_adjacent_entries: 3, - opaque_utf8_bytes: 3, - }, - CanonicalCountsV1 { - canonical_relations: 1, - applicable_relations: 1, - not_evaluated_relations: 0, - applicable_edges: 3, - }, - unlimited(), - u64::MAX, - ) - .unwrap(); - let matrix = [0_u8; 2]; - let partition = [0b0000_0111]; - let counters = EvaluationProofCountersV1 { - logical_assessments: 9, - passing_candidates: 3, - failing_candidates: 0, - }; - validate_variable_complete_result_v1(layout, &matrix, &partition, counters).unwrap(); - - let mut matrix_tail = matrix; - matrix_tail[1] |= 1 << 1; - assert!( - validate_variable_complete_result_v1(layout, &matrix_tail, &partition, counters).is_err(), - "unused matrix bits cannot carry covert state" - ); - - let partition_tail = [partition[0] | (1 << 3)]; - assert!( - validate_variable_complete_result_v1(layout, &matrix, &partition_tail, counters).is_err(), - "unused partition bits cannot carry covert state" - ); - - let mut used_cell = matrix; - used_cell[0] |= 1; - assert!( - validate_variable_complete_result_v1(layout, &used_cell, &partition, counters).is_err(), - "a row decision and its feasible bit must agree" - ); -} - fn hex(bytes: &[u8]) -> String { const HEX: &[u8; 16] = b"0123456789abcdef"; let mut output = String::with_capacity(bytes.len() * 2); diff --git a/crates/labcolors-core/tests/wcag22_explicit_feasibility.rs b/crates/labcolors-core/tests/wcag22_explicit_feasibility.rs deleted file mode 100644 index 01646ad5..00000000 --- a/crates/labcolors-core/tests/wcag22_explicit_feasibility.rs +++ /dev/null @@ -1,540 +0,0 @@ -//! Public RED contract for client-declared finite sRGB8 feasibility (#296-A). -//! -//! This target intentionally uses only the public Core API. The explicit set -//! owns opaque candidate identities; WCAG mathematics remains the existing -//! proof-bound atomic evaluator. - -use std::fs; -use std::process::Command; - -use labcolors_core::Srgb8; -use labcolors_core::wcag22::{Wcag22ClientDeclaredNotApplicableV1, Wcag22CriterionV1}; -use labcolors_core::wcag22_feasibility::explicit::{ - CandidateId, CandidateV1, DomainKindV1, DomainRequestV1, EvaluatedV1, FeasibilityV1, RequestV1, - evaluate, -}; -use labcolors_core::wcag22_feasibility::{ - DomainIdV1, ErrorV1, InvalidRequestV1, OccurrenceId, RelationId, RelationV1, - RequestV1 as NeutralRequestV1, ResourceProfileIdV1, evaluate as evaluate_neutral, -}; -use proptest::prelude::*; -use proptest::test_runner::{Config, RngAlgorithm, TestRng, TestRunner}; - -#[path = "../src/sha256.rs"] -#[allow(dead_code)] -mod fixture_sha256; - -const PROFILE: ResourceProfileIdV1 = ResourceProfileIdV1::Compile; -const IDENTITY_FIXTURE: &str = - include_str!("../contracts/wcag22-explicit-feasibility-identity-v1.json"); - -fn candidate(id: &str, emitted: [u8; 3]) -> CandidateV1 { - CandidateV1::new( - CandidateId::try_new(id).expect("test candidate ID is non-empty"), - Srgb8::new(emitted), - ) -} - -fn relation_id(value: &str) -> RelationId { - RelationId::try_new(value).expect("test relation ID is non-empty") -} - -fn occurrence_id(value: &str) -> OccurrenceId { - OccurrenceId::try_new(value).expect("test occurrence ID is non-empty") -} - -fn applicable(adjacent: Vec) -> RelationV1 { - RelationV1::applicable( - relation_id("contrast"), - occurrence_id("occurrence"), - Wcag22CriterionV1::Sc143TextDefault, - adjacent, - ) - .expect("test relation has adjacency") -} - -fn request(candidates: Vec, relations: Vec) -> RequestV1 { - let domain = DomainRequestV1::try_new(candidates).expect("test domain is non-empty"); - RequestV1::try_new(domain, relations, PROFILE).expect("test request has relations") -} - -fn evaluated(result: &FeasibilityV1) -> &EvaluatedV1 { - result.evaluated().expect("expected an evaluated terminal") -} - -fn hex(bytes: &[u8]) -> String { - const DIGITS: &[u8; 16] = b"0123456789abcdef"; - let mut output = String::with_capacity(bytes.len() * 2); - for byte in bytes { - output.push(char::from(DIGITS[usize::from(byte >> 4)])); - output.push(char::from(DIGITS[usize::from(byte & 0x0f)])); - } - output -} - -fn check_property(strategy: S, body: impl Fn(S::Value) -> Result<(), TestCaseError>) -where - S::Value: std::fmt::Debug, -{ - let mut runner = TestRunner::new_with_rng( - Config { - cases: 64, - failure_persistence: None, - ..Config::default() - }, - TestRng::deterministic_rng(RngAlgorithm::ChaCha), - ); - runner - .run(&strategy, body) - .expect("explicit feasibility property failed with a minimized counterexample"); -} - -fn assert_downstream_rejected(source: &str, expected_fragments: &[&str]) { - let temp = tempfile::tempdir().unwrap(); - fs::create_dir(temp.path().join("src")).unwrap(); - let package_dir = env!("CARGO_MANIFEST_DIR") - .replace('\\', "\\\\") - .replace('"', "\\\""); - fs::write( - temp.path().join("Cargo.toml"), - format!( - "[package]\nname = \"forge-explicit-feasibility\"\nversion = \"0.0.0\"\n\ - edition = \"2024\"\n\n[dependencies]\n\ - labcolors-core = {{ path = \"{package_dir}\" }}\n" - ), - ) - .unwrap(); - fs::write(temp.path().join("src/main.rs"), source).unwrap(); - - let output = Command::new(env!("CARGO")) - .arg("check") - .arg("--offline") - .env("CARGO_TARGET_DIR", temp.path().join("target")) - .current_dir(temp.path()) - .output() - .unwrap(); - assert!( - !output.status.success(), - "forged explicit proof unexpectedly compiled" - ); - let stderr = String::from_utf8_lossy(&output.stderr); - for fragment in expected_fragments { - assert!( - stderr.contains(fragment), - "expected downstream rejection mentioning {fragment:?}, stderr:\n{stderr}" - ); - } -} - -#[test] -fn empty_candidate_shapes_are_rejected_before_compilation() { - assert!(matches!( - CandidateId::try_new(""), - Err(InvalidRequestV1::EmptyCandidateId) - )); - assert!(matches!( - DomainRequestV1::try_new(Vec::new()), - Err(InvalidRequestV1::EmptyCandidates) - )); -} - -#[test] -fn candidate_order_is_canonical_but_duplicate_ids_are_never_deduplicated() { - let canonical = evaluate(request( - vec![candidate("alpha", [0x75; 3]), candidate("zeta", [0x76; 3])], - vec![applicable(vec![Srgb8::new([0; 3]), Srgb8::new([255; 3])])], - )) - .unwrap(); - let permuted = evaluate(request( - vec![candidate("zeta", [0x76; 3]), candidate("alpha", [0x75; 3])], - vec![applicable(vec![Srgb8::new([255; 3]), Srgb8::new([0; 3])])], - )) - .unwrap(); - - let canonical = evaluated(&canonical); - let permuted = evaluated(&permuted); - assert_eq!(canonical.domain_digest(), permuted.domain_digest()); - assert_eq!(canonical.evaluation_id(), permuted.evaluation_id()); - assert_eq!(canonical.failure_matrix(), permuted.failure_matrix()); - assert_eq!(canonical.proof().partition(), permuted.proof().partition()); - assert_eq!( - canonical - .candidates() - .iter() - .map(|value| value.candidate_id().as_str()) - .collect::>(), - ["alpha", "zeta"] - ); - - let error = evaluate(request( - vec![candidate("same", [1, 2, 3]), candidate("same", [4, 5, 6])], - vec![applicable(vec![Srgb8::new([255; 3])])], - )) - .expect_err("duplicate candidate IDs are contradictory, not duplicate noise"); - assert!(matches!( - error, - ErrorV1::InvalidRequest(InvalidRequestV1::DuplicateCandidateId { candidate_id }) - if candidate_id.as_str() == "same" - )); -} - -#[test] -fn same_emitted_bytes_under_distinct_ids_remain_distinct_matrix_rows() { - let result = evaluate(request( - vec![ - candidate("first", [0x75; 3]), - candidate("second", [0x75; 3]), - ], - vec![applicable(vec![Srgb8::new([0; 3])])], - )) - .unwrap(); - let record = evaluated(&result); - - assert_eq!(record.candidates().len(), 2); - assert_eq!(record.assessments().len(), 2); - assert_eq!(record.proof().domain().candidate_count(), 2); - assert_eq!( - record - .assessments() - .map(|value| (value.candidate_id().as_str(), value.emitted().bytes())) - .collect::>(), - [("first", [0x75; 3]), ("second", [0x75; 3])] - ); -} - -#[test] -fn variable_bit_layout_uses_one_contiguous_matrix_and_zero_tail_bits() { - let result = evaluate(request( - vec![ - candidate("a", [0; 3]), - candidate("b", [0x76; 3]), - candidate("c", [255; 3]), - ], - vec![applicable(vec![ - Srgb8::new([0; 3]), - Srgb8::new([0x76; 3]), - Srgb8::new([255; 3]), - ])], - )) - .unwrap(); - let record = evaluated(&result); - let proof = record.proof(); - - assert_eq!(proof.domain().kind(), DomainKindV1::ExplicitSrgb8Set); - assert_eq!(proof.domain().kind().key(), "explicit-srgb8-set-v1"); - assert_eq!(proof.domain().candidate_count(), 3); - assert_eq!(proof.applicable_edges(), 3); - assert_eq!(proof.logical_assessments(), 9); - assert_eq!(record.failure_matrix().len(), 2, "ceil(3*3/8)"); - assert_eq!(proof.partition().len(), 1, "ceil(3/8)"); - assert_eq!(record.failure_matrix()[1] & 0b1111_1110, 0); - assert_eq!(proof.partition()[0] & 0b1111_1000, 0); - assert_eq!(record.assessments().len(), 9); -} - -#[test] -fn variable_domain_crosses_the_256_candidate_boundary_without_truncation() { - const CANDIDATES: usize = 513; - let result = evaluate(request( - (0..CANDIDATES) - .map(|index| candidate(&format!("candidate/{index:03}"), [255; 3])) - .collect(), - vec![applicable(vec![Srgb8::new([0; 3])])], - )) - .unwrap(); - let record = evaluated(&result); - - assert_eq!(record.proof().domain().candidate_count(), 513); - assert_eq!(record.proof().logical_assessments(), 513); - assert_eq!(record.failure_matrix(), [0_u8; 65]); - assert_eq!(record.proof().partition()[..64], [u8::MAX; 64]); - assert_eq!(record.proof().partition()[64], 1); - assert_eq!(record.assessments().len(), 513); - assert_eq!(record.feasible_candidates().count(), 513); - assert_eq!( - record.candidates().last().unwrap().candidate_id().as_str(), - "candidate/512", - ); -} - -#[test] -fn full_explicit_neutral_set_matches_every_neutral_v1_physical_bit() { - let adjacent = vec![Srgb8::new([0x76; 3])]; - let neutral = evaluate_neutral( - NeutralRequestV1::try_new( - DomainIdV1::Srgb8NeutralAxis, - vec![applicable(adjacent.clone())], - PROFILE, - ) - .unwrap(), - ) - .unwrap(); - let explicit = evaluate(request( - (0_u16..256) - .map(|value| { - let value = value as u8; - candidate(&format!("grey/{value:03}"), [value; 3]) - }) - .collect(), - vec![applicable(adjacent)], - )) - .unwrap(); - let neutral = neutral.evaluated().unwrap(); - let explicit = evaluated(&explicit); - - assert_eq!(explicit.failure_matrix(), neutral.failure_matrix()); - assert_eq!(explicit.proof().partition(), neutral.proof().partition()); - assert_eq!( - explicit - .assessments() - .map(|value| (value.emitted(), value.adjacent(), value.decision())) - .collect::>(), - neutral - .assessments() - .map(|value| (value.candidate(), value.adjacent(), value.decision())) - .collect::>() - ); - assert_ne!(explicit.domain_digest(), neutral.domain_digest()); - assert_ne!(explicit.evaluation_id(), neutral.evaluation_id()); -} - -#[test] -fn no_applicable_relation_is_the_only_not_evaluated_terminal() { - let relation = RelationV1::not_applicable( - relation_id("ornament"), - occurrence_id("decorative"), - Wcag22ClientDeclaredNotApplicableV1::try_new("client-declared").unwrap(), - ); - let result = evaluate(request( - vec![candidate("one", [1, 2, 3]), candidate("two", [4, 5, 6])], - vec![relation], - )) - .unwrap(); - - assert!(result.is_not_evaluated()); - let record = result.not_evaluated().unwrap(); - assert_eq!(record.domain().kind(), DomainKindV1::ExplicitSrgb8Set); - assert_eq!(record.domain().candidate_count(), 2); - assert_eq!(record.candidates().len(), 2); - assert_eq!(record.relations().len(), 1); -} - -#[test] -fn candidate_ids_share_the_existing_opaque_byte_envelope_without_a_new_limit() { - let tiny_relation = || { - RelationV1::applicable( - relation_id("r"), - occurrence_id("o"), - Wcag22CriterionV1::Sc143TextDefault, - vec![Srgb8::new([255; 3])], - ) - .unwrap() - }; - let at_limit = evaluate(request( - vec![candidate(&"x".repeat(65_534), [0; 3])], - vec![tiny_relation()], - )); - assert!( - at_limit.is_ok(), - "candidate ID plus relation/occurrence is 65536 bytes" - ); - - let error = evaluate(request( - vec![candidate(&"x".repeat(65_535), [0; 3])], - vec![tiny_relation()], - )) - .expect_err("65537 aggregate opaque bytes must fail before evaluation"); - assert!(matches!( - error, - ErrorV1::ResourceLimitExceeded { - dimension: labcolors_core::wcag22_feasibility::ResourceDimensionV1::OpaqueUtf8Bytes, - requested: 65_537, - limit: 65_536, - .. - } - )); -} - -#[test] -fn property_variable_c_times_e_is_complete_canonical_and_exactly_packed() { - check_property((1_u8..18, 1_u8..10, any::()), |(c, e, reverse)| { - let mut candidates = (0..c) - .map(|index| { - candidate( - &format!("candidate/{index:03}"), - [index, index.wrapping_mul(17), index.wrapping_mul(31)], - ) - }) - .collect::>(); - if reverse { - candidates.reverse(); - } - let adjacent = (0..e) - .map(|index| Srgb8::new([index, index.wrapping_mul(11), 255 - index])) - .collect(); - let result = evaluate(request(candidates, vec![applicable(adjacent)])).unwrap(); - let record = evaluated(&result); - let work = u64::from(c) * u64::from(e); - let matrix_bytes = work.div_ceil(8) as usize; - let partition_bytes = u64::from(c).div_ceil(8) as usize; - - prop_assert_eq!(record.proof().logical_assessments(), work); - prop_assert_eq!(record.assessments().len(), work as usize); - prop_assert_eq!(record.failure_matrix().len(), matrix_bytes); - prop_assert_eq!(record.proof().partition().len(), partition_bytes); - prop_assert_eq!( - record.feasible_candidates().count() + record.infeasible_candidates().count(), - usize::from(c) - ); - prop_assert_eq!( - record - .candidates() - .windows(2) - .all(|pair| pair[0].candidate_id().as_str().as_bytes() - < pair[1].candidate_id().as_str().as_bytes()), - true - ); - let matrix_tail = (work % 8) as u8; - if matrix_tail != 0 { - let used = ((1_u16 << matrix_tail) - 1) as u8; - prop_assert_eq!(record.failure_matrix().last().unwrap() & !used, 0); - } - let partition_tail = c % 8; - if partition_tail != 0 { - let used = ((1_u16 << partition_tail) - 1) as u8; - prop_assert_eq!(record.proof().partition().last().unwrap() & !used, 0); - } - Ok(()) - }); -} - -#[test] -fn exact_unicode_bytes_and_emitted_bytes_both_belong_to_domain_identity() { - let compile = |id: &str, emitted: [u8; 3]| { - evaluate(request( - vec![candidate(id, emitted)], - vec![applicable(vec![Srgb8::new([255; 3])])], - )) - .unwrap() - }; - let composed = compile("caf\u{e9}", [1, 2, 3]); - let decomposed = compile("cafe\u{301}", [1, 2, 3]); - let changed_bytes = compile("caf\u{e9}", [1, 2, 4]); - - assert_ne!( - evaluated(&composed).domain_digest(), - evaluated(&decomposed).domain_digest(), - "Core must not normalize opaque Unicode IDs" - ); - assert_ne!( - evaluated(&composed).domain_digest(), - evaluated(&changed_bytes).domain_digest(), - "the emitted physical bytes are part of the declared-domain identity" - ); - assert_ne!( - evaluated(&composed).evaluation_id(), - evaluated(&changed_bytes).evaluation_id() - ); -} - -#[test] -fn production_identity_matches_the_independent_unicode_oracle_fixture() { - assert_eq!( - fixture_sha256::digest(IDENTITY_FIXTURE.as_bytes()).to_hex(), - "92a03a0ac961163b1e0e69f3166026544af3b7c3acf89fb4d13eaaa462952d7f" - ); - let applicable = RelationV1::applicable( - relation_id("alpha"), - occurrence_id("hover/🎨"), - Wcag22CriterionV1::Sc143TextDefault, - vec![ - Srgb8::new([255; 3]), - Srgb8::new([0; 3]), - Srgb8::new([118; 3]), - Srgb8::new([0; 3]), - ], - ) - .unwrap(); - let not_applicable = RelationV1::not_applicable( - relation_id("zeta"), - occurrence_id("ornament"), - Wcag22ClientDeclaredNotApplicableV1::try_new("client/не-применимо").unwrap(), - ); - let result = evaluate(request( - vec![ - candidate("🎨", [255, 128, 1]), - candidate("é", [18, 52, 86]), - candidate("海", [0, 0, 0]), - candidate("e\u{301}", [18, 52, 86]), - ], - vec![not_applicable, applicable], - )) - .unwrap(); - let record = evaluated(&result); - - assert_eq!( - record - .candidates() - .iter() - .map(|value| value.candidate_id().as_str()) - .collect::>(), - ["e\u{301}", "é", "海", "🎨"] - ); - assert_eq!(record.failure_matrix(), [0x5b, 0x0c]); - assert_eq!(record.proof().partition(), [0x00]); - assert_eq!( - hex(record.domain_digest().as_bytes()), - "71960b339a5af0421a5562e02aea28217b3f985c88a53f3244ea73f6c19258f4" - ); - assert_eq!( - hex(record.relation_set_digest().as_bytes()), - "990dbc58252dc518ccf63b2f4b63ef5ae227a2bed48dda9e5e5959f3e2477132" - ); - assert_eq!( - hex(record.evaluation_id().as_bytes()), - "59e69b867d8feb8afae4d28708bd353d0f3a0e89c12b0f34952f2e9a5e8be700" - ); - assert_eq!( - hex(record.proof().matrix_digest()), - "f414937d1b17276054be72790c34aef5a4eb5b6dc2132122599d47297bba5507" - ); -} - -#[test] -fn explicit_terminals_and_proof_views_cannot_be_forged_or_rewrapped_downstream() { - assert_downstream_rejected( - r#"use labcolors_core::wcag22_feasibility::explicit::{ - DomainDescriptorV1, EvaluatedV1, EvaluationProofV1, FeasibilityV1, - NotEvaluatedV1, -}; - -fn wrap_feasible(value: EvaluatedV1) -> FeasibilityV1 { - FeasibilityV1::Feasible(value) -} - -fn wrap_infeasible(value: EvaluatedV1) -> FeasibilityV1 { - FeasibilityV1::Infeasible(value) -} - -fn wrap_not_evaluated(value: NotEvaluatedV1) -> FeasibilityV1 { - FeasibilityV1::NotEvaluated(value) -} - -fn main() { - let _domain = DomainDescriptorV1 {}; - let _proof: EvaluationProofV1<'static> = EvaluationProofV1 {}; - let _evaluated = EvaluatedV1 {}; - let _not_evaluated = NotEvaluatedV1 {}; -} -"#, - &[ - "Feasible", - "Infeasible", - "NotEvaluated", - "DomainDescriptorV1", - "EvaluationProofV1", - "EvaluatedV1", - "NotEvaluatedV1", - ], - ); -} diff --git a/packages/colors/test/release-contract.test.mjs b/packages/colors/test/release-contract.test.mjs index 2697a69c..ea87d000 100644 --- a/packages/colors/test/release-contract.test.mjs +++ b/packages/colors/test/release-contract.test.mjs @@ -150,7 +150,7 @@ test("runtime and compiler resolve disjoint Core capability graphs", () => { assert.match(conformanceManifest, protocolEdge); assert.doesNotMatch(conformanceManifest, /features = \["wcag22-feasibility"\]/u); // Прямые Core-рёбра потребителей не несут capability-фич; explicit-домен - // после C4a заморожен внутри Core и потребителями не резолвится. + // вырезан целиком (C4b) — упоминание не смеет вернуться ни в один манифест. assert.doesNotMatch( conformanceManifest, /labcolors-core\/wcag22-explicit-feasibility/u, @@ -187,7 +187,7 @@ test("runtime and compiler resolve disjoint Core capability graphs", () => { ); assert.match( projection, - /core\["features"\]\.get\("default"\) != \[\n\s+"wcag22-feasibility",\n\s+"wcag22-explicit-feasibility",\n\s*\]:/u, + /core\["features"\]\.get\("default"\) != \["wcag22-feasibility"\]:/u, ); assert.match(projection, /protocol_core\["features"\] != \["wcag22-feasibility"\]/u); assert.match(projection, /core_dependency\["features"\]/u); @@ -202,7 +202,7 @@ test("runtime and compiler resolve disjoint Core capability graphs", () => { ); assert.match( projection, - /'labcolors-core feature "wcag22-explicit-feasibility"' in feature_tree/u, + /"wcag22-explicit" in feature_tree/u, ); assert.doesNotMatch( projection, diff --git a/scripts/verify_wcag22_explicit_feasibility_identity.py b/scripts/verify_wcag22_explicit_feasibility_identity.py deleted file mode 100755 index 502352cf..00000000 --- a/scripts/verify_wcag22_explicit_feasibility_identity.py +++ /dev/null @@ -1,484 +0,0 @@ -#!/usr/bin/env python3 -"""Independent byte oracle for explicit WCAG22 feasibility identities. - -This verifier imports no Rust code and performs no colour evaluation. It fixes -only the versioned cross-language byte grammar used by #296-A: exact UTF-8 -candidate ordering, the inherited relation grammar, explicit-domain identity -and explicit evaluation identity. -""" - -from __future__ import annotations - -import argparse -import copy -import hashlib -import json -from pathlib import Path -from typing import Any - - -ROOT = Path(__file__).resolve().parents[1] -FIXTURE = ROOT / ( - "crates/labcolors-core/contracts/" - "wcag22-explicit-feasibility-identity-v1.json" -) - -DOMAIN_SEPARATOR = ( - b"labcolors/wcag22-feasibility/domain/explicit-srgb8-set/v1\0" -) -RELATION_SEPARATOR = b"labcolors/wcag22-feasibility/relations/v1\0" -EVALUATION_SEPARATOR = ( - b"labcolors/wcag22-feasibility/evaluation/explicit-srgb8-set/v1\0" -) -DOMAIN_KIND = b"explicit-srgb8-set-v1" - -PROFILE_KEY = b"wcag22-srgb8-contrast-v1" -ARTIFACT_KEY = b"wcag22-srgb8-luminance-q55-v1" -BOUND_KEY = b"wcag22-srgb8-outward-q55-v1" -PROOF_KEY = b"wcag22-srgb8-full-domain-q55-v1" -PROOF_SHA256 = bytes.fromhex( - "d269e9de689009bb955788bf8762fce56680bf616fc0459b6526a367875a6a08" -) - -LAYOUT_FIELDS = ( - "canonicalRelations", - "applicableRelations", - "notApplicableRelations", - "applicableEdges", - "candidateCount", - "logicalAssessments", - "failureMatrixBytes", - "partitionBytes", - "packedResultBytes", -) - - -def require(condition: bool, message: str) -> None: - if not condition: - raise ValueError(message) - - -def u64(value: int) -> bytes: - require(type(value) is int and 0 <= value < 1 << 64, - f"not an unsigned 64-bit value: {value!r}") - return value.to_bytes(8, "big") - - -def field(value: bytes) -> bytes: - return u64(len(value)) + value - - -def sha256(value: bytes) -> bytes: - return hashlib.sha256(value).digest() - - -def rgb(value: Any, label: str) -> tuple[int, int, int]: - require( - isinstance(value, (list, tuple)) - and len(value) == 3 - and all(type(channel) is int and 0 <= channel <= 255 for channel in value), - f"{label} must be exactly three sRGB8 octets", - ) - return (value[0], value[1], value[2]) - - -def candidate_id_bytes(candidate: dict[str, Any]) -> bytes: - value = candidate.get("candidateId") - require(isinstance(value, str) and value != "", - "candidateId must be a non-empty UTF-8 string") - return value.encode("utf-8") - - -def canonical_candidates( - candidates: list[dict[str, Any]], -) -> list[dict[str, Any]]: - require(candidates != [], "explicit candidate set must be non-empty") - canonical = sorted(copy.deepcopy(candidates), key=candidate_id_bytes) - previous: bytes | None = None - for index, candidate in enumerate(canonical): - identity = candidate_id_bytes(candidate) - require(identity != previous, "duplicate exact candidate ID bytes") - candidate["emitted"] = list(rgb(candidate.get("emitted"), f"candidate[{index}]")) - previous = identity - return canonical - - -def domain_preimage_from_canonical( - candidates: list[dict[str, Any]], -) -> bytes: - preimage = bytearray(DOMAIN_SEPARATOR) - preimage += field(DOMAIN_KIND) - preimage += u64(len(candidates)) - for candidate in candidates: - preimage += field(candidate_id_bytes(candidate)) - preimage += bytes(rgb(candidate["emitted"], "canonical candidate emitted")) - return bytes(preimage) - - -def canonical_relation(relation: dict[str, Any]) -> dict[str, Any]: - result = copy.deepcopy(relation) - relation_id = result.get("relationId") - occurrence_id = result.get("occurrenceId") - require(isinstance(relation_id, str) and relation_id != "", - "relationId must be non-empty") - require(isinstance(occurrence_id, str) and occurrence_id != "", - "occurrenceId must be non-empty") - kind = result.get("kind") - if kind == "applicable": - criterion = result.get("criterion") - require(isinstance(criterion, str) and criterion != "", - "applicable criterion key must be non-empty") - adjacent = result.get("adjacent") - require(isinstance(adjacent, list) and adjacent != [], - "applicable relation needs adjacency") - result["adjacent"] = [ - list(value) - for value in sorted({rgb(value, "adjacent") for value in adjacent}) - ] - result.pop("reasonId", None) - elif kind == "notApplicable": - reason = result.get("reasonId") - require(isinstance(reason, str) and reason != "", - "NotApplicable reasonId must be non-empty") - result.pop("criterion", None) - result.pop("adjacent", None) - else: - raise ValueError(f"unknown relation kind: {kind!r}") - return result - - -def canonical_relations( - relations: list[dict[str, Any]], -) -> list[dict[str, Any]]: - require(relations != [], "relation set must be non-empty") - by_id: dict[bytes, dict[str, Any]] = {} - for relation in relations: - canonical = canonical_relation(relation) - identity = canonical["relationId"].encode("utf-8") - previous = by_id.get(identity) - require(previous is None or previous == canonical, - "same relation ID has conflicting canonical declarations") - by_id[identity] = canonical - return [by_id[identity] for identity in sorted(by_id)] - - -def relation_preimage(relations: list[dict[str, Any]]) -> bytes: - preimage = bytearray(RELATION_SEPARATOR) - preimage += u64(len(relations)) - for relation in relations: - kind = relation["kind"] - if kind == "applicable": - preimage += b"\x01" - preimage += field(relation["relationId"].encode("utf-8")) - preimage += field(relation["occurrenceId"].encode("utf-8")) - preimage += field(relation["criterion"].encode("utf-8")) - preimage += u64(len(relation["adjacent"])) - for adjacent in relation["adjacent"]: - preimage += bytes(rgb(adjacent, "canonical adjacent")) - else: - require(kind == "notApplicable", "canonical relation kind drifted") - preimage += b"\x02" - preimage += field(relation["relationId"].encode("utf-8")) - preimage += field(relation["occurrenceId"].encode("utf-8")) - preimage += field(relation["reasonId"].encode("utf-8")) - return bytes(preimage) - - -def evaluation_preimage( - domain_digest: bytes, - relation_digest: bytes, - layout: dict[str, int], - matrix: bytes, - partition: bytes, -) -> bytes: - require(len(domain_digest) == 32 and len(relation_digest) == 32, - "content digests must be SHA-256 values") - preimage = bytearray(EVALUATION_SEPARATOR) - preimage += domain_digest - preimage += relation_digest - for key in (PROFILE_KEY, ARTIFACT_KEY, BOUND_KEY, PROOF_KEY): - preimage += field(key) - preimage += PROOF_SHA256 - for name in LAYOUT_FIELDS: - require(name in layout, f"missing layout field {name}") - preimage += u64(layout[name]) - preimage += sha256(matrix) - preimage += field(partition) - return bytes(preimage) - - -def bit(value: bytes, index: int) -> bool: - return value[index // 8] & (1 << (index % 8)) != 0 - - -def validate_complete_fixture( - candidates: list[dict[str, Any]], - relations: list[dict[str, Any]], - layout: dict[str, int], - matrix: bytes, - partition: bytes, -) -> None: - candidate_count = len(candidates) - applicable = [value for value in relations if value["kind"] == "applicable"] - not_applicable = [ - value for value in relations if value["kind"] == "notApplicable" - ] - edges = sum(len(value["adjacent"]) for value in applicable) - work = candidate_count * edges - matrix_bytes = (work + 7) // 8 - partition_bytes = (candidate_count + 7) // 8 - expected = { - "canonicalRelations": len(relations), - "applicableRelations": len(applicable), - "notApplicableRelations": len(not_applicable), - "applicableEdges": edges, - "candidateCount": candidate_count, - "logicalAssessments": work, - "failureMatrixBytes": matrix_bytes, - "partitionBytes": partition_bytes, - "packedResultBytes": matrix_bytes + partition_bytes, - } - require(layout == expected, "fixture layout drifted from exact C x E laws") - require(len(matrix) == matrix_bytes, "matrix byte length is not ceil(C*E/8)") - require(len(partition) == partition_bytes, - "partition byte length is not ceil(C/8)") - if work % 8: - require(matrix[-1] >> (work % 8) == 0, "matrix tail bits must be zero") - if candidate_count % 8: - require(partition[-1] >> (candidate_count % 8) == 0, - "partition tail bits must be zero") - for candidate in range(candidate_count): - row_passes = all(not bit(matrix, candidate * edges + edge) - for edge in range(edges)) - require(bit(partition, candidate) == row_passes, - "partition must be the exact row-wise matrix reduction") - - -def fixture_model() -> dict[str, Any]: - # Deliberately shuffled. U+0065 U+0301 and U+00E9 look alike but are two - # exact, non-normalized client IDs and even carry the same physical bytes. - candidates = [ - {"candidateId": "🎨", "emitted": [255, 128, 1]}, - {"candidateId": "é", "emitted": [18, 52, 86]}, - {"candidateId": "海", "emitted": [0, 0, 0]}, - {"candidateId": "e\u0301", "emitted": [18, 52, 86]}, - ] - relations = [ - { - "relationId": "zeta", - "occurrenceId": "ornament", - "kind": "notApplicable", - "reasonId": "client/не-применимо", - }, - { - "relationId": "alpha", - "occurrenceId": "hover/🎨", - "kind": "applicable", - "criterion": "sc-1.4.3-text-default", - "adjacent": [[255, 255, 255], [0, 0, 0], [118, 118, 118], [0, 0, 0]], - }, - ] - return { - "candidates": candidates, - "relations": relations, - "layout": { - "canonicalRelations": 2, - "applicableRelations": 1, - "notApplicableRelations": 1, - "applicableEdges": 3, - "candidateCount": 4, - "logicalAssessments": 12, - "failureMatrixBytes": 2, - "partitionBytes": 1, - "packedResultBytes": 3, - }, - # Actual public WCAG path, candidate-major LSB0 rows: - # F,F,P | F,F,P | F,P,P | P,F,F. No candidate passes every edge. - "matrix": bytes.fromhex("5b0c"), - "partition": bytes.fromhex("00"), - } - - -def identities(model: dict[str, Any]) -> tuple[bytes, bytes, bytes]: - candidates = canonical_candidates(model["candidates"]) - relations = canonical_relations(model["relations"]) - domain = sha256(domain_preimage_from_canonical(candidates)) - relation = sha256(relation_preimage(relations)) - evaluation = sha256( - evaluation_preimage( - domain, - relation, - model["layout"], - model["matrix"], - model["partition"], - ) - ) - return domain, relation, evaluation - - -def build_fixture() -> dict[str, Any]: - model = fixture_model() - candidates = canonical_candidates(model["candidates"]) - relations = canonical_relations(model["relations"]) - validate_complete_fixture( - candidates, - relations, - model["layout"], - model["matrix"], - model["partition"], - ) - domain, relation, evaluation = identities(model) - return { - "schemaVersion": 1, - "artifactId": "wcag22-explicit-feasibility-identity-v1", - "encoding": { - "integer": "u64-big-endian", - "byteString": "u64-length-then-exact-bytes", - "candidateOrder": "lexicographic-exact-utf8-bytes-no-normalization", - "candidateRecord": "length-prefixed-id-then-three-srgb8-octets", - "relationGrammar": "wcag22-feasibility-relations-v1", - "matrixBitOrder": "candidate-major-contiguous-lsb0", - "partitionBitOrder": "canonical-candidate-index-lsb0", - "evaluationLayoutOrder": list(LAYOUT_FIELDS), - "partitionInEvaluation": "u64-length-then-exact-bytes", - }, - "fixture": { - "domainKind": DOMAIN_KIND.decode(), - "declaredCandidates": model["candidates"], - "canonicalCandidates": [ - { - **candidate, - "candidateIdUtf8Hex": candidate_id_bytes(candidate).hex(), - } - for candidate in candidates - ], - "canonicalRelations": relations, - "layout": model["layout"], - "failureMatrixHex": model["matrix"].hex(), - "matrixSha256": sha256(model["matrix"]).hex(), - "partitionHex": model["partition"].hex(), - }, - "expected": { - "domainDigestSha256": domain.hex(), - "relationSetDigestSha256": relation.hex(), - "evaluationIdSha256": evaluation.hex(), - }, - } - - -def canonical_bytes(value: dict[str, Any]) -> bytes: - return ( - json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) - + "\n" - ).encode("utf-8") - - -def mutation_self_tests() -> tuple[int, int]: - baseline_model = fixture_model() - baseline = identities(baseline_model) - mutation_checks = 0 - invariance_checks = 0 - - def changed(label: str, mutate: Any, expected_indices: tuple[int, ...]) -> None: - nonlocal mutation_checks - candidate = copy.deepcopy(baseline_model) - mutate(candidate) - observed = identities(candidate) - require( - all(observed[index] != baseline[index] for index in expected_indices), - f"mutation survived identity oracle: {label}", - ) - mutation_checks += 1 - - changed( - "candidate ID bytes", - lambda value: value["candidates"][1].__setitem__("candidateId", "É"), - (0, 2), - ) - changed( - "candidate emitted RGB", - lambda value: value["candidates"][1].__setitem__("emitted", [19, 52, 86]), - (0, 2), - ) - changed( - "layout count", - lambda value: value["layout"].__setitem__("logicalAssessments", 13), - (2,), - ) - changed( - "matrix byte", - lambda value: value.__setitem__( - "matrix", bytes([value["matrix"][0] ^ 1, value["matrix"][1]]) - ), - (2,), - ) - changed( - "partition byte", - lambda value: value.__setitem__("partition", bytes([value["partition"][0] ^ 1])), - (2,), - ) - changed( - "relation occurrence", - lambda value: value["relations"][1].__setitem__("occurrenceId", "hover/other"), - (1, 2), - ) - - # A caller permutation is intentionally invariant because Core sorts exact - # UTF-8 bytes. Hashing the reversed canonical records directly, however, - # is a wrong preimage and must not match the canonical domain identity. - permuted = copy.deepcopy(baseline_model) - permuted["candidates"].reverse() - require(identities(permuted) == baseline, - "declared candidate permutation changed canonical identity") - canonical = canonical_candidates(baseline_model["candidates"]) - wrong_order = sha256(domain_preimage_from_canonical(list(reversed(canonical)))) - require(wrong_order != baseline[0], - "non-canonical candidate record order matched canonical digest") - invariance_checks += 1 - - decomposed = "e\u0301".encode("utf-8") - composed = "é".encode("utf-8") - require(decomposed != composed, "normalization witness collapsed exact UTF-8 bytes") - require( - [candidate_id_bytes(value) for value in canonical] - == sorted([candidate_id_bytes(value) for value in canonical]), - "canonical candidate order is not exact byte order", - ) - invariance_checks += 1 - return mutation_checks, invariance_checks - - -def main() -> int: - parser = argparse.ArgumentParser() - parser.add_argument("--print", action="store_true", dest="print_fixture") - parser.add_argument("--self-test", action="store_true") - args = parser.parse_args() - - expected = canonical_bytes(build_fixture()) - if args.print_fixture: - print(expected.decode("utf-8"), end="") - return 0 - actual = FIXTURE.read_bytes() - require(actual == expected, - "explicit feasibility identity fixture drift; run --print and review exact bytes") - payload = json.loads(actual) - mutation_checks, invariance_checks = ( - mutation_self_tests() if args.self_test else (0, 0) - ) - print( - "WCAG22 explicit feasibility identity oracle: PASS; " - f"domain={payload['expected']['domainDigestSha256']}; " - f"relations={payload['expected']['relationSetDigestSha256']}; " - f"evaluation={payload['expected']['evaluationIdSha256']}; " - f"fixture_sha256={sha256(actual).hex()}; " - f"mutation_checks={mutation_checks}; " - f"invariance_checks={invariance_checks}" - ) - return 0 - - -if __name__ == "__main__": - try: - raise SystemExit(main()) - except (OSError, ValueError) as error: - raise SystemExit(f"WCAG22 explicit feasibility identity oracle: {error}") from error