@verndale/accessibility-standards 1.0.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/OPERATING.md +3 -1
- package/README.md +4 -0
- package/bin/accessibility-standards.mjs +1 -1
- package/dist/manifest.json +159 -101
- package/dist/profiles/ai-orchestration/coverage-manifest.json +724 -68
- package/dist/profiles/ai-orchestration/implementation/accessibility.md +825 -10
- package/dist/profiles/ai-orchestration/pattern-contracts.json +1015 -343
- package/dist/profiles/ai-orchestration/patterns/accordion.md +14 -0
- package/dist/profiles/ai-orchestration/patterns/alert.md +14 -0
- package/dist/profiles/ai-orchestration/patterns/carousel.md +14 -0
- package/dist/profiles/ai-orchestration/patterns/checkbox.md +27 -0
- package/dist/profiles/ai-orchestration/patterns/combobox.md +14 -0
- package/dist/profiles/ai-orchestration/patterns/consequential-submission.md +15 -1
- package/dist/profiles/ai-orchestration/patterns/dialog.md +14 -0
- package/dist/profiles/ai-orchestration/patterns/disclosure.md +14 -0
- package/dist/profiles/ai-orchestration/patterns/dynamic-status.md +14 -0
- package/dist/profiles/ai-orchestration/patterns/field.md +15 -0
- package/dist/profiles/ai-orchestration/patterns/image.md +14 -0
- package/dist/profiles/ai-orchestration/patterns/listbox.md +14 -0
- package/dist/profiles/ai-orchestration/patterns/media-player.md +17 -0
- package/dist/profiles/ai-orchestration/patterns/menu-button.md +14 -0
- package/dist/profiles/ai-orchestration/patterns/menu.md +30 -0
- package/dist/profiles/ai-orchestration/patterns/pagination.md +14 -0
- package/dist/profiles/ai-orchestration/patterns/popover.md +15 -0
- package/dist/profiles/ai-orchestration/patterns/progress.md +15 -0
- package/dist/profiles/ai-orchestration/patterns/radio-group.md +27 -0
- package/dist/profiles/ai-orchestration/patterns/slider.md +31 -0
- package/dist/profiles/ai-orchestration/patterns/spinbutton.md +28 -0
- package/dist/profiles/ai-orchestration/patterns/tabs.md +14 -0
- package/dist/profiles/ai-orchestration/patterns/tooltip.md +14 -0
- package/dist/profiles/ai-orchestration/patterns/tree-view.md +31 -0
- package/dist/profiles/ai-orchestration/patterns/validation.md +15 -0
- package/dist/profiles/ai-orchestration/review/accessibility.md +21 -6
- package/dist/profiles/ai-orchestration/semantics/accessible-authentication.md +13 -0
- package/dist/profiles/ai-orchestration/semantics/accessible-name.md +8 -1
- package/dist/profiles/ai-orchestration/semantics/alert.md +7 -0
- package/dist/profiles/ai-orchestration/semantics/consequential-action.md +6 -1
- package/dist/profiles/ai-orchestration/semantics/consistent-help.md +13 -0
- package/dist/profiles/ai-orchestration/semantics/data-table.md +7 -1
- package/dist/profiles/ai-orchestration/semantics/dragging-alternative.md +13 -0
- package/dist/profiles/ai-orchestration/semantics/expanded-state.md +6 -0
- package/dist/profiles/ai-orchestration/semantics/focus.initial.md +4 -0
- package/dist/profiles/ai-orchestration/semantics/focus.return.md +4 -0
- package/dist/profiles/ai-orchestration/semantics/focus.visible.md +8 -1
- package/dist/profiles/ai-orchestration/semantics/form-error.md +8 -1
- package/dist/profiles/ai-orchestration/semantics/form-label.md +7 -0
- package/dist/profiles/ai-orchestration/semantics/form-required.md +7 -0
- package/dist/profiles/ai-orchestration/semantics/headings.md +7 -0
- package/dist/profiles/ai-orchestration/semantics/keyboard.escape.md +4 -0
- package/dist/profiles/ai-orchestration/semantics/keyboard.md +7 -0
- package/dist/profiles/ai-orchestration/semantics/landmarks.md +6 -0
- package/dist/profiles/ai-orchestration/semantics/live-status.md +7 -1
- package/dist/profiles/ai-orchestration/semantics/media-alternatives.md +11 -0
- package/dist/profiles/ai-orchestration/semantics/motion-control.md +8 -1
- package/dist/profiles/ai-orchestration/semantics/name-role-value.md +6 -1
- package/dist/profiles/ai-orchestration/semantics/native-dialog.md +7 -0
- package/dist/profiles/ai-orchestration/semantics/native-elements.md +5 -0
- package/dist/profiles/ai-orchestration/semantics/non-text-alternative.md +5 -0
- package/dist/profiles/ai-orchestration/semantics/redundant-entry.md +13 -0
- package/dist/profiles/ai-orchestration/semantics/roles-states-properties.md +6 -0
- package/dist/profiles/ai-orchestration/semantics/selected-state.md +6 -0
- package/dist/profiles/ai-orchestration/semantics/target-size.md +6 -1
- package/dist/profiles/ai-orchestration/semantics.json +843 -100
- package/dist/profiles/ai-orchestration/testing/accessibility.md +33 -3
- package/dist/profiles/ai-orchestration/ui-design-brain-bindings.json +836 -0
- package/dist/profiles/conductor/a11y-semantics.md +270 -10
- package/dist/profiles/conductor/accessibility-quality.md +557 -1
- package/dist/profiles/conductor/applicability-matrix.json +270 -230
- package/dist/profiles/conductor/coverage-manifest.json +724 -68
- package/dist/profiles/conductor/pattern-contracts.json +1015 -343
- package/dist/profiles/conductor/patterns/accordion.json +42 -24
- package/dist/profiles/conductor/patterns/alert.json +36 -18
- package/dist/profiles/conductor/patterns/carousel.json +43 -25
- package/dist/profiles/conductor/patterns/checkbox.json +67 -0
- package/dist/profiles/conductor/patterns/combobox.json +44 -26
- package/dist/profiles/conductor/patterns/consequential-submission.json +41 -22
- package/dist/profiles/conductor/patterns/dialog.json +44 -26
- package/dist/profiles/conductor/patterns/disclosure.json +40 -22
- package/dist/profiles/conductor/patterns/dynamic-status.json +38 -19
- package/dist/profiles/conductor/patterns/field.json +58 -22
- package/dist/profiles/conductor/patterns/image.json +48 -19
- package/dist/profiles/conductor/patterns/listbox.json +42 -24
- package/dist/profiles/conductor/patterns/media-player.json +76 -26
- package/dist/profiles/conductor/patterns/menu-button.json +41 -23
- package/dist/profiles/conductor/patterns/menu.json +78 -0
- package/dist/profiles/conductor/patterns/pagination.json +41 -22
- package/dist/profiles/conductor/patterns/popover.json +49 -23
- package/dist/profiles/conductor/patterns/progress.json +45 -20
- package/dist/profiles/conductor/patterns/radio-group.json +69 -0
- package/dist/profiles/conductor/patterns/slider.json +84 -0
- package/dist/profiles/conductor/patterns/spinbutton.json +74 -0
- package/dist/profiles/conductor/patterns/tabs.json +41 -23
- package/dist/profiles/conductor/patterns/tooltip.json +38 -20
- package/dist/profiles/conductor/patterns/tree-view.json +77 -0
- package/dist/profiles/conductor/patterns/validation.json +59 -23
- package/dist/profiles/conductor/semantics.json +843 -100
- package/dist/profiles/conductor/ui-design-brain-bindings.json +836 -0
- package/dist/projection-manifest.json +123 -82
- package/dist/requirements-to-implementation.md +8 -0
- package/lib/build-manifest.mjs +12 -6
- package/lib/build.mjs +21 -7
- package/lib/compile-applicability.mjs +131 -10
- package/lib/coverage.mjs +20 -0
- package/lib/digest.mjs +8 -0
- package/lib/index.mjs +1 -1
- package/lib/load.mjs +44 -1
- package/lib/provenance.mjs +51 -0
- package/lib/render.mjs +115 -31
- package/lib/sync.mjs +242 -28
- package/lib/validate.mjs +557 -24
- package/package.json +27 -10
- package/profiles/ai-orchestration/profile.json +1 -1
- package/profiles/ai-orchestration/templates/patterns/README.md +1 -1
- package/profiles/ai-orchestration/templates/review/README.md +1 -1
- package/profiles/ai-orchestration/templates/semantics/README.md +1 -1
- package/profiles/ai-orchestration/templates/testing/README.md +1 -1
- package/profiles/conductor/profile.json +1 -1
- package/profiles/conductor/templates/evidence/README.md +1 -1
- package/profiles/conductor/templates/patterns/README.md +1 -1
- package/profiles/conductor/templates/semantics/README.md +1 -1
- package/schemas/consumer-config.schema.json +1 -1
- package/schemas/contract.schema.json +1 -0
- package/schemas/manifest.schema.json +1 -1
- package/schemas/pattern.schema.json +98 -1
- package/schemas/profile.schema.json +1 -1
- package/schemas/projection-manifest.schema.json +84 -1
- package/schemas/semantic.schema.json +26 -1
- package/schemas/standards-reference.schema.json +105 -0
- package/schemas/ui-design-brain-bindings.schema.json +86 -0
- package/schemas/wcag-coverage.schema.json +93 -0
- package/src/applicability/facts.yml +27 -1
- package/src/applicability/matrix.yml +8 -9
- package/src/applicability/ui-design-brain-bindings.yml +196 -0
- package/src/contract.yml +1 -0
- package/src/coverage/wcag-2.2.yml +68 -0
- package/src/patterns/disclosure/accordion.yml +1 -1
- package/src/patterns/disclosure/disclosure.yml +1 -1
- package/src/patterns/feedback/alert.yml +1 -1
- package/src/patterns/feedback/dynamic-status.yml +1 -1
- package/src/patterns/feedback/progress.yml +1 -1
- package/src/patterns/forms/consequential-submission.yml +1 -1
- package/src/patterns/forms/field.yml +1 -1
- package/src/patterns/forms/spinbutton.yml +1 -0
- package/src/patterns/forms/validation.yml +1 -1
- package/src/patterns/media/carousel.yml +1 -1
- package/src/patterns/media/image.yml +1 -1
- package/src/patterns/media/media-player.yml +1 -1
- package/src/patterns/navigation/menu-button.yml +1 -1
- package/src/patterns/navigation/menu.yml +1 -0
- package/src/patterns/navigation/pagination.yml +1 -1
- package/src/patterns/navigation/tabs.yml +1 -1
- package/src/patterns/navigation/tree-view.yml +1 -0
- package/src/patterns/overlays/dialog.yml +1 -1
- package/src/patterns/overlays/popover.yml +1 -1
- package/src/patterns/overlays/tooltip.yml +1 -1
- package/src/patterns/selection/checkbox.yml +1 -0
- package/src/patterns/selection/combobox.yml +1 -1
- package/src/patterns/selection/listbox.yml +1 -1
- package/src/patterns/selection/radio-group.yml +1 -0
- package/src/patterns/selection/slider.yml +1 -0
- package/src/semantics/accessible-names.yml +1 -1
- package/src/semantics/forms.yml +1 -1
- package/src/semantics/headings-landmarks.yml +1 -1
- package/src/semantics/keyboard-focus.yml +1 -1
- package/src/semantics/live-regions.yml +1 -1
- package/src/semantics/media.yml +1 -1
- package/src/semantics/native-elements.yml +1 -1
- package/src/semantics/roles-states-properties.yml +1 -1
- package/src/semantics/tables.yml +1 -1
- package/src/semantics/wcag-2.2.yml +1 -0
- package/src/sources.yml +1 -1
package/lib/validate.mjs
CHANGED
|
@@ -1,31 +1,564 @@
|
|
|
1
|
-
import { compileApplicability } from './compile-applicability.mjs';
|
|
1
|
+
import { compileApplicability, referencedFacts, validateExpression } from './compile-applicability.mjs';
|
|
2
|
+
|
|
3
|
+
const SEMVER = /^\d+\.\d+\.\d+$/;
|
|
4
|
+
const RECORD_ID = /^[a-z][a-z0-9.-]+$/;
|
|
5
|
+
const FACT_ID = /^[a-z][a-z0-9_-]*(?:\.[a-z][a-z0-9_-]*)+$/;
|
|
6
|
+
const RECORD_PREFIX = {
|
|
7
|
+
foundation: 'foundation.',
|
|
8
|
+
semantic: 'semantics.',
|
|
9
|
+
pattern: 'pattern.',
|
|
10
|
+
policy: 'policy.'
|
|
11
|
+
};
|
|
12
|
+
const WCAG_2_2_CRITERIA = [
|
|
13
|
+
['1.1.1', 'Non-text Content', 'A', 'non-text-content'],
|
|
14
|
+
['1.2.1', 'Audio-only and Video-only (Prerecorded)', 'A', 'audio-only-and-video-only-prerecorded'],
|
|
15
|
+
['1.2.2', 'Captions (Prerecorded)', 'A', 'captions-prerecorded'],
|
|
16
|
+
['1.2.3', 'Audio Description or Media Alternative (Prerecorded)', 'A', 'audio-description-or-media-alternative-prerecorded'],
|
|
17
|
+
['1.2.4', 'Captions (Live)', 'AA', 'captions-live'],
|
|
18
|
+
['1.2.5', 'Audio Description (Prerecorded)', 'AA', 'audio-description-prerecorded'],
|
|
19
|
+
['1.2.6', 'Sign Language (Prerecorded)', 'AAA', 'sign-language-prerecorded'],
|
|
20
|
+
['1.2.7', 'Extended Audio Description (Prerecorded)', 'AAA', 'extended-audio-description-prerecorded'],
|
|
21
|
+
['1.2.8', 'Media Alternative (Prerecorded)', 'AAA', 'media-alternative-prerecorded'],
|
|
22
|
+
['1.2.9', 'Audio-only (Live)', 'AAA', 'audio-only-live'],
|
|
23
|
+
['1.3.1', 'Info and Relationships', 'A', 'info-and-relationships'],
|
|
24
|
+
['1.3.2', 'Meaningful Sequence', 'A', 'meaningful-sequence'],
|
|
25
|
+
['1.3.3', 'Sensory Characteristics', 'A', 'sensory-characteristics'],
|
|
26
|
+
['1.3.4', 'Orientation', 'AA', 'orientation'],
|
|
27
|
+
['1.3.5', 'Identify Input Purpose', 'AA', 'identify-input-purpose'],
|
|
28
|
+
['1.3.6', 'Identify Purpose', 'AAA', 'identify-purpose'],
|
|
29
|
+
['1.4.1', 'Use of Color', 'A', 'use-of-color'],
|
|
30
|
+
['1.4.2', 'Audio Control', 'A', 'audio-control'],
|
|
31
|
+
['1.4.3', 'Contrast (Minimum)', 'AA', 'contrast-minimum'],
|
|
32
|
+
['1.4.4', 'Resize Text', 'AA', 'resize-text'],
|
|
33
|
+
['1.4.5', 'Images of Text', 'AA', 'images-of-text'],
|
|
34
|
+
['1.4.6', 'Contrast (Enhanced)', 'AAA', 'contrast-enhanced'],
|
|
35
|
+
['1.4.7', 'Low or No Background Audio', 'AAA', 'low-or-no-background-audio'],
|
|
36
|
+
['1.4.8', 'Visual Presentation', 'AAA', 'visual-presentation'],
|
|
37
|
+
['1.4.9', 'Images of Text (No Exception)', 'AAA', 'images-of-text-no-exception'],
|
|
38
|
+
['1.4.10', 'Reflow', 'AA', 'reflow'],
|
|
39
|
+
['1.4.11', 'Non-text Contrast', 'AA', 'non-text-contrast'],
|
|
40
|
+
['1.4.12', 'Text Spacing', 'AA', 'text-spacing'],
|
|
41
|
+
['1.4.13', 'Content on Hover or Focus', 'AA', 'content-on-hover-or-focus'],
|
|
42
|
+
['2.1.1', 'Keyboard', 'A', 'keyboard'],
|
|
43
|
+
['2.1.2', 'No Keyboard Trap', 'A', 'no-keyboard-trap'],
|
|
44
|
+
['2.1.3', 'Keyboard (No Exception)', 'AAA', 'keyboard-no-exception'],
|
|
45
|
+
['2.1.4', 'Character Key Shortcuts', 'A', 'character-key-shortcuts'],
|
|
46
|
+
['2.2.1', 'Timing Adjustable', 'A', 'timing-adjustable'],
|
|
47
|
+
['2.2.2', 'Pause, Stop, Hide', 'A', 'pause-stop-hide'],
|
|
48
|
+
['2.2.3', 'No Timing', 'AAA', 'no-timing'],
|
|
49
|
+
['2.2.4', 'Interruptions', 'AAA', 'interruptions'],
|
|
50
|
+
['2.2.5', 'Re-authenticating', 'AAA', 're-authenticating'],
|
|
51
|
+
['2.2.6', 'Timeouts', 'AAA', 'timeouts'],
|
|
52
|
+
['2.3.1', 'Three Flashes or Below Threshold', 'A', 'three-flashes-or-below-threshold'],
|
|
53
|
+
['2.3.2', 'Three Flashes', 'AAA', 'three-flashes'],
|
|
54
|
+
['2.3.3', 'Animation from Interactions', 'AAA', 'animation-from-interactions'],
|
|
55
|
+
['2.4.1', 'Bypass Blocks', 'A', 'bypass-blocks'],
|
|
56
|
+
['2.4.2', 'Page Titled', 'A', 'page-titled'],
|
|
57
|
+
['2.4.3', 'Focus Order', 'A', 'focus-order'],
|
|
58
|
+
['2.4.4', 'Link Purpose (In Context)', 'A', 'link-purpose-in-context'],
|
|
59
|
+
['2.4.5', 'Multiple Ways', 'AA', 'multiple-ways'],
|
|
60
|
+
['2.4.6', 'Headings and Labels', 'AA', 'headings-and-labels'],
|
|
61
|
+
['2.4.7', 'Focus Visible', 'AA', 'focus-visible'],
|
|
62
|
+
['2.4.8', 'Location', 'AAA', 'location'],
|
|
63
|
+
['2.4.9', 'Link Purpose (Link Only)', 'AAA', 'link-purpose-link-only'],
|
|
64
|
+
['2.4.10', 'Section Headings', 'AAA', 'section-headings'],
|
|
65
|
+
['2.4.11', 'Focus Not Obscured (Minimum)', 'AA', 'focus-not-obscured-minimum'],
|
|
66
|
+
['2.4.12', 'Focus Not Obscured (Enhanced)', 'AAA', 'focus-not-obscured-enhanced'],
|
|
67
|
+
['2.4.13', 'Focus Appearance', 'AAA', 'focus-appearance'],
|
|
68
|
+
['2.5.1', 'Pointer Gestures', 'A', 'pointer-gestures'],
|
|
69
|
+
['2.5.2', 'Pointer Cancellation', 'A', 'pointer-cancellation'],
|
|
70
|
+
['2.5.3', 'Label in Name', 'A', 'label-in-name'],
|
|
71
|
+
['2.5.4', 'Motion Actuation', 'A', 'motion-actuation'],
|
|
72
|
+
['2.5.5', 'Target Size (Enhanced)', 'AAA', 'target-size-enhanced'],
|
|
73
|
+
['2.5.6', 'Concurrent Input Mechanisms', 'AAA', 'concurrent-input-mechanisms'],
|
|
74
|
+
['2.5.7', 'Dragging Movements', 'AA', 'dragging-movements'],
|
|
75
|
+
['2.5.8', 'Target Size (Minimum)', 'AA', 'target-size-minimum'],
|
|
76
|
+
['3.1.1', 'Language of Page', 'A', 'language-of-page'],
|
|
77
|
+
['3.1.2', 'Language of Parts', 'AA', 'language-of-parts'],
|
|
78
|
+
['3.1.3', 'Unusual Words', 'AAA', 'unusual-words'],
|
|
79
|
+
['3.1.4', 'Abbreviations', 'AAA', 'abbreviations'],
|
|
80
|
+
['3.1.5', 'Reading Level', 'AAA', 'reading-level'],
|
|
81
|
+
['3.1.6', 'Pronunciation', 'AAA', 'pronunciation'],
|
|
82
|
+
['3.2.1', 'On Focus', 'A', 'on-focus'],
|
|
83
|
+
['3.2.2', 'On Input', 'A', 'on-input'],
|
|
84
|
+
['3.2.3', 'Consistent Navigation', 'AA', 'consistent-navigation'],
|
|
85
|
+
['3.2.4', 'Consistent Identification', 'AA', 'consistent-identification'],
|
|
86
|
+
['3.2.5', 'Change on Request', 'AAA', 'change-on-request'],
|
|
87
|
+
['3.2.6', 'Consistent Help', 'A', 'consistent-help'],
|
|
88
|
+
['3.3.1', 'Error Identification', 'A', 'error-identification'],
|
|
89
|
+
['3.3.2', 'Labels or Instructions', 'A', 'labels-or-instructions'],
|
|
90
|
+
['3.3.3', 'Error Suggestion', 'AA', 'error-suggestion'],
|
|
91
|
+
['3.3.4', 'Error Prevention (Legal, Financial, Data)', 'AA', 'error-prevention-legal-financial-data'],
|
|
92
|
+
['3.3.5', 'Help', 'AAA', 'help'],
|
|
93
|
+
['3.3.6', 'Error Prevention (All)', 'AAA', 'error-prevention-all'],
|
|
94
|
+
['3.3.7', 'Redundant Entry', 'A', 'redundant-entry'],
|
|
95
|
+
['3.3.8', 'Accessible Authentication (Minimum)', 'AA', 'accessible-authentication-minimum'],
|
|
96
|
+
['3.3.9', 'Accessible Authentication (Enhanced)', 'AAA', 'accessible-authentication-enhanced'],
|
|
97
|
+
['4.1.2', 'Name, Role, Value', 'A', 'name-role-value'],
|
|
98
|
+
['4.1.3', 'Status Messages', 'AA', 'status-messages']
|
|
99
|
+
];
|
|
100
|
+
const WCAG_2_2_BY_ID = new Map(WCAG_2_2_CRITERIA.map(([id, title, level, slug]) => [id, { title, level, slug }]));
|
|
101
|
+
const WCAG_2_2_AA_CRITERIA = WCAG_2_2_CRITERIA.filter(([, , level]) => level !== 'AAA');
|
|
102
|
+
const WCAG_2_2_AA_BY_ID = new Map(WCAG_2_2_AA_CRITERIA.map(([id, title, level, slug]) => [id, { title, level, slug }]));
|
|
103
|
+
const REQUIRED_DEFERRED_PATTERN_IDS = ['alert-dialog', 'grid', 'switch', 'toolbar', 'treegrid'];
|
|
104
|
+
const KNOWN_AUTHORITIES = new Map([
|
|
105
|
+
['wcag-2.2', { url: 'https://www.w3.org/TR/WCAG22/', normative: true }],
|
|
106
|
+
['wcag-2.2-understanding', { url: 'https://www.w3.org/WAI/WCAG22/Understanding/', normative: false }],
|
|
107
|
+
['wai-aria-1.2', { url: 'https://www.w3.org/TR/wai-aria-1.2/', normative: true }],
|
|
108
|
+
['aria-apg', { url: 'https://www.w3.org/WAI/ARIA/apg/', normative: false }]
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
function isRecord(value) {
|
|
112
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function isNonEmptyString(value) {
|
|
116
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function validateStringArray(label, value, errors, { required = true } = {}) {
|
|
120
|
+
if (!Array.isArray(value) || (required && value.length === 0) || value.some((item) => !isNonEmptyString(item))) {
|
|
121
|
+
errors.push(`${label} must be ${required ? 'a non-empty' : 'an'} array of non-empty strings`);
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
if (new Set(value).size !== value.length) errors.push(`${label} must not contain duplicates`);
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function validateAllowedKeys(label, value, allowed, errors) {
|
|
129
|
+
if (!isRecord(value)) return;
|
|
130
|
+
const extras = Object.keys(value).filter((key) => !allowed.includes(key));
|
|
131
|
+
if (extras.length) errors.push(`${label} has unsupported fields: ${extras.join(', ')}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function validateStandardsReferences(record, authorities, errors) {
|
|
135
|
+
const label = record?.id ?? '<malformed record>';
|
|
136
|
+
const refs = record?.standards_refs;
|
|
137
|
+
if (!Array.isArray(refs) || refs.length === 0) {
|
|
138
|
+
errors.push(`${label} standards_refs must be a non-empty array`);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const identities = [];
|
|
142
|
+
const seen = new Set();
|
|
143
|
+
for (const ref of refs) {
|
|
144
|
+
if (!isRecord(ref)) {
|
|
145
|
+
errors.push(`${label} has a malformed standards reference`);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const allowedKeys = ['authority', 'identifier', 'level', 'normative', 'url'];
|
|
149
|
+
const extraKeys = Object.keys(ref).filter((key) => !allowedKeys.includes(key));
|
|
150
|
+
if (extraKeys.length) errors.push(`${label} standards reference has unsupported fields: ${extraKeys.join(', ')}`);
|
|
151
|
+
if (!isNonEmptyString(ref.authority) || !isNonEmptyString(ref.identifier) || !/^https:\/\//.test(ref.url ?? '') || typeof ref.normative !== 'boolean') {
|
|
152
|
+
errors.push(`${label} has a malformed standards reference`);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (ref.level !== undefined && !['A', 'AA', 'AAA'].includes(ref.level)) errors.push(`${label} standards reference ${ref.authority} ${ref.identifier} has invalid level ${ref.level}`);
|
|
156
|
+
const identity = `${ref.authority}\u0000${ref.identifier}\u0000${ref.url}`;
|
|
157
|
+
const referenceKey = `${ref.authority}\u0000${ref.identifier}`;
|
|
158
|
+
identities.push(identity);
|
|
159
|
+
if (seen.has(referenceKey)) errors.push(`${label} has duplicate standards reference ${ref.authority} ${ref.identifier}`);
|
|
160
|
+
seen.add(referenceKey);
|
|
161
|
+
const authority = authorities.get(ref.authority);
|
|
162
|
+
if (!authority) {
|
|
163
|
+
errors.push(`${label} references unknown standards authority ${ref.authority}`);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (ref.normative !== authority.normative) errors.push(`${label} standards reference ${ref.authority} ${ref.identifier} has normative=${ref.normative}; expected ${authority.normative}`);
|
|
167
|
+
if (!ref.url.startsWith(authority.url)) errors.push(`${label} standards reference ${ref.authority} ${ref.identifier} URL is outside authority ${authority.url}`);
|
|
168
|
+
if (ref.authority === 'wcag-2.2' || ref.authority === 'wcag-2.2-understanding') {
|
|
169
|
+
const criterion = WCAG_2_2_BY_ID.get(ref.identifier);
|
|
170
|
+
if (!criterion) {
|
|
171
|
+
errors.push(`${label} standards reference ${ref.identifier} is not a WCAG 2.2 success criterion`);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (ref.level === undefined) errors.push(`${label} standards reference ${ref.identifier} must declare a WCAG level`);
|
|
175
|
+
if (ref.level !== undefined && ref.level !== criterion.level) errors.push(`${label} standards reference ${ref.identifier} declares Level ${ref.level}; expected ${criterion.level}`);
|
|
176
|
+
const expectedUrl = ref.authority === 'wcag-2.2'
|
|
177
|
+
? `https://www.w3.org/TR/WCAG22/#${criterion.slug}`
|
|
178
|
+
: `https://www.w3.org/WAI/WCAG22/Understanding/${criterion.slug}.html`;
|
|
179
|
+
if (ref.url !== expectedUrl) errors.push(`${label} standards reference ${ref.authority} ${ref.identifier} URL must be ${expectedUrl}`);
|
|
180
|
+
} else if (ref.level !== undefined) {
|
|
181
|
+
errors.push(`${label} standards reference ${ref.authority} ${ref.identifier} must not declare a WCAG level`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const sorted = [...identities].sort();
|
|
185
|
+
if (JSON.stringify(identities) !== JSON.stringify(sorted)) errors.push(`${label} standards_refs must be sorted by authority, identifier, and URL`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function validateSemanticReferencePairs(record, errors) {
|
|
189
|
+
if (!isRecord(record) || !Array.isArray(record.standards_refs)) return;
|
|
190
|
+
const normative = new Set(record.standards_refs.filter((ref) => ref?.authority === 'wcag-2.2').map((ref) => ref.identifier));
|
|
191
|
+
const understanding = new Set(record.standards_refs.filter((ref) => ref?.authority === 'wcag-2.2-understanding').map((ref) => ref.identifier));
|
|
192
|
+
for (const identifier of normative) {
|
|
193
|
+
if (!understanding.has(identifier)) errors.push(`${record.id} WCAG ${identifier} reference must include its Understanding document`);
|
|
194
|
+
}
|
|
195
|
+
for (const identifier of understanding) {
|
|
196
|
+
if (!normative.has(identifier)) errors.push(`${record.id} Understanding ${identifier} reference must include its normative WCAG criterion`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function validateAuthorityRecord(record, expectedKind, errors) {
|
|
201
|
+
const label = isRecord(record) && isNonEmptyString(record.id) ? record.id : `<malformed ${expectedKind}>`;
|
|
202
|
+
if (!isRecord(record)) {
|
|
203
|
+
errors.push(`Malformed ${expectedKind} record`);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (!isNonEmptyString(record.id) || !RECORD_ID.test(record.id) || !record.id.startsWith(RECORD_PREFIX[expectedKind])) errors.push(`${label} has invalid ${expectedKind} ID`);
|
|
207
|
+
if (!SEMVER.test(record.version ?? '')) errors.push(`${label} has invalid record version`);
|
|
208
|
+
if (!isNonEmptyString(record.title)) errors.push(`${label} has no title`);
|
|
209
|
+
if (record.kind !== expectedKind) errors.push(`${label} must declare kind ${expectedKind}`);
|
|
210
|
+
if (expectedKind !== 'pattern' && !isNonEmptyString(record.requirement)) errors.push(`${label} has no requirement`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function validateFacts(catalog, errors) {
|
|
214
|
+
if (!isRecord(catalog) || catalog.version !== 1 || !isRecord(catalog.facts) || Object.keys(catalog.facts).length === 0) {
|
|
215
|
+
errors.push('Malformed applicability fact catalog');
|
|
216
|
+
return {};
|
|
217
|
+
}
|
|
218
|
+
for (const [id, definition] of Object.entries(catalog.facts)) {
|
|
219
|
+
if (!FACT_ID.test(id)) errors.push(`Invalid applicability fact ID: ${id}`);
|
|
220
|
+
if (!isRecord(definition) || !['boolean', 'string', 'array'].includes(definition.type)) {
|
|
221
|
+
errors.push(`Fact ${id} has unsupported type ${definition?.type ?? '<missing>'}`);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
const extraKeys = Object.keys(definition).filter((key) => !['type', 'items', 'source', 'derived'].includes(key));
|
|
225
|
+
if (extraKeys.length) errors.push(`Fact ${id} has unsupported fields: ${extraKeys.join(', ')}`);
|
|
226
|
+
if (definition.type === 'array' && definition.items !== 'string') errors.push(`Fact ${id} must declare string array items`);
|
|
227
|
+
if (definition.type !== 'array' && Object.hasOwn(definition, 'items')) errors.push(`Fact ${id} cannot declare array items`);
|
|
228
|
+
if (Object.hasOwn(definition, 'source') && !isNonEmptyString(definition.source)) errors.push(`Fact ${id} has invalid source`);
|
|
229
|
+
if (Object.hasOwn(definition, 'derived') && definition.derived !== true) errors.push(`Fact ${id} has invalid derived marker`);
|
|
230
|
+
if (Object.hasOwn(definition, 'source') && Object.hasOwn(definition, 'derived')) errors.push(`Fact ${id} cannot be both sourced and derived`);
|
|
231
|
+
}
|
|
232
|
+
if (catalog.facts['component.ui_pattern_ids']?.source !== 'ui-design-brain') errors.push('component.ui_pattern_ids must be sourced from ui-design-brain');
|
|
233
|
+
if (catalog.facts['component.accessibility_pattern_ids']?.derived !== true) errors.push('component.accessibility_pattern_ids must be derived');
|
|
234
|
+
return catalog.facts;
|
|
235
|
+
}
|
|
2
236
|
|
|
3
237
|
export function validateStandards(data) {
|
|
4
238
|
const errors = [];
|
|
5
|
-
|
|
239
|
+
if (data.contract?.schema_version !== 3 || data.contract?.package_version !== '3.0.0') errors.push('Expected accessibility contract schema 3 targeting package 3.0.0');
|
|
240
|
+
if (data.package?.version !== data.contract?.package_version) errors.push(`Installed package version ${data.package?.version ?? '<missing>'} does not match contract target ${data.contract?.package_version ?? '<missing>'}`);
|
|
241
|
+
const authorities = validateSources(data.sources, errors);
|
|
242
|
+
validateProfiles(data.profiles, data.contract?.package_version, errors);
|
|
243
|
+
validateProfileTemplates(data.profileTemplates, errors);
|
|
244
|
+
const foundations = Array.isArray(data.foundations) ? data.foundations : [];
|
|
245
|
+
const semantics = Array.isArray(data.semantics) ? data.semantics : [];
|
|
246
|
+
const patterns = Array.isArray(data.patterns) ? data.patterns : [];
|
|
247
|
+
const policies = Array.isArray(data.policies) ? data.policies : [];
|
|
248
|
+
if (!Array.isArray(data.foundations) || !foundations.length) errors.push('Foundations must be a non-empty array');
|
|
249
|
+
if (!Array.isArray(data.semantics) || !semantics.length) errors.push('Semantics must be a non-empty array');
|
|
250
|
+
if (!Array.isArray(data.patterns) || !patterns.length) errors.push('Patterns must be a non-empty array');
|
|
251
|
+
if (!Array.isArray(data.policies) || !policies.length) errors.push('Policies must be a non-empty array');
|
|
252
|
+
const records = [...foundations, ...semantics, ...patterns, ...policies];
|
|
6
253
|
const ids = new Set();
|
|
7
|
-
for (const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
for (const semantic of
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
254
|
+
for (const [kind, group] of [['foundation', foundations], ['semantic', semantics], ['pattern', patterns], ['policy', policies]]) {
|
|
255
|
+
for (const record of group) {
|
|
256
|
+
validateAuthorityRecord(record, kind, errors);
|
|
257
|
+
if (!isRecord(record) || !isNonEmptyString(record.id)) continue;
|
|
258
|
+
if (ids.has(record.id)) errors.push(`Duplicate authority: ${record.id}`);
|
|
259
|
+
ids.add(record.id);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const facts = validateFacts(data.facts, errors);
|
|
263
|
+
const semanticIds = new Set(semantics.filter(isRecord).map((record) => record.id).filter(isNonEmptyString));
|
|
264
|
+
const patternIds = new Set(patterns.filter(isRecord).map((record) => record.id).filter(isNonEmptyString));
|
|
265
|
+
const usedFactIds = new Set();
|
|
266
|
+
for (const semantic of semantics) {
|
|
267
|
+
const id = semantic?.id ?? '<malformed semantic>';
|
|
268
|
+
validateAllowedKeys(id, semantic, ['id', 'kind', 'proof', 'requirement', 'standards_refs', 'title', 'version'], errors);
|
|
269
|
+
validateStringArray(`${id} proof`, semantic?.proof, errors);
|
|
270
|
+
validateStandardsReferences(semantic, authorities, errors);
|
|
271
|
+
validateSemanticReferencePairs(semantic, errors);
|
|
272
|
+
}
|
|
273
|
+
for (const pattern of patterns) {
|
|
274
|
+
const id = pattern?.id ?? '<malformed pattern>';
|
|
275
|
+
validateAllowedKeys(id, pattern, ['activation', 'behavior', 'evidence_routes', 'functional_spec_bindings', 'id', 'implementation_outcomes', 'kind', 'product_decisions', 'requires', 'scope', 'standards_refs', 'title', 'version'], errors);
|
|
276
|
+
validateStandardsReferences(pattern, authorities, errors);
|
|
277
|
+
if (!validateStringArray(`${id} semantic dependencies`, pattern?.requires, errors)) errors.push(`Orphan pattern: ${id}`);
|
|
278
|
+
for (const dependency of Array.isArray(pattern?.requires) ? pattern.requires : []) if (!semanticIds.has(dependency)) errors.push(`${id} has missing semantic dependency ${dependency}`);
|
|
279
|
+
if (!isNonEmptyString(pattern?.scope)) errors.push(`${id} has no scope`);
|
|
280
|
+
validateStringArray(`${id} behavior`, pattern?.behavior, errors);
|
|
281
|
+
validateStringArray(`${id} product decisions`, pattern?.product_decisions, errors);
|
|
282
|
+
validateStringArray(`${id} Functional Spec bindings`, pattern?.functional_spec_bindings, errors);
|
|
283
|
+
validateStringArray(`${id} implementation outcomes`, pattern?.implementation_outcomes, errors);
|
|
284
|
+
validateStringArray(`${id} evidence routes`, pattern?.evidence_routes, errors);
|
|
285
|
+
try {
|
|
286
|
+
validateExpression(pattern?.activation, facts);
|
|
287
|
+
for (const fact of referencedFacts(pattern.activation)) usedFactIds.add(fact);
|
|
288
|
+
} catch (error) { errors.push(`${id} activation: ${error.message}`); }
|
|
289
|
+
}
|
|
290
|
+
const proofKindObject = isRecord(data.evidence?.proof_kinds) ? data.evidence.proof_kinds : {};
|
|
291
|
+
if (data.evidence?.version !== 1 || !Object.keys(proofKindObject).length || !isNonEmptyString(data.evidence?.rule)) errors.push('Malformed evidence routing contract');
|
|
292
|
+
const expectedProofKinds = ['axe', 'e2e', 'human', 'unit'];
|
|
293
|
+
if (JSON.stringify(Object.keys(proofKindObject).sort()) !== JSON.stringify(expectedProofKinds)) errors.push('Evidence routes must be exactly unit, axe, e2e, and human');
|
|
294
|
+
const proofKinds = new Set(Object.keys(proofKindObject));
|
|
295
|
+
for (const [kind, route] of Object.entries(proofKindObject)) {
|
|
296
|
+
if (!isRecord(route) || !isNonEmptyString(route.purpose) || route.required_capability !== kind) errors.push(`Malformed evidence route ${kind}`);
|
|
297
|
+
}
|
|
298
|
+
for (const semantic of semantics) for (const route of Array.isArray(semantic?.proof) ? semantic.proof : []) if (!proofKinds.has(route)) errors.push(`${semantic?.id ?? '<malformed semantic>'} uses unsupported proof route ${route}`);
|
|
299
|
+
for (const pattern of patterns) for (const route of Array.isArray(pattern?.evidence_routes) ? pattern.evidence_routes : []) if (!proofKinds.has(route)) errors.push(`${pattern?.id ?? '<malformed pattern>'} uses unsupported proof route ${route}`);
|
|
300
|
+
const rows = Array.isArray(data.matrix?.rows) ? data.matrix.rows : [];
|
|
301
|
+
if (data.matrix?.version !== 1 || !rows.length) errors.push('Malformed applicability matrix');
|
|
302
|
+
const rowIds = new Set();
|
|
303
|
+
const matrixOutcomeIds = new Set();
|
|
304
|
+
for (const row of rows) {
|
|
305
|
+
const id = row?.id ?? '<malformed applicability row>';
|
|
306
|
+
if (!isRecord(row) || !/^applicability\.[a-z0-9]+(?:[.-][a-z0-9]+)*$/.test(id)) errors.push(`Invalid applicability row ID: ${id}`);
|
|
307
|
+
if (rowIds.has(id)) errors.push(`Duplicate applicability row: ${id}`);
|
|
308
|
+
rowIds.add(id);
|
|
309
|
+
validateStringArray(`${id} outcomes`, row?.outcomes, errors);
|
|
310
|
+
validateStringArray(`${id} evidence`, row?.evidence, errors);
|
|
311
|
+
try {
|
|
312
|
+
compileApplicability({ rows: [row] }, data.facts);
|
|
313
|
+
for (const fact of referencedFacts(row.when)) usedFactIds.add(fact);
|
|
314
|
+
} catch (error) { errors.push(`${id}: ${error.message}`); }
|
|
315
|
+
for (const outcome of Array.isArray(row?.outcomes) ? row.outcomes : []) {
|
|
316
|
+
matrixOutcomeIds.add(outcome);
|
|
317
|
+
if (!ids.has(outcome)) errors.push(`${id} references unknown outcome ${outcome}`);
|
|
318
|
+
}
|
|
319
|
+
for (const route of Array.isArray(row?.evidence) ? row.evidence : []) if (!proofKinds.has(route)) errors.push(`${id} uses unsupported proof route ${route}`);
|
|
320
|
+
}
|
|
321
|
+
const lanes = new Set([...semantics.flatMap((item) => Array.isArray(item?.proof) ? item.proof : []), ...patterns.flatMap((item) => Array.isArray(item?.evidence_routes) ? item.evidence_routes : []), ...rows.flatMap((item) => Array.isArray(item?.evidence) ? item.evidence : [])]);
|
|
27
322
|
for (const lane of proofKinds) if (!lanes.has(lane)) errors.push(`Dead evidence lane: ${lane}`);
|
|
28
|
-
|
|
323
|
+
const bindingReachability = validateUiDesignBrainBindings(data.uiDesignBrainBindings, { facts }, semanticIds, patternIds, errors);
|
|
324
|
+
for (const fact of bindingReachability.factIds) usedFactIds.add(fact);
|
|
325
|
+
const reachablePatternIds = new Set([...bindingReachability.patternIds, ...[...matrixOutcomeIds].filter((id) => id.startsWith('pattern.'))]);
|
|
326
|
+
for (const patternId of patternIds) if (!reachablePatternIds.has(patternId)) errors.push(`Unreachable pattern: ${patternId} is not selected by a UI binding or applicability row`);
|
|
327
|
+
const reachableSemanticIds = new Set([
|
|
328
|
+
...patterns.flatMap((pattern) => Array.isArray(pattern?.requires) ? pattern.requires : []),
|
|
329
|
+
...bindingReachability.semanticIds,
|
|
330
|
+
...[...matrixOutcomeIds].filter((id) => id.startsWith('semantics.'))
|
|
331
|
+
]);
|
|
332
|
+
for (const semanticId of semanticIds) if (!reachableSemanticIds.has(semanticId)) errors.push(`Unreachable semantic: ${semanticId} is not required by a pattern, UI baseline, or applicability row`);
|
|
333
|
+
for (const factId of Object.keys(facts)) if (!usedFactIds.has(factId)) errors.push(`Unused applicability fact: ${factId}`);
|
|
334
|
+
const wcagCriteria = validateWcagCoverage(data.wcagCoverage, authorities, semantics, semanticIds, errors);
|
|
29
335
|
if (errors.length) throw new Error(errors.join('\n'));
|
|
30
|
-
return { records: records.length, semantics:
|
|
336
|
+
return { records: records.length, semantics: semantics.length, patterns: patterns.length, applicability: rows.length, uiPatternBindings: data.uiDesignBrainBindings.bindings.length, wcagCriteria };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function validateProfileTemplates(profileTemplates, errors) {
|
|
340
|
+
const expected = {
|
|
341
|
+
conductor: ['applicability/README.md', 'evidence/README.md', 'patterns/README.md', 'semantics/README.md'],
|
|
342
|
+
'ai-orchestration': ['implementation/README.md', 'patterns/README.md', 'review/README.md', 'semantics/README.md', 'testing/README.md']
|
|
343
|
+
};
|
|
344
|
+
if (!isRecord(profileTemplates) || JSON.stringify(Object.keys(profileTemplates).sort()) !== JSON.stringify(Object.keys(expected).sort())) {
|
|
345
|
+
errors.push('Profile templates must be exactly conductor and ai-orchestration');
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
for (const [profile, paths] of Object.entries(expected)) {
|
|
349
|
+
const templates = profileTemplates[profile];
|
|
350
|
+
if (!isRecord(templates) || JSON.stringify(Object.keys(templates).sort()) !== JSON.stringify(paths)) {
|
|
351
|
+
errors.push(`Invalid profile template set: ${profile}`);
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
for (const [path, content] of Object.entries(templates)) {
|
|
355
|
+
if (!isNonEmptyString(content)) errors.push(`Empty profile template: ${profile}/${path}`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function validateSources(sources, errors) {
|
|
361
|
+
const authorities = new Map();
|
|
362
|
+
if (!isRecord(sources) || sources.version !== 1 || !Array.isArray(sources.authorities) || !sources.authorities.length) {
|
|
363
|
+
errors.push('Malformed standards source hierarchy');
|
|
364
|
+
return authorities;
|
|
365
|
+
}
|
|
366
|
+
const authorityIds = new Set();
|
|
367
|
+
for (const authority of sources.authorities) {
|
|
368
|
+
if (!isRecord(authority) || !isNonEmptyString(authority.id) || !/^https:\/\//.test(authority.url ?? '') || typeof authority.normative !== 'boolean') {
|
|
369
|
+
errors.push(`Malformed standards authority ${authority?.id ?? '<missing>'}`);
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
if (authorityIds.has(authority.id)) errors.push(`Duplicate standards authority ${authority.id}`);
|
|
373
|
+
authorityIds.add(authority.id);
|
|
374
|
+
authorities.set(authority.id, authority);
|
|
375
|
+
const expected = KNOWN_AUTHORITIES.get(authority.id);
|
|
376
|
+
if (!expected) {
|
|
377
|
+
errors.push(`Unsupported standards authority ${authority.id}`);
|
|
378
|
+
} else if (authority.url !== expected.url || authority.normative !== expected.normative) {
|
|
379
|
+
errors.push(`Standards authority ${authority.id} must use ${expected.url} with normative=${expected.normative}`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
for (const id of KNOWN_AUTHORITIES.keys()) if (!authorityIds.has(id)) errors.push(`Standards source hierarchy omits required authority ${id}`);
|
|
383
|
+
if (!validateStringArray('Standards precedence', sources.precedence, errors)) return authorities;
|
|
384
|
+
for (const id of authorityIds) if (!sources.precedence.includes(id)) errors.push(`Standards precedence omits authority ${id}`);
|
|
385
|
+
return authorities;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function validateWcagCoverage(coverage, authorities, semantics, semanticIds, errors) {
|
|
389
|
+
if (!isRecord(coverage) || coverage.version !== 1 || coverage.authority !== 'wcag-2.2' || !Array.isArray(coverage.deferred_patterns) || !Array.isArray(coverage.criteria)) {
|
|
390
|
+
errors.push('Malformed WCAG 2.2 coverage inventory');
|
|
391
|
+
return 0;
|
|
392
|
+
}
|
|
393
|
+
const extraRootKeys = Object.keys(coverage).filter((key) => !['authority', 'criteria', 'deferred_patterns', 'version'].includes(key));
|
|
394
|
+
if (extraRootKeys.length) errors.push(`WCAG 2.2 coverage inventory has unsupported fields: ${extraRootKeys.join(', ')}`);
|
|
395
|
+
const authority = authorities.get(coverage.authority);
|
|
396
|
+
if (!authority || authority.normative !== true) errors.push('WCAG 2.2 coverage inventory must reference the normative wcag-2.2 authority');
|
|
397
|
+
const deferredIds = new Set();
|
|
398
|
+
for (const [index, item] of coverage.deferred_patterns.entries()) {
|
|
399
|
+
if (!isRecord(item)) {
|
|
400
|
+
errors.push(`Malformed deferred APG pattern at index ${index}`);
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
const extraKeys = Object.keys(item).filter((key) => !['id', 'reason'].includes(key));
|
|
404
|
+
if (extraKeys.length) errors.push(`Deferred APG pattern ${item.id ?? '<missing>'} has unsupported fields: ${extraKeys.join(', ')}`);
|
|
405
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(item.id ?? '')) errors.push(`Deferred APG pattern ${item.id ?? '<missing>'} has an invalid ID`);
|
|
406
|
+
if (deferredIds.has(item.id)) errors.push(`Duplicate deferred APG pattern ${item.id}`);
|
|
407
|
+
deferredIds.add(item.id);
|
|
408
|
+
if (!isNonEmptyString(item.reason)) errors.push(`Deferred APG pattern ${item.id ?? '<missing>'} must have a non-empty reason`);
|
|
409
|
+
}
|
|
410
|
+
const deferredOrder = coverage.deferred_patterns.map((item) => item?.id);
|
|
411
|
+
if (JSON.stringify(deferredOrder) !== JSON.stringify([...deferredOrder].sort())) errors.push('Deferred APG patterns must be sorted by ID');
|
|
412
|
+
for (const id of REQUIRED_DEFERRED_PATTERN_IDS) if (!deferredIds.has(id)) errors.push(`WCAG 2.2 coverage inventory must retain deferred APG pattern ${id}`);
|
|
413
|
+
if (coverage.criteria.length !== WCAG_2_2_AA_CRITERIA.length) errors.push(`WCAG 2.2 coverage inventory must contain exactly ${WCAG_2_2_AA_CRITERIA.length} Level A/AA criteria`);
|
|
414
|
+
const seen = new Set();
|
|
415
|
+
const byId = new Map();
|
|
416
|
+
const semanticsById = new Map(semantics.filter(isRecord).map((semantic) => [semantic.id, semantic]));
|
|
417
|
+
for (const [index, criterion] of coverage.criteria.entries()) {
|
|
418
|
+
const expected = WCAG_2_2_AA_CRITERIA[index];
|
|
419
|
+
const label = isRecord(criterion) && isNonEmptyString(criterion.id) ? criterion.id : `<criterion ${index + 1}>`;
|
|
420
|
+
if (!isRecord(criterion)) {
|
|
421
|
+
errors.push(`Malformed WCAG 2.2 coverage criterion at index ${index}`);
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
const extraKeys = Object.keys(criterion).filter((key) => !['id', 'level', 'note', 'semantic_ids', 'status', 'title'].includes(key));
|
|
425
|
+
if (extraKeys.length) errors.push(`${label} coverage has unsupported fields: ${extraKeys.join(', ')}`);
|
|
426
|
+
if (seen.has(criterion.id)) errors.push(`Duplicate WCAG 2.2 coverage criterion ${criterion.id}`);
|
|
427
|
+
seen.add(criterion.id);
|
|
428
|
+
byId.set(criterion.id, criterion);
|
|
429
|
+
if (!expected || criterion.id !== expected[0]) errors.push(`${label} is out of canonical WCAG 2.2 A/AA order; expected ${expected?.[0] ?? '<none>'}`);
|
|
430
|
+
if (expected && criterion.title !== expected[1]) errors.push(`${label} title must be ${expected[1]}`);
|
|
431
|
+
if (expected && criterion.level !== expected[2]) errors.push(`${label} must declare Level ${expected[2]}`);
|
|
432
|
+
if (!['covered', 'partial', 'gap'].includes(criterion.status)) errors.push(`${label} has invalid coverage status ${criterion.status ?? '<missing>'}`);
|
|
433
|
+
validateSortedUnique(`${label} semantic_ids`, criterion.semantic_ids, errors);
|
|
434
|
+
const mappedIds = Array.isArray(criterion.semantic_ids) ? criterion.semantic_ids : [];
|
|
435
|
+
if (criterion.status === 'gap' && mappedIds.length) errors.push(`${label} gap coverage must not map semantic IDs`);
|
|
436
|
+
if (criterion.status !== 'gap' && mappedIds.length === 0) errors.push(`${label} ${criterion.status ?? '<missing>'} coverage must map at least one semantic ID`);
|
|
437
|
+
if (criterion.note !== undefined && !isNonEmptyString(criterion.note)) errors.push(`${label} coverage note must be a non-empty string`);
|
|
438
|
+
if (['partial', 'gap'].includes(criterion.status) && !isNonEmptyString(criterion.note)) errors.push(`${label} ${criterion.status} coverage must explain the shortfall`);
|
|
439
|
+
for (const semanticId of mappedIds) {
|
|
440
|
+
if (!semanticIds.has(semanticId)) {
|
|
441
|
+
errors.push(`${label} coverage references unknown semantic ${semanticId}`);
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
const semantic = semanticsById.get(semanticId);
|
|
445
|
+
const hasTrace = semantic?.standards_refs?.some((ref) => ref.authority === 'wcag-2.2' && ref.identifier === criterion.id);
|
|
446
|
+
if (!hasTrace) errors.push(`${label} coverage maps ${semanticId} without a matching normative WCAG standards reference`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
for (const [id] of WCAG_2_2_AA_CRITERIA) if (!seen.has(id)) errors.push(`WCAG 2.2 coverage inventory omits ${id}`);
|
|
450
|
+
for (const semantic of semantics) {
|
|
451
|
+
if (!isRecord(semantic)) continue;
|
|
452
|
+
for (const ref of Array.isArray(semantic.standards_refs) ? semantic.standards_refs : []) {
|
|
453
|
+
if (ref.authority !== 'wcag-2.2' || !WCAG_2_2_AA_BY_ID.has(ref.identifier)) continue;
|
|
454
|
+
const criterion = byId.get(ref.identifier);
|
|
455
|
+
if (!Array.isArray(criterion?.semantic_ids) || !criterion.semantic_ids.includes(semantic.id)) errors.push(`${semantic.id} references WCAG ${ref.identifier} but is absent from its coverage mapping`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return coverage.criteria.length;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function validateProfiles(profiles, packageVersion, errors) {
|
|
462
|
+
const expected = {
|
|
463
|
+
conductor: ['ui-pattern-bindings', 'semantics', 'patterns', 'applicability', 'evidence'],
|
|
464
|
+
'ai-orchestration': ['ui-pattern-bindings', 'semantics', 'patterns', 'implementation', 'testing', 'review']
|
|
465
|
+
};
|
|
466
|
+
if (!isRecord(profiles) || JSON.stringify(Object.keys(profiles).sort()) !== JSON.stringify(Object.keys(expected).sort())) {
|
|
467
|
+
errors.push('Profile contracts must be exactly conductor and ai-orchestration');
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
for (const [name, channels] of Object.entries(expected)) {
|
|
471
|
+
const profile = profiles[name];
|
|
472
|
+
if (!isRecord(profile) || profile.name !== name || profile.version !== packageVersion || JSON.stringify(profile.channels) !== JSON.stringify(channels)) {
|
|
473
|
+
errors.push(`Invalid or incorrectly ordered profile contract: ${name}`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function validateUiDesignBrainBindings(contract, facts, semanticIds, patternIds, errors) {
|
|
479
|
+
const reachability = { patternIds: new Set(), semanticIds: new Set(), factIds: new Set(['component.ui_pattern_ids']) };
|
|
480
|
+
if (contract?.version !== 1 || !contract.source || !Array.isArray(contract.bindings)) {
|
|
481
|
+
errors.push('Malformed UI Design Brain binding contract');
|
|
482
|
+
return reachability;
|
|
483
|
+
}
|
|
484
|
+
if (contract.source.package !== '@verndale/ui-design-brain') errors.push('UI Design Brain binding source package is invalid');
|
|
485
|
+
if (contract.source.repository !== 'https://github.com/verndale/ui-design-brain') errors.push('UI Design Brain binding repository is invalid');
|
|
486
|
+
if (contract.source.tracking_issue !== 'https://github.com/verndale/ui-design-brain/issues/47') errors.push('UI Design Brain binding tracking issue is invalid');
|
|
487
|
+
if (contract.source.package_version !== '1.16.0') errors.push('UI Design Brain projector package version is invalid');
|
|
488
|
+
if (contract.source.catalog_authority_version !== '1.15.1') errors.push('UI Design Brain catalog authority version is invalid');
|
|
489
|
+
if (!/^sha256:[a-f0-9]{64}$/.test(contract.source.source_digest ?? '')) errors.push('UI Design Brain source digest is invalid');
|
|
490
|
+
if (!/^sha256:[a-f0-9]{64}$/.test(contract.source.manifest_digest ?? '')) errors.push('UI Design Brain manifest digest is invalid');
|
|
491
|
+
if (contract.source.pattern_count !== contract.bindings.length) errors.push(`UI Design Brain binding count ${contract.bindings.length} does not match source count ${contract.source.pattern_count}`);
|
|
492
|
+
|
|
493
|
+
const actualOrder = contract.bindings.map((binding) => isRecord(binding) ? binding.ui_pattern_id : undefined);
|
|
494
|
+
const expectedOrder = [...actualOrder].sort();
|
|
495
|
+
if (JSON.stringify(actualOrder) !== JSON.stringify(expectedOrder)) errors.push('UI Design Brain bindings must be sorted by canonical ui_pattern_id');
|
|
496
|
+
const seen = new Set();
|
|
497
|
+
for (const binding of contract.bindings) {
|
|
498
|
+
if (!isRecord(binding)) {
|
|
499
|
+
errors.push('Malformed UI Design Brain binding');
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
const prefix = `UI Design Brain binding ${binding.ui_pattern_id ?? '<missing>'}`;
|
|
503
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(binding.ui_pattern_id ?? '')) errors.push(`${prefix} has invalid canonical ID`);
|
|
504
|
+
if (seen.has(binding.ui_pattern_id)) errors.push(`${prefix} is duplicated`);
|
|
505
|
+
seen.add(binding.ui_pattern_id);
|
|
506
|
+
if (!['direct', 'candidate', 'baseline-only'].includes(binding.classification)) errors.push(`${prefix} has invalid classification ${binding.classification}`);
|
|
507
|
+
validateSortedUnique(`${prefix} pattern_ids`, binding.pattern_ids ?? [], errors);
|
|
508
|
+
for (const patternId of Array.isArray(binding.pattern_ids) ? binding.pattern_ids : []) {
|
|
509
|
+
reachability.patternIds.add(patternId);
|
|
510
|
+
if (!patternIds.has(patternId)) errors.push(`${prefix} references unknown pattern ${patternId}`);
|
|
511
|
+
}
|
|
512
|
+
validateSortedUnique(`${prefix} baseline_semantic_ids`, binding.baseline_semantic_ids ?? [], errors);
|
|
513
|
+
for (const semanticId of Array.isArray(binding.baseline_semantic_ids) ? binding.baseline_semantic_ids : []) {
|
|
514
|
+
reachability.semanticIds.add(semanticId);
|
|
515
|
+
if (!semanticIds.has(semanticId)) errors.push(`${prefix} references unknown baseline semantic ${semanticId}`);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (binding.classification === 'direct') {
|
|
519
|
+
if (!binding.pattern_ids?.length) errors.push(`${prefix} must declare at least one direct pattern`);
|
|
520
|
+
if (binding.candidates || binding.discriminator_facts) errors.push(`${prefix} direct mapping cannot declare candidates`);
|
|
521
|
+
} else if (binding.classification === 'baseline-only') {
|
|
522
|
+
if (binding.pattern_ids || binding.candidates || binding.discriminator_facts) errors.push(`${prefix} baseline-only mapping cannot declare specialized patterns`);
|
|
523
|
+
if (!binding.baseline_semantic_ids?.length) errors.push(`${prefix} baseline-only mapping must declare at least one baseline semantic`);
|
|
524
|
+
} else if (binding.classification === 'candidate') {
|
|
525
|
+
if (!binding.candidates?.length || !binding.discriminator_facts?.length) errors.push(`${prefix} candidate mapping requires candidates and discriminator facts`);
|
|
526
|
+
validateSortedUnique(`${prefix} discriminator_facts`, binding.discriminator_facts ?? [], errors);
|
|
527
|
+
for (const fact of Array.isArray(binding.discriminator_facts) ? binding.discriminator_facts : []) {
|
|
528
|
+
reachability.factIds.add(fact);
|
|
529
|
+
if (!facts.facts[fact]) errors.push(`${prefix} references unknown discriminator fact ${fact}`);
|
|
530
|
+
}
|
|
531
|
+
const candidatePatternIds = [];
|
|
532
|
+
const usedFacts = new Set();
|
|
533
|
+
for (const candidate of Array.isArray(binding.candidates) ? binding.candidates : []) {
|
|
534
|
+
if (!isRecord(candidate)) {
|
|
535
|
+
errors.push(`${prefix} has a malformed candidate`);
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
candidatePatternIds.push(candidate.pattern_id);
|
|
539
|
+
reachability.patternIds.add(candidate.pattern_id);
|
|
540
|
+
if (!patternIds.has(candidate.pattern_id)) errors.push(`${prefix} references unknown candidate pattern ${candidate.pattern_id}`);
|
|
541
|
+
try {
|
|
542
|
+
validateExpression(candidate.when, facts.facts);
|
|
543
|
+
for (const fact of referencedFacts(candidate.when)) usedFacts.add(fact);
|
|
544
|
+
} catch (error) {
|
|
545
|
+
errors.push(`${prefix} ${candidate.pattern_id}: ${error.message}`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
validateSortedUnique(`${prefix} candidate pattern IDs`, candidatePatternIds, errors);
|
|
549
|
+
const declaredFacts = [...(Array.isArray(binding.discriminator_facts) ? binding.discriminator_facts : [])].sort();
|
|
550
|
+
const referenced = [...usedFacts].sort();
|
|
551
|
+
if (JSON.stringify(declaredFacts) !== JSON.stringify(referenced)) errors.push(`${prefix} discriminator facts do not exactly match candidate expressions`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return reachability;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function validateSortedUnique(label, values, errors) {
|
|
558
|
+
if (!Array.isArray(values) || values.some((value) => !isNonEmptyString(value))) {
|
|
559
|
+
errors.push(`${label} must be an array of non-empty strings`);
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
const sorted = [...new Set(values)].sort();
|
|
563
|
+
if (JSON.stringify(values) !== JSON.stringify(sorted)) errors.push(`${label} must be sorted and unique`);
|
|
31
564
|
}
|