ultra11y 2.31.3 → 2.32.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.
- 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/package.json +1 -1
- package/scripts/ultra11y.mjs +95 -8
- package/skills/review-a11y/SKILL.md +1 -1
- package/skills/review-a11y/scripts/ultra11y.mjs +95 -8
- package/skills/ultra11y/SKILL.md +2 -1
- package/skills/ultra11y/references/orchestrators.md +176 -0
- package/skills/ultra11y/scripts/ultra11y.mjs +95 -8
- package/src/adjudicate.ts +84 -2
- package/src/cli.ts +44 -3
- package/src/gh.ts +56 -9
- package/src/types.ts +1 -1
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"name": "ultra11y",
|
|
11
11
|
"source": "./",
|
|
12
12
|
"description": "Automatic accessibility review of the code under change before every commit, push and pull request, plus full WCAG 2.2 AA / RGAA audits and conformance deliverables on demand.",
|
|
13
|
-
"version": "2.
|
|
13
|
+
"version": "2.32.0",
|
|
14
14
|
"author": { "name": "maxgfr" },
|
|
15
15
|
"homepage": "https://github.com/maxgfr/ultra11y",
|
|
16
16
|
"repository": "https://github.com/maxgfr/ultra11y",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra11y",
|
|
3
3
|
"description": "Accessibility for WCAG 2.2 AA and country standards (RGAA…): an automatic review of the code under change before every commit, push and pull request, plus full audits and conformance deliverables on demand.",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.32.0",
|
|
5
5
|
"author": { "name": "maxgfr" },
|
|
6
6
|
"homepage": "https://github.com/maxgfr/ultra11y",
|
|
7
7
|
"repository": "https://github.com/maxgfr/ultra11y",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra11y",
|
|
3
3
|
"description": "Accessibility for WCAG 2.2 AA and country standards (RGAA…): an automatic review of the code under change before every commit, push and pull request, plus full audits and conformance deliverables on demand.",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.32.0",
|
|
5
5
|
"author": { "name": "maxgfr", "url": "https://github.com/maxgfr" },
|
|
6
6
|
"homepage": "https://github.com/maxgfr/ultra11y",
|
|
7
7
|
"repository": "https://github.com/maxgfr/ultra11y",
|
|
@@ -27,7 +27,7 @@ import { fileURLToPath } from "node:url";
|
|
|
27
27
|
// Ownership marker: `ultra11y install --opencode` refuses to overwrite a plugin file that
|
|
28
28
|
// does not carry it, so a user's own ultra11y.js is never clobbered.
|
|
29
29
|
const ULTRA11Y_OPENCODE_PLUGIN = "ULTRA11Y_OPENCODE_PLUGIN";
|
|
30
|
-
const ULTRA11Y_PLUGIN_VERSION = "2.
|
|
30
|
+
const ULTRA11Y_PLUGIN_VERSION = "2.32.0";
|
|
31
31
|
|
|
32
32
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
33
33
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra11y",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.32.0",
|
|
4
4
|
"description": "Audit HTML/CSS/JSX against WCAG 2.2 AA and produce a dated compliance report, or author/review accessible markup (native-HTML-first). A skills.sh agent skill: an install-free static engine (bundled, no npm install) plus the agent's judgment, with check/verify gates against hallucinated non-conformities, and pluggable in-repo standards packs (RGAA France first; Section 508 & EN 301 549 next).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@10.33.0",
|
package/scripts/ultra11y.mjs
CHANGED
|
@@ -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.
|
|
9
|
+
var VERSION = "2.32.0";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
|
|
12
12
|
// src/audit.ts
|
|
@@ -49698,6 +49698,22 @@ function issueBody(unit, lang, standard = "wcag", format = "audit") {
|
|
|
49698
49698
|
}
|
|
49699
49699
|
return lines.join("\n");
|
|
49700
49700
|
}
|
|
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
|
+
}));
|
|
49716
|
+
}
|
|
49701
49717
|
function createIssue(title2, body2, labels) {
|
|
49702
49718
|
const base = ["issue", "create", "--title", title2, "--body-file", "-"];
|
|
49703
49719
|
try {
|
|
@@ -49717,7 +49733,7 @@ function recordFailure(result, reason) {
|
|
|
49717
49733
|
if (reason && !result.errors.includes(reason)) result.errors.push(reason);
|
|
49718
49734
|
}
|
|
49719
49735
|
function pushIssues(units, lang, standard = "wcag", format = "audit") {
|
|
49720
|
-
const { label
|
|
49736
|
+
const { label } = standardTag(standard);
|
|
49721
49737
|
const existing = existingIssueTitles();
|
|
49722
49738
|
const result = { created: 0, skipped: 0, failed: 0, createdTitles: [], errors: [] };
|
|
49723
49739
|
for (const u of units) {
|
|
@@ -49726,8 +49742,7 @@ function pushIssues(units, lang, standard = "wcag", format = "audit") {
|
|
|
49726
49742
|
result.skipped++;
|
|
49727
49743
|
continue;
|
|
49728
49744
|
}
|
|
49729
|
-
const
|
|
49730
|
-
const r = createIssue(title2, issueBody(u, lang, standard, format), labels);
|
|
49745
|
+
const r = createIssue(title2, issueBody(u, lang, standard, format), issueLabels(u, standard));
|
|
49731
49746
|
if (r.ok) {
|
|
49732
49747
|
result.created++;
|
|
49733
49748
|
result.createdTitles.push(title2);
|
|
@@ -51629,6 +51644,18 @@ var adjudication_default = {
|
|
|
51629
51644
|
|
|
51630
51645
|
// src/adjudicate.ts
|
|
51631
51646
|
var ADJUDICATE_MAX_EVIDENCE = 30;
|
|
51647
|
+
function adjudicationContract() {
|
|
51648
|
+
return {
|
|
51649
|
+
verdicts: [...VERDICTS],
|
|
51650
|
+
manualReasons: [...MANUAL_REASON_VALUES],
|
|
51651
|
+
requires: {
|
|
51652
|
+
C: "a non-empty justification AND citations[] naming the harvested evidence it cleared (each anchor resolvable and drawn from this criterion's own evidence); a criterion with no harvested evidence cannot be C at all",
|
|
51653
|
+
NA: "a non-empty justification",
|
|
51654
|
+
NC: "at least one groundable finding, each citing a normativeRef that resolves against the active standard",
|
|
51655
|
+
manual: `a reason \u2208 {${MANUAL_REASON_VALUES.join(", ")}}`
|
|
51656
|
+
}
|
|
51657
|
+
};
|
|
51658
|
+
}
|
|
51632
51659
|
var selectorFor = (el) => {
|
|
51633
51660
|
const id = el.attribs.id ? `#${el.attribs.id}` : "";
|
|
51634
51661
|
const cls = el.attribs.class ? `.${el.attribs.class.trim().split(/\s+/)[0]}` : "";
|
|
@@ -51830,6 +51857,31 @@ function buildAdjudicationWorklist(audit2, opts = {}) {
|
|
|
51830
51857
|
}
|
|
51831
51858
|
var NC_SEVERITY_DEFAULT = "majeur";
|
|
51832
51859
|
var MANUAL_REASONS = /* @__PURE__ */ new Set(["needs-rendered-dom", "undecidable"]);
|
|
51860
|
+
var VERDICTS = ["C", "NC", "NA", "manual"];
|
|
51861
|
+
var MANUAL_REASON_VALUES = ["needs-rendered-dom", "undecidable"];
|
|
51862
|
+
function normalizeVerdict2(v) {
|
|
51863
|
+
if (typeof v !== "string") return void 0;
|
|
51864
|
+
const k = v.trim().toLowerCase();
|
|
51865
|
+
return VERDICTS.find((x) => x.toLowerCase() === k);
|
|
51866
|
+
}
|
|
51867
|
+
function normalizeManualReason(r) {
|
|
51868
|
+
if (typeof r !== "string") return void 0;
|
|
51869
|
+
const k = r.trim().toLowerCase();
|
|
51870
|
+
return MANUAL_REASON_VALUES.find((x) => x === k);
|
|
51871
|
+
}
|
|
51872
|
+
function canonicalizeAdjudication(adj) {
|
|
51873
|
+
for (const it of adj.items) {
|
|
51874
|
+
if (it.verdict !== null) {
|
|
51875
|
+
const v = normalizeVerdict2(it.verdict);
|
|
51876
|
+
if (v !== void 0) it.verdict = v;
|
|
51877
|
+
}
|
|
51878
|
+
if (it.reason !== null && it.reason !== void 0) {
|
|
51879
|
+
const r = normalizeManualReason(it.reason);
|
|
51880
|
+
if (r !== void 0) it.reason = r;
|
|
51881
|
+
}
|
|
51882
|
+
}
|
|
51883
|
+
return adj;
|
|
51884
|
+
}
|
|
51833
51885
|
function normativeRefResolves(ref, standard, itemCriterionId) {
|
|
51834
51886
|
const r = (ref ?? "").trim();
|
|
51835
51887
|
if (!r) return false;
|
|
@@ -51852,6 +51904,7 @@ function normativeRefResolves(ref, standard, itemCriterionId) {
|
|
|
51852
51904
|
}
|
|
51853
51905
|
function applyAdjudication(audit2, adj, opts = {}) {
|
|
51854
51906
|
const issues = [];
|
|
51907
|
+
canonicalizeAdjudication(adj);
|
|
51855
51908
|
const byId2 = new Map(adj.items.map((it) => [it.criteriaId, it]));
|
|
51856
51909
|
const packMode = !isCore(adj.standard);
|
|
51857
51910
|
const open = /* @__PURE__ */ new Set();
|
|
@@ -51913,9 +51966,9 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
51913
51966
|
}
|
|
51914
51967
|
} else if (v === "manual") {
|
|
51915
51968
|
if (!it.reason || !MANUAL_REASONS.has(it.reason))
|
|
51916
|
-
issues.push(`criterion ${it.criteriaId}: a manual verdict requires reason \u2208 {
|
|
51969
|
+
issues.push(`criterion ${it.criteriaId}: a manual verdict requires reason \u2208 {${MANUAL_REASON_VALUES.join(", ")}}`);
|
|
51917
51970
|
} else {
|
|
51918
|
-
issues.push(`criterion ${it.criteriaId}: unknown verdict "${String(v)}"`);
|
|
51971
|
+
issues.push(`criterion ${it.criteriaId}: unknown verdict "${String(v)}" \u2014 expected one of ${VERDICTS.join(" | ")}`);
|
|
51919
51972
|
}
|
|
51920
51973
|
for (const rec of it.recommendations ?? []) groundInputs.push({ file: rec.file, line: rec.line, selector: rec.selector, snippet: rec.snippet });
|
|
51921
51974
|
}
|
|
@@ -52173,6 +52226,7 @@ function writeAdjudication(items, outDir, opts) {
|
|
|
52173
52226
|
schemaVersion: SCHEMA_VERSION,
|
|
52174
52227
|
standard: opts.standard,
|
|
52175
52228
|
auditDate: opts.auditDate,
|
|
52229
|
+
contract: adjudicationContract(),
|
|
52176
52230
|
items
|
|
52177
52231
|
};
|
|
52178
52232
|
writeFileSync10(todoPath, JSON.stringify(file, null, 2) + "\n");
|
|
@@ -57796,6 +57850,9 @@ Options:
|
|
|
57796
57850
|
technique + Contexte de reproduction) for a pure-auditor block
|
|
57797
57851
|
--gh-issues prd: also create one GitHub issue per criterion via the gh CLI (opt-in)
|
|
57798
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
|
|
57799
57856
|
--scaffold render: write an SSR-snapshot harness (default: ultra11y-render.tsx)
|
|
57800
57857
|
--setup render: install the zero-touch test-render capture harvester (.ultra11y/capture-setup.mjs) + print the runner wiring
|
|
57801
57858
|
--coverage render: report rendered-capture coverage (covered vs blind-spot components); with --json emits the coverage object
|
|
@@ -58012,6 +58069,7 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
|
|
|
58012
58069
|
"no-technical",
|
|
58013
58070
|
"gh-issues",
|
|
58014
58071
|
"gh-single",
|
|
58072
|
+
"issues-json",
|
|
58015
58073
|
"override",
|
|
58016
58074
|
"local",
|
|
58017
58075
|
"docker",
|
|
@@ -58785,6 +58843,19 @@ async function cmdPrd(p) {
|
|
|
58785
58843
|
const paths = writePrd(result, { out: out2, lang, split, format, standard, technical });
|
|
58786
58844
|
const json = p.flags.json === true;
|
|
58787
58845
|
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
|
+
`
|
|
58856
|
+
);
|
|
58857
|
+
if (!json) console.log(issuesPath);
|
|
58858
|
+
}
|
|
58788
58859
|
const ghMode = p.flags["gh-single"] === true ? "single" : p.flags["gh-issues"] === true ? "per-criterion" : null;
|
|
58789
58860
|
let gh2;
|
|
58790
58861
|
if (ghMode) {
|
|
@@ -58808,7 +58879,8 @@ async function cmdPrd(p) {
|
|
|
58808
58879
|
}
|
|
58809
58880
|
}
|
|
58810
58881
|
}
|
|
58811
|
-
if (json)
|
|
58882
|
+
if (json)
|
|
58883
|
+
console.log(JSON.stringify({ paths, ...issuesPath ? { issuesPath } : {}, units: prdUnits(result, standard, lang), ...gh2 ? { gh: gh2 } : {} }, null, 2));
|
|
58812
58884
|
return gh2 && gh2.failed > 0 ? 1 : 0;
|
|
58813
58885
|
}
|
|
58814
58886
|
function depsAt(root) {
|
|
@@ -59204,7 +59276,22 @@ function applyAdjudicationFile(p, adj, lang) {
|
|
|
59204
59276
|
mkdirSync15(out2, { recursive: true });
|
|
59205
59277
|
const auditPath = join45(out2, "audit-latest.json");
|
|
59206
59278
|
writeFileSync17(auditPath, JSON.stringify(r.audit, null, 2) + "\n");
|
|
59207
|
-
if (p.flags.json)
|
|
59279
|
+
if (p.flags.json)
|
|
59280
|
+
console.log(
|
|
59281
|
+
JSON.stringify(
|
|
59282
|
+
{
|
|
59283
|
+
ok: true,
|
|
59284
|
+
auditPath,
|
|
59285
|
+
applied: r.applied,
|
|
59286
|
+
stillManual: r.stillManual,
|
|
59287
|
+
conformancePct: r.audit.conformancePct,
|
|
59288
|
+
findings: r.audit.findings.length,
|
|
59289
|
+
grounding: r.grounding
|
|
59290
|
+
},
|
|
59291
|
+
null,
|
|
59292
|
+
2
|
|
59293
|
+
)
|
|
59294
|
+
);
|
|
59208
59295
|
else
|
|
59209
59296
|
console.log(
|
|
59210
59297
|
lang === "fr" ? `\u2713 ${r.applied} crit\xE8re(s) adjug\xE9(s), ${r.stillManual} laiss\xE9(s) en r\xE9siduel \u2192 ${auditPath}` : `\u2713 ${r.applied} criterion(ia) adjudicated, ${r.stillManual} left residual \u2192 ${auditPath}`
|
|
@@ -5,7 +5,7 @@ when_to_use: "Invoke WITHOUT being asked whenever changed front-end code should
|
|
|
5
5
|
allowed-tools: Bash(node ${CLAUDE_SKILL_DIR}/scripts/ultra11y.mjs *), Read, Grep, Glob
|
|
6
6
|
license: MIT
|
|
7
7
|
metadata:
|
|
8
|
-
version: 2.
|
|
8
|
+
version: 2.32.0
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
# review-a11y — accessibility review of changed code
|
|
@@ -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.
|
|
9
|
+
var VERSION = "2.32.0";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
|
|
12
12
|
// src/audit.ts
|
|
@@ -49698,6 +49698,22 @@ function issueBody(unit, lang, standard = "wcag", format = "audit") {
|
|
|
49698
49698
|
}
|
|
49699
49699
|
return lines.join("\n");
|
|
49700
49700
|
}
|
|
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
|
+
}));
|
|
49716
|
+
}
|
|
49701
49717
|
function createIssue(title2, body2, labels) {
|
|
49702
49718
|
const base = ["issue", "create", "--title", title2, "--body-file", "-"];
|
|
49703
49719
|
try {
|
|
@@ -49717,7 +49733,7 @@ function recordFailure(result, reason) {
|
|
|
49717
49733
|
if (reason && !result.errors.includes(reason)) result.errors.push(reason);
|
|
49718
49734
|
}
|
|
49719
49735
|
function pushIssues(units, lang, standard = "wcag", format = "audit") {
|
|
49720
|
-
const { label
|
|
49736
|
+
const { label } = standardTag(standard);
|
|
49721
49737
|
const existing = existingIssueTitles();
|
|
49722
49738
|
const result = { created: 0, skipped: 0, failed: 0, createdTitles: [], errors: [] };
|
|
49723
49739
|
for (const u of units) {
|
|
@@ -49726,8 +49742,7 @@ function pushIssues(units, lang, standard = "wcag", format = "audit") {
|
|
|
49726
49742
|
result.skipped++;
|
|
49727
49743
|
continue;
|
|
49728
49744
|
}
|
|
49729
|
-
const
|
|
49730
|
-
const r = createIssue(title2, issueBody(u, lang, standard, format), labels);
|
|
49745
|
+
const r = createIssue(title2, issueBody(u, lang, standard, format), issueLabels(u, standard));
|
|
49731
49746
|
if (r.ok) {
|
|
49732
49747
|
result.created++;
|
|
49733
49748
|
result.createdTitles.push(title2);
|
|
@@ -51629,6 +51644,18 @@ var adjudication_default = {
|
|
|
51629
51644
|
|
|
51630
51645
|
// src/adjudicate.ts
|
|
51631
51646
|
var ADJUDICATE_MAX_EVIDENCE = 30;
|
|
51647
|
+
function adjudicationContract() {
|
|
51648
|
+
return {
|
|
51649
|
+
verdicts: [...VERDICTS],
|
|
51650
|
+
manualReasons: [...MANUAL_REASON_VALUES],
|
|
51651
|
+
requires: {
|
|
51652
|
+
C: "a non-empty justification AND citations[] naming the harvested evidence it cleared (each anchor resolvable and drawn from this criterion's own evidence); a criterion with no harvested evidence cannot be C at all",
|
|
51653
|
+
NA: "a non-empty justification",
|
|
51654
|
+
NC: "at least one groundable finding, each citing a normativeRef that resolves against the active standard",
|
|
51655
|
+
manual: `a reason \u2208 {${MANUAL_REASON_VALUES.join(", ")}}`
|
|
51656
|
+
}
|
|
51657
|
+
};
|
|
51658
|
+
}
|
|
51632
51659
|
var selectorFor = (el) => {
|
|
51633
51660
|
const id = el.attribs.id ? `#${el.attribs.id}` : "";
|
|
51634
51661
|
const cls = el.attribs.class ? `.${el.attribs.class.trim().split(/\s+/)[0]}` : "";
|
|
@@ -51830,6 +51857,31 @@ function buildAdjudicationWorklist(audit2, opts = {}) {
|
|
|
51830
51857
|
}
|
|
51831
51858
|
var NC_SEVERITY_DEFAULT = "majeur";
|
|
51832
51859
|
var MANUAL_REASONS = /* @__PURE__ */ new Set(["needs-rendered-dom", "undecidable"]);
|
|
51860
|
+
var VERDICTS = ["C", "NC", "NA", "manual"];
|
|
51861
|
+
var MANUAL_REASON_VALUES = ["needs-rendered-dom", "undecidable"];
|
|
51862
|
+
function normalizeVerdict2(v) {
|
|
51863
|
+
if (typeof v !== "string") return void 0;
|
|
51864
|
+
const k = v.trim().toLowerCase();
|
|
51865
|
+
return VERDICTS.find((x) => x.toLowerCase() === k);
|
|
51866
|
+
}
|
|
51867
|
+
function normalizeManualReason(r) {
|
|
51868
|
+
if (typeof r !== "string") return void 0;
|
|
51869
|
+
const k = r.trim().toLowerCase();
|
|
51870
|
+
return MANUAL_REASON_VALUES.find((x) => x === k);
|
|
51871
|
+
}
|
|
51872
|
+
function canonicalizeAdjudication(adj) {
|
|
51873
|
+
for (const it of adj.items) {
|
|
51874
|
+
if (it.verdict !== null) {
|
|
51875
|
+
const v = normalizeVerdict2(it.verdict);
|
|
51876
|
+
if (v !== void 0) it.verdict = v;
|
|
51877
|
+
}
|
|
51878
|
+
if (it.reason !== null && it.reason !== void 0) {
|
|
51879
|
+
const r = normalizeManualReason(it.reason);
|
|
51880
|
+
if (r !== void 0) it.reason = r;
|
|
51881
|
+
}
|
|
51882
|
+
}
|
|
51883
|
+
return adj;
|
|
51884
|
+
}
|
|
51833
51885
|
function normativeRefResolves(ref, standard, itemCriterionId) {
|
|
51834
51886
|
const r = (ref ?? "").trim();
|
|
51835
51887
|
if (!r) return false;
|
|
@@ -51852,6 +51904,7 @@ function normativeRefResolves(ref, standard, itemCriterionId) {
|
|
|
51852
51904
|
}
|
|
51853
51905
|
function applyAdjudication(audit2, adj, opts = {}) {
|
|
51854
51906
|
const issues = [];
|
|
51907
|
+
canonicalizeAdjudication(adj);
|
|
51855
51908
|
const byId2 = new Map(adj.items.map((it) => [it.criteriaId, it]));
|
|
51856
51909
|
const packMode = !isCore(adj.standard);
|
|
51857
51910
|
const open = /* @__PURE__ */ new Set();
|
|
@@ -51913,9 +51966,9 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
51913
51966
|
}
|
|
51914
51967
|
} else if (v === "manual") {
|
|
51915
51968
|
if (!it.reason || !MANUAL_REASONS.has(it.reason))
|
|
51916
|
-
issues.push(`criterion ${it.criteriaId}: a manual verdict requires reason \u2208 {
|
|
51969
|
+
issues.push(`criterion ${it.criteriaId}: a manual verdict requires reason \u2208 {${MANUAL_REASON_VALUES.join(", ")}}`);
|
|
51917
51970
|
} else {
|
|
51918
|
-
issues.push(`criterion ${it.criteriaId}: unknown verdict "${String(v)}"`);
|
|
51971
|
+
issues.push(`criterion ${it.criteriaId}: unknown verdict "${String(v)}" \u2014 expected one of ${VERDICTS.join(" | ")}`);
|
|
51919
51972
|
}
|
|
51920
51973
|
for (const rec of it.recommendations ?? []) groundInputs.push({ file: rec.file, line: rec.line, selector: rec.selector, snippet: rec.snippet });
|
|
51921
51974
|
}
|
|
@@ -52173,6 +52226,7 @@ function writeAdjudication(items, outDir, opts) {
|
|
|
52173
52226
|
schemaVersion: SCHEMA_VERSION,
|
|
52174
52227
|
standard: opts.standard,
|
|
52175
52228
|
auditDate: opts.auditDate,
|
|
52229
|
+
contract: adjudicationContract(),
|
|
52176
52230
|
items
|
|
52177
52231
|
};
|
|
52178
52232
|
writeFileSync10(todoPath, JSON.stringify(file, null, 2) + "\n");
|
|
@@ -57796,6 +57850,9 @@ Options:
|
|
|
57796
57850
|
technique + Contexte de reproduction) for a pure-auditor block
|
|
57797
57851
|
--gh-issues prd: also create one GitHub issue per criterion via the gh CLI (opt-in)
|
|
57798
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
|
|
57799
57856
|
--scaffold render: write an SSR-snapshot harness (default: ultra11y-render.tsx)
|
|
57800
57857
|
--setup render: install the zero-touch test-render capture harvester (.ultra11y/capture-setup.mjs) + print the runner wiring
|
|
57801
57858
|
--coverage render: report rendered-capture coverage (covered vs blind-spot components); with --json emits the coverage object
|
|
@@ -58012,6 +58069,7 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
|
|
|
58012
58069
|
"no-technical",
|
|
58013
58070
|
"gh-issues",
|
|
58014
58071
|
"gh-single",
|
|
58072
|
+
"issues-json",
|
|
58015
58073
|
"override",
|
|
58016
58074
|
"local",
|
|
58017
58075
|
"docker",
|
|
@@ -58785,6 +58843,19 @@ async function cmdPrd(p) {
|
|
|
58785
58843
|
const paths = writePrd(result, { out: out2, lang, split, format, standard, technical });
|
|
58786
58844
|
const json = p.flags.json === true;
|
|
58787
58845
|
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
|
+
`
|
|
58856
|
+
);
|
|
58857
|
+
if (!json) console.log(issuesPath);
|
|
58858
|
+
}
|
|
58788
58859
|
const ghMode = p.flags["gh-single"] === true ? "single" : p.flags["gh-issues"] === true ? "per-criterion" : null;
|
|
58789
58860
|
let gh2;
|
|
58790
58861
|
if (ghMode) {
|
|
@@ -58808,7 +58879,8 @@ async function cmdPrd(p) {
|
|
|
58808
58879
|
}
|
|
58809
58880
|
}
|
|
58810
58881
|
}
|
|
58811
|
-
if (json)
|
|
58882
|
+
if (json)
|
|
58883
|
+
console.log(JSON.stringify({ paths, ...issuesPath ? { issuesPath } : {}, units: prdUnits(result, standard, lang), ...gh2 ? { gh: gh2 } : {} }, null, 2));
|
|
58812
58884
|
return gh2 && gh2.failed > 0 ? 1 : 0;
|
|
58813
58885
|
}
|
|
58814
58886
|
function depsAt(root) {
|
|
@@ -59204,7 +59276,22 @@ function applyAdjudicationFile(p, adj, lang) {
|
|
|
59204
59276
|
mkdirSync15(out2, { recursive: true });
|
|
59205
59277
|
const auditPath = join45(out2, "audit-latest.json");
|
|
59206
59278
|
writeFileSync17(auditPath, JSON.stringify(r.audit, null, 2) + "\n");
|
|
59207
|
-
if (p.flags.json)
|
|
59279
|
+
if (p.flags.json)
|
|
59280
|
+
console.log(
|
|
59281
|
+
JSON.stringify(
|
|
59282
|
+
{
|
|
59283
|
+
ok: true,
|
|
59284
|
+
auditPath,
|
|
59285
|
+
applied: r.applied,
|
|
59286
|
+
stillManual: r.stillManual,
|
|
59287
|
+
conformancePct: r.audit.conformancePct,
|
|
59288
|
+
findings: r.audit.findings.length,
|
|
59289
|
+
grounding: r.grounding
|
|
59290
|
+
},
|
|
59291
|
+
null,
|
|
59292
|
+
2
|
|
59293
|
+
)
|
|
59294
|
+
);
|
|
59208
59295
|
else
|
|
59209
59296
|
console.log(
|
|
59210
59297
|
lang === "fr" ? `\u2713 ${r.applied} crit\xE8re(s) adjug\xE9(s), ${r.stillManual} laiss\xE9(s) en r\xE9siduel \u2192 ${auditPath}` : `\u2713 ${r.applied} criterion(ia) adjudicated, ${r.stillManual} left residual \u2192 ${auditPath}`
|
package/skills/ultra11y/SKILL.md
CHANGED
|
@@ -4,7 +4,7 @@ description: "Use to AUDIT existing HTML/CSS/JSX against WCAG 2.2 AA accessibili
|
|
|
4
4
|
when_to_use: "Invoke when the user asks for an accessibility AUDIT or a formal deliverable: audit a repo, site or page against WCAG 2.2 AA or a country standard (RGAA, Section 508, EN 301 549); produce a dated conformance report, a criterion grid, a PRD remediation backlog or GitHub issues; author accessible markup from scratch; wire the repo gate (init --hook/--ci) or a rendered-DOM capture pipeline. For a review of the code UNDER CHANGE (diff, branch, PR), use the `review-a11y` skill instead."
|
|
5
5
|
license: MIT
|
|
6
6
|
metadata:
|
|
7
|
-
version: 2.
|
|
7
|
+
version: 2.32.0
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# ultra11y — audit WCAG 2.2 AA and write accessible markup
|
|
@@ -90,6 +90,7 @@ Domain knowledge first, then the tooling. Read the one that matches the question
|
|
|
90
90
|
| `references/false-positives.md` | Where a finding may still be wrong, and how to refute it |
|
|
91
91
|
| `references/rendered.md` · `references/automation.md` | Auditing produced HTML, captures, hooks and CI |
|
|
92
92
|
| `references/harnesses.md` | Where this runs: Claude Code, Codex CLI, OpenCode, and the AGENTS.md fallback |
|
|
93
|
+
| `references/orchestrators.md` | Driving the engine from a workflow engine / non-GitHub CI: pinning, the six-step pipeline, exit codes, `--issues-json` for any tracker |
|
|
93
94
|
| `references/ci.md` | `--format sarif\|github`: inline PR annotations, code scanning, job summary |
|
|
94
95
|
| `references/pages.md` | Page snapshots + the per-page criterion grid (`pages`), and its honesty rules |
|
|
95
96
|
| `references/e2e.md` | `render --e2e`: auditing a page during your Playwright/Cypress run |
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Drive the engine from an orchestrator
|
|
2
|
+
|
|
3
|
+
The skill and the GitHub Action assume a person or a coding harness is in the loop. An
|
|
4
|
+
**orchestrator** — an agent workflow engine, a CI job that is not GitHub, a cron runner —
|
|
5
|
+
is different: nothing reads a banner, nothing retries on a hunch, and the only things that
|
|
6
|
+
travel between steps are exit codes and JSON. This page is the contract for that caller.
|
|
7
|
+
|
|
8
|
+
The shape that matters: **the engine finds, the agent adjudicates, the gates refuse.** Keep
|
|
9
|
+
the detection deterministic and there is no model between "a defect exists" and "a defect is
|
|
10
|
+
counted" — which is the whole reason to wire the engine in rather than ask a model to audit.
|
|
11
|
+
|
|
12
|
+
## Resolving the engine
|
|
13
|
+
|
|
14
|
+
Pin the version. `npx` resolves `latest` otherwise, so what runs can change with no commit
|
|
15
|
+
anywhere.
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
npm_config_update_notifier=false npx -y ultra11y@2.31.2 --version # → 2.31.2, exit 0
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
- **`npm_config_update_notifier=false` is not optional.** Without it npm prints its
|
|
22
|
+
"New major version available" notice into the output a step is about to parse.
|
|
23
|
+
- **`ULTRA11Y_BIN`** points at an engine you already have (a vendored `ultra11y.mjs`, a
|
|
24
|
+
container layer). The engine is one self-contained file with no runtime dependencies, so
|
|
25
|
+
vendoring is a supported path — that is exactly what the skills and the Action do.
|
|
26
|
+
- **Node ≥ 22.18.** No other system dependency, no API key. (`judge` takes a key; nothing
|
|
27
|
+
else does. `scan` needs a browser tier — see `references/dynamic.md`.)
|
|
28
|
+
- **Verify what you resolved, and fail if it is not what you asked for.** A step that lets a
|
|
29
|
+
missing engine fall through reports zero findings, which reads exactly like a clean audit.
|
|
30
|
+
This is the one failure mode worth being loud about.
|
|
31
|
+
|
|
32
|
+
## The pipeline
|
|
33
|
+
|
|
34
|
+
Six steps. Four are deterministic; only step 3 involves a model.
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
ENGINE="npx -y ultra11y@2.31.2" # or "node $ULTRA11Y_BIN"
|
|
38
|
+
RUN=/tmp/a11y-run # out-of-tree: see "Where output lands"
|
|
39
|
+
|
|
40
|
+
# 1. FIND — the static engine. No model. This is the finding set.
|
|
41
|
+
$ENGINE audit "<globs>" --graph --out "$RUN" --json --lang en > /dev/null
|
|
42
|
+
# …or, on a pull request, exactly what the branch introduced:
|
|
43
|
+
$ENGINE audit --since "$BASE_REF" --graph --out "$RUN" --json --lang en > /dev/null
|
|
44
|
+
|
|
45
|
+
# 2. WORKLIST — a draft report, then the criteria a static pass cannot decide.
|
|
46
|
+
$ENGINE report --in "$RUN/audit-latest.json" --out "$RUN" --lang en > /dev/null
|
|
47
|
+
$ENGINE verify --report "$RUN"/wcag-*.md --in "$RUN/audit-latest.json" --manual --out "$RUN" --json
|
|
48
|
+
|
|
49
|
+
# 3. ADJUDICATE — hand $RUN/ADJUDICATE.todo.json to the agent; it fills every `verdict`.
|
|
50
|
+
# The file carries its own `contract` (see below) — the agent needs nothing else.
|
|
51
|
+
|
|
52
|
+
# 4. FOLD — fail-closed. Exit 1 with per-criterion reasons if a verdict is unsupported.
|
|
53
|
+
$ENGINE verify --apply "$RUN/adjudication.json" --in "$RUN/audit-latest.json" --out "$RUN" --json
|
|
54
|
+
|
|
55
|
+
# 5. REPORT — deterministic rendering of the adjudicated audit.
|
|
56
|
+
$ENGINE report --in "$RUN/audit-latest.json" --out audits --standard rgaa --lang fr
|
|
57
|
+
|
|
58
|
+
# 6. GATE — integrity. Exit ≠ 0 on an invented criterion or unjustified NA.
|
|
59
|
+
$ENGINE check --report audits/rgaa-*.md --in "$RUN/audit-latest.json" --json
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Step 4 is the one to understand: it is a **deterministic gate on the model's output**. A
|
|
63
|
+
verdict with no justification, an NC with no resolvable `normativeRef`, a criterion missing
|
|
64
|
+
from the adjudication — each is refused by name, and the run stops. That is what keeps the
|
|
65
|
+
final report defensible.
|
|
66
|
+
|
|
67
|
+
## The adjudication contract
|
|
68
|
+
|
|
69
|
+
`ADJUDICATE.todo.json` declares what may be written into it, so the filler never has to read
|
|
70
|
+
ultra11y's source:
|
|
71
|
+
|
|
72
|
+
```jsonc
|
|
73
|
+
{
|
|
74
|
+
"contract": {
|
|
75
|
+
"verdicts": ["C", "NC", "NA", "manual"],
|
|
76
|
+
"manualReasons": ["needs-rendered-dom", "undecidable"],
|
|
77
|
+
"requires": {
|
|
78
|
+
"C": "a non-empty justification AND citations[] naming the harvested evidence it cleared …",
|
|
79
|
+
"NA": "a non-empty justification",
|
|
80
|
+
"NC": "at least one groundable finding, each citing a normativeRef that resolves against the active standard",
|
|
81
|
+
"manual": "a reason ∈ {needs-rendered-dom, undecidable}"
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
"items": [ /* one per criterion the engine could not decide; fill `verdict` */ ]
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Note what `C` costs: a clearing verdict is gated exactly like an accusing one. It must
|
|
89
|
+
name in `citations[]` the evidence it cleared, each anchor resolvable and drawn from that
|
|
90
|
+
criterion's own harvested evidence — and a criterion presented with **no** evidence cannot
|
|
91
|
+
be `C` at all. This is what stops "C everywhere with a plausible sentence" from passing.
|
|
92
|
+
|
|
93
|
+
The header is **advisory**. `verify --apply` validates against its own constants, so editing
|
|
94
|
+
the contract in the file widens nothing.
|
|
95
|
+
|
|
96
|
+
Verdicts are matched **case-insensitively** (`na` is `NA`, `MANUAL` is `manual`) — a spelling
|
|
97
|
+
accident is not a disagreement about accessibility. A verdict outside the vocabulary is still
|
|
98
|
+
refused, and the message names the four that are not.
|
|
99
|
+
|
|
100
|
+
`manual` is the honest verdict for anything that genuinely needs a rendered page. It keeps
|
|
101
|
+
the criterion in `residualRisks`, where the report declares it as a residual risk. **Never
|
|
102
|
+
map an undecided criterion to `C`** — that is the one way to turn this pipeline back into
|
|
103
|
+
the thing it replaces.
|
|
104
|
+
|
|
105
|
+
## Filing findings on a tracker that is not GitHub
|
|
106
|
+
|
|
107
|
+
`--gh-issues` shells out to the `gh` CLI. For any other board, take the same items as JSON:
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
$ENGINE prd --in "$RUN/audit-latest.json" --out "$RUN" --issues-json --lang en
|
|
111
|
+
# → $RUN/issues-<date>.json
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
```jsonc
|
|
115
|
+
{
|
|
116
|
+
"tool": "ultra11y", "kind": "issues", "schemaVersion": 1,
|
|
117
|
+
"standard": "wcag", "date": "2026-08-12", "count": 4,
|
|
118
|
+
"issues": [{
|
|
119
|
+
"criteriaId": "1.1.1",
|
|
120
|
+
"title": "[a11y] WCAG 1.1.1 — Non-text Content", // the de-dupe grain: stable across runs
|
|
121
|
+
"body": "…", // the auditor block, rendered
|
|
122
|
+
"labels": ["accessibility", "wcag", "bloquant"],
|
|
123
|
+
"severity": "bloquant",
|
|
124
|
+
"advisory": false, // true ⇒ a good practice, NOT a non-conformity
|
|
125
|
+
"occurrences": [{ "file": "src/Page.tsx", "line": 4, "selector": "img", "message": "…" }]
|
|
126
|
+
}]
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
One issue per criterion, not per occurrence — a missing `alt` on forty images is one item
|
|
131
|
+
with forty `occurrences`, not forty tickets. De-dupe on `title` and re-running files nothing
|
|
132
|
+
twice. `advisory: true` marks a non-normative recommendation: it must not be triaged as a
|
|
133
|
+
non-conformity, and it never enters the conformance rate.
|
|
134
|
+
|
|
135
|
+
## Where output lands
|
|
136
|
+
|
|
137
|
+
`--out <dir>` fully contains what a command writes: pointed outside the checkout, an audit
|
|
138
|
+
leaves the target repository byte-identical. That matters when the orchestrator audits a
|
|
139
|
+
repo it does not own — write the run's intermediates out-of-tree and let only the final
|
|
140
|
+
dated report land in the project, if at all.
|
|
141
|
+
|
|
142
|
+
`.ultra11y/pages/` is written only by the page-snapshot commands (`scan`, `snapshot`,
|
|
143
|
+
`dev`); `--root` relocates it. `audit`, `report`, `prd`, `verify` and `check` never touch it.
|
|
144
|
+
|
|
145
|
+
## Exit codes
|
|
146
|
+
|
|
147
|
+
| | 0 | 1 | 2 |
|
|
148
|
+
|---|---|---|---|
|
|
149
|
+
| `audit` | reported (no `--fail-on`), or clean | findings ≥ `--fail-on` | usage / bad input |
|
|
150
|
+
| `verify --apply` | folded | the adjudication was refused (see `issues[]`) | usage / bad input |
|
|
151
|
+
| `check` | report is sound | integrity failure | report not found / bad input |
|
|
152
|
+
| `prd` | written | `--gh-*` had failures | usage / bad input |
|
|
153
|
+
|
|
154
|
+
Read exit codes from the process, **not** from a pipeline — `cmd --json | head` reports
|
|
155
|
+
`head`'s status, which is how a failing gate reads as a pass.
|
|
156
|
+
|
|
157
|
+
## What the JSON tells you
|
|
158
|
+
|
|
159
|
+
- `audit --json` → the `AuditResult` (also at `<out>/audit-latest.json`): `findings[]` with
|
|
160
|
+
stable `findingId`, `criteriaId`, `severity`, `file`, `line`; `residualRisks[]`;
|
|
161
|
+
`conformancePct` — the **automatic static pass rate**, not a conformance rate.
|
|
162
|
+
- `verify --apply --json` → `{ ok, auditPath, applied, stillManual, conformancePct, findings, grounding }`
|
|
163
|
+
on success; `{ ok: false, issues[], audit }` when refused.
|
|
164
|
+
- `check --json` → `{ ok, issues[] }`.
|
|
165
|
+
|
|
166
|
+
## Honesty rules an orchestrator has to carry
|
|
167
|
+
|
|
168
|
+
The engine is explicit about what it did not decide; a wrapper that flattens that away
|
|
169
|
+
produces exactly the false clean bill of health this design exists to prevent.
|
|
170
|
+
|
|
171
|
+
1. **A missing engine is a failure, never an empty finding set.**
|
|
172
|
+
2. **Report the residual criteria.** The rendering criteria (contrast, focus, zoom, reflow)
|
|
173
|
+
are undecided until `scan` runs. Say so; do not let a step summary imply full coverage.
|
|
174
|
+
3. **`conformancePct` is the static pass rate.** Label it as such.
|
|
175
|
+
4. **Never let a model write the counts.** Take them from the JSON — the numbers exist
|
|
176
|
+
precisely so nothing has to be re-derived by a model that may drop a field.
|
|
@@ -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.
|
|
9
|
+
var VERSION = "2.32.0";
|
|
10
10
|
var SCHEMA_VERSION = 2;
|
|
11
11
|
|
|
12
12
|
// src/audit.ts
|
|
@@ -49698,6 +49698,22 @@ function issueBody(unit, lang, standard = "wcag", format = "audit") {
|
|
|
49698
49698
|
}
|
|
49699
49699
|
return lines.join("\n");
|
|
49700
49700
|
}
|
|
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
|
+
}));
|
|
49716
|
+
}
|
|
49701
49717
|
function createIssue(title2, body2, labels) {
|
|
49702
49718
|
const base = ["issue", "create", "--title", title2, "--body-file", "-"];
|
|
49703
49719
|
try {
|
|
@@ -49717,7 +49733,7 @@ function recordFailure(result, reason) {
|
|
|
49717
49733
|
if (reason && !result.errors.includes(reason)) result.errors.push(reason);
|
|
49718
49734
|
}
|
|
49719
49735
|
function pushIssues(units, lang, standard = "wcag", format = "audit") {
|
|
49720
|
-
const { label
|
|
49736
|
+
const { label } = standardTag(standard);
|
|
49721
49737
|
const existing = existingIssueTitles();
|
|
49722
49738
|
const result = { created: 0, skipped: 0, failed: 0, createdTitles: [], errors: [] };
|
|
49723
49739
|
for (const u of units) {
|
|
@@ -49726,8 +49742,7 @@ function pushIssues(units, lang, standard = "wcag", format = "audit") {
|
|
|
49726
49742
|
result.skipped++;
|
|
49727
49743
|
continue;
|
|
49728
49744
|
}
|
|
49729
|
-
const
|
|
49730
|
-
const r = createIssue(title2, issueBody(u, lang, standard, format), labels);
|
|
49745
|
+
const r = createIssue(title2, issueBody(u, lang, standard, format), issueLabels(u, standard));
|
|
49731
49746
|
if (r.ok) {
|
|
49732
49747
|
result.created++;
|
|
49733
49748
|
result.createdTitles.push(title2);
|
|
@@ -51629,6 +51644,18 @@ var adjudication_default = {
|
|
|
51629
51644
|
|
|
51630
51645
|
// src/adjudicate.ts
|
|
51631
51646
|
var ADJUDICATE_MAX_EVIDENCE = 30;
|
|
51647
|
+
function adjudicationContract() {
|
|
51648
|
+
return {
|
|
51649
|
+
verdicts: [...VERDICTS],
|
|
51650
|
+
manualReasons: [...MANUAL_REASON_VALUES],
|
|
51651
|
+
requires: {
|
|
51652
|
+
C: "a non-empty justification AND citations[] naming the harvested evidence it cleared (each anchor resolvable and drawn from this criterion's own evidence); a criterion with no harvested evidence cannot be C at all",
|
|
51653
|
+
NA: "a non-empty justification",
|
|
51654
|
+
NC: "at least one groundable finding, each citing a normativeRef that resolves against the active standard",
|
|
51655
|
+
manual: `a reason \u2208 {${MANUAL_REASON_VALUES.join(", ")}}`
|
|
51656
|
+
}
|
|
51657
|
+
};
|
|
51658
|
+
}
|
|
51632
51659
|
var selectorFor = (el) => {
|
|
51633
51660
|
const id = el.attribs.id ? `#${el.attribs.id}` : "";
|
|
51634
51661
|
const cls = el.attribs.class ? `.${el.attribs.class.trim().split(/\s+/)[0]}` : "";
|
|
@@ -51830,6 +51857,31 @@ function buildAdjudicationWorklist(audit2, opts = {}) {
|
|
|
51830
51857
|
}
|
|
51831
51858
|
var NC_SEVERITY_DEFAULT = "majeur";
|
|
51832
51859
|
var MANUAL_REASONS = /* @__PURE__ */ new Set(["needs-rendered-dom", "undecidable"]);
|
|
51860
|
+
var VERDICTS = ["C", "NC", "NA", "manual"];
|
|
51861
|
+
var MANUAL_REASON_VALUES = ["needs-rendered-dom", "undecidable"];
|
|
51862
|
+
function normalizeVerdict2(v) {
|
|
51863
|
+
if (typeof v !== "string") return void 0;
|
|
51864
|
+
const k = v.trim().toLowerCase();
|
|
51865
|
+
return VERDICTS.find((x) => x.toLowerCase() === k);
|
|
51866
|
+
}
|
|
51867
|
+
function normalizeManualReason(r) {
|
|
51868
|
+
if (typeof r !== "string") return void 0;
|
|
51869
|
+
const k = r.trim().toLowerCase();
|
|
51870
|
+
return MANUAL_REASON_VALUES.find((x) => x === k);
|
|
51871
|
+
}
|
|
51872
|
+
function canonicalizeAdjudication(adj) {
|
|
51873
|
+
for (const it of adj.items) {
|
|
51874
|
+
if (it.verdict !== null) {
|
|
51875
|
+
const v = normalizeVerdict2(it.verdict);
|
|
51876
|
+
if (v !== void 0) it.verdict = v;
|
|
51877
|
+
}
|
|
51878
|
+
if (it.reason !== null && it.reason !== void 0) {
|
|
51879
|
+
const r = normalizeManualReason(it.reason);
|
|
51880
|
+
if (r !== void 0) it.reason = r;
|
|
51881
|
+
}
|
|
51882
|
+
}
|
|
51883
|
+
return adj;
|
|
51884
|
+
}
|
|
51833
51885
|
function normativeRefResolves(ref, standard, itemCriterionId) {
|
|
51834
51886
|
const r = (ref ?? "").trim();
|
|
51835
51887
|
if (!r) return false;
|
|
@@ -51852,6 +51904,7 @@ function normativeRefResolves(ref, standard, itemCriterionId) {
|
|
|
51852
51904
|
}
|
|
51853
51905
|
function applyAdjudication(audit2, adj, opts = {}) {
|
|
51854
51906
|
const issues = [];
|
|
51907
|
+
canonicalizeAdjudication(adj);
|
|
51855
51908
|
const byId2 = new Map(adj.items.map((it) => [it.criteriaId, it]));
|
|
51856
51909
|
const packMode = !isCore(adj.standard);
|
|
51857
51910
|
const open = /* @__PURE__ */ new Set();
|
|
@@ -51913,9 +51966,9 @@ function applyAdjudication(audit2, adj, opts = {}) {
|
|
|
51913
51966
|
}
|
|
51914
51967
|
} else if (v === "manual") {
|
|
51915
51968
|
if (!it.reason || !MANUAL_REASONS.has(it.reason))
|
|
51916
|
-
issues.push(`criterion ${it.criteriaId}: a manual verdict requires reason \u2208 {
|
|
51969
|
+
issues.push(`criterion ${it.criteriaId}: a manual verdict requires reason \u2208 {${MANUAL_REASON_VALUES.join(", ")}}`);
|
|
51917
51970
|
} else {
|
|
51918
|
-
issues.push(`criterion ${it.criteriaId}: unknown verdict "${String(v)}"`);
|
|
51971
|
+
issues.push(`criterion ${it.criteriaId}: unknown verdict "${String(v)}" \u2014 expected one of ${VERDICTS.join(" | ")}`);
|
|
51919
51972
|
}
|
|
51920
51973
|
for (const rec of it.recommendations ?? []) groundInputs.push({ file: rec.file, line: rec.line, selector: rec.selector, snippet: rec.snippet });
|
|
51921
51974
|
}
|
|
@@ -52173,6 +52226,7 @@ function writeAdjudication(items, outDir, opts) {
|
|
|
52173
52226
|
schemaVersion: SCHEMA_VERSION,
|
|
52174
52227
|
standard: opts.standard,
|
|
52175
52228
|
auditDate: opts.auditDate,
|
|
52229
|
+
contract: adjudicationContract(),
|
|
52176
52230
|
items
|
|
52177
52231
|
};
|
|
52178
52232
|
writeFileSync10(todoPath, JSON.stringify(file, null, 2) + "\n");
|
|
@@ -57796,6 +57850,9 @@ Options:
|
|
|
57796
57850
|
technique + Contexte de reproduction) for a pure-auditor block
|
|
57797
57851
|
--gh-issues prd: also create one GitHub issue per criterion via the gh CLI (opt-in)
|
|
57798
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
|
|
57799
57856
|
--scaffold render: write an SSR-snapshot harness (default: ultra11y-render.tsx)
|
|
57800
57857
|
--setup render: install the zero-touch test-render capture harvester (.ultra11y/capture-setup.mjs) + print the runner wiring
|
|
57801
57858
|
--coverage render: report rendered-capture coverage (covered vs blind-spot components); with --json emits the coverage object
|
|
@@ -58012,6 +58069,7 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
|
|
|
58012
58069
|
"no-technical",
|
|
58013
58070
|
"gh-issues",
|
|
58014
58071
|
"gh-single",
|
|
58072
|
+
"issues-json",
|
|
58015
58073
|
"override",
|
|
58016
58074
|
"local",
|
|
58017
58075
|
"docker",
|
|
@@ -58785,6 +58843,19 @@ async function cmdPrd(p) {
|
|
|
58785
58843
|
const paths = writePrd(result, { out: out2, lang, split, format, standard, technical });
|
|
58786
58844
|
const json = p.flags.json === true;
|
|
58787
58845
|
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
|
+
`
|
|
58856
|
+
);
|
|
58857
|
+
if (!json) console.log(issuesPath);
|
|
58858
|
+
}
|
|
58788
58859
|
const ghMode = p.flags["gh-single"] === true ? "single" : p.flags["gh-issues"] === true ? "per-criterion" : null;
|
|
58789
58860
|
let gh2;
|
|
58790
58861
|
if (ghMode) {
|
|
@@ -58808,7 +58879,8 @@ async function cmdPrd(p) {
|
|
|
58808
58879
|
}
|
|
58809
58880
|
}
|
|
58810
58881
|
}
|
|
58811
|
-
if (json)
|
|
58882
|
+
if (json)
|
|
58883
|
+
console.log(JSON.stringify({ paths, ...issuesPath ? { issuesPath } : {}, units: prdUnits(result, standard, lang), ...gh2 ? { gh: gh2 } : {} }, null, 2));
|
|
58812
58884
|
return gh2 && gh2.failed > 0 ? 1 : 0;
|
|
58813
58885
|
}
|
|
58814
58886
|
function depsAt(root) {
|
|
@@ -59204,7 +59276,22 @@ function applyAdjudicationFile(p, adj, lang) {
|
|
|
59204
59276
|
mkdirSync15(out2, { recursive: true });
|
|
59205
59277
|
const auditPath = join45(out2, "audit-latest.json");
|
|
59206
59278
|
writeFileSync17(auditPath, JSON.stringify(r.audit, null, 2) + "\n");
|
|
59207
|
-
if (p.flags.json)
|
|
59279
|
+
if (p.flags.json)
|
|
59280
|
+
console.log(
|
|
59281
|
+
JSON.stringify(
|
|
59282
|
+
{
|
|
59283
|
+
ok: true,
|
|
59284
|
+
auditPath,
|
|
59285
|
+
applied: r.applied,
|
|
59286
|
+
stillManual: r.stillManual,
|
|
59287
|
+
conformancePct: r.audit.conformancePct,
|
|
59288
|
+
findings: r.audit.findings.length,
|
|
59289
|
+
grounding: r.grounding
|
|
59290
|
+
},
|
|
59291
|
+
null,
|
|
59292
|
+
2
|
|
59293
|
+
)
|
|
59294
|
+
);
|
|
59208
59295
|
else
|
|
59209
59296
|
console.log(
|
|
59210
59297
|
lang === "fr" ? `\u2713 ${r.applied} crit\xE8re(s) adjug\xE9(s), ${r.stillManual} laiss\xE9(s) en r\xE9siduel \u2192 ${auditPath}` : `\u2713 ${r.applied} criterion(ia) adjudicated, ${r.stillManual} left residual \u2192 ${auditPath}`
|
package/src/adjudicate.ts
CHANGED
|
@@ -102,9 +102,37 @@ export interface AdjudicationFile {
|
|
|
102
102
|
schemaVersion: number;
|
|
103
103
|
standard: StandardId;
|
|
104
104
|
auditDate: string;
|
|
105
|
+
// The contract, carried BY the worklist. Whoever fills this file — an agent in a coding
|
|
106
|
+
// harness, an orchestrator's tool node, a script — reads the file, not ultra11y's source,
|
|
107
|
+
// so the file has to say what a verdict may be and what each one additionally requires.
|
|
108
|
+
// Advisory to the reader, never read back by `applyAdjudication` (which validates against
|
|
109
|
+
// its own constants), so a hand-edited or stale header can never widen what is accepted.
|
|
110
|
+
contract?: AdjudicationContract;
|
|
105
111
|
items: AdjudicationItem[];
|
|
106
112
|
}
|
|
107
113
|
|
|
114
|
+
/** What the filler of an adjudication file is allowed to write, stated in the file. */
|
|
115
|
+
export interface AdjudicationContract {
|
|
116
|
+
verdicts: readonly string[];
|
|
117
|
+
manualReasons: readonly string[];
|
|
118
|
+
requires: Record<string, string>;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** The contract as written into every worklist. Derived from the same constants the gate
|
|
122
|
+
* validates against, so the two cannot drift. */
|
|
123
|
+
export function adjudicationContract(): AdjudicationContract {
|
|
124
|
+
return {
|
|
125
|
+
verdicts: [...VERDICTS],
|
|
126
|
+
manualReasons: [...MANUAL_REASON_VALUES],
|
|
127
|
+
requires: {
|
|
128
|
+
C: "a non-empty justification AND citations[] naming the harvested evidence it cleared (each anchor resolvable and drawn from this criterion's own evidence); a criterion with no harvested evidence cannot be C at all",
|
|
129
|
+
NA: "a non-empty justification",
|
|
130
|
+
NC: "at least one groundable finding, each citing a normativeRef that resolves against the active standard",
|
|
131
|
+
manual: `a reason ∈ {${MANUAL_REASON_VALUES.join(", ")}}`,
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
108
136
|
// ---- evidence harvesters ----
|
|
109
137
|
// Each harvester answers "for this SC, what did the engine see that the agent needs to
|
|
110
138
|
// rule?" — bounded, source-anchored, language-neutral. A criterion with no harvester gets
|
|
@@ -390,6 +418,53 @@ export interface ApplyAdjudicationResult {
|
|
|
390
418
|
const NC_SEVERITY_DEFAULT: Severity = "majeur";
|
|
391
419
|
const MANUAL_REASONS = new Set(["needs-rendered-dom", "undecidable"]);
|
|
392
420
|
|
|
421
|
+
/** The verdicts an adjudication may carry, in the exact spelling the file must use. Exported
|
|
422
|
+
* because it is a CONTRACT, not an implementation detail: the worklist declares it in its own
|
|
423
|
+
* header and the rejection message names it, so whoever fills the file never has to read this
|
|
424
|
+
* source to learn what they are allowed to write. */
|
|
425
|
+
export const VERDICTS = ["C", "NC", "NA", "manual"] as const;
|
|
426
|
+
/** The reasons a still-`manual` verdict may cite — same contract, same reason to export it. */
|
|
427
|
+
export const MANUAL_REASON_VALUES = ["needs-rendered-dom", "undecidable"] as const;
|
|
428
|
+
|
|
429
|
+
/** Canonicalise a verdict written in any case ("na", "Nc", "MANUAL" → "NA", "NC", "manual").
|
|
430
|
+
*
|
|
431
|
+
* Three of the four verdicts are upper-case and the fourth is not, which is exactly the kind
|
|
432
|
+
* of detail an agent filling the worklist gets wrong — and it used to cost the entire run,
|
|
433
|
+
* because `applyAdjudication` fail-closes on an unknown verdict. Case carries no meaning
|
|
434
|
+
* here: "na" can only ever mean NA. What stays rejected is a verdict outside the vocabulary,
|
|
435
|
+
* which is a real disagreement about the contract rather than a spelling accident. Returns
|
|
436
|
+
* undefined when there is no match. */
|
|
437
|
+
export function normalizeVerdict(v: unknown): Exclude<CriterionVerdict, null> | undefined {
|
|
438
|
+
if (typeof v !== "string") return undefined;
|
|
439
|
+
const k = v.trim().toLowerCase();
|
|
440
|
+
return VERDICTS.find((x) => x.toLowerCase() === k);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** The same tolerance for a `manual` verdict's reason. */
|
|
444
|
+
function normalizeManualReason(r: unknown): string | undefined {
|
|
445
|
+
if (typeof r !== "string") return undefined;
|
|
446
|
+
const k = r.trim().toLowerCase();
|
|
447
|
+
return MANUAL_REASON_VALUES.find((x) => x === k);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** Rewrite an adjudication's verdicts and manual reasons into their canonical spelling, in
|
|
451
|
+
* place. Anything unrecognised is left untouched, so the per-item validation below still
|
|
452
|
+
* reports it as the contract violation it is — this normalises spelling, it never invents a
|
|
453
|
+
* decision the file did not carry. */
|
|
454
|
+
export function canonicalizeAdjudication(adj: AdjudicationFile): AdjudicationFile {
|
|
455
|
+
for (const it of adj.items) {
|
|
456
|
+
if (it.verdict !== null) {
|
|
457
|
+
const v = normalizeVerdict(it.verdict);
|
|
458
|
+
if (v !== undefined) it.verdict = v;
|
|
459
|
+
}
|
|
460
|
+
if (it.reason !== null && it.reason !== undefined) {
|
|
461
|
+
const r = normalizeManualReason(it.reason);
|
|
462
|
+
if (r !== undefined) it.reason = r;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return adj;
|
|
466
|
+
}
|
|
467
|
+
|
|
393
468
|
/** Does an NC finding's `normativeRef` resolve against the ACTIVE standard?
|
|
394
469
|
*
|
|
395
470
|
* Core: a real success-criterion id (reuses `hasSC`).
|
|
@@ -433,6 +508,9 @@ function normativeRefResolves(ref: string | undefined, standard: StandardId, ite
|
|
|
433
508
|
* a shrunk residual set, and the `adjudicated` marker. */
|
|
434
509
|
export function applyAdjudication(audit: AuditResult, adj: AdjudicationFile, opts: { cwd?: string } = {}): ApplyAdjudicationResult {
|
|
435
510
|
const issues: string[] = [];
|
|
511
|
+
// Spelling first, decisions second: "na" is NA, and rejecting the run over the case of a
|
|
512
|
+
// verdict taught the caller nothing about accessibility. Everything below stays fail-closed.
|
|
513
|
+
canonicalizeAdjudication(adj);
|
|
436
514
|
const byId = new Map(adj.items.map((it) => [it.criteriaId, it]));
|
|
437
515
|
|
|
438
516
|
// Coverage. Under the core that means every residual success criterion; under a pack it
|
|
@@ -520,9 +598,12 @@ export function applyAdjudication(audit: AuditResult, adj: AdjudicationFile, opt
|
|
|
520
598
|
}
|
|
521
599
|
} else if (v === "manual") {
|
|
522
600
|
if (!it.reason || !MANUAL_REASONS.has(it.reason))
|
|
523
|
-
issues.push(`criterion ${it.criteriaId}: a manual verdict requires reason ∈ {
|
|
601
|
+
issues.push(`criterion ${it.criteriaId}: a manual verdict requires reason ∈ {${MANUAL_REASON_VALUES.join(", ")}}`);
|
|
524
602
|
} else {
|
|
525
|
-
|
|
603
|
+
// Name the vocabulary in the rejection. The caller is usually a model that has just
|
|
604
|
+
// spent a whole worklist filling this file; "unknown verdict" alone told it nothing
|
|
605
|
+
// about how to be right on the retry.
|
|
606
|
+
issues.push(`criterion ${it.criteriaId}: unknown verdict "${String(v)}" — expected one of ${VERDICTS.join(" | ")}`);
|
|
526
607
|
}
|
|
527
608
|
// Recommendations are independent of the verdict (a C criterion may still carry a good
|
|
528
609
|
// practice) and are grounded exactly like an NC finding — no normativeRef required, as
|
|
@@ -912,6 +993,7 @@ export function writeAdjudication(
|
|
|
912
993
|
schemaVersion: SCHEMA_VERSION,
|
|
913
994
|
standard: opts.standard,
|
|
914
995
|
auditDate: opts.auditDate,
|
|
996
|
+
contract: adjudicationContract(),
|
|
915
997
|
items,
|
|
916
998
|
};
|
|
917
999
|
writeFileSync(todoPath, JSON.stringify(file, null, 2) + "\n");
|
package/src/cli.ts
CHANGED
|
@@ -6,7 +6,7 @@ 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 { ghAvailable, pushIssues, pushPrComment, pushSingleIssue } from "./gh.js";
|
|
9
|
+
import { ghAvailable, issueSet, pushIssues, pushPrComment, pushSingleIssue, type IssueFormat } from "./gh.js";
|
|
10
10
|
import {
|
|
11
11
|
detectFrameworks,
|
|
12
12
|
renderPlan,
|
|
@@ -294,6 +294,9 @@ Options:
|
|
|
294
294
|
technique + Contexte de reproduction) for a pure-auditor block
|
|
295
295
|
--gh-issues prd: also create one GitHub issue per criterion via the gh CLI (opt-in)
|
|
296
296
|
--gh-single prd: file the whole audit as ONE consolidated GitHub issue (opt-in; wins over --gh-issues)
|
|
297
|
+
--issues-json prd: write the issue set (one item per criterion: title, body, labels,
|
|
298
|
+
severity, occurrences) to <out>/issues-<date>.json for a non-GitHub
|
|
299
|
+
tracker to file — same items --gh-issues would create, no gh needed
|
|
297
300
|
--scaffold render: write an SSR-snapshot harness (default: ultra11y-render.tsx)
|
|
298
301
|
--setup render: install the zero-touch test-render capture harvester (.ultra11y/capture-setup.mjs) + print the runner wiring
|
|
299
302
|
--coverage render: report rendered-capture coverage (covered vs blind-spot components); with --json emits the coverage object
|
|
@@ -527,6 +530,7 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
527
530
|
"no-technical",
|
|
528
531
|
"gh-issues",
|
|
529
532
|
"gh-single",
|
|
533
|
+
"issues-json",
|
|
530
534
|
"override",
|
|
531
535
|
"local",
|
|
532
536
|
"docker",
|
|
@@ -1557,6 +1561,23 @@ async function cmdPrd(p: ParsedArgs): Promise<number> {
|
|
|
1557
1561
|
const json = p.flags.json === true;
|
|
1558
1562
|
if (!json) for (const path of paths) console.log(path);
|
|
1559
1563
|
|
|
1564
|
+
// Tracker-agnostic export: the same de-duplicated items `--gh-issues` files, as JSON, so a
|
|
1565
|
+
// board that is not GitHub can file them itself. Written to a file (not stdout) so it
|
|
1566
|
+
// composes with --json, and so an orchestrator reads one stable path instead of parsing
|
|
1567
|
+
// a payload that also carries the markdown paths.
|
|
1568
|
+
let issuesPath: string | undefined;
|
|
1569
|
+
if (p.flags["issues-json"] === true) {
|
|
1570
|
+
const issueFormat: IssueFormat = format === "remediation" ? "remediation" : "audit";
|
|
1571
|
+
const issues = issueSet(prdUnits(result, standard, lang), lang, standard, issueFormat);
|
|
1572
|
+
mkdirSync(out, { recursive: true });
|
|
1573
|
+
issuesPath = join(out, `issues-${result.date}.json`);
|
|
1574
|
+
writeFileSync(
|
|
1575
|
+
issuesPath,
|
|
1576
|
+
`${JSON.stringify({ tool: "ultra11y", kind: "issues", schemaVersion: 1, standard, date: result.date, count: issues.length, issues }, null, 2)}\n`,
|
|
1577
|
+
);
|
|
1578
|
+
if (!json) console.log(issuesPath);
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1560
1581
|
// GitHub: always-written markdown above; issues are opt-in + best-effort.
|
|
1561
1582
|
// --gh-single → one consolidated issue; --gh-issues → one issue per criterion.
|
|
1562
1583
|
const ghMode: "single" | "per-criterion" | null = p.flags["gh-single"] === true ? "single" : p.flags["gh-issues"] === true ? "per-criterion" : null;
|
|
@@ -1586,7 +1607,8 @@ async function cmdPrd(p: ParsedArgs): Promise<number> {
|
|
|
1586
1607
|
}
|
|
1587
1608
|
}
|
|
1588
1609
|
}
|
|
1589
|
-
if (json)
|
|
1610
|
+
if (json)
|
|
1611
|
+
console.log(JSON.stringify({ paths, ...(issuesPath ? { issuesPath } : {}), units: prdUnits(result, standard, lang), ...(gh ? { gh } : {}) }, null, 2));
|
|
1590
1612
|
// Markdown was written above regardless; but if issue creation was attempted and had
|
|
1591
1613
|
// any failures, exit non-zero so a CI step / caller sees the GitHub push did not fully
|
|
1592
1614
|
// succeed (a total failure previously exited 0 and looked green).
|
|
@@ -2076,7 +2098,26 @@ function applyAdjudicationFile(p: ParsedArgs, adj: AdjudicationFile, lang: Lang)
|
|
|
2076
2098
|
mkdirSync(out, { recursive: true });
|
|
2077
2099
|
const auditPath = join(out, "audit-latest.json");
|
|
2078
2100
|
writeFileSync(auditPath, JSON.stringify(r.audit, null, 2) + "\n");
|
|
2079
|
-
|
|
2101
|
+
// Carry the numbers the fold produced. The failure payload has always described its own
|
|
2102
|
+
// outcome (`issues`); the success one described only the mechanics, so a caller that gates
|
|
2103
|
+
// on the adjudicated result — a CI step, an orchestrator's tool node — had to re-read and
|
|
2104
|
+
// re-parse the file it had just been handed the path to.
|
|
2105
|
+
if (p.flags.json)
|
|
2106
|
+
console.log(
|
|
2107
|
+
JSON.stringify(
|
|
2108
|
+
{
|
|
2109
|
+
ok: true,
|
|
2110
|
+
auditPath,
|
|
2111
|
+
applied: r.applied,
|
|
2112
|
+
stillManual: r.stillManual,
|
|
2113
|
+
conformancePct: r.audit.conformancePct,
|
|
2114
|
+
findings: r.audit.findings.length,
|
|
2115
|
+
grounding: r.grounding,
|
|
2116
|
+
},
|
|
2117
|
+
null,
|
|
2118
|
+
2,
|
|
2119
|
+
),
|
|
2120
|
+
);
|
|
2080
2121
|
else
|
|
2081
2122
|
console.log(
|
|
2082
2123
|
lang === "fr"
|
package/src/gh.ts
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
1
|
+
// The issue set an audit turns into, plus GitHub as one transport for it.
|
|
2
|
+
//
|
|
3
|
+
// The SET is tracker-agnostic — `issueSet()` renders one de-duplicated item per
|
|
4
|
+
// criterion (title, body, labels, severity) and is what `prd --issues-json`
|
|
5
|
+
// emits, so a board that is not GitHub (a native kanban, GitLab, Jira, Linear)
|
|
6
|
+
// files the same items without shelling out to anything. `pushIssues` /
|
|
7
|
+
// `pushSingleIssue` are the GitHub TRANSPORT for that set: they shell out to the
|
|
8
|
+
// `gh` CLI (which handles its own auth) — NO npm dependency, no tokens in
|
|
9
|
+
// ultra11y. Both paths build their titles, bodies and labels through the same
|
|
10
|
+
// three functions, so the exported JSON and the filed GitHub issue can never
|
|
11
|
+
// drift apart.
|
|
12
|
+
//
|
|
13
|
+
// Everything on the transport side is best-effort: if gh is absent or
|
|
14
|
+
// unauthenticated, the caller still wrote the markdown and we just report that
|
|
15
|
+
// issues were skipped. De-dupes by issue title so re-running never creates
|
|
16
|
+
// duplicates.
|
|
6
17
|
import { execFileSync } from "node:child_process";
|
|
7
18
|
import type { Lang, Severity } from "./types.js";
|
|
8
19
|
import type { PrdUnit } from "./prd.js";
|
|
@@ -99,6 +110,43 @@ export function issueBody(unit: PrdUnit, lang: Lang, standard: StandardId = "wca
|
|
|
99
110
|
return lines.join("\n");
|
|
100
111
|
}
|
|
101
112
|
|
|
113
|
+
/** The labels an issue carries: `accessibility`, the active standard's tag, its severity —
|
|
114
|
+
* plus `recommendation` when the unit is advisory, so a good practice is never triaged as a
|
|
115
|
+
* non-conformity. Shared by the GitHub transport and the tracker-agnostic export. */
|
|
116
|
+
export function issueLabels(unit: PrdUnit, standard: StandardId = "wcag"): string[] {
|
|
117
|
+
const { tag } = standardTag(standard);
|
|
118
|
+
return unit.advisory ? ["accessibility", tag, "recommendation", unit.severity] : ["accessibility", tag, unit.severity];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** One issue, rendered for whatever tracker is going to file it. */
|
|
122
|
+
export interface TrackerIssue {
|
|
123
|
+
criteriaId: string;
|
|
124
|
+
/** The de-dupe grain: stable across runs, so re-filing is a no-op on any tracker. */
|
|
125
|
+
title: string;
|
|
126
|
+
body: string;
|
|
127
|
+
labels: string[];
|
|
128
|
+
severity: Severity;
|
|
129
|
+
/** A non-normative good practice, not a non-conformity. Never counts toward conformance. */
|
|
130
|
+
advisory: boolean;
|
|
131
|
+
/** Occurrences behind this issue — a tracker that wants inline anchors reads them here. */
|
|
132
|
+
occurrences: { file: string; line: number; selector: string; message: string }[];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Render the audit's units as a tracker-agnostic issue set — the exact items `pushIssues`
|
|
136
|
+
* would file, minus the transport. This is what `prd --issues-json` emits. */
|
|
137
|
+
export function issueSet(units: PrdUnit[], lang: Lang, standard: StandardId = "wcag", format: IssueFormat = "audit"): TrackerIssue[] {
|
|
138
|
+
const { label } = standardTag(standard);
|
|
139
|
+
return units.map((u) => ({
|
|
140
|
+
criteriaId: u.criteriaId,
|
|
141
|
+
title: issueTitle(u, label),
|
|
142
|
+
body: issueBody(u, lang, standard, format),
|
|
143
|
+
labels: issueLabels(u, standard),
|
|
144
|
+
severity: u.severity,
|
|
145
|
+
advisory: u.advisory === true,
|
|
146
|
+
occurrences: u.findings.map((f) => ({ file: f.file, line: f.line, selector: f.selectorHint, message: resolveMessage(f, lang) })),
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
149
|
+
|
|
102
150
|
export interface CreateResult {
|
|
103
151
|
ok: boolean;
|
|
104
152
|
/** Concise `gh` stderr reason when the creation failed (both attempts). */
|
|
@@ -141,7 +189,7 @@ function recordFailure(result: PushResult, reason?: string): void {
|
|
|
141
189
|
|
|
142
190
|
/** Create a GitHub issue per unit, skipping titles that already exist. */
|
|
143
191
|
export function pushIssues(units: PrdUnit[], lang: Lang, standard: StandardId = "wcag", format: IssueFormat = "audit"): PushResult {
|
|
144
|
-
const { label
|
|
192
|
+
const { label } = standardTag(standard);
|
|
145
193
|
const existing = existingIssueTitles();
|
|
146
194
|
const result: PushResult = { created: 0, skipped: 0, failed: 0, createdTitles: [], errors: [] };
|
|
147
195
|
for (const u of units) {
|
|
@@ -151,9 +199,8 @@ export function pushIssues(units: PrdUnit[], lang: Lang, standard: StandardId =
|
|
|
151
199
|
continue;
|
|
152
200
|
}
|
|
153
201
|
// Advisory units get the `recommendation` label so they can be triaged apart from
|
|
154
|
-
// real non-conformities (which they are not).
|
|
155
|
-
const
|
|
156
|
-
const r = createIssue(title, issueBody(u, lang, standard, format), labels);
|
|
202
|
+
// real non-conformities (which they are not). Shared with the JSON export.
|
|
203
|
+
const r = createIssue(title, issueBody(u, lang, standard, format), issueLabels(u, standard));
|
|
157
204
|
if (r.ok) {
|
|
158
205
|
result.created++;
|
|
159
206
|
result.createdTitles.push(title);
|
package/src/types.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Single source of truth for shared types + the embedded version. sync-version.mjs
|
|
2
2
|
// rewrites VERSION at release time (kept in lockstep with package.json + SKILL.md).
|
|
3
|
-
export const VERSION = "2.
|
|
3
|
+
export const VERSION = "2.32.0";
|
|
4
4
|
export const SCHEMA_VERSION = 2;
|
|
5
5
|
|
|
6
6
|
// Lang = the UI FRAME's language — the `L` localization tables each module (report,
|