ultra11y 2.32.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.opencode/plugins/ultra11y.js +1 -1
  5. package/README.md +14 -2
  6. package/package.json +1 -1
  7. package/scripts/ultra11y.mjs +1133 -222
  8. package/skills/review-a11y/SKILL.md +1 -1
  9. package/skills/review-a11y/scripts/ultra11y.mjs +1133 -222
  10. package/skills/ultra11y/SKILL.md +17 -11
  11. package/skills/ultra11y/references/audit.md +1 -1
  12. package/skills/ultra11y/references/ci.md +63 -0
  13. package/skills/ultra11y/references/judgment.md +8 -0
  14. package/skills/ultra11y/references/methodology.md +1 -1
  15. package/skills/ultra11y/references/orchestrators.md +26 -11
  16. package/skills/ultra11y/references/prd.md +9 -36
  17. package/skills/ultra11y/references/tickets.md +156 -0
  18. package/skills/ultra11y/scripts/ultra11y.mjs +1133 -222
  19. package/src/cli.ts +386 -63
  20. package/src/config.ts +10 -0
  21. package/src/gh-cli.ts +39 -0
  22. package/src/mcp/handlers.ts +28 -0
  23. package/src/mcp/tools.ts +27 -0
  24. package/src/pages.ts +13 -0
  25. package/src/pr-comment.ts +78 -0
  26. package/src/tickets/grain.ts +218 -0
  27. package/src/tickets/http.ts +107 -0
  28. package/src/tickets/providers/github.ts +155 -0
  29. package/src/tickets/providers/gitlab.ts +158 -0
  30. package/src/tickets/providers/jira.ts +152 -0
  31. package/src/tickets/providers/remote.ts +40 -0
  32. package/src/tickets/push.ts +94 -0
  33. package/src/tickets/registry.ts +45 -0
  34. package/src/tickets/render.ts +157 -0
  35. package/src/tickets/types.ts +154 -0
  36. package/src/types.ts +1 -1
  37. package/src/gh.ts +0 -333
@@ -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.32.0";
9
+ var VERSION = "3.0.0";
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,137 +49784,662 @@ function issueBody(unit, lang, standard = "wcag", format = "audit") {
49698
49784
  }
49699
49785
  return lines.join("\n");
49700
49786
  }
49701
- function issueLabels(unit, standard = "wcag") {
49702
- const { tag } = standardTag(standard);
49703
- return unit.advisory ? ["accessibility", tag, "recommendation", unit.severity] : ["accessibility", tag, unit.severity];
49704
- }
49705
- function issueSet(units, lang, standard = "wcag", format = "audit") {
49706
- const { label } = standardTag(standard);
49707
- return units.map((u) => ({
49708
- criteriaId: u.criteriaId,
49709
- title: issueTitle(u, label),
49710
- body: issueBody(u, lang, standard, format),
49711
- labels: issueLabels(u, standard),
49712
- severity: u.severity,
49713
- advisory: u.advisory === true,
49714
- occurrences: u.findings.map((f) => ({ file: f.file, line: f.line, selector: f.selectorHint, message: resolveMessage(f, lang) }))
49715
- }));
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();
49716
49790
  }
49717
- function createIssue(title2, body2, labels) {
49718
- const base = ["issue", "create", "--title", title2, "--body-file", "-"];
49719
- try {
49720
- gh([...base, "--label", labels.join(",")], body2);
49721
- return { ok: true };
49722
- } catch (labelledErr) {
49723
- try {
49724
- gh(base, body2);
49725
- return { ok: true };
49726
- } catch (err2) {
49727
- return { ok: false, reason: ghErrorReason(err2) ?? ghErrorReason(labelledErr) };
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
+ });
49728
49899
  }
49900
+ return { tickets: tickets2, unattributed: 0 };
49729
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
+ });
49954
+ }
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: [] };
49730
49980
  }
49731
49981
  function recordFailure(result, reason) {
49732
49982
  result.failed++;
49733
49983
  if (reason && !result.errors.includes(reason)) result.errors.push(reason);
49734
49984
  }
49735
- function pushIssues(units, lang, standard = "wcag", format = "audit") {
49736
- const { label } = standardTag(standard);
49737
- const existing = existingIssueTitles();
49738
- const result = { created: 0, skipped: 0, failed: 0, createdTitles: [], errors: [] };
49739
- for (const u of units) {
49740
- const title2 = issueTitle(u, label);
49741
- 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") {
49742
50003
  result.skipped++;
49743
50004
  continue;
49744
50005
  }
49745
- const r = createIssue(title2, issueBody(u, lang, standard, format), issueLabels(u, standard));
49746
- 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) {
49747
50013
  result.created++;
49748
- result.createdTitles.push(title2);
49749
- existing.add(title2);
50014
+ result.createdTitles.push(ticket.title);
50015
+ if (outcome2.url) result.createdUrls.push(outcome2.url);
49750
50016
  } else {
49751
- recordFailure(result, r.reason);
50017
+ recordFailure(result, outcome2.reason);
49752
50018
  }
49753
50019
  }
49754
- return result;
50020
+ return { plan, result, dedupeChecked };
49755
50021
  }
49756
- function singleIssueTitle(label = "WCAG") {
49757
- 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;
49758
50045
  }
49759
- function singleIssueBody(units, lang, standard = "wcag", format = "audit") {
49760
- const { label } = standardTag(standard);
49761
- 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.`;
49762
- const lines = [intro, ""];
49763
- for (const sev of SEV_ORDER4) {
49764
- const group = units.filter((u) => u.severity === sev);
49765
- if (!group.length) continue;
49766
- lines.push(`## ${ICON4[sev]} ${SEV_LABEL2[lang][sev]} (${group.length})`, "");
49767
- for (const u of group) {
49768
- if (format === "audit") lines.push(...renderAuditorUnit(u, standard, lang, { heading: "###" }));
49769
- 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 };
49770
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));
49771
50086
  }
49772
- return lines.join("\n");
49773
50087
  }
49774
- function pushSingleIssue(units, lang, standard = "wcag", format = "audit") {
49775
- const { label, tag } = standardTag(standard);
49776
- const result = { created: 0, skipped: 0, failed: 0, createdTitles: [], errors: [] };
49777
- if (!units.length) return result;
49778
- const title2 = singleIssueTitle(label);
49779
- if (existingIssueTitles().has(title2)) {
49780
- result.skipped = 1;
49781
- return result;
49782
- }
49783
- const severity = [...units].sort((a, b) => SEV_RANK2[a.severity] - SEV_RANK2[b.severity])[0].severity;
49784
- const r = createIssue(title2, singleIssueBody(units, lang, standard, format), ["accessibility", tag, severity]);
49785
- if (r.ok) {
49786
- result.created = 1;
49787
- result.createdTitles.push(title2);
49788
- } else {
49789
- 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;
49790
50097
  }
49791
- return result;
49792
50098
  }
49793
- function COMMENT_MARKER(standard) {
49794
- 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;
49795
50115
  }
