@surea11y/core 1.2.0 → 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 +37 -7
- package/LICENSE +373 -21
- package/README.md +67 -1
- package/bin/core.js +144 -19
- package/docs/API_STABILITY.md +1 -1
- package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
- package/docs/CI_INTEGRATIONS.md +103 -0
- package/docs/CLI.md +54 -1
- package/docs/ENGINE_OPTIONS.md +2 -0
- package/docs/INTEGRATION.md +18 -0
- package/docs/OUTPUT_SCHEMA.md +1 -1
- 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 +42 -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 +55 -16
- 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 +42 -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 +30 -8
- 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 +34 -18
- 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 +109 -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 +28 -7
- package/src/checks/automatic/meta-refresh-timing-absent.js +20 -6
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +24 -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 +17 -6
- 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 -7
- package/src/checks/manual/empty-table-header-manual.js +17 -6
- 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 -7
- 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 +66 -16
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +53 -16
- package/src/checks/manual/landmark-main-is-top-level-manual.js +35 -10
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +29 -10
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +29 -10
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +17 -8
- package/src/checks/manual/landmark-one-main-manual.js +26 -20
- package/src/checks/manual/landmark-unique-manual.js +41 -15
- 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 +16 -5
- 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 +31 -22
- package/src/checks/manual/page-title-patterns-manual.js +78 -50
- package/src/checks/manual/presentation-role-conflict-manual.js +59 -19
- package/src/checks/manual/region-manual.js +241 -48
- package/src/checks/manual/scope-attr-valid-manual.js +10 -3
- 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 +10 -3
- 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 +1127 -886
- package/src/core/contrast-helpers.js +1217 -1062
- package/src/core/dom-helpers.js +4175 -3917
- package/src/core/dom-runner.js +720 -604
- 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 +58 -41
- package/src/core.js +36552 -29111
- package/src/i18n/en.js +1194 -889
- package/src/i18n/fr.js +1136 -795
- package/src/policy/contracts.js +13 -13
- package/src/policy/resolvePolicy.js +48 -44
- package/src/report.js +63 -43
- 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,76 +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
|
-
: '0.0.0';
|
|
80
|
+
const ruleVersion =
|
|
81
|
+
typeof m.ruleVersion === 'string' && m.ruleVersion.trim() ? m.ruleVersion.trim() : '0.0.0';
|
|
79
82
|
|
|
80
|
-
const normative =
|
|
81
|
-
const atomic =
|
|
83
|
+
const normative = typeof m.normative === 'boolean' ? m.normative : true;
|
|
84
|
+
const atomic = typeof m.atomic === 'boolean' ? m.atomic : true;
|
|
82
85
|
|
|
83
86
|
// Deprecation signal for the rule catalog (see docs/API_STABILITY.md).
|
|
84
87
|
// Purely informational -- a deprecated rule still runs and produces
|
|
85
88
|
// results completely normally; this is a catalog-level migration signal
|
|
86
89
|
// for integrators, not an automatic exclusion.
|
|
87
|
-
const deprecated =
|
|
88
|
-
const deprecation =
|
|
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)
|
|
89
96
|
? {
|
|
90
|
-
replacedBy:
|
|
91
|
-
|
|
92
|
-
|
|
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() : ''
|
|
93
104
|
}
|
|
94
105
|
: null;
|
|
95
106
|
|
|
96
107
|
if (deprecated && (!deprecation || !deprecation.reason || !deprecation.sinceVersion)) {
|
|
97
|
-
throw new Error(
|
|
108
|
+
throw new Error(
|
|
109
|
+
`Rule ${ruleId}: meta.deprecated:true requires meta.deprecation.reason and meta.deprecation.sinceVersion`
|
|
110
|
+
);
|
|
98
111
|
}
|
|
99
112
|
|
|
100
|
-
const category =
|
|
101
|
-
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;
|
|
102
115
|
|
|
103
|
-
const applicability =
|
|
104
|
-
const expectation =
|
|
116
|
+
const applicability = typeof m.applicability === 'string' ? m.applicability : '';
|
|
117
|
+
const expectation = typeof m.expectation === 'string' ? m.expectation : '';
|
|
105
118
|
|
|
106
119
|
const references = Array.isArray(m.references) ? m.references.slice() : [];
|
|
107
|
-
const requirements =
|
|
120
|
+
const requirements =
|
|
121
|
+
m.requirements === null ||
|
|
122
|
+
typeof m.requirements === 'string' ||
|
|
123
|
+
typeof m.requirements === 'object'
|
|
108
124
|
? m.requirements
|
|
109
125
|
: null;
|
|
110
126
|
|
|
111
|
-
const mappings =
|
|
127
|
+
const mappings =
|
|
128
|
+
m.mappings === null || typeof m.mappings === 'string' || typeof m.mappings === 'object'
|
|
112
129
|
? m.mappings
|
|
113
130
|
: null;
|
|
114
131
|
|
|
115
|
-
if (!Array.isArray(tags)) throw new Error(`Rule ${ruleId}: meta.tags must be an array`);
|
|
116
|
-
if (!Array.isArray(normativeMappings)) throw new Error(`Rule ${ruleId}: meta.normativeMappings must be an array`);
|
|
117
|
-
if (!Array.isArray(informativeReferences)) throw new Error(`Rule ${ruleId}: meta.informativeReferences must be an array`);
|
|
118
|
-
if (type !== 'automatic' && type !== 'manual') throw new Error(`Rule ${ruleId}: meta.type must be "automatic" or "manual"`);
|
|
119
|
-
|
|
120
132
|
if (i18n) {
|
|
121
133
|
if (typeof i18n.titleKey !== 'string' || !i18n.titleKey.trim()) {
|
|
122
134
|
throw new Error(`Rule ${ruleId}: meta.i18n.titleKey must be a non-empty string`);
|
|
123
135
|
}
|
|
124
|
-
if (
|
|
125
|
-
|
|
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
|
+
);
|
|
126
143
|
}
|
|
127
144
|
}
|
|
128
145
|
|