@verndale/accessibility-standards 2.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 +1 -1
- package/README.md +1 -1
- package/dist/manifest.json +156 -116
- package/dist/profiles/ai-orchestration/coverage-manifest.json +702 -153
- package/dist/profiles/ai-orchestration/implementation/accessibility.md +523 -42
- package/dist/profiles/ai-orchestration/pattern-contracts.json +892 -306
- package/dist/profiles/ai-orchestration/patterns/accordion.md +6 -1
- package/dist/profiles/ai-orchestration/patterns/alert.md +6 -1
- package/dist/profiles/ai-orchestration/patterns/carousel.md +6 -1
- package/dist/profiles/ai-orchestration/patterns/checkbox.md +27 -0
- package/dist/profiles/ai-orchestration/patterns/combobox.md +6 -1
- package/dist/profiles/ai-orchestration/patterns/consequential-submission.md +8 -3
- package/dist/profiles/ai-orchestration/patterns/dialog.md +6 -1
- package/dist/profiles/ai-orchestration/patterns/disclosure.md +6 -1
- package/dist/profiles/ai-orchestration/patterns/dynamic-status.md +6 -1
- package/dist/profiles/ai-orchestration/patterns/field.md +7 -1
- package/dist/profiles/ai-orchestration/patterns/image.md +6 -1
- package/dist/profiles/ai-orchestration/patterns/listbox.md +6 -1
- package/dist/profiles/ai-orchestration/patterns/media-player.md +9 -1
- package/dist/profiles/ai-orchestration/patterns/menu-button.md +6 -1
- package/dist/profiles/ai-orchestration/patterns/menu.md +30 -0
- package/dist/profiles/ai-orchestration/patterns/pagination.md +6 -1
- package/dist/profiles/ai-orchestration/patterns/popover.md +7 -1
- package/dist/profiles/ai-orchestration/patterns/progress.md +7 -1
- 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 +6 -1
- package/dist/profiles/ai-orchestration/patterns/tooltip.md +6 -1
- package/dist/profiles/ai-orchestration/patterns/tree-view.md +31 -0
- package/dist/profiles/ai-orchestration/patterns/validation.md +7 -1
- package/dist/profiles/ai-orchestration/review/accessibility.md +14 -6
- package/dist/profiles/ai-orchestration/semantics/accessible-authentication.md +13 -0
- package/dist/profiles/ai-orchestration/semantics/accessible-name.md +8 -2
- package/dist/profiles/ai-orchestration/semantics/alert.md +7 -1
- package/dist/profiles/ai-orchestration/semantics/consequential-action.md +6 -2
- package/dist/profiles/ai-orchestration/semantics/consistent-help.md +13 -0
- package/dist/profiles/ai-orchestration/semantics/data-table.md +7 -2
- package/dist/profiles/ai-orchestration/semantics/dragging-alternative.md +13 -0
- package/dist/profiles/ai-orchestration/semantics/expanded-state.md +6 -1
- package/dist/profiles/ai-orchestration/semantics/focus.initial.md +4 -1
- package/dist/profiles/ai-orchestration/semantics/focus.return.md +4 -1
- package/dist/profiles/ai-orchestration/semantics/focus.visible.md +8 -2
- package/dist/profiles/ai-orchestration/semantics/form-error.md +8 -2
- package/dist/profiles/ai-orchestration/semantics/form-label.md +7 -1
- package/dist/profiles/ai-orchestration/semantics/form-required.md +7 -1
- package/dist/profiles/ai-orchestration/semantics/headings.md +7 -1
- package/dist/profiles/ai-orchestration/semantics/keyboard.escape.md +4 -1
- package/dist/profiles/ai-orchestration/semantics/keyboard.md +7 -1
- package/dist/profiles/ai-orchestration/semantics/landmarks.md +6 -1
- package/dist/profiles/ai-orchestration/semantics/live-status.md +7 -2
- package/dist/profiles/ai-orchestration/semantics/media-alternatives.md +11 -1
- package/dist/profiles/ai-orchestration/semantics/motion-control.md +8 -2
- package/dist/profiles/ai-orchestration/semantics/name-role-value.md +6 -2
- package/dist/profiles/ai-orchestration/semantics/native-dialog.md +7 -1
- package/dist/profiles/ai-orchestration/semantics/native-elements.md +5 -1
- package/dist/profiles/ai-orchestration/semantics/non-text-alternative.md +5 -1
- package/dist/profiles/ai-orchestration/semantics/redundant-entry.md +13 -0
- package/dist/profiles/ai-orchestration/semantics/roles-states-properties.md +6 -1
- package/dist/profiles/ai-orchestration/semantics/selected-state.md +6 -1
- package/dist/profiles/ai-orchestration/semantics/target-size.md +6 -2
- package/dist/profiles/ai-orchestration/semantics.json +826 -130
- package/dist/profiles/ai-orchestration/testing/accessibility.md +33 -3
- package/dist/profiles/ai-orchestration/ui-design-brain-bindings.json +287 -307
- package/dist/profiles/conductor/a11y-semantics.md +249 -10
- package/dist/profiles/conductor/accessibility-quality.md +275 -33
- package/dist/profiles/conductor/applicability-matrix.json +226 -269
- package/dist/profiles/conductor/coverage-manifest.json +702 -153
- package/dist/profiles/conductor/pattern-contracts.json +892 -306
- package/dist/profiles/conductor/patterns/accordion.json +35 -27
- package/dist/profiles/conductor/patterns/alert.json +29 -21
- package/dist/profiles/conductor/patterns/carousel.json +36 -28
- package/dist/profiles/conductor/patterns/checkbox.json +67 -0
- package/dist/profiles/conductor/patterns/combobox.json +37 -29
- package/dist/profiles/conductor/patterns/consequential-submission.json +34 -25
- package/dist/profiles/conductor/patterns/dialog.json +37 -29
- package/dist/profiles/conductor/patterns/disclosure.json +33 -25
- package/dist/profiles/conductor/patterns/dynamic-status.json +31 -22
- package/dist/profiles/conductor/patterns/field.json +41 -25
- package/dist/profiles/conductor/patterns/image.json +31 -22
- package/dist/profiles/conductor/patterns/listbox.json +35 -27
- package/dist/profiles/conductor/patterns/media-player.json +59 -29
- package/dist/profiles/conductor/patterns/menu-button.json +34 -26
- package/dist/profiles/conductor/patterns/menu.json +78 -0
- package/dist/profiles/conductor/patterns/pagination.json +34 -25
- package/dist/profiles/conductor/patterns/popover.json +42 -26
- package/dist/profiles/conductor/patterns/progress.json +38 -23
- 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 +34 -26
- package/dist/profiles/conductor/patterns/tooltip.json +31 -23
- package/dist/profiles/conductor/patterns/tree-view.json +77 -0
- package/dist/profiles/conductor/patterns/validation.json +42 -26
- package/dist/profiles/conductor/semantics.json +826 -130
- package/dist/profiles/conductor/ui-design-brain-bindings.json +287 -307
- package/dist/projection-manifest.json +120 -104
- package/dist/requirements-to-implementation.md +7 -1
- package/lib/build.mjs +3 -3
- package/lib/digest.mjs +8 -0
- package/lib/load.mjs +13 -0
- package/lib/provenance.mjs +2 -0
- package/lib/render.mjs +51 -7
- package/lib/sync.mjs +2 -2
- package/lib/validate.mjs +335 -33
- package/package.json +6 -2
- 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 -1
- 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 +6 -6
- package/schemas/semantic.schema.json +26 -1
- package/schemas/standards-reference.schema.json +105 -0
- package/schemas/wcag-coverage.schema.json +93 -0
- package/src/applicability/facts.yml +3 -5
- package/src/applicability/matrix.yml +5 -7
- package/src/applicability/ui-design-brain-bindings.yml +8 -22
- package/src/contract.yml +1 -1
- 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
|
@@ -9,6 +9,104 @@ const RECORD_PREFIX = {
|
|
|
9
9
|
pattern: 'pattern.',
|
|
10
10
|
policy: 'policy.'
|
|
11
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
|
+
]);
|
|
12
110
|
|
|
13
111
|
function isRecord(value) {
|
|
14
112
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
@@ -27,6 +125,78 @@ function validateStringArray(label, value, errors, { required = true } = {}) {
|
|
|
27
125
|
return true;
|
|
28
126
|
}
|
|
29
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
|
+
|
|
30
200
|
function validateAuthorityRecord(record, expectedKind, errors) {
|
|
31
201
|
const label = isRecord(record) && isNonEmptyString(record.id) ? record.id : `<malformed ${expectedKind}>`;
|
|
32
202
|
if (!isRecord(record)) {
|
|
@@ -66,9 +236,9 @@ function validateFacts(catalog, errors) {
|
|
|
66
236
|
|
|
67
237
|
export function validateStandards(data) {
|
|
68
238
|
const errors = [];
|
|
69
|
-
if (data.contract?.schema_version !==
|
|
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');
|
|
70
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>'}`);
|
|
71
|
-
validateSources(data.sources, errors);
|
|
241
|
+
const authorities = validateSources(data.sources, errors);
|
|
72
242
|
validateProfiles(data.profiles, data.contract?.package_version, errors);
|
|
73
243
|
validateProfileTemplates(data.profileTemplates, errors);
|
|
74
244
|
const foundations = Array.isArray(data.foundations) ? data.foundations : [];
|
|
@@ -90,20 +260,32 @@ export function validateStandards(data) {
|
|
|
90
260
|
}
|
|
91
261
|
}
|
|
92
262
|
const facts = validateFacts(data.facts, errors);
|
|
93
|
-
const semanticIds = new Set(semantics.map((
|
|
94
|
-
const patternIds = new Set(patterns.map((
|
|
95
|
-
|
|
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
|
+
}
|
|
96
273
|
for (const pattern of patterns) {
|
|
97
|
-
const id = pattern
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (!
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
validateStringArray(`${id}
|
|
104
|
-
validateStringArray(`${id}
|
|
105
|
-
validateStringArray(`${id}
|
|
106
|
-
|
|
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}`); }
|
|
107
289
|
}
|
|
108
290
|
const proofKindObject = isRecord(data.evidence?.proof_kinds) ? data.evidence.proof_kinds : {};
|
|
109
291
|
if (data.evidence?.version !== 1 || !Object.keys(proofKindObject).length || !isNonEmptyString(data.evidence?.rule)) errors.push('Malformed evidence routing contract');
|
|
@@ -113,11 +295,12 @@ export function validateStandards(data) {
|
|
|
113
295
|
for (const [kind, route] of Object.entries(proofKindObject)) {
|
|
114
296
|
if (!isRecord(route) || !isNonEmptyString(route.purpose) || route.required_capability !== kind) errors.push(`Malformed evidence route ${kind}`);
|
|
115
297
|
}
|
|
116
|
-
for (const semantic of semantics) for (const route of Array.isArray(semantic
|
|
117
|
-
for (const pattern of patterns) for (const route of Array.isArray(pattern
|
|
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}`);
|
|
118
300
|
const rows = Array.isArray(data.matrix?.rows) ? data.matrix.rows : [];
|
|
119
301
|
if (data.matrix?.version !== 1 || !rows.length) errors.push('Malformed applicability matrix');
|
|
120
302
|
const rowIds = new Set();
|
|
303
|
+
const matrixOutcomeIds = new Set();
|
|
121
304
|
for (const row of rows) {
|
|
122
305
|
const id = row?.id ?? '<malformed applicability row>';
|
|
123
306
|
if (!isRecord(row) || !/^applicability\.[a-z0-9]+(?:[.-][a-z0-9]+)*$/.test(id)) errors.push(`Invalid applicability row ID: ${id}`);
|
|
@@ -125,16 +308,32 @@ export function validateStandards(data) {
|
|
|
125
308
|
rowIds.add(id);
|
|
126
309
|
validateStringArray(`${id} outcomes`, row?.outcomes, errors);
|
|
127
310
|
validateStringArray(`${id} evidence`, row?.evidence, errors);
|
|
128
|
-
try {
|
|
129
|
-
|
|
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
|
+
}
|
|
130
319
|
for (const route of Array.isArray(row?.evidence) ? row.evidence : []) if (!proofKinds.has(route)) errors.push(`${id} uses unsupported proof route ${route}`);
|
|
131
320
|
}
|
|
132
|
-
const lanes = new Set([...semantics.flatMap((item) => Array.isArray(item
|
|
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 : [])]);
|
|
133
322
|
for (const lane of proofKinds) if (!lanes.has(lane)) errors.push(`Dead evidence lane: ${lane}`);
|
|
134
|
-
validateUiDesignBrainBindings(data.uiDesignBrainBindings, { facts }, semanticIds, patternIds, errors);
|
|
135
|
-
|
|
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);
|
|
136
335
|
if (errors.length) throw new Error(errors.join('\n'));
|
|
137
|
-
return { records: records.length, semantics: semantics.length, patterns: patterns.length, applicability: rows.length, uiPatternBindings: data.uiDesignBrainBindings.bindings.length };
|
|
336
|
+
return { records: records.length, semantics: semantics.length, patterns: patterns.length, applicability: rows.length, uiPatternBindings: data.uiDesignBrainBindings.bindings.length, wcagCriteria };
|
|
138
337
|
}
|
|
139
338
|
|
|
140
339
|
function validateProfileTemplates(profileTemplates, errors) {
|
|
@@ -159,9 +358,10 @@ function validateProfileTemplates(profileTemplates, errors) {
|
|
|
159
358
|
}
|
|
160
359
|
|
|
161
360
|
function validateSources(sources, errors) {
|
|
361
|
+
const authorities = new Map();
|
|
162
362
|
if (!isRecord(sources) || sources.version !== 1 || !Array.isArray(sources.authorities) || !sources.authorities.length) {
|
|
163
363
|
errors.push('Malformed standards source hierarchy');
|
|
164
|
-
return;
|
|
364
|
+
return authorities;
|
|
165
365
|
}
|
|
166
366
|
const authorityIds = new Set();
|
|
167
367
|
for (const authority of sources.authorities) {
|
|
@@ -171,9 +371,91 @@ function validateSources(sources, errors) {
|
|
|
171
371
|
}
|
|
172
372
|
if (authorityIds.has(authority.id)) errors.push(`Duplicate standards authority ${authority.id}`);
|
|
173
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
|
+
}
|
|
174
381
|
}
|
|
175
|
-
if (!
|
|
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;
|
|
176
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;
|
|
177
459
|
}
|
|
178
460
|
|
|
179
461
|
function validateProfiles(profiles, packageVersion, errors) {
|
|
@@ -194,9 +476,10 @@ function validateProfiles(profiles, packageVersion, errors) {
|
|
|
194
476
|
}
|
|
195
477
|
|
|
196
478
|
function validateUiDesignBrainBindings(contract, facts, semanticIds, patternIds, errors) {
|
|
479
|
+
const reachability = { patternIds: new Set(), semanticIds: new Set(), factIds: new Set(['component.ui_pattern_ids']) };
|
|
197
480
|
if (contract?.version !== 1 || !contract.source || !Array.isArray(contract.bindings)) {
|
|
198
481
|
errors.push('Malformed UI Design Brain binding contract');
|
|
199
|
-
return;
|
|
482
|
+
return reachability;
|
|
200
483
|
}
|
|
201
484
|
if (contract.source.package !== '@verndale/ui-design-brain') errors.push('UI Design Brain binding source package is invalid');
|
|
202
485
|
if (contract.source.repository !== 'https://github.com/verndale/ui-design-brain') errors.push('UI Design Brain binding repository is invalid');
|
|
@@ -207,20 +490,30 @@ function validateUiDesignBrainBindings(contract, facts, semanticIds, patternIds,
|
|
|
207
490
|
if (!/^sha256:[a-f0-9]{64}$/.test(contract.source.manifest_digest ?? '')) errors.push('UI Design Brain manifest digest is invalid');
|
|
208
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}`);
|
|
209
492
|
|
|
210
|
-
const
|
|
211
|
-
const
|
|
493
|
+
const actualOrder = contract.bindings.map((binding) => isRecord(binding) ? binding.ui_pattern_id : undefined);
|
|
494
|
+
const expectedOrder = [...actualOrder].sort();
|
|
212
495
|
if (JSON.stringify(actualOrder) !== JSON.stringify(expectedOrder)) errors.push('UI Design Brain bindings must be sorted by canonical ui_pattern_id');
|
|
213
496
|
const seen = new Set();
|
|
214
497
|
for (const binding of contract.bindings) {
|
|
498
|
+
if (!isRecord(binding)) {
|
|
499
|
+
errors.push('Malformed UI Design Brain binding');
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
215
502
|
const prefix = `UI Design Brain binding ${binding.ui_pattern_id ?? '<missing>'}`;
|
|
216
503
|
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(binding.ui_pattern_id ?? '')) errors.push(`${prefix} has invalid canonical ID`);
|
|
217
504
|
if (seen.has(binding.ui_pattern_id)) errors.push(`${prefix} is duplicated`);
|
|
218
505
|
seen.add(binding.ui_pattern_id);
|
|
219
506
|
if (!['direct', 'candidate', 'baseline-only'].includes(binding.classification)) errors.push(`${prefix} has invalid classification ${binding.classification}`);
|
|
220
507
|
validateSortedUnique(`${prefix} pattern_ids`, binding.pattern_ids ?? [], errors);
|
|
221
|
-
for (const patternId of binding.pattern_ids
|
|
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
|
+
}
|
|
222
512
|
validateSortedUnique(`${prefix} baseline_semantic_ids`, binding.baseline_semantic_ids ?? [], errors);
|
|
223
|
-
for (const semanticId of binding.baseline_semantic_ids
|
|
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
|
+
}
|
|
224
517
|
|
|
225
518
|
if (binding.classification === 'direct') {
|
|
226
519
|
if (!binding.pattern_ids?.length) errors.push(`${prefix} must declare at least one direct pattern`);
|
|
@@ -231,11 +524,19 @@ function validateUiDesignBrainBindings(contract, facts, semanticIds, patternIds,
|
|
|
231
524
|
} else if (binding.classification === 'candidate') {
|
|
232
525
|
if (!binding.candidates?.length || !binding.discriminator_facts?.length) errors.push(`${prefix} candidate mapping requires candidates and discriminator facts`);
|
|
233
526
|
validateSortedUnique(`${prefix} discriminator_facts`, binding.discriminator_facts ?? [], errors);
|
|
234
|
-
for (const fact of binding.discriminator_facts
|
|
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
|
+
}
|
|
235
531
|
const candidatePatternIds = [];
|
|
236
532
|
const usedFacts = new Set();
|
|
237
|
-
for (const candidate of binding.candidates
|
|
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
|
+
}
|
|
238
538
|
candidatePatternIds.push(candidate.pattern_id);
|
|
539
|
+
reachability.patternIds.add(candidate.pattern_id);
|
|
239
540
|
if (!patternIds.has(candidate.pattern_id)) errors.push(`${prefix} references unknown candidate pattern ${candidate.pattern_id}`);
|
|
240
541
|
try {
|
|
241
542
|
validateExpression(candidate.when, facts.facts);
|
|
@@ -245,11 +546,12 @@ function validateUiDesignBrainBindings(contract, facts, semanticIds, patternIds,
|
|
|
245
546
|
}
|
|
246
547
|
}
|
|
247
548
|
validateSortedUnique(`${prefix} candidate pattern IDs`, candidatePatternIds, errors);
|
|
248
|
-
const declaredFacts = [...(binding.discriminator_facts
|
|
549
|
+
const declaredFacts = [...(Array.isArray(binding.discriminator_facts) ? binding.discriminator_facts : [])].sort();
|
|
249
550
|
const referenced = [...usedFacts].sort();
|
|
250
551
|
if (JSON.stringify(declaredFacts) !== JSON.stringify(referenced)) errors.push(`${prefix} discriminator facts do not exactly match candidate expressions`);
|
|
251
552
|
}
|
|
252
553
|
}
|
|
554
|
+
return reachability;
|
|
253
555
|
}
|
|
254
556
|
|
|
255
557
|
function validateSortedUnique(label, values, errors) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@verndale/accessibility-standards",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Deterministic accessibility semantics, patterns, applicability, and consumer projections",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -38,7 +38,8 @@
|
|
|
38
38
|
"graph:build": "node scripts/wiki/build-graph.cjs",
|
|
39
39
|
"graph:check": "node scripts/wiki/build-graph.cjs --check",
|
|
40
40
|
"wiki:check": "node scripts/wiki/check.cjs",
|
|
41
|
-
"prepare": "node scripts/install-hooks.mjs"
|
|
41
|
+
"prepare": "node scripts/install-hooks.mjs",
|
|
42
|
+
"pr:create": "ai-pr"
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|
|
44
45
|
"@semantic-release/changelog": "6.0.3",
|
|
@@ -55,5 +56,8 @@
|
|
|
55
56
|
"publishConfig": {
|
|
56
57
|
"access": "public",
|
|
57
58
|
"provenance": true
|
|
59
|
+
},
|
|
60
|
+
"dependencies": {
|
|
61
|
+
"dotenv": "^17.3.1"
|
|
58
62
|
}
|
|
59
63
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"ai-orchestration","version":"
|
|
1
|
+
{"name":"ai-orchestration","version":"3.0.0","channels":["ui-pattern-bindings","semantics","patterns","implementation","testing","review"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"conductor","version":"
|
|
1
|
+
{"name":"conductor","version":"3.0.0","channels":["ui-pattern-bindings","semantics","patterns","applicability","evidence"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"consumer-config.schema.json","type":"object","required":["package","profile","routes"],"properties":{"package":{"const":"@verndale/accessibility-standards@
|
|
1
|
+
{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"consumer-config.schema.json","type":"object","required":["package","profile","routes"],"properties":{"package":{"const":"@verndale/accessibility-standards@3.0.0"},"profile":{"enum":["conductor","ai-orchestration"]},"routes":{"type":"string"},"outputRoot":{"type":"string"}},"additionalProperties":false}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"contract.schema.json","type":"object","required":["schema_version","package_version"],"additionalProperties":false,"properties":{"schema_version":{"type":"integer","const":
|
|
1
|
+
{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"contract.schema.json","type":"object","required":["schema_version","package_version"],"additionalProperties":false,"properties":{"schema_version":{"type":"integer","const":3},"package_version":{"type":"string","const":"3.0.0"}}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"manifest.schema.json","type":"object","required":["_generated","package","schema_version","source_digest","profile_digests","ui_design_brain","records"],"properties":{"_generated":{"type":"string"},"package":{"
|
|
1
|
+
{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"manifest.schema.json","type":"object","required":["_generated","package","schema_version","source_digest","profile_digests","ui_design_brain","records"],"properties":{"_generated":{"type":"string"},"package":{"const":"@verndale/accessibility-standards@3.0.0"},"schema_version":{"const":3},"source_digest":{"type":"string","pattern":"^[a-f0-9]{64}$"},"profile_digests":{"type":"object"},"ui_design_brain":{"type":"object","required":["package","package_version","catalog_authority_version","source_digest","manifest_digest","binding_digest"]},"records":{"type":"array","items":{"type":"object","required":["id","digest"]}}}}
|