ultra11y 2.32.0 → 2.32.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.opencode/plugins/ultra11y.js +1 -1
- package/README.md +2 -1
- package/package.json +1 -1
- package/scripts/ultra11y.mjs +1066 -222
- package/skills/review-a11y/SKILL.md +1 -1
- package/skills/review-a11y/scripts/ultra11y.mjs +1066 -222
- package/skills/ultra11y/SKILL.md +17 -11
- package/skills/ultra11y/references/audit.md +1 -1
- package/skills/ultra11y/references/methodology.md +1 -1
- package/skills/ultra11y/references/orchestrators.md +19 -11
- package/skills/ultra11y/references/prd.md +9 -36
- package/skills/ultra11y/references/tickets.md +156 -0
- package/skills/ultra11y/scripts/ultra11y.mjs +1066 -222
- package/src/cli.ts +302 -63
- package/src/config.ts +10 -0
- package/src/gh-cli.ts +39 -0
- package/src/mcp/handlers.ts +28 -0
- package/src/mcp/tools.ts +27 -0
- package/src/pages.ts +13 -0
- package/src/pr-comment.ts +78 -0
- package/src/tickets/grain.ts +218 -0
- package/src/tickets/http.ts +107 -0
- package/src/tickets/providers/github.ts +155 -0
- package/src/tickets/providers/gitlab.ts +158 -0
- package/src/tickets/providers/jira.ts +152 -0
- package/src/tickets/providers/remote.ts +40 -0
- package/src/tickets/push.ts +94 -0
- package/src/tickets/registry.ts +45 -0
- package/src/tickets/render.ts +157 -0
- package/src/tickets/types.ts +154 -0
- package/src/types.ts +1 -1
- package/src/gh.ts +0 -333
package/src/cli.ts
CHANGED
|
@@ -6,7 +6,11 @@ import { runAudit } from "./audit.js";
|
|
|
6
6
|
import { decide, type PreToolUsePayload } from "./hook.js";
|
|
7
7
|
import { writeReport, untestedNeedsRendering, partialAuditBanner } from "./report.js";
|
|
8
8
|
import { writePrd, prdUnits, type PrdFormat } from "./prd.js";
|
|
9
|
-
import {
|
|
9
|
+
import { pushPrComment } from "./pr-comment.js";
|
|
10
|
+
import { buildTickets } from "./tickets/grain.js";
|
|
11
|
+
import { pushTickets } from "./tickets/push.js";
|
|
12
|
+
import { autoProvider, createProvider, isProviderId } from "./tickets/registry.js";
|
|
13
|
+
import { ALL_GRAINS, ALL_PROVIDERS, TICKET_SET_SCHEMA_VERSION, type TicketGrain, type TicketSetFile, type TransportMode } from "./tickets/types.js";
|
|
10
14
|
import {
|
|
11
15
|
detectFrameworks,
|
|
12
16
|
renderPlan,
|
|
@@ -46,7 +50,7 @@ import { crawlUrls, extractTitle, parseSitemapUrls } from "./crawl.js";
|
|
|
46
50
|
import { DEV_DEFAULT_PORT, nextOverlayComponent, startDevServer, type DevServer } from "./dev.js";
|
|
47
51
|
import { cypressCommands, cypressPlugin, detectE2eRunner, e2eSetupPlan, playwrightFixture, type E2ePaths, type E2eRunner } from "./e2e.js";
|
|
48
52
|
import { resolveStandard, getPack, isCore, CORE, type StandardId } from "./standards/index.js";
|
|
49
|
-
import { loadRuntimeStandards, loadConfig } from "./config.js";
|
|
53
|
+
import { loadRuntimeStandards, loadConfig, type Ultra11yConfig } from "./config.js";
|
|
50
54
|
import { runPackCheck, packScaffold } from "./pack.js";
|
|
51
55
|
import { listPhases, orchestrateRun, PHASES } from "./orchestrate.js";
|
|
52
56
|
import { readStdin, readText } from "./util.js";
|
|
@@ -66,7 +70,9 @@ Usage:
|
|
|
66
70
|
ultra11y audit [--captures <dir>] [--no-captures] [--require-captures] (rendered-DOM captures + .ultra11y/pages snapshots: audit real HTML)
|
|
67
71
|
ultra11y audit [--format sarif|github] (CI: SARIF for code scanning, or inline annotations + job summary)
|
|
68
72
|
ultra11y report --in <audit.json> [--out <dir>] [--standard <pack>] [--format sarif|github] [--lang auto|en|fr]
|
|
69
|
-
ultra11y prd --in <audit.json> [--out <dir>] [--split criterion] [--format audit|doc|remediation] [--no-technical] [--standard <pack>] [--
|
|
73
|
+
ultra11y prd --in <audit.json> [--out <dir>] [--split criterion] [--format audit|doc|remediation] [--no-technical] [--standard <pack>] [--lang auto|en|fr]
|
|
74
|
+
ultra11y tickets --in <audit.json> [--provider auto|github|gitlab|jira] [--grain criterion|page|page-criterion|single|file] [--transport auto|cli|rest]
|
|
75
|
+
ultra11y tickets [--out <dir>] [--max-tickets <n>] [--dry-run] [--json] [--standard <pack>] [--format audit|remediation] [--lang auto|en|fr]
|
|
70
76
|
ultra11y render [<dir>] [--scaffold | --setup | --e2e | --coverage | --storybook] [--runner playwright|cypress|auto] [--captures <dir>] [--out <file>] [--json] [--lang auto|en|fr]
|
|
71
77
|
ultra11y criteria [<sc>] [--list] [--standard <pack> [--theme <N>]] [--generate] [--json] [--lang auto|en|fr]
|
|
72
78
|
ultra11y criteria --standard <pack> --glossary [<term>] (the terms the standard DEFINES — its tests depend on them)
|
|
@@ -122,9 +128,14 @@ Commands:
|
|
|
122
128
|
active standard's vocabulary (RGAA "Thématique/Critère/Test", WCAG core
|
|
123
129
|
"Principle·Guideline/Success criterion/Technique") — theme, criterion +
|
|
124
130
|
official wording, test(s), WCAG mapping + level, finding, expected state,
|
|
125
|
-
verification. --split criterion writes one file per criterion
|
|
126
|
-
|
|
127
|
-
|
|
131
|
+
verification. --split criterion writes one file per criterion.
|
|
132
|
+
It writes MARKDOWN only — filing tickets is the 'tickets' command.
|
|
133
|
+
tickets File the audit as TRACKER TICKETS — GitHub, GitLab or Jira. Reads an
|
|
134
|
+
audit.json and pushes; it writes no markdown, exactly as prd/report
|
|
135
|
+
write markdown and push nothing. --grain chooses what ONE ticket is:
|
|
136
|
+
per criterion (default), per page, per page+criterion, per file, or one
|
|
137
|
+
consolidated. De-dupe is by exact title, so re-running never duplicates.
|
|
138
|
+
--dry-run prints the plan without creating anything.
|
|
128
139
|
render Get RENDERED HTML to audit (so component libraries like DSFR are
|
|
129
140
|
checked as the HTML they emit, not their JSX sources): detect the
|
|
130
141
|
framework and print the build→audit recipe, or --scaffold a
|
|
@@ -292,11 +303,14 @@ Options:
|
|
|
292
303
|
plus <out>/index.md (recommended past 3–4 pages: RGAA is 106 criteria)
|
|
293
304
|
--no-technical prd (audit format): omit the technical ticket sections (Partie
|
|
294
305
|
technique + Contexte de reproduction) for a pure-auditor block
|
|
295
|
-
--
|
|
296
|
-
|
|
297
|
-
--
|
|
298
|
-
|
|
299
|
-
|
|
306
|
+
--provider <id> tickets: auto|github|gitlab|jira. 'auto' reads ULTRA11Y_TICKET_PROVIDER,
|
|
307
|
+
then .ultra11yrc.json, then the git remote (Jira is never auto-detected)
|
|
308
|
+
--grain <mode> tickets: what one ticket is — criterion (default) | page | page-criterion | single | file
|
|
309
|
+
--transport <t> tickets: auto|cli|rest (mcp: stdio|http). 'auto' prefers the CLI (gh/glab),
|
|
310
|
+
falling back to REST when only a token is available. Jira is REST-only
|
|
311
|
+
--max-tickets <n> tickets: refuse to file more than n tickets in one run (default 200)
|
|
312
|
+
tickets --out <dir> also writes the tracker-agnostic set to
|
|
313
|
+
<dir>/issues-<date>.json, for a workflow engine to file itself
|
|
300
314
|
--scaffold render: write an SSR-snapshot harness (default: ultra11y-render.tsx)
|
|
301
315
|
--setup render: install the zero-touch test-render capture harvester (.ultra11y/capture-setup.mjs) + print the runner wiring
|
|
302
316
|
--coverage render: report rendered-capture coverage (covered vs blind-spot components); with --json emits the coverage object
|
|
@@ -406,6 +420,7 @@ export const COMMANDS = [
|
|
|
406
420
|
"audit",
|
|
407
421
|
"report",
|
|
408
422
|
"prd",
|
|
423
|
+
"tickets",
|
|
409
424
|
"render",
|
|
410
425
|
"criteria",
|
|
411
426
|
"check",
|
|
@@ -434,6 +449,9 @@ function isCommand(s: string | undefined): s is Command {
|
|
|
434
449
|
|
|
435
450
|
const VALUE_FLAGS = new Set([
|
|
436
451
|
"out",
|
|
452
|
+
"provider",
|
|
453
|
+
"grain",
|
|
454
|
+
"max-tickets",
|
|
437
455
|
"in",
|
|
438
456
|
"include",
|
|
439
457
|
"exclude",
|
|
@@ -528,9 +546,6 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
528
546
|
"semantic",
|
|
529
547
|
"manual",
|
|
530
548
|
"no-technical",
|
|
531
|
-
"gh-issues",
|
|
532
|
-
"gh-single",
|
|
533
|
-
"issues-json",
|
|
534
549
|
"override",
|
|
535
550
|
"local",
|
|
536
551
|
"docker",
|
|
@@ -1561,58 +1576,265 @@ async function cmdPrd(p: ParsedArgs): Promise<number> {
|
|
|
1561
1576
|
const json = p.flags.json === true;
|
|
1562
1577
|
if (!json) for (const path of paths) console.log(path);
|
|
1563
1578
|
|
|
1564
|
-
//
|
|
1565
|
-
//
|
|
1566
|
-
//
|
|
1567
|
-
// a
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1579
|
+
// `prd` writes MARKDOWN. It files nothing: ticket creation is `ultra11y tickets`, which
|
|
1580
|
+
// reads the same audit.json and writes no markdown. That split is deliberate — you could
|
|
1581
|
+
// not previously push without also producing a document, nor produce a document without
|
|
1582
|
+
// risking a push.
|
|
1583
|
+
if (json) console.log(JSON.stringify({ paths, units: prdUnits(result, standard, lang) }, null, 2));
|
|
1584
|
+
return 0;
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
/** Flags that USED to exist, mapped to what replaces them. Checked before the generic
|
|
1588
|
+
* unknown-flag warning so a removal is loud on the first run, not discovered later from an
|
|
1589
|
+
* empty tracker. Reusable for the next removal — one table, one place. */
|
|
1590
|
+
export const REMOVED_FLAGS: Record<string, string> = {
|
|
1591
|
+
"gh-issues": "ultra11y tickets --in <audit.json> --provider github --grain criterion",
|
|
1592
|
+
"gh-single": "ultra11y tickets --in <audit.json> --provider github --grain single",
|
|
1593
|
+
// The tracker-agnostic export moved WITH ticket filing: `tickets --out` writes the same
|
|
1594
|
+
// envelope (and a superset of the payload) at any grain, so an orchestrator still reads
|
|
1595
|
+
// one stable path — just not from the command that renders documents.
|
|
1596
|
+
"issues-json": "ultra11y tickets --in <audit.json> --out <dir> --grain criterion",
|
|
1597
|
+
};
|
|
1598
|
+
|
|
1599
|
+
/** Config keys that must NEVER appear in `.ultra11yrc.json` — a committed credential is a
|
|
1600
|
+
* leaked credential. Named explicitly rather than pattern-matched, so the error can say what
|
|
1601
|
+
* to do instead. */
|
|
1602
|
+
const SECRET_CONFIG_KEYS = ["token", "apiToken", "api_token", "password", "secret"];
|
|
1603
|
+
|
|
1604
|
+
async function cmdTickets(p: ParsedArgs): Promise<number> {
|
|
1605
|
+
const standard = stdOf(p, "tickets");
|
|
1606
|
+
if (standard === null) return 2;
|
|
1607
|
+
const inFlag = p.flags.in;
|
|
1608
|
+
if (typeof inFlag !== "string" || !inFlag) {
|
|
1609
|
+
console.error("ultra11y tickets: --in <audit.json> is required ('-' for stdin). Run `audit --out` first.");
|
|
1610
|
+
return 2;
|
|
1611
|
+
}
|
|
1612
|
+
const raw = inFlag === "-" ? await readStdin() : readInputFile(inFlag, "tickets", "--in");
|
|
1613
|
+
if (raw === null) return 2;
|
|
1614
|
+
let result: unknown;
|
|
1615
|
+
try {
|
|
1616
|
+
result = JSON.parse(raw);
|
|
1617
|
+
} catch {
|
|
1618
|
+
console.error("ultra11y tickets: --in is not valid JSON (expected an AuditResult).");
|
|
1619
|
+
return 2;
|
|
1620
|
+
}
|
|
1621
|
+
if (!isCurrentAudit(result)) {
|
|
1622
|
+
console.error("ultra11y tickets: input is not a current ultra11y AuditResult (WCAG-keyed, schema v2). Re-run `audit`.");
|
|
1623
|
+
return 2;
|
|
1624
|
+
}
|
|
1625
|
+
const lang = resolveLang(p.flags, { audit: result, standard });
|
|
1626
|
+
const fr = lang === "fr";
|
|
1627
|
+
const json = p.flags.json === true;
|
|
1628
|
+
const dryRun = p.flags["dry-run"] === true;
|
|
1629
|
+
|
|
1630
|
+
let config: Ultra11yConfig["tickets"];
|
|
1631
|
+
try {
|
|
1632
|
+
config = loadConfig(process.cwd())?.tickets;
|
|
1633
|
+
} catch (e) {
|
|
1634
|
+
console.error(`ultra11y tickets: ${e instanceof Error ? e.message : String(e)}`);
|
|
1635
|
+
return 2;
|
|
1636
|
+
}
|
|
1637
|
+
for (const key of SECRET_CONFIG_KEYS) {
|
|
1638
|
+
if (config && key in (config as Record<string, unknown>)) {
|
|
1639
|
+
console.error(
|
|
1640
|
+
`ultra11y tickets: .ultra11yrc.json must not carry a "${key}" — credentials belong in the environment (see references/tickets.md). Remove it and rotate that value.`,
|
|
1641
|
+
);
|
|
1642
|
+
return 2;
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
// --- usage, before any I/O --------------------------------------------------------------
|
|
1647
|
+
// Everything decidable from the argv alone is decided HERE, ahead of provider resolution.
|
|
1648
|
+
// Resolving a provider spawns `git config --get remote.origin.url`, and an `auto`
|
|
1649
|
+
// transport probes `gh`/`glab auth status` — which reaches the network. A mistyped
|
|
1650
|
+
// `--grain` should never cost a subprocess, let alone a network round-trip, and the error
|
|
1651
|
+
// the caller gets should name what they typed rather than a tracker they never mentioned.
|
|
1652
|
+
//
|
|
1653
|
+
// It was also a real flake: `tickets --grain nope` spent its whole 5s test budget in those
|
|
1654
|
+
// probes on a loaded CI runner, and the release gate went red on a purely syntactic error.
|
|
1655
|
+
const grainFlag = typeof p.flags.grain === "string" ? (p.flags.grain as string) : (config?.grain ?? "criterion");
|
|
1656
|
+
if (!(ALL_GRAINS as readonly string[]).includes(grainFlag)) {
|
|
1657
|
+
console.error(`ultra11y tickets: --grain must be one of ${ALL_GRAINS.join("|")} (got "${grainFlag}").`);
|
|
1658
|
+
return 2;
|
|
1659
|
+
}
|
|
1660
|
+
const transportFlag = typeof p.flags.transport === "string" ? (p.flags.transport as string) : (config?.transport ?? "auto");
|
|
1661
|
+
if (!["auto", "cli", "rest"].includes(transportFlag)) {
|
|
1662
|
+
console.error(`ultra11y tickets: --transport must be auto, cli or rest (got "${transportFlag}").`);
|
|
1663
|
+
return 2;
|
|
1664
|
+
}
|
|
1665
|
+
const maxTickets = Number.parseInt(String(p.flags["max-tickets"] ?? config?.maxTickets ?? 200), 10);
|
|
1666
|
+
if (!Number.isFinite(maxTickets) || maxTickets < 1) {
|
|
1667
|
+
console.error("ultra11y tickets: --max-tickets must be a positive integer.");
|
|
1668
|
+
return 2;
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
// --- provider ---------------------------------------------------------------------------
|
|
1672
|
+
const providerFlag = typeof p.flags.provider === "string" ? (p.flags.provider as string) : "auto";
|
|
1673
|
+
const providerId = providerFlag === "auto" ? autoProvider(process.env, config?.provider) : isProviderId(providerFlag) ? providerFlag : undefined;
|
|
1674
|
+
if (!providerId) {
|
|
1675
|
+
console.error(
|
|
1676
|
+
providerFlag === "auto"
|
|
1677
|
+
? "ultra11y tickets: could not tell which tracker to file into — pass --provider github|gitlab|jira (Jira is never auto-detected: it owns no git remote)."
|
|
1678
|
+
: `ultra11y tickets: --provider "${providerFlag}" is not one of ${ALL_PROVIDERS.join("|")}.`,
|
|
1577
1679
|
);
|
|
1578
|
-
|
|
1579
|
-
}
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1680
|
+
return 2;
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
const provider = createProvider(providerId, { transport: transportFlag as TransportMode });
|
|
1684
|
+
|
|
1685
|
+
// --- the plan ---------------------------------------------------------------------------
|
|
1686
|
+
const format = p.flags.format === "remediation" ? "remediation" : "audit";
|
|
1687
|
+
const plan = buildTickets(result, {
|
|
1688
|
+
grain: grainFlag as TicketGrain,
|
|
1689
|
+
standard,
|
|
1690
|
+
lang,
|
|
1691
|
+
format,
|
|
1692
|
+
bodyLimit: provider.capabilities.bodyLimit,
|
|
1693
|
+
baseDir: process.cwd(),
|
|
1694
|
+
technical: p.flags["no-technical"] !== true,
|
|
1695
|
+
});
|
|
1696
|
+
|
|
1697
|
+
if (plan.error === "no-pages") {
|
|
1698
|
+
console.error(
|
|
1699
|
+
fr
|
|
1700
|
+
? "ultra11y tickets : aucune page dans le périmètre. Capturez des instantanés (render --e2e) ou scannez un échantillon (scan --sample) avant d'utiliser --grain page."
|
|
1701
|
+
: "ultra11y tickets: no page in scope. Capture snapshots (render --e2e) or scan a sample (scan --sample) before using --grain page.",
|
|
1702
|
+
);
|
|
1703
|
+
return 1;
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
// A creation is hard to undo, and `page-criterion` on a large audit runs to the hundreds.
|
|
1707
|
+
// Refuse rather than flood somebody's tracker; never silently truncate.
|
|
1708
|
+
if (plan.tickets.length > maxTickets) {
|
|
1709
|
+
console.error(
|
|
1710
|
+
fr
|
|
1711
|
+
? `ultra11y tickets : ${plan.tickets.length} tickets pour --grain ${grainFlag}, au-delà de la limite de ${maxTickets}. Relancez avec --max-tickets ${plan.tickets.length}, un grain plus grossier, ou --dry-run pour inspecter.`
|
|
1712
|
+
: `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.`,
|
|
1713
|
+
);
|
|
1714
|
+
return 2;
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
// `--out` writes the tracker-agnostic SET to a stable path, for a workflow engine that
|
|
1718
|
+
// files the items itself. It is not a document — it is the ticket payload — which is why
|
|
1719
|
+
// it lives here rather than on `prd`. Writing it never files anything.
|
|
1720
|
+
const ticketsOut = typeof p.flags.out === "string" && p.flags.out ? (p.flags.out as string) : undefined;
|
|
1721
|
+
let setPath: string | undefined;
|
|
1722
|
+
if (ticketsOut) {
|
|
1723
|
+
mkdirSync(ticketsOut, { recursive: true });
|
|
1724
|
+
setPath = join(ticketsOut, `issues-${result.date}.json`);
|
|
1725
|
+
const payload: TicketSetFile = {
|
|
1726
|
+
tool: "ultra11y",
|
|
1727
|
+
kind: "issues",
|
|
1728
|
+
schemaVersion: TICKET_SET_SCHEMA_VERSION,
|
|
1729
|
+
standard,
|
|
1730
|
+
grain: grainFlag as TicketGrain,
|
|
1731
|
+
date: result.date,
|
|
1732
|
+
count: plan.tickets.length,
|
|
1733
|
+
issues: plan.tickets,
|
|
1734
|
+
};
|
|
1735
|
+
writeFileSync(setPath, `${JSON.stringify(payload, null, 2)}\n`);
|
|
1736
|
+
if (!json) console.log(setPath);
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
if (!plan.tickets.length) {
|
|
1740
|
+
if (!json)
|
|
1741
|
+
console.log(
|
|
1742
|
+
fr
|
|
1743
|
+
? "ultra11y tickets : rien à déposer — l'audit ne relève aucune non-conformité."
|
|
1744
|
+
: "ultra11y tickets: nothing to file — the audit found no non-conformity.",
|
|
1745
|
+
);
|
|
1746
|
+
if (json)
|
|
1747
|
+
console.log(
|
|
1748
|
+
JSON.stringify(
|
|
1749
|
+
{
|
|
1750
|
+
provider: providerId,
|
|
1751
|
+
transport: provider.transport,
|
|
1752
|
+
grain: grainFlag,
|
|
1753
|
+
standard,
|
|
1754
|
+
dryRun,
|
|
1755
|
+
...(setPath ? { setPath } : {}),
|
|
1756
|
+
tickets: [],
|
|
1757
|
+
unattributed: plan.unattributed,
|
|
1758
|
+
result: { created: 0, skipped: 0, failed: 0, createdTitles: [], createdUrls: [], errors: [] },
|
|
1759
|
+
},
|
|
1760
|
+
null,
|
|
1761
|
+
2,
|
|
1762
|
+
),
|
|
1763
|
+
);
|
|
1764
|
+
return 0;
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
// A push command that files nothing and reports green is a silent failure — unlike the old
|
|
1768
|
+
// `prd --gh-issues`, whose push was an optional extra on top of a document.
|
|
1769
|
+
if (!provider.available() && !dryRun) {
|
|
1770
|
+
console.error(`ultra11y tickets: ${providerId} is not usable here — ${provider.unavailableReason()}.`);
|
|
1771
|
+
return 1;
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
const { plan: planned, result: pushed, dedupeChecked } = await pushTickets(plan.tickets, provider, { dryRun: dryRun || !provider.available() });
|
|
1775
|
+
|
|
1776
|
+
if (json) {
|
|
1777
|
+
console.log(
|
|
1778
|
+
JSON.stringify(
|
|
1779
|
+
{
|
|
1780
|
+
provider: providerId,
|
|
1781
|
+
transport: provider.transport,
|
|
1782
|
+
grain: grainFlag,
|
|
1783
|
+
standard,
|
|
1784
|
+
dryRun,
|
|
1785
|
+
...(setPath ? { setPath } : {}),
|
|
1786
|
+
dedupeChecked,
|
|
1787
|
+
tickets: planned.map(({ ticket, action }) => ({
|
|
1788
|
+
title: ticket.title,
|
|
1789
|
+
labels: ticket.labels,
|
|
1790
|
+
severity: ticket.severity,
|
|
1791
|
+
advisory: ticket.advisory,
|
|
1792
|
+
scope: ticket.scope,
|
|
1793
|
+
bodyChars: ticket.body.length,
|
|
1794
|
+
action,
|
|
1795
|
+
// The body rides along ONLY under --dry-run: an agent inspecting the plan wants
|
|
1796
|
+
// it, a 200-ticket push payload does not.
|
|
1797
|
+
...(dryRun ? { body: ticket.body } : {}),
|
|
1798
|
+
})),
|
|
1799
|
+
unattributed: plan.unattributed,
|
|
1800
|
+
result: pushed,
|
|
1801
|
+
},
|
|
1802
|
+
null,
|
|
1803
|
+
2,
|
|
1804
|
+
),
|
|
1805
|
+
);
|
|
1806
|
+
} else if (dryRun || !provider.available()) {
|
|
1807
|
+
const create = planned.filter((x) => x.action === "create").length;
|
|
1808
|
+
console.log(
|
|
1809
|
+
fr
|
|
1810
|
+
? `ultra11y tickets : simulation (${providerId}/${provider.transport}, grain ${grainFlag}) — ${create} à créer, ${pushed.skipped} déjà présent(s).`
|
|
1811
|
+
: `ultra11y tickets: dry run (${providerId}/${provider.transport}, grain ${grainFlag}) — ${create} to create, ${pushed.skipped} already there.`,
|
|
1812
|
+
);
|
|
1813
|
+
for (const { ticket, action } of planned) console.log(` ${action === "create" ? "+" : "="} ${ticket.title}`);
|
|
1814
|
+
if (!dedupeChecked)
|
|
1815
|
+
console.error(
|
|
1816
|
+
fr
|
|
1817
|
+
? "ultra11y tickets : l'existant n'a pas pu être listé — le compte « déjà présent » n'est pas vérifié."
|
|
1818
|
+
: 'ultra11y tickets: could not list existing tickets — the "already there" count is unverified.',
|
|
1819
|
+
);
|
|
1820
|
+
if (!provider.available()) console.error(`ultra11y tickets: ${providerId} — ${provider.unavailableReason()}.`);
|
|
1821
|
+
} else {
|
|
1822
|
+
console.log(
|
|
1823
|
+
fr
|
|
1824
|
+
? `ultra11y tickets : ${providerId} — ${pushed.created} créé(s), ${pushed.skipped} déjà présent(s)${pushed.failed ? `, ${pushed.failed} en échec` : ""}.`
|
|
1825
|
+
: `ultra11y tickets: ${providerId} — ${pushed.created} created, ${pushed.skipped} already there${pushed.failed ? `, ${pushed.failed} failed` : ""}.`,
|
|
1826
|
+
);
|
|
1827
|
+
for (const u of pushed.createdUrls) console.log(` ${u}`);
|
|
1828
|
+
for (const e of pushed.errors) console.error(fr ? `ultra11y tickets : échec — ${e}` : `ultra11y tickets: failed — ${e}`);
|
|
1829
|
+
if (plan.unattributed) {
|
|
1830
|
+
console.error(
|
|
1831
|
+
fr
|
|
1832
|
+
? `ultra11y tickets : ${plan.unattributed} constat(s) non rattaché(s) à une page — déposés dans leur propre ticket, jamais répartis d'office.`
|
|
1833
|
+
: `ultra11y tickets: ${plan.unattributed} finding(s) attributed to no page — filed as their own ticket, never spread.`,
|
|
1834
|
+
);
|
|
1608
1835
|
}
|
|
1609
1836
|
}
|
|
1610
|
-
|
|
1611
|
-
console.log(JSON.stringify({ paths, ...(issuesPath ? { issuesPath } : {}), units: prdUnits(result, standard, lang), ...(gh ? { gh } : {}) }, null, 2));
|
|
1612
|
-
// Markdown was written above regardless; but if issue creation was attempted and had
|
|
1613
|
-
// any failures, exit non-zero so a CI step / caller sees the GitHub push did not fully
|
|
1614
|
-
// succeed (a total failure previously exited 0 and looked green).
|
|
1615
|
-
return gh && gh.failed > 0 ? 1 : 0;
|
|
1837
|
+
return pushed.failed > 0 ? 1 : 0;
|
|
1616
1838
|
}
|
|
1617
1839
|
|
|
1618
1840
|
/** Merged dependencies of the package.json at `root` (empty when absent/unparseable — the
|
|
@@ -2867,6 +3089,16 @@ export async function main(argv: string[]): Promise<number> {
|
|
|
2867
3089
|
console.log(HELP);
|
|
2868
3090
|
return 0;
|
|
2869
3091
|
}
|
|
3092
|
+
// A REMOVED flag must not fall through to the generic "unknown flag (ignored)" warning
|
|
3093
|
+
// below: a scripted CI would keep exiting 0 while filing nothing at all — precisely the
|
|
3094
|
+
// silent failure this release exists to end. Name the replacement and fail.
|
|
3095
|
+
for (const f of p.unknown) {
|
|
3096
|
+
const replacement = REMOVED_FLAGS[f];
|
|
3097
|
+
if (replacement) {
|
|
3098
|
+
console.error(`ultra11y: --${f} was removed in v3 — ticket creation is now its own command. Use: ${replacement}`);
|
|
3099
|
+
return 2;
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
2870
3102
|
// Warn (never silently ignore) on misspelled/unknown flags so `--grph` or
|
|
2871
3103
|
// `--standrd rgaa` can't quietly leave cross-file/a standard disabled.
|
|
2872
3104
|
for (const f of p.unknown) console.error(`ultra11y: unknown flag --${f} (ignored). Run \`ultra11y --help\`.`);
|
|
@@ -2885,6 +3117,11 @@ export async function main(argv: string[]): Promise<number> {
|
|
|
2885
3117
|
format: ["audit", "doc", "remediation", "sarif", "github", "grid", "report"],
|
|
2886
3118
|
split: ["criterion", "page"],
|
|
2887
3119
|
runtime: ["auto", "local", "docker"],
|
|
3120
|
+
provider: ["auto", "github", "gitlab", "jira"],
|
|
3121
|
+
grain: ["criterion", "page", "page-criterion", "single", "file"],
|
|
3122
|
+
// The union over every command that takes it: `mcp` serves stdio|http, `tickets`
|
|
3123
|
+
// picks cli|rest. Listing one command's values made the guard cry wolf on the other.
|
|
3124
|
+
transport: ["stdio", "http", "auto", "cli", "rest"],
|
|
2888
3125
|
};
|
|
2889
3126
|
for (const [flag, allowed] of Object.entries(ENUM_FLAGS)) {
|
|
2890
3127
|
const v = p.flags[flag];
|
|
@@ -2913,6 +3150,8 @@ export async function main(argv: string[]): Promise<number> {
|
|
|
2913
3150
|
return cmdAudit(p);
|
|
2914
3151
|
case "report":
|
|
2915
3152
|
return cmdReport(p);
|
|
3153
|
+
case "tickets":
|
|
3154
|
+
return cmdTickets(p);
|
|
2916
3155
|
case "prd":
|
|
2917
3156
|
return cmdPrd(p);
|
|
2918
3157
|
case "render":
|
package/src/config.ts
CHANGED
|
@@ -35,6 +35,16 @@ export interface Ultra11yConfig {
|
|
|
35
35
|
// severity at which a pending commit/push/PR asks the agent for a11y review; "off"
|
|
36
36
|
// disables the hook for this repo. Absent ⇒ "blocking", matching `init --hook`.
|
|
37
37
|
hook?: { failOn?: string };
|
|
38
|
+
// Defaults for `ultra11y tickets` (src/tickets/), so a repo declares its tracker once
|
|
39
|
+
// instead of repeating flags in every CI step. Flags always win, the same precedence
|
|
40
|
+
// `standard` has. CREDENTIALS ARE NOT ALLOWED HERE: this file is committed, so a token
|
|
41
|
+
// key in this block is a hard error — they belong in the environment.
|
|
42
|
+
tickets?: {
|
|
43
|
+
provider?: "github" | "gitlab" | "jira";
|
|
44
|
+
grain?: "criterion" | "page" | "page-criterion" | "single" | "file";
|
|
45
|
+
transport?: "auto" | "cli" | "rest";
|
|
46
|
+
maxTickets?: number;
|
|
47
|
+
};
|
|
38
48
|
}
|
|
39
49
|
|
|
40
50
|
export interface LoadResult {
|
package/src/gh-cli.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// The `gh` BINARY ADAPTER — nothing else. Shelling out is how ultra11y talks to GitHub
|
|
2
|
+
// without an npm dependency and without ever holding a token: `gh` handles its own auth.
|
|
3
|
+
//
|
|
4
|
+
// This module imports nothing from ultra11y on purpose, so it can be shared by the two
|
|
5
|
+
// unrelated things that need `gh` — the sticky PR comment (src/pr-comment.ts, a report
|
|
6
|
+
// surface) and the GitHub ticket provider (src/tickets/providers/github.ts) — without
|
|
7
|
+
// creating a cycle or making either depend on the other.
|
|
8
|
+
import { execFileSync } from "node:child_process";
|
|
9
|
+
|
|
10
|
+
/** Run `gh` and return its stdout. Captures stderr (pipe, not ignore) so a failed call
|
|
11
|
+
* carries a surfaceable reason on the thrown error instead of vanishing. */
|
|
12
|
+
export function ghExec(args: string[], input?: string): string {
|
|
13
|
+
return execFileSync("gh", args, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], ...(input !== undefined ? { input } : {}) });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Extract a concise, single-line failure reason from a thrown `gh` error — the first
|
|
17
|
+
* meaningful stderr line (where `gh` prints the API/auth error), else the error message. */
|
|
18
|
+
export function ghErrorReason(err: unknown): string | undefined {
|
|
19
|
+
if (!err || typeof err !== "object") return undefined;
|
|
20
|
+
const e = err as { stderr?: unknown; message?: unknown };
|
|
21
|
+
const stderr = typeof e.stderr === "string" ? e.stderr : Buffer.isBuffer(e.stderr) ? e.stderr.toString("utf8") : "";
|
|
22
|
+
const line = stderr
|
|
23
|
+
.split("\n")
|
|
24
|
+
.map((l) => l.trim())
|
|
25
|
+
.find(Boolean);
|
|
26
|
+
if (line) return line;
|
|
27
|
+
if (typeof e.message === "string" && e.message) return e.message.split("\n")[0]!.trim() || undefined;
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Is the `gh` CLI installed AND authenticated here? */
|
|
32
|
+
export function ghAvailable(): boolean {
|
|
33
|
+
try {
|
|
34
|
+
execFileSync("gh", ["auth", "status"], { stdio: "ignore" });
|
|
35
|
+
return true;
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/mcp/handlers.ts
CHANGED
|
@@ -9,6 +9,8 @@ import { writeHook, ciWorkflow } from "../init.js";
|
|
|
9
9
|
import { runPackCheck } from "../pack.js";
|
|
10
10
|
import { prdUnits, partitionUnits } from "../prd.js";
|
|
11
11
|
import { renderReport, renderPackReport } from "../report.js";
|
|
12
|
+
import { buildTickets } from "../tickets/grain.js";
|
|
13
|
+
import type { TicketGrain } from "../tickets/types.js";
|
|
12
14
|
import { buildWorklist, formatWorklist } from "../verify.js";
|
|
13
15
|
import { allSC, getSC } from "../wcag.js";
|
|
14
16
|
import { loadPack } from "../standards/index.js";
|
|
@@ -129,6 +131,8 @@ async function dispatch(name: string, args: Record<string, unknown>, cwd: string
|
|
|
129
131
|
return handleReport(args, cwd);
|
|
130
132
|
case "ultra11y_prd":
|
|
131
133
|
return handlePrd(args, cwd);
|
|
134
|
+
case "ultra11y_tickets":
|
|
135
|
+
return handleTickets(args, cwd);
|
|
132
136
|
case "ultra11y_check":
|
|
133
137
|
return handleCheck(args, cwd);
|
|
134
138
|
case "ultra11y_verify":
|
|
@@ -242,6 +246,30 @@ function handlePrd(args: Record<string, unknown>, cwd: string): unknown {
|
|
|
242
246
|
};
|
|
243
247
|
}
|
|
244
248
|
|
|
249
|
+
/** `ultra11y_tickets` — the ticket PLAN, and only the plan. It never files anything.
|
|
250
|
+
* Creating issues in somebody's tracker is an outward-facing, hard-to-undo side effect, and
|
|
251
|
+
* a prompt-injected agent must not be able to trigger it: filing stays a deliberate human
|
|
252
|
+
* `ultra11y tickets` invocation. This tool exists so an agent can SHOW the backlog it would
|
|
253
|
+
* open, then hand the reader the exact command. */
|
|
254
|
+
function handleTickets(args: Record<string, unknown>, cwd: string): unknown {
|
|
255
|
+
const standard = standardOf(args);
|
|
256
|
+
const lang = langOf(args);
|
|
257
|
+
const grain = (str(args.grain) ?? "criterion") as TicketGrain;
|
|
258
|
+
const plan = buildTickets(audit(args, cwd), { grain, standard, lang, baseDir: cwd });
|
|
259
|
+
if (plan.error === "no-pages") {
|
|
260
|
+
return { cwd, standard, grain, error: "no-pages", hint: "No page in scope — capture snapshots (render --e2e) or scan a sample (scan --sample) first." };
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
cwd,
|
|
264
|
+
standard,
|
|
265
|
+
grain,
|
|
266
|
+
dry_run: true,
|
|
267
|
+
tickets: plan.tickets.map((t) => ({ title: t.title, labels: t.labels, severity: t.severity, advisory: t.advisory, scope: t.scope, body: t.body })),
|
|
268
|
+
unattributed: plan.unattributed,
|
|
269
|
+
to_file_them: `ultra11y tickets --in <audit.json> --provider github|gitlab|jira --grain ${grain}`,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
245
273
|
function handleCheck(args: Record<string, unknown>, cwd: string): unknown {
|
|
246
274
|
const text = reportText(args, "check");
|
|
247
275
|
const res = checkReport(text, standardOf(args), langOf(args));
|
package/src/mcp/tools.ts
CHANGED
|
@@ -89,6 +89,28 @@ export const TOOLS: ToolDecl[] = [
|
|
|
89
89
|
required: ["cwd"],
|
|
90
90
|
},
|
|
91
91
|
},
|
|
92
|
+
{
|
|
93
|
+
name: "ultra11y_tickets",
|
|
94
|
+
title: "Preview the tracker tickets an audit would file",
|
|
95
|
+
description:
|
|
96
|
+
"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 " +
|
|
97
|
+
"file, or one consolidated. Returns the plan ONLY — it never creates anything; filing stays a deliberate `ultra11y tickets` run.",
|
|
98
|
+
inputSchema: {
|
|
99
|
+
type: "object",
|
|
100
|
+
properties: {
|
|
101
|
+
cwd: cwdProp,
|
|
102
|
+
globs: globsProp,
|
|
103
|
+
standard: standardProp,
|
|
104
|
+
lang: langProp,
|
|
105
|
+
grain: {
|
|
106
|
+
type: "string",
|
|
107
|
+
enum: ["criterion", "page", "page-criterion", "single", "file"],
|
|
108
|
+
description: "What ONE ticket is. Default 'criterion'.",
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
required: ["cwd"],
|
|
112
|
+
},
|
|
113
|
+
},
|
|
92
114
|
{
|
|
93
115
|
name: "ultra11y_criteria",
|
|
94
116
|
title: "The offline standards reference",
|
|
@@ -302,6 +324,11 @@ export const TOOL_META: Record<string, { write?: boolean; destructive?: boolean;
|
|
|
302
324
|
ultra11y_audit: { openWorld: false },
|
|
303
325
|
ultra11y_report: { openWorld: false },
|
|
304
326
|
ultra11y_prd: { openWorld: false },
|
|
327
|
+
// READ-ONLY and closed-world by construction: it returns the ticket PLAN and never files
|
|
328
|
+
// anything, so it neither touches the project nor reaches a tracker. Filing stays a
|
|
329
|
+
// deliberate `ultra11y tickets` run — an agent must not be able to open issues in
|
|
330
|
+
// somebody's tracker off the back of a prompt injection.
|
|
331
|
+
ultra11y_tickets: { openWorld: false },
|
|
305
332
|
ultra11y_criteria: { openWorld: false },
|
|
306
333
|
ultra11y_check: { openWorld: false },
|
|
307
334
|
ultra11y_verify: { openWorld: false },
|
package/src/pages.ts
CHANGED
|
@@ -225,6 +225,19 @@ const L = {
|
|
|
225
225
|
},
|
|
226
226
|
} as const;
|
|
227
227
|
|
|
228
|
+
/** Honesty rule 2, as one sentence: a page with no snapshot cannot earn conformity by
|
|
229
|
+
* silence. Exported so the grid, the per-page report AND the page-grain ticket all say the
|
|
230
|
+
* SAME thing — one string, several readers, no drift. Undefined for a snapshot page, which
|
|
231
|
+
* needs no caveat. */
|
|
232
|
+
export function pageBasisWarning(basis: PageScope["basis"], lang: Lang): string | undefined {
|
|
233
|
+
return basis === "snapshot" ? undefined : L[lang].basisNote;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Honesty rule 1, as one sentence: findings no page could claim are reported, never spread. */
|
|
237
|
+
export function unattributedNote(n: number, lang: Lang): string {
|
|
238
|
+
return L[lang].unattributed(n);
|
|
239
|
+
}
|
|
240
|
+
|
|
228
241
|
interface Row {
|
|
229
242
|
id: string;
|
|
230
243
|
label: string;
|