@surea11y/core 1.1.2 → 1.3.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 (160) hide show
  1. package/CHANGELOG.md +48 -4
  2. package/LICENSE +373 -21
  3. package/README.md +70 -1
  4. package/bin/core.js +240 -11
  5. package/docs/API_STABILITY.md +61 -0
  6. package/docs/BASELINE.md +66 -0
  7. package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
  8. package/docs/CI_INTEGRATIONS.md +103 -0
  9. package/docs/CLI.md +80 -1
  10. package/docs/ENGINE_OPTIONS.md +4 -0
  11. package/docs/INTEGRATION.md +19 -1
  12. package/docs/OUTPUT_SCHEMA.md +2 -2
  13. package/docs/REPORT.md +33 -0
  14. package/docs/RULE_AUTHORING.md +31 -0
  15. package/docs/RULE_CATALOG.md +1 -1
  16. package/docs/SARIF.md +59 -0
  17. package/package.json +15 -4
  18. package/src/baseline.js +0 -0
  19. package/src/catalogs/composites.wcag.js +414 -450
  20. package/src/checks/automatic/area-alt-present.js +59 -25
  21. package/src/checks/automatic/aria-allowed-attr.js +193 -33
  22. package/src/checks/automatic/aria-allowed-role.js +21 -7
  23. package/src/checks/automatic/aria-braille-equivalent.js +32 -10
  24. package/src/checks/automatic/aria-conditional-attr.js +24 -7
  25. package/src/checks/automatic/aria-deprecated-role.js +22 -8
  26. package/src/checks/automatic/aria-hidden-body.js +50 -19
  27. package/src/checks/automatic/aria-hidden-focus.js +408 -53
  28. package/src/checks/automatic/aria-prohibited-attr.js +296 -22
  29. package/src/checks/automatic/aria-prohibited-children.js +125 -29
  30. package/src/checks/automatic/aria-required-attr.js +23 -8
  31. package/src/checks/automatic/aria-required-children.js +37 -14
  32. package/src/checks/automatic/aria-required-parent.js +48 -14
  33. package/src/checks/automatic/aria-role-name-present.js +47 -21
  34. package/src/checks/automatic/aria-roles-valid.js +22 -12
  35. package/src/checks/automatic/aria-valid-attr-value.js +28 -7
  36. package/src/checks/automatic/aria-valid-attr.js +17 -5
  37. package/src/checks/automatic/autocomplete-valid.js +74 -16
  38. package/src/checks/automatic/avoid-inline-spacing.js +20 -6
  39. package/src/checks/automatic/binary-control-name-present.js +60 -50
  40. package/src/checks/automatic/button-name-present.js +48 -18
  41. package/src/checks/automatic/bypass-blocks-present.js +50 -25
  42. package/src/checks/automatic/canvas-text-alternative-present.js +57 -26
  43. package/src/checks/automatic/combobox-name-present.js +38 -45
  44. package/src/checks/automatic/contrast-computable.js +361 -341
  45. package/src/checks/automatic/contrast-enhanced.js +487 -466
  46. package/src/checks/automatic/contrast-minimum.js +486 -465
  47. package/src/checks/automatic/css-orientation-lock.js +39 -9
  48. package/src/checks/automatic/definition-list-children-valid.js +40 -19
  49. package/src/checks/automatic/deprecated-elements-not-used.js +21 -7
  50. package/src/checks/automatic/dialog-name-present.js +37 -75
  51. package/src/checks/automatic/dlitem-parent-valid.js +23 -8
  52. package/src/checks/automatic/duplicate-id-aria.js +24 -6
  53. package/src/checks/automatic/embed-text-alternative-present.js +86 -35
  54. package/src/checks/automatic/form-control-programmatic-label-present.js +79 -196
  55. package/src/checks/automatic/form-control-single-label.js +47 -10
  56. package/src/checks/automatic/html-xml-lang-mismatch.js +43 -19
  57. package/src/checks/automatic/iframe-focusable-content.js +26 -11
  58. package/src/checks/automatic/iframe-name-present.js +31 -9
  59. package/src/checks/automatic/iframe-title-unique.js +29 -8
  60. package/src/checks/automatic/img-alt-present.js +47 -43
  61. package/src/checks/automatic/input-image-alt-present.js +143 -112
  62. package/src/checks/automatic/label-in-name.js +50 -22
  63. package/src/checks/automatic/language-page-present.js +117 -109
  64. package/src/checks/automatic/link-in-text-block.js +59 -19
  65. package/src/checks/automatic/link-name-present.js +45 -14
  66. package/src/checks/automatic/list-children-valid.js +26 -9
  67. package/src/checks/automatic/listbox-name-present.js +39 -19
  68. package/src/checks/automatic/listitem-parent-valid.js +18 -6
  69. package/src/checks/automatic/menuitem-name-present.js +39 -61
  70. package/src/checks/automatic/meta-refresh-no-exceptions.js +37 -8
  71. package/src/checks/automatic/meta-refresh-timing-absent.js +28 -6
  72. package/src/checks/automatic/meta-viewport-zoom-enabled.js +32 -7
  73. package/src/checks/automatic/meter-name-present.js +36 -33
  74. package/src/checks/automatic/nested-interactive-controls-absent.js +31 -10
  75. package/src/checks/automatic/object-text-alternative-present.js +91 -39
  76. package/src/checks/automatic/option-name-present.js +38 -21
  77. package/src/checks/automatic/page-title-present.js +26 -7
  78. package/src/checks/automatic/progressbar-name-present.js +41 -34
  79. package/src/checks/automatic/role-img-alt-present.js +209 -157
  80. package/src/checks/automatic/searchbox-name-present.js +39 -19
  81. package/src/checks/automatic/server-side-image-map-absent.js +23 -8
  82. package/src/checks/automatic/slider-name-present.js +40 -47
  83. package/src/checks/automatic/spinbutton-name-present.js +39 -19
  84. package/src/checks/automatic/summary-name-present.js +37 -17
  85. package/src/checks/automatic/svg-image-text-alternative-present.js +114 -47
  86. package/src/checks/automatic/svg-text-alternative-present.js +246 -226
  87. package/src/checks/automatic/tab-name-present.js +37 -60
  88. package/src/checks/automatic/table-headers-attr-valid.js +24 -8
  89. package/src/checks/automatic/table-th-has-data-cells.js +22 -8
  90. package/src/checks/automatic/target-size-minimum.js +118 -48
  91. package/src/checks/automatic/td-has-header.js +29 -11
  92. package/src/checks/automatic/textbox-name-present.js +39 -19
  93. package/src/checks/automatic/tooltip-name-present.js +37 -18
  94. package/src/checks/automatic/treeitem-name-present.js +38 -21
  95. package/src/checks/automatic/valid-lang.js +20 -6
  96. package/src/checks/automatic/video-poster-text-alternative-present.js +79 -36
  97. package/src/checks/manual/accesskeys-manual.js +14 -5
  98. package/src/checks/manual/area-alt-decorative-manual.js +192 -193
  99. package/src/checks/manual/area-alt-quality-manual.js +182 -141
  100. package/src/checks/manual/aria-checked-state-mismatch-manual.js +34 -11
  101. package/src/checks/manual/aria-text-manual.js +14 -6
  102. package/src/checks/manual/canvas-text-alternative-quality-manual.js +149 -114
  103. package/src/checks/manual/css-hidden-focus.js +196 -165
  104. package/src/checks/manual/embed-text-alternative-quality-manual.js +171 -160
  105. package/src/checks/manual/empty-heading-manual.js +24 -12
  106. package/src/checks/manual/empty-table-header-manual.js +21 -14
  107. package/src/checks/manual/focus-order-semantics-manual.js +45 -10
  108. package/src/checks/manual/form-control-programmatic-label-quality-manual.js +207 -246
  109. package/src/checks/manual/heading-order-manual.js +22 -12
  110. package/src/checks/manual/identical-links-same-purpose-manual.js +34 -12
  111. package/src/checks/manual/image-redundant-alt-manual.js +17 -7
  112. package/src/checks/manual/img-alt-decorative-manual.js +131 -96
  113. package/src/checks/manual/img-alt-quality-manual.js +176 -127
  114. package/src/checks/manual/input-image-alt-decorative-manual.js +125 -92
  115. package/src/checks/manual/input-image-alt-quality-manual.js +125 -92
  116. package/src/checks/manual/label-title-only-manual.js +14 -5
  117. package/src/checks/manual/landmark-banner-is-top-level-manual.js +95 -29
  118. package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +82 -29
  119. package/src/checks/manual/landmark-main-is-top-level-manual.js +64 -23
  120. package/src/checks/manual/landmark-no-duplicate-banner-manual.js +54 -23
  121. package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +54 -23
  122. package/src/checks/manual/landmark-no-duplicate-main-manual.js +17 -8
  123. package/src/checks/manual/landmark-one-main-manual.js +35 -21
  124. package/src/checks/manual/landmark-unique-manual.js +73 -45
  125. package/src/checks/manual/link-name-quality-manual.js +43 -12
  126. package/src/checks/manual/media-transcript-present-manual.js +35 -22
  127. package/src/checks/manual/meta-viewport-large-manual.js +25 -6
  128. package/src/checks/manual/mouse-only-event-handlers-manual.js +38 -11
  129. package/src/checks/manual/no-autoplay-audio-manual.js +20 -6
  130. package/src/checks/manual/object-text-alternative-quality-manual.js +175 -154
  131. package/src/checks/manual/p-as-heading-manual.js +22 -7
  132. package/src/checks/manual/page-has-heading-one-manual.js +40 -23
  133. package/src/checks/manual/page-title-patterns-manual.js +87 -51
  134. package/src/checks/manual/presentation-role-conflict-manual.js +59 -19
  135. package/src/checks/manual/region-manual.js +275 -62
  136. package/src/checks/manual/scope-attr-valid-manual.js +11 -9
  137. package/src/checks/manual/scrollable-region-focusable-manual.js +37 -11
  138. package/src/checks/manual/skip-link-manual.js +35 -12
  139. package/src/checks/manual/svg-text-alternative-quality-manual.js +206 -165
  140. package/src/checks/manual/tabindex-manual.js +11 -9
  141. package/src/checks/manual/table-duplicate-name-manual.js +17 -7
  142. package/src/checks/manual/table-fake-caption-manual.js +24 -7
  143. package/src/checks/manual/video-caption-manual.js +15 -4
  144. package/src/checks/manual-review.js +56 -12
  145. package/src/core/aria-helpers.js +1128 -823
  146. package/src/core/contrast-helpers.js +1217 -1062
  147. package/src/core/dom-helpers.js +4176 -3886
  148. package/src/core/dom-runner.js +720 -596
  149. package/src/core/frame-messaging.js +189 -138
  150. package/src/core/frame-scan.js +94 -82
  151. package/src/core/rollup-composites.js +94 -102
  152. package/src/core/rule-meta.js +71 -35
  153. package/src/core.js +37939 -29121
  154. package/src/i18n/en.js +1194 -887
  155. package/src/i18n/fr.js +1136 -793
  156. package/src/policy/contracts.js +13 -13
  157. package/src/policy/resolvePolicy.js +48 -44
  158. package/src/report.js +502 -0
  159. package/src/sarif.js +175 -0
  160. package/surea11y.browser.js +36042 -0