49796
- function stickyBody(markdown, standard) {
49797
- return `${COMMENT_MARKER(standard)}
49798
- ${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
+ };
49799
50144
  }
49800
- function prNumberFromEnv(env = process.env) {
49801
- const explicit = Number.parseInt(env.ULTRA11Y_PR ?? "", 10);
49802
- if (Number.isFinite(explicit) && explicit > 0) return explicit;
49803
- const m = /^refs\/pull\/(\d+)\//.exec(env.GITHUB_REF ?? "");
49804
- const n = m ? Number.parseInt(m[1], 10) : Number.NaN;
49805
- 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
+ };
49806
50225
  }
49807
- function pickExistingComment(comments, marker) {
49808
- 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"] });
49809
50232
  }
49810
- function pushPrComment(markdown, standard = "wcag") {
49811
- const pr = prNumberFromEnv();
49812
- if (pr === void 0) return { ok: true, action: "skipped", reason: "not a pull-request run" };
49813
- const marker = COMMENT_MARKER(standard);
49814
- const body2 = stickyBody(markdown, standard);
50233
+ function glabAvailable() {
49815
50234
  try {
49816
- const repo = gh(["repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"]).trim();
49817
- 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) {
49818
50296
  try {
49819
- const raw = gh(["api", `repos/${repo}/issues/${pr}/comments`, "--paginate"]);
49820
- existing = pickExistingComment(JSON.parse(raw), marker);
49821
- } 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) };
49822
50307
  }
49823
- if (existing) {
49824
- gh(["api", "--method", "PATCH", `repos/${repo}/issues/comments/${existing.id}`, "-f", `body=${body2}`]);
49825
- 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") };
49826
50315
  }
49827
- gh(["api", "--method", "POST", `repos/${repo}/issues/${pr}/comments`, "-f", `body=${body2}`]);
49828
- return { ok: true, action: "created" };
49829
- } catch (e) {
49830
- return { ok: false, action: "skipped", reason: ghErrorReason(e) };
49831
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();
49832
50443
  }
49833
50444
 
49834
50445
  // src/render.ts
@@ -49860,7 +50471,7 @@ function detectFrameworks(deps, has2) {
49860
50471
  for (const [pkg, label] of Object.entries(KNOWN_LIBS)) if (dep(pkg)) componentLibraries.push(label);
49861
50472
  return { frameworks, componentLibraries };
49862
50473
  }
49863
- var L5 = {
50474
+ var L6 = {
49864
50475
  fr: {
49865
50476
  title: "Obtenir du HTML rendu \xE0 auditer (render)",
49866
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.",
@@ -49891,7 +50502,7 @@ var L5 = {
49891
50502
  }
49892
50503
  };
49893
50504
  function renderPlan(d, lang = "fr") {
49894
- const s = L5[lang];
50505
+ const s = L6[lang];
49895
50506
  const out2 = [`# ${s.title}`, "", `> ${s.why}`, ""];
49896
50507
  if (d.componentLibraries.length) out2.push(`> \u{1F9E9} ${s.libNote(d.componentLibraries.join(", "))}`, "");
49897
50508
  out2.push(`## ${s.detected}`, "");
@@ -50436,7 +51047,7 @@ function writeWorklist(items, outDir, semantic, standard = "wcag", lang = "en")
50436
51047
 
50437
51048
  // src/grounding.ts
50438
51049
  import { existsSync as existsSync17, readFileSync as readFileSync14 } from "fs";
50439
- import { resolve as resolve4 } from "path";
51050
+ import { resolve as resolve7 } from "path";
50440
51051
  var WINDOW = 10;
50441
51052
  var norm2 = (s) => s.replace(/\s+/g, " ").trim();
50442
51053
  var isUnresolvable = (file) => !file || file === "-" || file === "<stdin>" || file === "stdin";
@@ -50453,7 +51064,7 @@ function selectorProbes(selector) {
50453
51064
  }
50454
51065
  function groundFinding(g, opts = {}) {
50455
51066
  if (isUnresolvable(g.file)) return { ok: true, moved: false };
50456
- const path = resolve4(opts.cwd ?? process.cwd(), g.file);
51067
+ const path = resolve7(opts.cwd ?? process.cwd(), g.file);
50457
51068
  if (!existsSync17(path)) return { ok: false, moved: false, issue: `cited file not found: ${g.file}` };
50458
51069
  let text;
50459
51070
  try {
@@ -50508,7 +51119,7 @@ function isPagesReport(md) {
50508
51119
  return md.slice(0, 400).includes(`<!-- ${PAGES_REPORT_MARKER}`);
50509
51120
  }
50510
51121
  var MARK2 = { C: "C", NC: "NC", NA: "\u2014", manual: "?" };
50511
- var L6 = {
51122
+ var L7 = {
50512
51123
  fr: {
50513
51124
  docTitle: "Rapport d'accessibilit\xE9 page par page",
50514
51125
  indexTitle: "Rapport page par page \u2014 index",
@@ -50628,7 +51239,7 @@ function renderPageReport(result, page, opts = {}) {
50628
51239
  const standard = opts.standard ?? CORE2;
50629
51240
  const lang = opts.lang ?? "en";
50630
51241
  const h = opts.heading ?? "##";
50631
- const s = L6[lang];
51242
+ const s = L7[lang];
50632
51243
  const out2 = [];
50633
51244
  out2.push(`${h} ${page.name}${page.auth ? " \u{1F512}" : ""}`, "");
50634
51245
  const meta2 = [];
@@ -50674,7 +51285,7 @@ function renderPageReport(result, page, opts = {}) {
50674
51285
  return out2.join("\n");
50675
51286
  }
50676
51287
  function header2(result, pages, standard, lang, title2) {
50677
- const s = L6[lang];
51288
+ const s = L7[lang];
50678
51289
  return [
50679
51290
  pagesReportMarker(standard),
50680
51291
  "",
@@ -50689,7 +51300,7 @@ function header2(result, pages, standard, lang, title2) {
50689
51300
  function renderPagesIndex(result, pages, opts = {}) {
50690
51301
  const standard = opts.standard ?? CORE2;
50691
51302
  const lang = opts.lang ?? "en";
50692
- const s = L6[lang];
51303
+ const s = L7[lang];
50693
51304
  const out2 = header2(result, pages, standard, lang, s.indexTitle);
50694
51305
  out2.push(`> ${s.indexNote}`, "");
50695
51306
  out2.push(`| ${s.page} | ${s.url} | ${s.basis} | ${s.rate} | ${s.blocking} | ${s.major} | ${s.minor} | ${s.sheet} |`);
@@ -50717,7 +51328,7 @@ function renderPagesDocument(result, pages, opts = {}) {
50717
51328
  function renderPageDocument(result, page, opts = {}) {
50718
51329
  const standard = opts.standard ?? CORE2;
50719
51330
  const lang = opts.lang ?? "en";
50720
- const s = L6[lang];
51331
+ const s = L7[lang];
50721
51332
  const out2 = [pagesReportMarker(standard), "", `# ${s.docTitle} \u2014 ${page.name}`, "", `- **${s.date}** : ${result.date}`, ""];
50722
51333
  out2.push(renderPageReport(result, page, { ...opts, standard, lang, heading: "##" }));
50723
51334
  return out2.join("\n");
@@ -52423,10 +53034,10 @@ function applyRawVerdicts(items, verdicts) {
52423
53034
  }
52424
53035
 
52425
53036
  // src/scan.ts
52426
- import { execFileSync as execFileSync4 } from "child_process";
53037
+ import { execFileSync as execFileSync6 } from "child_process";
52427
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";
52428
53039
  import { tmpdir as tmpdir2 } from "os";
52429
- import { join as join33, resolve as resolve5 } from "path";
53040
+ import { join as join33, resolve as resolve8 } from "path";
52430
53041
  import { fileURLToPath as fileURLToPath3 } from "url";
52431
53042
 
52432
53043
  // src/axe-map.ts
@@ -52875,7 +53486,7 @@ ENTRYPOINT ["node", "/app/runner.mjs"]
52875
53486
  `;
52876
53487
  function dockerAvailable() {
52877
53488
  try {
52878
- execFileSync4("docker", ["info"], { stdio: "ignore", timeout: 1e4 });
53489
+ execFileSync6("docker", ["info"], { stdio: "ignore", timeout: 1e4 });
52879
53490
  return true;
52880
53491
  } catch {
52881
53492
  return false;
@@ -52883,7 +53494,7 @@ function dockerAvailable() {
52883
53494
  }
52884
53495
  function imageExists(tag) {
52885
53496
  try {
52886
- execFileSync4("docker", ["image", "inspect", tag], { stdio: "ignore" });
53497
+ execFileSync6("docker", ["image", "inspect", tag], { stdio: "ignore" });
52887
53498
  return true;
52888
53499
  } catch {
52889
53500
  return false;
@@ -52896,7 +53507,7 @@ function buildImage(tag = IMAGE_TAG) {
52896
53507
  writeFileSync11(join33(ctx, "runner.mjs"), RUNNER);
52897
53508
  writeFileSync11(join33(ctx, "package.json"), PKG);
52898
53509
  writeFileSync11(join33(ctx, "Dockerfile"), DOCKERFILE);
52899
- execFileSync4("docker", ["build", "-t", tag, ctx], { stdio: "inherit", timeout: 9e5 });
53510
+ execFileSync6("docker", ["build", "-t", tag, ctx], { stdio: "inherit", timeout: 9e5 });
52900
53511
  } finally {
52901
53512
  rmSync3(ctx, { recursive: true, force: true });
52902
53513
  }
@@ -52915,7 +53526,7 @@ function cleanDynamic(tag = IMAGE_TAG) {
52915
53526
  let imageRemoved = false;
52916
53527
  if (dockerAvailable() && imageExists(tag)) {
52917
53528
  try {
52918
- execFileSync4("docker", ["rmi", "-f", tag], { stdio: "ignore" });
53529
+ execFileSync6("docker", ["rmi", "-f", tag], { stdio: "ignore" });
52919
53530
  imageRemoved = true;
52920
53531
  } catch {
52921
53532
  }
@@ -52972,11 +53583,11 @@ var PROBE_FIELDS = [
52972
53583
  function runRunner(target, isFile, tag, snapshot = true) {
52973
53584
  const args2 = ["run", "--rm"];
52974
53585
  if (!snapshot) args2.push("-e", "ULTRA11Y_SNAPSHOT=0");
52975
- if (isFile) args2.push("-v", `${resolve5(target)}:${MOUNT}:ro`);
53586
+ if (isFile) args2.push("-v", `${resolve8(target)}:${MOUNT}:ro`);
52976
53587
  args2.push(tag, isFile ? MOUNT : target);
52977
53588
  let stdout;
52978
53589
  try {
52979
- 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"] });
52980
53591
  } catch (e) {
52981
53592
  const err2 = e;
52982
53593
  const detail = (err2.stderr ? String(err2.stderr).trim() : "") || err2.message || String(e);
@@ -53281,7 +53892,7 @@ function mergeSnapshotAudit(base, snap) {
53281
53892
  // src/scan-local.ts
53282
53893
  import { existsSync as existsSync21, statSync as statSync9 } from "fs";
53283
53894
  import { createRequire } from "module";
53284
- import { resolve as resolve6 } from "path";
53895
+ import { resolve as resolve9 } from "path";
53285
53896
  var LOCAL_ENGINE = "axe-core@playwright (local)";
53286
53897
  var LOCAL_TESTED_SCS = ["1.4.4", "1.4.10", "1.4.12", "2.4.7", "1.4.13"];
53287
53898
  function localTestedScs(interact) {
@@ -53291,7 +53902,7 @@ var PW_SPEC = "@playwright/test";
53291
53902
  var AXE_SPEC = "@axe-core/playwright";
53292
53903
  function localAvailable(cwd) {
53293
53904
  try {
53294
- const req = createRequire(resolve6(cwd, "package.json"));
53905
+ const req = createRequire(resolve9(cwd, "package.json"));
53295
53906
  req.resolve(PW_SPEC);
53296
53907
  req.resolve(AXE_SPEC);
53297
53908
  return true;
@@ -53303,7 +53914,7 @@ function resolveLocalDeps(cwd) {
53303
53914
  let chromium;
53304
53915
  let AxeBuilder;
53305
53916
  try {
53306
- const req = createRequire(resolve6(cwd, "package.json"));
53917
+ const req = createRequire(resolve9(cwd, "package.json"));
53307
53918
  const pw = req(PW_SPEC);
53308
53919
  const axeMod = req(AXE_SPEC);
53309
53920
  chromium = pw.chromium;
@@ -53782,7 +54393,7 @@ async function runOnPage(browser, AxeBuilder, target, isFile, opts) {
53782
54393
  const page = await context.newPage();
53783
54394
  const empty = [];
53784
54395
  try {
53785
- const url = isFile ? "file://" + resolve6(target) : target;
54396
+ const url = isFile ? "file://" + resolve9(target) : target;
53786
54397
  await page.goto(url, { waitUntil: "load", timeout: 45e3 });
53787
54398
  await page.waitForLoadState("networkidle", { timeout: 8e3 }).catch(() => {
53788
54399
  });
@@ -54247,12 +54858,12 @@ function fixSummary(r, lang = "fr", write = false) {
54247
54858
 
54248
54859
  // src/init.ts
54249
54860
  import { writeFileSync as writeFileSync13, mkdirSync as mkdirSync10, chmodSync, realpathSync as realpathSync2 } from "fs";
54250
- import { execFileSync as execFileSync5 } from "child_process";
54861
+ import { execFileSync as execFileSync7 } from "child_process";
54251
54862
  import { join as join34, relative as relative4, sep as sep4 } from "path";
54252
54863
  var EN_SEV = { bloquant: "blocking", majeur: "major", mineur: "minor" };
54253
54864
  function repoRoot() {
54254
54865
  try {
54255
- 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();
54256
54867
  } catch {
54257
54868
  return null;
54258
54869
  }
@@ -54970,7 +55581,7 @@ var STR = {
54970
55581
  function t(lang, key) {
54971
55582
  return STR[lang][key];
54972
55583
  }
54973
- var ICON5 = { bloquant: "\u{1F534}", majeur: "\u{1F7E0}", mineur: "\u{1F7E1}" };
55584
+ var ICON4 = { bloquant: "\u{1F534}", majeur: "\u{1F7E0}", mineur: "\u{1F7E1}" };
54974
55585
  function auditSummary(r, lang) {
54975
55586
  const lines = [];
54976
55587
  lines.push(`${t(lang, "summaryTitle")} \u2014 ${r.date}`);
@@ -54987,7 +55598,7 @@ function auditSummary(r, lang) {
54987
55598
  } else {
54988
55599
  lines.push(`${t(lang, "findingsTitle")} (${r.findings.length}) :`);
54989
55600
  for (const f of r.findings.slice(0, 20)) {
54990
- 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)}`);
54991
55602
  }
54992
55603
  if (r.findings.length > 20) lines.push(` \u2026 (+${r.findings.length - 20})`);
54993
55604
  }
@@ -55126,8 +55737,8 @@ function toSarif(result, opts = {}) {
55126
55737
 
55127
55738
  // src/annotate.ts
55128
55739
  var LEVEL2 = { bloquant: "error", majeur: "warning", mineur: "notice" };
55129
- var ICON6 = { bloquant: "\u{1F534}", majeur: "\u{1F7E0}", mineur: "\u{1F7E1}" };
55130
- 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"];
55131
55742
  function esc(s) {
55132
55743
  return s.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
55133
55744
  }
@@ -55221,13 +55832,13 @@ function stepSummary(result, opts = {}) {
55221
55832
  out2.push(perPageTable(result, standard, lang));
55222
55833
  return out2.join("\n");
55223
55834
  }
55224
- 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));
55225
55836
  out2.push(`### ${s.findings} (${sorted.length})`, "");
55226
55837
  out2.push(`| ${s.severity} | ${s.criterion} | ${s.where} | ${s.what} |`, "| --- | --- | --- | --- |");
55227
55838
  for (const f of sorted.slice(0, MAX_ROWS)) {
55228
55839
  const where = isUrl2(f.file) ? f.file : `${repoRelative(f.file, baseDir)}:${Math.max(1, f.line)}`;
55229
55840
  const msg = resolveMessage(f, lang).replace(/\|/g, "\\|");
55230
- 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} |`);
55231
55842
  }
55232
55843
  if (sorted.length > MAX_ROWS) out2.push("", s.more(sorted.length - MAX_ROWS));
55233
55844
  out2.push("");
@@ -55690,10 +56301,10 @@ function startDevServer(opts) {
55690
56301
  }
55691
56302
  res.writeHead(404, { "content-type": "text/plain" }).end("not found");
55692
56303
  });
55693
- return new Promise((resolve10, reject) => {
56304
+ return new Promise((resolve13, reject) => {
55694
56305
  server.once("error", reject);
55695
56306
  server.listen(opts.port, "127.0.0.1", () => {
55696
- resolve10({
56307
+ resolve13({
55697
56308
  port: server.address().port,
55698
56309
  close: () => new Promise((r) => server.close(() => r()))
55699
56310
  });
@@ -55978,7 +56589,7 @@ function e2eSetupPlan(runners, paths, lang = "en") {
55978
56589
 
55979
56590
  // src/orchestrate.ts
55980
56591
  import { existsSync as existsSync28, mkdirSync as mkdirSync14, readFileSync as readFileSync24, rmSync as rmSync7, writeFileSync as writeFileSync16 } from "fs";
55981
- import { join as join42, resolve as resolve7 } from "path";
56592
+ import { join as join42, resolve as resolve10 } from "path";
55982
56593
 
55983
56594
  // src/orchestrate-templates.ts
55984
56595
  import { join as join41 } from "path";
@@ -56189,7 +56800,7 @@ var PHASES = ["adjudicate", "verify-report"];
56189
56800
  var SMALL_WORKLIST = 3;
56190
56801
  var BATCH_SIZE2 = 8;
56191
56802
  function listPhases(runDir, engineAbs) {
56192
- const run2 = resolve7(runDir);
56803
+ const run2 = resolve10(runDir);
56193
56804
  const adjPath = join42(run2, "ADJUDICATE.todo.json");
56194
56805
  let adjIds = [];
56195
56806
  let adjReady = false;
@@ -56236,7 +56847,7 @@ function listPhases(runDir, engineAbs) {
56236
56847
  ];
56237
56848
  }
56238
56849
  function orchestrateRun(runDir, engineAbs, opts = {}) {
56239
- const run2 = resolve7(runDir);
56850
+ const run2 = resolve10(runDir);
56240
56851
  if (!existsSync28(run2)) {
56241
56852
  return { exitCode: 2, written: [], notices: [], errors: [`run dir not found: ${run2}`], phases: [] };
56242
56853
  }
@@ -56309,7 +56920,7 @@ import { createInterface as createInterface2 } from "readline";
56309
56920
 
56310
56921
  // src/mcp/handlers.ts
56311
56922
  import { existsSync as existsSync29, readFileSync as readFileSync25, realpathSync as realpathSync4, statSync as statSync11 } from "fs";
56312
- 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";
56313
56924
 
56314
56925
  // src/project-lock.ts
56315
56926
  var chains = /* @__PURE__ */ new Map();
@@ -56357,7 +56968,7 @@ function positive(v, key) {
56357
56968
  function requiredCwd(args2, defaults) {
56358
56969
  const cwd = str2(args2.cwd) ?? defaults.defaultCwd;
56359
56970
  if (!cwd) throw new ToolError("`cwd` is required: an absolute path to the project root.");
56360
- const abs = resolve8(cwd);
56971
+ const abs = resolve11(cwd);
56361
56972
  if (!existsSync29(abs)) throw new ToolError(`project root not found: ${abs}`);
56362
56973
  if (!statSync11(abs).isDirectory()) throw new ToolError(`\`cwd\` is not a directory: ${abs}`);
56363
56974
  return abs;
@@ -56388,6 +56999,8 @@ async function dispatch(name2, args2, cwd) {
56388
56999
  return handleReport(args2, cwd);
56389
57000
  case "ultra11y_prd":
56390
57001
  return handlePrd(args2, cwd);
57002
+ case "ultra11y_tickets":
57003
+ return handleTickets(args2, cwd);
56391
57004
  case "ultra11y_check":
56392
57005
  return handleCheck(args2, cwd);
56393
57006
  case "ultra11y_verify":
@@ -56476,6 +57089,24 @@ function handlePrd(args2, cwd) {
56476
57089
  ...str2(args2.split) === "criterion" ? { grouped_by: "criterion" } : {}
56477
57090
  };
56478
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
+ }
56479
57110
  function handleCheck(args2, cwd) {
56480
57111
  const text = reportText(args2, "check");
56481
57112
  const res = checkReport(text, standardOf(args2), langOf(args2));
@@ -56581,7 +57212,7 @@ function handleInit(args2, cwd) {
56581
57212
  throw new ToolError(`\`fail_on\` must be one of: error, warning, notice (got "${failOnRaw}")`);
56582
57213
  }
56583
57214
  const failOn = failOnRaw;
56584
- const enginePath = resolve8("scripts/ultra11y.mjs");
57215
+ const enginePath = resolve11("scripts/ultra11y.mjs");
56585
57216
  const written = [];
56586
57217
  if (bool(args2.hook)) written.push(writeHook(cwd, enginePath, failOn));
56587
57218
  if (bool(args2.ci)) written.push(join43(cwd, ".github/workflows/a11y.yml"));
@@ -56805,6 +57436,26 @@ var TOOLS2 = [
56805
57436
  required: ["cwd"]
56806
57437
  }
56807
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
+ },
56808
57459
  {
56809
57460
  name: "ultra11y_criteria",
56810
57461
  title: "The offline standards reference",
@@ -56983,6 +57634,11 @@ var TOOL_META2 = {
56983
57634
  ultra11y_audit: { openWorld: false },
56984
57635
  ultra11y_report: { openWorld: false },
56985
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 },
56986
57642
  ultra11y_criteria: { openWorld: false },
56987
57643
  ultra11y_check: { openWorld: false },
56988
57644
  ultra11y_verify: { openWorld: false },
@@ -57151,13 +57807,13 @@ var DECLARED = new Set([...TOOLS2, ...WRITE_TOOLS].map((t2) => t2.name));
57151
57807
 
57152
57808
  // src/mcp/resources.ts
57153
57809
  import { existsSync as existsSync30, readdirSync as readdirSync6, readFileSync as readFileSync26, realpathSync as realpathSync5, statSync as statSync12 } from "fs";
57154
- 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";
57155
57811
  import { fileURLToPath as fileURLToPath4 } from "url";
57156
57812
  var SKILL_NAME = "ultra11y";
57157
57813
  var URI_SCHEME = "skill://";
57158
57814
  function resolveSkillRoot(moduleDir) {
57159
57815
  const here = moduleDir ?? dirname13(fileURLToPath4(import.meta.url));
57160
- 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)];
57161
57817
  return candidates2.find((dir) => existsSync30(join44(dir, "SKILL.md")));
57162
57818
  }
57163
57819
  function listResources(moduleDir) {
@@ -57180,7 +57836,7 @@ function readResource(uri, moduleDir) {
57180
57836
  if (!root) throw new ResourceError("no skill payload found next to this build \u2014 nothing to read");
57181
57837
  const rel2 = uri.slice(URI_SCHEME.length);
57182
57838
  if (!rel2) throw new ResourceError("empty resource path");
57183
- const target = resolve9(root, rel2);
57839
+ const target = resolve12(root, rel2);
57184
57840
  const rootReal = realpathSync5(root);
57185
57841
  let targetReal;
57186
57842
  try {
@@ -57461,14 +58117,14 @@ function startHttpServer(opts = {}) {
57461
58117
  server.requestTimeout = 0;
57462
58118
  server.headersTimeout = 6e4;
57463
58119
  server.keepAliveTimeout = 12e4;
57464
- return new Promise((resolve10, reject) => {
58120
+ return new Promise((resolve13, reject) => {
57465
58121
  server.once("error", reject);
57466
58122
  server.listen(opts.port ?? 0, bind, () => {
57467
58123
  server.removeListener("error", reject);
57468
58124
  const addr2 = server.address();
57469
58125
  const port = typeof addr2 === "object" && addr2 ? addr2.port : opts.port ?? 0;
57470
58126
  const host = bind.includes(":") ? `[${bind}]` : bind;
57471
- resolve10({
58127
+ resolve13({
57472
58128
  server,
57473
58129
  port,
57474
58130
  url: `http://${host}:${port}${MCP_PATH}`,
@@ -57577,7 +58233,7 @@ function sendJson(res, status, body2, origin, extra = {}) {
57577
58233
  }
57578
58234
  var DRAIN_LIMIT = MAX_BODY_BYTES * 8;
57579
58235
  function readBody2(req) {
57580
- return new Promise((resolve10, reject) => {
58236
+ return new Promise((resolve13, reject) => {
57581
58237
  const chunks = [];
57582
58238
  let size = 0;
57583
58239
  let over = false;
@@ -57601,7 +58257,7 @@ function readBody2(req) {
57601
58257
  });
57602
58258
  req.on("end", () => {
57603
58259
  if (over) reject(new Error("too large"));
57604
- else resolve10(Buffer.concat(chunks).toString("utf8"));
58260
+ else resolve13(Buffer.concat(chunks).toString("utf8"));
57605
58261
  });
57606
58262
  req.on("error", reject);
57607
58263
  req.on("aborted", () => reject(new Error("client aborted the request")));
@@ -57621,8 +58277,11 @@ Usage:
57621
58277
  ultra11y audit [--changed | --since <ref> | --staged] [--max-files <n>] [--dedup exact|normalized|off] [--baseline <file>] [--fail-on blocking|major|minor]
57622
58278
  ultra11y audit [--captures <dir>] [--no-captures] [--require-captures] (rendered-DOM captures + .ultra11y/pages snapshots: audit real HTML)
57623
58279
  ultra11y audit [--format sarif|github] (CI: SARIF for code scanning, or inline annotations + job summary)
58280
+ ultra11y audit --in <audit.json> [--fail-on blocking|major|minor] [--format sarif|github] (re-gate an audit already computed \u2014 e.g. one carrying adjudicated verdicts \u2014 without a second detection pass)
57624
58281
  ultra11y report --in <audit.json> [--out <dir>] [--standard <pack>] [--format sarif|github] [--lang auto|en|fr]
57625
- 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]
58282
+ ultra11y prd --in <audit.json> [--out <dir>] [--split criterion] [--format audit|doc|remediation] [--no-technical] [--standard <pack>] [--lang auto|en|fr]
58283
+ ultra11y tickets --in <audit.json> [--provider auto|github|gitlab|jira] [--grain criterion|page|page-criterion|single|file] [--transport auto|cli|rest]
58284
+ ultra11y tickets [--out <dir>] [--max-tickets <n>] [--dry-run] [--json] [--standard <pack>] [--format audit|remediation] [--lang auto|en|fr]
57626
58285
  ultra11y render [<dir>] [--scaffold | --setup | --e2e | --coverage | --storybook] [--runner playwright|cypress|auto] [--captures <dir>] [--out <file>] [--json] [--lang auto|en|fr]
57627
58286
  ultra11y criteria [<sc>] [--list] [--standard <pack> [--theme <N>]] [--generate] [--json] [--lang auto|en|fr]
57628
58287
  ultra11y criteria --standard <pack> --glossary [<term>] (the terms the standard DEFINES \u2014 its tests depend on them)
@@ -57678,9 +58337,14 @@ Commands:
57678
58337
  active standard's vocabulary (RGAA "Th\xE9matique/Crit\xE8re/Test", WCAG core
57679
58338
  "Principle\xB7Guideline/Success criterion/Technique") \u2014 theme, criterion +
57680
58339
  official wording, test(s), WCAG mapping + level, finding, expected state,
57681
- verification. --split criterion writes one file per criterion;
57682
- --gh-issues files one de-duplicated GitHub issue per criterion, or
57683
- --gh-single files the whole audit as a single issue (gh CLI).
58340
+ verification. --split criterion writes one file per criterion.
58341
+ It writes MARKDOWN only \u2014 filing tickets is the 'tickets' command.
58342
+ tickets File the audit as TRACKER TICKETS \u2014 GitHub, GitLab or Jira. Reads an
58343
+ audit.json and pushes; it writes no markdown, exactly as prd/report
58344
+ write markdown and push nothing. --grain chooses what ONE ticket is:
58345
+ per criterion (default), per page, per page+criterion, per file, or one
58346
+ consolidated. De-dupe is by exact title, so re-running never duplicates.
58347
+ --dry-run prints the plan without creating anything.
57684
58348
  render Get RENDERED HTML to audit (so component libraries like DSFR are
57685
58349
  checked as the HTML they emit, not their JSX sources): detect the
57686
58350
  framework and print the build\u2192audit recipe, or --scaffold a
@@ -57848,11 +58512,14 @@ Options:
57848
58512
  plus <out>/index.md (recommended past 3\u20134 pages: RGAA is 106 criteria)
57849
58513
  --no-technical prd (audit format): omit the technical ticket sections (Partie
57850
58514
  technique + Contexte de reproduction) for a pure-auditor block
57851
- --gh-issues prd: also create one GitHub issue per criterion via the gh CLI (opt-in)
57852
- --gh-single prd: file the whole audit as ONE consolidated GitHub issue (opt-in; wins over --gh-issues)
57853
- --issues-json prd: write the issue set (one item per criterion: title, body, labels,
57854
- severity, occurrences) to <out>/issues-<date>.json for a non-GitHub
57855
- tracker to file \u2014 same items --gh-issues would create, no gh needed
58515
+ --provider <id> tickets: auto|github|gitlab|jira. 'auto' reads ULTRA11Y_TICKET_PROVIDER,
58516
+ then .ultra11yrc.json, then the git remote (Jira is never auto-detected)
58517
+ --grain <mode> tickets: what one ticket is \u2014 criterion (default) | page | page-criterion | single | file
58518
+ --transport <t> tickets: auto|cli|rest (mcp: stdio|http). 'auto' prefers the CLI (gh/glab),
58519
+ falling back to REST when only a token is available. Jira is REST-only
58520
+ --max-tickets <n> tickets: refuse to file more than n tickets in one run (default 200)
58521
+ tickets --out <dir> also writes the tracker-agnostic set to
58522
+ <dir>/issues-<date>.json, for a workflow engine to file itself
57856
58523
  --scaffold render: write an SSR-snapshot harness (default: ultra11y-render.tsx)
57857
58524
  --setup render: install the zero-touch test-render capture harvester (.ultra11y/capture-setup.mjs) + print the runner wiring
57858
58525
  --coverage render: report rendered-capture coverage (covered vs blind-spot components); with --json emits the coverage object
@@ -57961,6 +58628,7 @@ var COMMANDS = [
57961
58628
  "audit",
57962
58629
  "report",
57963
58630
  "prd",
58631
+ "tickets",
57964
58632
  "render",
57965
58633
  "criteria",
57966
58634
  "check",
@@ -57986,6 +58654,9 @@ function isCommand(s) {
57986
58654
  }
57987
58655
  var VALUE_FLAGS2 = /* @__PURE__ */ new Set([
57988
58656
  "out",
58657
+ "provider",
58658
+ "grain",
58659
+ "max-tickets",
57989
58660
  "in",
57990
58661
  "include",
57991
58662
  "exclude",
@@ -58067,9 +58738,6 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
58067
58738
  "semantic",
58068
58739
  "manual",
58069
58740
  "no-technical",
58070
- "gh-issues",
58071
- "gh-single",
58072
- "issues-json",
58073
58741
  "override",
58074
58742
  "local",
58075
58743
  "docker",
@@ -58202,7 +58870,73 @@ function emitCiFormat(result, format, standard, lang, failOn) {
58202
58870
  );
58203
58871
  }
58204
58872
  }
58873
+ async function cmdAuditFromFile(p, inPath) {
58874
+ const scoping = [
58875
+ "since",
58876
+ "changed",
58877
+ "staged",
58878
+ "baseline",
58879
+ "graph",
58880
+ "cross-file",
58881
+ "jsx",
58882
+ "include",
58883
+ "exclude",
58884
+ "ext",
58885
+ "max-files",
58886
+ "dedup",
58887
+ "captures",
58888
+ "no-captures",
58889
+ "require-captures",
58890
+ "no-default-excludes",
58891
+ "out"
58892
+ ];
58893
+ const offenders = scoping.filter((f) => p.flags[f] !== void 0);
58894
+ if (p.positionals.length) offenders.unshift("<paths>");
58895
+ if (offenders.length) {
58896
+ console.error(
58897
+ `ultra11y audit: --in re-gates an audit that already exists, so these have nothing left to change: ${offenders.join(", ")}. Drop them, or run a fresh audit without --in.`
58898
+ );
58899
+ return 2;
58900
+ }
58901
+ let result;
58902
+ try {
58903
+ const parsed = JSON.parse(inPath === "-" ? await readStdin() : readText(inPath));
58904
+ if (!isCurrentAudit(parsed)) {
58905
+ console.error("ultra11y audit: --in is not a current ultra11y AuditResult (WCAG-keyed, schema v2). Re-run `audit`.");
58906
+ return 2;
58907
+ }
58908
+ result = parsed;
58909
+ } catch {
58910
+ console.error(`ultra11y audit: --in file not found or not valid JSON: ${inPath}.`);
58911
+ return 2;
58912
+ }
58913
+ const lang = resolveLang(p.flags, { audit: result });
58914
+ const failOnRaw = p.flags["fail-on"];
58915
+ const failOnParsed = parseFailOn(failOnRaw);
58916
+ if (failOnRaw !== void 0 && failOnParsed === null) {
58917
+ console.error(`ultra11y audit: --fail-on must be blocking|major|minor (got "${String(failOnRaw)}").`);
58918
+ return 2;
58919
+ }
58920
+ const ciFormat = parseCiFormat(p.flags.format);
58921
+ if (ciFormat === null) {
58922
+ console.error(`ultra11y audit: --format must be sarif|github (got "${String(p.flags.format)}").`);
58923
+ return 2;
58924
+ }
58925
+ const failOnSet = failOnRaw !== void 0;
58926
+ const failOn = failOnSet ? failOnParsed ?? "bloquant" : void 0;
58927
+ const failing = failOn ? findingsAtOrAbove(result.findings, failOn) : [];
58928
+ if (ciFormat) emitCiFormat(result, ciFormat, CORE2, lang, failOn);
58929
+ else if (p.flags.json) console.log(JSON.stringify(result, null, 2));
58930
+ else {
58931
+ console.log(auditSummary(result, lang));
58932
+ if (failOnSet && failing.length)
58933
+ console.error(lang === "fr" ? `\u2717 ${failing.length} non-conformit\xE9(s) \u2265 ${failOn}.` : `\u2717 ${failing.length} non-conformity(ies) \u2265 ${failOn}.`);
58934
+ }
58935
+ return failing.length ? 1 : 0;
58936
+ }
58205
58937
  async function cmdAudit(p) {
58938
+ const inFlag = p.flags.in;
58939
+ if (typeof inFlag === "string" && inFlag) return cmdAuditFromFile(p, inFlag);
58206
58940
  const inputs = p.positionals.length ? p.positionals : ["."];
58207
58941
  if (inputs.length === 0) {
58208
58942
  console.error("ultra11y audit: provide files/globs, or '-' to read stdin.");
@@ -58371,9 +59105,9 @@ async function cmdDev(p) {
58371
59105
  }
58372
59106
  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}`);
58373
59107
  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.");
58374
- await new Promise((resolve10) => {
59108
+ await new Promise((resolve13) => {
58375
59109
  const stop2 = () => {
58376
- void server.close().then(resolve10);
59110
+ void server.close().then(resolve13);
58377
59111
  };
58378
59112
  process.once("SIGINT", stop2);
58379
59113
  process.once("SIGTERM", stop2);
@@ -58843,45 +59577,207 @@ async function cmdPrd(p) {
58843
59577
  const paths = writePrd(result, { out: out2, lang, split, format, standard, technical });
58844
59578
  const json = p.flags.json === true;
58845
59579
  if (!json) for (const path of paths) console.log(path);
58846
- let issuesPath;
58847
- if (p.flags["issues-json"] === true) {
58848
- const issueFormat = format === "remediation" ? "remediation" : "audit";
58849
- const issues = issueSet(prdUnits(result, standard, lang), lang, standard, issueFormat);
58850
- mkdirSync15(out2, { recursive: true });
58851
- issuesPath = join45(out2, `issues-${result.date}.json`);
58852
- writeFileSync17(
58853
- issuesPath,
58854
- `${JSON.stringify({ tool: "ultra11y", kind: "issues", schemaVersion: 1, standard, date: result.date, count: issues.length, issues }, null, 2)}
58855
- `
59580
+ if (json) console.log(JSON.stringify({ paths, units: prdUnits(result, standard, lang) }, null, 2));
59581
+ return 0;
59582
+ }
59583
+ var REMOVED_FLAGS = {
59584
+ "gh-issues": "ultra11y tickets --in <audit.json> --provider github --grain criterion",
59585
+ "gh-single": "ultra11y tickets --in <audit.json> --provider github --grain single",
59586
+ // The tracker-agnostic export moved WITH ticket filing: `tickets --out` writes the same
59587
+ // envelope (and a superset of the payload) at any grain, so an orchestrator still reads
59588
+ // one stable path — just not from the command that renders documents.
59589
+ "issues-json": "ultra11y tickets --in <audit.json> --out <dir> --grain criterion"
59590
+ };
59591
+ var SECRET_CONFIG_KEYS = ["token", "apiToken", "api_token", "password", "secret"];
59592
+ async function cmdTickets(p) {
59593
+ const standard = stdOf(p, "tickets");
59594
+ if (standard === null) return 2;
59595
+ const inFlag = p.flags.in;
59596
+ if (typeof inFlag !== "string" || !inFlag) {
59597
+ console.error("ultra11y tickets: --in <audit.json> is required ('-' for stdin). Run `audit --out` first.");
59598
+ return 2;
59599
+ }
59600
+ const raw = inFlag === "-" ? await readStdin() : readInputFile(inFlag, "tickets", "--in");
59601
+ if (raw === null) return 2;
59602
+ let result;
59603
+ try {
59604
+ result = JSON.parse(raw);
59605
+ } catch {
59606
+ console.error("ultra11y tickets: --in is not valid JSON (expected an AuditResult).");
59607
+ return 2;
59608
+ }
59609
+ if (!isCurrentAudit(result)) {
59610
+ console.error("ultra11y tickets: input is not a current ultra11y AuditResult (WCAG-keyed, schema v2). Re-run `audit`.");
59611
+ return 2;
59612
+ }
59613
+ const lang = resolveLang(p.flags, { audit: result, standard });
59614
+ const fr = lang === "fr";
59615
+ const json = p.flags.json === true;
59616
+ const dryRun = p.flags["dry-run"] === true;
59617
+ let config;
59618
+ try {
59619
+ config = loadConfig(process.cwd())?.tickets;
59620
+ } catch (e) {
59621
+ console.error(`ultra11y tickets: ${e instanceof Error ? e.message : String(e)}`);
59622
+ return 2;
59623
+ }
59624
+ for (const key of SECRET_CONFIG_KEYS) {
59625
+ if (config && key in config) {
59626
+ console.error(
59627
+ `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.`
59628
+ );
59629
+ return 2;
59630
+ }
59631
+ }
59632
+ const grainFlag = typeof p.flags.grain === "string" ? p.flags.grain : config?.grain ?? "criterion";
59633
+ if (!ALL_GRAINS.includes(grainFlag)) {
59634
+ console.error(`ultra11y tickets: --grain must be one of ${ALL_GRAINS.join("|")} (got "${grainFlag}").`);
59635
+ return 2;
59636
+ }
59637
+ const transportFlag = typeof p.flags.transport === "string" ? p.flags.transport : config?.transport ?? "auto";
59638
+ if (!["auto", "cli", "rest"].includes(transportFlag)) {
59639
+ console.error(`ultra11y tickets: --transport must be auto, cli or rest (got "${transportFlag}").`);
59640
+ return 2;
59641
+ }
59642
+ const maxTickets = Number.parseInt(String(p.flags["max-tickets"] ?? config?.maxTickets ?? 200), 10);
59643
+ if (!Number.isFinite(maxTickets) || maxTickets < 1) {
59644
+ console.error("ultra11y tickets: --max-tickets must be a positive integer.");
59645
+ return 2;
59646
+ }
59647
+ const providerFlag = typeof p.flags.provider === "string" ? p.flags.provider : "auto";
59648
+ const providerId = providerFlag === "auto" ? autoProvider(process.env, config?.provider) : isProviderId(providerFlag) ? providerFlag : void 0;
59649
+ if (!providerId) {
59650
+ console.error(
59651
+ 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("|")}.`
58856
59652
  );
58857
- if (!json) console.log(issuesPath);
59653
+ return 2;
58858
59654
  }
58859
- const ghMode = p.flags["gh-single"] === true ? "single" : p.flags["gh-issues"] === true ? "per-criterion" : null;
58860
- let gh2;
58861
- if (ghMode) {
58862
- const flag = ghMode === "single" ? "--gh-single" : "--gh-issues";
58863
- const units = prdUnits(result, standard, lang);
58864
- if (!ghAvailable()) {
58865
- if (!json)
58866
- console.error(`ultra11y prd: ${flag} skipped \u2014 \`gh\` is not installed or not authenticated (run \`gh auth login\`). Markdown was still written.`);
58867
- } else if (units.length === 0) {
58868
- if (!json) console.error(`ultra11y prd: ${flag} skipped \u2014 no findings to file.`);
58869
- } else {
58870
- const issueFormat = format === "remediation" ? "remediation" : "audit";
58871
- gh2 = ghMode === "single" ? pushSingleIssue(units, lang, standard, issueFormat) : pushIssues(units, lang, standard, issueFormat);
58872
- if (!json)
58873
- console.log(
58874
- 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` : ""}.`
58875
- );
58876
- if (gh2.failed && !json) {
58877
- 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}`);
58878
- else console.error(lang === "fr" ? `ultra11y prd : gh a \xE9chou\xE9 sans message d'erreur.` : `ultra11y prd: gh failed with no error output.`);
58879
- }
59655
+ const provider = createProvider(providerId, { transport: transportFlag });
59656
+ const format = p.flags.format === "remediation" ? "remediation" : "audit";
59657
+ const plan = buildTickets(result, {
59658
+ grain: grainFlag,
59659
+ standard,
59660
+ lang,
59661
+ format,
59662
+ bodyLimit: provider.capabilities.bodyLimit,
59663
+ baseDir: process.cwd(),
59664
+ technical: p.flags["no-technical"] !== true
59665
+ });
59666
+ if (plan.error === "no-pages") {
59667
+ console.error(
59668
+ 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."
59669
+ );
59670
+ return 1;
59671
+ }
59672
+ if (plan.tickets.length > maxTickets) {
59673
+ console.error(
59674
+ 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.`
59675
+ );
59676
+ return 2;
59677
+ }
59678
+ const ticketsOut = typeof p.flags.out === "string" && p.flags.out ? p.flags.out : void 0;
59679
+ let setPath;
59680
+ if (ticketsOut) {
59681
+ mkdirSync15(ticketsOut, { recursive: true });
59682
+ setPath = join45(ticketsOut, `issues-${result.date}.json`);
59683
+ const payload = {
59684
+ tool: "ultra11y",
59685
+ kind: "issues",
59686
+ schemaVersion: TICKET_SET_SCHEMA_VERSION,
59687
+ standard,
59688
+ grain: grainFlag,
59689
+ date: result.date,
59690
+ count: plan.tickets.length,
59691
+ issues: plan.tickets
59692
+ };
59693
+ writeFileSync17(setPath, `${JSON.stringify(payload, null, 2)}
59694
+ `);
59695
+ if (!json) console.log(setPath);
59696
+ }
59697
+ if (!plan.tickets.length) {
59698
+ if (!json)
59699
+ console.log(
59700
+ 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."
59701
+ );
59702
+ if (json)
59703
+ console.log(
59704
+ JSON.stringify(
59705
+ {
59706
+ provider: providerId,
59707
+ transport: provider.transport,
59708
+ grain: grainFlag,
59709
+ standard,
59710
+ dryRun,
59711
+ ...setPath ? { setPath } : {},
59712
+ tickets: [],
59713
+ unattributed: plan.unattributed,
59714
+ result: { created: 0, skipped: 0, failed: 0, createdTitles: [], createdUrls: [], errors: [] }
59715
+ },
59716
+ null,
59717
+ 2
59718
+ )
59719
+ );
59720
+ return 0;
59721
+ }
59722
+ if (!provider.available() && !dryRun) {
59723
+ console.error(`ultra11y tickets: ${providerId} is not usable here \u2014 ${provider.unavailableReason()}.`);
59724
+ return 1;
59725
+ }
59726
+ const { plan: planned, result: pushed, dedupeChecked } = await pushTickets(plan.tickets, provider, { dryRun: dryRun || !provider.available() });
59727
+ if (json) {
59728
+ console.log(
59729
+ JSON.stringify(
59730
+ {
59731
+ provider: providerId,
59732
+ transport: provider.transport,
59733
+ grain: grainFlag,
59734
+ standard,
59735
+ dryRun,
59736
+ ...setPath ? { setPath } : {},
59737
+ dedupeChecked,
59738
+ tickets: planned.map(({ ticket, action }) => ({
59739
+ title: ticket.title,
59740
+ labels: ticket.labels,
59741
+ severity: ticket.severity,
59742
+ advisory: ticket.advisory,
59743
+ scope: ticket.scope,
59744
+ bodyChars: ticket.body.length,
59745
+ action,
59746
+ // The body rides along ONLY under --dry-run: an agent inspecting the plan wants
59747
+ // it, a 200-ticket push payload does not.
59748
+ ...dryRun ? { body: ticket.body } : {}
59749
+ })),
59750
+ unattributed: plan.unattributed,
59751
+ result: pushed
59752
+ },
59753
+ null,
59754
+ 2
59755
+ )
59756
+ );
59757
+ } else if (dryRun || !provider.available()) {
59758
+ const create = planned.filter((x) => x.action === "create").length;
59759
+ console.log(
59760
+ 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.`
59761
+ );
59762
+ for (const { ticket, action } of planned) console.log(` ${action === "create" ? "+" : "="} ${ticket.title}`);
59763
+ if (!dedupeChecked)
59764
+ console.error(
59765
+ 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.'
59766
+ );
59767
+ if (!provider.available()) console.error(`ultra11y tickets: ${providerId} \u2014 ${provider.unavailableReason()}.`);
59768
+ } else {
59769
+ console.log(
59770
+ 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` : ""}.`
59771
+ );
59772
+ for (const u of pushed.createdUrls) console.log(` ${u}`);
59773
+ for (const e of pushed.errors) console.error(fr ? `ultra11y tickets : \xE9chec \u2014 ${e}` : `ultra11y tickets: failed \u2014 ${e}`);
59774
+ if (plan.unattributed) {
59775
+ console.error(
59776
+ 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.`
59777
+ );
58880
59778
  }
58881
59779
  }
58882
- if (json)
58883
- console.log(JSON.stringify({ paths, ...issuesPath ? { issuesPath } : {}, units: prdUnits(result, standard, lang), ...gh2 ? { gh: gh2 } : {} }, null, 2));
58884
- return gh2 && gh2.failed > 0 ? 1 : 0;
59780
+ return pushed.failed > 0 ? 1 : 0;
58885
59781
  }
58886
59782
  function depsAt(root) {
58887
59783
  const pkgPath = join45(root, "package.json");
@@ -59845,13 +60741,25 @@ async function main(argv) {
59845
60741
  console.log(HELP2);
59846
60742
  return 0;
59847
60743
  }
60744
+ for (const f of p.unknown) {
60745
+ const replacement = REMOVED_FLAGS[f];
60746
+ if (replacement) {
60747
+ console.error(`ultra11y: --${f} was removed in v3 \u2014 ticket creation is now its own command. Use: ${replacement}`);
60748
+ return 2;
60749
+ }
60750
+ }
59848
60751
  for (const f of p.unknown) console.error(`ultra11y: unknown flag --${f} (ignored). Run \`ultra11y --help\`.`);
59849
60752
  const ENUM_FLAGS = {
59850
60753
  lang: ["auto", "en", "fr"],
59851
60754
  dedup: ["exact", "normalized", "off"],
59852
60755
  format: ["audit", "doc", "remediation", "sarif", "github", "grid", "report"],
59853
60756
  split: ["criterion", "page"],
59854
- runtime: ["auto", "local", "docker"]
60757
+ runtime: ["auto", "local", "docker"],
60758
+ provider: ["auto", "github", "gitlab", "jira"],
60759
+ grain: ["criterion", "page", "page-criterion", "single", "file"],
60760
+ // The union over every command that takes it: `mcp` serves stdio|http, `tickets`
60761
+ // picks cli|rest. Listing one command's values made the guard cry wolf on the other.
60762
+ transport: ["stdio", "http", "auto", "cli", "rest"]
59855
60763
  };
59856
60764
  for (const [flag, allowed] of Object.entries(ENUM_FLAGS)) {
59857
60765
  const v = p.flags[flag];
@@ -59869,6 +60777,8 @@ async function main(argv) {
59869
60777
  return cmdAudit(p);
59870
60778
  case "report":
59871
60779
  return cmdReport(p);
60780
+ case "tickets":
60781
+ return cmdTickets(p);
59872
60782
  case "prd":
59873
60783
  return cmdPrd(p);
59874
60784
  case "render":
@@ -59937,6 +60847,7 @@ if (isInvokedDirectly()) {
59937
60847
  }
59938
60848
  export {
59939
60849
  COMMANDS,
60850
+ REMOVED_FLAGS,
59940
60851
  main,
59941
60852
  parseArgs
59942
60853
  };