ultra11y 3.0.0 → 4.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.opencode/plugins/ultra11y.js +1 -1
- package/README.md +3 -2
- package/package.json +1 -1
- package/scripts/build-extension-icons.mjs +11 -43
- package/scripts/ultra11y.mjs +646 -62
- package/skills/review-a11y/SKILL.md +1 -1
- package/skills/review-a11y/scripts/ultra11y.mjs +646 -62
- package/skills/ultra11y/SKILL.md +1 -1
- package/skills/ultra11y/references/ci.md +11 -9
- package/skills/ultra11y/references/dynamic.md +24 -0
- package/skills/ultra11y/references/pages.md +82 -10
- package/skills/ultra11y/scripts/ultra11y.mjs +646 -62
- package/src/annotate.ts +8 -4
- package/src/audit.ts +35 -4
- package/src/auditor.ts +0 -0
- package/src/capture.ts +13 -0
- package/src/cli.ts +342 -20
- package/src/dev.ts +17 -3
- package/src/evidence.ts +499 -0
- package/src/external/adapters/ara.ts +164 -0
- package/src/external/diff.ts +143 -0
- package/src/external/registry.ts +22 -0
- package/src/external/types.ts +59 -0
- package/src/mcp/handlers.ts +45 -6
- package/src/pages-report.ts +41 -18
- package/src/pages.ts +83 -13
- package/src/pixel.ts +174 -2
- package/src/report.ts +15 -1
- package/src/rules/cross-rule.ts +7 -0
- package/src/sample.ts +46 -0
- package/src/scan.ts +5 -0
- package/src/snapshot.ts +38 -4
- package/src/standards/pack-rules.ts +6 -0
- package/src/tickets/grain.ts +14 -2
- package/src/tickets/types.ts +5 -3
- package/src/types.ts +27 -8
- package/src/verify.ts +6 -1
package/scripts/ultra11y.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { join as join45, relative as relative5, sep as sep7, dirname as dirname1
|
|
|
6
6
|
import { fileURLToPath as fileURLToPath5, pathToFileURL as pathToFileURL3 } from "url";
|
|
7
7
|
|
|
8
8
|
// src/types.ts
|
|
9
|
-
var VERSION = "
|
|
9
|
+
var VERSION = "4.0.0";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
|
|
12
12
|
// src/audit.ts
|
|
@@ -34930,6 +34930,13 @@ function expandInputs(inputs, opts = {}) {
|
|
|
34930
34930
|
}
|
|
34931
34931
|
|
|
34932
34932
|
// src/capture.ts
|
|
34933
|
+
var CAPTURES_DIR = ".ultra11y/captures";
|
|
34934
|
+
function isUnderDir(file, dir) {
|
|
34935
|
+
const d = toPosix(dir).replace(/\/+$/, "");
|
|
34936
|
+
if (!d) return false;
|
|
34937
|
+
const f = toPosix(file);
|
|
34938
|
+
return f === d || f.startsWith(`${d}/`) || f.includes(`/${d}/`);
|
|
34939
|
+
}
|
|
34933
34940
|
function escapeCommentValue(s) {
|
|
34934
34941
|
return s.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">").replace(/--/g, "--");
|
|
34935
34942
|
}
|
|
@@ -35326,6 +35333,17 @@ function isSnapshotDom(file) {
|
|
|
35326
35333
|
const posix3 = file.split("\\").join("/");
|
|
35327
35334
|
return posix3.endsWith("/dom.html") && posix3.includes(`${PAGES_DIR}/`);
|
|
35328
35335
|
}
|
|
35336
|
+
function snapshotPageId(file) {
|
|
35337
|
+
if (!file) return void 0;
|
|
35338
|
+
const segs = file.split("\\").join("/").split("/");
|
|
35339
|
+
const marker = PAGES_DIR.split("/");
|
|
35340
|
+
if (segs.length < marker.length + 2) return void 0;
|
|
35341
|
+
if (segs[segs.length - 1] !== "dom.html") return void 0;
|
|
35342
|
+
const idAt = segs.length - 2;
|
|
35343
|
+
for (let i2 = 0; i2 < marker.length; i2++) if (segs[idAt - marker.length + i2] !== marker[i2]) return void 0;
|
|
35344
|
+
const id = segs[idAt];
|
|
35345
|
+
return id !== void 0 && ID_RE.test(id) ? id : void 0;
|
|
35346
|
+
}
|
|
35329
35347
|
function align(doc, entries) {
|
|
35330
35348
|
const out2 = /* @__PURE__ */ new Map();
|
|
35331
35349
|
for (const e of entries) {
|
|
@@ -39131,7 +39149,7 @@ var colorsRules = [contrastLiteral];
|
|
|
39131
39149
|
import { readFileSync as readFileSync12 } from "fs";
|
|
39132
39150
|
|
|
39133
39151
|
// src/pixel.ts
|
|
39134
|
-
import { inflateSync } from "zlib";
|
|
39152
|
+
import { deflateSync, inflateSync } from "zlib";
|
|
39135
39153
|
var SIGNATURE = [137, 80, 78, 71, 13, 10, 26, 10];
|
|
39136
39154
|
function paeth(a, b, c2) {
|
|
39137
39155
|
const p = a + b - c2;
|
|
@@ -39644,6 +39662,13 @@ function crossToFinding(doc, ruleId, def, cf) {
|
|
|
39644
39662
|
// regex transform), so its cross-file findings are provisional too — otherwise a cross
|
|
39645
39663
|
// finding reads as definitive while the per-doc findings in the same file are preliminary.
|
|
39646
39664
|
...doc.kind === "sfc" || doc.kind === "jsx-lossy" ? { preliminary: true } : {},
|
|
39665
|
+
// Capture provenance, exactly as src/rules/rule.ts does it. No cross rule can reach a page
|
|
39666
|
+
// snapshot today (the two that raise findings resolve through the component graph, and a
|
|
39667
|
+
// serialized DOM has no node in it), so this changes no current output — it is here so the
|
|
39668
|
+
// three Finding constructors stay in step, and a cross rule that later does become reachable
|
|
39669
|
+
// on a full document is attributed like everything else instead of silently orphaned.
|
|
39670
|
+
...doc.capture ? { origin: { capture: doc.file, sourceFile: doc.capture.sourceFile, component: doc.capture.component } } : {},
|
|
39671
|
+
...doc.capture?.page ? { page: doc.capture.page } : {},
|
|
39647
39672
|
...cf.related ? { related: cf.related } : {}
|
|
39648
39673
|
};
|
|
39649
39674
|
}
|
|
@@ -44683,6 +44708,12 @@ function toFinding2(doc, el, rule, packKey) {
|
|
|
44683
44708
|
message: { en: rule.message.en, fr: rule.message.fr },
|
|
44684
44709
|
remediation: { en: rule.remediation.en, fr: rule.remediation.fr }
|
|
44685
44710
|
},
|
|
44711
|
+
// Capture provenance, exactly as src/rules/rule.ts does it — keep the three constructors in
|
|
44712
|
+
// step. A pack finding that skips this stays unattributed, so `pageView`'s
|
|
44713
|
+
// `packFindings.filter(f => f.page === page.id)` (src/pages.ts) yields nothing and a pack rule
|
|
44714
|
+
// can be NC in the report while reaching no cell of the per-page grid.
|
|
44715
|
+
...doc.capture ? { origin: { capture: doc.file, sourceFile: doc.capture.sourceFile, component: doc.capture.component } } : {},
|
|
44716
|
+
...doc.capture?.page ? { page: doc.capture.page } : {},
|
|
44686
44717
|
...rule.advisory ? { advisory: true } : {}
|
|
44687
44718
|
};
|
|
44688
44719
|
}
|
|
@@ -45168,6 +45199,10 @@ function primarySubtag(lang) {
|
|
|
45168
45199
|
return m ? m[0].toLowerCase() : void 0;
|
|
45169
45200
|
}
|
|
45170
45201
|
function foldDoc(acc, doc, graph) {
|
|
45202
|
+
if (!doc.capture?.page) {
|
|
45203
|
+
const id = snapshotPageId(doc.file);
|
|
45204
|
+
if (id) doc.capture = { v: 1, ...doc.capture ?? {}, page: id };
|
|
45205
|
+
}
|
|
45171
45206
|
let findings = runRules(doc);
|
|
45172
45207
|
if (graph) {
|
|
45173
45208
|
const cross = runCrossRules(doc, graph);
|
|
@@ -45264,7 +45299,11 @@ function finalize(acc, inputs, extra = {}) {
|
|
|
45264
45299
|
components: [...new Set(acc.captures.map((c2) => c2.provenance.component).filter((x) => !!x))].sort()
|
|
45265
45300
|
}
|
|
45266
45301
|
} : {},
|
|
45267
|
-
...acc.langCounts.size ? { langs: [...acc.langCounts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([lang]) => lang) } : {}
|
|
45302
|
+
...acc.langCounts.size ? { langs: [...acc.langCounts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([lang]) => lang) } : {},
|
|
45303
|
+
// The pages this run genuinely read. Written UNCONDITIONALLY — `[]` is the whole point,
|
|
45304
|
+
// because "this audit read no page" is exactly the claim a source-only run needs to make,
|
|
45305
|
+
// and an omit-when-empty field would say nothing precisely then. See scope.pagesAudited.
|
|
45306
|
+
pagesAudited: [...new Set(acc.captures.map((c2) => c2.provenance.page).filter((x) => !!x))].sort()
|
|
45268
45307
|
},
|
|
45269
45308
|
guidelines,
|
|
45270
45309
|
criteria,
|
|
@@ -45351,7 +45390,8 @@ function runAudit(opts) {
|
|
|
45351
45390
|
}
|
|
45352
45391
|
seen.add(h);
|
|
45353
45392
|
}
|
|
45354
|
-
const
|
|
45393
|
+
const ingested = isSnapshotDom(file) || isUnderDir(file, opts.captureDir ?? CAPTURES_DIR);
|
|
45394
|
+
const doc = reused ?? parseSource(content, file, { forceJsx: opts.forceJsx && !ingested });
|
|
45355
45395
|
attachSignals(doc);
|
|
45356
45396
|
foldDoc(acc, doc, graph);
|
|
45357
45397
|
}
|
|
@@ -45365,7 +45405,7 @@ function runAudit(opts) {
|
|
|
45365
45405
|
});
|
|
45366
45406
|
if (graph) {
|
|
45367
45407
|
enrichCaptureOrigins(result.findings, graph);
|
|
45368
|
-
if (opts.captureCoverage) result.scope.captureCoverage = computeCaptureCoverage(graph, readCaptureDir(opts.captureDir ??
|
|
45408
|
+
if (opts.captureCoverage) result.scope.captureCoverage = computeCaptureCoverage(graph, readCaptureDir(opts.captureDir ?? CAPTURES_DIR));
|
|
45369
45409
|
}
|
|
45370
45410
|
return result;
|
|
45371
45411
|
}
|
|
@@ -48626,6 +48666,27 @@ var L = {
|
|
|
48626
48666
|
}
|
|
48627
48667
|
};
|
|
48628
48668
|
var uniq = (xs) => [...new Set(xs.filter(Boolean))];
|
|
48669
|
+
function groupOccurrences(findings, collapse2) {
|
|
48670
|
+
if (!collapse2) return findings.map((f) => [f]);
|
|
48671
|
+
const groups = /* @__PURE__ */ new Map();
|
|
48672
|
+
for (const f of findings) {
|
|
48673
|
+
const key = `${f.file}\0${f.ruleId}\0${f.selectorHint}`;
|
|
48674
|
+
const g = groups.get(key);
|
|
48675
|
+
if (g) g.push(f);
|
|
48676
|
+
else groups.set(key, [f]);
|
|
48677
|
+
}
|
|
48678
|
+
return [...groups.values()];
|
|
48679
|
+
}
|
|
48680
|
+
function renderOccurrenceDetails(out2, f, lang, s, indent) {
|
|
48681
|
+
if (f.secondary?.note) out2.push(`${indent} - \u21B3 ${f.secondary.note}`);
|
|
48682
|
+
if (f.related) out2.push(indent + relatedLine(f.related, lang, { selector: true }));
|
|
48683
|
+
if (f.origin) {
|
|
48684
|
+
const comp = f.origin.component ?? f.origin.sourceFile ?? f.file;
|
|
48685
|
+
const srcFile = f.origin.sourceFile ?? f.origin.capture;
|
|
48686
|
+
const src = f.origin.sourceFile && f.origin.sourceLine !== void 0 ? `${f.origin.sourceFile}:${f.origin.sourceLine}` : srcFile;
|
|
48687
|
+
if (comp !== src) out2.push(`${indent} - _${s.captureOf(comp, src)}_`);
|
|
48688
|
+
}
|
|
48689
|
+
}
|
|
48629
48690
|
var isUrlLocation = (file) => /^https?:\/\//i.test(file);
|
|
48630
48691
|
function sampleMetaOf(f) {
|
|
48631
48692
|
const meta2 = f.sample;
|
|
@@ -48681,15 +48742,12 @@ function renderAuditorUnit(unit, standard, lang, opts = {}) {
|
|
|
48681
48742
|
out2.push(`**${s.finding} (${v.nonConformant})** : ${normative.length} ${s.occ} \u2014 ${messages.join(" ; ")}`);
|
|
48682
48743
|
if (fixes.length) out2.push(`**${s.expected} (${v.conformant})** : ${fixes.join(" ; ")}`);
|
|
48683
48744
|
out2.push(`**${s.verification}** : ${s.verify}`, "");
|
|
48684
|
-
for (const
|
|
48685
|
-
out2.push(
|
|
48686
|
-
|
|
48687
|
-
|
|
48688
|
-
|
|
48689
|
-
|
|
48690
|
-
const srcFile = f.origin.sourceFile ?? f.origin.capture;
|
|
48691
|
-
const src = f.origin.sourceFile && f.origin.sourceLine !== void 0 ? `${f.origin.sourceFile}:${f.origin.sourceLine}` : srcFile;
|
|
48692
|
-
out2.push(` - _${s.captureOf(comp, src)}_`);
|
|
48745
|
+
for (const group of groupOccurrences(normative, opts.collapse === true)) {
|
|
48746
|
+
if (group.length > 1) out2.push(`- **\`${group[0].selectorHint}\`** \u2014 ${resolveMessage(group[0], lang)} \xB7 \xD7${group.length}`);
|
|
48747
|
+
for (const f of group) {
|
|
48748
|
+
const indent = group.length > 1 ? " " : "";
|
|
48749
|
+
out2.push(indent + occurrenceLine(f, lang, { marker: "checkbox" }));
|
|
48750
|
+
renderOccurrenceDetails(out2, f, lang, s, indent);
|
|
48693
48751
|
}
|
|
48694
48752
|
}
|
|
48695
48753
|
out2.push("");
|
|
@@ -49164,10 +49222,12 @@ function pageScopesFromSample(sample) {
|
|
|
49164
49222
|
}
|
|
49165
49223
|
function pagesOf(result) {
|
|
49166
49224
|
const fromScope = result.scope.pages ?? [];
|
|
49167
|
-
const
|
|
49168
|
-
const
|
|
49225
|
+
const audited = result.scope.pagesAudited;
|
|
49226
|
+
const checked = audited === void 0 ? fromScope : fromScope.map((p) => p.basis === "snapshot" && !audited.includes(p.id) ? { ...p, basis: "not-audited" } : p);
|
|
49227
|
+
const ids = new Set(checked.map((p) => p.id));
|
|
49228
|
+
const urls = new Set(checked.map((p) => p.url));
|
|
49169
49229
|
const extra = pageScopesFromSample(result.scope.sample).filter((p) => !ids.has(p.id) && !urls.has(p.url));
|
|
49170
|
-
return [...
|
|
49230
|
+
return [...checked, ...extra];
|
|
49171
49231
|
}
|
|
49172
49232
|
function pathMatch2(a, b) {
|
|
49173
49233
|
const x = a.split("\\").join("/");
|
|
@@ -49178,8 +49238,14 @@ function attributePages(result, pages) {
|
|
|
49178
49238
|
if (!pages.length) return;
|
|
49179
49239
|
const byName = new Map(pages.map((p) => [p.name.toLowerCase(), p.id]));
|
|
49180
49240
|
const byUrl = new Map(pages.map((p) => [p.url, p.id]));
|
|
49241
|
+
const byId2 = new Set(pages.map((p) => p.id));
|
|
49181
49242
|
for (const f of [...result.findings, ...result.packFindings ?? []]) {
|
|
49182
49243
|
if (f.page) continue;
|
|
49244
|
+
const snapId = snapshotPageId(f.file) ?? snapshotPageId(f.origin?.capture);
|
|
49245
|
+
if (snapId) {
|
|
49246
|
+
if (byId2.has(snapId)) f.page = snapId;
|
|
49247
|
+
continue;
|
|
49248
|
+
}
|
|
49183
49249
|
if (isUrlPath(f.file)) {
|
|
49184
49250
|
const hit = byUrl.get(f.file);
|
|
49185
49251
|
if (hit) f.page = hit;
|
|
@@ -49200,7 +49266,7 @@ function attributePages(result, pages) {
|
|
|
49200
49266
|
}
|
|
49201
49267
|
}
|
|
49202
49268
|
function unattributedFindings(result) {
|
|
49203
|
-
return result.findings.filter((f) => !f.page);
|
|
49269
|
+
return [...result.findings, ...result.packFindings ?? []].filter((f) => !f.page);
|
|
49204
49270
|
}
|
|
49205
49271
|
function pageStatus(c2, pageFindings, basis) {
|
|
49206
49272
|
if (pageFindings.some((f) => !f.advisory)) return "NC";
|
|
@@ -49212,7 +49278,8 @@ function pageStatus(c2, pageFindings, basis) {
|
|
|
49212
49278
|
function pct(criteria) {
|
|
49213
49279
|
const c2 = criteria.filter((x) => x.status === "C").length;
|
|
49214
49280
|
const nc = criteria.filter((x) => x.status === "NC").length;
|
|
49215
|
-
|
|
49281
|
+
const decided = c2 + nc;
|
|
49282
|
+
return { rate: decided === 0 ? null : Math.round(c2 / decided * 100), decided, total: criteria.length };
|
|
49216
49283
|
}
|
|
49217
49284
|
function derivePages(result, pages) {
|
|
49218
49285
|
if (!pages.length) return [];
|
|
@@ -49230,6 +49297,7 @@ function derivePages(result, pages) {
|
|
|
49230
49297
|
...c2.decidedBy ? { decidedBy: c2.decidedBy } : {}
|
|
49231
49298
|
};
|
|
49232
49299
|
});
|
|
49300
|
+
const { rate, decided, total } = pct(criteria);
|
|
49233
49301
|
out2.push({
|
|
49234
49302
|
id: p.id,
|
|
49235
49303
|
name: p.name,
|
|
@@ -49238,7 +49306,9 @@ function derivePages(result, pages) {
|
|
|
49238
49306
|
basis: p.basis,
|
|
49239
49307
|
criteria,
|
|
49240
49308
|
findings: own,
|
|
49241
|
-
conformancePct:
|
|
49309
|
+
conformancePct: rate,
|
|
49310
|
+
decided,
|
|
49311
|
+
total
|
|
49242
49312
|
});
|
|
49243
49313
|
}
|
|
49244
49314
|
return out2;
|
|
@@ -49255,7 +49325,9 @@ var L3 = {
|
|
|
49255
49325
|
unattributed: (n) => `${n} constat(s) non rattach\xE9(s) \xE0 une page (code partag\xE9, fichier hors routes) \u2014 compt\xE9s dans l'audit global, jamais r\xE9partis d'office.`,
|
|
49256
49326
|
rate: "Taux",
|
|
49257
49327
|
snapshot: "instantan\xE9",
|
|
49258
|
-
source: "source"
|
|
49328
|
+
source: "source",
|
|
49329
|
+
notAudited: "non audit\xE9",
|
|
49330
|
+
notAuditedNote: "Une page marqu\xE9e \xAB non audit\xE9 \xBB a bien un instantan\xE9, mais CET audit ne l'a pas lu (il ne portait que sur les sources). L'absence de constat n'y vaut donc PAS conformit\xE9 \u2014 relancez l'audit en incluant `.ultra11y/pages`."
|
|
49259
49331
|
},
|
|
49260
49332
|
en: {
|
|
49261
49333
|
title: "Per-page grid",
|
|
@@ -49267,11 +49339,21 @@ var L3 = {
|
|
|
49267
49339
|
unattributed: (n) => `${n} unattributed finding(s) (shared code, file outside any route) \u2014 counted in the overall audit, never spread across pages.`,
|
|
49268
49340
|
rate: "Rate",
|
|
49269
49341
|
snapshot: "snapshot",
|
|
49270
|
-
source: "source"
|
|
49342
|
+
source: "source",
|
|
49343
|
+
notAudited: "not audited",
|
|
49344
|
+
notAuditedNote: 'A page marked "not audited" does have a snapshot, but THIS audit never read it (it covered sources only). Absence of a finding there does NOT mean conforming \u2014 re-run the audit with `.ultra11y/pages` in scope.'
|
|
49271
49345
|
}
|
|
49272
49346
|
};
|
|
49347
|
+
function formatRate(rate, decided, total) {
|
|
49348
|
+
return `${rate === null ? "\u2014" : `${rate} %`} (${decided}/${total})`;
|
|
49349
|
+
}
|
|
49273
49350
|
function pageBasisWarning(basis, lang) {
|
|
49274
|
-
|
|
49351
|
+
if (basis === "snapshot") return void 0;
|
|
49352
|
+
return basis === "not-audited" ? L3[lang].notAuditedNote : L3[lang].basisNote;
|
|
49353
|
+
}
|
|
49354
|
+
function basisLabel(basis, lang) {
|
|
49355
|
+
const s = L3[lang];
|
|
49356
|
+
return basis === "snapshot" ? s.snapshot : basis === "not-audited" ? s.notAudited : s.source;
|
|
49275
49357
|
}
|
|
49276
49358
|
function unattributedNote(n, lang) {
|
|
49277
49359
|
return L3[lang].unattributed(n);
|
|
@@ -49314,8 +49396,8 @@ function renderPageGrid(result, pages, standard = CORE2, lang = "en") {
|
|
|
49314
49396
|
if (derived.some((p) => p.basis === "attributed")) out2.push(`> \u26A0\uFE0F ${s.basisNote}`, "");
|
|
49315
49397
|
const head = [isCore(standard) ? s.criterion : s.criterion, ...derived.map((p) => `${p.name}${p.auth ? " \u{1F512}" : ""}`)];
|
|
49316
49398
|
out2.push(`| ${head.join(" | ")} |`, `| ${head.map(() => "---").join(" | ")} |`);
|
|
49317
|
-
out2.push(`| **${s.rate}** | ${derived.map((p) => `**${p.conformancePct}
|
|
49318
|
-
out2.push(`| _${s.snapshot}?_ | ${derived.map((p) =>
|
|
49399
|
+
out2.push(`| **${s.rate}** | ${derived.map((p) => `**${formatRate(p.conformancePct, p.decided, p.total)}**`).join(" | ")} |`);
|
|
49400
|
+
out2.push(`| _${s.snapshot}?_ | ${derived.map((p) => `_${basisLabel(p.basis, lang)}_`).join(" | ")} |`);
|
|
49319
49401
|
const { rows, status } = gridOf(result, derived, standard, lang);
|
|
49320
49402
|
let group = "";
|
|
49321
49403
|
for (const row of rows) {
|
|
@@ -49335,6 +49417,7 @@ function renderPageGrid(result, pages, standard = CORE2, lang = "en") {
|
|
|
49335
49417
|
// src/report.ts
|
|
49336
49418
|
var ICON3 = { bloquant: "\u{1F534}", majeur: "\u{1F7E0}", mineur: "\u{1F7E1}" };
|
|
49337
49419
|
var SEV_ORDER3 = ["bloquant", "majeur", "mineur"];
|
|
49420
|
+
var PER_PAGE_MAX = 30;
|
|
49338
49421
|
var L4 = {
|
|
49339
49422
|
fr: {
|
|
49340
49423
|
title: (std) => `Rapport d'audit d'accessibilit\xE9 \u2014 ${std}`,
|
|
@@ -49388,6 +49471,7 @@ var L4 = {
|
|
|
49388
49471
|
authYes: "\u{1F512} authentification requise",
|
|
49389
49472
|
authNo: "\u{1F310} public",
|
|
49390
49473
|
ncCount: "non-conformit\xE9(s)",
|
|
49474
|
+
perPageMore: (hidden, total) => `\u2702\uFE0F ${hidden} autre(s) constat(s) sur cette page ne sont pas list\xE9s ici (${total} au total) \u2014 voir la fiche de page.`,
|
|
49391
49475
|
advCount: "recommandation(s)",
|
|
49392
49476
|
screenshotAlt: (n) => `Capture d'\xE9cran de la page ${n}`
|
|
49393
49477
|
},
|
|
@@ -49443,6 +49527,7 @@ var L4 = {
|
|
|
49443
49527
|
authYes: "\u{1F512} authentication required",
|
|
49444
49528
|
authNo: "\u{1F310} public",
|
|
49445
49529
|
ncCount: "non-conformity(ies)",
|
|
49530
|
+
perPageMore: (hidden, total) => `\u2702\uFE0F ${hidden} further finding(s) on this page are not listed here (${total} in total) \u2014 see its page sheet.`,
|
|
49446
49531
|
advCount: "recommendation(s)",
|
|
49447
49532
|
screenshotAlt: (n) => `Screenshot of the ${n} page`
|
|
49448
49533
|
}
|
|
@@ -49544,11 +49629,12 @@ function render(r, lang, opts) {
|
|
|
49544
49629
|
}
|
|
49545
49630
|
out2.push("", ``);
|
|
49546
49631
|
}
|
|
49547
|
-
for (const f of nc.slice(0,
|
|
49632
|
+
for (const f of nc.slice(0, PER_PAGE_MAX)) {
|
|
49548
49633
|
const crits = pack ? packCriteriaForFinding(pack, f) : [];
|
|
49549
49634
|
const label = crits.length ? crits.join(", ") : f.criteriaId;
|
|
49550
49635
|
out2.push(` - [${label}] \`${f.selectorHint}\` \u2014 ${resolveMessage(f, lang)}`);
|
|
49551
49636
|
}
|
|
49637
|
+
if (nc.length > PER_PAGE_MAX) out2.push(` - _${s.perPageMore(nc.length - PER_PAGE_MAX, nc.length)}_`);
|
|
49552
49638
|
out2.push("");
|
|
49553
49639
|
}
|
|
49554
49640
|
}
|
|
@@ -49938,13 +50024,19 @@ function buildTickets(result, opts) {
|
|
|
49938
50024
|
}
|
|
49939
50025
|
}
|
|
49940
50026
|
if (orphans.length) {
|
|
49941
|
-
const
|
|
50027
|
+
const coreOrphans = result.findings.filter((f) => !f.page);
|
|
50028
|
+
const view = {
|
|
50029
|
+
...result,
|
|
50030
|
+
findings: coreOrphans,
|
|
50031
|
+
...result.packFindings ? { packFindings: result.packFindings.filter((f) => !f.page) } : {}
|
|
50032
|
+
};
|
|
49942
50033
|
const units = prdUnits(view, standard, lang);
|
|
49943
50034
|
if (units.length) {
|
|
49944
50035
|
const advisory = units.every((u) => u.advisory === true);
|
|
50036
|
+
const announced = isCore(standard) ? coreOrphans.length : orphans.length;
|
|
49945
50037
|
tickets.push({
|
|
49946
50038
|
title: unattributedTitle(label),
|
|
49947
|
-
body: clamp2([`> ${unattributedNote(
|
|
50039
|
+
body: clamp2([`> ${unattributedNote(announced, lang)}`, "", renderAuditorBacklog(view, lang, standard, backlogOpts)].join("\n")),
|
|
49948
50040
|
labels: labelsFor(worstOf(units), advisory, tag),
|
|
49949
50041
|
severity: worstOf(units),
|
|
49950
50042
|
advisory,
|
|
@@ -50870,7 +50962,7 @@ function auditorCriterionLine(standard) {
|
|
|
50870
50962
|
const id = isCore(standard) ? "\\d{1,2}(?:\\.\\d{1,2}){2}" : idCaptureSource(loadPack(standard));
|
|
50871
50963
|
return new RegExp(`^\\*\\*[^*:]+\\*\\*\\s*:\\s*(${id})(?:\\s*\u2014.*)?\\s*$`);
|
|
50872
50964
|
}
|
|
50873
|
-
var AUDITOR_OCCURRENCE =
|
|
50965
|
+
var AUDITOR_OCCURRENCE = /^\s*-\s\[ \]\s+`([^`]+):(\d+)`\s+\(`([^`]*)`\)\s+—\s+(.*)$/;
|
|
50874
50966
|
var HEADING_LINE = /^#{2,4}\s/;
|
|
50875
50967
|
function buildWorklistFromAuditorBlocks(reportMd, standard, max) {
|
|
50876
50968
|
const items = [];
|
|
@@ -51136,6 +51228,10 @@ var L7 = {
|
|
|
51136
51228
|
producer: "Producteur",
|
|
51137
51229
|
auth: "Authentification requise",
|
|
51138
51230
|
rate: "Taux de r\xE9ussite automatique (v\xE9rifications statiques)",
|
|
51231
|
+
// The index has its own, shorter header — the sheet's label is a full sentence and would
|
|
51232
|
+
// wreck an eight-column table. Kept as a SEPARATE key: editing `rate` in place would
|
|
51233
|
+
// silently reword the sheet bullet too.
|
|
51234
|
+
rateShort: "Taux (crit\xE8res d\xE9cid\xE9s)",
|
|
51139
51235
|
rateNote: "sous-ensemble d\xE9cidable : C \xF7 (C + NC)",
|
|
51140
51236
|
tally: (c2, nc, na, m) => `${c2} conforme(s) \xB7 ${nc} non conforme(s) \xB7 ${na} non applicable(s) \xB7 ${m} \xE0 \xE9valuer`,
|
|
51141
51237
|
coverage: (decided, total) => `Couverture : ${decided}/${total} crit\xE8re(s) \xE9valu\xE9(s) \u2014 le taux ci-dessus ne porte que sur eux, et ne dit rien des ${total - decided} autres.`,
|
|
@@ -51158,7 +51254,7 @@ var L7 = {
|
|
|
51158
51254
|
blocking: "Bloquant",
|
|
51159
51255
|
major: "Majeur",
|
|
51160
51256
|
minor: "Mineur",
|
|
51161
|
-
indexNote: "Une fiche par page.
|
|
51257
|
+
indexNote: "Une fiche par page. `X % (d/t)` : le taux ne porte que sur les `d` crit\xE8res d\xE9cid\xE9s sur `t` \u2014 il ne dit rien des autres. `\u2014` signifie qu'aucun crit\xE8re n'a \xE9t\xE9 d\xE9cid\xE9 sur cette page, et ne vaut donc NI conformit\xE9 NI non-conformit\xE9."
|
|
51162
51258
|
},
|
|
51163
51259
|
en: {
|
|
51164
51260
|
docTitle: "Page-by-page accessibility report",
|
|
@@ -51176,6 +51272,7 @@ var L7 = {
|
|
|
51176
51272
|
producer: "Producer",
|
|
51177
51273
|
auth: "Authentication required",
|
|
51178
51274
|
rate: "Automatic static-check pass rate",
|
|
51275
|
+
rateShort: "Rate (decided criteria)",
|
|
51179
51276
|
rateNote: "decidable subset: C \xF7 (C + NC)",
|
|
51180
51277
|
tally: (c2, nc, na, m) => `${c2} conforming \xB7 ${nc} non-conforming \xB7 ${na} not applicable \xB7 ${m} to assess`,
|
|
51181
51278
|
coverage: (decided, total) => `Coverage: ${decided}/${total} criteria assessed \u2014 the rate above covers only those, and says nothing about the other ${total - decided}.`,
|
|
@@ -51198,7 +51295,7 @@ var L7 = {
|
|
|
51198
51295
|
blocking: "Blocking",
|
|
51199
51296
|
major: "Major",
|
|
51200
51297
|
minor: "Minor",
|
|
51201
|
-
indexNote: "One sheet per page.
|
|
51298
|
+
indexNote: "One sheet per page. `X % (d/t)`: the rate covers only the `d` criteria decided out of `t` \u2014 it says nothing about the others. `\u2014` means no criterion was decided on this page, so it is NEITHER conformity NOR non-conformity."
|
|
51202
51299
|
}
|
|
51203
51300
|
};
|
|
51204
51301
|
function rowsFor(result, page, standard, lang) {
|
|
@@ -51231,9 +51328,13 @@ function tally(rows) {
|
|
|
51231
51328
|
manual: rows.filter((r) => r.status === "manual").length
|
|
51232
51329
|
};
|
|
51233
51330
|
}
|
|
51331
|
+
function coverageOf(rows) {
|
|
51332
|
+
const t2 = tally(rows);
|
|
51333
|
+
return { decided: t2.c + t2.nc, total: rows.length };
|
|
51334
|
+
}
|
|
51234
51335
|
function ratePct(rows) {
|
|
51235
51336
|
const { c: c2, nc } = tally(rows);
|
|
51236
|
-
return c2 + nc === 0 ?
|
|
51337
|
+
return c2 + nc === 0 ? null : Math.round(c2 / (c2 + nc) * 100);
|
|
51237
51338
|
}
|
|
51238
51339
|
function renderPageReport(result, page, opts = {}) {
|
|
51239
51340
|
const standard = opts.standard ?? CORE2;
|
|
@@ -51244,14 +51345,16 @@ function renderPageReport(result, page, opts = {}) {
|
|
|
51244
51345
|
out2.push(`${h} ${page.name}${page.auth ? " \u{1F512}" : ""}`, "");
|
|
51245
51346
|
const meta2 = [];
|
|
51246
51347
|
meta2.push(`- **${s.url}** : \`${page.url}\``);
|
|
51247
|
-
meta2.push(`- **${s.basis}** : ${page.basis
|
|
51348
|
+
meta2.push(`- **${s.basis}** : ${basisLabel(page.basis, lang)}`);
|
|
51248
51349
|
if (page.auth) meta2.push(`- **${s.auth}** : \u2705`);
|
|
51249
51350
|
out2.push(...meta2);
|
|
51250
51351
|
const rows = rowsFor(result, page, standard, lang);
|
|
51251
51352
|
const t2 = tally(rows);
|
|
51252
|
-
|
|
51353
|
+
const cov = coverageOf(rows);
|
|
51354
|
+
const rate = ratePct(rows);
|
|
51355
|
+
out2.push(`- **${s.rate}** : **${rate === null ? "\u2014" : `${rate} %`}** _(${s.rateNote})_`);
|
|
51253
51356
|
out2.push(`- ${s.tally(t2.c, t2.nc, t2.na, t2.manual)}`);
|
|
51254
|
-
out2.push(`- ${s.coverage(
|
|
51357
|
+
out2.push(`- ${s.coverage(cov.decided, cov.total)}`, "");
|
|
51255
51358
|
if (page.basis !== "snapshot") out2.push(`> \u26A0\uFE0F ${s.sourceWarn}`, "");
|
|
51256
51359
|
const shot = opts.screenshots?.get(page.id);
|
|
51257
51360
|
if (shot) out2.push(``, "");
|
|
@@ -51277,10 +51380,10 @@ function renderPageReport(result, page, opts = {}) {
|
|
|
51277
51380
|
const advUnits = units.filter((u) => u.advisory);
|
|
51278
51381
|
out2.push(`${h}# ${s.ncTitle}`, "");
|
|
51279
51382
|
if (!ncUnits.length) out2.push(s.noNc, "");
|
|
51280
|
-
else for (const u of ncUnits) out2.push(...renderAuditorUnit(u, standard, lang, { heading: `${h}
|
|
51383
|
+
else for (const u of ncUnits) out2.push(...renderAuditorUnit(u, standard, lang, { heading: `${h}##`, collapse: true }));
|
|
51281
51384
|
if (advUnits.length) {
|
|
51282
51385
|
out2.push(`${h}# \u{1F4A1} ${s.recTitle}`, "", `> ${s.recNote}`, "");
|
|
51283
|
-
for (const u of advUnits) out2.push(...renderAuditorUnit(u, standard, lang, { heading: `${h}
|
|
51386
|
+
for (const u of advUnits) out2.push(...renderAuditorUnit(u, standard, lang, { heading: `${h}##`, collapse: true }));
|
|
51284
51387
|
}
|
|
51285
51388
|
return out2.join("\n");
|
|
51286
51389
|
}
|
|
@@ -51303,18 +51406,19 @@ function renderPagesIndex(result, pages, opts = {}) {
|
|
|
51303
51406
|
const s = L7[lang];
|
|
51304
51407
|
const out2 = header2(result, pages, standard, lang, s.indexTitle);
|
|
51305
51408
|
out2.push(`> ${s.indexNote}`, "");
|
|
51306
|
-
out2.push(`| ${s.page} | ${s.url} | ${s.basis} | ${s.
|
|
51409
|
+
out2.push(`| ${s.page} | ${s.url} | ${s.basis} | ${s.rateShort} | ${s.blocking} | ${s.major} | ${s.minor} | ${s.sheet} |`);
|
|
51307
51410
|
out2.push("| --- | --- | --- | --- | --- | --- | --- | --- |");
|
|
51308
51411
|
for (const p of pages) {
|
|
51309
51412
|
const rows = rowsFor(result, p, standard, lang);
|
|
51310
|
-
const
|
|
51413
|
+
const cov = coverageOf(rows);
|
|
51414
|
+
const nc = [...p.findings, ...(result.packFindings ?? []).filter((f) => f.page === p.id)].filter((f) => !f.advisory);
|
|
51311
51415
|
const href = opts.hrefs?.get(p.id);
|
|
51312
51416
|
out2.push(
|
|
51313
|
-
`| ${p.name}${p.auth ? " \u{1F512}" : ""} | \`${p.url}\` | ${p.basis
|
|
51417
|
+
`| ${p.name}${p.auth ? " \u{1F512}" : ""} | \`${p.url}\` | ${basisLabel(p.basis, lang)} | ${formatRate(ratePct(rows), cov.decided, cov.total)} | ${nc.filter((f) => f.severity === "bloquant").length} | ${nc.filter((f) => f.severity === "majeur").length} | ${nc.filter((f) => f.severity === "mineur").length} | ${href ? `[${p.id}](${href})` : p.id} |`
|
|
51314
51418
|
);
|
|
51315
51419
|
}
|
|
51316
51420
|
out2.push("");
|
|
51317
|
-
const orphans = result
|
|
51421
|
+
const orphans = unattributedFindings(result);
|
|
51318
51422
|
if (orphans.length) out2.push(`> ${s.unattributed(orphans.length)}`, "");
|
|
51319
51423
|
return out2.join("\n");
|
|
51320
51424
|
}
|
|
@@ -53429,6 +53533,27 @@ function mergeSample(existing, proposed) {
|
|
|
53429
53533
|
kept: pages.length - added.length
|
|
53430
53534
|
};
|
|
53431
53535
|
}
|
|
53536
|
+
function sampleFromSnapshots(snapshots) {
|
|
53537
|
+
return snapshots.map((s) => ({
|
|
53538
|
+
id: s.meta.id,
|
|
53539
|
+
name: s.meta.name,
|
|
53540
|
+
url: s.meta.url,
|
|
53541
|
+
...s.meta.auth !== void 0 ? { auth: s.meta.auth } : {},
|
|
53542
|
+
...s.meta.notes ? { notes: s.meta.notes } : {}
|
|
53543
|
+
}));
|
|
53544
|
+
}
|
|
53545
|
+
function unionSample(declared, snapshotted) {
|
|
53546
|
+
const pages = [...declared?.pages ?? []];
|
|
53547
|
+
const declaredIds = new Set(pages.map((p) => p.id));
|
|
53548
|
+
const snapIds = new Set(snapshotted.map((p) => p.id));
|
|
53549
|
+
const undeclared = snapshotted.filter((p) => !declaredIds.has(p.id));
|
|
53550
|
+
const uncaptured = pages.filter((p) => !snapIds.has(p.id));
|
|
53551
|
+
return {
|
|
53552
|
+
sample: { pages: [...pages, ...undeclared], ...declared?.transverse?.length ? { transverse: declared.transverse } : {} },
|
|
53553
|
+
undeclared,
|
|
53554
|
+
uncaptured
|
|
53555
|
+
};
|
|
53556
|
+
}
|
|
53432
53557
|
function kindLabel(kind, locale = "fr") {
|
|
53433
53558
|
return kind.label[locale] ?? Object.values(kind.label)[0] ?? kind.id;
|
|
53434
53559
|
}
|
|
@@ -53863,6 +53988,7 @@ function recomputeTallies2(merged) {
|
|
|
53863
53988
|
}
|
|
53864
53989
|
function mergeSnapshotAudit(base, snap) {
|
|
53865
53990
|
const merged = JSON.parse(JSON.stringify(base));
|
|
53991
|
+
merged.scope.pagesAudited = [.../* @__PURE__ */ new Set([...base.scope.pagesAudited ?? [], ...snap.scope.pagesAudited ?? []])].sort();
|
|
53866
53992
|
const byId2 = new Map(merged.criteria.map((c2) => [c2.id, c2]));
|
|
53867
53993
|
const snapById = new Map(snap.criteria.map((c2) => [c2.id, c2]));
|
|
53868
53994
|
for (const f of snap.findings) {
|
|
@@ -54560,6 +54686,195 @@ async function runCrawlScanLocal(opts) {
|
|
|
54560
54686
|
return runScanManyLocal(urls, opts);
|
|
54561
54687
|
}
|
|
54562
54688
|
|
|
54689
|
+
// src/external/adapters/ara.ts
|
|
54690
|
+
var ARA_STATUS = {
|
|
54691
|
+
COMPLIANT: "C",
|
|
54692
|
+
NOT_COMPLIANT: "NC",
|
|
54693
|
+
NOT_APPLICABLE: "NA",
|
|
54694
|
+
// "the auditor did not test this criterion". `manual` is the honest mapping — undecided — and
|
|
54695
|
+
// `rawStatus` is what lets a diff say « non retesté » rather than lumping it with the rest.
|
|
54696
|
+
NOT_TESTED: "manual"
|
|
54697
|
+
};
|
|
54698
|
+
var isObj = (x) => typeof x === "object" && x !== null && !Array.isArray(x);
|
|
54699
|
+
function commentOf(r) {
|
|
54700
|
+
const items = (r.notCompliantItems ?? []).map((i2) => [i2.title, i2.comment].filter(Boolean).join(" \u2014 ")).filter(Boolean);
|
|
54701
|
+
const parts2 = [r.compliantComment, r.notApplicableComment, ...items].filter((x) => typeof x === "string" && x.trim() !== "");
|
|
54702
|
+
return parts2.length ? parts2.join("\n\n") : void 0;
|
|
54703
|
+
}
|
|
54704
|
+
function impactOf2(r) {
|
|
54705
|
+
const order = ["BLOCKING", "MAJOR", "MINOR"];
|
|
54706
|
+
const found = (r.notCompliantItems ?? []).map((i2) => i2.userImpact).filter((x) => typeof x === "string");
|
|
54707
|
+
for (const level of order) if (found.includes(level)) return level;
|
|
54708
|
+
return void 0;
|
|
54709
|
+
}
|
|
54710
|
+
var araAdapter = {
|
|
54711
|
+
id: "ara",
|
|
54712
|
+
fetchUrl(id) {
|
|
54713
|
+
return `https://ara.numerique.gouv.fr/api/reports/${encodeURIComponent(id)}`;
|
|
54714
|
+
},
|
|
54715
|
+
parse(raw, opts) {
|
|
54716
|
+
const issues = [];
|
|
54717
|
+
if (!isObj(raw)) return { ok: false, issues: ["the report is not a JSON object"] };
|
|
54718
|
+
const context = isObj(raw.context) ? raw.context : {};
|
|
54719
|
+
const samples = Array.isArray(context.samples) ? context.samples : [];
|
|
54720
|
+
if (!samples.length) issues.push("context.samples is empty \u2014 the report declares no page, so no result can be located");
|
|
54721
|
+
const seen = /* @__PURE__ */ new Set();
|
|
54722
|
+
const pageById = /* @__PURE__ */ new Map();
|
|
54723
|
+
for (const s of samples) {
|
|
54724
|
+
if (typeof s?.id !== "number") {
|
|
54725
|
+
issues.push(`a page in context.samples has no numeric id: ${JSON.stringify(s)}`);
|
|
54726
|
+
continue;
|
|
54727
|
+
}
|
|
54728
|
+
const name2 = typeof s.name === "string" && s.name.trim() ? s.name : `page-${s.id}`;
|
|
54729
|
+
const url = typeof s.url === "string" ? s.url : "";
|
|
54730
|
+
let id = slugifyPageId(name2) || `page-${s.id}`;
|
|
54731
|
+
for (let n = 2; seen.has(id); n++) id = `${slugifyPageId(name2)}-${n}`;
|
|
54732
|
+
seen.add(id);
|
|
54733
|
+
pageById.set(s.id, { id, name: name2, url });
|
|
54734
|
+
}
|
|
54735
|
+
const results = [];
|
|
54736
|
+
const rawResults = Array.isArray(raw.results) ? raw.results : [];
|
|
54737
|
+
if (!rawResults.length) issues.push("the report carries no `results` array");
|
|
54738
|
+
const known = new Set(loadPack("rgaa").criteria.map((c2) => c2.id));
|
|
54739
|
+
for (const r of rawResults) {
|
|
54740
|
+
if (!isObj(r)) {
|
|
54741
|
+
issues.push(`a result is not an object: ${JSON.stringify(r)}`);
|
|
54742
|
+
continue;
|
|
54743
|
+
}
|
|
54744
|
+
const criterion = `${r.topic}.${r.criterium}`;
|
|
54745
|
+
const page = pageById.get(r.pageId);
|
|
54746
|
+
if (!page) {
|
|
54747
|
+
issues.push(`result ${criterion} names pageId ${r.pageId}, which context.samples never declares`);
|
|
54748
|
+
continue;
|
|
54749
|
+
}
|
|
54750
|
+
if (!known.has(criterion)) {
|
|
54751
|
+
issues.push(`result names criterion ${criterion}, which the RGAA pack does not define`);
|
|
54752
|
+
continue;
|
|
54753
|
+
}
|
|
54754
|
+
const status = ARA_STATUS[r.status];
|
|
54755
|
+
if (status === void 0) {
|
|
54756
|
+
issues.push(`result ${criterion} on ${page.id} carries status "${r.status}" \u2014 expected one of ${Object.keys(ARA_STATUS).join(", ")}`);
|
|
54757
|
+
continue;
|
|
54758
|
+
}
|
|
54759
|
+
const comment = commentOf(r);
|
|
54760
|
+
const userImpact = impactOf2(r);
|
|
54761
|
+
results.push({
|
|
54762
|
+
page: page.id,
|
|
54763
|
+
criterion,
|
|
54764
|
+
status,
|
|
54765
|
+
rawStatus: r.status,
|
|
54766
|
+
...comment ? { comment } : {},
|
|
54767
|
+
...userImpact ? { userImpact } : {}
|
|
54768
|
+
});
|
|
54769
|
+
}
|
|
54770
|
+
if (issues.length) return { ok: false, issues };
|
|
54771
|
+
const date = typeof raw.publishDate === "string" ? raw.publishDate.slice(0, 10) : typeof raw.creationDate === "string" ? raw.creationDate.slice(0, 10) : void 0;
|
|
54772
|
+
const audit2 = {
|
|
54773
|
+
tool: "ultra11y",
|
|
54774
|
+
kind: "external-audit",
|
|
54775
|
+
schemaVersion: 1,
|
|
54776
|
+
source: {
|
|
54777
|
+
adapter: "ara",
|
|
54778
|
+
...typeof raw.consultUniqueId === "string" ? { id: raw.consultUniqueId } : {},
|
|
54779
|
+
...opts.url ? { url: opts.url } : {},
|
|
54780
|
+
importedAt: opts.importedAt
|
|
54781
|
+
},
|
|
54782
|
+
standard: "rgaa",
|
|
54783
|
+
...date ? { date } : {},
|
|
54784
|
+
...typeof raw.procedureName === "string" ? { procedure: raw.procedureName } : {},
|
|
54785
|
+
...typeof context.auditorName === "string" ? { auditor: context.auditorName } : {},
|
|
54786
|
+
pages: [...pageById.values()],
|
|
54787
|
+
results
|
|
54788
|
+
};
|
|
54789
|
+
return { ok: true, audit: audit2 };
|
|
54790
|
+
}
|
|
54791
|
+
};
|
|
54792
|
+
|
|
54793
|
+
// src/external/registry.ts
|
|
54794
|
+
var EXTERNAL_SOURCES = ["ara"];
|
|
54795
|
+
function createAdapter(id) {
|
|
54796
|
+
switch (id) {
|
|
54797
|
+
case "ara":
|
|
54798
|
+
return araAdapter;
|
|
54799
|
+
default:
|
|
54800
|
+
throw new Error(`unknown external audit source "${id}" \u2014 expected one of: ${EXTERNAL_SOURCES.join(", ")}`);
|
|
54801
|
+
}
|
|
54802
|
+
}
|
|
54803
|
+
|
|
54804
|
+
// src/external/diff.ts
|
|
54805
|
+
function sideOfExternal(a) {
|
|
54806
|
+
const byPage = /* @__PURE__ */ new Map();
|
|
54807
|
+
const raw = /* @__PURE__ */ new Map();
|
|
54808
|
+
const comments = /* @__PURE__ */ new Map();
|
|
54809
|
+
for (const r of a.results) {
|
|
54810
|
+
const put = (m, v) => {
|
|
54811
|
+
const inner = m.get(r.page) ?? /* @__PURE__ */ new Map();
|
|
54812
|
+
inner.set(r.criterion, v);
|
|
54813
|
+
m.set(r.page, inner);
|
|
54814
|
+
};
|
|
54815
|
+
put(byPage, r.status);
|
|
54816
|
+
put(raw, r.rawStatus);
|
|
54817
|
+
if (r.comment) put(comments, r.comment);
|
|
54818
|
+
}
|
|
54819
|
+
return { byPage, raw, comments };
|
|
54820
|
+
}
|
|
54821
|
+
var ruled = (s) => s !== void 0 && s !== "manual";
|
|
54822
|
+
function bucketOf(left, right) {
|
|
54823
|
+
if (left === null && right === null) return "unchanged";
|
|
54824
|
+
if (right === null) {
|
|
54825
|
+
return left === "NC" ? "not-retested" : "only-left";
|
|
54826
|
+
}
|
|
54827
|
+
if (left === null) return "only-right";
|
|
54828
|
+
if (left === right) return "unchanged";
|
|
54829
|
+
if (left === "NC") return right === "C" ? "fixed" : "partially-fixed";
|
|
54830
|
+
return right === "NC" ? "regressed" : "unchanged";
|
|
54831
|
+
}
|
|
54832
|
+
var EMPTY_COUNTS = () => ({
|
|
54833
|
+
fixed: 0,
|
|
54834
|
+
unchanged: 0,
|
|
54835
|
+
"partially-fixed": 0,
|
|
54836
|
+
regressed: 0,
|
|
54837
|
+
"not-retested": 0,
|
|
54838
|
+
"only-left": 0,
|
|
54839
|
+
"only-right": 0
|
|
54840
|
+
});
|
|
54841
|
+
function diffSides(left, right) {
|
|
54842
|
+
const pages = [.../* @__PURE__ */ new Set([...left.byPage.keys(), ...right.byPage.keys()])].sort();
|
|
54843
|
+
const rows = [];
|
|
54844
|
+
const counts = EMPTY_COUNTS();
|
|
54845
|
+
for (const page of pages) {
|
|
54846
|
+
const l = left.byPage.get(page);
|
|
54847
|
+
const r = right.byPage.get(page);
|
|
54848
|
+
const criteria = [.../* @__PURE__ */ new Set([...l?.keys() ?? [], ...r?.keys() ?? []])].sort((a, b) => a.localeCompare(b, "en", { numeric: true }));
|
|
54849
|
+
for (const criterion of criteria) {
|
|
54850
|
+
const lv = l?.get(criterion);
|
|
54851
|
+
const rv = r?.get(criterion);
|
|
54852
|
+
const leftStatus = ruled(lv) ? lv : null;
|
|
54853
|
+
const rightStatus = ruled(rv) ? rv : null;
|
|
54854
|
+
const rightRaw = right.raw?.get(page)?.get(criterion);
|
|
54855
|
+
const bucket = bucketOf(leftStatus, rightStatus);
|
|
54856
|
+
if (leftStatus === null && rightStatus === null) continue;
|
|
54857
|
+
counts[bucket]++;
|
|
54858
|
+
rows.push({
|
|
54859
|
+
page,
|
|
54860
|
+
criterion,
|
|
54861
|
+
left: leftStatus,
|
|
54862
|
+
right: rightStatus,
|
|
54863
|
+
...left.raw?.get(page)?.get(criterion) ? { leftRaw: left.raw.get(page).get(criterion) } : {},
|
|
54864
|
+
...rightRaw ? { rightRaw } : {},
|
|
54865
|
+
bucket,
|
|
54866
|
+
...right.comments?.get(page)?.get(criterion) ? { comment: right.comments.get(page).get(criterion) } : {}
|
|
54867
|
+
});
|
|
54868
|
+
}
|
|
54869
|
+
}
|
|
54870
|
+
return {
|
|
54871
|
+
rows,
|
|
54872
|
+
counts,
|
|
54873
|
+
pagesOnlyLeft: [...left.byPage.keys()].filter((p) => !right.byPage.has(p)).sort(),
|
|
54874
|
+
pagesOnlyRight: [...right.byPage.keys()].filter((p) => !left.byPage.has(p)).sort()
|
|
54875
|
+
};
|
|
54876
|
+
}
|
|
54877
|
+
|
|
54563
54878
|
// src/fix.ts
|
|
54564
54879
|
import { writeFileSync as writeFileSync12 } from "fs";
|
|
54565
54880
|
|
|
@@ -55859,13 +56174,15 @@ function perPageTable(result, standard = CORE2, lang = "en") {
|
|
|
55859
56174
|
const nc = pg.findings.filter((f) => !f.advisory);
|
|
55860
56175
|
const n = (sev) => nc.filter((f) => f.severity === sev).length;
|
|
55861
56176
|
out2.push(
|
|
55862
|
-
`| ${pg.name}${pg.auth ? " \u{1F512}" : ""} \u2014 \`${pg.url}\` | ${pg.basis
|
|
56177
|
+
`| ${pg.name}${pg.auth ? " \u{1F512}" : ""} \u2014 \`${pg.url}\` | ${basisLabel(pg.basis, lang)} | ${formatRate(pg.conformancePct, pg.decided, pg.total)} | ${n("bloquant")} | ${n("majeur")} | ${n("mineur")} |`
|
|
55863
56178
|
);
|
|
55864
56179
|
}
|
|
55865
56180
|
out2.push("");
|
|
55866
|
-
const orphans = result.
|
|
56181
|
+
const orphans = unattributedFindings(result).filter((f) => !f.advisory).length;
|
|
55867
56182
|
if (orphans) out2.push(`> ${s.unattributed(orphans)}`, "");
|
|
55868
|
-
if (derived.some((p) => p.basis
|
|
56183
|
+
if (derived.some((p) => p.basis === "attributed")) out2.push(`> ${s.sourceBasis}`, "");
|
|
56184
|
+
const notAudited = pageBasisWarning("not-audited", lang);
|
|
56185
|
+
if (notAudited && derived.some((p) => p.basis === "not-audited")) out2.push(`> ${notAudited}`, "");
|
|
55869
56186
|
return out2.join("\n");
|
|
55870
56187
|
}
|
|
55871
56188
|
|
|
@@ -56098,7 +56415,9 @@ function dashboardHtml(result, pages, standard, lang) {
|
|
|
56098
56415
|
out2.push("<table><thead><tr><th></th>");
|
|
56099
56416
|
for (const p of pages) out2.push(`<th>${esc2(p.name)}${p.auth ? " \u{1F512}" : ""}<br><code>${esc2(p.url)}</code></th>`);
|
|
56100
56417
|
out2.push("</tr></thead><tbody>");
|
|
56101
|
-
out2.push(
|
|
56418
|
+
out2.push(
|
|
56419
|
+
`<tr><td>${fr ? "Taux" : "Rate"}</td>${pages.map((p) => `<td class="rate">${esc2(formatRate(p.conformancePct, p.decided, p.total))}</td>`).join("")}</tr>`
|
|
56420
|
+
);
|
|
56102
56421
|
let group = "";
|
|
56103
56422
|
for (const row of rows) {
|
|
56104
56423
|
if (row.group !== group) {
|
|
@@ -56154,6 +56473,11 @@ function projectPages(root) {
|
|
|
56154
56473
|
foldRecordedAdjudication(root, result);
|
|
56155
56474
|
return { result, pages: derivePages(result, scope) };
|
|
56156
56475
|
}
|
|
56476
|
+
function judgePages(result) {
|
|
56477
|
+
const scope = pagesOf(result);
|
|
56478
|
+
attributePages(result, scope);
|
|
56479
|
+
return derivePages(result, scope).map((p) => ({ id: p.id, name: p.name, rate: p.conformancePct, decided: p.decided, total: p.total }));
|
|
56480
|
+
}
|
|
56157
56481
|
function foldRecordedAdjudication(root, fresh) {
|
|
56158
56482
|
let prior;
|
|
56159
56483
|
try {
|
|
@@ -56285,7 +56609,7 @@ function startDevServer(opts) {
|
|
|
56285
56609
|
...auditPath ? { auditPath } : {},
|
|
56286
56610
|
issues: applied.ok ? [] : applied.issues.slice(0, 20),
|
|
56287
56611
|
failures,
|
|
56288
|
-
pages: applied.ok ?
|
|
56612
|
+
pages: applied.ok ? judgePages(applied.audit) : []
|
|
56289
56613
|
})
|
|
56290
56614
|
);
|
|
56291
56615
|
} catch (e) {
|
|
@@ -56938,7 +57262,6 @@ function noop() {
|
|
|
56938
57262
|
}
|
|
56939
57263
|
|
|
56940
57264
|
// src/mcp/handlers.ts
|
|
56941
|
-
var CAPTURES_DIR = ".ultra11y/captures";
|
|
56942
57265
|
var ToolError = class extends Error {
|
|
56943
57266
|
};
|
|
56944
57267
|
var MAX_READ_LINES = 2e3;
|
|
@@ -57010,7 +57333,7 @@ async function dispatch(name2, args2, cwd) {
|
|
|
57010
57333
|
case "ultra11y_pack_check":
|
|
57011
57334
|
return handlePackCheck(args2, cwd);
|
|
57012
57335
|
case "ultra11y_sample_check":
|
|
57013
|
-
return handleSampleCheck(cwd);
|
|
57336
|
+
return handleSampleCheck(cwd, standardOf(args2));
|
|
57014
57337
|
case "ultra11y_pages":
|
|
57015
57338
|
return handlePages(args2, cwd);
|
|
57016
57339
|
case "ultra11y_read":
|
|
@@ -57146,7 +57469,7 @@ function handlePackCheck(args2, cwd) {
|
|
|
57146
57469
|
const res = runPackCheck(pack, str2(args2.guidance));
|
|
57147
57470
|
return { cwd, ...res };
|
|
57148
57471
|
}
|
|
57149
|
-
function handleSampleCheck(cwd) {
|
|
57472
|
+
function handleSampleCheck(cwd, standard) {
|
|
57150
57473
|
const file = join43(cwd, ".ultra11yrc.json");
|
|
57151
57474
|
if (!existsSync29(file)) {
|
|
57152
57475
|
throw new ToolError(`no .ultra11yrc.json at ${cwd} \u2014 a page sample must be declared before it can be linted.`);
|
|
@@ -57157,7 +57480,26 @@ function handleSampleCheck(cwd) {
|
|
|
57157
57480
|
} catch (e) {
|
|
57158
57481
|
throw new ToolError(`.ultra11yrc.json is not valid JSON: ${e.message}`);
|
|
57159
57482
|
}
|
|
57160
|
-
|
|
57483
|
+
const v = validateSample(raw?.sample);
|
|
57484
|
+
if (!v.ok || !v.sample) {
|
|
57485
|
+
return { cwd, ok: false, issues: v.issues, next: "Fix the sample block before linting it." };
|
|
57486
|
+
}
|
|
57487
|
+
const snapshotted = sampleFromSnapshots(readSnapshots(cwd));
|
|
57488
|
+
const union = unionSample(v.sample, snapshotted);
|
|
57489
|
+
const methodology = isCore(standard) ? void 0 : getPack(standard)?.sampleMethodology;
|
|
57490
|
+
const missing = methodology ? lintSample(union.sample, methodology).missing : [];
|
|
57491
|
+
return {
|
|
57492
|
+
cwd,
|
|
57493
|
+
standard,
|
|
57494
|
+
ok: true,
|
|
57495
|
+
declared: v.sample.pages.length,
|
|
57496
|
+
snapshotted: snapshotted.length,
|
|
57497
|
+
undeclared: union.undeclared.map((p) => ({ id: p.id, url: p.url })),
|
|
57498
|
+
uncaptured: union.uncaptured.map((p) => ({ id: p.id, url: p.url })),
|
|
57499
|
+
missing: missing.map((k) => ({ id: k.id, label: kindLabel(k, "fr") })),
|
|
57500
|
+
warnings: v.warnings,
|
|
57501
|
+
next: union.undeclared.length > 0 ? "Snapshotted pages are missing from the declared sample: the declared list is not the audited surface. `pages discover --from-snapshots --write` folds them in." : "The sample covers the required page kinds over the union of what is declared and what was captured."
|
|
57502
|
+
};
|
|
57161
57503
|
}
|
|
57162
57504
|
function handlePages(args2, cwd) {
|
|
57163
57505
|
const standard = standardOf(args2);
|
|
@@ -57178,7 +57520,19 @@ function handlePages(args2, cwd) {
|
|
|
57178
57520
|
standard,
|
|
57179
57521
|
lang,
|
|
57180
57522
|
markdown,
|
|
57181
|
-
|
|
57523
|
+
// `conformancePct` is null when this page decided nothing — a machine consumer must be able
|
|
57524
|
+
// to tell "no criterion was assessed" from "every assessed criterion passed", which a bare
|
|
57525
|
+
// 100 conflates. `decided`/`total` are the denominator that makes the number quotable.
|
|
57526
|
+
pages: derived.map((p) => ({
|
|
57527
|
+
id: p.id,
|
|
57528
|
+
name: p.name,
|
|
57529
|
+
url: p.url,
|
|
57530
|
+
basis: p.basis,
|
|
57531
|
+
conformancePct: p.conformancePct,
|
|
57532
|
+
decided: p.decided,
|
|
57533
|
+
total: p.total,
|
|
57534
|
+
findings: p.findings.length
|
|
57535
|
+
})),
|
|
57182
57536
|
unattributed: unattributedFindings(r).length,
|
|
57183
57537
|
next: 'A page whose basis is "attributed" has no snapshot: absence of a finding there is NOT conformity. Its undecided criteria stay yours to adjudicate.'
|
|
57184
57538
|
};
|
|
@@ -58303,7 +58657,9 @@ Usage:
|
|
|
58303
58657
|
ultra11y snapshot list [--root <dir>] [--json]
|
|
58304
58658
|
ultra11y pages --in <audit.json> [--standard <pack>] [--json] [--lang auto|en|fr] (the per-page criterion grid)
|
|
58305
58659
|
ultra11y pages --in <audit.json> --format report [--split page] [--out <dir>] (the per-page report, with screenshots)
|
|
58306
|
-
ultra11y pages discover --sitemap <url> | --crawl <url> [--depth <n>] [--max <n>] [--write] [--json] (build the page sample)
|
|
58660
|
+
ultra11y pages discover --sitemap <url> | --crawl <url> | --from-snapshots [--depth <n>] [--max <n>] [--write] [--json] (build the page sample)
|
|
58661
|
+
ultra11y pages --in <audit.json> --standard <pack> --diff <external.json> (hold the grid against an audit someone else performed)
|
|
58662
|
+
ultra11y import --from file <report.json> | --from ara <id> [--source <adapter>] [--out <dir>] [--json]
|
|
58307
58663
|
ultra11y mcp [--transport stdio|http] [--cwd <dir>] [--allow-write] [--port <n>] [--bind <addr>] [--allow-remote] [--allow-origin <o>] [--max-response-bytes <n>]
|
|
58308
58664
|
ultra11y hook --claude-code|--codex|--opencode (internal: the PreToolUse hook; payload on stdin)
|
|
58309
58665
|
ultra11y install --claude-code | --codex | --opencode | --agents-md | --all [--project] [--dry-run] [--no-skills]
|
|
@@ -58591,6 +58947,13 @@ Options:
|
|
|
58591
58947
|
assertion. Unauthenticated scans keep clicks on regardless; the
|
|
58592
58948
|
destructive-name skip above applies in every case
|
|
58593
58949
|
--write pages discover: merge the discovered pages into .ultra11yrc.json
|
|
58950
|
+
--from-snapshots pages discover: build the sample from .ultra11y/pages instead of crawling \u2014
|
|
58951
|
+
the only route that sees a state-reached page (a modal, a funnel step)
|
|
58952
|
+
--from <src> import: "file" (a report on disk, the primary route) or an adapter id ("ara",
|
|
58953
|
+
which fetches it and writes the raw response beside the parsed one)
|
|
58954
|
+
--source <adapter> import --from file: which adapter reads it (default: ara)
|
|
58955
|
+
--diff <file> pages: compare the grid with an imported external audit \u2014 five buckets
|
|
58956
|
+
(fixed \xB7 unchanged \xB7 partially fixed \xB7 regressed \xB7 not retested)
|
|
58594
58957
|
(sample.pages). Without it the proposal is printed and nothing is
|
|
58595
58958
|
written. Pages already declared are kept verbatim \u2014 auth, storageState
|
|
58596
58959
|
and notes are human work and are never overwritten
|
|
@@ -58637,6 +59000,7 @@ var COMMANDS = [
|
|
|
58637
59000
|
"sample",
|
|
58638
59001
|
"snapshot",
|
|
58639
59002
|
"pages",
|
|
59003
|
+
"import",
|
|
58640
59004
|
"dev",
|
|
58641
59005
|
"mcp",
|
|
58642
59006
|
"fix",
|
|
@@ -58672,6 +59036,11 @@ var VALUE_FLAGS2 = /* @__PURE__ */ new Set([
|
|
|
58672
59036
|
"crawl",
|
|
58673
59037
|
"depth",
|
|
58674
59038
|
"max",
|
|
59039
|
+
// `import`: which external audit tool the report comes from, and (with `--from file`) which
|
|
59040
|
+
// adapter should read it. `pages --diff`: the imported audit to hold the grid against.
|
|
59041
|
+
"from",
|
|
59042
|
+
"source",
|
|
59043
|
+
"diff",
|
|
58675
59044
|
"since",
|
|
58676
59045
|
"max-files",
|
|
58677
59046
|
"dedup",
|
|
@@ -58728,6 +59097,9 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
|
|
|
58728
59097
|
"next",
|
|
58729
59098
|
"coverage",
|
|
58730
59099
|
"write",
|
|
59100
|
+
// `pages discover`: build the sample from the snapshots the E2E producer already wrote,
|
|
59101
|
+
// instead of crawling — the only route that can see a state-reached page.
|
|
59102
|
+
"from-snapshots",
|
|
58731
59103
|
"dry-run",
|
|
58732
59104
|
"iterate",
|
|
58733
59105
|
"safe",
|
|
@@ -58948,7 +59320,7 @@ async function cmdAudit(p) {
|
|
|
58948
59320
|
let lang = resolveLang(p.flags, {});
|
|
58949
59321
|
const requireCaptures = p.flags["require-captures"] === true;
|
|
58950
59322
|
const capturesFlag = typeof p.flags.captures === "string" && p.flags.captures ? p.flags.captures : void 0;
|
|
58951
|
-
const capturesDir = capturesFlag ??
|
|
59323
|
+
const capturesDir = capturesFlag ?? CAPTURES_DIR;
|
|
58952
59324
|
const scopedToDiff = p.flags.changed === true || p.flags.staged === true || since !== void 0;
|
|
58953
59325
|
const capturesWanted = p.flags["no-captures"] !== true && !inputs.includes("-") && (capturesFlag !== void 0 || existsSync31(capturesDir));
|
|
58954
59326
|
const useCaptures = capturesWanted && !scopedToDiff && !inputs.includes(capturesDir);
|
|
@@ -59118,9 +59490,20 @@ async function cmdPagesDiscover(p) {
|
|
|
59118
59490
|
const lang = resolveLang(p.flags, {});
|
|
59119
59491
|
const sitemap = typeof p.flags.sitemap === "string" ? p.flags.sitemap : void 0;
|
|
59120
59492
|
const crawl = typeof p.flags.crawl === "string" ? p.flags.crawl : void 0;
|
|
59493
|
+
if (p.flags["from-snapshots"] === true) {
|
|
59494
|
+
const root = typeof p.flags.root === "string" && p.flags.root ? p.flags.root : ".";
|
|
59495
|
+
const snaps = sampleFromSnapshots(readSnapshots(root));
|
|
59496
|
+
if (!snaps.length) {
|
|
59497
|
+
console.error(
|
|
59498
|
+
lang === "fr" ? `ultra11y pages discover : aucun instantan\xE9 sous ${join45(root, PAGES_DIR)} \u2014 lancez d'abord vos tests E2E avec checkA11y, ou \`scan --sample\`.` : `ultra11y pages discover: no snapshot under ${join45(root, PAGES_DIR)} \u2014 run your E2E tests with checkA11y first, or \`scan --sample\`.`
|
|
59499
|
+
);
|
|
59500
|
+
return 1;
|
|
59501
|
+
}
|
|
59502
|
+
return writeDiscoveredSample(p, lang, snaps, snaps.length);
|
|
59503
|
+
}
|
|
59121
59504
|
if (!sitemap && !crawl) {
|
|
59122
59505
|
console.error(
|
|
59123
|
-
lang === "fr" ? "ultra11y pages discover : passez --sitemap <url
|
|
59506
|
+
lang === "fr" ? "ultra11y pages discover : passez --sitemap <url>, --crawl <url> ou --from-snapshots." : "ultra11y pages discover: pass --sitemap <url>, --crawl <url> or --from-snapshots."
|
|
59124
59507
|
);
|
|
59125
59508
|
return 2;
|
|
59126
59509
|
}
|
|
@@ -59163,7 +59546,9 @@ async function cmdPagesDiscover(p) {
|
|
|
59163
59546
|
}
|
|
59164
59547
|
})
|
|
59165
59548
|
);
|
|
59166
|
-
|
|
59549
|
+
return writeDiscoveredSample(p, lang, proposeSamplePages(urls, titles), urls.length);
|
|
59550
|
+
}
|
|
59551
|
+
function writeDiscoveredSample(p, lang, proposed, found) {
|
|
59167
59552
|
let existing;
|
|
59168
59553
|
try {
|
|
59169
59554
|
existing = loadConfig(process.cwd())?.sample;
|
|
@@ -59184,8 +59569,8 @@ async function cmdPagesDiscover(p) {
|
|
|
59184
59569
|
console.log(JSON.stringify({ sample: v.sample }, null, 2));
|
|
59185
59570
|
console.log(
|
|
59186
59571
|
lang === "fr" ? `
|
|
59187
|
-
${
|
|
59188
|
-
${
|
|
59572
|
+
${found} page(s) d\xE9couverte(s), ${merged.added.length} nouvelle(s). Rien n'a \xE9t\xE9 \xE9crit \u2014 relancez avec --write pour fusionner dans .ultra11yrc.json.` : `
|
|
59573
|
+
${found} page(s) discovered, ${merged.added.length} new. Nothing was written \u2014 re-run with --write to merge into .ultra11yrc.json.`
|
|
59189
59574
|
);
|
|
59190
59575
|
}
|
|
59191
59576
|
return 0;
|
|
@@ -59242,6 +59627,9 @@ async function cmdPages(p) {
|
|
|
59242
59627
|
return 1;
|
|
59243
59628
|
}
|
|
59244
59629
|
attributePages(result, scope);
|
|
59630
|
+
if (typeof p.flags.diff === "string" && p.flags.diff) {
|
|
59631
|
+
return diffAgainstExternal(p, result, scope, standard, lang, p.flags.diff);
|
|
59632
|
+
}
|
|
59245
59633
|
if (p.flags.json) {
|
|
59246
59634
|
console.log(JSON.stringify({ pages: derivePages(result, scope), unattributed: unattributedFindings(result).length }, null, 2));
|
|
59247
59635
|
return 0;
|
|
@@ -60568,29 +60956,223 @@ function cmdSample(p) {
|
|
|
60568
60956
|
);
|
|
60569
60957
|
return 0;
|
|
60570
60958
|
}
|
|
60571
|
-
const
|
|
60959
|
+
const root = typeof p.flags.root === "string" && p.flags.root ? p.flags.root : ".";
|
|
60960
|
+
const snapshotted = sampleFromSnapshots(readSnapshots(root));
|
|
60961
|
+
const union = unionSample(v.sample, snapshotted);
|
|
60962
|
+
const { missing } = lintSample(union.sample, methodology);
|
|
60572
60963
|
const loc = pack?.defaultLocale ?? "fr";
|
|
60964
|
+
const counts = {
|
|
60965
|
+
declared: v.sample.pages.length,
|
|
60966
|
+
snapshotted: snapshotted.length,
|
|
60967
|
+
undeclared: union.undeclared.length,
|
|
60968
|
+
uncaptured: union.uncaptured.length
|
|
60969
|
+
};
|
|
60573
60970
|
if (p.flags.json) {
|
|
60574
60971
|
console.log(
|
|
60575
60972
|
JSON.stringify(
|
|
60576
|
-
{
|
|
60973
|
+
{
|
|
60974
|
+
ok: true,
|
|
60975
|
+
pages: union.sample.pages.length,
|
|
60976
|
+
...counts,
|
|
60977
|
+
undeclaredPages: union.undeclared.map((x) => ({ id: x.id, url: x.url })),
|
|
60978
|
+
uncapturedPages: union.uncaptured.map((x) => ({ id: x.id, url: x.url })),
|
|
60979
|
+
missing: missing.map((k) => ({ id: k.id, label: kindLabel(k, loc) })),
|
|
60980
|
+
warnings: v.warnings
|
|
60981
|
+
},
|
|
60577
60982
|
null,
|
|
60578
60983
|
2
|
|
60579
60984
|
)
|
|
60580
60985
|
);
|
|
60581
60986
|
return 0;
|
|
60582
60987
|
}
|
|
60583
|
-
|
|
60988
|
+
console.log(
|
|
60989
|
+
lang === "fr" ? `${counts.declared} d\xE9clar\xE9e(s) \xB7 ${counts.snapshotted} instantan\xE9e(s) \xB7 ${counts.undeclared} instantan\xE9e(s) non d\xE9clar\xE9e(s) \xB7 ${counts.uncaptured} d\xE9clar\xE9e(s) jamais captur\xE9e(s)` : `${counts.declared} declared \xB7 ${counts.snapshotted} snapshotted \xB7 ${counts.undeclared} snapshotted but undeclared \xB7 ${counts.uncaptured} declared but never captured`
|
|
60990
|
+
);
|
|
60991
|
+
if (missing.length) {
|
|
60992
|
+
console.log(
|
|
60993
|
+
(lang === "fr" ? `\u26A0\uFE0F \xC9chantillon incomplet (${union.sample.pages.length} page(s)) \u2014 types de page requis absents (${pack.name}) :` : `\u26A0\uFE0F Incomplete sample (${union.sample.pages.length} page(s)) \u2014 required page kinds missing (${pack.name}):`) + ` ${missing.map((k) => kindLabel(k, loc)).join(", ")}`
|
|
60994
|
+
);
|
|
60995
|
+
} else if (counts.undeclared > 0) {
|
|
60584
60996
|
console.log(
|
|
60585
|
-
lang === "fr" ? `\
|
|
60997
|
+
lang === "fr" ? `\u26A0\uFE0F Types de page requis couverts, mais l'\xE9chantillon d\xE9clar\xE9 n'est pas la surface audit\xE9e : ${counts.undeclared} page(s) instantan\xE9e(s) n'y figurent pas (${union.undeclared.slice(0, 5).map((x) => x.id).join(", ")}${counts.undeclared > 5 ? ", \u2026" : ""}). D\xE9clarez-les avec \`pages discover --from-snapshots --write\`.` : `\u26A0\uFE0F Required page kinds covered, but the declared sample is not the audited surface: ${counts.undeclared} snapshotted page(s) are absent from it (${union.undeclared.slice(0, 5).map((x) => x.id).join(", ")}${counts.undeclared > 5 ? ", \u2026" : ""}). Declare them with \`pages discover --from-snapshots --write\`.`
|
|
60586
60998
|
);
|
|
60587
60999
|
} else {
|
|
60588
61000
|
console.log(
|
|
60589
|
-
|
|
61001
|
+
lang === "fr" ? `\u2713 \xC9chantillon complet (${union.sample.pages.length} page(s)) \u2014 tous les types de page requis par ${pack.name} sont couverts.` : `\u2713 Sample complete (${union.sample.pages.length} page(s)) \u2014 every page kind ${pack.name} requires is covered.`
|
|
61002
|
+
);
|
|
61003
|
+
}
|
|
61004
|
+
if (counts.uncaptured > 0) {
|
|
61005
|
+
console.log(
|
|
61006
|
+
lang === "fr" ? `\u2139\uFE0F ${counts.uncaptured} page(s) d\xE9clar\xE9e(s) n'ont aucun instantan\xE9 \u2014 le rapport par page ne pourra rien conclure de leur silence.` : `\u2139\uFE0F ${counts.uncaptured} declared page(s) have no snapshot \u2014 the per-page report can conclude nothing from their silence.`
|
|
60590
61007
|
);
|
|
60591
61008
|
}
|
|
60592
61009
|
return 0;
|
|
60593
61010
|
}
|
|
61011
|
+
var DIFF_LABEL = {
|
|
61012
|
+
fixed: { fr: "Corrig\xE9", en: "Fixed" },
|
|
61013
|
+
unchanged: { fr: "Inchang\xE9", en: "Unchanged" },
|
|
61014
|
+
"partially-fixed": { fr: "Partiellement corrig\xE9", en: "Partially fixed" },
|
|
61015
|
+
regressed: { fr: "R\xE9gress\xE9", en: "Regressed" },
|
|
61016
|
+
"not-retested": { fr: "Non retest\xE9", en: "Not retested" },
|
|
61017
|
+
"only-left": { fr: "Nous seuls", en: "Ours only" },
|
|
61018
|
+
"only-right": { fr: "Eux seuls", en: "Theirs only" }
|
|
61019
|
+
};
|
|
61020
|
+
function diffAgainstExternal(p, result, scope, standard, lang, file) {
|
|
61021
|
+
let ext2;
|
|
61022
|
+
try {
|
|
61023
|
+
ext2 = JSON.parse(readText(file));
|
|
61024
|
+
} catch (e) {
|
|
61025
|
+
console.error(`ultra11y pages --diff: ${e instanceof Error ? e.message : String(e)}`);
|
|
61026
|
+
return 1;
|
|
61027
|
+
}
|
|
61028
|
+
if (ext2?.kind !== "external-audit") {
|
|
61029
|
+
console.error(`ultra11y pages --diff: ${file} is not an imported external audit (run \`ultra11y import\` first).`);
|
|
61030
|
+
return 2;
|
|
61031
|
+
}
|
|
61032
|
+
if (isCore(standard)) {
|
|
61033
|
+
console.error("ultra11y pages --diff: pass --standard <pack> \u2014 an external audit is keyed by the standard's own criterion ids, not by WCAG SCs.");
|
|
61034
|
+
return 2;
|
|
61035
|
+
}
|
|
61036
|
+
if (ext2.standard !== standard) {
|
|
61037
|
+
console.error(`ultra11y pages --diff: the imported audit is keyed by "${ext2.standard}" but --standard is "${standard}".`);
|
|
61038
|
+
return 2;
|
|
61039
|
+
}
|
|
61040
|
+
const ours = /* @__PURE__ */ new Map();
|
|
61041
|
+
for (const page of derivePages(result, scope)) {
|
|
61042
|
+
const m = /* @__PURE__ */ new Map();
|
|
61043
|
+
for (const c2 of derivePackResults(pageView(result, page), standard)) m.set(c2.id, c2.status);
|
|
61044
|
+
ours.set(page.id, m);
|
|
61045
|
+
}
|
|
61046
|
+
const d = diffSides({ byPage: ours }, sideOfExternal(ext2));
|
|
61047
|
+
if (p.flags.json) {
|
|
61048
|
+
console.log(JSON.stringify({ standard, external: ext2.source, ...d }, null, 2));
|
|
61049
|
+
return 0;
|
|
61050
|
+
}
|
|
61051
|
+
const fr = lang === "fr";
|
|
61052
|
+
console.log(fr ? `# \xC9cart avec l'audit externe (${ext2.source.adapter})` : `# Difference against the external audit (${ext2.source.adapter})`);
|
|
61053
|
+
console.log("");
|
|
61054
|
+
const order = ["regressed", "not-retested", "only-right", "partially-fixed", "fixed", "only-left", "unchanged"];
|
|
61055
|
+
console.log(order.map((b) => `${DIFF_LABEL[b][fr ? "fr" : "en"]} : ${d.counts[b]}`).join(" \xB7 "));
|
|
61056
|
+
console.log("");
|
|
61057
|
+
if (d.pagesOnlyRight.length) {
|
|
61058
|
+
console.log(
|
|
61059
|
+
fr ? `> \u26A0\uFE0F ${d.pagesOnlyRight.length} page(s) audit\xE9e(s) par l'auditeur externe n'existent pas dans votre grille : ${d.pagesOnlyRight.join(", ")}. Ses constats sur ces pages n'ont rien \xE0 quoi se comparer.` : `> \u26A0\uFE0F ${d.pagesOnlyRight.length} page(s) the external auditor ruled on are absent from your grid: ${d.pagesOnlyRight.join(", ")}. Their findings there have nothing to compare against.`
|
|
61060
|
+
);
|
|
61061
|
+
console.log("");
|
|
61062
|
+
}
|
|
61063
|
+
if (d.pagesOnlyLeft.length) {
|
|
61064
|
+
console.log(
|
|
61065
|
+
fr ? `> \u2139\uFE0F ${d.pagesOnlyLeft.length} page(s) de votre grille ne figurent pas dans l'audit externe : ${d.pagesOnlyLeft.join(", ")}.` : `> \u2139\uFE0F ${d.pagesOnlyLeft.length} page(s) in your grid are absent from the external audit: ${d.pagesOnlyLeft.join(", ")}.`
|
|
61066
|
+
);
|
|
61067
|
+
console.log("");
|
|
61068
|
+
}
|
|
61069
|
+
const mark = (s) => s === null ? "?" : s === "manual" ? "?" : s === "NA" ? "\u2014" : s;
|
|
61070
|
+
console.log(fr ? "| Page | Crit\xE8re | Nous | Eux | \xC9cart | Commentaire de l'auditeur |" : "| Page | Criterion | Ours | Theirs | Bucket | Auditor's comment |");
|
|
61071
|
+
console.log("| --- | --- | --- | --- | --- | --- |");
|
|
61072
|
+
for (const r of d.rows.filter((x) => x.bucket !== "unchanged")) {
|
|
61073
|
+
const note = (r.comment ?? "").replace(/\s*\n+\s*/g, " ").slice(0, 160);
|
|
61074
|
+
console.log(`| ${r.page} | ${r.criterion} | ${mark(r.left)} | ${mark(r.right)} | ${DIFF_LABEL[r.bucket][fr ? "fr" : "en"]} | ${note} |`);
|
|
61075
|
+
}
|
|
61076
|
+
return 0;
|
|
61077
|
+
}
|
|
61078
|
+
async function cmdImport(p) {
|
|
61079
|
+
const lang = resolveLang(p.flags, {});
|
|
61080
|
+
const from = typeof p.flags.from === "string" ? p.flags.from : void 0;
|
|
61081
|
+
const arg = p.positionals[0];
|
|
61082
|
+
if (!from || !arg) {
|
|
61083
|
+
console.error(
|
|
61084
|
+
lang === "fr" ? "ultra11y import : usage `import --from file <rapport.json>` ou `import --from ara <id>` (voir --help)." : "ultra11y import: usage `import --from file <report.json>` or `import --from ara <id>` (see --help)."
|
|
61085
|
+
);
|
|
61086
|
+
return 2;
|
|
61087
|
+
}
|
|
61088
|
+
const outDir = typeof p.flags.out === "string" ? p.flags.out : void 0;
|
|
61089
|
+
let rawText;
|
|
61090
|
+
let sourceUrl;
|
|
61091
|
+
let adapterId;
|
|
61092
|
+
if (from === "file") {
|
|
61093
|
+
adapterId = typeof p.flags.source === "string" ? p.flags.source : "ara";
|
|
61094
|
+
try {
|
|
61095
|
+
rawText = arg === "-" ? await readStdin() : readText(arg);
|
|
61096
|
+
} catch (e) {
|
|
61097
|
+
console.error(`ultra11y import: ${e instanceof Error ? e.message : String(e)}`);
|
|
61098
|
+
return 1;
|
|
61099
|
+
}
|
|
61100
|
+
} else {
|
|
61101
|
+
adapterId = from;
|
|
61102
|
+
let adapter2;
|
|
61103
|
+
try {
|
|
61104
|
+
adapter2 = createAdapter(adapterId);
|
|
61105
|
+
} catch (e) {
|
|
61106
|
+
console.error(`ultra11y import: ${e instanceof Error ? e.message : String(e)}`);
|
|
61107
|
+
return 2;
|
|
61108
|
+
}
|
|
61109
|
+
if (!adapter2.fetchUrl) {
|
|
61110
|
+
console.error(`ultra11y import: the "${adapterId}" adapter has no remote endpoint \u2014 use \`--from file <report.json>\`.`);
|
|
61111
|
+
return 2;
|
|
61112
|
+
}
|
|
61113
|
+
sourceUrl = adapter2.fetchUrl(arg);
|
|
61114
|
+
try {
|
|
61115
|
+
const res = await fetch(sourceUrl, { redirect: "follow" });
|
|
61116
|
+
if (!res.ok) {
|
|
61117
|
+
console.error(`ultra11y import: ${sourceUrl} returned HTTP ${res.status}.`);
|
|
61118
|
+
return 1;
|
|
61119
|
+
}
|
|
61120
|
+
rawText = await res.text();
|
|
61121
|
+
} catch (e) {
|
|
61122
|
+
console.error(`ultra11y import: could not reach ${sourceUrl} \u2014 ${e instanceof Error ? e.message : String(e)}`);
|
|
61123
|
+
return 1;
|
|
61124
|
+
}
|
|
61125
|
+
if (outDir) {
|
|
61126
|
+
mkdirSync15(outDir, { recursive: true });
|
|
61127
|
+
const rawFile = join45(outDir, `external-${adapterId}-${arg}.raw.json`);
|
|
61128
|
+
writeFileSync17(rawFile, rawText.endsWith("\n") ? rawText : `${rawText}
|
|
61129
|
+
`);
|
|
61130
|
+
if (!p.flags.json) console.log(rawFile);
|
|
61131
|
+
}
|
|
61132
|
+
}
|
|
61133
|
+
let raw;
|
|
61134
|
+
try {
|
|
61135
|
+
raw = JSON.parse(rawText);
|
|
61136
|
+
} catch (e) {
|
|
61137
|
+
console.error(`ultra11y import: the report is not valid JSON \u2014 ${e instanceof Error ? e.message : String(e)}`);
|
|
61138
|
+
return 1;
|
|
61139
|
+
}
|
|
61140
|
+
let adapter;
|
|
61141
|
+
try {
|
|
61142
|
+
adapter = createAdapter(adapterId);
|
|
61143
|
+
} catch (e) {
|
|
61144
|
+
console.error(`ultra11y import: ${e instanceof Error ? e.message : String(e)}`);
|
|
61145
|
+
return 2;
|
|
61146
|
+
}
|
|
61147
|
+
const parsed = adapter.parse(raw, { importedAt: (/* @__PURE__ */ new Date()).toISOString(), ...sourceUrl ? { url: sourceUrl } : {} });
|
|
61148
|
+
if (!parsed.ok) {
|
|
61149
|
+
console.error(
|
|
61150
|
+
lang === "fr" ? `ultra11y import : le rapport n'a pas pu \xEAtre lu sans perte (${parsed.issues.length} probl\xE8me(s)) \u2014 rien n'est \xE9crit, plut\xF4t qu'un import partiel qui aurait l'air complet :` : `ultra11y import: the report could not be read without loss (${parsed.issues.length} issue(s)) \u2014 nothing is written, rather than a partial import that would look complete:`
|
|
61151
|
+
);
|
|
61152
|
+
for (const i2 of parsed.issues.slice(0, 20)) console.error(` \u2717 ${i2}`);
|
|
61153
|
+
if (parsed.issues.length > 20) console.error(` \u2026 ${parsed.issues.length - 20} more`);
|
|
61154
|
+
return 1;
|
|
61155
|
+
}
|
|
61156
|
+
const audit2 = parsed.audit;
|
|
61157
|
+
if (p.flags.json) {
|
|
61158
|
+
console.log(JSON.stringify(audit2, null, 2));
|
|
61159
|
+
return 0;
|
|
61160
|
+
}
|
|
61161
|
+
if (outDir) {
|
|
61162
|
+
mkdirSync15(outDir, { recursive: true });
|
|
61163
|
+
const file = join45(outDir, "external-latest.json");
|
|
61164
|
+
writeFileSync17(file, `${JSON.stringify(audit2, null, 2)}
|
|
61165
|
+
`);
|
|
61166
|
+
console.log(file);
|
|
61167
|
+
} else {
|
|
61168
|
+
console.log(JSON.stringify(audit2, null, 2));
|
|
61169
|
+
}
|
|
61170
|
+
const decided = audit2.results.filter((r) => r.status !== "manual").length;
|
|
61171
|
+
console.error(
|
|
61172
|
+
lang === "fr" ? `${audit2.pages.length} page(s), ${audit2.results.length} r\xE9sultat(s) dont ${decided} tranch\xE9(s) \u2014 audit externe, jamais fusionn\xE9 dans le verdict du moteur. Comparez : \`pages --in <audit.json> --diff <ce fichier>\`.` : `${audit2.pages.length} page(s), ${audit2.results.length} result(s), ${decided} ruled \u2014 an external audit, never merged into the engine's own verdict. Compare with: \`pages --in <audit.json> --diff <this file>\`.`
|
|
61173
|
+
);
|
|
61174
|
+
return 0;
|
|
61175
|
+
}
|
|
60594
61176
|
function standardLabelSafe(standard) {
|
|
60595
61177
|
return isCore(standard) ? "WCAG 2.2 AA" : getPack(standard)?.name ?? standard;
|
|
60596
61178
|
}
|
|
@@ -60793,6 +61375,8 @@ async function main(argv) {
|
|
|
60793
61375
|
return cmdScan(p);
|
|
60794
61376
|
case "sample":
|
|
60795
61377
|
return cmdSample(p);
|
|
61378
|
+
case "import":
|
|
61379
|
+
return cmdImport(p);
|
|
60796
61380
|
case "snapshot":
|
|
60797
61381
|
return cmdSnapshot(p);
|
|
60798
61382
|
case "pages":
|