@@ -3,8 +3,8 @@
3
3
  const LEVEL_RANK = Object.freeze({ A: 1, AA: 2, AAA: 3 });
4
4
 
5
5
  function normalizeScanLevel(scanLevel) {
6
- if (scanLevel === 'A' || scanLevel === 'AA' || scanLevel === 'AAA') return scanLevel;
7
- return null;
6
+ if (scanLevel === 'A' || scanLevel === 'AA' || scanLevel === 'AAA') return scanLevel;
7
+ return null;
8
8
  }
9
9
 
10
10
  /**
@@ -12,38 +12,38 @@ function normalizeScanLevel(scanLevel) {
12
12
  * (If you already pass scanLevel explicitly, you can ignore this.)
13
13
  */
14
14
  function inferScanLevelFromRunOnly(runOnly) {
15
- const tags = runOnly && Array.isArray(runOnly.tags) ? runOnly.tags : null;
16
- if (!tags || tags.length === 0) return null;
15
+ const tags = runOnly && Array.isArray(runOnly.tags) ? runOnly.tags : null;
16
+ if (!tags || tags.length === 0) return null;
17
17
 
18
- // Common reference-engine-style tags; adjust if your app uses different ones.
19
- if (tags.includes('wcag2aaa') || tags.includes('wcag22aaa')) return 'AAA';
20
- if (tags.includes('wcag2aa') || tags.includes('wcag22aa')) return 'AA';
21
- if (tags.includes('wcag2a') || tags.includes('wcag22a')) return 'A';
18
+ // Common reference-engine-style tags; adjust if your app uses different ones.
19
+ if (tags.includes('wcag2aaa') || tags.includes('wcag22aaa')) return 'AAA';
20
+ if (tags.includes('wcag2aa') || tags.includes('wcag22aa')) return 'AA';
21
+ if (tags.includes('wcag2a') || tags.includes('wcag22a')) return 'A';
22
22
 
23
- return null;
23
+ return null;
24
24
  }
