ultra11y 3.1.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.
@@ -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 = "3.1.0";
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, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/--/g, "&#45;&#45;");
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) {
@@ -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 doc = reused ?? parseSource(content, file, { forceJsx: opts.forceJsx });
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 ?? ".ultra11y/captures"));
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 f of normative) {
48685
- out2.push(occurrenceLine(f, lang, { marker: "checkbox" }));
48686
- if (f.secondary?.note) out2.push(` - \u21B3 ${f.secondary.note}`);
48687
- if (f.related) out2.push(relatedLine(f.related, lang, { selector: true }));
48688
- if (f.origin) {
48689
- const comp = f.origin.component ?? f.origin.sourceFile ?? f.file;
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 ids = new Set(fromScope.map((p) => p.id));
49168
- const urls = new Set(fromScope.map((p) => p.url));
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 [...fromScope, ...extra];
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
- return c2 + nc === 0 ? 100 : Math.round(c2 / (c2 + nc) * 100);
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: pct(criteria)
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
- return basis === "snapshot" ? void 0 : L3[lang].basisNote;
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}%**`).join(" | ")} |`);
49318
- out2.push(`| _${s.snapshot}?_ | ${derived.map((p) => p.basis === "snapshot" ? `_${s.snapshot}_` : `_${s.source}_`).join(" | ")} |`);
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("", `![${s.screenshotAlt(pg.name)}](${href})`);
49546
49631
  }
49547
- for (const f of nc.slice(0, 30)) {
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 view = { ...result, findings: orphans, ...result.packFindings ? { packFindings: result.packFindings.filter((f) => !f.page) } : {} };
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(orphans.length, lang)}`, "", renderAuditorBacklog(view, lang, standard, backlogOpts)].join("\n")),
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 = /^-\s\[ \]\s+`([^`]+):(\d+)`\s+\(`([^`]*)`\)\s+—\s+(.*)$/;
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. Le taux ne porte que sur les crit\xE8res d\xE9cid\xE9s \u2014 il ne dit rien des crit\xE8res restant \xE0 \xE9valuer."
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. The rate covers only the decided criteria \u2014 it says nothing about those left to assess."
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 ? 100 : Math.round(c2 / (c2 + nc) * 100);
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 === "snapshot" ? s.snapshot : s.source}`);
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
- out2.push(`- **${s.rate}** : **${ratePct(rows)} %** _(${s.rateNote})_`);
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(t2.c + t2.nc, rows.length)}`, "");
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(`![${s.screenshotAlt(page.name)}](${shot})`, "");
@@ -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.rate} | ${s.blocking} | ${s.major} | ${s.minor} | ${s.sheet} |`);
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 nc = p.findings.filter((f) => !f.advisory);
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 === "snapshot" ? s.snapshot : s.source} | ${ratePct(rows)} % | ${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} |`
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.findings.filter((f) => !f.page);
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 === "snapshot" ? s.snapshot : s.source} | ${pg.conformancePct}% | ${n("bloquant")} | ${n("majeur")} | ${n("mineur")} |`
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.findings.filter((f) => !f.page && !f.advisory).length;
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 !== "snapshot")) out2.push(`> ${s.sourceBasis}`, "");
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(`<tr><td>${fr ? "Taux" : "Rate"}</td>${pages.map((p) => `<td class="rate">${p.conformancePct}%</td>`).join("")}</tr>`);
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 ? derivePages(applied.audit, pagesOf(applied.audit)).map((p) => ({ id: p.id, name: p.name, rate: p.conformancePct })) : []
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
- return { cwd, config: raw, next: "Check the sample covers every page kind the methodology requires, not just the convenient ones." };
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
- pages: derived.map((p) => ({ id: p.id, name: p.name, url: p.url, basis: p.basis, conformancePct: p.conformancePct, findings: p.findings.length })),
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 ?? ".ultra11y/captures";
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> ou --crawl <url>." : "ultra11y pages discover: pass --sitemap <url> or --crawl <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
- const proposed = proposeSamplePages(urls, titles);
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
- ${urls.length} URL(s) d\xE9couverte(s), ${merged.added.length} nouvelle(s). Rien n'a \xE9t\xE9 \xE9crit \u2014 relancez avec --write pour fusionner dans .ultra11yrc.json.` : `
59188
- ${urls.length} URL(s) discovered, ${merged.added.length} new. Nothing was written \u2014 re-run with --write to merge into .ultra11yrc.json.`
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 { missing } = lintSample(v.sample, methodology);
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
- { ok: true, pages: v.sample.pages.length, missing: missing.map((k) => ({ id: k.id, label: kindLabel(k, loc) })), warnings: v.warnings },
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
- if (missing.length === 0) {
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" ? `\u2713 \xC9chantillon complet (${v.sample.pages.length} page(s)) \u2014 tous les types de page requis par ${pack.name} sont couverts.` : `\u2713 Sample complete (${v.sample.pages.length} page(s)) \u2014 every page kind ${pack.name} requires is covered.`
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
- (lang === "fr" ? `\u26A0\uFE0F \xC9chantillon incomplet (${v.sample.pages.length} page(s)) \u2014 types de page requis absents (${pack.name}) :` : `\u26A0\uFE0F Incomplete sample (${v.sample.pages.length} page(s)) \u2014 required page kinds missing (${pack.name}):`) + ` ${missing.map((k) => kindLabel(k, loc)).join(", ")}`
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":