ultra11y 2.31.3 → 2.32.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,7 @@ import { join as join45, relative as relative5, sep as sep7, dirname as dirname1
6
6
  import { fileURLToPath as fileURLToPath5, pathToFileURL as pathToFileURL3 } from "url";
7
7
 
8
8
  // src/types.ts
9
- var VERSION = "2.31.3";
9
+ var VERSION = "2.32.1";
10
10
  var SCHEMA_VERSION = 2;
11
11
 
12
12
  // src/audit.ts
@@ -49270,6 +49270,12 @@ var L3 = {
49270
49270
  source: "source"
49271
49271
  }
49272
49272
  };
49273
+ function pageBasisWarning(basis, lang) {
49274
+ return basis === "snapshot" ? void 0 : L3[lang].basisNote;
49275
+ }
49276
+ function unattributedNote(n, lang) {
49277
+ return L3[lang].unattributed(n);
49278
+ }
49273
49279
  function pageView(result, page) {
49274
49280
  return {
49275
49281
  ...result,
@@ -49637,22 +49643,9 @@ function writeReport(r, opts) {
49637
49643
  return path;
49638
49644
  }
49639
49645
 
49640
- // src/gh.ts
49646
+ // src/gh-cli.ts
49641
49647
  import { execFileSync as execFileSync3 } from "child_process";
49642
- var SEV_ORDER4 = ["bloquant", "majeur", "mineur"];
49643
- var SEV_RANK2 = { bloquant: 0, majeur: 1, mineur: 2 };
49644
- var ICON4 = { bloquant: "\u{1F534}", majeur: "\u{1F7E0}", mineur: "\u{1F7E1}" };
49645
- var SEV_LABEL2 = {
49646
- fr: { bloquant: "Bloquant", majeur: "Majeur", mineur: "Mineur" },
49647
- en: { bloquant: "Blocking", majeur: "Major", mineur: "Minor" }
49648
- };
49649
- function standardTag(standard) {
49650
- return isCore(standard) ? { label: "WCAG", tag: "wcag" } : (() => {
49651
- const p = loadPack(standard);
49652
- return { label: p.name, tag: p.key };
49653
- })();
49654
- }
49655
- function gh(args2, input) {
49648
+ function ghExec(args2, input) {
49656
49649
  return execFileSync3("gh", args2, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], ...input !== void 0 ? { input } : {} });
49657
49650
  }
49658
49651
  function ghErrorReason(err2) {
@@ -49672,22 +49665,115 @@ function ghAvailable() {
49672
49665
  return false;
49673
49666
  }
49674
49667
  }
49675
- var RECOMMENDATION_SUFFIX = " (recommendation)";
49676
- function issueTitle(unit, label = "WCAG") {
49677
- return `[a11y] ${label} ${unit.criteriaId} \u2014 ${unit.title}${unit.advisory ? RECOMMENDATION_SUFFIX : ""}`;
49668
+
49669
+ // src/pr-comment.ts
49670
+ function COMMENT_MARKER(standard) {
49671
+ return `<!-- ultra11y:report standard="${standard}" -->`;
49672
+ }
49673
+ function stickyBody(markdown, standard) {
49674
+ return `${COMMENT_MARKER(standard)}
49675
+ ${markdown}`;
49678
49676
  }
49679
- function existingIssueTitles() {
49677
+ function prNumberFromEnv(env = process.env) {
49678
+ const explicit = Number.parseInt(env.ULTRA11Y_PR ?? "", 10);
49679
+ if (Number.isFinite(explicit) && explicit > 0) return explicit;
49680
+ const m = /^refs\/pull\/(\d+)\//.exec(env.GITHUB_REF ?? "");
49681
+ const n = m ? Number.parseInt(m[1], 10) : Number.NaN;
49682
+ return Number.isFinite(n) && n > 0 ? n : void 0;
49683
+ }
49684
+ function pickExistingComment(comments, marker) {
49685
+ return comments.find((c2) => typeof c2.body === "string" && c2.body.includes(marker));
49686
+ }
49687
+ function pushPrComment(markdown, standard = "wcag") {
49688
+ const pr = prNumberFromEnv();
49689
+ if (pr === void 0) return { ok: true, action: "skipped", reason: "not a pull-request run" };
49690
+ const marker = COMMENT_MARKER(standard);
49691
+ const body2 = stickyBody(markdown, standard);
49680
49692
  try {
49681
- const raw = gh(["issue", "list", "--state", "all", "--limit", "1000", "--json", "title"]);
49682
- const arr = JSON.parse(raw);
49683
- return new Set(arr.map((i2) => i2.title ?? "").filter(Boolean));
49684
- } catch {
49685
- return /* @__PURE__ */ new Set();
49693
+ const repo = ghExec(["repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"]).trim();
49694
+ let existing;
49695
+ try {
49696
+ const raw = ghExec(["api", `repos/${repo}/issues/${pr}/comments`, "--paginate"]);
49697
+ existing = pickExistingComment(JSON.parse(raw), marker);
49698
+ } catch {
49699
+ }
49700
+ if (existing) {
49701
+ ghExec(["api", "--method", "PATCH", `repos/${repo}/issues/comments/${existing.id}`, "-f", `body=${body2}`]);
49702
+ return { ok: true, action: "updated" };
49703
+ }
49704
+ ghExec(["api", "--method", "POST", `repos/${repo}/issues/${pr}/comments`, "-f", `body=${body2}`]);
49705
+ return { ok: true, action: "created" };
49706
+ } catch (e) {
49707
+ return { ok: false, action: "skipped", reason: ghErrorReason(e) };
49686
49708
  }
49687
49709
  }
49688
- function issueBody(unit, lang, standard = "wcag", format = "audit") {
49689
- if (format === "audit") return renderAuditorUnit(unit, standard, lang).join("\n").trimEnd();
49690
- const t2 = lang === "fr" ? { fix: "Correction", occ: "Occurrence(s)", def: "\u21B3 d\xE9finition" } : { fix: "Fix", occ: "Occurrence(s)", def: "\u21B3 definition" };
49710
+
49711
+ // src/tickets/types.ts
49712
+ var ALL_PROVIDERS = ["github", "gitlab", "jira"];
49713
+ var ALL_GRAINS = ["criterion", "page", "page-criterion", "single", "file"];
49714
+ var UNATTRIBUTED_ID = "unattributed";
49715
+ var TICKET_SET_SCHEMA_VERSION = 1;
49716
+
49717
+ // src/tickets/render.ts
49718
+ var RECOMMENDATION_SUFFIX = " (recommendation)";
49719
+ var AUDIT_FRAME = "Accessibility audit";
49720
+ function standardTag(standard) {
49721
+ return isCore(standard) ? { label: "WCAG", tag: "wcag" } : (() => {
49722
+ const p = loadPack(standard);
49723
+ return { label: p.name, tag: p.key };
49724
+ })();
49725
+ }
49726
+ function criterionTitle(unit, label = "WCAG") {
49727
+ return `[a11y] ${label} ${unit.criteriaId} \u2014 ${unit.title}${unit.advisory ? RECOMMENDATION_SUFFIX : ""}`;
49728
+ }
49729
+ function singleTitle(label = "WCAG") {
49730
+ return `[a11y] ${label} \u2014 ${AUDIT_FRAME}`;
49731
+ }
49732
+ function pageTitle(pageId, label = "WCAG") {
49733
+ return `[a11y] ${label} [page:${pageId}] \u2014 ${AUDIT_FRAME}`;
49734
+ }
49735
+ function pageCriterionTitle(pageId, unit, label = "WCAG") {
49736
+ return `[a11y] ${label} [page:${pageId}] ${unit.criteriaId} \u2014 ${unit.title}${unit.advisory ? RECOMMENDATION_SUFFIX : ""}`;
49737
+ }
49738
+ function fileTitle(file, label = "WCAG") {
49739
+ return `[a11y] ${label} [file:${file}] \u2014 ${AUDIT_FRAME}`;
49740
+ }
49741
+ function unattributedTitle(label = "WCAG") {
49742
+ return `[a11y] ${label} [${UNATTRIBUTED_ID}] \u2014 ${AUDIT_FRAME}`;
49743
+ }
49744
+ function labelsFor(severity, advisory, tag) {
49745
+ return advisory ? ["accessibility", tag, "recommendation", severity] : ["accessibility", tag, severity];
49746
+ }
49747
+ var L5 = {
49748
+ fr: {
49749
+ fix: "Correction",
49750
+ occ: "Occurrence(s)",
49751
+ def: "\u21B3 d\xE9finition",
49752
+ page: "Page",
49753
+ url: "URL",
49754
+ auth: "Authentification requise",
49755
+ yes: "oui",
49756
+ file: "Fichier",
49757
+ fileNote: "Ticket regroup\xE9 par fichier source. Un fichier n'est pas une page : il ne porte aucun taux de conformit\xE9 \u2014 seul le p\xE9rim\xE8tre global et la grille par page en portent un.",
49758
+ truncated: (path) => `_\u2026 corps tronqu\xE9 : le ticket d\xE9passait la limite du tracker. Le d\xE9tail complet est dans \`${path}\`._`,
49759
+ prdPath: "audits/prd-<date>.md"
49760
+ },
49761
+ en: {
49762
+ fix: "Fix",
49763
+ occ: "Occurrence(s)",
49764
+ def: "\u21B3 definition",
49765
+ page: "Page",
49766
+ url: "URL",
49767
+ auth: "Authentication required",
49768
+ yes: "yes",
49769
+ file: "File",
49770
+ fileNote: "Ticket grouped by source file. A file is not a page: it carries no conformance rate \u2014 only the overall scope and the per-page grid do.",
49771
+ truncated: (path) => `_\u2026 body truncated: the ticket exceeded the tracker's limit. The full detail is in \`${path}\`._`,
49772
+ prdPath: "audits/prd-<date>.md"
49773
+ }
49774
+ };
49775
+ function renderRemediationBody(unit, lang) {
49776
+ const t2 = L5[lang];
49691
49777
  const lines = [];
49692
49778
  if (unit.refs.length) lines.push(`**WCAG** : ${unit.refs.join(", ")}`, "");
49693
49779
  for (const fx of [...new Set(unit.findings.map((f) => resolveRemediation(f, lang)))]) lines.push(`**${t2.fix}** : ${fx}`);
@@ -49698,122 +49784,662 @@ function issueBody(unit, lang, standard = "wcag", format = "audit") {
49698
49784
  }
49699
49785
  return lines.join("\n");
49700
49786
  }
49701
- function createIssue(title2, body2, labels) {
49702
- const base = ["issue", "create", "--title", title2, "--body-file", "-"];
49703
- try {
49704
- gh([...base, "--label", labels.join(",")], body2);
49705
- return { ok: true };
49706
- } catch (labelledErr) {
49707
- try {
49708
- gh(base, body2);
49709
- return { ok: true };
49710
- } catch (err2) {
49711
- return { ok: false, reason: ghErrorReason(err2) ?? ghErrorReason(labelledErr) };
49787
+ function renderCriterionBody(unit, standard, lang, opts = {}) {
49788
+ if (opts.format === "remediation") return renderRemediationBody(unit, lang);
49789
+ return renderAuditorUnit(unit, standard, lang, { ...opts.technical !== void 0 ? { technical: opts.technical } : {} }).join("\n").trimEnd();
49790
+ }
49791
+ function pagePreamble(page, lang, basisWarning) {
49792
+ const t2 = L5[lang];
49793
+ const out2 = [`**${t2.page}** : ${page.name}`, `**${t2.url}** : ${page.url}`];
49794
+ if (page.auth) out2.push(`**${t2.auth}** : ${t2.yes}`);
49795
+ out2.push("");
49796
+ if (basisWarning) out2.push(`> \u26A0\uFE0F ${basisWarning}`, "");
49797
+ return out2;
49798
+ }
49799
+ function filePreamble(file, lang) {
49800
+ const t2 = L5[lang];
49801
+ return [`**${t2.file}** : \`${file}\``, "", `> ${t2.fileNote}`, ""];
49802
+ }
49803
+ function clampBody(body2, limit, lang) {
49804
+ if (body2.length <= limit) return body2;
49805
+ const notice = `
49806
+
49807
+ ${L5[lang].truncated(L5[lang].prdPath)}`;
49808
+ const budget = limit - notice.length;
49809
+ const head = body2.slice(0, Math.max(0, budget));
49810
+ const cut = head.lastIndexOf("\n## ");
49811
+ return `${(cut > 0 ? head.slice(0, cut) : head).trimEnd()}${notice}`;
49812
+ }
49813
+
49814
+ // src/tickets/grain.ts
49815
+ var SEV_RANK2 = { bloquant: 0, majeur: 1, mineur: 2 };
49816
+ function worstOf(items) {
49817
+ return items.reduce((worst, x) => SEV_RANK2[x.severity] < SEV_RANK2[worst] ? x.severity : worst, "mineur");
49818
+ }
49819
+ function fileView(result, file, baseDir) {
49820
+ const owns = (f) => sourceOf(f, baseDir) === file;
49821
+ return {
49822
+ ...result,
49823
+ findings: result.findings.filter(owns),
49824
+ ...result.packFindings ? { packFindings: result.packFindings.filter(owns) } : {}
49825
+ };
49826
+ }
49827
+ function occurrencesOf(units, lang) {
49828
+ return units.flatMap((u) => u.findings.map((f) => ({ file: f.file, line: f.line, selector: f.selectorHint, message: resolveMessage(f, lang) })));
49829
+ }
49830
+ function sourceOf(f, baseDir) {
49831
+ return repoRelative(f.origin?.sourceFile ?? f.file, baseDir);
49832
+ }
49833
+ function buildTickets(result, opts) {
49834
+ const { grain, standard, lang } = opts;
49835
+ const { label, tag } = standardTag(standard);
49836
+ const limit = opts.bodyLimit ?? Number.POSITIVE_INFINITY;
49837
+ const bodyOpts = {
49838
+ ...opts.format !== void 0 ? { format: opts.format } : {},
49839
+ ...opts.technical !== void 0 ? { technical: opts.technical } : {}
49840
+ };
49841
+ const backlogOpts = opts.technical !== void 0 ? { technical: opts.technical } : {};
49842
+ const baseDir = opts.baseDir ?? "";
49843
+ const clamp2 = (body2) => clampBody(body2, limit, lang);
49844
+ const ticketFromUnit = (unit, title2, scope2) => ({
49845
+ title: title2,
49846
+ body: clamp2(renderCriterionBody(unit, standard, lang, bodyOpts)),
49847
+ labels: labelsFor(unit.severity, unit.advisory === true, tag),
49848
+ severity: unit.severity,
49849
+ advisory: unit.advisory === true,
49850
+ scope: scope2,
49851
+ occurrences: occurrencesOf([unit], lang)
49852
+ });
49853
+ if (grain === "criterion") {
49854
+ const tickets2 = prdUnits(result, standard, lang).map((u) => ticketFromUnit(u, criterionTitle(u, label), { grain: "criterion", criteriaId: u.criteriaId }));
49855
+ return { tickets: tickets2, unattributed: 0 };
49856
+ }
49857
+ if (grain === "single") {
49858
+ const units = prdUnits(result, standard, lang);
49859
+ if (!units.length) return { tickets: [], unattributed: 0 };
49860
+ return {
49861
+ tickets: [
49862
+ {
49863
+ title: singleTitle(label),
49864
+ // renderAuditorBacklog — not a second template. It also fixes a real inconsistency
49865
+ // in the pre-v3 consolidated body, which sectioned advisory units AMONG the
49866
+ // severity groups instead of isolating them in their own trailing section.
49867
+ body: clamp2(renderAuditorBacklog(result, lang, standard, backlogOpts)),
49868
+ labels: labelsFor(
49869
+ worstOf(units),
49870
+ units.every((u) => u.advisory === true),
49871
+ tag
49872
+ ),
49873
+ severity: worstOf(units),
49874
+ advisory: units.every((u) => u.advisory === true),
49875
+ scope: { grain: "single" },
49876
+ occurrences: occurrencesOf(units, lang)
49877
+ }
49878
+ ],
49879
+ unattributed: 0
49880
+ };
49881
+ }
49882
+ if (grain === "file") {
49883
+ const files = [...new Set(result.findings.map((f) => sourceOf(f, baseDir)))].sort();
49884
+ const tickets2 = [];
49885
+ for (const file of files) {
49886
+ const view = fileView(result, file, baseDir);
49887
+ const units = prdUnits(view, standard, lang);
49888
+ if (!units.length) continue;
49889
+ const advisory = units.every((u) => u.advisory === true);
49890
+ tickets2.push({
49891
+ title: fileTitle(file, label),
49892
+ body: clamp2([...filePreamble(file, lang), renderAuditorBacklog(view, lang, standard, backlogOpts)].join("\n")),
49893
+ labels: labelsFor(worstOf(units), advisory, tag),
49894
+ severity: worstOf(units),
49895
+ advisory,
49896
+ scope: { grain: "file", file },
49897
+ occurrences: occurrencesOf(units, lang)
49898
+ });
49899
+ }
49900
+ return { tickets: tickets2, unattributed: 0 };
49901
+ }
49902
+ const scope = pagesOf(result);
49903
+ if (!scope.length) return { tickets: [], unattributed: 0, error: "no-pages" };
49904
+ attributePages(result, scope);
49905
+ const derived = derivePages(result, scope);
49906
+ const orphans = unattributedFindings(result);
49907
+ const tickets = [];
49908
+ for (const page of derived) {
49909
+ const view = pageView(result, page);
49910
+ const units = prdUnits(view, standard, lang);
49911
+ if (!units.length) continue;
49912
+ const warning = pageBasisWarning(page.basis, lang);
49913
+ const preamble = pagePreamble(page, lang, warning);
49914
+ if (grain === "page") {
49915
+ const advisory = units.every((u) => u.advisory === true);
49916
+ tickets.push({
49917
+ title: pageTitle(page.id, label),
49918
+ body: clamp2([...preamble, renderAuditorBacklog(view, lang, standard, backlogOpts)].join("\n")),
49919
+ labels: labelsFor(worstOf(units), advisory, tag),
49920
+ severity: worstOf(units),
49921
+ advisory,
49922
+ scope: pageScopeOf(page),
49923
+ occurrences: occurrencesOf(units, lang)
49924
+ });
49925
+ continue;
49926
+ }
49927
+ for (const u of units) {
49928
+ const t2 = ticketFromUnit(u, pageCriterionTitle(page.id, u, label), {
49929
+ grain: "page-criterion",
49930
+ pageId: page.id,
49931
+ pageName: page.name,
49932
+ url: page.url,
49933
+ ...page.auth !== void 0 ? { auth: page.auth } : {},
49934
+ basis: page.basis,
49935
+ criteriaId: u.criteriaId
49936
+ });
49937
+ tickets.push({ ...t2, body: clamp2([...preamble, t2.body].join("\n")) });
49938
+ }
49939
+ }
49940
+ if (orphans.length) {
49941
+ const view = { ...result, findings: orphans, ...result.packFindings ? { packFindings: result.packFindings.filter((f) => !f.page) } : {} };
49942
+ const units = prdUnits(view, standard, lang);
49943
+ if (units.length) {
49944
+ const advisory = units.every((u) => u.advisory === true);
49945
+ tickets.push({
49946
+ title: unattributedTitle(label),
49947
+ body: clamp2([`> ${unattributedNote(orphans.length, lang)}`, "", renderAuditorBacklog(view, lang, standard, backlogOpts)].join("\n")),
49948
+ labels: labelsFor(worstOf(units), advisory, tag),
49949
+ severity: worstOf(units),
49950
+ advisory,
49951
+ scope: { grain: "page", pageId: UNATTRIBUTED_ID, pageName: UNATTRIBUTED_ID, url: "", basis: "none" },
49952
+ occurrences: occurrencesOf(units, lang)
49953
+ });
49712
49954
  }
49713
49955
  }
49956
+ return { tickets, unattributed: orphans.length };
49957
+ }
49958
+ function pageScopeOf(page) {
49959
+ return {
49960
+ grain: "page",
49961
+ pageId: page.id,
49962
+ pageName: page.name,
49963
+ url: page.url,
49964
+ ...page.auth !== void 0 ? { auth: page.auth } : {},
49965
+ basis: page.basis
49966
+ };
49967
+ }
49968
+
49969
+ // src/tickets/push.ts
49970
+ function planPush(tickets, existing) {
49971
+ const seen = new Set(existing.map((e) => e.title));
49972
+ return tickets.map((ticket) => {
49973
+ if (seen.has(ticket.title)) return { ticket, action: "skip" };
49974
+ seen.add(ticket.title);
49975
+ return { ticket, action: "create" };
49976
+ });
49977
+ }
49978
+ function emptyResult() {
49979
+ return { created: 0, skipped: 0, failed: 0, createdTitles: [], createdUrls: [], errors: [] };
49714
49980
  }
49715
49981
  function recordFailure(result, reason) {
49716
49982
  result.failed++;
49717
49983
  if (reason && !result.errors.includes(reason)) result.errors.push(reason);
49718
49984
  }
49719
- function pushIssues(units, lang, standard = "wcag", format = "audit") {
49720
- const { label, tag } = standardTag(standard);
49721
- const existing = existingIssueTitles();
49722
- const result = { created: 0, skipped: 0, failed: 0, createdTitles: [], errors: [] };
49723
- for (const u of units) {
49724
- const title2 = issueTitle(u, label);
49725
- if (existing.has(title2)) {
49985
+ async function pushTickets(tickets, provider, opts = {}) {
49986
+ const result = emptyResult();
49987
+ if (!tickets.length) return { plan: [], result, dedupeChecked: false };
49988
+ let existing = [];
49989
+ let dedupeChecked = false;
49990
+ try {
49991
+ existing = await provider.list();
49992
+ dedupeChecked = true;
49993
+ } catch {
49994
+ dedupeChecked = false;
49995
+ }
49996
+ const plan = planPush(tickets, existing);
49997
+ if (opts.dryRun) {
49998
+ result.skipped = plan.filter((p) => p.action === "skip").length;
49999
+ return { plan, result, dedupeChecked };
50000
+ }
50001
+ for (const { ticket, action } of plan) {
50002
+ if (action === "skip") {
49726
50003
  result.skipped++;
49727
50004
  continue;
49728
50005
  }
49729
- const labels = u.advisory ? ["accessibility", tag, "recommendation", u.severity] : ["accessibility", tag, u.severity];
49730
- const r = createIssue(title2, issueBody(u, lang, standard, format), labels);
49731
- if (r.ok) {
50006
+ let outcome2;
50007
+ try {
50008
+ outcome2 = await provider.create(ticket);
50009
+ } catch (e) {
50010
+ outcome2 = { ok: false, reason: e instanceof Error ? e.message.split("\n")[0] : String(e) };
50011
+ }
50012
+ if (outcome2.ok) {
49732
50013
  result.created++;
49733
- result.createdTitles.push(title2);
49734
- existing.add(title2);
50014
+ result.createdTitles.push(ticket.title);
50015
+ if (outcome2.url) result.createdUrls.push(outcome2.url);
49735
50016
  } else {
49736
- recordFailure(result, r.reason);
50017
+ recordFailure(result, outcome2.reason);
49737
50018
  }
49738
50019
  }
49739
- return result;
50020
+ return { plan, result, dedupeChecked };
49740
50021
  }
49741
- function singleIssueTitle(label = "WCAG") {
49742
- return `[a11y] ${label} \u2014 Accessibility audit`;
50022
+
50023
+ // src/tickets/http.ts
50024
+ var USER_AGENT = `ultra11y/${VERSION}`;
50025
+ var SECRET_HEADERS = ["authorization", "private-token", "x-atlassian-token"];
50026
+ var HttpError = class extends Error {
50027
+ constructor(status, detail, url) {
50028
+ super(`HTTP ${status}: ${detail}`);
50029
+ this.status = status;
50030
+ this.detail = detail;
50031
+ this.url = url;
50032
+ this.name = "HttpError";
50033
+ }
50034
+ status;
50035
+ detail;
50036
+ url;
50037
+ };
50038
+ var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
50039
+ function scrub(text, headers) {
50040
+ let out2 = text;
50041
+ for (const [k, v] of Object.entries(headers)) {
50042
+ if (v && SECRET_HEADERS.includes(k.toLowerCase())) out2 = out2.split(v).join("<redacted>");
50043
+ }
50044
+ return out2;
49743
50045
  }
49744
- function singleIssueBody(units, lang, standard = "wcag", format = "audit") {
49745
- const { label } = standardTag(standard);
49746
- const intro = lang === "fr" ? `Audit d'accessibilit\xE9 ${label} \u2014 ${units.length} crit\xE8re(s) non conforme(s).` : `${label} accessibility audit \u2014 ${units.length} non-conforming criteria.`;
49747
- const lines = [intro, ""];
49748
- for (const sev of SEV_ORDER4) {
49749
- const group = units.filter((u) => u.severity === sev);
49750
- if (!group.length) continue;
49751
- lines.push(`## ${ICON4[sev]} ${SEV_LABEL2[lang][sev]} (${group.length})`, "");
49752
- for (const u of group) {
49753
- if (format === "audit") lines.push(...renderAuditorUnit(u, standard, lang, { heading: "###" }));
49754
- else lines.push(`### ${u.label}`, "", issueBody(u, lang, standard, format), "");
50046
+ function reasonFrom(detail) {
50047
+ try {
50048
+ const j = JSON.parse(detail);
50049
+ if (Array.isArray(j.errorMessages) && typeof j.errorMessages[0] === "string") return j.errorMessages[0];
50050
+ if (j.errors && typeof j.errors === "object" && !Array.isArray(j.errors)) {
50051
+ const first = Object.entries(j.errors)[0];
50052
+ if (first) return `${first[0]}: ${String(first[1])}`;
50053
+ }
50054
+ if (typeof j.message === "string") return j.message;
50055
+ if (typeof j.error === "string") return j.error;
50056
+ } catch {
50057
+ }
50058
+ return detail.split("\n")[0]?.slice(0, 300) ?? detail.slice(0, 300);
50059
+ }
50060
+ async function requestJson(url, opts = {}) {
50061
+ const doFetch = opts.fetchImpl ?? fetch;
50062
+ const sleep2 = opts.sleep ?? defaultSleep;
50063
+ const retries = opts.retries ?? 2;
50064
+ const headers = {
50065
+ Accept: "application/json",
50066
+ "User-Agent": USER_AGENT,
50067
+ // GitHub REJECTS a request without one.
50068
+ ...opts.body !== void 0 ? { "Content-Type": "application/json" } : {},
50069
+ ...opts.headers
50070
+ };
50071
+ for (let attempt = 0; ; attempt++) {
50072
+ const res = await doFetch(url, {
50073
+ method: opts.method ?? "GET",
50074
+ headers,
50075
+ ...opts.body !== void 0 ? { body: JSON.stringify(opts.body) } : {}
50076
+ });
50077
+ if (res.ok) {
50078
+ const text = await res.text();
50079
+ return { data: text ? JSON.parse(text) : {}, headers: res.headers };
49755
50080
  }
50081
+ const detail = scrub(await res.text().catch(() => ""), headers);
50082
+ const retryable = res.status === 429 || res.status === 503;
50083
+ if (!retryable || attempt >= retries) throw new HttpError(res.status, detail, url);
50084
+ const after = Number.parseInt(res.headers.get("retry-after") ?? "", 10);
50085
+ await sleep2(Math.min(Number.isFinite(after) ? after * 1e3 : 1e3 * 2 ** attempt, 3e4));
49756
50086
  }
49757
- return lines.join("\n");
49758
50087
  }
49759
- function pushSingleIssue(units, lang, standard = "wcag", format = "audit") {
49760
- const { label, tag } = standardTag(standard);
49761
- const result = { created: 0, skipped: 0, failed: 0, createdTitles: [], errors: [] };
49762
- if (!units.length) return result;
49763
- const title2 = singleIssueTitle(label);
49764
- if (existingIssueTitles().has(title2)) {
49765
- result.skipped = 1;
49766
- return result;
49767
- }
49768
- const severity = [...units].sort((a, b) => SEV_RANK2[a.severity] - SEV_RANK2[b.severity])[0].severity;
49769
- const r = createIssue(title2, singleIssueBody(units, lang, standard, format), ["accessibility", tag, severity]);
49770
- if (r.ok) {
49771
- result.created = 1;
49772
- result.createdTitles.push(title2);
49773
- } else {
49774
- recordFailure(result, r.reason);
50088
+
50089
+ // src/tickets/providers/remote.ts
50090
+ import { execFileSync as execFileSync4 } from "child_process";
50091
+ function originUrl() {
50092
+ try {
50093
+ const url = execFileSync4("git", ["config", "--get", "remote.origin.url"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
50094
+ return url || void 0;
50095
+ } catch {
50096
+ return void 0;
49775
50097
  }
49776
- return result;
49777
50098
  }
49778
- function COMMENT_MARKER(standard) {
49779
- return `<!-- ultra11y:report standard="${standard}" -->`;
50099
+ function gitRemoteSlug(host) {
50100
+ const url = originUrl();
50101
+ if (!url) return void 0;
50102
+ const m = /^(?:git@|ssh:\/\/git@|https?:\/\/)(?:[^@/]*@)?([^:/]+)[:/](.+?)(?:\.git)?\/?$/.exec(url);
50103
+ if (!m) return void 0;
50104
+ const [, remoteHost, path] = m;
50105
+ if (!remoteHost || !path) return void 0;
50106
+ const matches = host.endsWith(".") ? remoteHost.startsWith(host) : remoteHost === host || remoteHost.endsWith(`.${host}`);
50107
+ return matches ? path : void 0;
50108
+ }
50109
+ function providerFromRemote() {
50110
+ const url = originUrl();
50111
+ if (!url) return void 0;
50112
+ if (/github\.com|github\./i.test(url)) return "github";
50113
+ if (/gitlab\.com|gitlab\./i.test(url)) return "gitlab";
50114
+ return void 0;
49780
50115
  }
49781
- function stickyBody(markdown, standard) {
49782
- return `${COMMENT_MARKER(standard)}
49783
- ${markdown}`;
50116
+
50117
+ // src/tickets/providers/github.ts
50118
+ var CAPABILITIES = { bodyLimit: 65536, labels: true };
50119
+ function resolve4(opts) {
50120
+ const env = opts.env ?? process.env;
50121
+ const mode = opts.transport ?? "auto";
50122
+ const api = (env.GITHUB_API_URL || "https://api.github.com").replace(/\/+$/, "");
50123
+ const token = env.GH_TOKEN || env.GITHUB_TOKEN;
50124
+ const cliOk = (opts.cliAvailable ?? ghAvailable)();
50125
+ if (mode === "cli") {
50126
+ return cliOk ? { transport: "cli", api } : { transport: "cli", api, reason: "`gh` is not installed or not authenticated (run `gh auth login`)" };
50127
+ }
50128
+ const repo = env.ULTRA11Y_GITHUB_REPO || env.GITHUB_REPOSITORY || (mode === "rest" || !cliOk ? gitRemoteSlug("github.com") : void 0);
50129
+ if (mode === "rest") {
50130
+ if (!token) return { transport: "rest", api, reason: "no GitHub token \u2014 set GH_TOKEN or GITHUB_TOKEN" };
50131
+ if (!repo) return { transport: "rest", api, token, reason: "no repository \u2014 set GITHUB_REPOSITORY (owner/name) or ULTRA11Y_GITHUB_REPO" };
50132
+ return { transport: "rest", api, token, repo };
50133
+ }
50134
+ if (cliOk) return { transport: "cli", api };
50135
+ if (token && repo) return { transport: "rest", api, token, repo };
50136
+ const missing = [!token ? "a token (GH_TOKEN/GITHUB_TOKEN)" : "", !repo ? "a repository (GITHUB_REPOSITORY)" : ""].filter(Boolean);
50137
+ return {
50138
+ transport: "rest",
50139
+ api,
50140
+ ...token ? { token } : {},
50141
+ ...repo ? { repo } : {},
50142
+ reason: `\`gh\` is unavailable and REST is missing ${missing.join(" and ")}`
50143
+ };
49784
50144
  }
49785
- function prNumberFromEnv(env = process.env) {
49786
- const explicit = Number.parseInt(env.ULTRA11Y_PR ?? "", 10);
49787
- if (Number.isFinite(explicit) && explicit > 0) return explicit;
49788
- const m = /^refs\/pull\/(\d+)\//.exec(env.GITHUB_REF ?? "");
49789
- const n = m ? Number.parseInt(m[1], 10) : Number.NaN;
49790
- return Number.isFinite(n) && n > 0 ? n : void 0;
50145
+ function createGithubProvider(opts = {}) {
50146
+ const r = resolve4(opts);
50147
+ const auth = { Authorization: `Bearer ${r.token ?? ""}`, Accept: "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28" };
50148
+ const http = {
50149
+ ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {},
50150
+ ...opts.sleep ? { sleep: opts.sleep } : {}
50151
+ };
50152
+ async function listRest() {
50153
+ const out2 = [];
50154
+ for (let page = 1; page <= 10; page++) {
50155
+ const url = `${r.api}/repos/${r.repo}/issues?state=all&per_page=100&page=${page}`;
50156
+ const { data: data2 } = await requestJson(url, {
50157
+ headers: auth,
50158
+ ...http
50159
+ });
50160
+ if (!Array.isArray(data2) || data2.length === 0) break;
50161
+ for (const i2 of data2) {
50162
+ if (i2.pull_request) continue;
50163
+ if (i2.title) out2.push({ title: i2.title, ...i2.number !== void 0 ? { id: String(i2.number) } : {}, ...i2.html_url ? { url: i2.html_url } : {} });
50164
+ }
50165
+ if (data2.length < 100) break;
50166
+ }
50167
+ return out2;
50168
+ }
50169
+ function listCli() {
50170
+ const raw = ghExec(["issue", "list", "--state", "all", "--limit", "1000", "--json", "title"]);
50171
+ return JSON.parse(raw).filter((i2) => i2.title).map((i2) => ({ title: i2.title }));
50172
+ }
50173
+ async function createRest(t2) {
50174
+ const url = `${r.api}/repos/${r.repo}/issues`;
50175
+ const post = async (labels) => requestJson(url, {
50176
+ method: "POST",
50177
+ headers: auth,
50178
+ body: { title: t2.title, body: t2.body, ...labels ? { labels } : {} },
50179
+ ...http
50180
+ });
50181
+ try {
50182
+ const { data: data2 } = await post(t2.labels);
50183
+ return { ok: true, ...data2.number !== void 0 ? { id: String(data2.number) } : {}, ...data2.html_url ? { url: data2.html_url } : {} };
50184
+ } catch (e) {
50185
+ if (e instanceof HttpError && e.status === 422) {
50186
+ try {
50187
+ const { data: data2 } = await post();
50188
+ return { ok: true, ...data2.number !== void 0 ? { id: String(data2.number) } : {}, ...data2.html_url ? { url: data2.html_url } : {} };
50189
+ } catch (retryErr) {
50190
+ return { ok: false, reason: retryErr instanceof HttpError ? reasonFrom(retryErr.detail) : String(retryErr) };
50191
+ }
50192
+ }
50193
+ return { ok: false, reason: e instanceof HttpError ? reasonFrom(e.detail) : e instanceof Error ? e.message : String(e) };
50194
+ }
50195
+ }
50196
+ function createCli(t2) {
50197
+ const base = ["issue", "create", "--title", t2.title, "--body-file", "-"];
50198
+ try {
50199
+ ghExec([...base, "--label", t2.labels.join(",")], t2.body);
50200
+ return { ok: true };
50201
+ } catch (labelledErr) {
50202
+ try {
50203
+ ghExec(base, t2.body);
50204
+ return { ok: true };
50205
+ } catch (err2) {
50206
+ return { ok: false, reason: ghErrorReason(err2) ?? ghErrorReason(labelledErr) };
50207
+ }
50208
+ }
50209
+ }
50210
+ return {
50211
+ id: "github",
50212
+ transport: r.transport,
50213
+ capabilities: CAPABILITIES,
50214
+ available: () => r.reason === void 0,
50215
+ unavailableReason: () => r.reason,
50216
+ list: async () => {
50217
+ try {
50218
+ return r.transport === "cli" ? listCli() : await listRest();
50219
+ } catch {
50220
+ return [];
50221
+ }
50222
+ },
50223
+ create: async (t2) => r.transport === "cli" ? createCli(t2) : createRest(t2)
50224
+ };
49791
50225
  }
49792
- function pickExistingComment(comments, marker) {
49793
- return comments.find((c2) => typeof c2.body === "string" && c2.body.includes(marker));
50226
+
50227
+ // src/tickets/providers/gitlab.ts
50228
+ import { execFileSync as execFileSync5 } from "child_process";
50229
+ var CAPABILITIES2 = { bodyLimit: 1e6, labels: true };
50230
+ function glabExec(args2) {
50231
+ return execFileSync5("glab", args2, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] });
49794
50232
  }
49795
- function pushPrComment(markdown, standard = "wcag") {
49796
- const pr = prNumberFromEnv();
49797
- if (pr === void 0) return { ok: true, action: "skipped", reason: "not a pull-request run" };
49798
- const marker = COMMENT_MARKER(standard);
49799
- const body2 = stickyBody(markdown, standard);
50233
+ function glabAvailable() {
49800
50234
  try {
49801
- const repo = gh(["repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"]).trim();
49802
- let existing;
50235
+ execFileSync5("glab", ["auth", "status"], { stdio: "ignore" });
50236
+ return true;
50237
+ } catch {
50238
+ return false;
50239
+ }
50240
+ }
50241
+ function execReason(err2) {
50242
+ if (!err2 || typeof err2 !== "object") return void 0;
50243
+ const e = err2;
50244
+ const stderr = typeof e.stderr === "string" ? e.stderr : Buffer.isBuffer(e.stderr) ? e.stderr.toString("utf8") : "";
50245
+ const line = stderr.split("\n").map((l) => l.trim()).find(Boolean);
50246
+ if (line) return line;
50247
+ return typeof e.message === "string" ? e.message.split("\n")[0]?.trim() ?? void 0 : void 0;
50248
+ }
50249
+ function resolve5(opts) {
50250
+ const env = opts.env ?? process.env;
50251
+ const mode = opts.transport ?? "auto";
50252
+ const api = (env.CI_API_V4_URL || "https://gitlab.com/api/v4").replace(/\/+$/, "");
50253
+ const token = env.GITLAB_TOKEN || env.CI_JOB_TOKEN;
50254
+ const jobToken = !env.GITLAB_TOKEN && !!env.CI_JOB_TOKEN;
50255
+ const cliOk = (opts.cliAvailable ?? glabAvailable)();
50256
+ if (mode === "cli") {
50257
+ return cliOk ? { transport: "cli", api, jobToken } : { transport: "cli", api, jobToken, reason: "`glab` is not installed or not authenticated (run `glab auth login`)" };
50258
+ }
50259
+ const project = env.ULTRA11Y_GITLAB_PROJECT || env.CI_PROJECT_ID || gitRemoteSlug("gitlab.");
50260
+ if (mode === "rest" || !cliOk) {
50261
+ const base = { transport: "rest", api, jobToken, ...token ? { token } : {}, ...project ? { project } : {} };
50262
+ if (!token) return { ...base, reason: `${mode === "auto" ? "`glab` is unavailable and REST has " : ""}no GitLab token \u2014 set GITLAB_TOKEN` };
50263
+ if (!project) return { ...base, reason: "no project \u2014 set CI_PROJECT_ID or ULTRA11Y_GITLAB_PROJECT (id or group/project)" };
50264
+ return base;
50265
+ }
50266
+ return { transport: "cli", api, jobToken };
50267
+ }
50268
+ function createGitlabProvider(opts = {}) {
50269
+ const r = resolve5(opts);
50270
+ const id = encodeURIComponent(r.project ?? "");
50271
+ const auth = { "PRIVATE-TOKEN": r.token ?? "" };
50272
+ const http = {
50273
+ ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {},
50274
+ ...opts.sleep ? { sleep: opts.sleep } : {}
50275
+ };
50276
+ const withTokenHint = (reason) => r.jobToken && /403|forbidden|unauthorized|401/i.test(reason) ? `${reason} \u2014 CI_JOB_TOKEN cannot create issues; use a project access token in GITLAB_TOKEN` : reason;
50277
+ async function listRest() {
50278
+ const out2 = [];
50279
+ for (let page = 1; page <= 10; page++) {
50280
+ const url = `${r.api}/projects/${id}/issues?state=all&per_page=100&page=${page}`;
50281
+ const { data: data2, headers } = await requestJson(url, { headers: auth, ...http });
50282
+ if (!Array.isArray(data2) || data2.length === 0) break;
50283
+ for (const i2 of data2)
50284
+ if (i2.title) out2.push({ title: i2.title, ...i2.iid !== void 0 ? { id: String(i2.iid) } : {}, ...i2.web_url ? { url: i2.web_url } : {} });
50285
+ if (!headers.get("x-next-page")) break;
50286
+ }
50287
+ return out2;
50288
+ }
50289
+ function listCli() {
50290
+ const raw = glabExec(["issue", "list", "--all", "--output", "json"]);
50291
+ const parsed = JSON.parse(raw);
50292
+ const rows = Array.isArray(parsed) ? parsed : parsed.issues ?? [];
50293
+ return rows.filter((i2) => i2.title).map((i2) => ({ title: i2.title }));
50294
+ }
50295
+ async function createRest(t2) {
49803
50296
  try {
49804
- const raw = gh(["api", `repos/${repo}/issues/${pr}/comments`, "--paginate"]);
49805
- existing = pickExistingComment(JSON.parse(raw), marker);
49806
- } catch {
50297
+ const { data: data2 } = await requestJson(`${r.api}/projects/${id}/issues`, {
50298
+ method: "POST",
50299
+ headers: auth,
50300
+ body: { title: t2.title, description: t2.body, labels: t2.labels.join(",") },
50301
+ ...http
50302
+ });
50303
+ return { ok: true, ...data2.iid !== void 0 ? { id: String(data2.iid) } : {}, ...data2.web_url ? { url: data2.web_url } : {} };
50304
+ } catch (e) {
50305
+ const reason = e instanceof HttpError ? reasonFrom(e.detail) : e instanceof Error ? e.message : String(e);
50306
+ return { ok: false, reason: withTokenHint(e instanceof HttpError ? `${e.status} ${reason}` : reason) };
49807
50307
  }
49808
- if (existing) {
49809
- gh(["api", "--method", "PATCH", `repos/${repo}/issues/comments/${existing.id}`, "-f", `body=${body2}`]);
49810
- return { ok: true, action: "updated" };
50308
+ }
50309
+ function createCli(t2) {
50310
+ try {
50311
+ glabExec(["issue", "create", "--title", t2.title, "--description", t2.body, "--label", t2.labels.join(",")]);
50312
+ return { ok: true };
50313
+ } catch (err2) {
50314
+ return { ok: false, reason: withTokenHint(execReason(err2) ?? "glab issue create failed") };
49811
50315
  }
49812
- gh(["api", "--method", "POST", `repos/${repo}/issues/${pr}/comments`, "-f", `body=${body2}`]);
49813
- return { ok: true, action: "created" };
49814
- } catch (e) {
49815
- return { ok: false, action: "skipped", reason: ghErrorReason(e) };
49816
50316
  }
50317
+ return {
50318
+ id: "gitlab",
50319
+ transport: r.transport,
50320
+ capabilities: CAPABILITIES2,
50321
+ available: () => r.reason === void 0,
50322
+ unavailableReason: () => r.reason,
50323
+ list: async () => {
50324
+ try {
50325
+ return r.transport === "cli" ? listCli() : await listRest();
50326
+ } catch {
50327
+ return [];
50328
+ }
50329
+ },
50330
+ create: async (t2) => r.transport === "cli" ? createCli(t2) : createRest(t2)
50331
+ };
50332
+ }
50333
+
50334
+ // src/tickets/providers/jira.ts
50335
+ var CAPABILITIES3 = { bodyLimit: 32767, labels: true };
50336
+ var JIRA_SCOPE_LABEL = "ultra11y";
50337
+ var PRIORITY = { bloquant: "Highest", majeur: "High", mineur: "Low" };
50338
+ function resolve6(opts) {
50339
+ const env = opts.env ?? process.env;
50340
+ const issueType = env.ULTRA11Y_JIRA_ISSUE_TYPE || "Task";
50341
+ const apiVersion = env.ULTRA11Y_JIRA_API === "2" ? "2" : "3";
50342
+ const url = (env.ULTRA11Y_JIRA_URL || "").replace(/\/+$/, "");
50343
+ const project = env.ULTRA11Y_JIRA_PROJECT;
50344
+ const base = { issueType, apiVersion, ...url ? { url } : {}, ...project ? { project } : {} };
50345
+ if (opts.transport === "cli") return { ...base, reason: "Jira has no CLI transport \u2014 use --transport rest (the default) with ULTRA11Y_JIRA_URL and a token" };
50346
+ if (!url) return { ...base, reason: "no Jira site \u2014 set ULTRA11Y_JIRA_URL (e.g. https://acme.atlassian.net)" };
50347
+ if (!project) return { ...base, reason: "no Jira project \u2014 set ULTRA11Y_JIRA_PROJECT (the project KEY)" };
50348
+ if (env.JIRA_API_TOKEN && env.JIRA_EMAIL) return { ...base, auth: `Basic ${Buffer.from(`${env.JIRA_EMAIL}:${env.JIRA_API_TOKEN}`).toString("base64")}` };
50349
+ if (env.JIRA_TOKEN) return { ...base, auth: `Bearer ${env.JIRA_TOKEN}` };
50350
+ return { ...base, reason: "no Jira credentials \u2014 set JIRA_EMAIL + JIRA_API_TOKEN (Cloud) or JIRA_TOKEN (Server/DC)" };
50351
+ }
50352
+ function toAdf(markdown) {
50353
+ const content = markdown.split("\n").map((line) => line.trimEnd()).filter((line) => line.length > 0).map((line) => ({ type: "paragraph", content: [{ type: "text", text: line }] }));
50354
+ return { type: "doc", version: 1, content: content.length ? content : [{ type: "paragraph", content: [] }] };
50355
+ }
50356
+ function createJiraProvider(opts = {}) {
50357
+ const r = resolve6(opts);
50358
+ const headers = { Authorization: r.auth ?? "" };
50359
+ const http = {
50360
+ ...opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {},
50361
+ ...opts.sleep ? { sleep: opts.sleep } : {}
50362
+ };
50363
+ async function list() {
50364
+ const out2 = [];
50365
+ const jql = `project = "${r.project}" AND labels = "${JIRA_SCOPE_LABEL}" ORDER BY created DESC`;
50366
+ for (let startAt = 0; startAt < 500; startAt += 100) {
50367
+ const url = `${r.url}/rest/api/2/search?jql=${encodeURIComponent(jql)}&fields=summary&maxResults=100&startAt=${startAt}`;
50368
+ const { data: data2 } = await requestJson(url, { headers, ...http });
50369
+ const issues = data2.issues ?? [];
50370
+ if (!issues.length) break;
50371
+ for (const i2 of issues) if (i2.fields?.summary) out2.push({ title: i2.fields.summary, ...i2.key ? { id: i2.key, url: `${r.url}/browse/${i2.key}` } : {} });
50372
+ if (issues.length < 100) break;
50373
+ }
50374
+ return out2;
50375
+ }
50376
+ function fields(t2, opt) {
50377
+ return {
50378
+ project: { key: r.project },
50379
+ summary: t2.title,
50380
+ description: r.apiVersion === "3" ? toAdf(t2.body) : t2.body,
50381
+ issuetype: { name: r.issueType },
50382
+ ...opt.labels ? { labels: [.../* @__PURE__ */ new Set([...t2.labels, JIRA_SCOPE_LABEL])].map((l) => l.replace(/\s+/g, "-")) } : {},
50383
+ ...opt.priority ? { priority: { name: PRIORITY[t2.severity] } } : {}
50384
+ };
50385
+ }
50386
+ async function post(t2, opt) {
50387
+ return requestJson(`${r.url}/rest/api/${r.apiVersion}/issue`, { method: "POST", headers, body: { fields: fields(t2, opt) }, ...http });
50388
+ }
50389
+ async function create(t2) {
50390
+ const attempts = [
50391
+ { priority: true, labels: true },
50392
+ { priority: false, labels: true },
50393
+ { priority: false, labels: false }
50394
+ ];
50395
+ let last = "";
50396
+ for (const opt of attempts) {
50397
+ try {
50398
+ const { data: data2 } = await post(t2, opt);
50399
+ return { ok: true, ...data2.key ? { id: data2.key, url: `${r.url}/browse/${data2.key}` } : {} };
50400
+ } catch (e) {
50401
+ last = e instanceof HttpError ? reasonFrom(e.detail) : e instanceof Error ? e.message : String(e);
50402
+ if (!(e instanceof HttpError) || e.status !== 400) return { ok: false, reason: last };
50403
+ }
50404
+ }
50405
+ return { ok: false, reason: last };
50406
+ }
50407
+ return {
50408
+ id: "jira",
50409
+ transport: "rest",
50410
+ capabilities: CAPABILITIES3,
50411
+ available: () => r.reason === void 0,
50412
+ unavailableReason: () => r.reason,
50413
+ list: async () => {
50414
+ try {
50415
+ return await list();
50416
+ } catch {
50417
+ return [];
50418
+ }
50419
+ },
50420
+ create
50421
+ };
50422
+ }
50423
+
50424
+ // src/tickets/registry.ts
50425
+ function isProviderId(v) {
50426
+ return ALL_PROVIDERS.includes(v);
50427
+ }
50428
+ function createProvider(id, opts = {}) {
50429
+ switch (id) {
50430
+ case "github":
50431
+ return createGithubProvider(opts);
50432
+ case "gitlab":
50433
+ return createGitlabProvider(opts);
50434
+ case "jira":
50435
+ return createJiraProvider(opts);
50436
+ }
50437
+ }
50438
+ function autoProvider(env = process.env, configured) {
50439
+ const fromEnv = env.ULTRA11Y_TICKET_PROVIDER;
50440
+ if (fromEnv && isProviderId(fromEnv)) return fromEnv;
50441
+ if (configured && isProviderId(configured)) return configured;
50442
+ return providerFromRemote();
49817
50443
  }
49818
50444
 
49819
50445
  // src/render.ts
@@ -49845,7 +50471,7 @@ function detectFrameworks(deps, has2) {
49845
50471
  for (const [pkg, label] of Object.entries(KNOWN_LIBS)) if (dep(pkg)) componentLibraries.push(label);
49846
50472
  return { frameworks, componentLibraries };
49847
50473
  }
49848
- var L5 = {
50474
+ var L6 = {
49849
50475
  fr: {
49850
50476
  title: "Obtenir du HTML rendu \xE0 auditer (render)",
49851
50477
  why: "Auditer les sources JSX d'une biblioth\xE8que de composants donne des faux n\xE9gatifs : il faut auditer le HTML r\xE9ellement produit.",
@@ -49876,7 +50502,7 @@ var L5 = {
49876
50502
  }
49877
50503
  };
49878
50504
  function renderPlan(d, lang = "fr") {
49879
- const s = L5[lang];
50505
+ const s = L6[lang];
49880
50506
  const out2 = [`# ${s.title}`, "", `> ${s.why}`, ""];
49881
50507
  if (d.componentLibraries.length) out2.push(`> \u{1F9E9} ${s.libNote(d.componentLibraries.join(", "))}`, "");
49882
50508
  out2.push(`## ${s.detected}`, "");
@@ -50421,7 +51047,7 @@ function writeWorklist(items, outDir, semantic, standard = "wcag", lang = "en")
50421
51047
 
50422
51048
  // src/grounding.ts
50423
51049
  import { existsSync as existsSync17, readFileSync as readFileSync14 } from "fs";
50424
- import { resolve as resolve4 } from "path";
51050
+ import { resolve as resolve7 } from "path";
50425
51051
  var WINDOW = 10;
50426
51052
  var norm2 = (s) => s.replace(/\s+/g, " ").trim();
50427
51053
  var isUnresolvable = (file) => !file || file === "-" || file === "<stdin>" || file === "stdin";
@@ -50438,7 +51064,7 @@ function selectorProbes(selector) {
50438
51064
  }
50439
51065
  function groundFinding(g, opts = {}) {
50440
51066
  if (isUnresolvable(g.file)) return { ok: true, moved: false };
50441
- const path = resolve4(opts.cwd ?? process.cwd(), g.file);
51067
+ const path = resolve7(opts.cwd ?? process.cwd(), g.file);
50442
51068
  if (!existsSync17(path)) return { ok: false, moved: false, issue: `cited file not found: ${g.file}` };
50443
51069
  let text;
50444
51070
  try {
@@ -50493,7 +51119,7 @@ function isPagesReport(md) {
50493
51119
  return md.slice(0, 400).includes(`<!-- ${PAGES_REPORT_MARKER}`);
50494
51120
  }
50495
51121
  var MARK2 = { C: "C", NC: "NC", NA: "\u2014", manual: "?" };
50496
- var L6 = {
51122
+ var L7 = {
50497
51123
  fr: {
50498
51124
  docTitle: "Rapport d'accessibilit\xE9 page par page",
50499
51125
  indexTitle: "Rapport page par page \u2014 index",
@@ -50613,7 +51239,7 @@ function renderPageReport(result, page, opts = {}) {
50613
51239
  const standard = opts.standard ?? CORE2;
50614
51240
  const lang = opts.lang ?? "en";
50615
51241
  const h = opts.heading ?? "##";
50616
- const s = L6[lang];
51242
+ const s = L7[lang];
50617
51243
  const out2 = [];
50618
51244
  out2.push(`${h} ${page.name}${page.auth ? " \u{1F512}" : ""}`, "");
50619
51245
  const meta2 = [];
@@ -50659,7 +51285,7 @@ function renderPageReport(result, page, opts = {}) {
50659
51285
  return out2.join("\n");
50660
51286
  }
50661
51287
  function header2(result, pages, standard, lang, title2) {
50662
- const s = L6[lang];
51288
+ const s = L7[lang];
50663
51289
  return [
50664
51290
  pagesReportMarker(standard),
50665
51291
  "",
@@ -50674,7 +51300,7 @@ function header2(result, pages, standard, lang, title2) {
50674
51300
  function renderPagesIndex(result, pages, opts = {}) {
50675
51301
  const standard = opts.standard ?? CORE2;
50676
51302
  const lang = opts.lang ?? "en";
50677
- const s = L6[lang];
51303
+ const s = L7[lang];
50678
51304
  const out2 = header2(result, pages, standard, lang, s.indexTitle);
50679
51305
  out2.push(`> ${s.indexNote}`, "");
50680
51306
  out2.push(`| ${s.page} | ${s.url} | ${s.basis} | ${s.rate} | ${s.blocking} | ${s.major} | ${s.minor} | ${s.sheet} |`);
@@ -50702,7 +51328,7 @@ function renderPagesDocument(result, pages, opts = {}) {
50702
51328
  function renderPageDocument(result, page, opts = {}) {
50703
51329
  const standard = opts.standard ?? CORE2;
50704
51330
  const lang = opts.lang ?? "en";
50705
- const s = L6[lang];
51331
+ const s = L7[lang];
50706
51332
  const out2 = [pagesReportMarker(standard), "", `# ${s.docTitle} \u2014 ${page.name}`, "", `- **${s.date}** : ${result.date}`, ""];
50707
51333
  out2.push(renderPageReport(result, page, { ...opts, standard, lang, heading: "##" }));
50708
51334
  return out2.join("\n");
@@ -51629,6 +52255,18 @@ var adjudication_default = {
51629
52255
 
51630
52256
  // src/adjudicate.ts
51631
52257
  var ADJUDICATE_MAX_EVIDENCE = 30;
52258
+ function adjudicationContract() {
52259
+ return {
52260
+ verdicts: [...VERDICTS],
52261
+ manualReasons: [...MANUAL_REASON_VALUES],
52262
+ requires: {
52263
+ C: "a non-empty justification AND citations[] naming the harvested evidence it cleared (each anchor resolvable and drawn from this criterion's own evidence); a criterion with no harvested evidence cannot be C at all",
52264
+ NA: "a non-empty justification",
52265
+ NC: "at least one groundable finding, each citing a normativeRef that resolves against the active standard",
52266
+ manual: `a reason \u2208 {${MANUAL_REASON_VALUES.join(", ")}}`
52267
+ }
52268
+ };
52269
+ }
51632
52270
  var selectorFor = (el) => {
51633
52271
  const id = el.attribs.id ? `#${el.attribs.id}` : "";
51634
52272
  const cls = el.attribs.class ? `.${el.attribs.class.trim().split(/\s+/)[0]}` : "";
@@ -51830,6 +52468,31 @@ function buildAdjudicationWorklist(audit2, opts = {}) {
51830
52468
  }
51831
52469
  var NC_SEVERITY_DEFAULT = "majeur";
51832
52470
  var MANUAL_REASONS = /* @__PURE__ */ new Set(["needs-rendered-dom", "undecidable"]);
52471
+ var VERDICTS = ["C", "NC", "NA", "manual"];
52472
+ var MANUAL_REASON_VALUES = ["needs-rendered-dom", "undecidable"];
52473
+ function normalizeVerdict2(v) {
52474
+ if (typeof v !== "string") return void 0;
52475
+ const k = v.trim().toLowerCase();
52476
+ return VERDICTS.find((x) => x.toLowerCase() === k);
52477
+ }
52478
+ function normalizeManualReason(r) {
52479
+ if (typeof r !== "string") return void 0;
52480
+ const k = r.trim().toLowerCase();
52481
+ return MANUAL_REASON_VALUES.find((x) => x === k);
52482
+ }
52483
+ function canonicalizeAdjudication(adj) {
52484
+ for (const it of adj.items) {
52485
+ if (it.verdict !== null) {
52486
+ const v = normalizeVerdict2(it.verdict);
52487
+ if (v !== void 0) it.verdict = v;
52488
+ }
52489
+ if (it.reason !== null && it.reason !== void 0) {
52490
+ const r = normalizeManualReason(it.reason);
52491
+ if (r !== void 0) it.reason = r;
52492
+ }
52493
+ }
52494
+ return adj;
52495
+ }
51833
52496
  function normativeRefResolves(ref, standard, itemCriterionId) {
51834
52497
  const r = (ref ?? "").trim();
51835
52498
  if (!r) return false;
@@ -51852,6 +52515,7 @@ function normativeRefResolves(ref, standard, itemCriterionId) {
51852
52515
  }
51853
52516
  function applyAdjudication(audit2, adj, opts = {}) {
51854
52517
  const issues = [];
52518
+ canonicalizeAdjudication(adj);
51855
52519
  const byId2 = new Map(adj.items.map((it) => [it.criteriaId, it]));
51856
52520
  const packMode = !isCore(adj.standard);
51857
52521
  const open = /* @__PURE__ */ new Set();
@@ -51913,9 +52577,9 @@ function applyAdjudication(audit2, adj, opts = {}) {
51913
52577
  }
51914
52578
  } else if (v === "manual") {
51915
52579
  if (!it.reason || !MANUAL_REASONS.has(it.reason))
51916
- issues.push(`criterion ${it.criteriaId}: a manual verdict requires reason \u2208 {needs-rendered-dom, undecidable}`);
52580
+ issues.push(`criterion ${it.criteriaId}: a manual verdict requires reason \u2208 {${MANUAL_REASON_VALUES.join(", ")}}`);
51917
52581
  } else {
51918
- issues.push(`criterion ${it.criteriaId}: unknown verdict "${String(v)}"`);
52582
+ issues.push(`criterion ${it.criteriaId}: unknown verdict "${String(v)}" \u2014 expected one of ${VERDICTS.join(" | ")}`);
51919
52583
  }
51920
52584
  for (const rec of it.recommendations ?? []) groundInputs.push({ file: rec.file, line: rec.line, selector: rec.selector, snippet: rec.snippet });
51921
52585
  }
@@ -52173,6 +52837,7 @@ function writeAdjudication(items, outDir, opts) {
52173
52837
  schemaVersion: SCHEMA_VERSION,
52174
52838
  standard: opts.standard,
52175
52839
  auditDate: opts.auditDate,
52840
+ contract: adjudicationContract(),
52176
52841
  items
52177
52842
  };
52178
52843
  writeFileSync10(todoPath, JSON.stringify(file, null, 2) + "\n");
@@ -52369,10 +53034,10 @@ function applyRawVerdicts(items, verdicts) {
52369
53034
  }
52370
53035
 
52371
53036
  // src/scan.ts
52372
- import { execFileSync as execFileSync4 } from "child_process";
53037
+ import { execFileSync as execFileSync6 } from "child_process";
52373
53038
  import { mkdtempSync as mkdtempSync2, writeFileSync as writeFileSync11, existsSync as existsSync20, statSync as statSync8, readdirSync as readdirSync5, rmSync as rmSync3, readFileSync as readFileSync16 } from "fs";
52374
53039
  import { tmpdir as tmpdir2 } from "os";
52375
- import { join as join33, resolve as resolve5 } from "path";
53040
+ import { join as join33, resolve as resolve8 } from "path";
52376
53041
  import { fileURLToPath as fileURLToPath3 } from "url";
52377
53042
 
52378
53043
  // src/axe-map.ts
@@ -52821,7 +53486,7 @@ ENTRYPOINT ["node", "/app/runner.mjs"]
52821
53486
  `;
52822
53487
  function dockerAvailable() {
52823
53488
  try {
52824
- execFileSync4("docker", ["info"], { stdio: "ignore", timeout: 1e4 });
53489
+ execFileSync6("docker", ["info"], { stdio: "ignore", timeout: 1e4 });
52825
53490
  return true;
52826
53491
  } catch {
52827
53492
  return false;
@@ -52829,7 +53494,7 @@ function dockerAvailable() {
52829
53494
  }
52830
53495
  function imageExists(tag) {
52831
53496
  try {
52832
- execFileSync4("docker", ["image", "inspect", tag], { stdio: "ignore" });
53497
+ execFileSync6("docker", ["image", "inspect", tag], { stdio: "ignore" });
52833
53498
  return true;
52834
53499
  } catch {
52835
53500
  return false;
@@ -52842,7 +53507,7 @@ function buildImage(tag = IMAGE_TAG) {
52842
53507
  writeFileSync11(join33(ctx, "runner.mjs"), RUNNER);
52843
53508
  writeFileSync11(join33(ctx, "package.json"), PKG);
52844
53509
  writeFileSync11(join33(ctx, "Dockerfile"), DOCKERFILE);
52845
- execFileSync4("docker", ["build", "-t", tag, ctx], { stdio: "inherit", timeout: 9e5 });
53510
+ execFileSync6("docker", ["build", "-t", tag, ctx], { stdio: "inherit", timeout: 9e5 });
52846
53511
  } finally {
52847
53512
  rmSync3(ctx, { recursive: true, force: true });
52848
53513
  }
@@ -52861,7 +53526,7 @@ function cleanDynamic(tag = IMAGE_TAG) {
52861
53526
  let imageRemoved = false;
52862
53527
  if (dockerAvailable() && imageExists(tag)) {
52863
53528
  try {
52864
- execFileSync4("docker", ["rmi", "-f", tag], { stdio: "ignore" });
53529
+ execFileSync6("docker", ["rmi", "-f", tag], { stdio: "ignore" });
52865
53530
  imageRemoved = true;
52866
53531
  } catch {
52867
53532
  }
@@ -52918,11 +53583,11 @@ var PROBE_FIELDS = [
52918
53583
  function runRunner(target, isFile, tag, snapshot = true) {
52919
53584
  const args2 = ["run", "--rm"];
52920
53585
  if (!snapshot) args2.push("-e", "ULTRA11Y_SNAPSHOT=0");
52921
- if (isFile) args2.push("-v", `${resolve5(target)}:${MOUNT}:ro`);
53586
+ if (isFile) args2.push("-v", `${resolve8(target)}:${MOUNT}:ro`);
52922
53587
  args2.push(tag, isFile ? MOUNT : target);
52923
53588
  let stdout;
52924
53589
  try {
52925
- stdout = execFileSync4("docker", args2, { encoding: "utf8", timeout: 24e4, maxBuffer: 192 * 1024 * 1024, stdio: ["ignore", "pipe", "pipe"] });
53590
+ stdout = execFileSync6("docker", args2, { encoding: "utf8", timeout: 24e4, maxBuffer: 192 * 1024 * 1024, stdio: ["ignore", "pipe", "pipe"] });
52926
53591
  } catch (e) {
52927
53592
  const err2 = e;
52928
53593
  const detail = (err2.stderr ? String(err2.stderr).trim() : "") || err2.message || String(e);
@@ -53227,7 +53892,7 @@ function mergeSnapshotAudit(base, snap) {
53227
53892
  // src/scan-local.ts
53228
53893
  import { existsSync as existsSync21, statSync as statSync9 } from "fs";
53229
53894
  import { createRequire } from "module";
53230
- import { resolve as resolve6 } from "path";
53895
+ import { resolve as resolve9 } from "path";
53231
53896
  var LOCAL_ENGINE = "axe-core@playwright (local)";
53232
53897
  var LOCAL_TESTED_SCS = ["1.4.4", "1.4.10", "1.4.12", "2.4.7", "1.4.13"];
53233
53898
  function localTestedScs(interact) {
@@ -53237,7 +53902,7 @@ var PW_SPEC = "@playwright/test";
53237
53902
  var AXE_SPEC = "@axe-core/playwright";
53238
53903
  function localAvailable(cwd) {
53239
53904
  try {
53240
- const req = createRequire(resolve6(cwd, "package.json"));
53905
+ const req = createRequire(resolve9(cwd, "package.json"));
53241
53906
  req.resolve(PW_SPEC);
53242
53907
  req.resolve(AXE_SPEC);
53243
53908
  return true;
@@ -53249,7 +53914,7 @@ function resolveLocalDeps(cwd) {
53249
53914
  let chromium;
53250
53915
  let AxeBuilder;
53251
53916
  try {
53252
- const req = createRequire(resolve6(cwd, "package.json"));
53917
+ const req = createRequire(resolve9(cwd, "package.json"));
53253
53918
  const pw = req(PW_SPEC);
53254
53919
  const axeMod = req(AXE_SPEC);
53255
53920
  chromium = pw.chromium;
@@ -53728,7 +54393,7 @@ async function runOnPage(browser, AxeBuilder, target, isFile, opts) {
53728
54393
  const page = await context.newPage();
53729
54394
  const empty = [];
53730
54395
  try {
53731
- const url = isFile ? "file://" + resolve6(target) : target;
54396
+ const url = isFile ? "file://" + resolve9(target) : target;
53732
54397
  await page.goto(url, { waitUntil: "load", timeout: 45e3 });
53733
54398
  await page.waitForLoadState("networkidle", { timeout: 8e3 }).catch(() => {
53734
54399
  });
@@ -54193,12 +54858,12 @@ function fixSummary(r, lang = "fr", write = false) {
54193
54858
 
54194
54859
  // src/init.ts
54195
54860
  import { writeFileSync as writeFileSync13, mkdirSync as mkdirSync10, chmodSync, realpathSync as realpathSync2 } from "fs";
54196
- import { execFileSync as execFileSync5 } from "child_process";
54861
+ import { execFileSync as execFileSync7 } from "child_process";
54197
54862
  import { join as join34, relative as relative4, sep as sep4 } from "path";
54198
54863
  var EN_SEV = { bloquant: "blocking", majeur: "major", mineur: "minor" };
54199
54864
  function repoRoot() {
54200
54865
  try {
54201
- return execFileSync5("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
54866
+ return execFileSync7("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
54202
54867
  } catch {
54203
54868
  return null;
54204
54869
  }
@@ -54916,7 +55581,7 @@ var STR = {
54916
55581
  function t(lang, key) {
54917
55582
  return STR[lang][key];
54918
55583
  }
54919
- var ICON5 = { bloquant: "\u{1F534}", majeur: "\u{1F7E0}", mineur: "\u{1F7E1}" };
55584
+ var ICON4 = { bloquant: "\u{1F534}", majeur: "\u{1F7E0}", mineur: "\u{1F7E1}" };
54920
55585
  function auditSummary(r, lang) {
54921
55586
  const lines = [];
54922
55587
  lines.push(`${t(lang, "summaryTitle")} \u2014 ${r.date}`);
@@ -54933,7 +55598,7 @@ function auditSummary(r, lang) {
54933
55598
  } else {
54934
55599
  lines.push(`${t(lang, "findingsTitle")} (${r.findings.length}) :`);
54935
55600
  for (const f of r.findings.slice(0, 20)) {
54936
- lines.push(` ${ICON5[f.severity]} [${f.criteriaId}] ${f.file}:${f.line} ${resolveMessage(f, lang)}`);
55601
+ lines.push(` ${ICON4[f.severity]} [${f.criteriaId}] ${f.file}:${f.line} ${resolveMessage(f, lang)}`);
54937
55602
  }
54938
55603
  if (r.findings.length > 20) lines.push(` \u2026 (+${r.findings.length - 20})`);
54939
55604
  }
@@ -55072,8 +55737,8 @@ function toSarif(result, opts = {}) {
55072
55737
 
55073
55738
  // src/annotate.ts
55074
55739
  var LEVEL2 = { bloquant: "error", majeur: "warning", mineur: "notice" };
55075
- var ICON6 = { bloquant: "\u{1F534}", majeur: "\u{1F7E0}", mineur: "\u{1F7E1}" };
55076
- var SEV_ORDER5 = ["bloquant", "majeur", "mineur"];
55740
+ var ICON5 = { bloquant: "\u{1F534}", majeur: "\u{1F7E0}", mineur: "\u{1F7E1}" };
55741
+ var SEV_ORDER4 = ["bloquant", "majeur", "mineur"];
55077
55742
  function esc(s) {
55078
55743
  return s.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
55079
55744
  }
@@ -55167,13 +55832,13 @@ function stepSummary(result, opts = {}) {
55167
55832
  out2.push(perPageTable(result, standard, lang));
55168
55833
  return out2.join("\n");
55169
55834
  }
55170
- const sorted = [...all].sort((a, b) => SEV_ORDER5.indexOf(a.severity) - SEV_ORDER5.indexOf(b.severity));
55835
+ const sorted = [...all].sort((a, b) => SEV_ORDER4.indexOf(a.severity) - SEV_ORDER4.indexOf(b.severity));
55171
55836
  out2.push(`### ${s.findings} (${sorted.length})`, "");
55172
55837
  out2.push(`| ${s.severity} | ${s.criterion} | ${s.where} | ${s.what} |`, "| --- | --- | --- | --- |");
55173
55838
  for (const f of sorted.slice(0, MAX_ROWS)) {
55174
55839
  const where = isUrl2(f.file) ? f.file : `${repoRelative(f.file, baseDir)}:${Math.max(1, f.line)}`;
55175
55840
  const msg = resolveMessage(f, lang).replace(/\|/g, "\\|");
55176
- out2.push(`| ${ICON6[f.severity]} ${f.severity} | ${criterionLabel2(f, standard)} | \`${where}\` | ${msg} |`);
55841
+ out2.push(`| ${ICON5[f.severity]} ${f.severity} | ${criterionLabel2(f, standard)} | \`${where}\` | ${msg} |`);
55177
55842
  }
55178
55843
  if (sorted.length > MAX_ROWS) out2.push("", s.more(sorted.length - MAX_ROWS));
55179
55844
  out2.push("");
@@ -55636,10 +56301,10 @@ function startDevServer(opts) {
55636
56301
  }
55637
56302
  res.writeHead(404, { "content-type": "text/plain" }).end("not found");
55638
56303
  });
55639
- return new Promise((resolve10, reject) => {
56304
+ return new Promise((resolve13, reject) => {
55640
56305
  server.once("error", reject);
55641
56306
  server.listen(opts.port, "127.0.0.1", () => {
55642
- resolve10({
56307
+ resolve13({
55643
56308
  port: server.address().port,
55644
56309
  close: () => new Promise((r) => server.close(() => r()))
55645
56310
  });
@@ -55924,7 +56589,7 @@ function e2eSetupPlan(runners, paths, lang = "en") {
55924
56589
 
55925
56590
  // src/orchestrate.ts
55926
56591
  import { existsSync as existsSync28, mkdirSync as mkdirSync14, readFileSync as readFileSync24, rmSync as rmSync7, writeFileSync as writeFileSync16 } from "fs";
55927
- import { join as join42, resolve as resolve7 } from "path";
56592
+ import { join as join42, resolve as resolve10 } from "path";
55928
56593
 
55929
56594
  // src/orchestrate-templates.ts
55930
56595
  import { join as join41 } from "path";
@@ -56135,7 +56800,7 @@ var PHASES = ["adjudicate", "verify-report"];
56135
56800
  var SMALL_WORKLIST = 3;
56136
56801
  var BATCH_SIZE2 = 8;
56137
56802
  function listPhases(runDir, engineAbs) {
56138
- const run2 = resolve7(runDir);
56803
+ const run2 = resolve10(runDir);
56139
56804
  const adjPath = join42(run2, "ADJUDICATE.todo.json");
56140
56805
  let adjIds = [];
56141
56806
  let adjReady = false;
@@ -56182,7 +56847,7 @@ function listPhases(runDir, engineAbs) {
56182
56847
  ];
56183
56848
  }
56184
56849
  function orchestrateRun(runDir, engineAbs, opts = {}) {
56185
- const run2 = resolve7(runDir);
56850
+ const run2 = resolve10(runDir);
56186
56851
  if (!existsSync28(run2)) {
56187
56852
  return { exitCode: 2, written: [], notices: [], errors: [`run dir not found: ${run2}`], phases: [] };
56188
56853
  }
@@ -56255,7 +56920,7 @@ import { createInterface as createInterface2 } from "readline";
56255
56920
 
56256
56921
  // src/mcp/handlers.ts
56257
56922
  import { existsSync as existsSync29, readFileSync as readFileSync25, realpathSync as realpathSync4, statSync as statSync11 } from "fs";
56258
- import { isAbsolute as isAbsolute3, join as join43, resolve as resolve8, sep as sep5 } from "path";
56923
+ import { isAbsolute as isAbsolute3, join as join43, resolve as resolve11, sep as sep5 } from "path";
56259
56924
 
56260
56925
  // src/project-lock.ts
56261
56926
  var chains = /* @__PURE__ */ new Map();
@@ -56303,7 +56968,7 @@ function positive(v, key) {
56303
56968
  function requiredCwd(args2, defaults) {
56304
56969
  const cwd = str2(args2.cwd) ?? defaults.defaultCwd;
56305
56970
  if (!cwd) throw new ToolError("`cwd` is required: an absolute path to the project root.");
56306
- const abs = resolve8(cwd);
56971
+ const abs = resolve11(cwd);
56307
56972
  if (!existsSync29(abs)) throw new ToolError(`project root not found: ${abs}`);
56308
56973
  if (!statSync11(abs).isDirectory()) throw new ToolError(`\`cwd\` is not a directory: ${abs}`);
56309
56974
  return abs;
@@ -56334,6 +56999,8 @@ async function dispatch(name2, args2, cwd) {
56334
56999
  return handleReport(args2, cwd);
56335
57000
  case "ultra11y_prd":
56336
57001
  return handlePrd(args2, cwd);
57002
+ case "ultra11y_tickets":
57003
+ return handleTickets(args2, cwd);
56337
57004
  case "ultra11y_check":
56338
57005
  return handleCheck(args2, cwd);
56339
57006
  case "ultra11y_verify":
@@ -56422,6 +57089,24 @@ function handlePrd(args2, cwd) {
56422
57089
  ...str2(args2.split) === "criterion" ? { grouped_by: "criterion" } : {}
56423
57090
  };
56424
57091
  }
57092
+ function handleTickets(args2, cwd) {
57093
+ const standard = standardOf(args2);
57094
+ const lang = langOf(args2);
57095
+ const grain = str2(args2.grain) ?? "criterion";
57096
+ const plan = buildTickets(audit(args2, cwd), { grain, standard, lang, baseDir: cwd });
57097
+ if (plan.error === "no-pages") {
57098
+ return { cwd, standard, grain, error: "no-pages", hint: "No page in scope \u2014 capture snapshots (render --e2e) or scan a sample (scan --sample) first." };
57099
+ }
57100
+ return {
57101
+ cwd,
57102
+ standard,
57103
+ grain,
57104
+ dry_run: true,
57105
+ tickets: plan.tickets.map((t2) => ({ title: t2.title, labels: t2.labels, severity: t2.severity, advisory: t2.advisory, scope: t2.scope, body: t2.body })),
57106
+ unattributed: plan.unattributed,
57107
+ to_file_them: `ultra11y tickets --in <audit.json> --provider github|gitlab|jira --grain ${grain}`
57108
+ };
57109
+ }
56425
57110
  function handleCheck(args2, cwd) {
56426
57111
  const text = reportText(args2, "check");
56427
57112
  const res = checkReport(text, standardOf(args2), langOf(args2));
@@ -56527,7 +57212,7 @@ function handleInit(args2, cwd) {
56527
57212
  throw new ToolError(`\`fail_on\` must be one of: error, warning, notice (got "${failOnRaw}")`);
56528
57213
  }
56529
57214
  const failOn = failOnRaw;
56530
- const enginePath = resolve8("scripts/ultra11y.mjs");
57215
+ const enginePath = resolve11("scripts/ultra11y.mjs");
56531
57216
  const written = [];
56532
57217
  if (bool(args2.hook)) written.push(writeHook(cwd, enginePath, failOn));
56533
57218
  if (bool(args2.ci)) written.push(join43(cwd, ".github/workflows/a11y.yml"));
@@ -56751,6 +57436,26 @@ var TOOLS2 = [
56751
57436
  required: ["cwd"]
56752
57437
  }
56753
57438
  },
57439
+ {
57440
+ name: "ultra11y_tickets",
57441
+ title: "Preview the tracker tickets an audit would file",
57442
+ description: "Show the GitHub/GitLab/Jira tickets this audit would open, at a granularity you choose: one per criterion (default), per page, per page+criterion, per file, or one consolidated. Returns the plan ONLY \u2014 it never creates anything; filing stays a deliberate `ultra11y tickets` run.",
57443
+ inputSchema: {
57444
+ type: "object",
57445
+ properties: {
57446
+ cwd: cwdProp,
57447
+ globs: globsProp,
57448
+ standard: standardProp,
57449
+ lang: langProp,
57450
+ grain: {
57451
+ type: "string",
57452
+ enum: ["criterion", "page", "page-criterion", "single", "file"],
57453
+ description: "What ONE ticket is. Default 'criterion'."
57454
+ }
57455
+ },
57456
+ required: ["cwd"]
57457
+ }
57458
+ },
56754
57459
  {
56755
57460
  name: "ultra11y_criteria",
56756
57461
  title: "The offline standards reference",
@@ -56929,6 +57634,11 @@ var TOOL_META2 = {
56929
57634
  ultra11y_audit: { openWorld: false },
56930
57635
  ultra11y_report: { openWorld: false },
56931
57636
  ultra11y_prd: { openWorld: false },
57637
+ // READ-ONLY and closed-world by construction: it returns the ticket PLAN and never files
57638
+ // anything, so it neither touches the project nor reaches a tracker. Filing stays a
57639
+ // deliberate `ultra11y tickets` run — an agent must not be able to open issues in
57640
+ // somebody's tracker off the back of a prompt injection.
57641
+ ultra11y_tickets: { openWorld: false },
56932
57642
  ultra11y_criteria: { openWorld: false },
56933
57643
  ultra11y_check: { openWorld: false },
56934
57644
  ultra11y_verify: { openWorld: false },
@@ -57097,13 +57807,13 @@ var DECLARED = new Set([...TOOLS2, ...WRITE_TOOLS].map((t2) => t2.name));
57097
57807
 
57098
57808
  // src/mcp/resources.ts
57099
57809
  import { existsSync as existsSync30, readdirSync as readdirSync6, readFileSync as readFileSync26, realpathSync as realpathSync5, statSync as statSync12 } from "fs";
57100
- import { basename as basename3, dirname as dirname13, join as join44, resolve as resolve9, sep as sep6 } from "path";
57810
+ import { basename as basename3, dirname as dirname13, join as join44, resolve as resolve12, sep as sep6 } from "path";
57101
57811
  import { fileURLToPath as fileURLToPath4 } from "url";
57102
57812
  var SKILL_NAME = "ultra11y";
57103
57813
  var URI_SCHEME = "skill://";
57104
57814
  function resolveSkillRoot(moduleDir) {
57105
57815
  const here = moduleDir ?? dirname13(fileURLToPath4(import.meta.url));
57106
- const candidates2 = [resolve9(here, ".."), resolve9(here, "..", "skills", SKILL_NAME), resolve9(here, "..", "..", "skills", SKILL_NAME)];
57816
+ const candidates2 = [resolve12(here, ".."), resolve12(here, "..", "skills", SKILL_NAME), resolve12(here, "..", "..", "skills", SKILL_NAME)];
57107
57817
  return candidates2.find((dir) => existsSync30(join44(dir, "SKILL.md")));
57108
57818
  }
57109
57819
  function listResources(moduleDir) {
@@ -57126,7 +57836,7 @@ function readResource(uri, moduleDir) {
57126
57836
  if (!root) throw new ResourceError("no skill payload found next to this build \u2014 nothing to read");
57127
57837
  const rel2 = uri.slice(URI_SCHEME.length);
57128
57838
  if (!rel2) throw new ResourceError("empty resource path");
57129
- const target = resolve9(root, rel2);
57839
+ const target = resolve12(root, rel2);
57130
57840
  const rootReal = realpathSync5(root);
57131
57841
  let targetReal;
57132
57842
  try {
@@ -57407,14 +58117,14 @@ function startHttpServer(opts = {}) {
57407
58117
  server.requestTimeout = 0;
57408
58118
  server.headersTimeout = 6e4;
57409
58119
  server.keepAliveTimeout = 12e4;
57410
- return new Promise((resolve10, reject) => {
58120
+ return new Promise((resolve13, reject) => {
57411
58121
  server.once("error", reject);
57412
58122
  server.listen(opts.port ?? 0, bind, () => {
57413
58123
  server.removeListener("error", reject);
57414
58124
  const addr2 = server.address();
57415
58125
  const port = typeof addr2 === "object" && addr2 ? addr2.port : opts.port ?? 0;
57416
58126
  const host = bind.includes(":") ? `[${bind}]` : bind;
57417
- resolve10({
58127
+ resolve13({
57418
58128
  server,
57419
58129
  port,
57420
58130
  url: `http://${host}:${port}${MCP_PATH}`,
@@ -57523,7 +58233,7 @@ function sendJson(res, status, body2, origin, extra = {}) {
57523
58233
  }
57524
58234
  var DRAIN_LIMIT = MAX_BODY_BYTES * 8;
57525
58235
  function readBody2(req) {
57526
- return new Promise((resolve10, reject) => {
58236
+ return new Promise((resolve13, reject) => {
57527
58237
  const chunks = [];
57528
58238
  let size = 0;
57529
58239
  let over = false;
@@ -57547,7 +58257,7 @@ function readBody2(req) {
57547
58257
  });
57548
58258
  req.on("end", () => {
57549
58259
  if (over) reject(new Error("too large"));
57550
- else resolve10(Buffer.concat(chunks).toString("utf8"));
58260
+ else resolve13(Buffer.concat(chunks).toString("utf8"));
57551
58261
  });
57552
58262
  req.on("error", reject);
57553
58263
  req.on("aborted", () => reject(new Error("client aborted the request")));
@@ -57568,7 +58278,9 @@ Usage:
57568
58278
  ultra11y audit [--captures <dir>] [--no-captures] [--require-captures] (rendered-DOM captures + .ultra11y/pages snapshots: audit real HTML)
57569
58279
  ultra11y audit [--format sarif|github] (CI: SARIF for code scanning, or inline annotations + job summary)
57570
58280
  ultra11y report --in <audit.json> [--out <dir>] [--standard <pack>] [--format sarif|github] [--lang auto|en|fr]
57571
- ultra11y prd --in <audit.json> [--out <dir>] [--split criterion] [--format audit|doc|remediation] [--no-technical] [--standard <pack>] [--gh-issues | --gh-single] [--lang auto|en|fr]
58281
+ ultra11y prd --in <audit.json> [--out <dir>] [--split criterion] [--format audit|doc|remediation] [--no-technical] [--standard <pack>] [--lang auto|en|fr]
58282
+ ultra11y tickets --in <audit.json> [--provider auto|github|gitlab|jira] [--grain criterion|page|page-criterion|single|file] [--transport auto|cli|rest]
58283
+ ultra11y tickets [--out <dir>] [--max-tickets <n>] [--dry-run] [--json] [--standard <pack>] [--format audit|remediation] [--lang auto|en|fr]
57572
58284
  ultra11y render [<dir>] [--scaffold | --setup | --e2e | --coverage | --storybook] [--runner playwright|cypress|auto] [--captures <dir>] [--out <file>] [--json] [--lang auto|en|fr]
57573
58285
  ultra11y criteria [<sc>] [--list] [--standard <pack> [--theme <N>]] [--generate] [--json] [--lang auto|en|fr]
57574
58286
  ultra11y criteria --standard <pack> --glossary [<term>] (the terms the standard DEFINES \u2014 its tests depend on them)
@@ -57624,9 +58336,14 @@ Commands:
57624
58336
  active standard's vocabulary (RGAA "Th\xE9matique/Crit\xE8re/Test", WCAG core
57625
58337
  "Principle\xB7Guideline/Success criterion/Technique") \u2014 theme, criterion +
57626
58338
  official wording, test(s), WCAG mapping + level, finding, expected state,
57627
- verification. --split criterion writes one file per criterion;
57628
- --gh-issues files one de-duplicated GitHub issue per criterion, or
57629
- --gh-single files the whole audit as a single issue (gh CLI).
58339
+ verification. --split criterion writes one file per criterion.
58340
+ It writes MARKDOWN only \u2014 filing tickets is the 'tickets' command.
58341
+ tickets File the audit as TRACKER TICKETS \u2014 GitHub, GitLab or Jira. Reads an
58342
+ audit.json and pushes; it writes no markdown, exactly as prd/report
58343
+ write markdown and push nothing. --grain chooses what ONE ticket is:
58344
+ per criterion (default), per page, per page+criterion, per file, or one
58345
+ consolidated. De-dupe is by exact title, so re-running never duplicates.
58346
+ --dry-run prints the plan without creating anything.
57630
58347
  render Get RENDERED HTML to audit (so component libraries like DSFR are
57631
58348
  checked as the HTML they emit, not their JSX sources): detect the
57632
58349
  framework and print the build\u2192audit recipe, or --scaffold a
@@ -57794,8 +58511,14 @@ Options:
57794
58511
  plus <out>/index.md (recommended past 3\u20134 pages: RGAA is 106 criteria)
57795
58512
  --no-technical prd (audit format): omit the technical ticket sections (Partie
57796
58513
  technique + Contexte de reproduction) for a pure-auditor block
57797
- --gh-issues prd: also create one GitHub issue per criterion via the gh CLI (opt-in)
57798
- --gh-single prd: file the whole audit as ONE consolidated GitHub issue (opt-in; wins over --gh-issues)
58514
+ --provider <id> tickets: auto|github|gitlab|jira. 'auto' reads ULTRA11Y_TICKET_PROVIDER,
58515
+ then .ultra11yrc.json, then the git remote (Jira is never auto-detected)
58516
+ --grain <mode> tickets: what one ticket is \u2014 criterion (default) | page | page-criterion | single | file
58517
+ --transport <t> tickets: auto|cli|rest (mcp: stdio|http). 'auto' prefers the CLI (gh/glab),
58518
+ falling back to REST when only a token is available. Jira is REST-only
58519
+ --max-tickets <n> tickets: refuse to file more than n tickets in one run (default 200)
58520
+ tickets --out <dir> also writes the tracker-agnostic set to
58521
+ <dir>/issues-<date>.json, for a workflow engine to file itself
57799
58522
  --scaffold render: write an SSR-snapshot harness (default: ultra11y-render.tsx)
57800
58523
  --setup render: install the zero-touch test-render capture harvester (.ultra11y/capture-setup.mjs) + print the runner wiring
57801
58524
  --coverage render: report rendered-capture coverage (covered vs blind-spot components); with --json emits the coverage object
@@ -57904,6 +58627,7 @@ var COMMANDS = [
57904
58627
  "audit",
57905
58628
  "report",
57906
58629
  "prd",
58630
+ "tickets",
57907
58631
  "render",
57908
58632
  "criteria",
57909
58633
  "check",
@@ -57929,6 +58653,9 @@ function isCommand(s) {
57929
58653
  }
57930
58654
  var VALUE_FLAGS2 = /* @__PURE__ */ new Set([
57931
58655
  "out",
58656
+ "provider",
58657
+ "grain",
58658
+ "max-tickets",
57932
58659
  "in",
57933
58660
  "include",
57934
58661
  "exclude",
@@ -58010,8 +58737,6 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
58010
58737
  "semantic",
58011
58738
  "manual",
58012
58739
  "no-technical",
58013
- "gh-issues",
58014
- "gh-single",
58015
58740
  "override",
58016
58741
  "local",
58017
58742
  "docker",
@@ -58313,9 +59038,9 @@ async function cmdDev(p) {
58313
59038
  }
58314
59039
  console.error(fr ? `ultra11y dev : tableau de bord sur http://127.0.0.1:${server.port}` : `ultra11y dev: dashboard on http://127.0.0.1:${server.port}`);
58315
59040
  console.error(fr ? "Boucle locale uniquement (l'outil \xE9crit des fichiers). Ctrl-C pour arr\xEAter." : "Loopback only (the tool writes files). Ctrl-C to stop.");
58316
- await new Promise((resolve10) => {
59041
+ await new Promise((resolve13) => {
58317
59042
  const stop2 = () => {
58318
- void server.close().then(resolve10);
59043
+ void server.close().then(resolve13);
58319
59044
  };
58320
59045
  process.once("SIGINT", stop2);
58321
59046
  process.once("SIGTERM", stop2);
@@ -58785,31 +59510,207 @@ async function cmdPrd(p) {
58785
59510
  const paths = writePrd(result, { out: out2, lang, split, format, standard, technical });
58786
59511
  const json = p.flags.json === true;
58787
59512
  if (!json) for (const path of paths) console.log(path);
58788
- const ghMode = p.flags["gh-single"] === true ? "single" : p.flags["gh-issues"] === true ? "per-criterion" : null;
58789
- let gh2;
58790
- if (ghMode) {
58791
- const flag = ghMode === "single" ? "--gh-single" : "--gh-issues";
58792
- const units = prdUnits(result, standard, lang);
58793
- if (!ghAvailable()) {
58794
- if (!json)
58795
- console.error(`ultra11y prd: ${flag} skipped \u2014 \`gh\` is not installed or not authenticated (run \`gh auth login\`). Markdown was still written.`);
58796
- } else if (units.length === 0) {
58797
- if (!json) console.error(`ultra11y prd: ${flag} skipped \u2014 no findings to file.`);
58798
- } else {
58799
- const issueFormat = format === "remediation" ? "remediation" : "audit";
58800
- gh2 = ghMode === "single" ? pushSingleIssue(units, lang, standard, issueFormat) : pushIssues(units, lang, standard, issueFormat);
58801
- if (!json)
58802
- console.log(
58803
- lang === "fr" ? `ultra11y prd : issues GitHub \u2014 ${gh2.created} cr\xE9\xE9e(s), ${gh2.skipped} d\xE9j\xE0 existante(s)${gh2.failed ? `, ${gh2.failed} en \xE9chec` : ""}.` : `ultra11y prd: GitHub issues \u2014 ${gh2.created} created, ${gh2.skipped} already existed${gh2.failed ? `, ${gh2.failed} failed` : ""}.`
58804
- );
58805
- if (gh2.failed && !json) {
58806
- if (gh2.errors.length) for (const e of gh2.errors) console.error(lang === "fr" ? `ultra11y prd : gh a \xE9chou\xE9 \u2014 ${e}` : `ultra11y prd: gh failed \u2014 ${e}`);
58807
- else console.error(lang === "fr" ? `ultra11y prd : gh a \xE9chou\xE9 sans message d'erreur.` : `ultra11y prd: gh failed with no error output.`);
58808
- }
59513
+ if (json) console.log(JSON.stringify({ paths, units: prdUnits(result, standard, lang) }, null, 2));
59514
+ return 0;
59515
+ }
59516
+ var REMOVED_FLAGS = {
59517
+ "gh-issues": "ultra11y tickets --in <audit.json> --provider github --grain criterion",
59518
+ "gh-single": "ultra11y tickets --in <audit.json> --provider github --grain single",
59519
+ // The tracker-agnostic export moved WITH ticket filing: `tickets --out` writes the same
59520
+ // envelope (and a superset of the payload) at any grain, so an orchestrator still reads
59521
+ // one stable path — just not from the command that renders documents.
59522
+ "issues-json": "ultra11y tickets --in <audit.json> --out <dir> --grain criterion"
59523
+ };
59524
+ var SECRET_CONFIG_KEYS = ["token", "apiToken", "api_token", "password", "secret"];
59525
+ async function cmdTickets(p) {
59526
+ const standard = stdOf(p, "tickets");
59527
+ if (standard === null) return 2;
59528
+ const inFlag = p.flags.in;
59529
+ if (typeof inFlag !== "string" || !inFlag) {
59530
+ console.error("ultra11y tickets: --in <audit.json> is required ('-' for stdin). Run `audit --out` first.");
59531
+ return 2;
59532
+ }
59533
+ const raw = inFlag === "-" ? await readStdin() : readInputFile(inFlag, "tickets", "--in");
59534
+ if (raw === null) return 2;
59535
+ let result;
59536
+ try {
59537
+ result = JSON.parse(raw);
59538
+ } catch {
59539
+ console.error("ultra11y tickets: --in is not valid JSON (expected an AuditResult).");
59540
+ return 2;
59541
+ }
59542
+ if (!isCurrentAudit(result)) {
59543
+ console.error("ultra11y tickets: input is not a current ultra11y AuditResult (WCAG-keyed, schema v2). Re-run `audit`.");
59544
+ return 2;
59545
+ }
59546
+ const lang = resolveLang(p.flags, { audit: result, standard });
59547
+ const fr = lang === "fr";
59548
+ const json = p.flags.json === true;
59549
+ const dryRun = p.flags["dry-run"] === true;
59550
+ let config;
59551
+ try {
59552
+ config = loadConfig(process.cwd())?.tickets;
59553
+ } catch (e) {
59554
+ console.error(`ultra11y tickets: ${e instanceof Error ? e.message : String(e)}`);
59555
+ return 2;
59556
+ }
59557
+ for (const key of SECRET_CONFIG_KEYS) {
59558
+ if (config && key in config) {
59559
+ console.error(
59560
+ `ultra11y tickets: .ultra11yrc.json must not carry a "${key}" \u2014 credentials belong in the environment (see references/tickets.md). Remove it and rotate that value.`
59561
+ );
59562
+ return 2;
58809
59563
  }
58810
59564
  }
58811
- if (json) console.log(JSON.stringify({ paths, units: prdUnits(result, standard, lang), ...gh2 ? { gh: gh2 } : {} }, null, 2));
58812
- return gh2 && gh2.failed > 0 ? 1 : 0;
59565
+ const grainFlag = typeof p.flags.grain === "string" ? p.flags.grain : config?.grain ?? "criterion";
59566
+ if (!ALL_GRAINS.includes(grainFlag)) {
59567
+ console.error(`ultra11y tickets: --grain must be one of ${ALL_GRAINS.join("|")} (got "${grainFlag}").`);
59568
+ return 2;
59569
+ }
59570
+ const transportFlag = typeof p.flags.transport === "string" ? p.flags.transport : config?.transport ?? "auto";
59571
+ if (!["auto", "cli", "rest"].includes(transportFlag)) {
59572
+ console.error(`ultra11y tickets: --transport must be auto, cli or rest (got "${transportFlag}").`);
59573
+ return 2;
59574
+ }
59575
+ const maxTickets = Number.parseInt(String(p.flags["max-tickets"] ?? config?.maxTickets ?? 200), 10);
59576
+ if (!Number.isFinite(maxTickets) || maxTickets < 1) {
59577
+ console.error("ultra11y tickets: --max-tickets must be a positive integer.");
59578
+ return 2;
59579
+ }
59580
+ const providerFlag = typeof p.flags.provider === "string" ? p.flags.provider : "auto";
59581
+ const providerId = providerFlag === "auto" ? autoProvider(process.env, config?.provider) : isProviderId(providerFlag) ? providerFlag : void 0;
59582
+ if (!providerId) {
59583
+ console.error(
59584
+ providerFlag === "auto" ? "ultra11y tickets: could not tell which tracker to file into \u2014 pass --provider github|gitlab|jira (Jira is never auto-detected: it owns no git remote)." : `ultra11y tickets: --provider "${providerFlag}" is not one of ${ALL_PROVIDERS.join("|")}.`
59585
+ );
59586
+ return 2;
59587
+ }
59588
+ const provider = createProvider(providerId, { transport: transportFlag });
59589
+ const format = p.flags.format === "remediation" ? "remediation" : "audit";
59590
+ const plan = buildTickets(result, {
59591
+ grain: grainFlag,
59592
+ standard,
59593
+ lang,
59594
+ format,
59595
+ bodyLimit: provider.capabilities.bodyLimit,
59596
+ baseDir: process.cwd(),
59597
+ technical: p.flags["no-technical"] !== true
59598
+ });
59599
+ if (plan.error === "no-pages") {
59600
+ console.error(
59601
+ fr ? "ultra11y tickets : aucune page dans le p\xE9rim\xE8tre. Capturez des instantan\xE9s (render --e2e) ou scannez un \xE9chantillon (scan --sample) avant d'utiliser --grain page." : "ultra11y tickets: no page in scope. Capture snapshots (render --e2e) or scan a sample (scan --sample) before using --grain page."
59602
+ );
59603
+ return 1;
59604
+ }
59605
+ if (plan.tickets.length > maxTickets) {
59606
+ console.error(
59607
+ fr ? `ultra11y tickets : ${plan.tickets.length} tickets pour --grain ${grainFlag}, au-del\xE0 de la limite de ${maxTickets}. Relancez avec --max-tickets ${plan.tickets.length}, un grain plus grossier, ou --dry-run pour inspecter.` : `ultra11y tickets: ${plan.tickets.length} tickets for --grain ${grainFlag}, past the limit of ${maxTickets}. Re-run with --max-tickets ${plan.tickets.length}, a coarser grain, or --dry-run to inspect.`
59608
+ );
59609
+ return 2;
59610
+ }
59611
+ const ticketsOut = typeof p.flags.out === "string" && p.flags.out ? p.flags.out : void 0;
59612
+ let setPath;
59613
+ if (ticketsOut) {
59614
+ mkdirSync15(ticketsOut, { recursive: true });
59615
+ setPath = join45(ticketsOut, `issues-${result.date}.json`);
59616
+ const payload = {
59617
+ tool: "ultra11y",
59618
+ kind: "issues",
59619
+ schemaVersion: TICKET_SET_SCHEMA_VERSION,
59620
+ standard,
59621
+ grain: grainFlag,
59622
+ date: result.date,
59623
+ count: plan.tickets.length,
59624
+ issues: plan.tickets
59625
+ };
59626
+ writeFileSync17(setPath, `${JSON.stringify(payload, null, 2)}
59627
+ `);
59628
+ if (!json) console.log(setPath);
59629
+ }
59630
+ if (!plan.tickets.length) {
59631
+ if (!json)
59632
+ console.log(
59633
+ fr ? "ultra11y tickets : rien \xE0 d\xE9poser \u2014 l'audit ne rel\xE8ve aucune non-conformit\xE9." : "ultra11y tickets: nothing to file \u2014 the audit found no non-conformity."
59634
+ );
59635
+ if (json)
59636
+ console.log(
59637
+ JSON.stringify(
59638
+ {
59639
+ provider: providerId,
59640
+ transport: provider.transport,
59641
+ grain: grainFlag,
59642
+ standard,
59643
+ dryRun,
59644
+ ...setPath ? { setPath } : {},
59645
+ tickets: [],
59646
+ unattributed: plan.unattributed,
59647
+ result: { created: 0, skipped: 0, failed: 0, createdTitles: [], createdUrls: [], errors: [] }
59648
+ },
59649
+ null,
59650
+ 2
59651
+ )
59652
+ );
59653
+ return 0;
59654
+ }
59655
+ if (!provider.available() && !dryRun) {
59656
+ console.error(`ultra11y tickets: ${providerId} is not usable here \u2014 ${provider.unavailableReason()}.`);
59657
+ return 1;
59658
+ }
59659
+ const { plan: planned, result: pushed, dedupeChecked } = await pushTickets(plan.tickets, provider, { dryRun: dryRun || !provider.available() });
59660
+ if (json) {
59661
+ console.log(
59662
+ JSON.stringify(
59663
+ {
59664
+ provider: providerId,
59665
+ transport: provider.transport,
59666
+ grain: grainFlag,
59667
+ standard,
59668
+ dryRun,
59669
+ ...setPath ? { setPath } : {},
59670
+ dedupeChecked,
59671
+ tickets: planned.map(({ ticket, action }) => ({
59672
+ title: ticket.title,
59673
+ labels: ticket.labels,
59674
+ severity: ticket.severity,
59675
+ advisory: ticket.advisory,
59676
+ scope: ticket.scope,
59677
+ bodyChars: ticket.body.length,
59678
+ action,
59679
+ // The body rides along ONLY under --dry-run: an agent inspecting the plan wants
59680
+ // it, a 200-ticket push payload does not.
59681
+ ...dryRun ? { body: ticket.body } : {}
59682
+ })),
59683
+ unattributed: plan.unattributed,
59684
+ result: pushed
59685
+ },
59686
+ null,
59687
+ 2
59688
+ )
59689
+ );
59690
+ } else if (dryRun || !provider.available()) {
59691
+ const create = planned.filter((x) => x.action === "create").length;
59692
+ console.log(
59693
+ fr ? `ultra11y tickets : simulation (${providerId}/${provider.transport}, grain ${grainFlag}) \u2014 ${create} \xE0 cr\xE9er, ${pushed.skipped} d\xE9j\xE0 pr\xE9sent(s).` : `ultra11y tickets: dry run (${providerId}/${provider.transport}, grain ${grainFlag}) \u2014 ${create} to create, ${pushed.skipped} already there.`
59694
+ );
59695
+ for (const { ticket, action } of planned) console.log(` ${action === "create" ? "+" : "="} ${ticket.title}`);
59696
+ if (!dedupeChecked)
59697
+ console.error(
59698
+ fr ? "ultra11y tickets : l'existant n'a pas pu \xEAtre list\xE9 \u2014 le compte \xAB d\xE9j\xE0 pr\xE9sent \xBB n'est pas v\xE9rifi\xE9." : 'ultra11y tickets: could not list existing tickets \u2014 the "already there" count is unverified.'
59699
+ );
59700
+ if (!provider.available()) console.error(`ultra11y tickets: ${providerId} \u2014 ${provider.unavailableReason()}.`);
59701
+ } else {
59702
+ console.log(
59703
+ fr ? `ultra11y tickets : ${providerId} \u2014 ${pushed.created} cr\xE9\xE9(s), ${pushed.skipped} d\xE9j\xE0 pr\xE9sent(s)${pushed.failed ? `, ${pushed.failed} en \xE9chec` : ""}.` : `ultra11y tickets: ${providerId} \u2014 ${pushed.created} created, ${pushed.skipped} already there${pushed.failed ? `, ${pushed.failed} failed` : ""}.`
59704
+ );
59705
+ for (const u of pushed.createdUrls) console.log(` ${u}`);
59706
+ for (const e of pushed.errors) console.error(fr ? `ultra11y tickets : \xE9chec \u2014 ${e}` : `ultra11y tickets: failed \u2014 ${e}`);
59707
+ if (plan.unattributed) {
59708
+ console.error(
59709
+ fr ? `ultra11y tickets : ${plan.unattributed} constat(s) non rattach\xE9(s) \xE0 une page \u2014 d\xE9pos\xE9s dans leur propre ticket, jamais r\xE9partis d'office.` : `ultra11y tickets: ${plan.unattributed} finding(s) attributed to no page \u2014 filed as their own ticket, never spread.`
59710
+ );
59711
+ }
59712
+ }
59713
+ return pushed.failed > 0 ? 1 : 0;
58813
59714
  }
58814
59715
  function depsAt(root) {
58815
59716
  const pkgPath = join45(root, "package.json");
@@ -59204,7 +60105,22 @@ function applyAdjudicationFile(p, adj, lang) {
59204
60105
  mkdirSync15(out2, { recursive: true });
59205
60106
  const auditPath = join45(out2, "audit-latest.json");
59206
60107
  writeFileSync17(auditPath, JSON.stringify(r.audit, null, 2) + "\n");
59207
- if (p.flags.json) console.log(JSON.stringify({ ok: true, auditPath, applied: r.applied, stillManual: r.stillManual, grounding: r.grounding }, null, 2));
60108
+ if (p.flags.json)
60109
+ console.log(
60110
+ JSON.stringify(
60111
+ {
60112
+ ok: true,
60113
+ auditPath,
60114
+ applied: r.applied,
60115
+ stillManual: r.stillManual,
60116
+ conformancePct: r.audit.conformancePct,
60117
+ findings: r.audit.findings.length,
60118
+ grounding: r.grounding
60119
+ },
60120
+ null,
60121
+ 2
60122
+ )
60123
+ );
59208
60124
  else
59209
60125
  console.log(
59210
60126
  lang === "fr" ? `\u2713 ${r.applied} crit\xE8re(s) adjug\xE9(s), ${r.stillManual} laiss\xE9(s) en r\xE9siduel \u2192 ${auditPath}` : `\u2713 ${r.applied} criterion(ia) adjudicated, ${r.stillManual} left residual \u2192 ${auditPath}`
@@ -59758,13 +60674,25 @@ async function main(argv) {
59758
60674
  console.log(HELP2);
59759
60675
  return 0;
59760
60676
  }
60677
+ for (const f of p.unknown) {
60678
+ const replacement = REMOVED_FLAGS[f];
60679
+ if (replacement) {
60680
+ console.error(`ultra11y: --${f} was removed in v3 \u2014 ticket creation is now its own command. Use: ${replacement}`);
60681
+ return 2;
60682
+ }
60683
+ }
59761
60684
  for (const f of p.unknown) console.error(`ultra11y: unknown flag --${f} (ignored). Run \`ultra11y --help\`.`);
59762
60685
  const ENUM_FLAGS = {
59763
60686
  lang: ["auto", "en", "fr"],
59764
60687
  dedup: ["exact", "normalized", "off"],
59765
60688
  format: ["audit", "doc", "remediation", "sarif", "github", "grid", "report"],
59766
60689
  split: ["criterion", "page"],
59767
- runtime: ["auto", "local", "docker"]
60690
+ runtime: ["auto", "local", "docker"],
60691
+ provider: ["auto", "github", "gitlab", "jira"],
60692
+ grain: ["criterion", "page", "page-criterion", "single", "file"],
60693
+ // The union over every command that takes it: `mcp` serves stdio|http, `tickets`
60694
+ // picks cli|rest. Listing one command's values made the guard cry wolf on the other.
60695
+ transport: ["stdio", "http", "auto", "cli", "rest"]
59768
60696
  };
59769
60697
  for (const [flag, allowed] of Object.entries(ENUM_FLAGS)) {
59770
60698
  const v = p.flags[flag];
@@ -59782,6 +60710,8 @@ async function main(argv) {
59782
60710
  return cmdAudit(p);
59783
60711
  case "report":
59784
60712
  return cmdReport(p);
60713
+ case "tickets":
60714
+ return cmdTickets(p);
59785
60715
  case "prd":
59786
60716
  return cmdPrd(p);
59787
60717
  case "render":
@@ -59850,6 +60780,7 @@ if (isInvokedDirectly()) {
59850
60780
  }
59851
60781
  export {
59852
60782
  COMMANDS,
60783
+ REMOVED_FLAGS,
59853
60784
  main,
59854
60785
  parseArgs
59855
60786
  };