25
25
 
26
26
  function isWithinTargetLevel(compositeLevel, scanLevel) {
27
- const c = LEVEL_RANK[compositeLevel];
28
- const s = LEVEL_RANK[scanLevel];
29
- if (!c || !s) return false;
30
- return c <= s;
27
+ const c = LEVEL_RANK[compositeLevel];
28
+ const s = LEVEL_RANK[scanLevel];
29
+ if (!c || !s) return false;
30
+ return c <= s;
31
31
  }
32
32
 
33
33
  function aggregateOutcome(childOutcomes) {
34
- // Deterministic priority: fail > cantTell > pass > notApplicable
35
- let hasPass = false;
36
- let hasCantTell = false;
37
-
38
- for (const o of childOutcomes) {
39
- if (o === 'fail') return 'fail';
40
- if (o === 'cantTell') hasCantTell = true;
41
- else if (o === 'pass') hasPass = true;
42
- }
43
-
44
- if (hasCantTell) return 'cantTell';
45
- if (hasPass) return 'pass';
46
- return 'notApplicable';
34
+ // Deterministic priority: fail > cantTell > pass > notApplicable
35
+ let hasPass = false;
36
+ let hasCantTell = false;
37
+
38
+ for (const o of childOutcomes) {
39
+ if (o === 'fail') return 'fail';
40
+ if (o === 'cantTell') hasCantTell = true;
41
+ else if (o === 'pass') hasPass = true;
42
+ }
43
+
44
+ if (hasCantTell) return 'cantTell';
45
+ if (hasPass) return 'pass';
46
+ return 'notApplicable';
47
47
  }
