@surea11y/core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -0
- package/LICENSE +21 -0
- package/README.md +145 -0
- package/bin/core.js +244 -0
- package/docs/BINDING_AUTHORS_GUIDE.md +41 -0
- package/docs/CLI.md +49 -0
- package/docs/ENGINE_OPTIONS.md +155 -0
- package/docs/I18N.md +47 -0
- package/docs/INTEGRATION.md +156 -0
- package/docs/LIMITATIONS.md +31 -0
- package/docs/OUTPUT_SCHEMA.md +237 -0
- package/docs/POLICY.md +71 -0
- package/docs/RULE_AUTHORING.md +375 -0
- package/docs/RULE_CATALOG.md +180 -0
- package/docs/RULE_TAXONOMY.md +145 -0
- package/docs/TROUBLESHOOTING.md +48 -0
- package/docs/WCAG_CONFORMANCE.md +49 -0
- package/package.json +60 -0
- package/src/catalogs/composites.wcag.js +490 -0
- package/src/checks/automatic/area-alt-present.js +225 -0
- package/src/checks/automatic/aria-allowed-attr.js +206 -0
- package/src/checks/automatic/aria-allowed-role.js +102 -0
- package/src/checks/automatic/aria-braille-equivalent.js +139 -0
- package/src/checks/automatic/aria-conditional-attr.js +110 -0
- package/src/checks/automatic/aria-deprecated-role.js +106 -0
- package/src/checks/automatic/aria-hidden-body.js +87 -0
- package/src/checks/automatic/aria-hidden-focus.js +480 -0
- package/src/checks/automatic/aria-prohibited-attr.js +156 -0
- package/src/checks/automatic/aria-prohibited-children.js +265 -0
- package/src/checks/automatic/aria-required-attr.js +154 -0
- package/src/checks/automatic/aria-required-children.js +274 -0
- package/src/checks/automatic/aria-required-parent.js +222 -0
- package/src/checks/automatic/aria-role-name-present.js +201 -0
- package/src/checks/automatic/aria-roles-valid.js +110 -0
- package/src/checks/automatic/aria-valid-attr-value.js +123 -0
- package/src/checks/automatic/aria-valid-attr.js +109 -0
- package/src/checks/automatic/autocomplete-valid.js +134 -0
- package/src/checks/automatic/avoid-inline-spacing.js +107 -0
- package/src/checks/automatic/binary-control-name-present.js +294 -0
- package/src/checks/automatic/button-name-present.js +146 -0
- package/src/checks/automatic/bypass-blocks-present.js +162 -0
- package/src/checks/automatic/canvas-text-alternative-present.js +140 -0
- package/src/checks/automatic/combobox-name-present.js +267 -0
- package/src/checks/automatic/contrast-computable.js +378 -0
- package/src/checks/automatic/contrast-enhanced.js +517 -0
- package/src/checks/automatic/contrast-minimum.js +512 -0
- package/src/checks/automatic/css-orientation-lock.js +206 -0
- package/src/checks/automatic/definition-list-children-valid.js +148 -0
- package/src/checks/automatic/deprecated-elements-not-used.js +91 -0
- package/src/checks/automatic/dialog-name-present.js +209 -0
- package/src/checks/automatic/dlitem-parent-valid.js +100 -0
- package/src/checks/automatic/duplicate-id-aria.js +126 -0
- package/src/checks/automatic/embed-text-alternative-present.js +190 -0
- package/src/checks/automatic/form-control-programmatic-label-present.js +409 -0
- package/src/checks/automatic/form-control-single-label.js +117 -0
- package/src/checks/automatic/html-xml-lang-mismatch.js +91 -0
- package/src/checks/automatic/iframe-focusable-content.js +141 -0
- package/src/checks/automatic/iframe-name-present.js +102 -0
- package/src/checks/automatic/iframe-title-unique.js +107 -0
- package/src/checks/automatic/img-alt-present.js +223 -0
- package/src/checks/automatic/input-image-alt-present.js +155 -0
- package/src/checks/automatic/label-in-name.js +326 -0
- package/src/checks/automatic/language-page-present.js +159 -0
- package/src/checks/automatic/link-in-text-block.js +218 -0
- package/src/checks/automatic/link-name-present.js +114 -0
- package/src/checks/automatic/list-children-valid.js +152 -0
- package/src/checks/automatic/listbox-name-present.js +236 -0
- package/src/checks/automatic/listitem-parent-valid.js +118 -0
- package/src/checks/automatic/menuitem-name-present.js +201 -0
- package/src/checks/automatic/meta-refresh-no-exceptions.js +105 -0
- package/src/checks/automatic/meta-refresh-timing-absent.js +107 -0
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +118 -0
- package/src/checks/automatic/meter-name-present.js +160 -0
- package/src/checks/automatic/nested-interactive-controls-absent.js +135 -0
- package/src/checks/automatic/object-text-alternative-present.js +193 -0
- package/src/checks/automatic/option-name-present.js +157 -0
- package/src/checks/automatic/page-title-present.js +86 -0
- package/src/checks/automatic/progressbar-name-present.js +165 -0
- package/src/checks/automatic/role-img-alt-present.js +206 -0
- package/src/checks/automatic/searchbox-name-present.js +236 -0
- package/src/checks/automatic/server-side-image-map-absent.js +88 -0
- package/src/checks/automatic/slider-name-present.js +276 -0
- package/src/checks/automatic/spinbutton-name-present.js +236 -0
- package/src/checks/automatic/summary-name-present.js +153 -0
- package/src/checks/automatic/svg-image-text-alternative-present.js +220 -0
- package/src/checks/automatic/svg-text-alternative-present.js +298 -0
- package/src/checks/automatic/tab-name-present.js +200 -0
- package/src/checks/automatic/table-headers-attr-valid.js +122 -0
- package/src/checks/automatic/table-th-has-data-cells.js +117 -0
- package/src/checks/automatic/target-size-minimum.js +605 -0
- package/src/checks/automatic/td-has-header.js +151 -0
- package/src/checks/automatic/textbox-name-present.js +236 -0
- package/src/checks/automatic/tooltip-name-present.js +158 -0
- package/src/checks/automatic/treeitem-name-present.js +157 -0
- package/src/checks/automatic/valid-lang.js +100 -0
- package/src/checks/automatic/video-poster-text-alternative-present.js +193 -0
- package/src/checks/manual/accesskeys-manual.js +93 -0
- package/src/checks/manual/area-alt-decorative-manual.js +247 -0
- package/src/checks/manual/area-alt-quality-manual.js +204 -0
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +141 -0
- package/src/checks/manual/aria-text-manual.js +109 -0
- package/src/checks/manual/canvas-text-alternative-quality-manual.js +170 -0
- package/src/checks/manual/css-hidden-focus.js +259 -0
- package/src/checks/manual/embed-text-alternative-quality-manual.js +204 -0
- package/src/checks/manual/empty-heading-manual.js +182 -0
- package/src/checks/manual/empty-table-header-manual.js +163 -0
- package/src/checks/manual/focus-order-semantics-manual.js +117 -0
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +291 -0
- package/src/checks/manual/heading-order-manual.js +130 -0
- package/src/checks/manual/identical-links-same-purpose-manual.js +142 -0
- package/src/checks/manual/image-redundant-alt-manual.js +118 -0
- package/src/checks/manual/img-alt-decorative-manual.js +148 -0
- package/src/checks/manual/img-alt-quality-manual.js +182 -0
- package/src/checks/manual/input-image-alt-decorative-manual.js +144 -0
- package/src/checks/manual/input-image-alt-quality-manual.js +144 -0
- package/src/checks/manual/label-title-only-manual.js +115 -0
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +180 -0
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +169 -0
- package/src/checks/manual/landmark-main-is-top-level-manual.js +167 -0
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +177 -0
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +169 -0
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +132 -0
- package/src/checks/manual/landmark-one-main-manual.js +151 -0
- package/src/checks/manual/landmark-unique-manual.js +252 -0
- package/src/checks/manual/link-name-quality-manual.js +143 -0
- package/src/checks/manual/media-transcript-present-manual.js +373 -0
- package/src/checks/manual/meta-viewport-large-manual.js +119 -0
- package/src/checks/manual/mouse-only-event-handlers-manual.js +134 -0
- package/src/checks/manual/no-autoplay-audio-manual.js +116 -0
- package/src/checks/manual/object-text-alternative-quality-manual.js +194 -0
- package/src/checks/manual/p-as-heading-manual.js +163 -0
- package/src/checks/manual/page-has-heading-one-manual.js +110 -0
- package/src/checks/manual/page-title-patterns-manual.js +262 -0
- package/src/checks/manual/presentation-role-conflict-manual.js +159 -0
- package/src/checks/manual/region-manual.js +183 -0
- package/src/checks/manual/scope-attr-valid-manual.js +93 -0
- package/src/checks/manual/scrollable-region-focusable-manual.js +168 -0
- package/src/checks/manual/skip-link-manual.js +150 -0
- package/src/checks/manual/svg-text-alternative-quality-manual.js +209 -0
- package/src/checks/manual/tabindex-manual.js +94 -0
- package/src/checks/manual/table-duplicate-name-manual.js +99 -0
- package/src/checks/manual/table-fake-caption-manual.js +122 -0
- package/src/checks/manual/video-caption-manual.js +118 -0
- package/src/checks/manual-review.js +95 -0
- package/src/checks/rules-and-tags.full.csv +19 -0
- package/src/checks/rules-and-tags.full.json +259 -0
- package/src/core/aria-helpers.js +906 -0
- package/src/core/contrast-helpers.js +1147 -0
- package/src/core/dom-helpers.js +4085 -0
- package/src/core/dom-runner.js +627 -0
- package/src/core/frame-messaging.js +210 -0
- package/src/core/frame-scan.js +178 -0
- package/src/core/rollup-composites.js +135 -0
- package/src/core/rule-meta.js +140 -0
- package/src/core.js +79055 -0
- package/src/coverage/wcag-facets.js +1079 -0
- package/src/coverage/wcag-version-map.js +84 -0
- package/src/i18n/en.js +919 -0
- package/src/i18n/fr.js +527 -0
- package/src/index.js +4 -0
- package/src/policy/contracts.js +18 -0
- package/src/policy/resolvePolicy.js +55 -0
- package/src/policy/schemas/engine-options.schema.json +103 -0
- package/src/policy/schemas/policy-contract.schema.json +40 -0
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* DOM runner implementation.
|
|
5
|
+
*
|
|
6
|
+
* IMPORTANT:
|
|
7
|
+
* - This function is inlined into generated core.js (Node + in-page runner).
|
|
8
|
+
* - It intentionally references shared runtime helpers that core.js defines:
|
|
9
|
+
* resolvePolicy, POLICY_CONTRACTS, resolveRuleDefI18n, ruleMatchesRunOnly,
|
|
10
|
+
* normalizeRuleResult, normalizeLocale, createDomHelpers, normalizeSelectorList,
|
|
11
|
+
* resolveContextRoots (src/core/dom-helpers.js -- also used by frame-scan.js),
|
|
12
|
+
* normalizeRuleMeta (src/core/rule-meta.js -- used for engineOptions.customRules).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
function runCore(pageUrl, contextSelector, engineOptions, runOnly, CHECK_DEFS, RULE_IMPLS, ENGINE_TAG, SCHEMA_VERSION, COMPOSITE_RULES) {
|
|
16
|
+
// Normalize contrast options without mutating caller-provided engineOptions.
|
|
17
|
+
function __normalizeContrastOptions(engineOptions2) {
|
|
18
|
+
const eo = engineOptions2 && typeof engineOptions2 === 'object' ? engineOptions2 : {};
|
|
19
|
+
const c = (eo.contrast && typeof eo.contrast === 'object') ? eo.contrast : {};
|
|
20
|
+
const mode = (c.mode === 'auditorAssist') ? 'auditorAssist' : 'strictConformance';
|
|
21
|
+
const rootCanvasFallback =
|
|
22
|
+
(typeof c.rootCanvasFallback === 'string' && c.rootCanvasFallback.trim())
|
|
23
|
+
? c.rootCanvasFallback.trim()
|
|
24
|
+
: '#ffffff';
|
|
25
|
+
return { mode, rootCanvasFallback };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const engineOptionsResolved =
|
|
29
|
+
(engineOptions && typeof engineOptions === 'object')
|
|
30
|
+
? { ...engineOptions, contrast: __normalizeContrastOptions(engineOptions) }
|
|
31
|
+
: { contrast: __normalizeContrastOptions(null) };
|
|
32
|
+
|
|
33
|
+
const policy = resolvePolicy(POLICY_CONTRACTS, engineOptionsResolved);
|
|
34
|
+
|
|
35
|
+
// contextSelector accepts a single selector string (which may itself be a
|
|
36
|
+
// comma-separated selector list -- ordinary CSS union semantics) OR an
|
|
37
|
+
// array of selector strings for scanning multiple, possibly disjoint
|
|
38
|
+
// regions in one run (a widely-used reference engine's multi-.include()
|
|
39
|
+
// capability has no equivalent here otherwise). Both forms resolve via querySelectorAll,
|
|
40
|
+
// not querySelector -- previously a single string only ever scanned its
|
|
41
|
+
// FIRST match, silently ignoring the rest if it happened to match more
|
|
42
|
+
// than one element. That was a real gap, not a deliberate "one region
|
|
43
|
+
// only" design: switched to querySelectorAll for both forms so
|
|
44
|
+
// "matches this selector" actually means all matches, consistently.
|
|
45
|
+
// Shared with frame-scan.js (same resolution used to discover which
|
|
46
|
+
// child <iframe>/<frame> elements fall within the same scan scope).
|
|
47
|
+
const { ctxSelector, roots } = resolveContextRoots(document, contextSelector);
|
|
48
|
+
|
|
49
|
+
// Default on: opt OUT with `includeShadowDom: false`, not opt in.
|
|
50
|
+
const includeShadowDom = !(engineOptionsResolved && engineOptionsResolved.includeShadowDom === false);
|
|
51
|
+
const excludeSelectors = normalizeSelectorList(engineOptionsResolved && engineOptionsResolved.excludeSelectors);
|
|
52
|
+
|
|
53
|
+
const url = pageUrl || (document.location && document.location.href) || null;
|
|
54
|
+
const title = document.title || null;
|
|
55
|
+
// Deterministic timestamp: only use host-provided value (no time-based logic).
|
|
56
|
+
const timestamp =
|
|
57
|
+
(engineOptionsResolved && typeof engineOptionsResolved.timestamp === 'string' && engineOptionsResolved.timestamp.trim())
|
|
58
|
+
? engineOptionsResolved.timestamp.trim()
|
|
59
|
+
: null;
|
|
60
|
+
|
|
61
|
+
const sharedHelpers = createDomHelpers({
|
|
62
|
+
document,
|
|
63
|
+
window,
|
|
64
|
+
root: roots,
|
|
65
|
+
includeShadowDom,
|
|
66
|
+
excludeSelectors,
|
|
67
|
+
// Optional perf counters (bench/debug only). Deterministic and per-run.
|
|
68
|
+
perfStats: !!(engineOptionsResolved && engineOptionsResolved.perfStats)
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const profileRules = !!(engineOptionsResolved && engineOptionsResolved.profileRules);
|
|
72
|
+
const ruleTimings = profileRules ? Object.create(null) : null;
|
|
73
|
+
|
|
74
|
+
function nowMs() {
|
|
75
|
+
// performance.now() if available, else Date.now()
|
|
76
|
+
try {
|
|
77
|
+
if (typeof performance !== 'undefined' && performance && typeof performance.now === 'function') {
|
|
78
|
+
return performance.now();
|
|
79
|
+
}
|
|
80
|
+
} catch (e) {}
|
|
81
|
+
return Date.now();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// =========================
|
|
85
|
+
// Probes (optional evidence fed by the host app)
|
|
86
|
+
// Keep deterministic + serializable + no-throws.
|
|
87
|
+
// =========================
|
|
88
|
+
function sanitizeProbeValue(v, depth) {
|
|
89
|
+
// depth-bounded, JSON-safe sanitizer
|
|
90
|
+
if (depth <= 0) return null;
|
|
91
|
+
if (v == null) return null;
|
|
92
|
+
|
|
93
|
+
const t = typeof v;
|
|
94
|
+
if (t === 'string') return v.length > 2000 ? v.slice(0, 2000) : v;
|
|
95
|
+
if (t === 'number') return Number.isFinite(v) ? v : null;
|
|
96
|
+
if (t === 'boolean') return v;
|
|
97
|
+
if (t === 'function') return null;
|
|
98
|
+
|
|
99
|
+
if (Array.isArray(v)) {
|
|
100
|
+
// cap arrays to avoid huge payloads
|
|
101
|
+
const out = [];
|
|
102
|
+
const n = Math.min(v.length, 200);
|
|
103
|
+
for (let i = 0; i < n; i++) out.push(sanitizeProbeValue(v[i], depth - 1));
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (t === 'object') {
|
|
108
|
+
const out = {};
|
|
109
|
+
const keys = Object.keys(v).sort();
|
|
110
|
+
// cap object keys
|
|
111
|
+
const n = Math.min(keys.length, 50);
|
|
112
|
+
for (let i = 0; i < n; i++) {
|
|
113
|
+
const k = keys[i];
|
|
114
|
+
// only allow string keys
|
|
115
|
+
if (typeof k !== 'string') continue;
|
|
116
|
+
out[k] = sanitizeProbeValue(v[k], depth - 1);
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let probes = null;
|
|
125
|
+
try {
|
|
126
|
+
const rawProbes = engineOptionsResolved && typeof engineOptionsResolved.probes === 'object' ? engineOptionsResolved.probes : null;
|
|
127
|
+
probes = rawProbes ? sanitizeProbeValue(rawProbes, 6) : null;
|
|
128
|
+
if (!probes || typeof probes !== 'object' || Array.isArray(probes)) probes = null;
|
|
129
|
+
} catch (e) {
|
|
130
|
+
probes = null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// =========================
|
|
134
|
+
// Runtime custom rules (engineOptions.customRules)
|
|
135
|
+
// =========================
|
|
136
|
+
// Same module shape as an internal rule file: { id, meta, runInPage, applicability?, data? }.
|
|
137
|
+
// runInPage/applicability may be a real function (fine for same-realm/Node/jsdom callers)
|
|
138
|
+
// or a function-source string (required for cross-realm callers, e.g. a Playwright
|
|
139
|
+
// page.evaluate(runa11yCoreInPage, { engineOptions }) call -- engineOptions crosses a
|
|
140
|
+
// structured-clone/JSON boundary there, so a live Function reference can't survive it,
|
|
141
|
+
// but a string can). Reconstructed via `new Function`, matching exactly how build-core.js
|
|
142
|
+
// already embeds each built-in rule's own runInPage source into the in-page runner.
|
|
143
|
+
// Scan-scoped only (not added to the static CHECK_DEFS/getRulesCatalog() catalog) --
|
|
144
|
+
// matches surea11y's existing "fresh engineOptions per call, no mutable global config"
|
|
145
|
+
// design (see ROADMAP.md), rather than a widely-used reference engine's stateful global configure().
|
|
146
|
+
let effectiveCheckDefs = CHECK_DEFS;
|
|
147
|
+
let effectiveRuleImpls = RULE_IMPLS;
|
|
148
|
+
let overriddenBuiltinIds = [];
|
|
149
|
+
const rawCustomRules = Array.isArray(engineOptionsResolved.customRules) ? engineOptionsResolved.customRules : [];
|
|
150
|
+
if (rawCustomRules.length) {
|
|
151
|
+
function reviveRuleFn(value) {
|
|
152
|
+
if (typeof value === 'function') return value;
|
|
153
|
+
if (typeof value === 'string' && value.trim()) {
|
|
154
|
+
try {
|
|
155
|
+
const fn = new Function('return (' + value + ')')();
|
|
156
|
+
if (typeof fn === 'function') return fn;
|
|
157
|
+
} catch (e) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const extraDefsById = new Map();
|
|
165
|
+
const extraImpls = {};
|
|
166
|
+
for (const c of rawCustomRules) {
|
|
167
|
+
if (!c || typeof c !== 'object') continue;
|
|
168
|
+
const ruleId = typeof c.id === 'string' ? c.id.trim() : '';
|
|
169
|
+
if (!ruleId) continue;
|
|
170
|
+
const runFn = reviveRuleFn(c.runInPage);
|
|
171
|
+
if (typeof runFn !== 'function') continue; // invalid custom rule: skipped, not a crash
|
|
172
|
+
|
|
173
|
+
const applicabilityFn = reviveRuleFn(c.applicability);
|
|
174
|
+
const normalizedMeta = normalizeRuleMeta(ruleId, ruleId, c.meta, ENGINE_TAG);
|
|
175
|
+
|
|
176
|
+
// Overriding a built-in rule id is supported (see docs/ENGINE_OPTIONS.md),
|
|
177
|
+
// but a same-named custom rule is just as likely to be an accidental
|
|
178
|
+
// collision (a generic name like "region" or "tabindex" picked without
|
|
179
|
+
// realizing it's already a built-in id) as a deliberate override -- so
|
|
180
|
+
// surface it either way rather than silently swapping the rule out.
|
|
181
|
+
if (CHECK_DEFS.some((d) => d && d.ruleId === ruleId)) {
|
|
182
|
+
overriddenBuiltinIds.push(ruleId);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
extraDefsById.set(ruleId, {
|
|
186
|
+
ruleId,
|
|
187
|
+
title: normalizedMeta.title,
|
|
188
|
+
description: normalizedMeta.description,
|
|
189
|
+
i18n: normalizedMeta.i18n,
|
|
190
|
+
helpUrl: normalizedMeta.helpUrl,
|
|
191
|
+
tags: normalizedMeta.tags,
|
|
192
|
+
wcagSc: normalizedMeta.wcagSc,
|
|
193
|
+
normativeMappings: normalizedMeta.normativeMappings,
|
|
194
|
+
defaultSeverity: normalizedMeta.defaultSeverity,
|
|
195
|
+
defaultConfidence: normalizedMeta.defaultConfidence,
|
|
196
|
+
type: normalizedMeta.type,
|
|
197
|
+
coverage: normalizedMeta.coverage,
|
|
198
|
+
data: (c.data === undefined ? null : c.data),
|
|
199
|
+
ruleInterfaceVersion: normalizedMeta.ruleInterfaceVersion,
|
|
200
|
+
ruleVersion: normalizedMeta.ruleVersion,
|
|
201
|
+
normative: normalizedMeta.normative,
|
|
202
|
+
atomic: normalizedMeta.atomic,
|
|
203
|
+
category: normalizedMeta.category,
|
|
204
|
+
standard: normalizedMeta.standard,
|
|
205
|
+
applicability: normalizedMeta.applicability,
|
|
206
|
+
expectation: normalizedMeta.expectation,
|
|
207
|
+
references: normalizedMeta.references,
|
|
208
|
+
requirements: normalizedMeta.requirements,
|
|
209
|
+
mappings: normalizedMeta.mappings
|
|
210
|
+
});
|
|
211
|
+
extraImpls[ruleId] = { run: runFn, applicability: applicabilityFn || null };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (extraDefsById.size) {
|
|
215
|
+
effectiveCheckDefs = CHECK_DEFS
|
|
216
|
+
.filter((d) => !extraDefsById.has(d.ruleId))
|
|
217
|
+
.concat(Array.from(extraDefsById.values()));
|
|
218
|
+
effectiveRuleImpls = { ...RULE_IMPLS, ...extraImpls };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (overriddenBuiltinIds.length) {
|
|
222
|
+
try {
|
|
223
|
+
console.warn('[surea11y] customRules overriding built-in rule id(s) for this scan: ' + overriddenBuiltinIds.join(', '));
|
|
224
|
+
} catch (e) {}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const checksResults = [];
|
|
229
|
+
|
|
230
|
+
for (const def of effectiveCheckDefs) {
|
|
231
|
+
const t0 = ruleTimings ? nowMs() : 0;
|
|
232
|
+
const defResolved = resolveRuleDefI18n(def, engineOptionsResolved);
|
|
233
|
+
if (!ruleMatchesRunOnly(defResolved, runOnly, ENGINE_TAG)) continue;
|
|
234
|
+
|
|
235
|
+
const implEntry = effectiveRuleImpls[defResolved.ruleId];
|
|
236
|
+
const impl = implEntry && typeof implEntry.run === 'function' ? implEntry.run : null;
|
|
237
|
+
const applicabilityFn = implEntry && typeof implEntry.applicability === 'function' ? implEntry.applicability : null;
|
|
238
|
+
if (typeof impl !== 'function') continue;
|
|
239
|
+
|
|
240
|
+
const ruleConfig =
|
|
241
|
+
engineOptionsResolved && engineOptionsResolved.rules && engineOptionsResolved.rules[defResolved.ruleId]
|
|
242
|
+
? engineOptionsResolved.rules[defResolved.ruleId]
|
|
243
|
+
: null;
|
|
244
|
+
|
|
245
|
+
const ctx = {
|
|
246
|
+
document,
|
|
247
|
+
window,
|
|
248
|
+
root: roots,
|
|
249
|
+
rule: defResolved,
|
|
250
|
+
config: ruleConfig,
|
|
251
|
+
helpers: sharedHelpers,
|
|
252
|
+
engineTag: ENGINE_TAG,
|
|
253
|
+
contextSelector: ctxSelector,
|
|
254
|
+
engineOptions: (engineOptionsResolved && typeof engineOptionsResolved === 'object') ? engineOptionsResolved : {},
|
|
255
|
+
|
|
256
|
+
// Optional evidence channel provided by host app
|
|
257
|
+
inputs: {
|
|
258
|
+
probes
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
if (typeof applicabilityFn === 'function') {
|
|
263
|
+
let applicable = true;
|
|
264
|
+
try {
|
|
265
|
+
const res = applicabilityFn(ctx);
|
|
266
|
+
if (typeof res === 'boolean') applicable = res;
|
|
267
|
+
else if (res && typeof res === 'object' && typeof res.applicable === 'boolean') applicable = res.applicable;
|
|
268
|
+
} catch (err) {
|
|
269
|
+
const raw = {
|
|
270
|
+
outcome: 'cantTell',
|
|
271
|
+
occurrences: [],
|
|
272
|
+
error: String(err && err.message ? err.message : err),
|
|
273
|
+
engineOptions: { ...(ctx.engineOptions || {}), locale: normalizeLocale(engineOptionsResolved && engineOptionsResolved.locale) }
|
|
274
|
+
};
|
|
275
|
+
checksResults.push(normalizeRuleResult(defResolved, raw, SCHEMA_VERSION, policy, sharedHelpers));
|
|
276
|
+
if (ruleTimings) ruleTimings[defResolved.ruleId] = (ruleTimings[defResolved.ruleId] || 0) + (nowMs() - t0);
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (!applicable) {
|
|
281
|
+
const raw = {
|
|
282
|
+
outcome: 'notApplicable',
|
|
283
|
+
occurrences: [],
|
|
284
|
+
engineOptions: { ...(ctx.engineOptions || {}), locale: normalizeLocale(engineOptionsResolved && engineOptionsResolved.locale) }
|
|
285
|
+
};
|
|
286
|
+
checksResults.push(normalizeRuleResult(defResolved, raw, SCHEMA_VERSION, policy, sharedHelpers));
|
|
287
|
+
if (ruleTimings) ruleTimings[defResolved.ruleId] = (ruleTimings[defResolved.ruleId] || 0) + (nowMs() - t0);
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
let result;
|
|
293
|
+
try {
|
|
294
|
+
result = impl(ctx);
|
|
295
|
+
} catch (err) {
|
|
296
|
+
result = {
|
|
297
|
+
outcome: 'cantTell',
|
|
298
|
+
occurrences: [],
|
|
299
|
+
error: String(err && err.message ? err.message : err),
|
|
300
|
+
engineOptions: { ...(ctx.engineOptions || {}), locale: normalizeLocale(engineOptionsResolved && engineOptionsResolved.locale) }
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (!result || typeof result !== 'object') {
|
|
305
|
+
if (ruleTimings) ruleTimings[defResolved.ruleId] = (ruleTimings[defResolved.ruleId] || 0) + (nowMs() - t0);
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
if (!result.engineOptions) {
|
|
309
|
+
result.engineOptions = { ...(ctx.engineOptions || {}), locale: normalizeLocale(engineOptionsResolved && engineOptionsResolved.locale) };
|
|
310
|
+
}
|
|
311
|
+
checksResults.push(normalizeRuleResult(defResolved, result, SCHEMA_VERSION, policy, sharedHelpers));
|
|
312
|
+
if (ruleTimings) ruleTimings[defResolved.ruleId] = (ruleTimings[defResolved.ruleId] || 0) + (nowMs() - t0);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// =========================
|
|
316
|
+
// Composite rule aggregation (data-only rollups)
|
|
317
|
+
// =========================
|
|
318
|
+
const rulesResults = [];
|
|
319
|
+
try {
|
|
320
|
+
const composites = Array.isArray(COMPOSITE_RULES) ? COMPOSITE_RULES : [];
|
|
321
|
+
|
|
322
|
+
// Determine target conformance level from runOnly.tags (already normalized by caller)
|
|
323
|
+
const LEVEL_RANK = { A: 1, AA: 2, AAA: 3 };
|
|
324
|
+
|
|
325
|
+
function inferTargetLevelFromRunOnly(runOnly2) {
|
|
326
|
+
const tags = runOnly2 && Array.isArray(runOnly2.tags) ? runOnly2.tags : [];
|
|
327
|
+
// tags are already lowercase
|
|
328
|
+
if (tags.includes('wcag2aaa') || tags.includes('wcag22aaa') || tags.includes('wcag21aaa')) return 'AAA';
|
|
329
|
+
if (tags.includes('wcag2aa') || tags.includes('wcag22aa') || tags.includes('wcag21aa')) return 'AA';
|
|
330
|
+
if (tags.includes('wcag2a') || tags.includes('wcag22a') || tags.includes('wcag21a')) return 'A';
|
|
331
|
+
return null; // if not specified, don't filter composites (back-compat)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function normalizeLevel(s) {
|
|
335
|
+
const v = typeof s === 'string' ? s.trim().toUpperCase() : '';
|
|
336
|
+
return (v === 'A' || v === 'AA' || v === 'AAA') ? v : null;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function isAllowedByTargetLevel(compositeLevel, targetLevel) {
|
|
340
|
+
if (!targetLevel) return true;
|
|
341
|
+
const c = LEVEL_RANK[compositeLevel];
|
|
342
|
+
const t = LEVEL_RANK[targetLevel];
|
|
343
|
+
if (!c || !t) return false; // unknown level => safest: exclude
|
|
344
|
+
return c <= t;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const targetLevel = inferTargetLevelFromRunOnly(runOnly);
|
|
348
|
+
|
|
349
|
+
// Severity rollup (deterministic)
|
|
350
|
+
const SEVERITY_RANK = { minor: 1, moderate: 2, serious: 3, critical: 4 };
|
|
351
|
+
|
|
352
|
+
function normalizeSeverity(s) {
|
|
353
|
+
const v = typeof s === 'string' ? s.trim().toLowerCase() : '';
|
|
354
|
+
return SEVERITY_RANK[v] ? v : null;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function maxSeverity(a, b) {
|
|
358
|
+
if (!a) return b || null;
|
|
359
|
+
if (!b) return a || null;
|
|
360
|
+
return (SEVERITY_RANK[b] > SEVERITY_RANK[a]) ? b : a;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Index atomic results by ruleId (deterministic)
|
|
364
|
+
const byRuleId = Object.create(null);
|
|
365
|
+
for (const rr of checksResults) {
|
|
366
|
+
if (rr && typeof rr === 'object' && typeof rr.ruleId === 'string' && rr.ruleId) {
|
|
367
|
+
byRuleId[rr.ruleId] = rr;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function isNonEmptyString(s) {
|
|
372
|
+
return typeof s === 'string' && !!s.trim();
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function buildCompositeDef(entry) {
|
|
376
|
+
if (!entry || typeof entry !== 'object') return null;
|
|
377
|
+
|
|
378
|
+
const ruleId = isNonEmptyString(entry.id) ? entry.id.trim() : String(entry.id || '').trim();
|
|
379
|
+
if (!ruleId) return null;
|
|
380
|
+
|
|
381
|
+
const metaIn = (entry.meta && typeof entry.meta === 'object' && !Array.isArray(entry.meta)) ? entry.meta : {};
|
|
382
|
+
|
|
383
|
+
const titleKey = (typeof metaIn.titleKey === 'string' && metaIn.titleKey.trim()) ? metaIn.titleKey.trim() : '';
|
|
384
|
+
const descriptionKey = (typeof metaIn.descriptionKey === 'string' && metaIn.descriptionKey.trim()) ? metaIn.descriptionKey.trim() : '';
|
|
385
|
+
|
|
386
|
+
const wcagSc = Array.isArray(metaIn.wcagSc) ? metaIn.wcagSc.map(String).map(s => s.trim()).filter(Boolean) : [];
|
|
387
|
+
|
|
388
|
+
const tags = [];
|
|
389
|
+
tags.push(String(ENGINE_TAG || 'a11ycore').toLowerCase());
|
|
390
|
+
tags.push('composite');
|
|
391
|
+
|
|
392
|
+
// Fixed WCAG-version-introduction lists (2.1 and 2.2 additions only -- every other
|
|
393
|
+
// SC, including all pre-2.1 ones, is WCAG 2.0 baseline). Keep in sync with
|
|
394
|
+
// src/coverage/wcag-version-map.js (the canonical copy the rule-authoring
|
|
395
|
+
// consistency test checks against) -- this one has to stay a self-contained
|
|
396
|
+
// literal since runCore is inlined via .toString() with no module access at runtime.
|
|
397
|
+
const WCAG21_NEW_SCS = ['1.3.4', '1.3.5', '1.3.6', '1.4.10', '1.4.11', '1.4.12', '1.4.13', '2.1.4', '2.2.6', '2.3.3', '2.5.1', '2.5.2', '2.5.3', '2.5.4', '2.5.5', '2.5.6', '4.1.3'];
|
|
398
|
+
const WCAG22_NEW_SCS = ['2.4.11', '2.4.12', '2.4.13', '2.5.7', '2.5.8', '3.2.6', '3.3.7', '3.3.8', '3.3.9'];
|
|
399
|
+
const isWcag22Sc = wcagSc.some((sc) => WCAG22_NEW_SCS.includes(sc));
|
|
400
|
+
const isWcag21Sc = !isWcag22Sc && wcagSc.some((sc) => WCAG21_NEW_SCS.includes(sc));
|
|
401
|
+
const versionTagPrefix = isWcag22Sc ? 'wcag22' : (isWcag21Sc ? 'wcag21' : 'wcag2');
|
|
402
|
+
|
|
403
|
+
const lvl = (typeof metaIn.level === 'string' ? metaIn.level.trim().toUpperCase() : '');
|
|
404
|
+
if (lvl === 'A') {
|
|
405
|
+
tags.push(versionTagPrefix + 'a');
|
|
406
|
+
} else if (lvl === 'AA') {
|
|
407
|
+
tags.push(versionTagPrefix + 'a', versionTagPrefix + 'aa');
|
|
408
|
+
} else if (lvl === 'AAA') {
|
|
409
|
+
tags.push(versionTagPrefix + 'a', versionTagPrefix + 'aa', versionTagPrefix + 'aaa');
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Build normativeMappings so downstream consumers (like adapters) can derive WCAG SC/level
|
|
413
|
+
const normativeMappingsFromMeta = wcagSc.map((sc) => {
|
|
414
|
+
const m = { standard: 'WCAG', requirement: sc };
|
|
415
|
+
if (lvl === 'A' || lvl === 'AA' || lvl === 'AAA') m.level = lvl;
|
|
416
|
+
return m;
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
const checksIds =
|
|
420
|
+
Array.isArray(entry.checksIds)
|
|
421
|
+
? entry.checksIds.map(String).map(s => s.trim()).filter(Boolean)
|
|
422
|
+
: [];
|
|
423
|
+
|
|
424
|
+
return {
|
|
425
|
+
ruleId,
|
|
426
|
+
title: metaIn.title || ruleId,
|
|
427
|
+
description: metaIn.description || '',
|
|
428
|
+
|
|
429
|
+
i18n: (titleKey || descriptionKey) ? { titleKey: titleKey || '', descriptionKey: descriptionKey || '' } : null,
|
|
430
|
+
|
|
431
|
+
helpUrl: '',
|
|
432
|
+
tags,
|
|
433
|
+
|
|
434
|
+
normativeMappings: normativeMappingsFromMeta,
|
|
435
|
+
|
|
436
|
+
defaultSeverity: 'serious',
|
|
437
|
+
defaultConfidence: 'medium',
|
|
438
|
+
type: 'automatic',
|
|
439
|
+
coverage: null,
|
|
440
|
+
|
|
441
|
+
ruleInterfaceVersion: '1.0.0',
|
|
442
|
+
ruleVersion: '0.0.0',
|
|
443
|
+
normative: true,
|
|
444
|
+
atomic: false,
|
|
445
|
+
category: null,
|
|
446
|
+
standard: null,
|
|
447
|
+
applicability: '',
|
|
448
|
+
expectation: '',
|
|
449
|
+
references: [],
|
|
450
|
+
requirements: null,
|
|
451
|
+
mappings: null,
|
|
452
|
+
|
|
453
|
+
// optional catalog meta passthrough
|
|
454
|
+
data: {
|
|
455
|
+
details: {
|
|
456
|
+
kind: 'compositeRule',
|
|
457
|
+
wcagSc,
|
|
458
|
+
level: (typeof metaIn.level === 'string' && metaIn.level.trim()) ? metaIn.level.trim() : null
|
|
459
|
+
}
|
|
460
|
+
},
|
|
461
|
+
|
|
462
|
+
__checksIds: checksIds
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
for (let i = 0; i < composites.length; i++) {
|
|
467
|
+
const entry = composites[i];
|
|
468
|
+
const cDef0 = buildCompositeDef(entry);
|
|
469
|
+
if (!cDef0) continue;
|
|
470
|
+
|
|
471
|
+
// Conformance-level gate: if scan is AA, suppress AAA composites even if tags match
|
|
472
|
+
const compositeLevel =
|
|
473
|
+
cDef0 &&
|
|
474
|
+
cDef0.data &&
|
|
475
|
+
cDef0.data.details &&
|
|
476
|
+
normalizeLevel(cDef0.data.details.level);
|
|
477
|
+
|
|
478
|
+
if (!isAllowedByTargetLevel(compositeLevel, targetLevel)) continue;
|
|
479
|
+
|
|
480
|
+
// Localize title/description (uses def.i18n.* keys)
|
|
481
|
+
const cDefResolved = resolveRuleDefI18n(cDef0, engineOptionsResolved);
|
|
482
|
+
|
|
483
|
+
// Apply same selection logic to composites
|
|
484
|
+
if (!ruleMatchesRunOnly(cDefResolved, runOnly, ENGINE_TAG)) continue;
|
|
485
|
+
|
|
486
|
+
const checksIds = Array.isArray(cDef0.__checksIds) ? cDef0.__checksIds : [];
|
|
487
|
+
|
|
488
|
+
// rollup metrics (stable order)
|
|
489
|
+
let failCount = 0;
|
|
490
|
+
let cantTellCount = 0;
|
|
491
|
+
let notApplicableCount = 0;
|
|
492
|
+
let passCount = 0;
|
|
493
|
+
let missingCount = 0;
|
|
494
|
+
|
|
495
|
+
const contributors = [];
|
|
496
|
+
|
|
497
|
+
let rolledFailSeverity = null; // max severity among FAIL contributors
|
|
498
|
+
let rolledCantTellSeverity = null; // max severity among CANTTELL contributors (optional)
|
|
499
|
+
|
|
500
|
+
for (let j = 0; j < checksIds.length; j++) {
|
|
501
|
+
const tid = checksIds[j];
|
|
502
|
+
const child = tid ? byRuleId[tid] : null;
|
|
503
|
+
|
|
504
|
+
if (!child) {
|
|
505
|
+
missingCount += 1;
|
|
506
|
+
contributors.push({ testId: tid, outcome: 'missing' });
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const out = child.outcome;
|
|
511
|
+
const childSev = normalizeSeverity(child && child.severity);
|
|
512
|
+
|
|
513
|
+
contributors.push({ testId: tid, outcome: out, severity: childSev || null });
|
|
514
|
+
|
|
515
|
+
if (out === 'fail' && childSev) {
|
|
516
|
+
rolledFailSeverity = maxSeverity(rolledFailSeverity, childSev);
|
|
517
|
+
} else if (out === 'cantTell' && childSev) {
|
|
518
|
+
rolledCantTellSeverity = maxSeverity(rolledCantTellSeverity, childSev);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (out === 'fail') failCount += 1;
|
|
522
|
+
else if (out === 'cantTell') cantTellCount += 1;
|
|
523
|
+
else if (out === 'notApplicable') notApplicableCount += 1;
|
|
524
|
+
else if (out === 'pass') passCount += 1;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// outcome precedence:
|
|
528
|
+
// fail if any fail
|
|
529
|
+
// cantTell if any cantTell OR missing and none fail
|
|
530
|
+
// notApplicable if all notApplicable (and there is at least one test)
|
|
531
|
+
// pass otherwise
|
|
532
|
+
let outcome = 'pass';
|
|
533
|
+
let reasonCode = 'composite.rollup.pass.otherwise';
|
|
534
|
+
|
|
535
|
+
if (failCount > 0) {
|
|
536
|
+
outcome = 'fail';
|
|
537
|
+
reasonCode = 'composite.rollup.fail.anyFail';
|
|
538
|
+
} else if (cantTellCount > 0) {
|
|
539
|
+
outcome = 'cantTell';
|
|
540
|
+
reasonCode = 'composite.rollup.cantTell.anyCantTell';
|
|
541
|
+
} else if (missingCount > 0) {
|
|
542
|
+
outcome = 'cantTell';
|
|
543
|
+
reasonCode = 'composite.rollup.cantTell.missingChild';
|
|
544
|
+
} else if (checksIds.length > 0 && notApplicableCount === checksIds.length) {
|
|
545
|
+
outcome = 'notApplicable';
|
|
546
|
+
reasonCode = 'composite.rollup.notApplicable.allInapplicable';
|
|
547
|
+
} else if (checksIds.length === 0) {
|
|
548
|
+
outcome = 'cantTell';
|
|
549
|
+
reasonCode = 'composite.rollup.cantTell.emptyComposite';
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const raw = {
|
|
553
|
+
outcome,
|
|
554
|
+
occurrences: [],
|
|
555
|
+
|
|
556
|
+
// REQUIRED by your reporting schema (top-level)
|
|
557
|
+
summaryKey: 'Composite rule rollup',
|
|
558
|
+
i18nKey: 'composite_rollup_summary',
|
|
559
|
+
i18nParams: { reasonCode, testCount: String(checksIds.length) },
|
|
560
|
+
|
|
561
|
+
// REQUIRED by your reporting schema (machine-readable payload)
|
|
562
|
+
data: {
|
|
563
|
+
details: {
|
|
564
|
+
reasonCode,
|
|
565
|
+
checksIds: checksIds.slice(),
|
|
566
|
+
contributors,
|
|
567
|
+
metrics: {
|
|
568
|
+
failCount,
|
|
569
|
+
cantTellCount,
|
|
570
|
+
notApplicableCount,
|
|
571
|
+
passCount,
|
|
572
|
+
missingCount
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
},
|
|
576
|
+
|
|
577
|
+
engineOptions: {
|
|
578
|
+
...(engineOptionsResolved || {}),
|
|
579
|
+
locale: normalizeLocale(engineOptionsResolved && engineOptionsResolved.locale)
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
// Promote composite severity based on contributors (deterministic).
|
|
584
|
+
// - If composite fails: use max severity among failing children.
|
|
585
|
+
// - If composite cantTell: use max severity among cantTell children (fallback to failing if you prefer).
|
|
586
|
+
if (outcome === 'fail' && rolledFailSeverity) {
|
|
587
|
+
raw.severity = rolledFailSeverity;
|
|
588
|
+
} else if (outcome === 'cantTell' && rolledCantTellSeverity) {
|
|
589
|
+
raw.severity = rolledCantTellSeverity;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
rulesResults.push(normalizeRuleResult(cDefResolved, raw, SCHEMA_VERSION, policy, sharedHelpers));
|
|
593
|
+
}
|
|
594
|
+
} catch (e) {
|
|
595
|
+
// no-throws: omit rulesResults if anything goes wrong
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Optional perf counters passthrough (only when enabled). Deterministic.
|
|
599
|
+
let perfStats = null;
|
|
600
|
+
try {
|
|
601
|
+
if (engineOptionsResolved && engineOptionsResolved.perfStats && sharedHelpers && typeof sharedHelpers.getPerfStats === 'function') {
|
|
602
|
+
perfStats = sharedHelpers.getPerfStats();
|
|
603
|
+
}
|
|
604
|
+
} catch (e) {
|
|
605
|
+
perfStats = null;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (ruleTimings) {
|
|
609
|
+
if (perfStats && engineOptionsResolved && engineOptionsResolved.profileRules) {
|
|
610
|
+
perfStats.ruleTimings = ruleTimings; // (whatever your timing map is)
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
return {
|
|
615
|
+
engine: { tag: ENGINE_TAG, schemaVersion: SCHEMA_VERSION },
|
|
616
|
+
url,
|
|
617
|
+
title,
|
|
618
|
+
timestamp,
|
|
619
|
+
perfStats,
|
|
620
|
+
contextSelector: ctxSelector,
|
|
621
|
+
checksResults,
|
|
622
|
+
rulesResults,
|
|
623
|
+
overriddenBuiltinIds
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
module.exports = { runCore };
|