@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.
- package/CHANGELOG.md +48 -4
- package/LICENSE +373 -21
- package/README.md +70 -1
- package/bin/core.js +240 -11
- package/docs/API_STABILITY.md +61 -0
- package/docs/BASELINE.md +66 -0
- package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
- package/docs/CI_INTEGRATIONS.md +103 -0
- package/docs/CLI.md +80 -1
- package/docs/ENGINE_OPTIONS.md +4 -0
- package/docs/INTEGRATION.md +19 -1
- package/docs/OUTPUT_SCHEMA.md +2 -2
- package/docs/REPORT.md +33 -0
- package/docs/RULE_AUTHORING.md +31 -0
- package/docs/RULE_CATALOG.md +1 -1
- package/docs/SARIF.md +59 -0
- package/package.json +15 -4
- package/src/baseline.js +0 -0
- package/src/catalogs/composites.wcag.js +414 -450
- package/src/checks/automatic/area-alt-present.js +59 -25
- package/src/checks/automatic/aria-allowed-attr.js +193 -33
- package/src/checks/automatic/aria-allowed-role.js +21 -7
- package/src/checks/automatic/aria-braille-equivalent.js +32 -10
- package/src/checks/automatic/aria-conditional-attr.js +24 -7
- package/src/checks/automatic/aria-deprecated-role.js +22 -8
- package/src/checks/automatic/aria-hidden-body.js +50 -19
- package/src/checks/automatic/aria-hidden-focus.js +408 -53
- package/src/checks/automatic/aria-prohibited-attr.js +296 -22
- package/src/checks/automatic/aria-prohibited-children.js +125 -29
- package/src/checks/automatic/aria-required-attr.js +23 -8
- package/src/checks/automatic/aria-required-children.js +37 -14
- package/src/checks/automatic/aria-required-parent.js +48 -14
- package/src/checks/automatic/aria-role-name-present.js +47 -21
- package/src/checks/automatic/aria-roles-valid.js +22 -12
- package/src/checks/automatic/aria-valid-attr-value.js +28 -7
- package/src/checks/automatic/aria-valid-attr.js +17 -5
- package/src/checks/automatic/autocomplete-valid.js +74 -16
- package/src/checks/automatic/avoid-inline-spacing.js +20 -6
- package/src/checks/automatic/binary-control-name-present.js +60 -50
- package/src/checks/automatic/button-name-present.js +48 -18
- package/src/checks/automatic/bypass-blocks-present.js +50 -25
- package/src/checks/automatic/canvas-text-alternative-present.js +57 -26
- package/src/checks/automatic/combobox-name-present.js +38 -45
- package/src/checks/automatic/contrast-computable.js +361 -341
- package/src/checks/automatic/contrast-enhanced.js +487 -466
- package/src/checks/automatic/contrast-minimum.js +486 -465
- package/src/checks/automatic/css-orientation-lock.js +39 -9
- package/src/checks/automatic/definition-list-children-valid.js +40 -19
- package/src/checks/automatic/deprecated-elements-not-used.js +21 -7
- package/src/checks/automatic/dialog-name-present.js +37 -75
- package/src/checks/automatic/dlitem-parent-valid.js +23 -8
- package/src/checks/automatic/duplicate-id-aria.js +24 -6
- package/src/checks/automatic/embed-text-alternative-present.js +86 -35
- package/src/checks/automatic/form-control-programmatic-label-present.js +79 -196
- package/src/checks/automatic/form-control-single-label.js +47 -10
- package/src/checks/automatic/html-xml-lang-mismatch.js +43 -19
- package/src/checks/automatic/iframe-focusable-content.js +26 -11
- package/src/checks/automatic/iframe-name-present.js +31 -9
- package/src/checks/automatic/iframe-title-unique.js +29 -8
- package/src/checks/automatic/img-alt-present.js +47 -43
- package/src/checks/automatic/input-image-alt-present.js +143 -112
- package/src/checks/automatic/label-in-name.js +50 -22
- package/src/checks/automatic/language-page-present.js +117 -109
- package/src/checks/automatic/link-in-text-block.js +59 -19
- package/src/checks/automatic/link-name-present.js +45 -14
- package/src/checks/automatic/list-children-valid.js +26 -9
- package/src/checks/automatic/listbox-name-present.js +39 -19
- package/src/checks/automatic/listitem-parent-valid.js +18 -6
- package/src/checks/automatic/menuitem-name-present.js +39 -61
- package/src/checks/automatic/meta-refresh-no-exceptions.js +37 -8
- package/src/checks/automatic/meta-refresh-timing-absent.js +28 -6
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +32 -7
- package/src/checks/automatic/meter-name-present.js +36 -33
- package/src/checks/automatic/nested-interactive-controls-absent.js +31 -10
- package/src/checks/automatic/object-text-alternative-present.js +91 -39
- package/src/checks/automatic/option-name-present.js +38 -21
- package/src/checks/automatic/page-title-present.js +26 -7
- package/src/checks/automatic/progressbar-name-present.js +41 -34
- package/src/checks/automatic/role-img-alt-present.js +209 -157
- package/src/checks/automatic/searchbox-name-present.js +39 -19
- package/src/checks/automatic/server-side-image-map-absent.js +23 -8
- package/src/checks/automatic/slider-name-present.js +40 -47
- package/src/checks/automatic/spinbutton-name-present.js +39 -19
- package/src/checks/automatic/summary-name-present.js +37 -17
- package/src/checks/automatic/svg-image-text-alternative-present.js +114 -47
- package/src/checks/automatic/svg-text-alternative-present.js +246 -226
- package/src/checks/automatic/tab-name-present.js +37 -60
- package/src/checks/automatic/table-headers-attr-valid.js +24 -8
- package/src/checks/automatic/table-th-has-data-cells.js +22 -8
- package/src/checks/automatic/target-size-minimum.js +118 -48
- package/src/checks/automatic/td-has-header.js +29 -11
- package/src/checks/automatic/textbox-name-present.js +39 -19
- package/src/checks/automatic/tooltip-name-present.js +37 -18
- package/src/checks/automatic/treeitem-name-present.js +38 -21
- package/src/checks/automatic/valid-lang.js +20 -6
- package/src/checks/automatic/video-poster-text-alternative-present.js +79 -36
- package/src/checks/manual/accesskeys-manual.js +14 -5
- package/src/checks/manual/area-alt-decorative-manual.js +192 -193
- package/src/checks/manual/area-alt-quality-manual.js +182 -141
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +34 -11
- package/src/checks/manual/aria-text-manual.js +14 -6
- package/src/checks/manual/canvas-text-alternative-quality-manual.js +149 -114
- package/src/checks/manual/css-hidden-focus.js +196 -165
- package/src/checks/manual/embed-text-alternative-quality-manual.js +171 -160
- package/src/checks/manual/empty-heading-manual.js +24 -12
- package/src/checks/manual/empty-table-header-manual.js +21 -14
- package/src/checks/manual/focus-order-semantics-manual.js +45 -10
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +207 -246
- package/src/checks/manual/heading-order-manual.js +22 -12
- package/src/checks/manual/identical-links-same-purpose-manual.js +34 -12
- package/src/checks/manual/image-redundant-alt-manual.js +17 -7
- package/src/checks/manual/img-alt-decorative-manual.js +131 -96
- package/src/checks/manual/img-alt-quality-manual.js +176 -127
- package/src/checks/manual/input-image-alt-decorative-manual.js +125 -92
- package/src/checks/manual/input-image-alt-quality-manual.js +125 -92
- package/src/checks/manual/label-title-only-manual.js +14 -5
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +95 -29
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +82 -29
- package/src/checks/manual/landmark-main-is-top-level-manual.js +64 -23
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +54 -23
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +54 -23
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +17 -8
- package/src/checks/manual/landmark-one-main-manual.js +35 -21
- package/src/checks/manual/landmark-unique-manual.js +73 -45
- package/src/checks/manual/link-name-quality-manual.js +43 -12
- package/src/checks/manual/media-transcript-present-manual.js +35 -22
- package/src/checks/manual/meta-viewport-large-manual.js +25 -6
- package/src/checks/manual/mouse-only-event-handlers-manual.js +38 -11
- package/src/checks/manual/no-autoplay-audio-manual.js +20 -6
- package/src/checks/manual/object-text-alternative-quality-manual.js +175 -154
- package/src/checks/manual/p-as-heading-manual.js +22 -7
- package/src/checks/manual/page-has-heading-one-manual.js +40 -23
- package/src/checks/manual/page-title-patterns-manual.js +87 -51
- package/src/checks/manual/presentation-role-conflict-manual.js +59 -19
- package/src/checks/manual/region-manual.js +275 -62
- package/src/checks/manual/scope-attr-valid-manual.js +11 -9
- package/src/checks/manual/scrollable-region-focusable-manual.js +37 -11
- package/src/checks/manual/skip-link-manual.js +35 -12
- package/src/checks/manual/svg-text-alternative-quality-manual.js +206 -165
- package/src/checks/manual/tabindex-manual.js +11 -9
- package/src/checks/manual/table-duplicate-name-manual.js +17 -7
- package/src/checks/manual/table-fake-caption-manual.js +24 -7
- package/src/checks/manual/video-caption-manual.js +15 -4
- package/src/checks/manual-review.js +56 -12
- package/src/core/aria-helpers.js +1128 -823
- package/src/core/contrast-helpers.js +1217 -1062
- package/src/core/dom-helpers.js +4176 -3886
- package/src/core/dom-runner.js +720 -596
- package/src/core/frame-messaging.js +189 -138
- package/src/core/frame-scan.js +94 -82
- package/src/core/rollup-composites.js +94 -102
- package/src/core/rule-meta.js +71 -35
- package/src/core.js +37939 -29121
- package/src/i18n/en.js +1194 -887
- package/src/i18n/fr.js +1136 -793
- package/src/policy/contracts.js +13 -13
- package/src/policy/resolvePolicy.js +48 -44
- package/src/report.js +502 -0
- package/src/sarif.js +175 -0
- 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
|
-
|
|
7
|
-
|
|
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
|
-
|
|
16
|
-
|
|
15
|
+
const tags = runOnly && Array.isArray(runOnly.tags) ? runOnly.tags : null;
|
|
16
|
+
if (!tags || tags.length === 0) return null;
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
23
|
+
return null;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
function isWithinTargetLevel(compositeLevel, scanLevel) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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 };
|
package/src/core/rule-meta.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
24
|
-
|
|
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 =
|
|
42
|
+
const m = meta && typeof meta === 'object' ? meta : {};
|
|
40
43
|
|
|
41
|
-
const title =
|
|
42
|
-
const description =
|
|
43
|
-
const 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 =
|
|
46
|
-
|
|
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 =
|
|
58
|
+
const defaultSeverity =
|
|
59
|
+
typeof m.defaultSeverity === 'string' && m.defaultSeverity.trim()
|
|
57
60
|
? m.defaultSeverity.trim()
|
|
58
61
|
: 'moderate';
|
|
59
62
|
|
|
60
|
-
const defaultConfidence =
|
|
63
|
+
const defaultConfidence =
|
|
64
|
+
typeof m.defaultConfidence === 'string' && m.defaultConfidence.trim()
|
|
61
65
|
? m.defaultConfidence.trim()
|
|
62
66
|
: 'medium';
|
|
63
67
|
|
|
64
|
-
const type =
|
|
65
|
-
? m.type
|
|
66
|
-
: 'automatic';
|
|
68
|
+
const type = m.type === 'manual' || m.type === 'automatic' ? m.type : 'automatic';
|
|
67
69
|
|
|
68
|
-
const coverage =
|
|
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 =
|
|
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 =
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
81
|
-
|
|
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 =
|
|
84
|
-
const standard =
|
|
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 =
|
|
87
|
-
const 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 =
|
|
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 =
|
|
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 (
|
|
108
|
-
|
|
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,
|