ultra11y 5.40.1 → 5.40.2

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 before commit, push and pull request, plus detailed evidenced Markdown, HTML and per-page WCAG 2.2 AA / RGAA reports in Claude Code.",
13
- "version": "5.40.1",
13
+ "version": "5.40.2",
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…): automatic review before commit, push and pull request, plus detailed evidenced Markdown, HTML and per-page conformance reports in interactive agent sessions.",
4
- "version": "5.40.1",
4
+ "version": "5.40.2",
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…): automatic review before commit, push and pull request, plus detailed evidenced Markdown, HTML and per-page conformance reports in interactive agent sessions.",
4
- "version": "5.40.1",
4
+ "version": "5.40.2",
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.40.1";
30
+ const ULTRA11Y_PLUGIN_VERSION = "5.40.2";
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.40.1",
3
+ "version": "5.40.2",
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 join53, relative as relative6, resolve as resolve16, 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.40.1";
9
+ var VERSION = "5.40.2";
10
10
  var SCHEMA_VERSION = 2;
11
11
  var INAPPLICABLE_STATUS = "C";
12
12
 
@@ -2300,7 +2300,7 @@ var wcag_universe_default = {
2300
2300
  wcagVersion: "2.2",
2301
2301
  source: "https://www.w3.org/TR/WCAG22/",
2302
2302
  criteriaSource: "https://github.com/w3c/wcag",
2303
- provenance: "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-08-30 via `node scripts/build-standards.mjs --refresh-universe`. Classification: core-AA = ships in src/data/wcag.json (the shipped WCAG 2.2 AA core); out-of-core = WCAG AAA; removed = obsolete (4.1.1).",
2303
+ provenance: "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-08-31 via `node scripts/build-standards.mjs --refresh-universe`. Classification: core-AA = ships in src/data/wcag.json (the shipped WCAG 2.2 AA core); out-of-core = WCAG AAA; removed = obsolete (4.1.1).",
2304
2304
  criteria: [
2305
2305
  {
2306
2306
  id: "1.1.1",
@@ -56992,7 +56992,7 @@ function judgmentGuard(r, pc) {
56992
56992
  return { ...r, status: "manual", judgment: true };
56993
56993
  }
56994
56994
  function measuredRescue(r, pc, cov, ran, pageId) {
56995
- if (r.status !== "manual" || pc.judgment || r.outOfScope || pc.automation?.completeBySilence !== true) return r;
56995
+ if (r.status !== "manual" || pc.judgment || r.outOfScope || pc.automation && pc.automation.completeBySilence !== true) return r;
56996
56996
  if (!criterionMeasuredOn(pc.appliesTo?.ruleIds, pc.wcag, cov, ran)) return r;
56997
56997
  const { scopedOut: _scopedOut, judgment: _judgment, ...rest } = r;
56998
56998
  return { ...rest, status: "C", decidedBy: "scan", justification: measuredReason(pc, pageId) };
@@ -63329,6 +63329,12 @@ function apiKeyFromEnv() {
63329
63329
  function modelFromEnv() {
63330
63330
  return process.env.ULTRA11Y_LLM_MODEL?.trim() || DEFAULT_MODEL;
63331
63331
  }
63332
+ var BudgetExceededError = class extends Error {
63333
+ constructor(message) {
63334
+ super(message);
63335
+ this.name = "BudgetExceededError";
63336
+ }
63337
+ };
63332
63338
  function isProviderUnavailableError(error) {
63333
63339
  const message = error instanceof Error ? error.message : String(error);
63334
63340
  return /(?:api status|http)\s*(?:429|5\d\d)|\b429\b.*rate.?limit|rate.?limit|overload|service unavailable/i.test(message);
@@ -63481,6 +63487,7 @@ async function judgeAll(batches, opts) {
63481
63487
  const verdicts = [];
63482
63488
  const failures = [];
63483
63489
  let done = 0;
63490
+ let total = batches.length;
63484
63491
  const queue = [...batches];
63485
63492
  const backend = opts.backend ?? judgeBatch;
63486
63493
  const lanes = Math.max(1, opts.concurrency ?? CONCURRENCY);
@@ -63498,10 +63505,18 @@ async function judgeAll(batches, opts) {
63498
63505
  verdicts.push(...checked.accepted);
63499
63506
  if (checked.accepted.length) opts.onVerdicts?.(checked.accepted);
63500
63507
  } catch (e) {
63508
+ if (e instanceof BudgetExceededError && b.items.length > 1 && opts.render) {
63509
+ const half = Math.ceil(b.items.length / 2);
63510
+ const parts2 = [b.items.slice(0, half), b.items.slice(half)];
63511
+ queue.unshift(...parts2.map((items) => ({ items, prompt: opts.render(items) })));
63512
+ total += parts2.length - 1;
63513
+ opts.onProgress?.(done, total);
63514
+ continue;
63515
+ }
63501
63516
  failures.push(e instanceof Error ? e.message : String(e));
63502
63517
  if (opts.abortOnError?.(e)) aborted = true;
63503
63518
  }
63504
- opts.onProgress?.(++done, batches.length);
63519
+ opts.onProgress?.(++done, total);
63505
63520
  }
63506
63521
  })
63507
63522
  );
@@ -63707,6 +63722,14 @@ async function runCli2(argv, prompt, opts, extract) {
63707
63722
  }
63708
63723
  if (env.is_error || env.subtype !== "success") {
63709
63724
  lastError = `the CLI reported an error (${env.subtype ?? "unknown"}${env.api_error_status ? `, api status ${env.api_error_status}` : ""}).`;
63725
+ if (env.subtype === "error_max_budget_usd") {
63726
+ try {
63727
+ const salvaged = extract(env);
63728
+ if (Array.isArray(salvaged) ? salvaged.length > 0 : salvaged !== void 0) return salvaged;
63729
+ } catch {
63730
+ }
63731
+ throw new BudgetExceededError(`ultra11y judge: ${lastError}`);
63732
+ }
63710
63733
  if (env.api_error_status && (env.api_error_status === 429 || env.api_error_status >= 500)) continue;
63711
63734
  break;
63712
63735
  }
@@ -74910,6 +74933,9 @@ async function cmdJudge(p) {
74910
74933
  // runner with room.
74911
74934
  concurrency: runner === "claude" || runner === "codex" ? cliConcurrency : void 0,
74912
74935
  abortOnError: isProviderUnavailableError,
74936
+ // The seam that lets a budget abort be halved rather than lost: the same renderer
74937
+ // `batchWorklist` used, so a re-queued half is prompted exactly like a first-class batch.
74938
+ render: render2,
74913
74939
  maxBudgetUsd: Number.isFinite(maxBudgetUsd) ? maxBudgetUsd : void 0,
74914
74940
  effort,
74915
74941
  timeoutMs: Number.isFinite(timeoutMs) ? timeoutMs : void 0,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "source": "https://www.w3.org/Translations/WCAG22-fr/",
3
- "fetchedAt": "2026-08-30",
3
+ "fetchedAt": "2026-08-31",
4
4
  "principles": {
5
5
  "1": "Perceptible",
6
6
  "2": "Utilisable",
@@ -2,7 +2,7 @@
2
2
  "wcagVersion": "2.2",
3
3
  "source": "https://www.w3.org/TR/WCAG22/",
4
4
  "criteriaSource": "https://github.com/w3c/wcag",
5
- "provenance": "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-08-30 via `node scripts/build-standards.mjs --refresh-universe`. Classification: core-AA = ships in src/data/wcag.json (the shipped WCAG 2.2 AA core); out-of-core = WCAG AAA; removed = obsolete (4.1.1).",
5
+ "provenance": "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-08-31 via `node scripts/build-standards.mjs --refresh-universe`. Classification: core-AA = ships in src/data/wcag.json (the shipped WCAG 2.2 AA core); out-of-core = WCAG AAA; removed = obsolete (4.1.1).",
6
6
  "principles": [
7
7
  {
8
8
  "number": 1,
@@ -2,7 +2,7 @@
2
2
  "source": "https://github.com/w3c/wcag",
3
3
  "understanding": "https://www.w3.org/WAI/WCAG22/Understanding/",
4
4
  "license": "W3C Document License (code samples: W3C Software License)",
5
- "fetchedAt": "2026-08-30",
5
+ "fetchedAt": "2026-08-31",
6
6
  "criteria": {
7
7
  "1.1.1": {
8
8
  "slug": "non-text-content",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "source": "https://github.com/w3c/wcag",
3
3
  "license": "W3C Document License",
4
- "fetchedAt": "2026-08-30",
4
+ "fetchedAt": "2026-08-31",
5
5
  "criteria": {
6
6
  "1.1.1": "All non-text content that is presented to the user has a text alternative that serves the equivalent purpose, except for the situations listed below.\nControls, Input:\nIf non-text content is a control or accepts user input, then it has a name that describes its purpose. (Refer to Success Criterion 4.1.2 Name, Role, Value for additional requirements for controls and content that accepts user input.)\nTime-Based Media:\nIf non-text content is time-based media, then text alternatives at least provide descriptive identification of the non-text content. (Refer to Guideline 1.2 for additional requirements for media.)\nTest:\nIf non-text content is a test or exercise that would be invalid if presented in text, then text alternatives at least provide descriptive identification of the non-text content.\nSensory:\nIf non-text content is primarily intended to create a specific sensory experience, then text alternatives at least provide descriptive identification of the non-text content.\nCAPTCHA:\nIf the purpose of non-text content is to confirm that content is being accessed by a person rather than a computer, then text alternatives that identify and describe the purpose of the non-text content are provided, and alternative forms of CAPTCHA using output modes for different types of sensory perception are provided to accommodate different disabilities.\nDecoration, Formatting, Invisible:\nIf non-text content is pure decoration, is used only for visual formatting, or is not presented to users, then it is implemented in a way that it can be ignored by assistive technology.",
7
7
  "1.2.1": "For prerecorded audio-only and prerecorded video-only media, the following are true, except when the audio or video is a media alternative for text and is clearly labeled as such:\nPrerecorded Audio-only:\nAn alternative for time-based media is provided that presents equivalent information for prerecorded audio-only content.\nPrerecorded Video-only:\nEither an alternative for time-based media or an audio track is provided that presents equivalent information for prerecorded video-only content.",
@@ -3,7 +3,7 @@ name: review-a11y
3
3
  description: "Use to REVIEW changed frontend code for accessibility: staged files, a working diff, branch, or PR; prompts such as 'review a11y', 'is this accessible?', or 'anything to fix before merge?'; and hook or CI findings on a change. Run the bundled install-free engine with cross-file JSX/TSX analysis, then adjudicate candidates and return a severity-ranked WCAG 2.2 AA review with file:line fixes, residual rendering risks, and a verdict. Use `ultra11y` instead for whole-repo audits, dated conformance reports, country standards, PRD backlogs, or authoring accessible markup."
4
4
  license: MIT
5
5
  metadata:
6
- version: 5.40.1
6
+ version: 5.40.2
7
7
  ---
8
8
 
9
9
  # review-a11y — accessibility review of changed code
@@ -6,7 +6,7 @@ import { join as join53, relative as relative6, resolve as resolve16, 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.40.1";
9
+ var VERSION = "5.40.2";
10
10
  var SCHEMA_VERSION = 2;
11
11
  var INAPPLICABLE_STATUS = "C";
12
12
 
@@ -2300,7 +2300,7 @@ var wcag_universe_default = {
2300
2300
  wcagVersion: "2.2",
2301
2301
  source: "https://www.w3.org/TR/WCAG22/",
2302
2302
  criteriaSource: "https://github.com/w3c/wcag",
2303
- provenance: "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-08-30 via `node scripts/build-standards.mjs --refresh-universe`. Classification: core-AA = ships in src/data/wcag.json (the shipped WCAG 2.2 AA core); out-of-core = WCAG AAA; removed = obsolete (4.1.1).",
2303
+ provenance: "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-08-31 via `node scripts/build-standards.mjs --refresh-universe`. Classification: core-AA = ships in src/data/wcag.json (the shipped WCAG 2.2 AA core); out-of-core = WCAG AAA; removed = obsolete (4.1.1).",
2304
2304
  criteria: [
2305
2305
  {
2306
2306
  id: "1.1.1",
@@ -56992,7 +56992,7 @@ function judgmentGuard(r, pc) {
56992
56992
  return { ...r, status: "manual", judgment: true };
56993
56993
  }
56994
56994
  function measuredRescue(r, pc, cov, ran, pageId) {
56995
- if (r.status !== "manual" || pc.judgment || r.outOfScope || pc.automation?.completeBySilence !== true) return r;
56995
+ if (r.status !== "manual" || pc.judgment || r.outOfScope || pc.automation && pc.automation.completeBySilence !== true) return r;
56996
56996
  if (!criterionMeasuredOn(pc.appliesTo?.ruleIds, pc.wcag, cov, ran)) return r;
56997
56997
  const { scopedOut: _scopedOut, judgment: _judgment, ...rest } = r;
56998
56998
  return { ...rest, status: "C", decidedBy: "scan", justification: measuredReason(pc, pageId) };
@@ -63329,6 +63329,12 @@ function apiKeyFromEnv() {
63329
63329
  function modelFromEnv() {
63330
63330
  return process.env.ULTRA11Y_LLM_MODEL?.trim() || DEFAULT_MODEL;
63331
63331
  }
63332
+ var BudgetExceededError = class extends Error {
63333
+ constructor(message) {
63334
+ super(message);
63335
+ this.name = "BudgetExceededError";
63336
+ }
63337
+ };
63332
63338
  function isProviderUnavailableError(error) {
63333
63339
  const message = error instanceof Error ? error.message : String(error);
63334
63340
  return /(?:api status|http)\s*(?:429|5\d\d)|\b429\b.*rate.?limit|rate.?limit|overload|service unavailable/i.test(message);
@@ -63481,6 +63487,7 @@ async function judgeAll(batches, opts) {
63481
63487
  const verdicts = [];
63482
63488
  const failures = [];
63483
63489
  let done = 0;
63490
+ let total = batches.length;
63484
63491
  const queue = [...batches];
63485
63492
  const backend = opts.backend ?? judgeBatch;
63486
63493
  const lanes = Math.max(1, opts.concurrency ?? CONCURRENCY);
@@ -63498,10 +63505,18 @@ async function judgeAll(batches, opts) {
63498
63505
  verdicts.push(...checked.accepted);
63499
63506
  if (checked.accepted.length) opts.onVerdicts?.(checked.accepted);
63500
63507
  } catch (e) {
63508
+ if (e instanceof BudgetExceededError && b.items.length > 1 && opts.render) {
63509
+ const half = Math.ceil(b.items.length / 2);
63510
+ const parts2 = [b.items.slice(0, half), b.items.slice(half)];
63511
+ queue.unshift(...parts2.map((items) => ({ items, prompt: opts.render(items) })));
63512
+ total += parts2.length - 1;
63513
+ opts.onProgress?.(done, total);
63514
+ continue;
63515
+ }
63501
63516
  failures.push(e instanceof Error ? e.message : String(e));
63502
63517
  if (opts.abortOnError?.(e)) aborted = true;
63503
63518
  }
63504
- opts.onProgress?.(++done, batches.length);
63519
+ opts.onProgress?.(++done, total);
63505
63520
  }
63506
63521
  })
63507
63522
  );
@@ -63707,6 +63722,14 @@ async function runCli2(argv, prompt, opts, extract) {
63707
63722
  }
63708
63723
  if (env.is_error || env.subtype !== "success") {
63709
63724
  lastError = `the CLI reported an error (${env.subtype ?? "unknown"}${env.api_error_status ? `, api status ${env.api_error_status}` : ""}).`;
63725
+ if (env.subtype === "error_max_budget_usd") {
63726
+ try {
63727
+ const salvaged = extract(env);
63728
+ if (Array.isArray(salvaged) ? salvaged.length > 0 : salvaged !== void 0) return salvaged;
63729
+ } catch {
63730
+ }
63731
+ throw new BudgetExceededError(`ultra11y judge: ${lastError}`);
63732
+ }
63710
63733
  if (env.api_error_status && (env.api_error_status === 429 || env.api_error_status >= 500)) continue;
63711
63734
  break;
63712
63735
  }
@@ -74910,6 +74933,9 @@ async function cmdJudge(p) {
74910
74933
  // runner with room.
74911
74934
  concurrency: runner === "claude" || runner === "codex" ? cliConcurrency : void 0,
74912
74935
  abortOnError: isProviderUnavailableError,
74936
+ // The seam that lets a budget abort be halved rather than lost: the same renderer
74937
+ // `batchWorklist` used, so a re-queued half is prompted exactly like a first-class batch.
74938
+ render: render2,
74913
74939
  maxBudgetUsd: Number.isFinite(maxBudgetUsd) ? maxBudgetUsd : void 0,
74914
74940
  effort,
74915
74941
  timeoutMs: Number.isFinite(timeoutMs) ? timeoutMs : void 0,
@@ -3,7 +3,7 @@ name: ultra11y
3
3
  description: "Use to AUDIT a repository, site, or page against WCAG 2.2 AA or a country standard such as RGAA; produce a dated conformance report, criterion grid, PRD backlog, or tickets; AUTHOR accessible markup; fix accessibility; or wire a repo gate and rendered-DOM capture pipeline. The bundled install-free engine runs 93 static checks tied to success criteria, while the agent adjudicates judgment criteria and routes rendering criteria to browser scans; check/verify reject invented non-conformities. Use `review-a11y` instead for a review scoped only to a diff, branch, or PR."
4
4
  license: MIT
5
5
  metadata:
6
- version: 5.40.1
6
+ version: 5.40.2
7
7
  ---
8
8
 
9
9
  # ultra11y — audit WCAG 2.2 AA and write accessible markup
@@ -6,7 +6,7 @@ import { join as join53, relative as relative6, resolve as resolve16, 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.40.1";
9
+ var VERSION = "5.40.2";
10
10
  var SCHEMA_VERSION = 2;
11
11
  var INAPPLICABLE_STATUS = "C";
12
12
 
@@ -2300,7 +2300,7 @@ var wcag_universe_default = {
2300
2300
  wcagVersion: "2.2",
2301
2301
  source: "https://www.w3.org/TR/WCAG22/",
2302
2302
  criteriaSource: "https://github.com/w3c/wcag",
2303
- provenance: "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-08-30 via `node scripts/build-standards.mjs --refresh-universe`. Classification: core-AA = ships in src/data/wcag.json (the shipped WCAG 2.2 AA core); out-of-core = WCAG AAA; removed = obsolete (4.1.1).",
2303
+ provenance: "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-08-31 via `node scripts/build-standards.mjs --refresh-universe`. Classification: core-AA = ships in src/data/wcag.json (the shipped WCAG 2.2 AA core); out-of-core = WCAG AAA; removed = obsolete (4.1.1).",
2304
2304
  criteria: [
2305
2305
  {
2306
2306
  id: "1.1.1",
@@ -56992,7 +56992,7 @@ function judgmentGuard(r, pc) {
56992
56992
  return { ...r, status: "manual", judgment: true };
56993
56993
  }
56994
56994
  function measuredRescue(r, pc, cov, ran, pageId) {
56995
- if (r.status !== "manual" || pc.judgment || r.outOfScope || pc.automation?.completeBySilence !== true) return r;
56995
+ if (r.status !== "manual" || pc.judgment || r.outOfScope || pc.automation && pc.automation.completeBySilence !== true) return r;
56996
56996
  if (!criterionMeasuredOn(pc.appliesTo?.ruleIds, pc.wcag, cov, ran)) return r;
56997
56997
  const { scopedOut: _scopedOut, judgment: _judgment, ...rest } = r;
56998
56998
  return { ...rest, status: "C", decidedBy: "scan", justification: measuredReason(pc, pageId) };
@@ -63329,6 +63329,12 @@ function apiKeyFromEnv() {
63329
63329
  function modelFromEnv() {
63330
63330
  return process.env.ULTRA11Y_LLM_MODEL?.trim() || DEFAULT_MODEL;
63331
63331
  }
63332
+ var BudgetExceededError = class extends Error {
63333
+ constructor(message) {
63334
+ super(message);
63335
+ this.name = "BudgetExceededError";
63336
+ }
63337
+ };
63332
63338
  function isProviderUnavailableError(error) {
63333
63339
  const message = error instanceof Error ? error.message : String(error);
63334
63340
  return /(?:api status|http)\s*(?:429|5\d\d)|\b429\b.*rate.?limit|rate.?limit|overload|service unavailable/i.test(message);
@@ -63481,6 +63487,7 @@ async function judgeAll(batches, opts) {
63481
63487
  const verdicts = [];
63482
63488
  const failures = [];
63483
63489
  let done = 0;
63490
+ let total = batches.length;
63484
63491
  const queue = [...batches];
63485
63492
  const backend = opts.backend ?? judgeBatch;
63486
63493
  const lanes = Math.max(1, opts.concurrency ?? CONCURRENCY);
@@ -63498,10 +63505,18 @@ async function judgeAll(batches, opts) {
63498
63505
  verdicts.push(...checked.accepted);
63499
63506
  if (checked.accepted.length) opts.onVerdicts?.(checked.accepted);
63500
63507
  } catch (e) {
63508
+ if (e instanceof BudgetExceededError && b.items.length > 1 && opts.render) {
63509
+ const half = Math.ceil(b.items.length / 2);
63510
+ const parts2 = [b.items.slice(0, half), b.items.slice(half)];
63511
+ queue.unshift(...parts2.map((items) => ({ items, prompt: opts.render(items) })));
63512
+ total += parts2.length - 1;
63513
+ opts.onProgress?.(done, total);
63514
+ continue;
63515
+ }
63501
63516
  failures.push(e instanceof Error ? e.message : String(e));
63502
63517
  if (opts.abortOnError?.(e)) aborted = true;
63503
63518
  }
63504
- opts.onProgress?.(++done, batches.length);
63519
+ opts.onProgress?.(++done, total);
63505
63520
  }
63506
63521
  })
63507
63522
  );
@@ -63707,6 +63722,14 @@ async function runCli2(argv, prompt, opts, extract) {
63707
63722
  }
63708
63723
  if (env.is_error || env.subtype !== "success") {
63709
63724
  lastError = `the CLI reported an error (${env.subtype ?? "unknown"}${env.api_error_status ? `, api status ${env.api_error_status}` : ""}).`;
63725
+ if (env.subtype === "error_max_budget_usd") {
63726
+ try {
63727
+ const salvaged = extract(env);
63728
+ if (Array.isArray(salvaged) ? salvaged.length > 0 : salvaged !== void 0) return salvaged;
63729
+ } catch {
63730
+ }
63731
+ throw new BudgetExceededError(`ultra11y judge: ${lastError}`);
63732
+ }
63710
63733
  if (env.api_error_status && (env.api_error_status === 429 || env.api_error_status >= 500)) continue;
63711
63734
  break;
63712
63735
  }
@@ -74910,6 +74933,9 @@ async function cmdJudge(p) {
74910
74933
  // runner with room.
74911
74934
  concurrency: runner === "claude" || runner === "codex" ? cliConcurrency : void 0,
74912
74935
  abortOnError: isProviderUnavailableError,
74936
+ // The seam that lets a budget abort be halved rather than lost: the same renderer
74937
+ // `batchWorklist` used, so a re-queued half is prompted exactly like a first-class batch.
74938
+ render: render2,
74913
74939
  maxBudgetUsd: Number.isFinite(maxBudgetUsd) ? maxBudgetUsd : void 0,
74914
74940
  effort,
74915
74941
  timeoutMs: Number.isFinite(timeoutMs) ? timeoutMs : void 0,
@@ -27,7 +27,7 @@ variables:
27
27
  ULTRA11Y_LANG: en
28
28
  # Exact by default: a CI report must say which engine produced it. The release script keeps
29
29
  # this pin aligned with the package version; override only for a deliberate upgrade test.
30
- ULTRA11Y_VERSION: '5.40.1'
30
+ ULTRA11Y_VERSION: '5.40.2'
31
31
  # Batches of 8 amortise the prompt; each accepted verdict is checkpointed before the next
32
32
  # batch, and later passes receive only the residue.
33
33
  ULTRA11Y_GRAIN: batch
package/src/agent-cli.ts CHANGED
@@ -22,7 +22,7 @@ import { spawn } from "node:child_process";
22
22
  import { existsSync } from "node:fs";
23
23
  import { delimiter, join } from "node:path";
24
24
  import type { AdjudicationItem } from "./adjudicate.js";
25
- import { VERDICT_TOOL } from "./llm.js";
25
+ import { BudgetExceededError, VERDICT_TOOL } from "./llm.js";
26
26
  import type { LlmOptions, RawVerdict } from "./llm.js";
27
27
  import type { Lang } from "./types.js";
28
28
  import { refuteSchema, refuteSystemPrompt, type VerifyItem } from "./verify.js";
@@ -350,6 +350,21 @@ async function runCli<T>(argv: string[], prompt: string, opts: LlmOptions, extra
350
350
  }
351
351
  if (env.is_error || env.subtype !== "success") {
352
352
  lastError = `the CLI reported an error (${env.subtype ?? "unknown"}${env.api_error_status ? `, api status ${env.api_error_status}` : ""}).`;
353
+ // A BUDGET ABORT IS NOT AN EMPTY ANSWER. The ceiling can trip after the model has
354
+ // already produced its structured output, so LOOK before giving up — this envelope was
355
+ // paid for either way. `extract` is allowed to throw (usually it does: an abort with no
356
+ // final answer has nothing to parse), and that is the ordinary case, not an error.
357
+ if (env.subtype === "error_max_budget_usd") {
358
+ try {
359
+ const salvaged = extract(env);
360
+ if (Array.isArray(salvaged) ? salvaged.length > 0 : salvaged !== undefined) return salvaged;
361
+ } catch {
362
+ /* nothing usable in the envelope — fall through to the typed throw below */
363
+ }
364
+ // Not retried: attempt 2 is a fresh process with a fresh ceiling and the same work,
365
+ // so it buys the same abort at full price. Split it instead — see BudgetExceededError.
366
+ throw new BudgetExceededError(`ultra11y judge: ${lastError}`);
367
+ }
353
368
  // Only a transport fault is worth another attempt; a refusal is not.
354
369
  if (env.api_error_status && (env.api_error_status === 429 || env.api_error_status >= 500)) continue;
355
370
  break;
package/src/cli.ts CHANGED
@@ -3784,6 +3784,9 @@ async function cmdJudge(p: ParsedArgs): Promise<number> {
3784
3784
  // runner with room.
3785
3785
  concurrency: runner === "claude" || runner === "codex" ? cliConcurrency : undefined,
3786
3786
  abortOnError: isProviderUnavailableError,
3787
+ // The seam that lets a budget abort be halved rather than lost: the same renderer
3788
+ // `batchWorklist` used, so a re-queued half is prompted exactly like a first-class batch.
3789
+ render,
3787
3790
  maxBudgetUsd: Number.isFinite(maxBudgetUsd) ? maxBudgetUsd : undefined,
3788
3791
  effort,
3789
3792
  timeoutMs: Number.isFinite(timeoutMs) ? timeoutMs : undefined,
@@ -2,7 +2,7 @@
2
2
  "wcagVersion": "2.2",
3
3
  "source": "https://www.w3.org/TR/WCAG22/",
4
4
  "criteriaSource": "https://github.com/w3c/wcag",
5
- "provenance": "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-08-30 via `node scripts/build-standards.mjs --refresh-universe`. Classification: core-AA = ships in src/data/wcag.json (the shipped WCAG 2.2 AA core); out-of-core = WCAG AAA; removed = obsolete (4.1.1).",
5
+ "provenance": "Full WCAG 2.x SC universe (all levels incl. AAA, and the removed 4.1.1 Parsing) fetched from raw.githubusercontent.com/w3c/wcag@main on 2026-08-31 via `node scripts/build-standards.mjs --refresh-universe`. Classification: core-AA = ships in src/data/wcag.json (the shipped WCAG 2.2 AA core); out-of-core = WCAG AAA; removed = obsolete (4.1.1).",
6
6
  "criteria": [
7
7
  {
8
8
  "id": "1.1.1",
package/src/llm.ts CHANGED
@@ -120,6 +120,29 @@ export interface LlmOptions {
120
120
  * without waiting out the real curve. */
121
121
  backoffMs?: (attempt: number) => number;
122
122
  onProgress?: (done: number, total: number) => void;
123
+ /** Re-render the prompt for an arbitrary slice of a worklist — the same function
124
+ * `batchWorklist` was given. Supplying it lets `judgeAll` HALVE a batch the provider
125
+ * aborted on `--max-budget-usd` and requeue both halves, because that ceiling is per
126
+ * invocation: the same criteria in two calls get two ceilings. Without it a budget abort
127
+ * costs the whole batch, which is the behaviour before this option existed. */
128
+ render?: (items: AdjudicationItem[]) => string;
129
+ }
130
+
131
+ /** The CLI stopped because `--max-budget-usd` was reached.
132
+ *
133
+ * Typed, and not folded into the generic failure, because it is the ONE failure whose right
134
+ * answer is neither "retry" nor "give up": the ceiling is per INVOCATION, so the same work
135
+ * split across two invocations gets two ceilings. `judgeAll` halves the batch and requeues it,
136
+ * which turns a loss of eight criteria into a loss of one.
137
+ *
138
+ * Measured on a real run before this existed — 30 batch invocations, $38.90 spent, 12 verdicts
139
+ * kept: every batch that passed the ceiling was thrown away whole, having already been paid
140
+ * for. The cost is booked (`onCost` fires before this throws); only the work was lost. */
141
+ export class BudgetExceededError extends Error {
142
+ constructor(message: string) {
143
+ super(message);
144
+ this.name = "BudgetExceededError";
145
+ }
123
146
  }
124
147
 
125
148
  /** Provider saturation is shared state, not a property of one criterion. Transport backends
@@ -327,6 +350,9 @@ export async function judgeAll(
327
350
  const verdicts: RawVerdict[] = [];
328
351
  const failures: string[] = [];
329
352
  let done = 0;
353
+ // Not `batches.length`: a budget abort splits a batch in two and requeues both, so the
354
+ // denominator the progress callback reports has to grow with the queue.
355
+ let total = batches.length;
330
356
  const queue = [...batches];
331
357
  const backend = opts.backend ?? judgeBatch;
332
358
  const lanes = Math.max(1, opts.concurrency ?? CONCURRENCY);
@@ -346,10 +372,23 @@ export async function judgeAll(
346
372
  // behind what it had already ruled on rather than nothing at all.
347
373
  if (checked.accepted.length) opts.onVerdicts?.(checked.accepted);
348
374
  } catch (e) {
375
+ // A BUDGET ABORT IS HALVED, NOT MOURNED. The ceiling is per invocation, so two calls
376
+ // get two ceilings: re-queueing the halves converts « eight criteria lost » into at
377
+ // worst « one criterion lost », and a batch that only just overran usually lands
378
+ // whole on the retry. Bounded by construction — halving 8 reaches 1 in three steps —
379
+ // and a single item that still overruns is a real failure, reported as one.
380
+ if (e instanceof BudgetExceededError && b.items.length > 1 && opts.render) {
381
+ const half = Math.ceil(b.items.length / 2);
382
+ const parts = [b.items.slice(0, half), b.items.slice(half)];
383
+ queue.unshift(...parts.map((items) => ({ items, prompt: opts.render!(items) })));
384
+ total += parts.length - 1;
385
+ opts.onProgress?.(done, total);
386
+ continue;
387
+ }
349
388
  failures.push(e instanceof Error ? e.message : String(e));
350
389
  if (opts.abortOnError?.(e)) aborted = true;
351
390
  }
352
- opts.onProgress?.(++done, batches.length);
391
+ opts.onProgress?.(++done, total);
353
392
  }
354
393
  }),
355
394
  );
@@ -225,11 +225,18 @@ function judgmentGuard(r: PackCriterionResult, pc: PackCriterion): PackCriterion
225
225
  * `judgmentGuard` then reopened as « à évaluer ». A criterion that already carries a verdict
226
226
  * keeps it; this only ever touches `manual`.
227
227
  *
228
- * Three refusals:
228
+ * Four refusals:
229
229
  * • `judgment` criteria. « All its rules were silent » does not answer a question about
230
230
  * PERTINENCE, which is the whole reason `judgmentGuard` exists. Read from the PACK, not
231
231
  * from the derived flag: that flag is only set when the guard actually intercepted a `C`.
232
232
  * • `outOfScope`: no core success criterion to project from at all, which no measurement fixes.
233
+ * • a criterion whose pack DECLARES an automation contract without `completeBySilence`. That
234
+ * contract is the pack saying, test by test, that silence here does not prove conformity —
235
+ * and it outranks a measurement that only shows the rules ran. Written as `pc.automation &&`
236
+ * rather than `pc.automation?.` DELIBERATELY, and it is the same shape as `judgmentGuard`:
237
+ * a pack that declares NO contract has not refused anything, so optional chaining would
238
+ * read its silence as a refusal and kill this tier for every pack but RGAA — which is
239
+ * exactly what it did, without a test to notice.
233
240
  * • an ADJUDICATED verdict never reaches here — the adjudication branch returns before this.
234
241
  * An agent that ruled « undecidable » examined the criterion and said so; a rule that
235
242
  * measured something narrower must not overturn it. That is what keeps RGAA 11.9 (« chaque
@@ -242,7 +249,7 @@ function measuredRescue(
242
249
  ran: PageCoverage | undefined,
243
250
  pageId: string | undefined,
244
251
  ): PackCriterionResult {
245
- if (r.status !== "manual" || pc.judgment || r.outOfScope || pc.automation?.completeBySilence !== true) return r;
252
+ if (r.status !== "manual" || pc.judgment || r.outOfScope || (pc.automation && pc.automation.completeBySilence !== true)) return r;
246
253
  if (!criterionMeasuredOn(pc.appliesTo?.ruleIds, pc.wcag, cov, ran)) return r;
247
254
  // The stale explanations go with the stale status: `scopedOut` and `judgment` say why the
248
255
  // criterion STAYED open, and it no longer is.
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.40.1";
3
+ export const VERSION = "5.40.2";
4
4
  export const SCHEMA_VERSION = 2;
5
5
 
6
6
  // Lang = the UI FRAME's language — the `L` localization tables each module (report,