48
48
 
49
49
  /**
@@ -51,85 +51,77 @@ function aggregateOutcome(childOutcomes) {
51
51
  *
52
52
  * Inputs are plain JS objects/arrays; function never throws and is deterministic.
53
53
  */
54
- function rollupComposites({
55
- atomicResults,
56
- composites,
57
- scanLevel,
58
- runOnly
59
- }) {
60
- try {
61
- const target =
62
- normalizeScanLevel(scanLevel) ||
63
- inferScanLevelFromRunOnly(runOnly) ||
64
- 'AAA'; // safest default: if unspecified, do not hide anything
65
-
66
- const atomicByRuleId = Object.create(null);
67
- if (Array.isArray(atomicResults)) {
68
- for (const r of atomicResults) {
69
- if (!r || typeof r.ruleId !== 'string') continue;
70
- atomicByRuleId[r.ruleId] = r;
71
- }
72
- }
54
+ function rollupComposites({ atomicResults, composites, scanLevel, runOnly }) {
55
+ try {
56
+ const target = normalizeScanLevel(scanLevel) || inferScanLevelFromRunOnly(runOnly) || 'AAA'; // safest default: if unspecified, do not hide anything
57
+
58
+ const atomicByRuleId = Object.create(null);
59
+ if (Array.isArray(atomicResults)) {
60
+ for (const r of atomicResults) {
61
+ if (!r || typeof r.ruleId !== 'string') continue;
62
+ atomicByRuleId[r.ruleId] = r;
63
+ }
64
+ }
73
65
 
74
- const out = [];
75
- const list = Array.isArray(composites) ? composites : [];
76
- for (const c of list) {
77
- const level = c && c.meta && c.meta.level;
78
- if (!isWithinTargetLevel(level, target)) continue;
79
-
80
- const checksIds = Array.isArray(c.checksIds) ? c.checksIds : [];
81
- const childOutcomes = checksIds.map((id) => {
82
- const rr = atomicByRuleId[id];
83
- return rr && typeof rr.outcome === 'string' ? rr.outcome : 'notApplicable';
84
- });
85
-
86
- const outcome = aggregateOutcome(childOutcomes);
87
-
88
- // Deterministic i18n keys + structured details (matches your reporting schema expectations)
89
- const i18nKey =
90
- outcome === 'fail'
91
- ? 'composite.outcome.fail'
92
- : outcome === 'cantTell'
93
- ? 'composite.outcome.cantTell'
94
- : outcome === 'pass'
95
- ? 'composite.outcome.pass'
96
- : 'composite.outcome.notApplicable';
97
-
98
- out.push({
99
- ruleId: c.id,
100
- outcome,
101
- confidence: outcome === 'cantTell' ? 'low' : 'high',
102
- summaryKey: i18nKey, // resolved at build-time by your i18n layer
103
- i18nKey,
104
- i18nParams: {
105
- wcagSc: (c.meta && c.meta.wcagSc && c.meta.wcagSc[0]) || null,
106
- level: level || null,
107
- scanLevel: target
108
- },
109
- data: {
110
- details: {
111
- reasonCode:
112
- outcome === 'cantTell'
113
- ? 'oneOrMoreChecksCantTell'
114
- : outcome === 'fail'
115
- ? 'oneOrMoreChecksFailed'
116
- : outcome === 'pass'
117
- ? 'allApplicableChecksPassed'
118
- : 'allChecksNotApplicable',
119
- scanLevel: target,
120
- compositeLevel: level || null,
121
- checksIds,
122
- childOutcomes
123
- }
124
- }
125
- });
66
+ const out = [];
67
+ const list = Array.isArray(composites) ? composites : [];
68
+ for (const c of list) {
69
+ const level = c && c.meta && c.meta.level;
70
+ if (!isWithinTargetLevel(level, target)) continue;
71
+
72
+ const checksIds = Array.isArray(c.checksIds) ? c.checksIds : [];
73
+ const childOutcomes = checksIds.map((id) => {
74
+ const rr = atomicByRuleId[id];
75
+ return rr && typeof rr.outcome === 'string' ? rr.outcome : 'notApplicable';
76
+ });
77
+
78
+ const outcome = aggregateOutcome(childOutcomes);
79
+
80
+ // Deterministic i18n keys + structured details (matches your reporting schema expectations)
81
+ const i18nKey =
82
+ outcome === 'fail'
83
+ ? 'composite.outcome.fail'
84
+ : outcome === 'cantTell'
85
+ ? 'composite.outcome.cantTell'
86
+ : outcome === 'pass'
87
+ ? 'composite.outcome.pass'
88
+ : 'composite.outcome.notApplicable';
89
+
90
+ out.push({
91
+ ruleId: c.id,
92
+ outcome,
93
+ confidence: outcome === 'cantTell' ? 'low' : 'high',
94
+ summaryKey: i18nKey, // resolved at build-time by your i18n layer
95
+ i18nKey,
96
+ i18nParams: {
97
+ wcagSc: (c.meta && c.meta.wcagSc && c.meta.wcagSc[0]) || null,
98
+ level: level || null,
99
+ scanLevel: target
100
+ },
101
+ data: {
102
+ details: {
103
+ reasonCode:
104
+ outcome === 'cantTell'
105
+ ? 'oneOrMoreChecksCantTell'
106
+ : outcome === 'fail'
107
+ ? 'oneOrMoreChecksFailed'
108
+ : outcome === 'pass'
109
+ ? 'allApplicableChecksPassed'
110
+ : 'allChecksNotApplicable',
111
+ scanLevel: target,
112
+ compositeLevel: level || null,
113
+ checksIds,
114
+ childOutcomes
115
+ }
126
116
  }
127
-
128
- return out;
129
- } catch (e) {
130
- // no-throws guarantee
131
- return [];
117
+ });
132
118
  }
119
+
120
+ return out;
121
+ } catch (e) {
122
+ // no-throws guarantee
123
+ return [];
124
+ }
133
125
  }
134
126
 
135
127
  module.exports = { rollupComposites };
@@ -14,14 +14,17 @@
14
14
  function normalizeRuleMeta(ruleId, id, meta, engineTag) {
15
15
  function normalizeStringArray(value) {
16
16
  if (!Array.isArray(value)) return [];
17
- return value.map(String).map((s) => s.trim()).filter(Boolean);
17
+ return value
18
+ .map(String)
19
+ .map((s) => s.trim())
20
+ .filter(Boolean);
18
21
  }
19
22
 
20
23
  function normalizeObjectArray(value) {
21
24
  if (!Array.isArray(value)) return [];
22
25
  return value
23
- .filter((v) => v && typeof v === 'object' && !Array.isArray(v))
24
- .map((v) => ({ ...v }));
26
+ .filter((v) => v && typeof v === 'object' && !Array.isArray(v))
27
+ .map((v) => ({ ...v }));
25
28
  }
26
29
 
27
30
  function deriveWcagScFromNormativeMappings(normativeMappings) {
@@ -36,15 +39,14 @@ function normalizeRuleMeta(ruleId, id, meta, engineTag) {
36
39
  return Array.from(out).sort();
37
40
  }
38
41
 
39
- const m = (meta && typeof meta === 'object') ? meta : {};
42
+ const m = meta && typeof meta === 'object' ? meta : {};
40
43
 
41
- const title = (typeof m.title === 'string' && m.title.trim()) ? m.title.trim() : id;
42
- const description = (typeof m.description === 'string') ? m.description : '';
43
- const helpUrl = (typeof m.helpUrl === 'string') ? m.helpUrl : '';
44
+ const title = typeof m.title === 'string' && m.title.trim() ? m.title.trim() : id;
45
+ const description = typeof m.description === 'string' ? m.description : '';
46
+ const helpUrl = typeof m.helpUrl === 'string' ? m.helpUrl : '';
44
47
 
45
- const i18n = (m.i18n && typeof m.i18n === 'object' && !Array.isArray(m.i18n))
46
- ? { ...m.i18n }
47
- : null;
48
+ const i18n =
49
+ m.i18n && typeof m.i18n === 'object' && !Array.isArray(m.i18n) ? { ...m.i18n } : null;
48
50
 
49
51
  const tags = normalizeStringArray(m.tags).map((t) => t.toLowerCase());
50
52
  if (!tags.includes(engineTag)) tags.push(engineTag);
@@ -53,59 +55,91 @@ function normalizeRuleMeta(ruleId, id, meta, engineTag) {
53
55
  const wcagSc = deriveWcagScFromNormativeMappings(normativeMappings);
54
56
  const informativeReferences = normalizeObjectArray(m.informativeReferences);
55
57
 
56
- const defaultSeverity = (typeof m.defaultSeverity === 'string' && m.defaultSeverity.trim())
58
+ const defaultSeverity =
59
+ typeof m.defaultSeverity === 'string' && m.defaultSeverity.trim()
57
60
  ? m.defaultSeverity.trim()
58
61
  : 'moderate';
59
62
 
60
- const defaultConfidence = (typeof m.defaultConfidence === 'string' && m.defaultConfidence.trim())
63
+ const defaultConfidence =
64
+ typeof m.defaultConfidence === 'string' && m.defaultConfidence.trim()
61
65
  ? m.defaultConfidence.trim()
62
66
  : 'medium';
63
67
 
64
- const type = (m.type === 'manual' || m.type === 'automatic')
65
- ? m.type
66
- : 'automatic';
68
+ const type = m.type === 'manual' || m.type === 'automatic' ? m.type : 'automatic';
67
69
 
68
- const coverage = (m.coverage === null || typeof m.coverage === 'string' || typeof m.coverage === 'object')
70
+ const coverage =
71
+ m.coverage === null || typeof m.coverage === 'string' || typeof m.coverage === 'object'
69
72
  ? m.coverage
70
73
  : null;
71
74
 
72
- const ruleInterfaceVersion = (typeof m.ruleInterfaceVersion === 'string' && m.ruleInterfaceVersion.trim())
75
+ const ruleInterfaceVersion =
76
+ typeof m.ruleInterfaceVersion === 'string' && m.ruleInterfaceVersion.trim()
73
77
  ? m.ruleInterfaceVersion.trim()
74
78
  : '1.0.0';
75
79
 
76
- const ruleVersion = (typeof m.ruleVersion === 'string' && m.ruleVersion.trim())
77
- ? m.ruleVersion.trim()
78
- : '0.0.0';
80
+ const ruleVersion =
81
+ typeof m.ruleVersion === 'string' && m.ruleVersion.trim() ? m.ruleVersion.trim() : '0.0.0';
82
+
83
+ const normative = typeof m.normative === 'boolean' ? m.normative : true;
84
+ const atomic = typeof m.atomic === 'boolean' ? m.atomic : true;
85
+
86
+ // Deprecation signal for the rule catalog (see docs/API_STABILITY.md).
87
+ // Purely informational -- a deprecated rule still runs and produces
88
+ // results completely normally; this is a catalog-level migration signal
89
+ // for integrators, not an automatic exclusion.
90
+ const deprecated = typeof m.deprecated === 'boolean' ? m.deprecated : false;
91
+ const deprecation =
92
+ deprecated &&
93
+ m.deprecation &&
94
+ typeof m.deprecation === 'object' &&
95
+ !Array.isArray(m.deprecation)
96
+ ? {
97
+ replacedBy:
98
+ typeof m.deprecation.replacedBy === 'string' && m.deprecation.replacedBy.trim()
99
+ ? m.deprecation.replacedBy.trim()
100
+ : null,
101
+ reason: typeof m.deprecation.reason === 'string' ? m.deprecation.reason.trim() : '',
102
+ sinceVersion:
103
+ typeof m.deprecation.sinceVersion === 'string' ? m.deprecation.sinceVersion.trim() : ''
104
+ }
105
+ : null;
79
106
 
80
- const normative = (typeof m.normative === 'boolean') ? m.normative : true;
81
- const atomic = (typeof m.atomic === 'boolean') ? m.atomic : true;
107
+ if (deprecated && (!deprecation || !deprecation.reason || !deprecation.sinceVersion)) {
108
+ throw new Error(
109
+ `Rule ${ruleId}: meta.deprecated:true requires meta.deprecation.reason and meta.deprecation.sinceVersion`
110
+ );
111
+ }
82
112
 
83
- const category = (typeof m.category === 'string' && m.category.trim()) ? m.category.trim() : null;
84
- const standard = (typeof m.standard === 'string' && m.standard.trim()) ? m.standard.trim() : null;
113
+ const category = typeof m.category === 'string' && m.category.trim() ? m.category.trim() : null;
114
+ const standard = typeof m.standard === 'string' && m.standard.trim() ? m.standard.trim() : null;
85
115
 
86
- const applicability = (typeof m.applicability === 'string') ? m.applicability : '';
87
- const expectation = (typeof m.expectation === 'string') ? m.expectation : '';
116
+ const applicability = typeof m.applicability === 'string' ? m.applicability : '';
117
+ const expectation = typeof m.expectation === 'string' ? m.expectation : '';
88
118
 
89
119
  const references = Array.isArray(m.references) ? m.references.slice() : [];
90
- const requirements = (m.requirements === null || typeof m.requirements === 'string' || typeof m.requirements === 'object')
120
+ const requirements =
121
+ m.requirements === null ||
122
+ typeof m.requirements === 'string' ||
123
+ typeof m.requirements === 'object'
91
124
  ? m.requirements
92
125
  : null;
93
126
 
94
- const mappings = (m.mappings === null || typeof m.mappings === 'string' || typeof m.mappings === 'object')
127
+ const mappings =
128
+ m.mappings === null || typeof m.mappings === 'string' || typeof m.mappings === 'object'
95
129
  ? m.mappings
96
130
  : null;
97
131
 
98
- if (!Array.isArray(tags)) throw new Error(`Rule ${ruleId}: meta.tags must be an array`);
99
- if (!Array.isArray(normativeMappings)) throw new Error(`Rule ${ruleId}: meta.normativeMappings must be an array`);
100
- if (!Array.isArray(informativeReferences)) throw new Error(`Rule ${ruleId}: meta.informativeReferences must be an array`);
101
- if (type !== 'automatic' && type !== 'manual') throw new Error(`Rule ${ruleId}: meta.type must be "automatic" or "manual"`);
102
-
103
132
  if (i18n) {
104
133
  if (typeof i18n.titleKey !== 'string' || !i18n.titleKey.trim()) {
105
134
  throw new Error(`Rule ${ruleId}: meta.i18n.titleKey must be a non-empty string`);
106
135
  }
107
- if (i18n.descriptionKey != null && (typeof i18n.descriptionKey !== 'string' || !i18n.descriptionKey.trim())) {
108
- throw new Error(`Rule ${ruleId}: meta.i18n.descriptionKey must be a non-empty string when provided`);
136
+ if (
137
+ i18n.descriptionKey != null &&
138
+ (typeof i18n.descriptionKey !== 'string' || !i18n.descriptionKey.trim())
139
+ ) {
140
+ throw new Error(
141
+ `Rule ${ruleId}: meta.i18n.descriptionKey must be a non-empty string when provided`
142
+ );
109
143
  }
110
144
  }
111
145
 
@@ -127,6 +161,8 @@ function normalizeRuleMeta(ruleId, id, meta, engineTag) {
127
161
  ruleVersion,
128
162
  normative,
129
163
  atomic,
164
+ deprecated,
165
+ deprecation,
130
166
  category,
131
167
  standard,
132
168
  applicability,