@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.
Files changed (165) hide show
  1. package/OPERATING.md +1 -1
  2. package/README.md +1 -1
  3. package/dist/manifest.json +156 -116
  4. package/dist/profiles/ai-orchestration/coverage-manifest.json +702 -153
  5. package/dist/profiles/ai-orchestration/implementation/accessibility.md +523 -42
  6. package/dist/profiles/ai-orchestration/pattern-contracts.json +892 -306
  7. package/dist/profiles/ai-orchestration/patterns/accordion.md +6 -1
  8. package/dist/profiles/ai-orchestration/patterns/alert.md +6 -1
  9. package/dist/profiles/ai-orchestration/patterns/carousel.md +6 -1
  10. package/dist/profiles/ai-orchestration/patterns/checkbox.md +27 -0
  11. package/dist/profiles/ai-orchestration/patterns/combobox.md +6 -1
  12. package/dist/profiles/ai-orchestration/patterns/consequential-submission.md +8 -3
  13. package/dist/profiles/ai-orchestration/patterns/dialog.md +6 -1
  14. package/dist/profiles/ai-orchestration/patterns/disclosure.md +6 -1
  15. package/dist/profiles/ai-orchestration/patterns/dynamic-status.md +6 -1
  16. package/dist/profiles/ai-orchestration/patterns/field.md +7 -1
  17. package/dist/profiles/ai-orchestration/patterns/image.md +6 -1
  18. package/dist/profiles/ai-orchestration/patterns/listbox.md +6 -1
  19. package/dist/profiles/ai-orchestration/patterns/media-player.md +9 -1
  20. package/dist/profiles/ai-orchestration/patterns/menu-button.md +6 -1
  21. package/dist/profiles/ai-orchestration/patterns/menu.md +30 -0
  22. package/dist/profiles/ai-orchestration/patterns/pagination.md +6 -1
  23. package/dist/profiles/ai-orchestration/patterns/popover.md +7 -1
  24. package/dist/profiles/ai-orchestration/patterns/progress.md +7 -1
  25. package/dist/profiles/ai-orchestration/patterns/radio-group.md +27 -0
  26. package/dist/profiles/ai-orchestration/patterns/slider.md +31 -0
  27. package/dist/profiles/ai-orchestration/patterns/spinbutton.md +28 -0
  28. package/dist/profiles/ai-orchestration/patterns/tabs.md +6 -1
  29. package/dist/profiles/ai-orchestration/patterns/tooltip.md +6 -1
  30. package/dist/profiles/ai-orchestration/patterns/tree-view.md +31 -0
  31. package/dist/profiles/ai-orchestration/patterns/validation.md +7 -1
  32. package/dist/profiles/ai-orchestration/review/accessibility.md +14 -6
  33. package/dist/profiles/ai-orchestration/semantics/accessible-authentication.md +13 -0
  34. package/dist/profiles/ai-orchestration/semantics/accessible-name.md +8 -2
  35. package/dist/profiles/ai-orchestration/semantics/alert.md +7 -1
  36. package/dist/profiles/ai-orchestration/semantics/consequential-action.md +6 -2
  37. package/dist/profiles/ai-orchestration/semantics/consistent-help.md +13 -0
  38. package/dist/profiles/ai-orchestration/semantics/data-table.md +7 -2
  39. package/dist/profiles/ai-orchestration/semantics/dragging-alternative.md +13 -0
  40. package/dist/profiles/ai-orchestration/semantics/expanded-state.md +6 -1
  41. package/dist/profiles/ai-orchestration/semantics/focus.initial.md +4 -1
  42. package/dist/profiles/ai-orchestration/semantics/focus.return.md +4 -1
  43. package/dist/profiles/ai-orchestration/semantics/focus.visible.md +8 -2
  44. package/dist/profiles/ai-orchestration/semantics/form-error.md +8 -2
  45. package/dist/profiles/ai-orchestration/semantics/form-label.md +7 -1
  46. package/dist/profiles/ai-orchestration/semantics/form-required.md +7 -1
  47. package/dist/profiles/ai-orchestration/semantics/headings.md +7 -1
  48. package/dist/profiles/ai-orchestration/semantics/keyboard.escape.md +4 -1
  49. package/dist/profiles/ai-orchestration/semantics/keyboard.md +7 -1
  50. package/dist/profiles/ai-orchestration/semantics/landmarks.md +6 -1
  51. package/dist/profiles/ai-orchestration/semantics/live-status.md +7 -2
  52. package/dist/profiles/ai-orchestration/semantics/media-alternatives.md +11 -1
  53. package/dist/profiles/ai-orchestration/semantics/motion-control.md +8 -2
  54. package/dist/profiles/ai-orchestration/semantics/name-role-value.md +6 -2
  55. package/dist/profiles/ai-orchestration/semantics/native-dialog.md +7 -1
  56. package/dist/profiles/ai-orchestration/semantics/native-elements.md +5 -1
  57. package/dist/profiles/ai-orchestration/semantics/non-text-alternative.md +5 -1
  58. package/dist/profiles/ai-orchestration/semantics/redundant-entry.md +13 -0
  59. package/dist/profiles/ai-orchestration/semantics/roles-states-properties.md +6 -1
  60. package/dist/profiles/ai-orchestration/semantics/selected-state.md +6 -1
  61. package/dist/profiles/ai-orchestration/semantics/target-size.md +6 -2
  62. package/dist/profiles/ai-orchestration/semantics.json +826 -130
  63. package/dist/profiles/ai-orchestration/testing/accessibility.md +33 -3
  64. package/dist/profiles/ai-orchestration/ui-design-brain-bindings.json +287 -307
  65. package/dist/profiles/conductor/a11y-semantics.md +249 -10
  66. package/dist/profiles/conductor/accessibility-quality.md +275 -33
  67. package/dist/profiles/conductor/applicability-matrix.json +226 -269
  68. package/dist/profiles/conductor/coverage-manifest.json +702 -153
  69. package/dist/profiles/conductor/pattern-contracts.json +892 -306
  70. package/dist/profiles/conductor/patterns/accordion.json +35 -27
  71. package/dist/profiles/conductor/patterns/alert.json +29 -21
  72. package/dist/profiles/conductor/patterns/carousel.json +36 -28
  73. package/dist/profiles/conductor/patterns/checkbox.json +67 -0
  74. package/dist/profiles/conductor/patterns/combobox.json +37 -29
  75. package/dist/profiles/conductor/patterns/consequential-submission.json +34 -25
  76. package/dist/profiles/conductor/patterns/dialog.json +37 -29
  77. package/dist/profiles/conductor/patterns/disclosure.json +33 -25
  78. package/dist/profiles/conductor/patterns/dynamic-status.json +31 -22
  79. package/dist/profiles/conductor/patterns/field.json +41 -25
  80. package/dist/profiles/conductor/patterns/image.json +31 -22
  81. package/dist/profiles/conductor/patterns/listbox.json +35 -27
  82. package/dist/profiles/conductor/patterns/media-player.json +59 -29
  83. package/dist/profiles/conductor/patterns/menu-button.json +34 -26
  84. package/dist/profiles/conductor/patterns/menu.json +78 -0
  85. package/dist/profiles/conductor/patterns/pagination.json +34 -25
  86. package/dist/profiles/conductor/patterns/popover.json +42 -26
  87. package/dist/profiles/conductor/patterns/progress.json +38 -23
  88. package/dist/profiles/conductor/patterns/radio-group.json +69 -0
  89. package/dist/profiles/conductor/patterns/slider.json +84 -0
  90. package/dist/profiles/conductor/patterns/spinbutton.json +74 -0
  91. package/dist/profiles/conductor/patterns/tabs.json +34 -26
  92. package/dist/profiles/conductor/patterns/tooltip.json +31 -23
  93. package/dist/profiles/conductor/patterns/tree-view.json +77 -0
  94. package/dist/profiles/conductor/patterns/validation.json +42 -26
  95. package/dist/profiles/conductor/semantics.json +826 -130
  96. package/dist/profiles/conductor/ui-design-brain-bindings.json +287 -307
  97. package/dist/projection-manifest.json +120 -104
  98. package/dist/requirements-to-implementation.md +7 -1
  99. package/lib/build.mjs +3 -3
  100. package/lib/digest.mjs +8 -0
  101. package/lib/load.mjs +13 -0
  102. package/lib/provenance.mjs +2 -0
  103. package/lib/render.mjs +51 -7
  104. package/lib/sync.mjs +2 -2
  105. package/lib/validate.mjs +335 -33
  106. package/package.json +6 -2
  107. package/profiles/ai-orchestration/profile.json +1 -1
  108. package/profiles/ai-orchestration/templates/patterns/README.md +1 -1
  109. package/profiles/ai-orchestration/templates/review/README.md +1 -1
  110. package/profiles/ai-orchestration/templates/semantics/README.md +1 -1
  111. package/profiles/ai-orchestration/templates/testing/README.md +1 -1
  112. package/profiles/conductor/profile.json +1 -1
  113. package/profiles/conductor/templates/evidence/README.md +1 -1
  114. package/profiles/conductor/templates/patterns/README.md +1 -1
  115. package/profiles/conductor/templates/semantics/README.md +1 -1
  116. package/schemas/consumer-config.schema.json +1 -1
  117. package/schemas/contract.schema.json +1 -1
  118. package/schemas/manifest.schema.json +1 -1
  119. package/schemas/pattern.schema.json +98 -1
  120. package/schemas/profile.schema.json +1 -1
  121. package/schemas/projection-manifest.schema.json +6 -6
  122. package/schemas/semantic.schema.json +26 -1
  123. package/schemas/standards-reference.schema.json +105 -0
  124. package/schemas/wcag-coverage.schema.json +93 -0
  125. package/src/applicability/facts.yml +3 -5
  126. package/src/applicability/matrix.yml +5 -7
  127. package/src/applicability/ui-design-brain-bindings.yml +8 -22
  128. package/src/contract.yml +1 -1
  129. package/src/coverage/wcag-2.2.yml +68 -0
  130. package/src/patterns/disclosure/accordion.yml +1 -1
  131. package/src/patterns/disclosure/disclosure.yml +1 -1
  132. package/src/patterns/feedback/alert.yml +1 -1
  133. package/src/patterns/feedback/dynamic-status.yml +1 -1
  134. package/src/patterns/feedback/progress.yml +1 -1
  135. package/src/patterns/forms/consequential-submission.yml +1 -1
  136. package/src/patterns/forms/field.yml +1 -1
  137. package/src/patterns/forms/spinbutton.yml +1 -0
  138. package/src/patterns/forms/validation.yml +1 -1
  139. package/src/patterns/media/carousel.yml +1 -1
  140. package/src/patterns/media/image.yml +1 -1
  141. package/src/patterns/media/media-player.yml +1 -1
  142. package/src/patterns/navigation/menu-button.yml +1 -1
  143. package/src/patterns/navigation/menu.yml +1 -0
  144. package/src/patterns/navigation/pagination.yml +1 -1
  145. package/src/patterns/navigation/tabs.yml +1 -1
  146. package/src/patterns/navigation/tree-view.yml +1 -0
  147. package/src/patterns/overlays/dialog.yml +1 -1
  148. package/src/patterns/overlays/popover.yml +1 -1
  149. package/src/patterns/overlays/tooltip.yml +1 -1
  150. package/src/patterns/selection/checkbox.yml +1 -0
  151. package/src/patterns/selection/combobox.yml +1 -1
  152. package/src/patterns/selection/listbox.yml +1 -1
  153. package/src/patterns/selection/radio-group.yml +1 -0
  154. package/src/patterns/selection/slider.yml +1 -0
  155. package/src/semantics/accessible-names.yml +1 -1
  156. package/src/semantics/forms.yml +1 -1
  157. package/src/semantics/headings-landmarks.yml +1 -1
  158. package/src/semantics/keyboard-focus.yml +1 -1
  159. package/src/semantics/live-regions.yml +1 -1
  160. package/src/semantics/media.yml +1 -1
  161. package/src/semantics/native-elements.yml +1 -1
  162. package/src/semantics/roles-states-properties.yml +1 -1
  163. package/src/semantics/tables.yml +1 -1
  164. package/src/semantics/wcag-2.2.yml +1 -0
  165. 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 !== 2 || data.contract?.package_version !== '2.0.0') errors.push('Expected accessibility contract schema 2 targeting package 2.0.0');
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(({ id }) => id));
94
- const patternIds = new Set(patterns.map(({ id }) => id));
95
- for (const semantic of semantics) validateStringArray(`${semantic.id ?? '<malformed semantic>'} proof`, semantic.proof, 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
+ }
96
273
  for (const pattern of patterns) {
97
- const id = pattern.id ?? '<malformed pattern>';
98
- if (!validateStringArray(`${id} semantic dependencies`, pattern.requires, errors)) errors.push(`Orphan pattern: ${id}`);
99
- for (const dependency of Array.isArray(pattern.requires) ? pattern.requires : []) if (!semanticIds.has(dependency)) errors.push(`${id} has missing semantic dependency ${dependency}`);
100
- if (!isNonEmptyString(pattern.scope)) errors.push(`${id} has no scope`);
101
- validateStringArray(`${id} behavior`, pattern.behavior, errors);
102
- validateStringArray(`${id} product decisions`, pattern.product_decisions, errors);
103
- validateStringArray(`${id} Functional Spec bindings`, pattern.functional_spec_bindings, errors);
104
- validateStringArray(`${id} implementation outcomes`, pattern.implementation_outcomes, errors);
105
- validateStringArray(`${id} evidence routes`, pattern.evidence_routes, errors);
106
- try { validateExpression(pattern.activation, facts); } catch (error) { errors.push(`${id} activation: ${error.message}`); }
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.proof) ? semantic.proof : []) if (!proofKinds.has(route)) errors.push(`${semantic.id} uses unsupported proof route ${route}`);
117
- 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} uses unsupported proof route ${route}`);
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 { compileApplicability({ rows: [row] }, data.facts); } catch (error) { errors.push(`${id}: ${error.message}`); }
129
- for (const outcome of Array.isArray(row?.outcomes) ? row.outcomes : []) if (!ids.has(outcome)) errors.push(`${id} references unknown outcome ${outcome}`);
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.proof) ? item.proof : []), ...patterns.flatMap((item) => Array.isArray(item.evidence_routes) ? item.evidence_routes : []), ...rows.flatMap((item) => Array.isArray(item.evidence) ? item.evidence : [])]);
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
- if (rows.length !== 19) errors.push(`Expected 19 applicability rows, found ${rows.length}`);
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 (!validateStringArray('Standards precedence', sources.precedence, errors)) return;
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 expectedOrder = contract.bindings.map(({ ui_pattern_id }) => ui_pattern_id).sort();
211
- const actualOrder = contract.bindings.map(({ ui_pattern_id }) => ui_pattern_id);
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 ?? []) if (!patternIds.has(patternId)) errors.push(`${prefix} references unknown pattern ${patternId}`);
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 ?? []) if (!semanticIds.has(semanticId)) errors.push(`${prefix} references unknown baseline semantic ${semanticId}`);
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 ?? []) if (!facts.facts[fact]) errors.push(`${prefix} references unknown discriminator fact ${fact}`);
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 ?? [])].sort();
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": "2.0.0",
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":"2.0.0","channels":["ui-pattern-bindings","semantics","patterns","implementation","testing","review"]}
1
+ {"name":"ai-orchestration","version":"3.0.0","channels":["ui-pattern-bindings","semantics","patterns","implementation","testing","review"]}
@@ -1,3 +1,3 @@
1
1
  # Implementation patterns template
2
2
 
3
- Expand patterns to semantic dependencies and preserve source IDs.
3
+ Expand patterns to semantic dependencies and preserve source IDs, activation, and informative APG references.
@@ -1,3 +1,3 @@
1
1
  # Review template
2
2
 
3
- Review source authority, semantics, patterns, implementation, tests, and evidence in that order.
3
+ Review source authority, WCAG coverage status, semantics, patterns, implementation, tests, and evidence in that order.
@@ -1,3 +1,3 @@
1
1
  # Implementation semantics template
2
2
 
3
- Always load applicable semantic requirements before patterns.
3
+ Always load applicable semantic requirements and their standards references before patterns.
@@ -1,3 +1,3 @@
1
1
  # Testing template
2
2
 
3
- Route each outcome to its exact unit, axe, E2E, and human lanes.
3
+ Route each outcome and WCAG coverage claim to its exact unit, axe, E2E, and human lanes.
@@ -1 +1 @@
1
- {"name":"conductor","version":"2.0.0","channels":["ui-pattern-bindings","semantics","patterns","applicability","evidence"]}
1
+ {"name":"conductor","version":"3.0.0","channels":["ui-pattern-bindings","semantics","patterns","applicability","evidence"]}
@@ -1,3 +1,3 @@
1
1
  # Conductor evidence template
2
2
 
3
- Bind each applicable outcome to proof kinds and artifact evidence references.
3
+ Bind each applicable outcome and WCAG coverage claim to proof kinds and artifact evidence references.
@@ -1,3 +1,3 @@
1
1
  # Conductor patterns template
2
2
 
3
- Render activation, behavior, decisions, bindings, outcomes, and evidence without restating semantic rules.
3
+ Render standards references, activation, behavior, decisions, bindings, outcomes, and evidence without restating semantic rules.
@@ -1,3 +1,3 @@
1
1
  # Conductor semantics template
2
2
 
3
- Render requirements in stable semantic ID order for Functional Specification authors.
3
+ Render requirements and normalized standards references in stable semantic ID order for Functional Specification authors.
@@ -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@2.0.0"},"profile":{"enum":["conductor","ai-orchestration"]},"routes":{"type":"string"},"outputRoot":{"type":"string"}},"additionalProperties":false}
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":2},"package_version":{"type":"string","const":"2.0.0"}}}
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":{"type":"string"},"schema_version":{"type":"integer"},"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"]}}}}
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"]}}}}