ultra11y 5.3.3 → 5.3.4

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.
@@ -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": "5.3.3",
13
+ "version": "5.3.4",
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": "5.3.3",
4
+ "version": "5.3.4",
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": "5.3.3",
4
+ "version": "5.3.4",
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 = "5.3.3";
30
+ const ULTRA11Y_PLUGIN_VERSION = "5.3.4";
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": "5.3.3",
3
+ "version": "5.3.4",
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",
@@ -6,7 +6,7 @@ import { join as join50, relative as relative5, resolve as resolve15, sep as sep
6
6
  import { fileURLToPath as fileURLToPath5, pathToFileURL as pathToFileURL3 } from "url";
7
7
 
8
8
  // src/types.ts
9
- var VERSION = "5.3.3";
9
+ var VERSION = "5.3.4";
10
10
  var SCHEMA_VERSION = 2;
11
11
 
12
12
  // src/audit.ts
@@ -56773,11 +56773,11 @@ function packGuidanceBlock(standard, criterionId, lang) {
56773
56773
  function plainTest(s) {
56774
56774
  return s.replace(/\[([^\]]+)\]\(#[^)]*\)/g, "$1");
56775
56775
  }
56776
- function formatAdjudication(items, lang = "en", standard = CORE2) {
56776
+ function formatAdjudication(items, lang = "en", standard = CORE2, opts = {}) {
56777
56777
  const s = T2[lang];
56778
56778
  const pack = isCore(standard) ? void 0 : loadPack(standard);
56779
- const out2 = [s.title, "", s.intro, "", ...s.verdicts, "", s.rule, "", s.then, ""];
56780
- if (pack) out2.push(`> ${s.packIntro(pack.name)}`, "");
56779
+ const out2 = opts.preamble === false ? [] : [s.title, "", s.intro, "", ...s.verdicts, "", s.rule, "", s.then, ""];
56780
+ if (pack && opts.preamble !== false) out2.push(`> ${s.packIntro(pack.name)}`, "");
56781
56781
  for (const it of items) {
56782
56782
  out2.push(`## ${pack ? `${pack.name} ` : ""}${it.criteriaId}${it.title ? ` \u2014 ${it.title}` : ""} _(${it.automatability})_`);
56783
56783
  out2.push("", `> ${s.evidence} (${it.evidence.length}${it.evidenceTruncated ? ` / ${it.evidenceTruncated.total}` : ""}):`, "");
@@ -56838,7 +56838,31 @@ function writeAdjudication(items, outDir, opts) {
56838
56838
  };
56839
56839
  writeFileSync10(todoPath, JSON.stringify(file, null, 2) + "\n");
56840
56840
  writeFileSync10(mdPath, formatAdjudication(items, opts.lang ?? "en", opts.standard));
56841
- return { todoPath, mdPath, count: items.length };
56841
+ const verdictsPath = join35(outDir, "ADJUDICATE.verdicts.json");
56842
+ writeFileSync10(verdictsPath, JSON.stringify({ ...file, items: slimAdjudicationItems(items) }, null, 2) + "\n");
56843
+ const itemsDir = join35(outDir, "adjudicate");
56844
+ mkdirSync9(itemsDir, { recursive: true });
56845
+ for (const it of items) {
56846
+ writeFileSync10(join35(itemsDir, `${it.criteriaId}.md`), formatAdjudication([it], opts.lang ?? "en", opts.standard, { preamble: false }));
56847
+ }
56848
+ return { todoPath, mdPath, verdictsPath, itemsDir, count: items.length };
56849
+ }
56850
+ function slimAdjudicationItems(items) {
56851
+ return items.map((it) => {
56852
+ const { evidence: _evidence, evidenceTruncated: _truncated, ...rest } = it;
56853
+ return { ...rest, evidence: [] };
56854
+ });
56855
+ }
56856
+ function hydrateAdjudication(adj, audit2, opts = {}) {
56857
+ const needs = adj.items.filter((it) => !it.evidence || it.evidence.length === 0);
56858
+ if (!needs.length) return;
56859
+ const derived = new Map(buildAdjudicationWorklist(audit2, { ...opts, standard: adj.standard }).map((it) => [it.criteriaId, it]));
56860
+ for (const it of needs) {
56861
+ const full = derived.get(it.criteriaId);
56862
+ if (!full) continue;
56863
+ it.evidence = full.evidence;
56864
+ if (full.evidenceTruncated) it.evidenceTruncated = full.evidenceTruncated;
56865
+ }
56842
56866
  }
56843
56867
 
56844
56868
  // src/llm.ts
@@ -66559,6 +66583,7 @@ function applyAdjudicationFile(p, adj, lang) {
66559
66583
  console.error(`ultra11y verify: --in file not found or not valid JSON: ${inFlag}.`);
66560
66584
  return 2;
66561
66585
  }
66586
+ hydrateAdjudication(adj, audit2, { cwd: typeof p.flags.cwd === "string" ? p.flags.cwd : void 0 });
66562
66587
  const r = applyAdjudication(audit2, adj);
66563
66588
  if (!r.ok) {
66564
66589
  if (p.flags.json) console.log(JSON.stringify(r, null, 2));
@@ -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: 5.3.3
8
+ version: 5.3.4
9
9
  ---
10
10
 
11
11
  # review-a11y — accessibility review of changed code
@@ -6,7 +6,7 @@ import { join as join50, relative as relative5, resolve as resolve15, sep as sep
6
6
  import { fileURLToPath as fileURLToPath5, pathToFileURL as pathToFileURL3 } from "url";
7
7
 
8
8
  // src/types.ts
9
- var VERSION = "5.3.3";
9
+ var VERSION = "5.3.4";
10
10
  var SCHEMA_VERSION = 2;
11
11
 
12
12
  // src/audit.ts
@@ -56773,11 +56773,11 @@ function packGuidanceBlock(standard, criterionId, lang) {
56773
56773
  function plainTest(s) {
56774
56774
  return s.replace(/\[([^\]]+)\]\(#[^)]*\)/g, "$1");
56775
56775
  }
56776
- function formatAdjudication(items, lang = "en", standard = CORE2) {
56776
+ function formatAdjudication(items, lang = "en", standard = CORE2, opts = {}) {
56777
56777
  const s = T2[lang];
56778
56778
  const pack = isCore(standard) ? void 0 : loadPack(standard);
56779
- const out2 = [s.title, "", s.intro, "", ...s.verdicts, "", s.rule, "", s.then, ""];
56780
- if (pack) out2.push(`> ${s.packIntro(pack.name)}`, "");
56779
+ const out2 = opts.preamble === false ? [] : [s.title, "", s.intro, "", ...s.verdicts, "", s.rule, "", s.then, ""];
56780
+ if (pack && opts.preamble !== false) out2.push(`> ${s.packIntro(pack.name)}`, "");
56781
56781
  for (const it of items) {
56782
56782
  out2.push(`## ${pack ? `${pack.name} ` : ""}${it.criteriaId}${it.title ? ` \u2014 ${it.title}` : ""} _(${it.automatability})_`);
56783
56783
  out2.push("", `> ${s.evidence} (${it.evidence.length}${it.evidenceTruncated ? ` / ${it.evidenceTruncated.total}` : ""}):`, "");
@@ -56838,7 +56838,31 @@ function writeAdjudication(items, outDir, opts) {
56838
56838
  };
56839
56839
  writeFileSync10(todoPath, JSON.stringify(file, null, 2) + "\n");
56840
56840
  writeFileSync10(mdPath, formatAdjudication(items, opts.lang ?? "en", opts.standard));
56841
- return { todoPath, mdPath, count: items.length };
56841
+ const verdictsPath = join35(outDir, "ADJUDICATE.verdicts.json");
56842
+ writeFileSync10(verdictsPath, JSON.stringify({ ...file, items: slimAdjudicationItems(items) }, null, 2) + "\n");
56843
+ const itemsDir = join35(outDir, "adjudicate");
56844
+ mkdirSync9(itemsDir, { recursive: true });
56845
+ for (const it of items) {
56846
+ writeFileSync10(join35(itemsDir, `${it.criteriaId}.md`), formatAdjudication([it], opts.lang ?? "en", opts.standard, { preamble: false }));
56847
+ }
56848
+ return { todoPath, mdPath, verdictsPath, itemsDir, count: items.length };
56849
+ }
56850
+ function slimAdjudicationItems(items) {
56851
+ return items.map((it) => {
56852
+ const { evidence: _evidence, evidenceTruncated: _truncated, ...rest } = it;
56853
+ return { ...rest, evidence: [] };
56854
+ });
56855
+ }
56856
+ function hydrateAdjudication(adj, audit2, opts = {}) {
56857
+ const needs = adj.items.filter((it) => !it.evidence || it.evidence.length === 0);
56858
+ if (!needs.length) return;
56859
+ const derived = new Map(buildAdjudicationWorklist(audit2, { ...opts, standard: adj.standard }).map((it) => [it.criteriaId, it]));
56860
+ for (const it of needs) {
56861
+ const full = derived.get(it.criteriaId);
56862
+ if (!full) continue;
56863
+ it.evidence = full.evidence;
56864
+ if (full.evidenceTruncated) it.evidenceTruncated = full.evidenceTruncated;
56865
+ }
56842
56866
  }
56843
56867
 
56844
56868
  // src/llm.ts
@@ -66559,6 +66583,7 @@ function applyAdjudicationFile(p, adj, lang) {
66559
66583
  console.error(`ultra11y verify: --in file not found or not valid JSON: ${inFlag}.`);
66560
66584
  return 2;
66561
66585
  }
66586
+ hydrateAdjudication(adj, audit2, { cwd: typeof p.flags.cwd === "string" ? p.flags.cwd : void 0 });
66562
66587
  const r = applyAdjudication(audit2, adj);
66563
66588
  if (!r.ok) {
66564
66589
  if (p.flags.json) console.log(JSON.stringify(r, null, 2));
@@ -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 backlog, or tickets (GitHub/GitLab/Jira); 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: 5.3.3
7
+ version: 5.3.4
8
8
  ---
9
9
 
10
10
  # ultra11y — audit WCAG 2.2 AA and write accessible markup
@@ -71,6 +71,31 @@ test).
71
71
  the **produced** HTML (see `render` / audit the build), otherwise the verdict is a false
72
72
  negative.
73
73
 
74
+ ## When the adjudicator has no shell (CI)
75
+
76
+ `verify --manual` writes the worklist **twice**, plus one brief per criterion:
77
+
78
+ | file | for |
79
+ |---|---|
80
+ | `ADJUDICATE.todo.json` · `ADJUDICATE.md` | a session **with a shell**: evidence inline, edit in place, fold yourself |
81
+ | `ADJUDICATE.verdicts.json` | the **only file to write** when you have no shell — verdicts, no evidence |
82
+ | `adjudicate/<criteriaId>.md` | one small brief per criterion: its evidence, its protocol, its citable tests |
83
+
84
+ Why the split exists, measured rather than assumed: under RGAA the worklist is 96 criteria
85
+ carrying 1590 harvested anchors — **536 KB** of JSON and **466 KB** of Markdown. An agent given
86
+ `Read/Grep/Glob/Edit/Write` cannot work with that: reading either document swamps its context,
87
+ and filling 96 verdicts inside half a megabyte is 96 exact-match edits. A real CI run spent 75
88
+ of 424 turns, hit 17 permission denials trying to run the commands the runbook prescribes, and
89
+ returned the file untouched — so the fail-closed fold discarded all 96 verdicts and every
90
+ criterion stayed « to assess », in a job that reported success. The verdicts file is **37 KB**
91
+ for the same worklist, and each brief is a few KB.
92
+
93
+ `verify --apply` accepts either file. Given the verdicts-only one it **re-derives the evidence
94
+ from the audit** (the worklist is a pure function of the audit) and then runs the identical
95
+ fold: same coverage checks in both directions, same citation matching against *that criterion's*
96
+ own anchors, same refusals. The smaller surface buys nothing from the gate — a `C` that cites
97
+ an anchor it was never shown is refused exactly as before.
98
+
74
99
  ## Adversarial verification of the non-conformities — `verify --report`
75
100
 
76
101
  Unchanged: `node scripts/ultra11y.mjs verify --report audits/wcag-YYYY-MM-DD.md` writes `VERIFY.md`
@@ -6,7 +6,7 @@ import { join as join50, relative as relative5, resolve as resolve15, sep as sep
6
6
  import { fileURLToPath as fileURLToPath5, pathToFileURL as pathToFileURL3 } from "url";
7
7
 
8
8
  // src/types.ts
9
- var VERSION = "5.3.3";
9
+ var VERSION = "5.3.4";
10
10
  var SCHEMA_VERSION = 2;
11
11
 
12
12
  // src/audit.ts
@@ -56773,11 +56773,11 @@ function packGuidanceBlock(standard, criterionId, lang) {
56773
56773
  function plainTest(s) {
56774
56774
  return s.replace(/\[([^\]]+)\]\(#[^)]*\)/g, "$1");
56775
56775
  }
56776
- function formatAdjudication(items, lang = "en", standard = CORE2) {
56776
+ function formatAdjudication(items, lang = "en", standard = CORE2, opts = {}) {
56777
56777
  const s = T2[lang];
56778
56778
  const pack = isCore(standard) ? void 0 : loadPack(standard);
56779
- const out2 = [s.title, "", s.intro, "", ...s.verdicts, "", s.rule, "", s.then, ""];
56780
- if (pack) out2.push(`> ${s.packIntro(pack.name)}`, "");
56779
+ const out2 = opts.preamble === false ? [] : [s.title, "", s.intro, "", ...s.verdicts, "", s.rule, "", s.then, ""];
56780
+ if (pack && opts.preamble !== false) out2.push(`> ${s.packIntro(pack.name)}`, "");
56781
56781
  for (const it of items) {
56782
56782
  out2.push(`## ${pack ? `${pack.name} ` : ""}${it.criteriaId}${it.title ? ` \u2014 ${it.title}` : ""} _(${it.automatability})_`);
56783
56783
  out2.push("", `> ${s.evidence} (${it.evidence.length}${it.evidenceTruncated ? ` / ${it.evidenceTruncated.total}` : ""}):`, "");
@@ -56838,7 +56838,31 @@ function writeAdjudication(items, outDir, opts) {
56838
56838
  };
56839
56839
  writeFileSync10(todoPath, JSON.stringify(file, null, 2) + "\n");
56840
56840
  writeFileSync10(mdPath, formatAdjudication(items, opts.lang ?? "en", opts.standard));
56841
- return { todoPath, mdPath, count: items.length };
56841
+ const verdictsPath = join35(outDir, "ADJUDICATE.verdicts.json");
56842
+ writeFileSync10(verdictsPath, JSON.stringify({ ...file, items: slimAdjudicationItems(items) }, null, 2) + "\n");
56843
+ const itemsDir = join35(outDir, "adjudicate");
56844
+ mkdirSync9(itemsDir, { recursive: true });
56845
+ for (const it of items) {
56846
+ writeFileSync10(join35(itemsDir, `${it.criteriaId}.md`), formatAdjudication([it], opts.lang ?? "en", opts.standard, { preamble: false }));
56847
+ }
56848
+ return { todoPath, mdPath, verdictsPath, itemsDir, count: items.length };
56849
+ }
56850
+ function slimAdjudicationItems(items) {
56851
+ return items.map((it) => {
56852
+ const { evidence: _evidence, evidenceTruncated: _truncated, ...rest } = it;
56853
+ return { ...rest, evidence: [] };
56854
+ });
56855
+ }
56856
+ function hydrateAdjudication(adj, audit2, opts = {}) {
56857
+ const needs = adj.items.filter((it) => !it.evidence || it.evidence.length === 0);
56858
+ if (!needs.length) return;
56859
+ const derived = new Map(buildAdjudicationWorklist(audit2, { ...opts, standard: adj.standard }).map((it) => [it.criteriaId, it]));
56860
+ for (const it of needs) {
56861
+ const full = derived.get(it.criteriaId);
56862
+ if (!full) continue;
56863
+ it.evidence = full.evidence;
56864
+ if (full.evidenceTruncated) it.evidenceTruncated = full.evidenceTruncated;
56865
+ }
56842
56866
  }
56843
56867
 
56844
56868
  // src/llm.ts
@@ -66559,6 +66583,7 @@ function applyAdjudicationFile(p, adj, lang) {
66559
66583
  console.error(`ultra11y verify: --in file not found or not valid JSON: ${inFlag}.`);
66560
66584
  return 2;
66561
66585
  }
66586
+ hydrateAdjudication(adj, audit2, { cwd: typeof p.flags.cwd === "string" ? p.flags.cwd : void 0 });
66562
66587
  const r = applyAdjudication(audit2, adj);
66563
66588
  if (!r.ok) {
66564
66589
  if (p.flags.json) console.log(JSON.stringify(r, null, 2));
package/src/adjudicate.ts CHANGED
@@ -909,11 +909,18 @@ function plainTest(s: string): string {
909
909
  return s.replace(/\[([^\]]+)\]\(#[^)]*\)/g, "$1");
910
910
  }
911
911
 
912
- export function formatAdjudication(items: AdjudicationItem[], lang: Lang = "en", standard: StandardId = CORE): string {
912
+ /** `preamble: false` renders the criterion sections ALONE.
913
+ *
914
+ * The preamble names the file to write into and the command to fold with, which is right for
915
+ * the one combined document and wrong for a per-criterion brief: repeated 96 times it is 96
916
+ * copies of instructions that contradict a CI adjudicator's toolset (it has no shell) and
917
+ * point at the wrong file. The briefs carry evidence and protocol; the instructions live once,
918
+ * in the worklist's `contract` field and the dispatch contract. */
919
+ export function formatAdjudication(items: AdjudicationItem[], lang: Lang = "en", standard: StandardId = CORE, opts: { preamble?: boolean } = {}): string {
913
920
  const s = T[lang];
914
921
  const pack = isCore(standard) ? undefined : loadPack(standard);
915
- const out: string[] = [s.title, "", s.intro, "", ...s.verdicts, "", s.rule, "", s.then, ""];
916
- if (pack) out.push(`> ${s.packIntro(pack.name)}`, "");
922
+ const out: string[] = opts.preamble === false ? [] : [s.title, "", s.intro, "", ...s.verdicts, "", s.rule, "", s.then, ""];
923
+ if (pack && opts.preamble !== false) out.push(`> ${s.packIntro(pack.name)}`, "");
917
924
  for (const it of items) {
918
925
  out.push(`## ${pack ? `${pack.name} ` : ""}${it.criteriaId}${it.title ? ` — ${it.title}` : ""} _(${it.automatability})_`);
919
926
  out.push("", `> ${s.evidence} (${it.evidence.length}${it.evidenceTruncated ? ` / ${it.evidenceTruncated.total}` : ""}):`, "");
@@ -970,6 +977,10 @@ export function formatAdjudication(items: AdjudicationItem[], lang: Lang = "en",
970
977
  export interface WriteAdjudicationResult {
971
978
  todoPath: string;
972
979
  mdPath: string;
980
+ /** The verdicts-only file, for an adjudicator that cannot edit half a megabyte of JSON. */
981
+ verdictsPath: string;
982
+ /** One small Markdown brief per criterion, for one that cannot read it either. */
983
+ itemsDir: string;
973
984
  count: number;
974
985
  }
975
986
 
@@ -992,5 +1003,60 @@ export function writeAdjudication(
992
1003
  };
993
1004
  writeFileSync(todoPath, JSON.stringify(file, null, 2) + "\n");
994
1005
  writeFileSync(mdPath, formatAdjudication(items, opts.lang ?? "en", opts.standard));
995
- return { todoPath, mdPath, count: items.length };
1006
+
1007
+ // THE SPLIT SURFACE, for an adjudicator that cannot shell out.
1008
+ //
1009
+ // The two documents above are sized for a session with a shell: 96 RGAA criteria carrying
1010
+ // 30 evidence anchors each come to ~540 KB of JSON and ~470 KB of Markdown, and the runbook
1011
+ // tells the reader to edit the JSON in place and then run the engine to fold it. That is
1012
+ // fine locally. In CI the adjudicator is handed Read/Grep/Glob/Edit/Write and nothing else,
1013
+ // and against those tools the same instructions are impossible: reading either document
1014
+ // swamps the context, editing 96 verdicts inside half a megabyte of JSON is 96 exact-match
1015
+ // edits, and every prescribed `node …` command is denied. Measured on a real run: 17
1016
+ // permission denials, 75 of 424 turns used, and a todo file that came back untouched — so
1017
+ // the fail-closed fold correctly discarded all 96 and every criterion stayed "to assess".
1018
+ //
1019
+ // So the same worklist is also written as one small file to WRITE and one small file per
1020
+ // criterion to READ. Additive on purpose: the two documents above keep their exact shape and
1021
+ // every existing caller, gate and test is untouched.
1022
+ const verdictsPath = join(outDir, "ADJUDICATE.verdicts.json");
1023
+ writeFileSync(verdictsPath, JSON.stringify({ ...file, items: slimAdjudicationItems(items) }, null, 2) + "\n");
1024
+ const itemsDir = join(outDir, "adjudicate");
1025
+ mkdirSync(itemsDir, { recursive: true });
1026
+ for (const it of items) {
1027
+ writeFileSync(join(itemsDir, `${it.criteriaId}.md`), formatAdjudication([it], opts.lang ?? "en", opts.standard, { preamble: false }));
1028
+ }
1029
+ return { todoPath, mdPath, verdictsPath, itemsDir, count: items.length };
1030
+ }
1031
+
1032
+ /** The worklist with the evidence stripped: what an adjudicator has to WRITE, without the
1033
+ * bulk it only has to READ. Every field the fold requires — verdict, justification, reason,
1034
+ * findings, citations, recommendations — is kept, so a filled slim file carries a complete
1035
+ * decision. The evidence comes back at fold time via `hydrateAdjudication`. */
1036
+ export function slimAdjudicationItems(items: AdjudicationItem[]): AdjudicationItem[] {
1037
+ return items.map((it) => {
1038
+ const { evidence: _evidence, evidenceTruncated: _truncated, ...rest } = it;
1039
+ return { ...rest, evidence: [] } as AdjudicationItem;
1040
+ });
1041
+ }
1042
+
1043
+ /** Put the evidence back on a slim adjudication, re-derived from the audit it was built from.
1044
+ *
1045
+ * Safe because the worklist is a pure function of the audit: same audit, same harvesters, same
1046
+ * anchors. This is what lets `verify --apply` accept a verdicts-only file WITHOUT relaxing a
1047
+ * single check — the citation gate still matches each cited anchor against this criterion's
1048
+ * own harvested evidence, exactly as it does for an inline worklist.
1049
+ *
1050
+ * An item the re-derivation does not know is left alone: it is a surplus verdict, and the
1051
+ * coverage check in `applyAdjudication` is what must reject it. */
1052
+ export function hydrateAdjudication(adj: AdjudicationFile, audit: AuditResult, opts: { cwd?: string } = {}): void {
1053
+ const needs = adj.items.filter((it) => !it.evidence || it.evidence.length === 0);
1054
+ if (!needs.length) return;
1055
+ const derived = new Map(buildAdjudicationWorklist(audit, { ...opts, standard: adj.standard }).map((it) => [it.criteriaId, it]));
1056
+ for (const it of needs) {
1057
+ const full = derived.get(it.criteriaId);
1058
+ if (!full) continue;
1059
+ it.evidence = full.evidence;
1060
+ if (full.evidenceTruncated) it.evidenceTruncated = full.evidenceTruncated;
1061
+ }
996
1062
  }
package/src/cli.ts CHANGED
@@ -42,7 +42,14 @@ import { runCriteria, renderCriteriaReference } from "./criteria.js";
42
42
  import { checkReport, checkSemantic } from "./check.js";
43
43
  import { buildWorklist, writeWorklist, applyVerdicts, VERIFY_MAX, type VerifyItem } from "./verify.js";
44
44
  import { groundItems } from "./grounding.js";
45
- import { buildAdjudicationWorklist, formatAdjudication, writeAdjudication, applyAdjudication, type AdjudicationFile } from "./adjudicate.js";
45
+ import {
46
+ buildAdjudicationWorklist,
47
+ formatAdjudication,
48
+ writeAdjudication,
49
+ applyAdjudication,
50
+ hydrateAdjudication,
51
+ type AdjudicationFile,
52
+ } from "./adjudicate.js";
46
53
  import { BATCH_SIZE, apiKeyFromEnv, applyRawVerdicts, judgeAll, modelFromEnv } from "./llm.js";
47
54
  import { runScan, runScanMany, runCrawlScan, runSampleScan, mergeDynamic, mergeSnapshotAudit, cleanDynamic, dockerAvailable } from "./scan.js";
48
55
  import { runScanLocal, runScanManyLocal, runCrawlScanLocal, runSampleScanLocal, localAvailable } from "./scan-local.js";
@@ -2637,6 +2644,11 @@ function applyAdjudicationFile(p: ParsedArgs, adj: AdjudicationFile, lang: Lang)
2637
2644
  console.error(`ultra11y verify: --in file not found or not valid JSON: ${inFlag}.`);
2638
2645
  return 2;
2639
2646
  }
2647
+ // A verdicts-only file (`ADJUDICATE.verdicts.json`) carries decisions without the evidence
2648
+ // they were made against. Put it back from the audit before folding: the worklist is a pure
2649
+ // function of the audit, so this reconstructs the very anchors the citation gate checks
2650
+ // against — the fold below is the same fold, with the same refusals.
2651
+ hydrateAdjudication(adj, audit, { cwd: typeof p.flags.cwd === "string" ? (p.flags.cwd as string) : undefined });
2640
2652
  const r = applyAdjudication(audit, adj);
2641
2653
  if (!r.ok) {
2642
2654
  if (p.flags.json) console.log(JSON.stringify(r, null, 2));
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 = "5.3.3";
3
+ export const VERSION = "5.3.4";
4
4
  export const SCHEMA_VERSION = 2;
5
5
 
6
6
  // Lang = the UI FRAME's language — the `L` localization tables each module (report,