ultra11y 2.25.0 → 2.27.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.
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { realpathSync as realpathSync4, writeFileSync as writeFileSync14, mkdirSync as mkdirSync11, existsSync as existsSync23, readFileSync as readFileSync20, appendFileSync } from "fs";
5
- import { join as join39, relative as relative3, sep as sep6, dirname as dirname11 } from "path";
4
+ import { realpathSync as realpathSync4, writeFileSync as writeFileSync15, mkdirSync as mkdirSync12, existsSync as existsSync24, readFileSync as readFileSync20, appendFileSync } from "fs";
5
+ import { join as join40, relative as relative3, sep as sep6, dirname as dirname11 } from "path";
6
6
  import { fileURLToPath as fileURLToPath5, pathToFileURL as pathToFileURL3 } from "url";
7
7
 
8
8
  // src/types.ts
9
- var VERSION = "2.25.0";
9
+ var VERSION = "2.27.0";
10
10
  var SCHEMA_VERSION = 2;
11
11
 
12
12
  // src/audit.ts
@@ -1197,7 +1197,7 @@ var wcag_universe_default = {
1197
1197
  wcagVersion: "2.2",
1198
1198
  source: "https://www.w3.org/TR/WCAG22/",
1199
1199
  criteriaSource: "https://github.com/w3c/wcag",
1200
- provenance: "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-07-31 via `node scripts/build-standards.mjs --refresh-universe`. Classification: core-AA = ships in src/data/wcag.json (the shipped WCAG 2.2 AA core); out-of-core = WCAG AAA; removed = obsolete (4.1.1).",
1200
+ provenance: "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-08-01 via `node scripts/build-standards.mjs --refresh-universe`. Classification: core-AA = ships in src/data/wcag.json (the shipped WCAG 2.2 AA core); out-of-core = WCAG AAA; removed = obsolete (4.1.1).",
1201
1201
  criteria: [
1202
1202
  {
1203
1203
  id: "1.1.1",
@@ -18887,9 +18887,9 @@ var EXTRACTOR_VERSION;
18887
18887
  var init_types = __esm({
18888
18888
  "src/types.ts"() {
18889
18889
  "use strict";
18890
- ENGINE_VERSION = "2.22.0";
18890
+ ENGINE_VERSION = "2.24.2";
18891
18891
  SCHEMA_VERSION2 = 5;
18892
- EXTRACTOR_VERSION = 11;
18892
+ EXTRACTOR_VERSION = 12;
18893
18893
  }
18894
18894
  });
18895
18895
  function sh(cmd, args2, opts = {}) {
@@ -19861,6 +19861,18 @@ var init_js_ts = __esm({
19861
19861
  { re: /^\s*module\.exports\.(?<name>[\w$]+)\s*=/, kind: "const", exported: true },
19862
19862
  // top-level const arrow function (not exported)
19863
19863
  { re: /^\s*(?:const|let)\s+(?<name>[\w$]+)\s*=\s*(?:async\s*)?\([^)]*\)\s*(?::[^=]+)?=>/, kind: "const", exported: false },
19864
+ // A module constant bound to a VALUE, not exported: `const RE = /href=/g`.
19865
+ // The AST tier indexes these (a top-level binding is a declaration, only an
19866
+ // in-function one is a local), and a fallback tier that answers "no such
19867
+ // symbol" for every module constant diverges from the engine it stands in for.
19868
+ //
19869
+ // Anchored at column 0, unlike every rule above it: a line scanner cannot see
19870
+ // scope, so `^\s*` here would sweep in each `const x = 1` inside every function
19871
+ // body — the locals-flood the AST tier's inFunctionBody filter exists to
19872
+ // prevent, and the surplus this project criticises in a flat tags file. Column
19873
+ // 0 is the one module-scope proxy a line can carry. Must stay AFTER the arrow
19874
+ // and export rules: `scan()` keeps the first rule that matches a line.
19875
+ { re: /^(?:const|let|var)\s+(?<name>[\w$]+)\s*[:=]/, kind: "const", exported: false },
19864
19876
  // `export default Foo;` — a class/const declared above and exported by reference.
19865
19877
  { re: /^\s*export\s+default\s+(?<name>[A-Za-z_$][\w$]*)\s*;?\s*$/, kind: "default", exported: true }
19866
19878
  ];
@@ -24974,7 +24986,15 @@ var init_specs = __esm({
24974
24986
  "arguments",
24975
24987
  "arrow_function",
24976
24988
  "function_expression",
24977
- "function"
24989
+ "function",
24990
+ // The IIFE's own link: `(function () { … })()` parses as
24991
+ // expression_statement → call_expression → parenthesized_expression →
24992
+ // function_expression, and without the parens listed the descent stopped one
24993
+ // node short — the same break the statement containers above were added for.
24994
+ // Whole categories of file are written inside one (browser scripts, UMD and
24995
+ // loader bundles), so for those the index held no functions at all. Named by
24996
+ // the ctags differential on a real loader script.
24997
+ "parenthesized_expression"
24978
24998
  ]),
24979
24999
  exported: neverExport,
24980
25000
  // export is tracked structurally; see LangSpec.exportMarkers
@@ -44894,13 +44914,60 @@ function runAudit(opts) {
44894
44914
  return result;
44895
44915
  }
44896
44916
 
44897
- // src/report.ts
44898
- import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync7 } from "fs";
44917
+ // src/hook.ts
44918
+ import { execFileSync as execFileSync2 } from "child_process";
44919
+ import { createHash as createHash5 } from "crypto";
44920
+ import { existsSync as existsSync15, mkdirSync as mkdirSync5, writeFileSync as writeFileSync6 } from "fs";
44921
+ import { tmpdir } from "os";
44899
44922
  import { join as join27 } from "path";
44900
44923
 
44901
- // src/prd.ts
44902
- import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync6 } from "fs";
44903
- import { join as join26 } from "path";
44924
+ // src/baseline.ts
44925
+ var RANK = { bloquant: 0, majeur: 1, mineur: 2 };
44926
+ function findingId(f) {
44927
+ const pos = f.sourceStart !== void 0 ? `b${f.sourceStart}-${f.sourceEnd}` : `l${f.line}:${f.col}:${f.selectorHint}`;
44928
+ return `${f.ruleId}|${f.criteriaId}|${f.file}|${pos}`;
44929
+ }
44930
+ function parseFailOn(v) {
44931
+ if (v === void 0 || v === true) return "bloquant";
44932
+ if (v === "bloquant" || v === "blocking") return "bloquant";
44933
+ if (v === "majeur" || v === "major") return "majeur";
44934
+ if (v === "mineur" || v === "minor") return "mineur";
44935
+ return null;
44936
+ }
44937
+ function findingsAtOrAbove(findings, failOn) {
44938
+ return findings.filter((f) => RANK[f.severity] <= RANK[failOn]);
44939
+ }
44940
+ function diffAgainstBaseline(current, baseline, failOn = "bloquant") {
44941
+ const baseIds = new Set((baseline?.findings ?? []).map(findingId));
44942
+ const curIds = new Set(current.findings.map(findingId));
44943
+ const newFindings = current.findings.filter((f) => !baseIds.has(findingId(f)));
44944
+ const auditedFiles = new Set(current.findings.map((f) => f.file));
44945
+ const fixedFindings = (baseline?.findings ?? []).filter((f) => auditedFiles.has(f.file) && !curIds.has(findingId(f)));
44946
+ const ok = !newFindings.some((f) => RANK[f.severity] <= RANK[failOn]);
44947
+ return { newFindings, fixedFindings, ok, failOn };
44948
+ }
44949
+ function baselineSummary(diff, lang = "fr") {
44950
+ const out2 = [];
44951
+ const blocking = diff.newFindings.filter((f) => RANK[f.severity] <= RANK[diff.failOn]);
44952
+ if (diff.ok) {
44953
+ out2.push(
44954
+ lang === "fr" ? `\u2713 Aucune nouvelle non-conformit\xE9 \u2265 ${diff.failOn} (${diff.newFindings.length} nouvelle(s) au total, ${diff.fixedFindings.length} corrig\xE9e(s)).` : `\u2713 No new non-conformity \u2265 ${diff.failOn} (${diff.newFindings.length} new total, ${diff.fixedFindings.length} fixed).`
44955
+ );
44956
+ } else {
44957
+ out2.push(
44958
+ lang === "fr" ? `\u2717 ${blocking.length} nouvelle(s) non-conformit\xE9(s) \u2265 ${diff.failOn} introduite(s) :` : `\u2717 ${blocking.length} new non-conformity(ies) \u2265 ${diff.failOn} introduced:`
44959
+ );
44960
+ for (const f of blocking) out2.push(` [${f.severity}] ${f.ruleId} (WCAG ${f.criteriaId}) \u2014 ${f.file}:${f.line} (${f.selectorHint})`);
44961
+ }
44962
+ return out2.join("\n");
44963
+ }
44964
+
44965
+ // src/config.ts
44966
+ import { existsSync as existsSync14, statSync as statSync7 } from "fs";
44967
+ import { join as join26, isAbsolute as isAbsolute2 } from "path";
44968
+
44969
+ // src/pack.ts
44970
+ import { existsSync as existsSync13 } from "fs";
44904
44971
 
44905
44972
  // src/standards/pack.ts
44906
44973
  function localize(pack, s, lang) {
@@ -44946,167 +45013,150 @@ function idCaptureSource(pack) {
44946
45013
  return neutralizeCaptureGroups(pack.idPattern.replace(/^\^/, "").replace(/\$$/, ""));
44947
45014
  }
44948
45015
 
44949
- // src/standards/derive.ts
44950
- function aggregate2(statuses) {
44951
- if (statuses.includes("NC")) return "NC";
44952
- if (statuses.includes("C")) return "C";
44953
- if (statuses.includes("manual")) return "manual";
44954
- return "NA";
44955
- }
44956
- function ruleMatches(ruleId, patterns) {
44957
- for (const p of patterns) {
44958
- if (p === "*") return true;
44959
- if (p.endsWith(":*") || p.endsWith("-*")) {
44960
- if (ruleId.startsWith(p.slice(0, -1))) return true;
44961
- } else if (p === ruleId) return true;
45016
+ // src/pack.ts
45017
+ function exampleParses(ex) {
45018
+ for (const code2 of [ex.bad, ex.good]) {
45019
+ if (!code2) continue;
45020
+ if (ex.lang === "jsx") {
45021
+ if (!parseJsxAst(code2)) return `unparseable JSX example: ${code2.slice(0, 48)}\u2026`;
45022
+ } else if (ex.lang === "html") {
45023
+ try {
45024
+ parseHtml(code2, "<guidance>.html", false);
45025
+ } catch (e) {
45026
+ return `unparseable HTML example (${e instanceof Error ? e.message : String(e)})`;
45027
+ }
45028
+ }
44962
45029
  }
44963
- return false;
44964
- }
44965
- function packCriteriaForFinding(pack, finding) {
44966
- return pack.criteria.filter((pc) => pc.wcag.includes(finding.criteriaId) && (!pc.appliesTo || ruleMatches(finding.ruleId, pc.appliesTo.ruleIds))).map((pc) => pc.id);
44967
- }
44968
- function packConformancePct(derived) {
44969
- const c2 = derived.filter((d) => d.status === "C").length;
44970
- const nc = derived.filter((d) => d.status === "NC").length;
44971
- return c2 + nc === 0 ? 100 : Math.round(c2 / (c2 + nc) * 100);
44972
- }
44973
- function overrideFinding(f, overrides) {
44974
- const o = overrides?.[f.ruleId];
44975
- if (!o) return f;
44976
- const patched = { ...f };
44977
- if (o.advisory !== void 0) patched.advisory = o.advisory;
44978
- if (o.severity) patched.severity = o.severity;
44979
- return patched;
44980
- }
44981
- function pickLocale(ls, preferred) {
44982
- if (!ls) return void 0;
44983
- return ls[preferred] ?? ls.en ?? ls.fr ?? Object.values(ls).find((v) => typeof v === "string");
44984
- }
44985
- function tagSecondary(f, note) {
44986
- return { ...f, secondary: note ? { note } : {} };
44987
- }
44988
- function applySecondaryMappings(base, pc, enabled, sources, defaultLocale) {
44989
- const active = enabled.filter((m) => m.criterion === pc.id);
44990
- if (!active.length) return base;
44991
- const added = [];
44992
- for (const m of active) for (const f of sources) if (f.ruleId === m.ruleId) added.push(tagSecondary(f, pickLocale(m.note, defaultLocale)));
44993
- if (!added.length) return base;
44994
- const findings = [...base.findings, ...added];
44995
- if (added.some((f) => !f.advisory)) return { id: pc.id, theme: pc.theme, status: aggregate2([base.status, "NC"]), findings, scs: base.scs };
44996
- return { ...base, findings };
45030
+ return null;
44997
45031
  }
44998
- function derivePackResults(audit2, packKey) {
44999
- const pack = loadPack(packKey);
45000
- const byScId = new Map(audit2.criteria.map((c2) => [c2.id, c2]));
45001
- const myPackFindings = (audit2.packFindings ?? []).filter((f) => f.ruleId.startsWith(`pack:${packKey}:`));
45002
- const overrides = pack.overrides;
45003
- const deriveBase = (pc) => {
45004
- const outOfScope = pc.wcag.every((sc) => {
45005
- const s = knownScStatus(sc);
45006
- return s === "out-of-core" || s === "removed";
45007
- });
45008
- if (outOfScope) {
45009
- return { id: pc.id, theme: pc.theme, status: "manual", findings: [], scs: pc.wcag, outOfScope: true };
45010
- }
45011
- const scResults = pc.wcag.map((sc) => byScId.get(sc)).filter((x) => !!x);
45012
- const packFs = myPackFindings.filter((f) => pc.wcag.includes(f.criteriaId));
45013
- const allFindings = [...scResults.flatMap((r) => r.findings), ...packFs].map((f) => overrideFinding(f, overrides));
45014
- if (!pc.appliesTo) {
45015
- const status2 = scResults.length ? aggregate2(scResults.map((r) => r.status)) : "NA";
45016
- return { id: pc.id, theme: pc.theme, status: status2, findings: allFindings, scs: pc.wcag };
45017
- }
45018
- const findings = allFindings.filter((f) => ruleMatches(f.ruleId, pc.appliesTo.ruleIds));
45019
- const normativeFindings = findings.filter((f) => !f.advisory);
45020
- if (normativeFindings.length) {
45021
- return { id: pc.id, theme: pc.theme, status: "NC", findings, scs: pc.wcag };
45032
+ function checkGuidance(ds, pack) {
45033
+ const errors = [];
45034
+ const warnings = [];
45035
+ if (typeof ds !== "object" || ds === null || !Array.isArray(ds.entries)) {
45036
+ errors.push("guidance: must be an object with an entries[] array");
45037
+ return { errors, warnings };
45038
+ }
45039
+ const dataset = ds;
45040
+ if (dataset.pack && dataset.pack !== pack.key) warnings.push(`guidance: dataset.pack "${dataset.pack}" \u2260 pack key "${pack.key}"`);
45041
+ dataset.entries.forEach((e, i2) => {
45042
+ const at = `guidance.entries[${i2}]${e.id ? ` (${e.id})` : ""}`;
45043
+ const crit = e.criterionId ? getCriterion(pack, e.criterionId) : void 0;
45044
+ if (!e.criterionId) {
45045
+ errors.push(`${at}: criterionId is unresolved \u2014 resolve it to a real ${pack.key} criterion (no fabrication)`);
45046
+ } else if (!crit) {
45047
+ errors.push(`${at}: criterionId "${e.criterionId}" does not exist in ${pack.key}`);
45022
45048
  }
45023
- if (scResults.some((r) => r.status === "NC")) {
45024
- return { id: pc.id, theme: pc.theme, status: "manual", findings, scs: pc.wcag, scopedOut: true };
45049
+ if (!Array.isArray(e.wcag) || e.wcag.length === 0) {
45050
+ errors.push(`${at}: must map to at least one WCAG SC`);
45051
+ } else {
45052
+ for (const sc of e.wcag) {
45053
+ switch (classifySc(sc)) {
45054
+ case "malformed":
45055
+ errors.push(`${at}: malformed SC "${sc}"`);
45056
+ break;
45057
+ case "unknown":
45058
+ errors.push(`${at}: SC "${sc}" is not a recognized WCAG success criterion \u2014 fabricated?`);
45059
+ break;
45060
+ case "out-of-core":
45061
+ warnings.push(`${at}: SC "${sc}" is a real WCAG AAA success criterion, outside the WCAG 2.2 AA core`);
45062
+ break;
45063
+ case "removed":
45064
+ warnings.push(`${at}: SC "${sc}" is a real but removed WCAG success criterion (obsolete)`);
45065
+ break;
45066
+ }
45067
+ }
45068
+ if (crit) {
45069
+ const extra = e.wcag.filter((sc) => !crit.wcag.includes(sc));
45070
+ if (extra.length) warnings.push(`${at}: SC(s) ${extra.join(", ")} not in criterion ${crit.id} mapping (${crit.wcag.join(", ")})`);
45071
+ }
45025
45072
  }
45026
- const status = scResults.length ? aggregate2(scResults.map((r) => r.status)) : "NA";
45027
- return { id: pc.id, theme: pc.theme, status, findings, scs: pc.wcag };
45028
- };
45029
- const enabledSecondary = (pack.secondaryMappings ?? []).filter((m) => m.enabled === true);
45030
- const secondarySources = enabledSecondary.length ? [...audit2.criteria.flatMap((c2) => c2.findings), ...myPackFindings] : [];
45031
- const adjudicated = audit2.packAdjudication?.standard === packKey ? new Map(audit2.packAdjudication.criteria.map((c2) => [c2.id, c2])) : void 0;
45032
- return pack.criteria.map((pc) => {
45033
- const decided = adjudicated?.get(pc.id);
45034
- if (decided) {
45035
- return {
45036
- id: pc.id,
45037
- theme: pc.theme,
45038
- status: decided.status,
45039
- findings: decided.findings,
45040
- scs: pc.wcag,
45041
- ...decided.justification ? { justification: decided.justification } : {},
45042
- decidedBy: "agent"
45043
- };
45073
+ if (!Array.isArray(e.examples) || e.examples.length === 0) {
45074
+ warnings.push(`${at}: no code examples`);
45075
+ } else {
45076
+ e.examples.forEach((ex, j) => {
45077
+ const bad = exampleParses(ex);
45078
+ if (bad) errors.push(`${at}.examples[${j}]: ${bad}`);
45079
+ });
45044
45080
  }
45045
- const base = deriveBase(pc);
45046
- return enabledSecondary.length ? applySecondaryMappings(base, pc, enabledSecondary, secondarySources, pack.defaultLocale) : base;
45047
45081
  });
45082
+ return { errors, warnings };
45048
45083
  }
45049
- function findingsForStandard(audit2, standard) {
45050
- if (standard === CORE_KEY) return audit2.findings;
45051
- const mine = (audit2.packFindings ?? []).filter((f) => f.ruleId.startsWith(`pack:${standard}:`));
45052
- return mine.length ? [...audit2.findings, ...mine] : audit2.findings;
45053
- }
45054
-
45055
- // src/standards/vocabulary.ts
45056
- var DEFAULT = {
45057
- theme: { en: "Theme", fr: "Th\xE9matique" },
45058
- criterion: { en: "Criterion", fr: "Crit\xE8re" },
45059
- test: { en: "Test", fr: "Test" },
45060
- conformant: { en: "Conformant", fr: "Conforme" },
45061
- nonConformant: { en: "Non-conformant", fr: "Non conforme" },
45062
- notApplicable: { en: "Not applicable", fr: "Non applicable" },
45063
- auditorHeading: { en: "Accessibility criterion", fr: "Crit\xE8re d'accessibilit\xE9" }
45064
- };
45065
- var CORE = {
45066
- theme: { en: "Principle \xB7 Guideline", fr: "Principe \xB7 R\xE8gle" },
45067
- criterion: { en: "Success criterion", fr: "Crit\xE8re de succ\xE8s" },
45068
- test: { en: "Technique", fr: "Technique" },
45069
- conformant: { en: "Pass", fr: "Conforme" },
45070
- nonConformant: { en: "Fail", fr: "Non conforme" },
45071
- notApplicable: { en: "Not applicable", fr: "Non applicable" },
45072
- auditorHeading: { en: "Accessibility criterion", fr: "Crit\xE8re d'accessibilit\xE9" }
45073
- };
45074
- function pick(s, lang, fallbackLang) {
45075
- if (!s) return void 0;
45076
- return s[lang] ?? s[fallbackLang] ?? s.en ?? Object.values(s)[0];
45077
- }
45078
- function vocabularyFor(standard, lang) {
45079
- const base = isCore(standard) ? CORE : DEFAULT;
45080
- const pack = isCore(standard) ? void 0 : getPack(standard);
45081
- const voc = pack?.vocabulary;
45082
- const packDefault = pack?.defaultLocale ?? "en";
45083
- const term = (k) => pick(voc?.[k], lang, packDefault) ?? base[k][lang] ?? base[k].en;
45084
- return {
45085
- theme: term("theme"),
45086
- criterion: term("criterion"),
45087
- test: term("test"),
45088
- conformant: term("conformant"),
45089
- nonConformant: term("nonConformant"),
45090
- notApplicable: term("notApplicable"),
45091
- auditorHeading: term("auditorHeading"),
45092
- normativeNote: pick(voc?.normativeNote, lang, packDefault)
45093
- };
45094
- }
45095
-
45096
- // src/standards/index.ts
45097
- var CORE2 = CORE_KEY;
45098
- function resolveStandard(flag) {
45099
- if (flag === void 0 || flag === true || flag === "") return CORE_KEY;
45100
- const key = String(flag).toLowerCase();
45101
- if (!hasStandard(key)) {
45102
- throw new Error(`unknown standard "${key}" (known: ${listStandards().join(", ")})`);
45084
+ function runPackCheck(packPath, guidancePath) {
45085
+ const errors = [];
45086
+ const warnings = [];
45087
+ if (!existsSync13(packPath)) return { ok: false, errors: [`pack file not found: ${packPath}`], warnings };
45088
+ let raw;
45089
+ try {
45090
+ raw = JSON.parse(readText(packPath));
45091
+ } catch {
45092
+ return { ok: false, errors: [`${packPath}: not valid JSON`], warnings };
45103
45093
  }
45104
- return key;
45094
+ const v = validatePack(raw);
45095
+ for (const issue of v.issues) (issue.severity === "error" ? errors : warnings).push(`${issue.path ? `${issue.path}: ` : ""}${issue.message}`);
45096
+ if (v.ok && v.pack) {
45097
+ const known = new Set(ruleIds());
45098
+ const isNamespaced = (r) => /^(axe:|dyn-|agent:)/.test(r);
45099
+ const ownRuleIds = new Set((v.pack.rules ?? []).map((r) => `pack:${v.pack.key}:${r.id}`));
45100
+ const resolvable = (r) => isNamespaced(r) || known.has(r) || r.startsWith(`pack:${v.pack.key}:`) || ownRuleIds.has(r);
45101
+ for (const c2 of v.pack.criteria) {
45102
+ for (const r of c2.appliesTo?.ruleIds ?? []) {
45103
+ if (!resolvable(r)) warnings.push(`criteria "${c2.id}": appliesTo ruleId "${r}" is not a known engine rule (renamed/future?)`);
45104
+ }
45105
+ }
45106
+ for (const key of Object.keys(v.pack.overrides ?? {})) {
45107
+ if (!resolvable(key)) warnings.push(`overrides: ruleId "${key}" is not a known engine rule or a rule of this pack (renamed/future?)`);
45108
+ }
45109
+ }
45110
+ if (v.ok && v.pack && guidancePath) {
45111
+ if (!existsSync13(guidancePath)) {
45112
+ errors.push(`guidance file not found: ${guidancePath}`);
45113
+ } else {
45114
+ let gRaw;
45115
+ try {
45116
+ gRaw = JSON.parse(readText(guidancePath));
45117
+ } catch {
45118
+ errors.push(`${guidancePath}: not valid JSON`);
45119
+ }
45120
+ if (gRaw !== void 0) {
45121
+ const g = checkGuidance(gRaw, v.pack);
45122
+ errors.push(...g.errors);
45123
+ warnings.push(...g.warnings);
45124
+ }
45125
+ }
45126
+ }
45127
+ return { ok: errors.length === 0, errors, warnings };
45105
45128
  }
45106
- function standardLabel(standard) {
45107
- if (isCore(standard)) return "WCAG 2.2 AA";
45108
- const p = loadPack(standard);
45109
- return `${p.name} ${p.baseVersion}`;
45129
+ function packScaffold() {
45130
+ return JSON.stringify(
45131
+ {
45132
+ key: "mypack",
45133
+ name: "MyPack",
45134
+ fullName: "My national accessibility standard",
45135
+ org: "Org",
45136
+ country: "XX",
45137
+ baseVersion: "1.0",
45138
+ wcagVersion: "2.2",
45139
+ locales: ["en"],
45140
+ defaultLocale: "en",
45141
+ license: "\u2026",
45142
+ source: "https://\u2026",
45143
+ attribution: "\u2026 \xA9 \u2026",
45144
+ idPattern: "^\\d+\\.\\d+$",
45145
+ vocabulary: {
45146
+ theme: { en: "Theme" },
45147
+ criterion: { en: "Criterion" },
45148
+ test: { en: "Test" },
45149
+ conformant: { en: "Conformant" },
45150
+ nonConformant: { en: "Non-conformant" },
45151
+ notApplicable: { en: "Not applicable" },
45152
+ auditorHeading: { en: "Accessibility criterion" }
45153
+ },
45154
+ themes: [{ number: 1, name: { en: "Theme one" }, count: 1 }],
45155
+ criteria: [{ id: "1.1", theme: 1, title: { en: "Criterion one" }, titlePlain: { en: "Criterion one" }, wcag: ["1.1.1"] }]
45156
+ },
45157
+ null,
45158
+ 2
45159
+ );
45110
45160
  }
45111
45161
 
45112
45162
  // src/data/guidance/rgaa.json
@@ -47573,6 +47623,441 @@ function hasGuidance(packKey) {
47573
47623
  return datasets.has(packKey);
47574
47624
  }
47575
47625
 
47626
+ // src/config.ts
47627
+ var CONFIG_FILE = ".ultra11yrc.json";
47628
+ function loadConfig(cwd) {
47629
+ const p = join26(cwd, CONFIG_FILE);
47630
+ if (!existsSync14(p)) return null;
47631
+ let parsed;
47632
+ try {
47633
+ parsed = JSON.parse(readText(p));
47634
+ } catch {
47635
+ throw new Error(`${CONFIG_FILE}: not valid JSON`);
47636
+ }
47637
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
47638
+ throw new Error(`${CONFIG_FILE}: must be a JSON object`);
47639
+ }
47640
+ return parsed;
47641
+ }
47642
+ function packPaths(path) {
47643
+ if (!existsSync14(path)) return null;
47644
+ if (statSync7(path).isDirectory()) {
47645
+ const pack = join26(path, "pack.json");
47646
+ if (!existsSync14(pack)) return null;
47647
+ const glossary = join26(path, "glossary.json");
47648
+ const guidance = join26(path, "guidance.json");
47649
+ return {
47650
+ pack,
47651
+ glossary: existsSync14(glossary) ? glossary : void 0,
47652
+ guidance: existsSync14(guidance) ? guidance : void 0
47653
+ };
47654
+ }
47655
+ return { pack: path };
47656
+ }
47657
+ function loadRuntimeStandards(cwd, packFlags, onWarn, override = false) {
47658
+ const result = { errors: [], loadedPacks: [], loadedGuidance: [] };
47659
+ let config = null;
47660
+ try {
47661
+ config = loadConfig(cwd);
47662
+ } catch (e) {
47663
+ result.errors.push(e instanceof Error ? e.message : String(e));
47664
+ return result;
47665
+ }
47666
+ result.defaultStandard = config?.standard;
47667
+ for (const raw of [...config?.packs ?? [], ...packFlags]) {
47668
+ const paths = packPaths(isAbsolute2(raw) ? raw : join26(cwd, raw));
47669
+ if (!paths) {
47670
+ result.errors.push(`--pack ${raw}: not found (expected a pack JSON file or a directory with pack.json)`);
47671
+ continue;
47672
+ }
47673
+ let packObj;
47674
+ try {
47675
+ packObj = JSON.parse(readText(paths.pack));
47676
+ } catch {
47677
+ result.errors.push(`--pack ${paths.pack}: not valid JSON`);
47678
+ continue;
47679
+ }
47680
+ let glossary = {};
47681
+ if (paths.glossary) {
47682
+ try {
47683
+ glossary = JSON.parse(readText(paths.glossary));
47684
+ } catch {
47685
+ onWarn(`ultra11y: ${paths.glossary} is not valid JSON \u2014 ignoring glossary.`);
47686
+ }
47687
+ }
47688
+ const v = registerRuntimePack(packObj, glossary, { override });
47689
+ for (const issue of v.issues) if (issue.severity === "warn") onWarn(`ultra11y: ${raw}: ${issue.path ? `${issue.path}: ` : ""}${issue.message}`);
47690
+ if (!v.ok || !v.pack) {
47691
+ result.errors.push(`--pack ${paths.pack}: invalid pack
47692
+ ${formatIssues(v.issues).join("\n")}`);
47693
+ continue;
47694
+ }
47695
+ result.loadedPacks.push(v.pack.key);
47696
+ if (paths.guidance) loadGuidanceFile(paths.guidance, result, onWarn);
47697
+ }
47698
+ for (const g of config?.guidance ?? []) loadGuidanceFile(isAbsolute2(g) ? g : join26(cwd, g), result, onWarn);
47699
+ for (const sm of config?.secondaryMappings ?? []) {
47700
+ if (!sm || typeof sm.standard !== "string" || typeof sm.ruleId !== "string" || typeof sm.criterion !== "string") {
47701
+ result.errors.push("secondaryMappings: each entry must be { standard, ruleId, criterion, note? }");
47702
+ continue;
47703
+ }
47704
+ if (!getPack(sm.standard)) {
47705
+ result.errors.push(`secondaryMappings: unknown standard "${sm.standard}" (register its pack first; the WCAG core has no pack criteria)`);
47706
+ continue;
47707
+ }
47708
+ enableSecondaryMapping(sm.standard, { ruleId: sm.ruleId, criterion: sm.criterion, note: sm.note });
47709
+ }
47710
+ return result;
47711
+ }
47712
+ function loadGuidanceFile(path, result, onWarn) {
47713
+ if (!existsSync14(path)) {
47714
+ onWarn(`ultra11y: guidance ${path} not found \u2014 skipping.`);
47715
+ return;
47716
+ }
47717
+ let ds;
47718
+ try {
47719
+ ds = JSON.parse(readText(path));
47720
+ } catch {
47721
+ onWarn(`ultra11y: guidance ${path} is not valid JSON \u2014 skipping.`);
47722
+ return;
47723
+ }
47724
+ if (!ds || typeof ds.pack !== "string" || !Array.isArray(ds.entries)) {
47725
+ onWarn(`ultra11y: guidance ${path} is not a valid dataset (needs { pack, entries[] }) \u2014 skipping.`);
47726
+ return;
47727
+ }
47728
+ const pack = getPack(ds.pack);
47729
+ if (pack) {
47730
+ const g = checkGuidance(ds, pack);
47731
+ for (const w of g.warnings) onWarn(`ultra11y: guidance ${path}: ${w}`);
47732
+ if (g.errors.length) {
47733
+ result.errors.push(`guidance ${path}: invalid
47734
+ ${g.errors.map((e) => ` \u2717 ${e}`).join("\n")}`);
47735
+ return;
47736
+ }
47737
+ } else {
47738
+ for (const e of ds.entries) {
47739
+ if (!Array.isArray(e?.wcag) || !Array.isArray(e?.examples)) {
47740
+ onWarn(`ultra11y: guidance ${path}: entry "${e?.id ?? "?"}" missing wcag[]/examples[] \u2014 skipping dataset.`);
47741
+ return;
47742
+ }
47743
+ }
47744
+ }
47745
+ if (hasGuidance(ds.pack)) onWarn(`ultra11y: guidance ${path} overrides the existing guidance for pack "${ds.pack}".`);
47746
+ registerRuntimeGuidance(ds);
47747
+ result.loadedGuidance.push(path);
47748
+ }
47749
+
47750
+ // src/hook.ts
47751
+ var AT_CMD = String.raw`(?:^|[\n;|&(]\s*)`;
47752
+ var OPTS = String.raw`(?:\s+(?:-[cC]\s+\S+|--?[\w-]+(?:=\S+)?))*`;
47753
+ var INTENTS = [
47754
+ // Ordered by breadth of what the command publishes: a `git commit && git push` chain is
47755
+ // reviewed against the base branch (push scope), not just the staged snapshot.
47756
+ ["pr", new RegExp(`${AT_CMD}gh${OPTS}\\s+pr\\s+create(?![\\w-])`)],
47757
+ ["push", new RegExp(`${AT_CMD}git${OPTS}\\s+push(?![\\w-])`)],
47758
+ ["commit", new RegExp(`${AT_CMD}git${OPTS}\\s+commit(?![\\w-])`)]
47759
+ ];
47760
+ var OPTED_OUT = /(?:^|\s)--(?:no-verify|dry-run)(?![\w-])/;
47761
+ function matchGitIntent(command) {
47762
+ if (OPTED_OUT.test(command)) return null;
47763
+ for (const [intent, re] of INTENTS) if (re.test(command)) return intent;
47764
+ return null;
47765
+ }
47766
+ function git2(cwd, args2) {
47767
+ try {
47768
+ return execFileSync2("git", args2, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
47769
+ } catch {
47770
+ return null;
47771
+ }
47772
+ }
47773
+ function resolveBase(command, cwd) {
47774
+ const explicit = command.match(/--base[=\s]+(\S+)/)?.[1]?.replace(/^["']|["']$/g, "");
47775
+ if (explicit) return explicit.includes("/") ? explicit : git2(cwd, ["rev-parse", "--verify", `origin/${explicit}`]) ? `origin/${explicit}` : explicit;
47776
+ const head = git2(cwd, ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"]);
47777
+ if (head) return head;
47778
+ for (const ref of ["origin/main", "origin/master"]) if (git2(cwd, ["rev-parse", "--verify", ref])) return ref;
47779
+ return null;
47780
+ }
47781
+ function scopeFor(intent, command, cwd) {
47782
+ if (intent === "commit") return { staged: true };
47783
+ const base = resolveBase(command, cwd);
47784
+ return base ? { since: base } : null;
47785
+ }
47786
+ function resolveThreshold(cwd, env) {
47787
+ const fromEnv = env.ULTRA11Y_HOOK_FAIL_ON;
47788
+ if (fromEnv) return fromEnv === "off" ? null : parseFailOn(fromEnv);
47789
+ let configured;
47790
+ try {
47791
+ configured = loadConfig(cwd)?.hook?.failOn;
47792
+ } catch {
47793
+ }
47794
+ if (configured) return configured === "off" ? null : parseFailOn(configured);
47795
+ return "bloquant";
47796
+ }
47797
+ function loopKey(sessionId, intent, findings) {
47798
+ const h = createHash5("sha256");
47799
+ h.update(`${sessionId}\0${intent}\0`);
47800
+ for (const id of findings.map(findingId).sort()) h.update(`${id}\0`);
47801
+ return h.digest("hex").slice(0, 32);
47802
+ }
47803
+ function firstSighting(key) {
47804
+ try {
47805
+ const dir = join27(tmpdir(), "ultra11y-hook");
47806
+ mkdirSync5(dir, { recursive: true });
47807
+ const marker = join27(dir, key);
47808
+ if (existsSync15(marker)) return false;
47809
+ writeFileSync6(marker, "");
47810
+ return true;
47811
+ } catch {
47812
+ return false;
47813
+ }
47814
+ }
47815
+ var SEV_EN = { bloquant: "blocking", majeur: "major", mineur: "minor" };
47816
+ var MAX_DIGEST = 10;
47817
+ function digestOf(findings) {
47818
+ const shown = findings.slice(0, MAX_DIGEST).map((f) => `- ${f.file}:${f.line} \u2014 ${f.criteriaId} (${SEV_EN[f.severity]}) \u2014 ${f.message}`);
47819
+ if (findings.length > MAX_DIGEST) shown.push(`- \u2026and ${findings.length - MAX_DIGEST} more (the skill re-runs the engine and sees all of them).`);
47820
+ return shown.join("\n");
47821
+ }
47822
+ var SCOPE_HINT = {
47823
+ commit: "--staged --graph",
47824
+ push: (base) => `--since ${base} --graph`
47825
+ };
47826
+ function realAudit(scope, cwd) {
47827
+ const previous = process.cwd();
47828
+ try {
47829
+ process.chdir(cwd);
47830
+ return runAudit({
47831
+ inputs: ["."],
47832
+ changed: "since" in scope,
47833
+ ..."since" in scope ? { since: scope.since } : { staged: true },
47834
+ graph: true,
47835
+ onWarn: () => {
47836
+ }
47837
+ // stdout/stderr belong to the hook protocol — stay quiet
47838
+ });
47839
+ } catch {
47840
+ return null;
47841
+ } finally {
47842
+ try {
47843
+ process.chdir(previous);
47844
+ } catch {
47845
+ }
47846
+ }
47847
+ }
47848
+ function decide(payload, deps = {}) {
47849
+ const env = deps.env ?? process.env;
47850
+ const audit2 = deps.audit ?? realAudit;
47851
+ const seen = deps.seen ?? firstSighting;
47852
+ if (payload.tool_name !== "Bash") return null;
47853
+ const command = payload.tool_input?.command;
47854
+ if (!command) return null;
47855
+ if (env.SKIP_A11Y || env.ULTRA11Y_HOOK === "off") return null;
47856
+ const intent = matchGitIntent(command);
47857
+ if (!intent) return null;
47858
+ const cwd = payload.cwd || process.cwd();
47859
+ if (!git2(cwd, ["rev-parse", "--git-dir"])) return null;
47860
+ const threshold = resolveThreshold(cwd, env);
47861
+ if (!threshold) return null;
47862
+ const scope = scopeFor(intent, command, cwd);
47863
+ if (!scope) return null;
47864
+ const result = audit2(scope, cwd);
47865
+ if (!result) return null;
47866
+ const findings = findingsAtOrAbove(result.findings, threshold);
47867
+ if (findings.length === 0) return null;
47868
+ if (!seen(loopKey(payload.session_id ?? "", intent, findings))) return null;
47869
+ const counts = ["bloquant", "majeur", "mineur"].map((s) => [s, findings.filter((f) => f.severity === s).length]).filter(([, n]) => n > 0).map(([s, n]) => `${n} ${SEV_EN[s]}`).join(", ");
47870
+ const where = intent === "commit" ? "about to be committed" : intent === "push" ? "about to be pushed" : "about to go into this pull request";
47871
+ const hint = "since" in scope ? SCOPE_HINT.push(scope.since) : SCOPE_HINT.commit;
47872
+ return {
47873
+ hookSpecificOutput: {
47874
+ hookEventName: "PreToolUse",
47875
+ permissionDecision: "deny",
47876
+ permissionDecisionReason: `ultra11y: ${findings.length} accessibility finding(s) (${counts}) in the code ${where}.`,
47877
+ additionalContext: [
47878
+ `Invoke the \`review-a11y\` skill now, scoped to \`${hint}\`, before running this command again.`,
47879
+ "",
47880
+ "The engine already found:",
47881
+ digestOf(findings),
47882
+ "",
47883
+ "Adjudicate each one against the cited code \u2014 refute false positives with evidence, apply the fixes the user agrees to.",
47884
+ "Then re-run the command: this gate does not fire twice for the same set of findings, so it will go through."
47885
+ ].join("\n")
47886
+ }
47887
+ };
47888
+ }
47889
+
47890
+ // src/report.ts
47891
+ import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
47892
+ import { join as join29 } from "path";
47893
+
47894
+ // src/prd.ts
47895
+ import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync7 } from "fs";
47896
+ import { join as join28 } from "path";
47897
+
47898
+ // src/standards/derive.ts
47899
+ function aggregate2(statuses) {
47900
+ if (statuses.includes("NC")) return "NC";
47901
+ if (statuses.includes("C")) return "C";
47902
+ if (statuses.includes("manual")) return "manual";
47903
+ return "NA";
47904
+ }
47905
+ function ruleMatches(ruleId, patterns) {
47906
+ for (const p of patterns) {
47907
+ if (p === "*") return true;
47908
+ if (p.endsWith(":*") || p.endsWith("-*")) {
47909
+ if (ruleId.startsWith(p.slice(0, -1))) return true;
47910
+ } else if (p === ruleId) return true;
47911
+ }
47912
+ return false;
47913
+ }
47914
+ function packCriteriaForFinding(pack, finding) {
47915
+ return pack.criteria.filter((pc) => pc.wcag.includes(finding.criteriaId) && (!pc.appliesTo || ruleMatches(finding.ruleId, pc.appliesTo.ruleIds))).map((pc) => pc.id);
47916
+ }
47917
+ function packConformancePct(derived) {
47918
+ const c2 = derived.filter((d) => d.status === "C").length;
47919
+ const nc = derived.filter((d) => d.status === "NC").length;
47920
+ return c2 + nc === 0 ? 100 : Math.round(c2 / (c2 + nc) * 100);
47921
+ }
47922
+ function overrideFinding(f, overrides) {
47923
+ const o = overrides?.[f.ruleId];
47924
+ if (!o) return f;
47925
+ const patched = { ...f };
47926
+ if (o.advisory !== void 0) patched.advisory = o.advisory;
47927
+ if (o.severity) patched.severity = o.severity;
47928
+ return patched;
47929
+ }
47930
+ function pickLocale(ls, preferred) {
47931
+ if (!ls) return void 0;
47932
+ return ls[preferred] ?? ls.en ?? ls.fr ?? Object.values(ls).find((v) => typeof v === "string");
47933
+ }
47934
+ function tagSecondary(f, note) {
47935
+ return { ...f, secondary: note ? { note } : {} };
47936
+ }
47937
+ function applySecondaryMappings(base, pc, enabled, sources, defaultLocale) {
47938
+ const active = enabled.filter((m) => m.criterion === pc.id);
47939
+ if (!active.length) return base;
47940
+ const added = [];
47941
+ for (const m of active) for (const f of sources) if (f.ruleId === m.ruleId) added.push(tagSecondary(f, pickLocale(m.note, defaultLocale)));
47942
+ if (!added.length) return base;
47943
+ const findings = [...base.findings, ...added];
47944
+ if (added.some((f) => !f.advisory)) return { id: pc.id, theme: pc.theme, status: aggregate2([base.status, "NC"]), findings, scs: base.scs };
47945
+ return { ...base, findings };
47946
+ }
47947
+ function derivePackResults(audit2, packKey) {
47948
+ const pack = loadPack(packKey);
47949
+ const byScId = new Map(audit2.criteria.map((c2) => [c2.id, c2]));
47950
+ const myPackFindings = (audit2.packFindings ?? []).filter((f) => f.ruleId.startsWith(`pack:${packKey}:`));
47951
+ const overrides = pack.overrides;
47952
+ const deriveBase = (pc) => {
47953
+ const outOfScope = pc.wcag.every((sc) => {
47954
+ const s = knownScStatus(sc);
47955
+ return s === "out-of-core" || s === "removed";
47956
+ });
47957
+ if (outOfScope) {
47958
+ return { id: pc.id, theme: pc.theme, status: "manual", findings: [], scs: pc.wcag, outOfScope: true };
47959
+ }
47960
+ const scResults = pc.wcag.map((sc) => byScId.get(sc)).filter((x) => !!x);
47961
+ const packFs = myPackFindings.filter((f) => pc.wcag.includes(f.criteriaId));
47962
+ const allFindings = [...scResults.flatMap((r) => r.findings), ...packFs].map((f) => overrideFinding(f, overrides));
47963
+ if (!pc.appliesTo) {
47964
+ const status2 = scResults.length ? aggregate2(scResults.map((r) => r.status)) : "NA";
47965
+ return { id: pc.id, theme: pc.theme, status: status2, findings: allFindings, scs: pc.wcag };
47966
+ }
47967
+ const findings = allFindings.filter((f) => ruleMatches(f.ruleId, pc.appliesTo.ruleIds));
47968
+ const normativeFindings = findings.filter((f) => !f.advisory);
47969
+ if (normativeFindings.length) {
47970
+ return { id: pc.id, theme: pc.theme, status: "NC", findings, scs: pc.wcag };
47971
+ }
47972
+ if (scResults.some((r) => r.status === "NC")) {
47973
+ return { id: pc.id, theme: pc.theme, status: "manual", findings, scs: pc.wcag, scopedOut: true };
47974
+ }
47975
+ const status = scResults.length ? aggregate2(scResults.map((r) => r.status)) : "NA";
47976
+ return { id: pc.id, theme: pc.theme, status, findings, scs: pc.wcag };
47977
+ };
47978
+ const enabledSecondary = (pack.secondaryMappings ?? []).filter((m) => m.enabled === true);
47979
+ const secondarySources = enabledSecondary.length ? [...audit2.criteria.flatMap((c2) => c2.findings), ...myPackFindings] : [];
47980
+ const adjudicated = audit2.packAdjudication?.standard === packKey ? new Map(audit2.packAdjudication.criteria.map((c2) => [c2.id, c2])) : void 0;
47981
+ return pack.criteria.map((pc) => {
47982
+ const decided = adjudicated?.get(pc.id);
47983
+ if (decided) {
47984
+ return {
47985
+ id: pc.id,
47986
+ theme: pc.theme,
47987
+ status: decided.status,
47988
+ findings: decided.findings,
47989
+ scs: pc.wcag,
47990
+ ...decided.justification ? { justification: decided.justification } : {},
47991
+ decidedBy: "agent"
47992
+ };
47993
+ }
47994
+ const base = deriveBase(pc);
47995
+ return enabledSecondary.length ? applySecondaryMappings(base, pc, enabledSecondary, secondarySources, pack.defaultLocale) : base;
47996
+ });
47997
+ }
47998
+ function findingsForStandard(audit2, standard) {
47999
+ if (standard === CORE_KEY) return audit2.findings;
48000
+ const mine = (audit2.packFindings ?? []).filter((f) => f.ruleId.startsWith(`pack:${standard}:`));
48001
+ return mine.length ? [...audit2.findings, ...mine] : audit2.findings;
48002
+ }
48003
+
48004
+ // src/standards/vocabulary.ts
48005
+ var DEFAULT = {
48006
+ theme: { en: "Theme", fr: "Th\xE9matique" },
48007
+ criterion: { en: "Criterion", fr: "Crit\xE8re" },
48008
+ test: { en: "Test", fr: "Test" },
48009
+ conformant: { en: "Conformant", fr: "Conforme" },
48010
+ nonConformant: { en: "Non-conformant", fr: "Non conforme" },
48011
+ notApplicable: { en: "Not applicable", fr: "Non applicable" },
48012
+ auditorHeading: { en: "Accessibility criterion", fr: "Crit\xE8re d'accessibilit\xE9" }
48013
+ };
48014
+ var CORE = {
48015
+ theme: { en: "Principle \xB7 Guideline", fr: "Principe \xB7 R\xE8gle" },
48016
+ criterion: { en: "Success criterion", fr: "Crit\xE8re de succ\xE8s" },
48017
+ test: { en: "Technique", fr: "Technique" },
48018
+ conformant: { en: "Pass", fr: "Conforme" },
48019
+ nonConformant: { en: "Fail", fr: "Non conforme" },
48020
+ notApplicable: { en: "Not applicable", fr: "Non applicable" },
48021
+ auditorHeading: { en: "Accessibility criterion", fr: "Crit\xE8re d'accessibilit\xE9" }
48022
+ };
48023
+ function pick(s, lang, fallbackLang) {
48024
+ if (!s) return void 0;
48025
+ return s[lang] ?? s[fallbackLang] ?? s.en ?? Object.values(s)[0];
48026
+ }
48027
+ function vocabularyFor(standard, lang) {
48028
+ const base = isCore(standard) ? CORE : DEFAULT;
48029
+ const pack = isCore(standard) ? void 0 : getPack(standard);
48030
+ const voc = pack?.vocabulary;
48031
+ const packDefault = pack?.defaultLocale ?? "en";
48032
+ const term = (k) => pick(voc?.[k], lang, packDefault) ?? base[k][lang] ?? base[k].en;
48033
+ return {
48034
+ theme: term("theme"),
48035
+ criterion: term("criterion"),
48036
+ test: term("test"),
48037
+ conformant: term("conformant"),
48038
+ nonConformant: term("nonConformant"),
48039
+ notApplicable: term("notApplicable"),
48040
+ auditorHeading: term("auditorHeading"),
48041
+ normativeNote: pick(voc?.normativeNote, lang, packDefault)
48042
+ };
48043
+ }
48044
+
48045
+ // src/standards/index.ts
48046
+ var CORE2 = CORE_KEY;
48047
+ function resolveStandard(flag) {
48048
+ if (flag === void 0 || flag === true || flag === "") return CORE_KEY;
48049
+ const key = String(flag).toLowerCase();
48050
+ if (!hasStandard(key)) {
48051
+ throw new Error(`unknown standard "${key}" (known: ${listStandards().join(", ")})`);
48052
+ }
48053
+ return key;
48054
+ }
48055
+ function standardLabel(standard) {
48056
+ if (isCore(standard)) return "WCAG 2.2 AA";
48057
+ const p = loadPack(standard);
48058
+ return `${p.name} ${p.baseVersion}`;
48059
+ }
48060
+
47576
48061
  // src/auditor.ts
47577
48062
  var SEV_ORDER = ["bloquant", "majeur", "mineur"];
47578
48063
  var ICON = { bloquant: "\u{1F534}", majeur: "\u{1F7E0}", mineur: "\u{1F7E1}" };
@@ -48149,10 +48634,10 @@ function renderPrdDoc(r, lang = "en", standard = "wcag") {
48149
48634
  return out2.join("\n");
48150
48635
  }
48151
48636
  function writePrd(r, opts) {
48152
- mkdirSync5(opts.out, { recursive: true });
48637
+ mkdirSync6(opts.out, { recursive: true });
48153
48638
  if (opts.format === "doc") {
48154
- const p2 = join26(opts.out, `prd-doc-${r.date}.md`);
48155
- writeFileSync6(p2, renderPrdDoc(r, opts.lang, opts.standard));
48639
+ const p2 = join28(opts.out, `prd-doc-${r.date}.md`);
48640
+ writeFileSync7(p2, renderPrdDoc(r, opts.lang, opts.standard));
48156
48641
  return [p2];
48157
48642
  }
48158
48643
  const remediation = opts.format === "remediation";
@@ -48161,14 +48646,14 @@ function writePrd(r, opts) {
48161
48646
  const files = remediation ? renderPerCriterion(r, opts.lang, opts.standard) : renderAuditorPerCriterion(r, opts.lang, opts.standard, { technical });
48162
48647
  const paths = [];
48163
48648
  for (const f of files) {
48164
- const p2 = join26(opts.out, f.name);
48165
- writeFileSync6(p2, f.content);
48649
+ const p2 = join28(opts.out, f.name);
48650
+ writeFileSync7(p2, f.content);
48166
48651
  paths.push(p2);
48167
48652
  }
48168
48653
  return paths;
48169
48654
  }
48170
- const p = join26(opts.out, `prd-${r.date}.md`);
48171
- writeFileSync6(p, remediation ? renderBacklog(r, opts.lang, opts.standard) : renderAuditorBacklog(r, opts.lang, opts.standard, { technical }));
48655
+ const p = join28(opts.out, `prd-${r.date}.md`);
48656
+ writeFileSync7(p, remediation ? renderBacklog(r, opts.lang, opts.standard) : renderAuditorBacklog(r, opts.lang, opts.standard, { technical }));
48172
48657
  return [p];
48173
48658
  }
48174
48659
 
@@ -48619,14 +49104,14 @@ function renderPackReport(r, pack, lang = "en") {
48619
49104
  function writeReport(r, opts) {
48620
49105
  const core = isCore(opts.standard);
48621
49106
  const md = core ? renderReport(r, opts.lang) : renderPackReport(r, loadPack(opts.standard), opts.lang);
48622
- mkdirSync6(opts.out, { recursive: true });
48623
- const path = join27(opts.out, `${core ? "wcag" : opts.standard}-${r.date}.md`);
48624
- writeFileSync7(path, md);
49107
+ mkdirSync7(opts.out, { recursive: true });
49108
+ const path = join29(opts.out, `${core ? "wcag" : opts.standard}-${r.date}.md`);
49109
+ writeFileSync8(path, md);
48625
49110
  return path;
48626
49111
  }
48627
49112
 
48628
49113
  // src/gh.ts
48629
- import { execFileSync as execFileSync2 } from "child_process";
49114
+ import { execFileSync as execFileSync3 } from "child_process";
48630
49115
  var SEV_ORDER4 = ["bloquant", "majeur", "mineur"];
48631
49116
  var SEV_RANK2 = { bloquant: 0, majeur: 1, mineur: 2 };
48632
49117
  var ICON4 = { bloquant: "\u{1F534}", majeur: "\u{1F7E0}", mineur: "\u{1F7E1}" };
@@ -48641,7 +49126,7 @@ function standardTag(standard) {
48641
49126
  })();
48642
49127
  }
48643
49128
  function gh(args2, input) {
48644
- return execFileSync2("gh", args2, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], ...input !== void 0 ? { input } : {} });
49129
+ return execFileSync3("gh", args2, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], ...input !== void 0 ? { input } : {} });
48645
49130
  }
48646
49131
  function ghErrorReason(err2) {
48647
49132
  if (!err2 || typeof err2 !== "object") return void 0;
@@ -48654,7 +49139,7 @@ function ghErrorReason(err2) {
48654
49139
  }
48655
49140
  function ghAvailable() {
48656
49141
  try {
48657
- execFileSync2("gh", ["auth", "status"], { stdio: "ignore" });
49142
+ execFileSync3("gh", ["auth", "status"], { stdio: "ignore" });
48658
49143
  return true;
48659
49144
  } catch {
48660
49145
  return false;
@@ -49220,12 +49705,12 @@ function renderCriteriaReference() {
49220
49705
  }
49221
49706
 
49222
49707
  // src/check.ts
49223
- import { existsSync as existsSync14, readFileSync as readFileSync15 } from "fs";
49224
- import { dirname as dirname9, join as join29 } from "path";
49708
+ import { existsSync as existsSync17, readFileSync as readFileSync15 } from "fs";
49709
+ import { dirname as dirname9, join as join31 } from "path";
49225
49710
 
49226
49711
  // src/verify.ts
49227
- import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
49228
- import { join as join28 } from "path";
49712
+ import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync9 } from "fs";
49713
+ import { join as join30 } from "path";
49229
49714
  var VERIFY_MAX = 40;
49230
49715
  var plain = (s) => s.replace(/\[([^\]]+)\]\(#[^)]*\)/g, "$1");
49231
49716
  function auditorCriterionLine(standard) {
@@ -49399,16 +49884,16 @@ function applyVerdicts(items, expected) {
49399
49884
  return { ok: failures.length === 0, total, refuted, unsupported, unadjudicated, invalid, missing, failures };
49400
49885
  }
49401
49886
  function writeWorklist(items, outDir, semantic, standard = "wcag", lang = "en") {
49402
- mkdirSync7(outDir, { recursive: true });
49403
- const todoPath = join28(outDir, "VERIFY.todo.json");
49404
- const mdPath = join28(outDir, "VERIFY.md");
49405
- writeFileSync8(todoPath, JSON.stringify(items, null, 2) + "\n");
49406
- writeFileSync8(mdPath, formatWorklist(items, semantic, standard, lang));
49887
+ mkdirSync8(outDir, { recursive: true });
49888
+ const todoPath = join30(outDir, "VERIFY.todo.json");
49889
+ const mdPath = join30(outDir, "VERIFY.md");
49890
+ writeFileSync9(todoPath, JSON.stringify(items, null, 2) + "\n");
49891
+ writeFileSync9(mdPath, formatWorklist(items, semantic, standard, lang));
49407
49892
  return { todoPath, mdPath, count: items.length };
49408
49893
  }
49409
49894
 
49410
49895
  // src/grounding.ts
49411
- import { existsSync as existsSync13, readFileSync as readFileSync14 } from "fs";
49896
+ import { existsSync as existsSync16, readFileSync as readFileSync14 } from "fs";
49412
49897
  import { resolve as resolve4 } from "path";
49413
49898
  var WINDOW = 10;
49414
49899
  var norm2 = (s) => s.replace(/\s+/g, " ").trim();
@@ -49427,7 +49912,7 @@ function selectorProbes(selector) {
49427
49912
  function groundFinding(g, opts = {}) {
49428
49913
  if (isUnresolvable(g.file)) return { ok: true, moved: false };
49429
49914
  const path = resolve4(opts.cwd ?? process.cwd(), g.file);
49430
- if (!existsSync13(path)) return { ok: false, moved: false, issue: `cited file not found: ${g.file}` };
49915
+ if (!existsSync16(path)) return { ok: false, moved: false, issue: `cited file not found: ${g.file}` };
49431
49916
  let text;
49432
49917
  try {
49433
49918
  text = readFileSync14(path, "utf8");
@@ -49577,8 +50062,8 @@ function checkSemantic(md, opts) {
49577
50062
  const standard = opts.standard ?? "wcag";
49578
50063
  const s = M[lang];
49579
50064
  const empty = { total: 0, grounded: 0, moved: 0, failed: 0 };
49580
- const artifact = opts.verdictsPath ?? join29(dirname9(opts.reportPath), "VERIFY.todo.json");
49581
- if (!existsSync14(artifact)) return { ok: false, issues: [s.semanticMissing(artifact)], ...empty };
50065
+ const artifact = opts.verdictsPath ?? join31(dirname9(opts.reportPath), "VERIFY.todo.json");
50066
+ if (!existsSync17(artifact)) return { ok: false, issues: [s.semanticMissing(artifact)], ...empty };
49582
50067
  let items;
49583
50068
  try {
49584
50069
  const parsed = JSON.parse(readFileSync15(artifact, "utf8"));
@@ -49613,8 +50098,8 @@ function sectionBody(md, n) {
49613
50098
  }
49614
50099
 
49615
50100
  // src/adjudicate.ts
49616
- import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync9 } from "fs";
49617
- import { join as join30 } from "path";
50101
+ import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync10 } from "fs";
50102
+ import { join as join32 } from "path";
49618
50103
 
49619
50104
  // src/data/adjudication.json
49620
50105
  var adjudication_default = {
@@ -50537,7 +51022,7 @@ function docsForAudit(audit2, cwd) {
50537
51022
  const docs = [];
50538
51023
  for (const f of files) {
50539
51024
  try {
50540
- docs.push(parseSource(readText(cwd ? join30(cwd, f) : f), f));
51025
+ docs.push(parseSource(readText(cwd ? join32(cwd, f) : f), f));
50541
51026
  } catch {
50542
51027
  }
50543
51028
  }
@@ -50894,9 +51379,9 @@ function formatAdjudication(items, lang = "en", standard = CORE2) {
50894
51379
  return out2.join("\n");
50895
51380
  }
50896
51381
  function writeAdjudication(items, outDir, opts) {
50897
- mkdirSync8(outDir, { recursive: true });
50898
- const todoPath = join30(outDir, "ADJUDICATE.todo.json");
50899
- const mdPath = join30(outDir, "ADJUDICATE.md");
51382
+ mkdirSync9(outDir, { recursive: true });
51383
+ const todoPath = join32(outDir, "ADJUDICATE.todo.json");
51384
+ const mdPath = join32(outDir, "ADJUDICATE.md");
50900
51385
  const file = {
50901
51386
  tool: "ultra11y",
50902
51387
  kind: "adjudication",
@@ -50905,16 +51390,16 @@ function writeAdjudication(items, outDir, opts) {
50905
51390
  auditDate: opts.auditDate,
50906
51391
  items
50907
51392
  };
50908
- writeFileSync9(todoPath, JSON.stringify(file, null, 2) + "\n");
50909
- writeFileSync9(mdPath, formatAdjudication(items, opts.lang ?? "en", opts.standard));
51393
+ writeFileSync10(todoPath, JSON.stringify(file, null, 2) + "\n");
51394
+ writeFileSync10(mdPath, formatAdjudication(items, opts.lang ?? "en", opts.standard));
50910
51395
  return { todoPath, mdPath, count: items.length };
50911
51396
  }
50912
51397
 
50913
51398
  // src/scan.ts
50914
- import { execFileSync as execFileSync3 } from "child_process";
50915
- import { mkdtempSync as mkdtempSync2, writeFileSync as writeFileSync10, existsSync as existsSync16, statSync as statSync7, readdirSync as readdirSync5, rmSync as rmSync3, readFileSync as readFileSync16 } from "fs";
50916
- import { tmpdir } from "os";
50917
- import { join as join31, resolve as resolve5 } from "path";
51399
+ import { execFileSync as execFileSync4 } from "child_process";
51400
+ import { mkdtempSync as mkdtempSync2, writeFileSync as writeFileSync11, existsSync as existsSync19, statSync as statSync8, readdirSync as readdirSync5, rmSync as rmSync3, readFileSync as readFileSync16 } from "fs";
51401
+ import { tmpdir as tmpdir2 } from "os";
51402
+ import { join as join33, resolve as resolve5 } from "path";
50918
51403
  import { fileURLToPath as fileURLToPath3 } from "url";
50919
51404
 
50920
51405
  // src/axe-map.ts
@@ -51156,7 +51641,7 @@ async function crawlUrls(start2, opts) {
51156
51641
  }
51157
51642
 
51158
51643
  // src/sample.ts
51159
- import { existsSync as existsSync15 } from "fs";
51644
+ import { existsSync as existsSync18 } from "fs";
51160
51645
  function looksLikeTarget(u) {
51161
51646
  return /^https?:\/\//i.test(u) || /^(\.\.?[/\\]|[/\\])/.test(u) || /\.x?html?$/i.test(u);
51162
51647
  }
@@ -51192,7 +51677,7 @@ function validateSample(raw) {
51192
51677
  if (p.auth !== void 0 && typeof p.auth !== "boolean") err2(`sample.pages[${i2}].auth`, "auth must be a boolean");
51193
51678
  if (p.storageState !== void 0 && (typeof p.storageState !== "string" || p.storageState.trim() === "")) {
51194
51679
  err2(`sample.pages[${i2}].storageState`, "storageState must be a non-empty file path string");
51195
- } else if (typeof p.storageState === "string" && p.storageState.trim() !== "" && !existsSync15(p.storageState)) {
51680
+ } else if (typeof p.storageState === "string" && p.storageState.trim() !== "" && !existsSync18(p.storageState)) {
51196
51681
  warn(
51197
51682
  `sample.pages[${i2}].storageState`,
51198
51683
  `storageState path not found: "${p.storageState}" (resolved from the current directory) \u2014 the scan will not be able to load this session`
@@ -51301,7 +51786,7 @@ ENTRYPOINT ["node", "/app/runner.mjs"]
51301
51786
  `;
51302
51787
  function dockerAvailable() {
51303
51788
  try {
51304
- execFileSync3("docker", ["info"], { stdio: "ignore", timeout: 1e4 });
51789
+ execFileSync4("docker", ["info"], { stdio: "ignore", timeout: 1e4 });
51305
51790
  return true;
51306
51791
  } catch {
51307
51792
  return false;
@@ -51309,7 +51794,7 @@ function dockerAvailable() {
51309
51794
  }
51310
51795
  function imageExists(tag) {
51311
51796
  try {
51312
- execFileSync3("docker", ["image", "inspect", tag], { stdio: "ignore" });
51797
+ execFileSync4("docker", ["image", "inspect", tag], { stdio: "ignore" });
51313
51798
  return true;
51314
51799
  } catch {
51315
51800
  return false;
@@ -51317,22 +51802,22 @@ function imageExists(tag) {
51317
51802
  }
51318
51803
  var CTX_PREFIX = "ultra11y-dyn-";
51319
51804
  function buildImage(tag = IMAGE_TAG) {
51320
- const ctx = mkdtempSync2(join31(tmpdir(), CTX_PREFIX));
51805
+ const ctx = mkdtempSync2(join33(tmpdir2(), CTX_PREFIX));
51321
51806
  try {
51322
- writeFileSync10(join31(ctx, "runner.mjs"), RUNNER);
51323
- writeFileSync10(join31(ctx, "package.json"), PKG);
51324
- writeFileSync10(join31(ctx, "Dockerfile"), DOCKERFILE);
51325
- execFileSync3("docker", ["build", "-t", tag, ctx], { stdio: "inherit", timeout: 9e5 });
51807
+ writeFileSync11(join33(ctx, "runner.mjs"), RUNNER);
51808
+ writeFileSync11(join33(ctx, "package.json"), PKG);
51809
+ writeFileSync11(join33(ctx, "Dockerfile"), DOCKERFILE);
51810
+ execFileSync4("docker", ["build", "-t", tag, ctx], { stdio: "inherit", timeout: 9e5 });
51326
51811
  } finally {
51327
51812
  rmSync3(ctx, { recursive: true, force: true });
51328
51813
  }
51329
51814
  }
51330
51815
  function cleanTempContexts() {
51331
51816
  let removed = 0;
51332
- const dir = tmpdir();
51817
+ const dir = tmpdir2();
51333
51818
  for (const name2 of readdirSync5(dir)) {
51334
51819
  if (!name2.startsWith(CTX_PREFIX)) continue;
51335
- rmSync3(join31(dir, name2), { recursive: true, force: true });
51820
+ rmSync3(join33(dir, name2), { recursive: true, force: true });
51336
51821
  removed++;
51337
51822
  }
51338
51823
  return removed;
@@ -51341,7 +51826,7 @@ function cleanDynamic(tag = IMAGE_TAG) {
51341
51826
  let imageRemoved = false;
51342
51827
  if (dockerAvailable() && imageExists(tag)) {
51343
51828
  try {
51344
- execFileSync3("docker", ["rmi", "-f", tag], { stdio: "ignore" });
51829
+ execFileSync4("docker", ["rmi", "-f", tag], { stdio: "ignore" });
51345
51830
  imageRemoved = true;
51346
51831
  } catch {
51347
51832
  }
@@ -51364,7 +51849,7 @@ function runRunner(target, isFile, tag) {
51364
51849
  args2.push(tag, isFile ? MOUNT : target);
51365
51850
  let stdout;
51366
51851
  try {
51367
- stdout = execFileSync3("docker", args2, { encoding: "utf8", timeout: 24e4, maxBuffer: 32 * 1024 * 1024, stdio: ["ignore", "pipe", "pipe"] });
51852
+ stdout = execFileSync4("docker", args2, { encoding: "utf8", timeout: 24e4, maxBuffer: 32 * 1024 * 1024, stdio: ["ignore", "pipe", "pipe"] });
51368
51853
  } catch (e) {
51369
51854
  const err2 = e;
51370
51855
  const detail = (err2.stderr ? String(err2.stderr).trim() : "") || err2.message || String(e);
@@ -51443,7 +51928,7 @@ function toDynamicResult(out2, target, lang = "en", engine = "axe-core@playwrigh
51443
51928
  var DOCKER_TESTED_SCS = ["1.4.10"];
51444
51929
  function runScan(opts) {
51445
51930
  const isUrl3 = /^https?:\/\//i.test(opts.target);
51446
- if (!isUrl3 && !existsSync16(opts.target)) {
51931
+ if (!isUrl3 && !existsSync19(opts.target)) {
51447
51932
  throw new Error(`File not found: ${opts.target}. Pass an http(s):// URL or an existing HTML file.`);
51448
51933
  }
51449
51934
  if (!dockerAvailable()) {
@@ -51451,7 +51936,7 @@ function runScan(opts) {
51451
51936
  }
51452
51937
  const tag = opts.tag ?? IMAGE_TAG;
51453
51938
  if (!imageExists(tag)) buildImage(tag);
51454
- const isFile = !isUrl3 && existsSync16(opts.target) && statSync7(opts.target).isFile();
51939
+ const isFile = !isUrl3 && existsSync19(opts.target) && statSync8(opts.target).isFile();
51455
51940
  const out2 = runRunner(opts.target, isFile, tag);
51456
51941
  return { ...toDynamicResult(out2, opts.target), testedScs: [...DOCKER_TESTED_SCS] };
51457
51942
  }
@@ -51529,7 +52014,7 @@ async function runCrawlScan(opts) {
51529
52014
  var sevRank = { bloquant: 3, majeur: 2, mineur: 1 };
51530
52015
  function resolveHostAnchor(file, snippet2) {
51531
52016
  const s = snippet2?.trim();
51532
- if (!s || !existsSync16(file)) return null;
52017
+ if (!s || !existsSync19(file)) return null;
51533
52018
  let source;
51534
52019
  try {
51535
52020
  source = readFileSync16(file, "utf8");
@@ -51628,7 +52113,7 @@ function mergeDynamic(audit2, dynamic, lang = "en") {
51628
52113
  }
51629
52114
 
51630
52115
  // src/scan-local.ts
51631
- import { existsSync as existsSync17, statSync as statSync8 } from "fs";
52116
+ import { existsSync as existsSync20, statSync as statSync9 } from "fs";
51632
52117
  import { createRequire } from "module";
51633
52118
  import { resolve as resolve6 } from "path";
51634
52119
  var LOCAL_ENGINE = "axe-core@playwright (local)";
@@ -52184,10 +52669,10 @@ async function runOnPage(browser, AxeBuilder, target, isFile, opts) {
52184
52669
  }
52185
52670
  async function runScanLocal(opts) {
52186
52671
  const isUrl3 = /^https?:\/\//i.test(opts.target);
52187
- if (!isUrl3 && !existsSync17(opts.target)) {
52672
+ if (!isUrl3 && !existsSync20(opts.target)) {
52188
52673
  throw new Error(`File not found: ${opts.target}. Pass an http(s):// URL or an existing HTML file.`);
52189
52674
  }
52190
- const isFile = !isUrl3 && statSync8(opts.target).isFile();
52675
+ const isFile = !isUrl3 && statSync9(opts.target).isFile();
52191
52676
  const lang = opts.lang ?? "en";
52192
52677
  const interact = opts.interact !== false;
52193
52678
  const { chromium, AxeBuilder } = resolveLocalDeps(opts.cwd);
@@ -52265,7 +52750,7 @@ async function runCrawlScanLocal(opts) {
52265
52750
  }
52266
52751
 
52267
52752
  // src/fix.ts
52268
- import { writeFileSync as writeFileSync11 } from "fs";
52753
+ import { writeFileSync as writeFileSync12 } from "fs";
52269
52754
 
52270
52755
  // src/fix/edits.ts
52271
52756
  function openTag(source, start2) {
@@ -52479,7 +52964,7 @@ function fixOne(file, source, opts, canWrite = true) {
52479
52964
  regression = after.length > findings.length || after.some((f) => !beforeKinds.has(f.ruleId));
52480
52965
  if (opts.write && !regression && file !== "<stdin>") {
52481
52966
  if (canWrite) {
52482
- writeFileSync11(file, output);
52967
+ writeFileSync12(file, output);
52483
52968
  written = true;
52484
52969
  } else {
52485
52970
  skippedPartialStage = true;
@@ -52560,55 +53045,14 @@ function fixSummary(r, lang = "fr", write = false) {
52560
53045
  return out2.join("\n");
52561
53046
  }
52562
53047
 
52563
- // src/baseline.ts
52564
- var RANK = { bloquant: 0, majeur: 1, mineur: 2 };
52565
- function findingId(f) {
52566
- const pos = f.sourceStart !== void 0 ? `b${f.sourceStart}-${f.sourceEnd}` : `l${f.line}:${f.col}:${f.selectorHint}`;
52567
- return `${f.ruleId}|${f.criteriaId}|${f.file}|${pos}`;
52568
- }
52569
- function parseFailOn(v) {
52570
- if (v === void 0 || v === true) return "bloquant";
52571
- if (v === "bloquant" || v === "blocking") return "bloquant";
52572
- if (v === "majeur" || v === "major") return "majeur";
52573
- if (v === "mineur" || v === "minor") return "mineur";
52574
- return null;
52575
- }
52576
- function findingsAtOrAbove(findings, failOn) {
52577
- return findings.filter((f) => RANK[f.severity] <= RANK[failOn]);
52578
- }
52579
- function diffAgainstBaseline(current, baseline, failOn = "bloquant") {
52580
- const baseIds = new Set((baseline?.findings ?? []).map(findingId));
52581
- const curIds = new Set(current.findings.map(findingId));
52582
- const newFindings = current.findings.filter((f) => !baseIds.has(findingId(f)));
52583
- const auditedFiles = new Set(current.findings.map((f) => f.file));
52584
- const fixedFindings = (baseline?.findings ?? []).filter((f) => auditedFiles.has(f.file) && !curIds.has(findingId(f)));
52585
- const ok = !newFindings.some((f) => RANK[f.severity] <= RANK[failOn]);
52586
- return { newFindings, fixedFindings, ok, failOn };
52587
- }
52588
- function baselineSummary(diff, lang = "fr") {
52589
- const out2 = [];
52590
- const blocking = diff.newFindings.filter((f) => RANK[f.severity] <= RANK[diff.failOn]);
52591
- if (diff.ok) {
52592
- out2.push(
52593
- lang === "fr" ? `\u2713 Aucune nouvelle non-conformit\xE9 \u2265 ${diff.failOn} (${diff.newFindings.length} nouvelle(s) au total, ${diff.fixedFindings.length} corrig\xE9e(s)).` : `\u2713 No new non-conformity \u2265 ${diff.failOn} (${diff.newFindings.length} new total, ${diff.fixedFindings.length} fixed).`
52594
- );
52595
- } else {
52596
- out2.push(
52597
- lang === "fr" ? `\u2717 ${blocking.length} nouvelle(s) non-conformit\xE9(s) \u2265 ${diff.failOn} introduite(s) :` : `\u2717 ${blocking.length} new non-conformity(ies) \u2265 ${diff.failOn} introduced:`
52598
- );
52599
- for (const f of blocking) out2.push(` [${f.severity}] ${f.ruleId} (WCAG ${f.criteriaId}) \u2014 ${f.file}:${f.line} (${f.selectorHint})`);
52600
- }
52601
- return out2.join("\n");
52602
- }
52603
-
52604
53048
  // src/init.ts
52605
- import { writeFileSync as writeFileSync12, mkdirSync as mkdirSync9, chmodSync } from "fs";
52606
- import { execFileSync as execFileSync4 } from "child_process";
52607
- import { join as join32 } from "path";
53049
+ import { writeFileSync as writeFileSync13, mkdirSync as mkdirSync10, chmodSync } from "fs";
53050
+ import { execFileSync as execFileSync5 } from "child_process";
53051
+ import { join as join34 } from "path";
52608
53052
  var EN_SEV = { bloquant: "blocking", majeur: "major", mineur: "minor" };
52609
53053
  function repoRoot() {
52610
53054
  try {
52611
- return execFileSync4("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
53055
+ return execFileSync5("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
52612
53056
  } catch {
52613
53057
  return null;
52614
53058
  }
@@ -52700,18 +53144,18 @@ jobs:
52700
53144
  `;
52701
53145
  }
52702
53146
  function writeHook(root, enginePath, failOn, mode = "staged") {
52703
- const dir = join32(root, ".git", "hooks");
52704
- mkdirSync9(dir, { recursive: true });
52705
- const path = join32(dir, "pre-commit");
52706
- writeFileSync12(path, mode === "baseline" ? hookScript(enginePath, failOn) : stagedHookScript(enginePath, failOn));
53147
+ const dir = join34(root, ".git", "hooks");
53148
+ mkdirSync10(dir, { recursive: true });
53149
+ const path = join34(dir, "pre-commit");
53150
+ writeFileSync13(path, mode === "baseline" ? hookScript(enginePath, failOn) : stagedHookScript(enginePath, failOn));
52707
53151
  chmodSync(path, 493);
52708
53152
  return path;
52709
53153
  }
52710
53154
  function writeCi(root, enginePath, failOn) {
52711
- const dir = join32(root, ".github", "workflows");
52712
- mkdirSync9(dir, { recursive: true });
52713
- const path = join32(dir, "a11y.yml");
52714
- writeFileSync12(path, ciWorkflow(enginePath, failOn));
53155
+ const dir = join34(root, ".github", "workflows");
53156
+ mkdirSync10(dir, { recursive: true });
53157
+ const path = join34(dir, "a11y.yml");
53158
+ writeFileSync13(path, ciWorkflow(enginePath, failOn));
52715
53159
  return path;
52716
53160
  }
52717
53161
 
@@ -53012,7 +53456,7 @@ function stepSummary(result, opts = {}) {
53012
53456
 
53013
53457
  // src/dev.ts
53014
53458
  import { createServer } from "http";
53015
- import { join as join33 } from "path";
53459
+ import { join as join35 } from "path";
53016
53460
  var DEV_DEFAULT_PORT = 4111;
53017
53461
  function criterionLabel3(f, standard) {
53018
53462
  if (isCore(standard)) return `WCAG ${f.criteriaId}`;
@@ -53278,13 +53722,13 @@ function auditCollected(root, payload) {
53278
53722
  ...payload.axtree ? { axtree: payload.axtree } : {},
53279
53723
  ...payload.css ? { css: payload.css } : {}
53280
53724
  });
53281
- return { ok: true, result: runAudit({ inputs: [join33(dir, "dom.html")] }) };
53725
+ return { ok: true, result: runAudit({ inputs: [join35(dir, "dom.html")] }) };
53282
53726
  }
53283
53727
  function projectPages(root) {
53284
53728
  const snaps = readSnapshots(root);
53285
53729
  if (!snaps.length) return { result: null, pages: [] };
53286
53730
  const scope = pageScopesFrom(snaps);
53287
- const result = runAudit({ inputs: [join33(root, ".ultra11y/pages")] });
53731
+ const result = runAudit({ inputs: [join35(root, ".ultra11y/pages")] });
53288
53732
  result.scope.pages = scope;
53289
53733
  attributePages(result, scope);
53290
53734
  return { result, pages: derivePages(result, scope) };
@@ -53608,287 +54052,12 @@ function e2eSetupPlan(runners, paths, lang = "en") {
53608
54052
  return out2.join("\n");
53609
54053
  }
53610
54054
 
53611
- // src/config.ts
53612
- import { existsSync as existsSync19, statSync as statSync9 } from "fs";
53613
- import { join as join34, isAbsolute as isAbsolute2 } from "path";
53614
-
53615
- // src/pack.ts
53616
- import { existsSync as existsSync18 } from "fs";
53617
- function exampleParses(ex) {
53618
- for (const code2 of [ex.bad, ex.good]) {
53619
- if (!code2) continue;
53620
- if (ex.lang === "jsx") {
53621
- if (!parseJsxAst(code2)) return `unparseable JSX example: ${code2.slice(0, 48)}\u2026`;
53622
- } else if (ex.lang === "html") {
53623
- try {
53624
- parseHtml(code2, "<guidance>.html", false);
53625
- } catch (e) {
53626
- return `unparseable HTML example (${e instanceof Error ? e.message : String(e)})`;
53627
- }
53628
- }
53629
- }
53630
- return null;
53631
- }
53632
- function checkGuidance(ds, pack) {
53633
- const errors = [];
53634
- const warnings = [];
53635
- if (typeof ds !== "object" || ds === null || !Array.isArray(ds.entries)) {
53636
- errors.push("guidance: must be an object with an entries[] array");
53637
- return { errors, warnings };
53638
- }
53639
- const dataset = ds;
53640
- if (dataset.pack && dataset.pack !== pack.key) warnings.push(`guidance: dataset.pack "${dataset.pack}" \u2260 pack key "${pack.key}"`);
53641
- dataset.entries.forEach((e, i2) => {
53642
- const at = `guidance.entries[${i2}]${e.id ? ` (${e.id})` : ""}`;
53643
- const crit = e.criterionId ? getCriterion(pack, e.criterionId) : void 0;
53644
- if (!e.criterionId) {
53645
- errors.push(`${at}: criterionId is unresolved \u2014 resolve it to a real ${pack.key} criterion (no fabrication)`);
53646
- } else if (!crit) {
53647
- errors.push(`${at}: criterionId "${e.criterionId}" does not exist in ${pack.key}`);
53648
- }
53649
- if (!Array.isArray(e.wcag) || e.wcag.length === 0) {
53650
- errors.push(`${at}: must map to at least one WCAG SC`);
53651
- } else {
53652
- for (const sc of e.wcag) {
53653
- switch (classifySc(sc)) {
53654
- case "malformed":
53655
- errors.push(`${at}: malformed SC "${sc}"`);
53656
- break;
53657
- case "unknown":
53658
- errors.push(`${at}: SC "${sc}" is not a recognized WCAG success criterion \u2014 fabricated?`);
53659
- break;
53660
- case "out-of-core":
53661
- warnings.push(`${at}: SC "${sc}" is a real WCAG AAA success criterion, outside the WCAG 2.2 AA core`);
53662
- break;
53663
- case "removed":
53664
- warnings.push(`${at}: SC "${sc}" is a real but removed WCAG success criterion (obsolete)`);
53665
- break;
53666
- }
53667
- }
53668
- if (crit) {
53669
- const extra = e.wcag.filter((sc) => !crit.wcag.includes(sc));
53670
- if (extra.length) warnings.push(`${at}: SC(s) ${extra.join(", ")} not in criterion ${crit.id} mapping (${crit.wcag.join(", ")})`);
53671
- }
53672
- }
53673
- if (!Array.isArray(e.examples) || e.examples.length === 0) {
53674
- warnings.push(`${at}: no code examples`);
53675
- } else {
53676
- e.examples.forEach((ex, j) => {
53677
- const bad = exampleParses(ex);
53678
- if (bad) errors.push(`${at}.examples[${j}]: ${bad}`);
53679
- });
53680
- }
53681
- });
53682
- return { errors, warnings };
53683
- }
53684
- function runPackCheck(packPath, guidancePath) {
53685
- const errors = [];
53686
- const warnings = [];
53687
- if (!existsSync18(packPath)) return { ok: false, errors: [`pack file not found: ${packPath}`], warnings };
53688
- let raw;
53689
- try {
53690
- raw = JSON.parse(readText(packPath));
53691
- } catch {
53692
- return { ok: false, errors: [`${packPath}: not valid JSON`], warnings };
53693
- }
53694
- const v = validatePack(raw);
53695
- for (const issue of v.issues) (issue.severity === "error" ? errors : warnings).push(`${issue.path ? `${issue.path}: ` : ""}${issue.message}`);
53696
- if (v.ok && v.pack) {
53697
- const known = new Set(ruleIds());
53698
- const isNamespaced = (r) => /^(axe:|dyn-|agent:)/.test(r);
53699
- const ownRuleIds = new Set((v.pack.rules ?? []).map((r) => `pack:${v.pack.key}:${r.id}`));
53700
- const resolvable = (r) => isNamespaced(r) || known.has(r) || r.startsWith(`pack:${v.pack.key}:`) || ownRuleIds.has(r);
53701
- for (const c2 of v.pack.criteria) {
53702
- for (const r of c2.appliesTo?.ruleIds ?? []) {
53703
- if (!resolvable(r)) warnings.push(`criteria "${c2.id}": appliesTo ruleId "${r}" is not a known engine rule (renamed/future?)`);
53704
- }
53705
- }
53706
- for (const key of Object.keys(v.pack.overrides ?? {})) {
53707
- if (!resolvable(key)) warnings.push(`overrides: ruleId "${key}" is not a known engine rule or a rule of this pack (renamed/future?)`);
53708
- }
53709
- }
53710
- if (v.ok && v.pack && guidancePath) {
53711
- if (!existsSync18(guidancePath)) {
53712
- errors.push(`guidance file not found: ${guidancePath}`);
53713
- } else {
53714
- let gRaw;
53715
- try {
53716
- gRaw = JSON.parse(readText(guidancePath));
53717
- } catch {
53718
- errors.push(`${guidancePath}: not valid JSON`);
53719
- }
53720
- if (gRaw !== void 0) {
53721
- const g = checkGuidance(gRaw, v.pack);
53722
- errors.push(...g.errors);
53723
- warnings.push(...g.warnings);
53724
- }
53725
- }
53726
- }
53727
- return { ok: errors.length === 0, errors, warnings };
53728
- }
53729
- function packScaffold() {
53730
- return JSON.stringify(
53731
- {
53732
- key: "mypack",
53733
- name: "MyPack",
53734
- fullName: "My national accessibility standard",
53735
- org: "Org",
53736
- country: "XX",
53737
- baseVersion: "1.0",
53738
- wcagVersion: "2.2",
53739
- locales: ["en"],
53740
- defaultLocale: "en",
53741
- license: "\u2026",
53742
- source: "https://\u2026",
53743
- attribution: "\u2026 \xA9 \u2026",
53744
- idPattern: "^\\d+\\.\\d+$",
53745
- vocabulary: {
53746
- theme: { en: "Theme" },
53747
- criterion: { en: "Criterion" },
53748
- test: { en: "Test" },
53749
- conformant: { en: "Conformant" },
53750
- nonConformant: { en: "Non-conformant" },
53751
- notApplicable: { en: "Not applicable" },
53752
- auditorHeading: { en: "Accessibility criterion" }
53753
- },
53754
- themes: [{ number: 1, name: { en: "Theme one" }, count: 1 }],
53755
- criteria: [{ id: "1.1", theme: 1, title: { en: "Criterion one" }, titlePlain: { en: "Criterion one" }, wcag: ["1.1.1"] }]
53756
- },
53757
- null,
53758
- 2
53759
- );
53760
- }
53761
-
53762
- // src/config.ts
53763
- var CONFIG_FILE = ".ultra11yrc.json";
53764
- function loadConfig(cwd) {
53765
- const p = join34(cwd, CONFIG_FILE);
53766
- if (!existsSync19(p)) return null;
53767
- let parsed;
53768
- try {
53769
- parsed = JSON.parse(readText(p));
53770
- } catch {
53771
- throw new Error(`${CONFIG_FILE}: not valid JSON`);
53772
- }
53773
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
53774
- throw new Error(`${CONFIG_FILE}: must be a JSON object`);
53775
- }
53776
- return parsed;
53777
- }
53778
- function packPaths(path) {
53779
- if (!existsSync19(path)) return null;
53780
- if (statSync9(path).isDirectory()) {
53781
- const pack = join34(path, "pack.json");
53782
- if (!existsSync19(pack)) return null;
53783
- const glossary = join34(path, "glossary.json");
53784
- const guidance = join34(path, "guidance.json");
53785
- return {
53786
- pack,
53787
- glossary: existsSync19(glossary) ? glossary : void 0,
53788
- guidance: existsSync19(guidance) ? guidance : void 0
53789
- };
53790
- }
53791
- return { pack: path };
53792
- }
53793
- function loadRuntimeStandards(cwd, packFlags, onWarn, override = false) {
53794
- const result = { errors: [], loadedPacks: [], loadedGuidance: [] };
53795
- let config = null;
53796
- try {
53797
- config = loadConfig(cwd);
53798
- } catch (e) {
53799
- result.errors.push(e instanceof Error ? e.message : String(e));
53800
- return result;
53801
- }
53802
- result.defaultStandard = config?.standard;
53803
- for (const raw of [...config?.packs ?? [], ...packFlags]) {
53804
- const paths = packPaths(isAbsolute2(raw) ? raw : join34(cwd, raw));
53805
- if (!paths) {
53806
- result.errors.push(`--pack ${raw}: not found (expected a pack JSON file or a directory with pack.json)`);
53807
- continue;
53808
- }
53809
- let packObj;
53810
- try {
53811
- packObj = JSON.parse(readText(paths.pack));
53812
- } catch {
53813
- result.errors.push(`--pack ${paths.pack}: not valid JSON`);
53814
- continue;
53815
- }
53816
- let glossary = {};
53817
- if (paths.glossary) {
53818
- try {
53819
- glossary = JSON.parse(readText(paths.glossary));
53820
- } catch {
53821
- onWarn(`ultra11y: ${paths.glossary} is not valid JSON \u2014 ignoring glossary.`);
53822
- }
53823
- }
53824
- const v = registerRuntimePack(packObj, glossary, { override });
53825
- for (const issue of v.issues) if (issue.severity === "warn") onWarn(`ultra11y: ${raw}: ${issue.path ? `${issue.path}: ` : ""}${issue.message}`);
53826
- if (!v.ok || !v.pack) {
53827
- result.errors.push(`--pack ${paths.pack}: invalid pack
53828
- ${formatIssues(v.issues).join("\n")}`);
53829
- continue;
53830
- }
53831
- result.loadedPacks.push(v.pack.key);
53832
- if (paths.guidance) loadGuidanceFile(paths.guidance, result, onWarn);
53833
- }
53834
- for (const g of config?.guidance ?? []) loadGuidanceFile(isAbsolute2(g) ? g : join34(cwd, g), result, onWarn);
53835
- for (const sm of config?.secondaryMappings ?? []) {
53836
- if (!sm || typeof sm.standard !== "string" || typeof sm.ruleId !== "string" || typeof sm.criterion !== "string") {
53837
- result.errors.push("secondaryMappings: each entry must be { standard, ruleId, criterion, note? }");
53838
- continue;
53839
- }
53840
- if (!getPack(sm.standard)) {
53841
- result.errors.push(`secondaryMappings: unknown standard "${sm.standard}" (register its pack first; the WCAG core has no pack criteria)`);
53842
- continue;
53843
- }
53844
- enableSecondaryMapping(sm.standard, { ruleId: sm.ruleId, criterion: sm.criterion, note: sm.note });
53845
- }
53846
- return result;
53847
- }
53848
- function loadGuidanceFile(path, result, onWarn) {
53849
- if (!existsSync19(path)) {
53850
- onWarn(`ultra11y: guidance ${path} not found \u2014 skipping.`);
53851
- return;
53852
- }
53853
- let ds;
53854
- try {
53855
- ds = JSON.parse(readText(path));
53856
- } catch {
53857
- onWarn(`ultra11y: guidance ${path} is not valid JSON \u2014 skipping.`);
53858
- return;
53859
- }
53860
- if (!ds || typeof ds.pack !== "string" || !Array.isArray(ds.entries)) {
53861
- onWarn(`ultra11y: guidance ${path} is not a valid dataset (needs { pack, entries[] }) \u2014 skipping.`);
53862
- return;
53863
- }
53864
- const pack = getPack(ds.pack);
53865
- if (pack) {
53866
- const g = checkGuidance(ds, pack);
53867
- for (const w of g.warnings) onWarn(`ultra11y: guidance ${path}: ${w}`);
53868
- if (g.errors.length) {
53869
- result.errors.push(`guidance ${path}: invalid
53870
- ${g.errors.map((e) => ` \u2717 ${e}`).join("\n")}`);
53871
- return;
53872
- }
53873
- } else {
53874
- for (const e of ds.entries) {
53875
- if (!Array.isArray(e?.wcag) || !Array.isArray(e?.examples)) {
53876
- onWarn(`ultra11y: guidance ${path}: entry "${e?.id ?? "?"}" missing wcag[]/examples[] \u2014 skipping dataset.`);
53877
- return;
53878
- }
53879
- }
53880
- }
53881
- if (hasGuidance(ds.pack)) onWarn(`ultra11y: guidance ${path} overrides the existing guidance for pack "${ds.pack}".`);
53882
- registerRuntimeGuidance(ds);
53883
- result.loadedGuidance.push(path);
53884
- }
53885
-
53886
54055
  // src/orchestrate.ts
53887
- import { existsSync as existsSync20, mkdirSync as mkdirSync10, readFileSync as readFileSync17, rmSync as rmSync4, writeFileSync as writeFileSync13 } from "fs";
53888
- import { join as join36, resolve as resolve7 } from "path";
54056
+ import { existsSync as existsSync21, mkdirSync as mkdirSync11, readFileSync as readFileSync17, rmSync as rmSync4, writeFileSync as writeFileSync14 } from "fs";
54057
+ import { join as join37, resolve as resolve7 } from "path";
53889
54058
 
53890
54059
  // src/orchestrate-templates.ts
53891
- import { join as join35 } from "path";
54060
+ import { join as join36 } from "path";
53892
54061
  var ONE_WRITER_FOOTER = `
53893
54062
  ## Return, don't write
53894
54063
 
@@ -53960,7 +54129,7 @@ var PHASE_SPECS = {
53960
54129
  title: "Adjudicate",
53961
54130
  schema: ADJUDICATE_SCHEMA,
53962
54131
  description: (n) => `Adjudicate the ${n} residual judgment criterion(ia) of an ultra11y audit (fan-out, fail-closed fold)`,
53963
- applyHint: (engine, worklist, run2) => `node ${engine} verify --apply ${worklist} --in ${join35(run2, "audit-latest.json")} --out ${run2}`
54132
+ applyHint: (engine, worklist, run2) => `node ${engine} verify --apply ${worklist} --in ${join36(run2, "audit-latest.json")} --out ${run2}`
53964
54133
  },
53965
54134
  "verify-report": {
53966
54135
  role: "refuter",
@@ -53982,7 +54151,7 @@ function toBatches(ids, batchSize) {
53982
54151
  }
53983
54152
  function phaseWorkflowScript(ph, runAbs, engineAbs, batchSize) {
53984
54153
  const spec = phaseSpec(ph.name);
53985
- const scriptPath = join35(runAbs, "orchestration", `${ph.name}.workflow.mjs`);
54154
+ const scriptPath = join36(runAbs, "orchestration", `${ph.name}.workflow.mjs`);
53986
54155
  const meta2 = { name: `ultra11y-${ph.name}`, description: spec.description(ph.items), phases: [{ title: spec.title }] };
53987
54156
  return [
53988
54157
  `export const meta = ${JSON.stringify(meta2)}`,
@@ -54026,7 +54195,7 @@ function agentContracts(runAbs, engineAbs) {
54026
54195
 
54027
54196
  You adjudicate the residual judgment criteria of an ultra11y audit \u2014 the ones the deterministic engine could not decide (alt-text relevance, link purpose in context, reading order\u2026). The ACTIVE STANDARD is recorded in the worklist's \`standard\` field: under a country standard (e.g. \`rgaa\`) the items are that standard's OWN criteria, each carrying its numbered tests \u2014 not WCAG success criteria.
54028
54197
 
54029
- Worklist: \`${join35(runAbs, "ADJUDICATE.todo.json")}\` (an object with \`kind: "adjudication"\` and \`items[]\`). Handle ONLY the criteria whose \`criteriaId\` is named in your prompt (\`ITEMS=<id,\u2026>\`).
54198
+ Worklist: \`${join36(runAbs, "ADJUDICATE.todo.json")}\` (an object with \`kind: "adjudication"\` and \`items[]\`). Handle ONLY the criteria whose \`criteriaId\` is named in your prompt (\`ITEMS=<id,\u2026>\`).
54030
54199
 
54031
54200
  For EACH of your criteria:
54032
54201
 
@@ -54044,7 +54213,7 @@ ${footer}`,
54044
54213
 
54045
54214
  You are an adversarial skeptic verifying the non-conformities of an ultra11y report. Your job is to try to REFUTE each claim: assume it is wrong until the source proves it.
54046
54215
 
54047
- Worklist: \`${join35(runAbs, "VERIFY.todo.json")}\` (a JSON array; each entry has \`n\`, \`criteriaId\`, \`file\`, \`line\`, \`selector\`, \`claim\`). Handle ONLY the entries whose \`n\` is named in your prompt (\`ITEMS=<n,\u2026>\`).
54216
+ Worklist: \`${join36(runAbs, "VERIFY.todo.json")}\` (a JSON array; each entry has \`n\`, \`criteriaId\`, \`file\`, \`line\`, \`selector\`, \`claim\`). Handle ONLY the entries whose \`n\` is named in your prompt (\`ITEMS=<n,\u2026>\`).
54048
54217
 
54049
54218
  For EACH of your entries:
54050
54219
 
@@ -54080,14 +54249,14 @@ ${status}
54080
54249
 
54081
54250
  ## The loop (play every role yourself, one item at a time)
54082
54251
 
54083
- 1. **Audit** (if not done): \`${engine} audit "<globs>" --graph --out ${runAbs}\` \u2192 \`${join35(runAbs, "audit-latest.json")}\`.
54084
- 2. **Adjudicate the residual criteria** \u2014 \`${engine} verify --manual --in ${join35(runAbs, "audit-latest.json")} --out ${runAbs}\` writes \`${join35(runAbs, "ADJUDICATE.todo.json")}\`. For EVERY item, apply \`${join35(runAbs, "orchestration", "agents", "adjudicator.md")}\` yourself (read the evidence, rule C/NC/NA/manual, fill the required justification/findings/reason IN the todo file). Then fold, fail-closed: \`${engine} verify --apply ${join35(runAbs, "ADJUDICATE.todo.json")} --in ${join35(runAbs, "audit-latest.json")} --out ${runAbs}\`.
54085
- 3. **Report**: \`${engine} report --in ${join35(runAbs, "audit-latest.json")} --out ${runAbs}\`.
54086
- 4. **Verify the report's claims** \u2014 \`${engine} verify --report <the report .md> --out ${runAbs}\` writes \`${join35(runAbs, "VERIFY.todo.json")}\`. For EVERY entry, apply \`${join35(runAbs, "orchestration", "agents", "refuter.md")}\` yourself (open file:line, verdict supported/partial/refuted/unsupported + note IN the todo file). Then: \`${engine} verify --apply ${join35(runAbs, "VERIFY.todo.json")} --report <the report .md>\`.
54252
+ 1. **Audit** (if not done): \`${engine} audit "<globs>" --graph --out ${runAbs}\` \u2192 \`${join36(runAbs, "audit-latest.json")}\`.
54253
+ 2. **Adjudicate the residual criteria** \u2014 \`${engine} verify --manual --in ${join36(runAbs, "audit-latest.json")} --out ${runAbs}\` writes \`${join36(runAbs, "ADJUDICATE.todo.json")}\`. For EVERY item, apply \`${join36(runAbs, "orchestration", "agents", "adjudicator.md")}\` yourself (read the evidence, rule C/NC/NA/manual, fill the required justification/findings/reason IN the todo file). Then fold, fail-closed: \`${engine} verify --apply ${join36(runAbs, "ADJUDICATE.todo.json")} --in ${join36(runAbs, "audit-latest.json")} --out ${runAbs}\`.
54254
+ 3. **Report**: \`${engine} report --in ${join36(runAbs, "audit-latest.json")} --out ${runAbs}\`.
54255
+ 4. **Verify the report's claims** \u2014 \`${engine} verify --report <the report .md> --out ${runAbs}\` writes \`${join36(runAbs, "VERIFY.todo.json")}\`. For EVERY entry, apply \`${join36(runAbs, "orchestration", "agents", "refuter.md")}\` yourself (open file:line, verdict supported/partial/refuted/unsupported + note IN the todo file). Then: \`${engine} verify --apply ${join36(runAbs, "VERIFY.todo.json")} --report <the report .md>\`.
54087
54256
  5. **Gate**: \`${engine} check --report <the report .md> --semantic\` must exit 0 before presenting anything.
54088
54257
  6. **Fix & re-audit**: \`${engine} fix <globs> --write --iterate\`, hand-apply the judgment fixes, then loop from step 1 until the gate stays green.
54089
54258
 
54090
- With subagents available, prefer the emitted workflows instead: \`orchestrate --run ${runAbs} --phase <p>\` then \`Workflow({ scriptPath: "${join35(runAbs, "orchestration", "<p>.workflow.mjs")}" })\` \u2014 you stay the sole writer either way.
54259
+ With subagents available, prefer the emitted workflows instead: \`orchestrate --run ${runAbs} --phase <p>\` then \`Workflow({ scriptPath: "${join36(runAbs, "orchestration", "<p>.workflow.mjs")}" })\` \u2014 you stay the sole writer either way.
54091
54260
  `;
54092
54261
  }
54093
54262
 
@@ -54097,10 +54266,10 @@ var SMALL_WORKLIST = 3;
54097
54266
  var BATCH_SIZE = 8;
54098
54267
  function listPhases(runDir, engineAbs) {
54099
54268
  const run2 = resolve7(runDir);
54100
- const adjPath = join36(run2, "ADJUDICATE.todo.json");
54269
+ const adjPath = join37(run2, "ADJUDICATE.todo.json");
54101
54270
  let adjIds = [];
54102
54271
  let adjReady = false;
54103
- if (existsSync20(adjPath)) {
54272
+ if (existsSync21(adjPath)) {
54104
54273
  try {
54105
54274
  const f = JSON.parse(readFileSync17(adjPath, "utf8"));
54106
54275
  if (f && f.kind === "adjudication" && Array.isArray(f.items)) {
@@ -54110,10 +54279,10 @@ function listPhases(runDir, engineAbs) {
54110
54279
  } catch {
54111
54280
  }
54112
54281
  }
54113
- const verPath = join36(run2, "VERIFY.todo.json");
54282
+ const verPath = join37(run2, "VERIFY.todo.json");
54114
54283
  let verIds = [];
54115
54284
  let verReady = false;
54116
- if (existsSync20(verPath)) {
54285
+ if (existsSync21(verPath)) {
54117
54286
  try {
54118
54287
  const items = JSON.parse(readFileSync17(verPath, "utf8"));
54119
54288
  if (Array.isArray(items)) {
@@ -54130,7 +54299,7 @@ function listPhases(runDir, engineAbs) {
54130
54299
  worklist: adjPath,
54131
54300
  items: adjIds.length,
54132
54301
  ids: adjIds,
54133
- prerequisite: `node ${engineAbs} verify --manual --in ${join36(run2, "audit-latest.json")} --out ${run2}`
54302
+ prerequisite: `node ${engineAbs} verify --manual --in ${join37(run2, "audit-latest.json")} --out ${run2}`
54134
54303
  },
54135
54304
  {
54136
54305
  name: "verify-report",
@@ -54144,7 +54313,7 @@ function listPhases(runDir, engineAbs) {
54144
54313
  }
54145
54314
  function orchestrateRun(runDir, engineAbs, opts = {}) {
54146
54315
  const run2 = resolve7(runDir);
54147
- if (!existsSync20(run2)) {
54316
+ if (!existsSync21(run2)) {
54148
54317
  return { exitCode: 2, written: [], notices: [], errors: [`run dir not found: ${run2}`], phases: [] };
54149
54318
  }
54150
54319
  const phases = listPhases(run2, engineAbs);
@@ -54171,22 +54340,22 @@ function orchestrateRun(runDir, engineAbs, opts = {}) {
54171
54340
  }
54172
54341
  selected = [ph];
54173
54342
  }
54174
- const orchDir = join36(run2, "orchestration");
54175
- const agentsDir = join36(orchDir, "agents");
54176
- mkdirSync10(join36(orchDir, "out"), { recursive: true });
54177
- mkdirSync10(agentsDir, { recursive: true });
54343
+ const orchDir = join37(run2, "orchestration");
54344
+ const agentsDir = join37(orchDir, "agents");
54345
+ mkdirSync11(join37(orchDir, "out"), { recursive: true });
54346
+ mkdirSync11(agentsDir, { recursive: true });
54178
54347
  const written = [];
54179
54348
  const notices = [];
54180
54349
  for (const [name2, content] of Object.entries(agentContracts(run2, engineAbs))) {
54181
- const p = join36(agentsDir, `${name2}.md`);
54182
- writeFileSync13(p, content);
54350
+ const p = join37(agentsDir, `${name2}.md`);
54351
+ writeFileSync14(p, content);
54183
54352
  written.push(p);
54184
54353
  }
54185
54354
  const emitted = new Set(opts.eco ? [] : selected.filter((p) => p.items > 0).map((p) => p.name));
54186
54355
  for (const ph of phases) {
54187
54356
  if (emitted.has(ph.name)) continue;
54188
- const stale = join36(orchDir, `${ph.name}.workflow.mjs`);
54189
- if (existsSync20(stale)) {
54357
+ const stale = join37(orchDir, `${ph.name}.workflow.mjs`);
54358
+ if (existsSync21(stale)) {
54190
54359
  rmSync4(stale, { force: true });
54191
54360
  notices.push(`phase "${ph.name}": stale workflow removed \u2014 its worklist ${ph.ready ? "is now empty" : "no longer exists"}.`);
54192
54361
  }
@@ -54200,13 +54369,13 @@ function orchestrateRun(runDir, engineAbs, opts = {}) {
54200
54369
  if (ph.items <= SMALL_WORKLIST) {
54201
54370
  notices.push(`phase "${ph.name}": only ${ph.items} item(s) \u2014 the sequential --eco path is equivalent and cheaper.`);
54202
54371
  }
54203
- const p = join36(orchDir, `${ph.name}.workflow.mjs`);
54204
- writeFileSync13(p, phaseWorkflowScript(ph, run2, engineAbs, BATCH_SIZE));
54372
+ const p = join37(orchDir, `${ph.name}.workflow.mjs`);
54373
+ writeFileSync14(p, phaseWorkflowScript(ph, run2, engineAbs, BATCH_SIZE));
54205
54374
  written.push(p);
54206
54375
  }
54207
54376
  }
54208
- const rb = join36(orchDir, "RUNBOOK.md");
54209
- writeFileSync13(rb, runbookMd(phases, run2, engineAbs));
54377
+ const rb = join37(orchDir, "RUNBOOK.md");
54378
+ writeFileSync14(rb, runbookMd(phases, run2, engineAbs));
54210
54379
  written.push(rb);
54211
54380
  return { exitCode: 0, written, notices, errors: [], phases };
54212
54381
  }
@@ -54215,8 +54384,8 @@ function orchestrateRun(runDir, engineAbs, opts = {}) {
54215
54384
  import { createInterface as createInterface2 } from "readline";
54216
54385
 
54217
54386
  // src/mcp/handlers.ts
54218
- import { existsSync as existsSync21, readFileSync as readFileSync18, realpathSync as realpathSync2, statSync as statSync10 } from "fs";
54219
- import { isAbsolute as isAbsolute3, join as join37, resolve as resolve8, sep as sep4 } from "path";
54387
+ import { existsSync as existsSync22, readFileSync as readFileSync18, realpathSync as realpathSync2, statSync as statSync10 } from "fs";
54388
+ import { isAbsolute as isAbsolute3, join as join38, resolve as resolve8, sep as sep4 } from "path";
54220
54389
 
54221
54390
  // src/project-lock.ts
54222
54391
  var chains = /* @__PURE__ */ new Map();
@@ -54264,7 +54433,7 @@ function requiredCwd(args2, defaults) {
54264
54433
  const cwd = str2(args2.cwd) ?? defaults.defaultCwd;
54265
54434
  if (!cwd) throw new ToolError("`cwd` is required: an absolute path to the project root.");
54266
54435
  const abs = resolve8(cwd);
54267
- if (!existsSync21(abs)) throw new ToolError(`project root not found: ${abs}`);
54436
+ if (!existsSync22(abs)) throw new ToolError(`project root not found: ${abs}`);
54268
54437
  if (!statSync10(abs).isDirectory()) throw new ToolError(`\`cwd\` is not a directory: ${abs}`);
54269
54438
  return abs;
54270
54439
  }
@@ -54409,8 +54578,8 @@ function handlePackCheck(args2, cwd) {
54409
54578
  return { cwd, ...res };
54410
54579
  }
54411
54580
  function handleSampleCheck(cwd) {
54412
- const file = join37(cwd, ".ultra11yrc.json");
54413
- if (!existsSync21(file)) {
54581
+ const file = join38(cwd, ".ultra11yrc.json");
54582
+ if (!existsSync22(file)) {
54414
54583
  throw new ToolError(`no .ultra11yrc.json at ${cwd} \u2014 a page sample must be declared before it can be linted.`);
54415
54584
  }
54416
54585
  let raw;
@@ -54453,7 +54622,7 @@ function handleInit(args2, cwd) {
54453
54622
  const enginePath = resolve8("scripts/ultra11y.mjs");
54454
54623
  const written = [];
54455
54624
  if (bool(args2.hook)) written.push(writeHook(cwd, enginePath, failOn));
54456
- if (bool(args2.ci)) written.push(join37(cwd, ".github/workflows/a11y.yml"));
54625
+ if (bool(args2.ci)) written.push(join38(cwd, ".github/workflows/a11y.yml"));
54457
54626
  if (!written.length) {
54458
54627
  throw new ToolError("nothing to install \u2014 pass hook:true and/or ci:true.");
54459
54628
  }
@@ -54473,7 +54642,7 @@ function reportText(args2, tool) {
54473
54642
  if (inline) return inline;
54474
54643
  if (!file) throw new ToolError(`\`report_text\` is required \u2014 the report markdown for ultra11y_${tool} to work on.`);
54475
54644
  if (!isAbsolute3(file)) throw new ToolError("`report_file` must be an absolute path.");
54476
- if (!existsSync21(file)) throw new ToolError(`report file not found: ${file}`);
54645
+ if (!existsSync22(file)) throw new ToolError(`report file not found: ${file}`);
54477
54646
  return readFileSync18(file, "utf8");
54478
54647
  }
54479
54648
  function handleCriteria(args2) {
@@ -54489,7 +54658,7 @@ function handleCriteria(args2) {
54489
54658
  function handleRead(args2, cwd) {
54490
54659
  const raw = str2(args2.path);
54491
54660
  if (!raw) throw new ToolError("`path` is required \u2014 relative to the project root, or an absolute path inside it.");
54492
- const target = isAbsolute3(raw) ? raw : join37(cwd, raw);
54661
+ const target = isAbsolute3(raw) ? raw : join38(cwd, raw);
54493
54662
  let real;
54494
54663
  try {
54495
54664
  real = realpathSync2(target);
@@ -54998,25 +55167,25 @@ function str3(v) {
54998
55167
  var DECLARED = new Set([...TOOLS2, ...WRITE_TOOLS].map((t2) => t2.name));
54999
55168
 
55000
55169
  // src/mcp/resources.ts
55001
- import { existsSync as existsSync22, readdirSync as readdirSync6, readFileSync as readFileSync19, realpathSync as realpathSync3, statSync as statSync11 } from "fs";
55002
- import { basename as basename3, dirname as dirname10, join as join38, resolve as resolve9, sep as sep5 } from "path";
55170
+ import { existsSync as existsSync23, readdirSync as readdirSync6, readFileSync as readFileSync19, realpathSync as realpathSync3, statSync as statSync11 } from "fs";
55171
+ import { basename as basename3, dirname as dirname10, join as join39, resolve as resolve9, sep as sep5 } from "path";
55003
55172
  import { fileURLToPath as fileURLToPath4 } from "url";
55004
55173
  var SKILL_NAME = "ultra11y";
55005
55174
  var URI_SCHEME = "skill://";
55006
55175
  function resolveSkillRoot(moduleDir) {
55007
55176
  const here = moduleDir ?? dirname10(fileURLToPath4(import.meta.url));
55008
55177
  const candidates2 = [resolve9(here, ".."), resolve9(here, "..", "skills", SKILL_NAME), resolve9(here, "..", "..", "skills", SKILL_NAME)];
55009
- return candidates2.find((dir) => existsSync22(join38(dir, "SKILL.md")));
55178
+ return candidates2.find((dir) => existsSync23(join39(dir, "SKILL.md")));
55010
55179
  }
55011
55180
  function listResources(moduleDir) {
55012
55181
  const root = resolveSkillRoot(moduleDir);
55013
55182
  if (!root) return [];
55014
55183
  const out2 = [describe(root, "SKILL.md", `${SKILL_NAME}: the skill`)];
55015
- const refDir = join38(root, "references");
55016
- if (!existsSync22(refDir)) return out2;
55184
+ const refDir = join39(root, "references");
55185
+ if (!existsSync23(refDir)) return out2;
55017
55186
  for (const file of readdirSync6(refDir).sort()) {
55018
55187
  if (!file.endsWith(".md")) continue;
55019
- out2.push(describe(root, join38("references", file), `${SKILL_NAME} reference: ${basename3(file, ".md")}`));
55188
+ out2.push(describe(root, join39("references", file), `${SKILL_NAME} reference: ${basename3(file, ".md")}`));
55020
55189
  }
55021
55190
  return out2;
55022
55191
  }
@@ -55051,7 +55220,7 @@ function describe(root, rel2, fallbackTitle) {
55051
55220
  title: fallbackTitle,
55052
55221
  mimeType: "text/markdown"
55053
55222
  };
55054
- const summary = firstProse(join38(root, rel2));
55223
+ const summary = firstProse(join39(root, rel2));
55055
55224
  if (summary) decl.description = summary;
55056
55225
  return decl;
55057
55226
  }
@@ -55491,6 +55660,7 @@ Usage:
55491
55660
  ultra11y snapshot list [--root <dir>] [--json]
55492
55661
  ultra11y pages --in <audit.json> [--standard <pack>] [--json] [--lang auto|en|fr] (the per-page criterion grid)
55493
55662
  ultra11y mcp [--transport stdio|http] [--cwd <dir>] [--allow-write] [--port <n>] [--bind <addr>] [--allow-remote] [--allow-origin <o>] [--max-response-bytes <n>]
55663
+ ultra11y hook --claude-code (internal: the plugin's PreToolUse hook; payload on stdin)
55494
55664
  ultra11y dev [--port <n>] [--root <dir>] [--standard <pack>] [--lang auto|en|fr] (dev side-car: live overlay + per-page dashboard)
55495
55665
  ultra11y dev --next [--port <n>] (write the Next overlay component, then wire one line into your layout)
55496
55666
 
@@ -55631,6 +55801,13 @@ Commands:
55631
55801
  --transport http listens on 127.0.0.1 (--port, --bind), and stays
55632
55802
  loopback-only unless you pass --allow-remote. Read-only by default:
55633
55803
  --allow-write opts into the commands that touch files.
55804
+ hook INTERNAL \u2014 the decision half of the Claude Code plugin's PreToolUse
55805
+ hook. Reads the payload on stdin; when a pending 'git commit', 'git
55806
+ push' or 'gh pr create' carries findings >= the threshold, prints the
55807
+ hook JSON that gets the review-a11y skill invoked, and prints nothing
55808
+ otherwise. Threshold: ULTRA11Y_HOOK_FAIL_ON, else hook.failOn in
55809
+ .ultra11yrc.json, else blocking. Disable with ULTRA11Y_HOOK=off or
55810
+ SKIP_A11Y=1. Called by hooks/pre-tool-use.mjs, not by hand.
55634
55811
  pages The per-page criterion grid \u2014 RGAA is a per-page norm, the engine's
55635
55812
  verdict is scope-wide, and this bridges the two. One row per criterion
55636
55813
  (the pack's own under --standard), one column per page. Rebuilt from a
@@ -55784,7 +55961,8 @@ var COMMANDS = [
55784
55961
  "fix",
55785
55962
  "init",
55786
55963
  "pack",
55787
- "orchestrate"
55964
+ "orchestrate",
55965
+ "hook"
55788
55966
  ];
55789
55967
  function isCommand(s) {
55790
55968
  return !!s && COMMANDS.includes(s);
@@ -56005,9 +56183,9 @@ async function cmdAudit(p) {
56005
56183
  const capturesFlag = typeof p.flags.captures === "string" && p.flags.captures ? p.flags.captures : void 0;
56006
56184
  const capturesDir = capturesFlag ?? ".ultra11y/captures";
56007
56185
  const scopedToDiff = p.flags.changed === true || p.flags.staged === true || since !== void 0;
56008
- const capturesWanted = p.flags["no-captures"] !== true && !inputs.includes("-") && (capturesFlag !== void 0 || existsSync23(capturesDir));
56186
+ const capturesWanted = p.flags["no-captures"] !== true && !inputs.includes("-") && (capturesFlag !== void 0 || existsSync24(capturesDir));
56009
56187
  const useCaptures = capturesWanted && !scopedToDiff && !inputs.includes(capturesDir);
56010
- const pagesWanted = p.flags["no-captures"] !== true && !inputs.includes("-") && existsSync23(PAGES_DIR);
56188
+ const pagesWanted = p.flags["no-captures"] !== true && !inputs.includes("-") && existsSync24(PAGES_DIR);
56011
56189
  const usePages = pagesWanted && !scopedToDiff && !inputs.includes(PAGES_DIR);
56012
56190
  const auditInputs = [...inputs, ...useCaptures ? [capturesDir] : [], ...usePages ? [PAGES_DIR] : []];
56013
56191
  if (useCaptures)
@@ -56048,10 +56226,10 @@ async function cmdAudit(p) {
56048
56226
  if (typeof p.flags.out === "string") {
56049
56227
  const out2 = p.flags.out;
56050
56228
  const asFile = out2.toLowerCase().endsWith(".json");
56051
- const target = asFile ? out2 : join39(out2, "audit-latest.json");
56229
+ const target = asFile ? out2 : join40(out2, "audit-latest.json");
56052
56230
  try {
56053
- mkdirSync11(asFile ? dirname11(out2) : out2, { recursive: true });
56054
- writeFileSync14(target, JSON.stringify(result, null, 2) + "\n");
56231
+ mkdirSync12(asFile ? dirname11(out2) : out2, { recursive: true });
56232
+ writeFileSync15(target, JSON.stringify(result, null, 2) + "\n");
56055
56233
  console.error(lang === "fr" ? `\u2192 audit \xE9crit dans ${target}` : `\u2192 audit written to ${target}`);
56056
56234
  } catch {
56057
56235
  }
@@ -56070,7 +56248,7 @@ async function cmdAudit(p) {
56070
56248
  const baselineFlag = p.flags.baseline;
56071
56249
  if (typeof baselineFlag === "string" && baselineFlag) {
56072
56250
  let baseline = null;
56073
- if (existsSync23(baselineFlag)) {
56251
+ if (existsSync24(baselineFlag)) {
56074
56252
  try {
56075
56253
  const parsed = JSON.parse(readText(baselineFlag));
56076
56254
  if (isCurrentAudit(parsed)) baseline = parsed;
@@ -56124,11 +56302,11 @@ async function cmdDev(p) {
56124
56302
  return 2;
56125
56303
  }
56126
56304
  if (p.flags.next === true) {
56127
- const dir = join39(root, ".ultra11y", "next");
56305
+ const dir = join40(root, ".ultra11y", "next");
56128
56306
  const rel2 = ".ultra11y/next/overlay.jsx";
56129
56307
  try {
56130
- mkdirSync11(dir, { recursive: true });
56131
- writeFileSync14(join39(dir, "overlay.jsx"), nextOverlayComponent(port));
56308
+ mkdirSync12(dir, { recursive: true });
56309
+ writeFileSync15(join40(dir, "overlay.jsx"), nextOverlayComponent(port));
56132
56310
  } catch (e) {
56133
56311
  console.error(`ultra11y dev: could not write ${rel2}: ${e instanceof Error ? e.message : String(e)}`);
56134
56312
  return 1;
@@ -56216,7 +56394,7 @@ async function cmdSnapshot(p) {
56216
56394
  2
56217
56395
  )
56218
56396
  );
56219
- else if (!snaps.length) console.log(lang === "fr" ? `Aucun instantan\xE9 dans ${join39(root, PAGES_DIR)}.` : `No snapshot in ${join39(root, PAGES_DIR)}.`);
56397
+ else if (!snaps.length) console.log(lang === "fr" ? `Aucun instantan\xE9 dans ${join40(root, PAGES_DIR)}.` : `No snapshot in ${join40(root, PAGES_DIR)}.`);
56220
56398
  else for (const s of snaps) console.log(`${s.meta.id} ${s.meta.name} ${s.meta.url}${s.meta.auth ? " [auth]" : ""}`);
56221
56399
  return 0;
56222
56400
  }
@@ -56261,12 +56439,12 @@ async function cmdSnapshot(p) {
56261
56439
  }
56262
56440
  if (typeof payload.screenshot === "string" && payload.screenshot) {
56263
56441
  try {
56264
- writeFileSync14(join39(dir, "screen.png"), Buffer.from(payload.screenshot, "base64"));
56442
+ writeFileSync15(join40(dir, "screen.png"), Buffer.from(payload.screenshot, "base64"));
56265
56443
  } catch {
56266
56444
  console.error("ultra11y snapshot write: the screenshot could not be written \u2014 the pixel tier will be skipped for this page.");
56267
56445
  }
56268
56446
  }
56269
- const result = runAudit({ inputs: [join39(dir, "dom.html")], onWarn: (m) => console.error(m) });
56447
+ const result = runAudit({ inputs: [join40(dir, "dom.html")], onWarn: (m) => console.error(m) });
56270
56448
  const failOnRaw = p.flags["fail-on"];
56271
56449
  const failOnParsed = parseFailOn(failOnRaw);
56272
56450
  if (failOnRaw !== void 0 && failOnParsed === null) {
@@ -56303,9 +56481,9 @@ function cmdInit(p) {
56303
56481
  if (want.baseline) {
56304
56482
  const inputs = p.positionals.length ? p.positionals : ["."];
56305
56483
  const result = runAudit({ inputs, onWarn: (m) => console.error(m) });
56306
- mkdirSync11(join39(root, "audits"), { recursive: true });
56307
- const bp = join39(root, "audits", "baseline.json");
56308
- writeFileSync14(bp, JSON.stringify(result, null, 2) + "\n");
56484
+ mkdirSync12(join40(root, "audits"), { recursive: true });
56485
+ const bp = join40(root, "audits", "baseline.json");
56486
+ writeFileSync15(bp, JSON.stringify(result, null, 2) + "\n");
56309
56487
  wrote.push(bp);
56310
56488
  }
56311
56489
  if (want.hook) wrote.push(writeHook(root, engineRel, failOn, legacy ? "baseline" : "staged"));
@@ -56315,6 +56493,18 @@ function cmdInit(p) {
56315
56493
  else console.log(`ultra11y init: done. The pre-commit gate audits staged changes and auto-applies safe fixes (bypass once with SKIP_A11Y=1).`);
56316
56494
  return 0;
56317
56495
  }
56496
+ async function cmdHook(p) {
56497
+ if (p.flags["claude-code"] !== true) {
56498
+ console.error("ultra11y hook: expected `hook --claude-code` (the payload is read on stdin).");
56499
+ return 2;
56500
+ }
56501
+ try {
56502
+ const decision = decide(JSON.parse(await readStdin()));
56503
+ if (decision) process.stdout.write(JSON.stringify(decision));
56504
+ } catch {
56505
+ }
56506
+ return 0;
56507
+ }
56318
56508
  function cmdCriteria(p) {
56319
56509
  if (p.flags.generate === true) {
56320
56510
  process.stdout.write(renderCriteriaReference());
@@ -56442,8 +56632,8 @@ async function cmdPrd(p) {
56442
56632
  return gh2 && gh2.failed > 0 ? 1 : 0;
56443
56633
  }
56444
56634
  function depsAt(root) {
56445
- const pkgPath = join39(root, "package.json");
56446
- if (!existsSync23(pkgPath)) return {};
56635
+ const pkgPath = join40(root, "package.json");
56636
+ if (!existsSync24(pkgPath)) return {};
56447
56637
  try {
56448
56638
  const pkg = JSON.parse(readText(pkgPath));
56449
56639
  return { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
@@ -56469,24 +56659,24 @@ function cmdRender(p) {
56469
56659
  console.error(`ultra11y render: --runner must be playwright|cypress|auto (got "${forced}").`);
56470
56660
  return 2;
56471
56661
  }
56472
- const detected = detectE2eRunner(depsAt(root), (f) => existsSync23(join39(root, f)));
56662
+ const detected = detectE2eRunner(depsAt(root), (f) => existsSync24(join40(root, f)));
56473
56663
  const runners = forced && forced !== "auto" ? [forced] : detected;
56474
56664
  if (!runners.length) {
56475
56665
  console.error(e2eSetupPlan([], {}, lang));
56476
56666
  return 1;
56477
56667
  }
56478
56668
  const engineRef = engineRefFor(root);
56479
- const dir = join39(root, ".ultra11y", "e2e");
56669
+ const dir = join40(root, ".ultra11y", "e2e");
56480
56670
  const paths = {};
56481
56671
  try {
56482
- mkdirSync11(dir, { recursive: true });
56672
+ mkdirSync12(dir, { recursive: true });
56483
56673
  if (runners.includes("playwright")) {
56484
- writeFileSync14(join39(dir, "playwright.mjs"), playwrightFixture(engineRef));
56674
+ writeFileSync15(join40(dir, "playwright.mjs"), playwrightFixture(engineRef));
56485
56675
  paths.playwright = ".ultra11y/e2e/playwright.mjs";
56486
56676
  }
56487
56677
  if (runners.includes("cypress")) {
56488
- writeFileSync14(join39(dir, "cypress-plugin.mjs"), cypressPlugin(engineRef));
56489
- writeFileSync14(join39(dir, "cypress-commands.mjs"), cypressCommands());
56678
+ writeFileSync15(join40(dir, "cypress-plugin.mjs"), cypressPlugin(engineRef));
56679
+ writeFileSync15(join40(dir, "cypress-commands.mjs"), cypressCommands());
56490
56680
  paths.cypressPlugin = ".ultra11y/e2e/cypress-plugin.mjs";
56491
56681
  paths.cypressCommands = ".ultra11y/e2e/cypress-commands.mjs";
56492
56682
  }
@@ -56501,7 +56691,7 @@ function cmdRender(p) {
56501
56691
  if (p.flags.scaffold === true) {
56502
56692
  const out2 = typeof p.flags.out === "string" && p.flags.out ? p.flags.out : "ultra11y-render.tsx";
56503
56693
  try {
56504
- writeFileSync14(out2, ssrHarness());
56694
+ writeFileSync15(out2, ssrHarness());
56505
56695
  } catch (e) {
56506
56696
  console.error(`ultra11y render: could not write ${out2}: ${e instanceof Error ? e.message : String(e)}`);
56507
56697
  return 1;
@@ -56515,29 +56705,29 @@ Fill in COMPONENTS, run it (e.g. npx tsx ${out2}), then: node scripts/ultra11y.m
56515
56705
  }
56516
56706
  if (p.flags.setup === true) {
56517
56707
  const rel2 = ".ultra11y/capture-setup.mjs";
56518
- const out2 = join39(root, rel2);
56708
+ const out2 = join40(root, rel2);
56519
56709
  try {
56520
- mkdirSync11(dirname11(out2), { recursive: true });
56521
- writeFileSync14(out2, captureSetup());
56710
+ mkdirSync12(dirname11(out2), { recursive: true });
56711
+ writeFileSync15(out2, captureSetup());
56522
56712
  } catch (e) {
56523
56713
  console.error(`ultra11y render: could not write ${out2}: ${e instanceof Error ? e.message : String(e)}`);
56524
56714
  return 1;
56525
56715
  }
56526
56716
  let setupDeps = {};
56527
- const setupPkg = join39(root, "package.json");
56528
- if (existsSync23(setupPkg)) {
56717
+ const setupPkg = join40(root, "package.json");
56718
+ if (existsSync24(setupPkg)) {
56529
56719
  try {
56530
56720
  const pkg = JSON.parse(readText(setupPkg));
56531
56721
  setupDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
56532
56722
  } catch {
56533
56723
  }
56534
56724
  }
56535
- const tr = detectTestRunner(setupDeps, (f) => existsSync23(join39(root, f)));
56725
+ const tr = detectTestRunner(setupDeps, (f) => existsSync24(join40(root, f)));
56536
56726
  console.log(captureSetupPlan(tr, rel2, lang));
56537
56727
  const gaLine = ".ultra11y/captures/*.html text eol=lf linguist-generated=true";
56538
- const gaPath = join39(root, ".gitattributes");
56728
+ const gaPath = join40(root, ".gitattributes");
56539
56729
  try {
56540
- const existing = existsSync23(gaPath) ? readFileSync20(gaPath, "utf8") : "";
56730
+ const existing = existsSync24(gaPath) ? readFileSync20(gaPath, "utf8") : "";
56541
56731
  if (!existing.includes(".ultra11y/captures/")) {
56542
56732
  appendFileSync(gaPath, (existing && !existing.endsWith("\n") ? "\n" : "") + gaLine + "\n");
56543
56733
  console.log(lang === "fr" ? `.gitattributes : ajout\xE9 \xAB ${gaLine} \xBB` : `.gitattributes: added "${gaLine}"`);
@@ -56545,8 +56735,8 @@ Fill in COMPONENTS, run it (e.g. npx tsx ${out2}), then: node scripts/ultra11y.m
56545
56735
  } catch {
56546
56736
  }
56547
56737
  try {
56548
- const giPath = join39(root, ".gitignore");
56549
- if (existsSync23(giPath) && /^\s*\/?\.ultra11y(\/\**)?\/?\s*$/m.test(readFileSync20(giPath, "utf8")))
56738
+ const giPath = join40(root, ".gitignore");
56739
+ if (existsSync24(giPath) && /^\s*\/?\.ultra11y(\/\**)?\/?\s*$/m.test(readFileSync20(giPath, "utf8")))
56550
56740
  console.error(
56551
56741
  lang === "fr" ? "\u26A0\uFE0F .ultra11y semble ignor\xE9 par .gitignore \u2014 les captures doivent \xEAtre committ\xE9es pour le gate (ajoutez \xAB !.ultra11y/captures/ \xBB)." : '\u26A0\uFE0F .ultra11y appears gitignored \u2014 captures must be committed for the gate (add "!.ultra11y/captures/").'
56552
56742
  );
@@ -56556,8 +56746,8 @@ Fill in COMPONENTS, run it (e.g. npx tsx ${out2}), then: node scripts/ultra11y.m
56556
56746
  }
56557
56747
  if (p.flags.storybook === true || typeof p.flags.storybook === "string") {
56558
56748
  const sbDir = p.positionals[0] ?? "storybook-static";
56559
- const indexPath = existsSync23(join39(sbDir, "index.json")) ? join39(sbDir, "index.json") : join39(sbDir, "stories.json");
56560
- if (!existsSync23(indexPath)) {
56749
+ const indexPath = existsSync24(join40(sbDir, "index.json")) ? join40(sbDir, "index.json") : join40(sbDir, "stories.json");
56750
+ if (!existsSync24(indexPath)) {
56561
56751
  console.error(
56562
56752
  lang === "fr" ? `ultra11y render : aucun index Storybook (index.json/stories.json) dans ${sbDir}.` : `ultra11y render: no Storybook index (index.json/stories.json) in ${sbDir}.`
56563
56753
  );
@@ -56567,7 +56757,7 @@ Fill in COMPONENTS, run it (e.g. npx tsx ${out2}), then: node scripts/ultra11y.m
56567
56757
  const provById = new Map(stories.map((s) => [s.id, storyProvenance(s)]));
56568
56758
  const capturesFlag = typeof p.flags.captures === "string" && p.flags.captures ? p.flags.captures : void 0;
56569
56759
  const htmlDir = capturesFlag ?? sbDir;
56570
- const htmlFiles = existsSync23(htmlDir) ? discover([htmlDir]).files.filter((f) => /\.html?$/i.test(f)) : [];
56760
+ const htmlFiles = existsSync24(htmlDir) ? discover([htmlDir]).files.filter((f) => /\.html?$/i.test(f)) : [];
56571
56761
  const outDir = ".ultra11y/captures";
56572
56762
  let attributed = 0;
56573
56763
  let skipped = 0;
@@ -56589,8 +56779,8 @@ Fill in COMPONENTS, run it (e.g. npx tsx ${out2}), then: node scripts/ultra11y.m
56589
56779
  continue;
56590
56780
  }
56591
56781
  try {
56592
- mkdirSync11(outDir, { recursive: true });
56593
- writeFileSync14(join39(outDir, `${hitId}.html`), `${formatCaptureComment(prov)}
56782
+ mkdirSync12(outDir, { recursive: true });
56783
+ writeFileSync15(join40(outDir, `${hitId}.html`), `${formatCaptureComment(prov)}
56594
56784
  ${raw}${raw.endsWith("\n") ? "" : "\n"}`);
56595
56785
  attributed++;
56596
56786
  } catch {
@@ -56612,11 +56802,11 @@ ${raw}${raw.endsWith("\n") ? "" : "\n"}`);
56612
56802
  }
56613
56803
  if (p.flags.coverage === true) {
56614
56804
  const capturesFlag = typeof p.flags.captures === "string" && p.flags.captures ? p.flags.captures : void 0;
56615
- const capturesDir = capturesFlag ?? join39(root, ".ultra11y/captures");
56805
+ const capturesDir = capturesFlag ?? join40(root, ".ultra11y/captures");
56616
56806
  const graphExt = [...GRAPH_ONLY_EXT, ...asList(p.flags.ext) ?? []];
56617
56807
  const sourceFiles = discover([root], { include: asList(p.flags.include), exclude: asList(p.flags.exclude), ext: graphExt }).files;
56618
56808
  const graph = buildGraphStreaming(sourceFiles);
56619
- const capFiles = existsSync23(capturesDir) ? discover([capturesDir]).files : [];
56809
+ const capFiles = existsSync24(capturesDir) ? discover([capturesDir]).files : [];
56620
56810
  const entries = capFiles.map((f) => ({ file: toPosix(f), provenance: parseCaptureProvenance(readText(f)) }));
56621
56811
  const cov = computeCaptureCoverage(graph, entries);
56622
56812
  if (p.flags.json) console.log(JSON.stringify(cov, null, 2));
@@ -56624,15 +56814,15 @@ ${raw}${raw.endsWith("\n") ? "" : "\n"}`);
56624
56814
  return 0;
56625
56815
  }
56626
56816
  let deps = {};
56627
- const pkgPath = join39(root, "package.json");
56628
- if (existsSync23(pkgPath)) {
56817
+ const pkgPath = join40(root, "package.json");
56818
+ if (existsSync24(pkgPath)) {
56629
56819
  try {
56630
56820
  const pkg = JSON.parse(readText(pkgPath));
56631
56821
  deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
56632
56822
  } catch {
56633
56823
  }
56634
56824
  }
56635
- const detection = detectFrameworks(deps, (f) => existsSync23(join39(root, f)));
56825
+ const detection = detectFrameworks(deps, (f) => existsSync24(join40(root, f)));
56636
56826
  if (p.flags.json) console.log(JSON.stringify(detection, null, 2));
56637
56827
  else console.log(renderPlan(detection, lang));
56638
56828
  return 0;
@@ -56831,9 +57021,9 @@ function applyAdjudicationFile(p, adj, lang) {
56831
57021
  return 1;
56832
57022
  }
56833
57023
  const out2 = typeof p.flags.out === "string" ? p.flags.out : ".";
56834
- mkdirSync11(out2, { recursive: true });
56835
- const auditPath = join39(out2, "audit-latest.json");
56836
- writeFileSync14(auditPath, JSON.stringify(r.audit, null, 2) + "\n");
57024
+ mkdirSync12(out2, { recursive: true });
57025
+ const auditPath = join40(out2, "audit-latest.json");
57026
+ writeFileSync15(auditPath, JSON.stringify(r.audit, null, 2) + "\n");
56837
57027
  if (p.flags.json) console.log(JSON.stringify({ ok: true, auditPath, applied: r.applied, stillManual: r.stillManual, grounding: r.grounding }, null, 2));
56838
57028
  else
56839
57029
  console.log(
@@ -56903,7 +57093,7 @@ async function cmdScan(p) {
56903
57093
  return 0;
56904
57094
  }
56905
57095
  for (const target of p.positionals.filter((a) => a !== "-")) {
56906
- if (/^https?:\/\//i.test(target) || existsSync23(target)) continue;
57096
+ if (/^https?:\/\//i.test(target) || existsSync24(target)) continue;
56907
57097
  console.error(
56908
57098
  lang === "fr" ? `ultra11y scan : fichier introuvable (file not found) : ${target}. Passez une URL http(s):// ou un fichier HTML existant.` : `ultra11y scan: File not found: ${target}. Pass an http(s):// URL or an existing HTML file.`
56909
57099
  );
@@ -57028,12 +57218,12 @@ async function cmdScan(p) {
57028
57218
  audit2 = parsed;
57029
57219
  }
57030
57220
  const merged = mergeDynamic(audit2, dynamic, lang);
57031
- mkdirSync11(out2, { recursive: true });
57032
- writeFileSync14(join39(out2, "audit-latest.json"), JSON.stringify(merged, null, 2) + "\n");
57221
+ mkdirSync12(out2, { recursive: true });
57222
+ writeFileSync15(join40(out2, "audit-latest.json"), JSON.stringify(merged, null, 2) + "\n");
57033
57223
  if (p.flags.json) console.log(JSON.stringify(merged, null, 2));
57034
57224
  else
57035
57225
  console.log(
57036
- lang === "fr" ? `Audit statique + dynamique fusionn\xE9 \u2192 ${join39(out2, "audit-latest.json")} (${merged.conformancePct}% r\xE9ussite, ${merged.findings.length} findings).` : `Static + dynamic audit merged \u2192 ${join39(out2, "audit-latest.json")} (${merged.conformancePct}% pass rate, ${merged.findings.length} findings).`
57226
+ lang === "fr" ? `Audit statique + dynamique fusionn\xE9 \u2192 ${join40(out2, "audit-latest.json")} (${merged.conformancePct}% r\xE9ussite, ${merged.findings.length} findings).` : `Static + dynamic audit merged \u2192 ${join40(out2, "audit-latest.json")} (${merged.conformancePct}% pass rate, ${merged.findings.length} findings).`
57037
57227
  );
57038
57228
  return 0;
57039
57229
  }
@@ -57213,7 +57403,7 @@ function cmdOrchestrate(p) {
57213
57403
  }
57214
57404
  const engineAbs = realpathSync4(fileURLToPath5(import.meta.url));
57215
57405
  if (p.flags.list === true) {
57216
- if (!existsSync23(runFlag)) {
57406
+ if (!existsSync24(runFlag)) {
57217
57407
  console.error(`ultra11y orchestrate: run dir not found: ${runFlag}.`);
57218
57408
  return 2;
57219
57409
  }
@@ -57240,7 +57430,7 @@ function cmdOrchestrate(p) {
57240
57430
  );
57241
57431
  } else {
57242
57432
  console.log(
57243
- lang === "fr" ? `Suivez ${join39(runFlag, "orchestration", "RUNBOOK.md")} s\xE9quentiellement (chemin \xE9co).` : `Follow ${join39(runFlag, "orchestration", "RUNBOOK.md")} sequentially (the eco path).`
57433
+ lang === "fr" ? `Suivez ${join40(runFlag, "orchestration", "RUNBOOK.md")} s\xE9quentiellement (chemin \xE9co).` : `Follow ${join40(runFlag, "orchestration", "RUNBOOK.md")} sequentially (the eco path).`
57244
57434
  );
57245
57435
  }
57246
57436
  if (p.flags.phase === void 0 && workflows.length === 0 && p.flags.eco !== true) {
@@ -57324,6 +57514,8 @@ async function main(argv) {
57324
57514
  return cmdOrchestrate(p);
57325
57515
  case "mcp":
57326
57516
  return cmdMcp(p);
57517
+ case "hook":
57518
+ return cmdHook(p);
57327
57519
  default:
57328
57520
  console.error(`ultra11y: "${p.command}" is not implemented yet`);
57329
57521
  return 1;