ultra11y 5.33.1 → 5.34.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.33.1",
13
+ "version": "5.34.1",
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.33.1",
4
+ "version": "5.34.1",
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.33.1",
4
+ "version": "5.34.1",
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.33.1";
30
+ const ULTRA11Y_PLUGIN_VERSION = "5.34.1";
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.33.1",
3
+ "version": "5.34.1",
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 join52, relative as relative5, 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.33.1";
9
+ var VERSION = "5.34.1";
10
10
  var SCHEMA_VERSION = 2;
11
11
  var INAPPLICABLE_STATUS = "C";
12
12
 
@@ -2293,7 +2293,7 @@ var wcag_universe_default = {
2293
2293
  wcagVersion: "2.2",
2294
2294
  source: "https://www.w3.org/TR/WCAG22/",
2295
2295
  criteriaSource: "https://github.com/w3c/wcag",
2296
- 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-24 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).",
2296
+ 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-25 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).",
2297
2297
  criteria: [
2298
2298
  {
2299
2299
  id: "1.1.1",
@@ -59367,6 +59367,12 @@ function auditFiles(audit2, cwd) {
59367
59367
  return /* @__PURE__ */ new Set();
59368
59368
  }
59369
59369
  }
59370
+ function offHarvestHint(criteriaId, evidence) {
59371
+ const anchors = [...new Set(evidence.map((e) => e.selector?.trim() || `${e.file}:${e.line}`).filter(Boolean))];
59372
+ const shown = anchors.slice(0, 4).join(", ");
59373
+ const rest = anchors.length > 4 ? `, +${anchors.length - 4}` : "";
59374
+ return anchors.length ? ` \u2014 this anchor is not among the evidence harvested for ${criteriaId}, so the snippet was verified literally rather than vouched for by the harvest. Cite one of: ${shown}${rest}.` : ` \u2014 ${criteriaId} was harvested no evidence, so nothing can vouch for a transcription here. If the criterion has no subject in scope, the verdict is NA with a justification and no citation to ground.`;
59375
+ }
59370
59376
  function tagOf2(x) {
59371
59377
  const fromSnippet = /<\s*([a-zA-Z][\w-]*)/.exec(x.snippet ?? "");
59372
59378
  if (fromSnippet) return fromSnippet[1].toLowerCase();
@@ -59538,8 +59544,8 @@ function applyAdjudication(audit2, adj, opts = {}) {
59538
59544
  }
59539
59545
  return engineNcCache.get(key2) ?? EMPTY_IDS;
59540
59546
  };
59541
- const toGround = (criteriaId, g, fallback) => {
59542
- const entry = { g, ...fallback ? { fallback } : {} };
59547
+ const toGround = (criteriaId, g, fallback, offHarvest) => {
59548
+ const entry = { g, ...fallback ? { fallback } : {}, ...offHarvest ? { offHarvest } : {} };
59543
59549
  const list = groundInputs.get(criteriaId);
59544
59550
  if (list) list.push(entry);
59545
59551
  else groundInputs.set(criteriaId, [entry]);
@@ -59584,7 +59590,7 @@ function applyAdjudication(audit2, adj, opts = {}) {
59584
59590
  anchor.representative ? { file: anchor.at.file, line: anchor.at.line, selector: anchor.at.selector ?? c2.selector, snippet: anchor.at.snippet } : { file: c2.file, line: c2.line, selector: anchor.at.selector ?? c2.selector }
59585
59591
  );
59586
59592
  } else {
59587
- toGround(it.criteriaId, cite);
59593
+ toGround(it.criteriaId, cite, void 0, anchor ? void 0 : offHarvestHint(it.criteriaId, it.evidence));
59588
59594
  }
59589
59595
  }
59590
59596
  }
@@ -59644,7 +59650,7 @@ function applyAdjudication(audit2, adj, opts = {}) {
59644
59650
  }
59645
59651
  const grounding = { grounded: 0, moved: 0, failed: 0, issues: [] };
59646
59652
  for (const [criteriaId, inputs] of groundInputs) {
59647
- for (const { g, fallback } of inputs) {
59653
+ for (const { g, fallback, offHarvest } of inputs) {
59648
59654
  let r = groundFinding(g, { cwd: opts.cwd });
59649
59655
  if (!r.ok && fallback) {
59650
59656
  const viaAnchor = groundFinding(fallback, { cwd: opts.cwd });
@@ -59656,8 +59662,9 @@ function applyAdjudication(audit2, adj, opts = {}) {
59656
59662
  } else {
59657
59663
  grounding.failed++;
59658
59664
  if (r.issue) {
59659
- grounding.issues.push(r.issue);
59660
- blame(criteriaId, r.issue);
59665
+ const issue = offHarvest && r.issue.startsWith("cited snippet not found") ? `${r.issue}${offHarvest}` : r.issue;
59666
+ grounding.issues.push(issue);
59667
+ blame(criteriaId, issue);
59661
59668
  }
59662
59669
  }
59663
59670
  }
@@ -60378,7 +60385,9 @@ var VERDICT_KINDS = `Rule it (the apply gate is FAIL-CLOSED \u2014 a verdict mis
60378
60385
  - \`NC\` (non-conforming) \u2014 REQUIRES \`findings\`: at least one groundable \`{ file, line, selector?, message, snippet?, severity?, normativeRef }\` pointing at REAL source. The fold re-grounds every finding; an invented file:line is rejected, and so is a finding with no \`file\` at all. \`normativeRef\` MUST cite the precise failed test \u2014 under a country standard, one of the criterion's OWN numbered tests, listed in its brief under \xAB tests to rule on \xBB. A WCAG id looks alike, denotes an unrelated test, and is rejected.
60379
60386
  - \`NA\` (not applicable) \u2014 REQUIRES \`justification\`, AND \`citations[]\` whenever evidence WAS presented, to say which of those items fall outside the criterion's scope.
60380
60387
  - \`manual\` (still undecidable) \u2014 REQUIRES \`reason\`: \`needs-rendered-dom\` (only a rendered DOM can decide it, and no capture in this run carries its subject) or \`undecidable\` (the evidence cannot settle it either way).`;
60381
- var ABSENCE_RULE = `AN NC SHAPED LIKE AN ABSENCE IS STILL ANCHORED. \xAB No second navigation system \xBB, \xAB no search engine \xBB, \xAB no error message suggests the expected format \xBB \u2014 an absence is OBSERVED somewhere: cite the element and the page you observed it on. And when the criterion's subject exists nowhere in the audited scope, the verdict is \`NA\` with its justification, never \`NC\`.`;
60388
+ var ABSENCE_RULE = `AN NC SHAPED LIKE AN ABSENCE IS STILL ANCHORED. \xAB No second navigation system \xBB, \xAB no search engine \xBB, \xAB no error message suggests the expected format \xBB \u2014 an absence is OBSERVED somewhere: cite the element and the page you observed it on. And when the criterion's subject exists nowhere in the audited scope, the verdict is \`NA\` with its justification, never \`NC\`.
60389
+
60390
+ AND CITE FROM THIS CRITERION'S OWN ANCHORS, NOT THE THING YOU ARE RULING OUT. An absence pulls you toward the element you are arguing ABOUT \u2014 the search form that is not a site search engine, the menu that is not a second navigation system \u2014 and that element is, precisely because it is off-topic, absent from what this criterion was harvested. A citation on one of the criterion's own anchors is vouched for by the harvest; one outside it has its snippet verified LITERALLY, character for character, and a retyping then fails. Cite the region you inspected \u2014 the \`header\`, the \`nav\`, the \`footer\` the brief listed \u2014 and say in the justification what you did not find in it.`;
60382
60391
  var CAPTURE_RULE = `THE RENDERED PAGE MAY BE ON DISK. When a criterion's evidence is anchored under \`.ultra11y/pages/<id>/\`, the browser already ran: \`dom.html\`, \`styles.json\`, \`boxes.json\`, \`axtree.json\` and \`screen.png\` are there to read. \`needs-rendered-dom\` is refused on such a criterion \u2014 decide it from those files, or answer \`undecidable\` and say what the capture does not settle.`;
60383
60392
  var NEVER_GUESS_RULE = `Never guess. A criterion you cannot decide from real evidence stays \`manual\` with its reason \u2014 that is a valid, honest verdict, and it is worth more than a verdict the gate throws away.`;
60384
60393
  var SCOPE_RULE = `Rule ONLY on the criteria presented. Never introduce another \u2014 a verdict for a criterion nobody asked about is dropped, and under the fold it would otherwise overwrite what the deterministic engine already decided.`;
@@ -60588,6 +60597,7 @@ function applyRawVerdicts(items, verdicts) {
60588
60597
  var ALLOWED_TOOLS = "Read,Grep,Glob";
60589
60598
  var MAX_ATTEMPTS2 = 4;
60590
60599
  var DEFAULT_CLI_MODEL = "claude-haiku-4-5-20251001";
60600
+ var EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
60591
60601
  var DEFAULT_TIMEOUT_MS2 = 10 * 6e4;
60592
60602
  function realSpawn(argv, input, timeoutMs) {
60593
60603
  return new Promise((resolve17, reject) => {
@@ -60664,6 +60674,7 @@ function cliArgv(opts, items) {
60664
60674
  opts.model ?? DEFAULT_CLI_MODEL
60665
60675
  ];
60666
60676
  if (opts.maxBudgetUsd !== void 0) argv.push("--max-budget-usd", String(opts.maxBudgetUsd));
60677
+ if (opts.effort) argv.push("--effort", opts.effort);
60667
60678
  return argv;
60668
60679
  }
60669
60680
  function verdictsFromText(text) {
@@ -60726,6 +60737,7 @@ async function refuteBatchCli(items, prompt, opts, lang = "en") {
60726
60737
  opts.model ?? DEFAULT_CLI_MODEL
60727
60738
  ];
60728
60739
  if (opts.maxBudgetUsd !== void 0) argv.push("--max-budget-usd", String(opts.maxBudgetUsd));
60740
+ if (opts.effort) argv.push("--effort", opts.effort);
60729
60741
  return runCli2(argv, prompt, opts, (env) => {
60730
60742
  const structured = env.structured_output?.verdicts;
60731
60743
  const raw = Array.isArray(structured) ? structured : verdictsArrayFromText(env.result ?? "");
@@ -68889,7 +68901,7 @@ Usage:
68889
68901
  ultra11y orchestrate --run <dir> [--phase adjudicate|verify-report] [--eco] [--list] [--lang auto|en|fr]
68890
68902
  ultra11y fix <globs\u2026 | -> [--write] [--iterate] [--changed | --since <ref> | --staged] [--safe] [--include <glob>] [--exclude <glob>] [--ext <list>] [--only <ids>] [--jsx] [--json] [--lang auto|en|fr]
68891
68903
  ultra11y init [--hook] [--ci] [--baseline] [--fail-on blocking|major|minor]
68892
- ultra11y judge --in <audit.json> [--standard <pack>] [--runner api|cli] [--grain batch|criterion] [--max <n>] [--model <id>] [--max-budget-usd <n>] [--timeout <s>] [--out <dir>] [--apply] (adjudicate the manual criteria with a model \u2014 api needs ANTHROPIC_API_KEY; cli runs the Claude Code CLI and takes CLAUDE_CODE_OAUTH_TOKEN too)
68904
+ ultra11y judge --in <audit.json> [--standard <pack>] [--runner api|cli] [--grain batch|criterion] [--max <n>] [--model <id>] [--effort <level>] [--max-budget-usd <n>] [--timeout <s>] [--out <dir>] [--apply] (adjudicate the manual criteria with a model \u2014 api needs ANTHROPIC_API_KEY; cli runs the Claude Code CLI and takes CLAUDE_CODE_OAUTH_TOKEN too)
68893
68905
  ultra11y pack check <pack.json> [--guidance <g.json>] [--json] | pack scaffold
68894
68906
  ultra11y scan <url|file\u2026> [--runtime auto|local|docker] [--cwd <dir>] [--storage-state <file>] [--no-interact] [--interact-clicks] [--no-snapshot] [--merge <audit.json>] [--out <dir>] [--json]
68895
68907
  ultra11y scan --sample [--runtime \u2026] [--cwd <dir>] [--storage-state <file>] [--merge <audit.json>] [--json] (scan the .ultra11yrc.json page sample)
@@ -69122,6 +69134,8 @@ Options:
69122
69134
  --provider <id> tickets: auto|github|gitlab|jira. 'auto' reads ULTRA11Y_TICKET_PROVIDER,
69123
69135
  then .ultra11yrc.json, then the git remote (Jira is never auto-detected)
69124
69136
  --grain <mode> tickets: what one ticket is \u2014 criterion (default) | page | page-criterion | single | file
69137
+ --effort <level> judge (--runner cli): reasoning effort per invocation \u2014 low | medium |
69138
+ high | xhigh | max. Unset leaves the CLI's own default
69125
69139
  --transport <t> tickets: auto|cli|rest (mcp: stdio|http). 'auto' prefers the CLI (gh/glab),
69126
69140
  falling back to REST when only a token is available. Jira is REST-only
69127
69141
  --max-tickets <n> tickets: refuse to file more than n tickets in one run (default 200)
@@ -69325,6 +69339,7 @@ var VALUE_FLAGS2 = /* @__PURE__ */ new Set([
69325
69339
  "runner",
69326
69340
  "grain",
69327
69341
  "max-budget-usd",
69342
+ "effort",
69328
69343
  "timeout",
69329
69344
  "concurrency",
69330
69345
  "out",
@@ -71460,6 +71475,15 @@ async function cmdJudge(p) {
71460
71475
  console.error(`ultra11y judge: --grain '${grain}' is not a grain \u2014 expected 'batch' or 'criterion'.`);
71461
71476
  return 2;
71462
71477
  }
71478
+ const effort = typeof p.flags.effort === "string" ? p.flags.effort : void 0;
71479
+ if (effort !== void 0 && !EFFORT_LEVELS.includes(effort)) {
71480
+ console.error(`ultra11y judge: --effort '${effort}' is not a level \u2014 expected ${EFFORT_LEVELS.map((l) => `'${l}'`).join(", ")}.`);
71481
+ return 2;
71482
+ }
71483
+ if (effort !== void 0 && runner !== "cli") {
71484
+ console.error("ultra11y judge: --effort applies to `--runner cli` only \u2014 the Messages API has no session to set an effort on.");
71485
+ return 2;
71486
+ }
71463
71487
  const key2 = typeof p.flags["api-key"] === "string" && p.flags["api-key"] ? p.flags["api-key"] : apiKeyFromEnv();
71464
71488
  if (!key2 && runner === "api") {
71465
71489
  console.error(
@@ -71533,6 +71557,7 @@ async function cmdJudge(p) {
71533
71557
  // requests do. `--concurrency` overrides it for a runner with room.
71534
71558
  concurrency: runner === "cli" ? cliConcurrency : void 0,
71535
71559
  maxBudgetUsd: Number.isFinite(maxBudgetUsd) ? maxBudgetUsd : void 0,
71560
+ effort,
71536
71561
  timeoutMs: Number.isFinite(timeoutMs) ? timeoutMs : void 0,
71537
71562
  onCost: (usd) => {
71538
71563
  spent += usd;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "source": "https://www.w3.org/Translations/WCAG22-fr/",
3
- "fetchedAt": "2026-08-24",
3
+ "fetchedAt": "2026-08-25",
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-24 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-25 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-24",
5
+ "fetchedAt": "2026-08-25",
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-24",
4
+ "fetchedAt": "2026-08-25",
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.",
@@ -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.33.1
8
+ version: 5.34.1
9
9
  ---
10
10
 
11
11
  # review-a11y — accessibility review of changed code
@@ -6,7 +6,7 @@ import { join as join52, relative as relative5, 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.33.1";
9
+ var VERSION = "5.34.1";
10
10
  var SCHEMA_VERSION = 2;
11
11
  var INAPPLICABLE_STATUS = "C";
12
12
 
@@ -2293,7 +2293,7 @@ var wcag_universe_default = {
2293
2293
  wcagVersion: "2.2",
2294
2294
  source: "https://www.w3.org/TR/WCAG22/",
2295
2295
  criteriaSource: "https://github.com/w3c/wcag",
2296
- 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-24 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).",
2296
+ 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-25 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).",
2297
2297
  criteria: [
2298
2298
  {
2299
2299
  id: "1.1.1",
@@ -59367,6 +59367,12 @@ function auditFiles(audit2, cwd) {
59367
59367
  return /* @__PURE__ */ new Set();
59368
59368
  }
59369
59369
  }
59370
+ function offHarvestHint(criteriaId, evidence) {
59371
+ const anchors = [...new Set(evidence.map((e) => e.selector?.trim() || `${e.file}:${e.line}`).filter(Boolean))];
59372
+ const shown = anchors.slice(0, 4).join(", ");
59373
+ const rest = anchors.length > 4 ? `, +${anchors.length - 4}` : "";
59374
+ return anchors.length ? ` \u2014 this anchor is not among the evidence harvested for ${criteriaId}, so the snippet was verified literally rather than vouched for by the harvest. Cite one of: ${shown}${rest}.` : ` \u2014 ${criteriaId} was harvested no evidence, so nothing can vouch for a transcription here. If the criterion has no subject in scope, the verdict is NA with a justification and no citation to ground.`;
59375
+ }
59370
59376
  function tagOf2(x) {
59371
59377
  const fromSnippet = /<\s*([a-zA-Z][\w-]*)/.exec(x.snippet ?? "");
59372
59378
  if (fromSnippet) return fromSnippet[1].toLowerCase();
@@ -59538,8 +59544,8 @@ function applyAdjudication(audit2, adj, opts = {}) {
59538
59544
  }
59539
59545
  return engineNcCache.get(key2) ?? EMPTY_IDS;
59540
59546
  };
59541
- const toGround = (criteriaId, g, fallback) => {
59542
- const entry = { g, ...fallback ? { fallback } : {} };
59547
+ const toGround = (criteriaId, g, fallback, offHarvest) => {
59548
+ const entry = { g, ...fallback ? { fallback } : {}, ...offHarvest ? { offHarvest } : {} };
59543
59549
  const list = groundInputs.get(criteriaId);
59544
59550
  if (list) list.push(entry);
59545
59551
  else groundInputs.set(criteriaId, [entry]);
@@ -59584,7 +59590,7 @@ function applyAdjudication(audit2, adj, opts = {}) {
59584
59590
  anchor.representative ? { file: anchor.at.file, line: anchor.at.line, selector: anchor.at.selector ?? c2.selector, snippet: anchor.at.snippet } : { file: c2.file, line: c2.line, selector: anchor.at.selector ?? c2.selector }
59585
59591
  );
59586
59592
  } else {
59587
- toGround(it.criteriaId, cite);
59593
+ toGround(it.criteriaId, cite, void 0, anchor ? void 0 : offHarvestHint(it.criteriaId, it.evidence));
59588
59594
  }
59589
59595
  }
59590
59596
  }
@@ -59644,7 +59650,7 @@ function applyAdjudication(audit2, adj, opts = {}) {
59644
59650
  }
59645
59651
  const grounding = { grounded: 0, moved: 0, failed: 0, issues: [] };
59646
59652
  for (const [criteriaId, inputs] of groundInputs) {
59647
- for (const { g, fallback } of inputs) {
59653
+ for (const { g, fallback, offHarvest } of inputs) {
59648
59654
  let r = groundFinding(g, { cwd: opts.cwd });
59649
59655
  if (!r.ok && fallback) {
59650
59656
  const viaAnchor = groundFinding(fallback, { cwd: opts.cwd });
@@ -59656,8 +59662,9 @@ function applyAdjudication(audit2, adj, opts = {}) {
59656
59662
  } else {
59657
59663
  grounding.failed++;
59658
59664
  if (r.issue) {
59659
- grounding.issues.push(r.issue);
59660
- blame(criteriaId, r.issue);
59665
+ const issue = offHarvest && r.issue.startsWith("cited snippet not found") ? `${r.issue}${offHarvest}` : r.issue;
59666
+ grounding.issues.push(issue);
59667
+ blame(criteriaId, issue);
59661
59668
  }
59662
59669
  }
59663
59670
  }
@@ -60378,7 +60385,9 @@ var VERDICT_KINDS = `Rule it (the apply gate is FAIL-CLOSED \u2014 a verdict mis
60378
60385
  - \`NC\` (non-conforming) \u2014 REQUIRES \`findings\`: at least one groundable \`{ file, line, selector?, message, snippet?, severity?, normativeRef }\` pointing at REAL source. The fold re-grounds every finding; an invented file:line is rejected, and so is a finding with no \`file\` at all. \`normativeRef\` MUST cite the precise failed test \u2014 under a country standard, one of the criterion's OWN numbered tests, listed in its brief under \xAB tests to rule on \xBB. A WCAG id looks alike, denotes an unrelated test, and is rejected.
60379
60386
  - \`NA\` (not applicable) \u2014 REQUIRES \`justification\`, AND \`citations[]\` whenever evidence WAS presented, to say which of those items fall outside the criterion's scope.
60380
60387
  - \`manual\` (still undecidable) \u2014 REQUIRES \`reason\`: \`needs-rendered-dom\` (only a rendered DOM can decide it, and no capture in this run carries its subject) or \`undecidable\` (the evidence cannot settle it either way).`;
60381
- var ABSENCE_RULE = `AN NC SHAPED LIKE AN ABSENCE IS STILL ANCHORED. \xAB No second navigation system \xBB, \xAB no search engine \xBB, \xAB no error message suggests the expected format \xBB \u2014 an absence is OBSERVED somewhere: cite the element and the page you observed it on. And when the criterion's subject exists nowhere in the audited scope, the verdict is \`NA\` with its justification, never \`NC\`.`;
60388
+ var ABSENCE_RULE = `AN NC SHAPED LIKE AN ABSENCE IS STILL ANCHORED. \xAB No second navigation system \xBB, \xAB no search engine \xBB, \xAB no error message suggests the expected format \xBB \u2014 an absence is OBSERVED somewhere: cite the element and the page you observed it on. And when the criterion's subject exists nowhere in the audited scope, the verdict is \`NA\` with its justification, never \`NC\`.
60389
+
60390
+ AND CITE FROM THIS CRITERION'S OWN ANCHORS, NOT THE THING YOU ARE RULING OUT. An absence pulls you toward the element you are arguing ABOUT \u2014 the search form that is not a site search engine, the menu that is not a second navigation system \u2014 and that element is, precisely because it is off-topic, absent from what this criterion was harvested. A citation on one of the criterion's own anchors is vouched for by the harvest; one outside it has its snippet verified LITERALLY, character for character, and a retyping then fails. Cite the region you inspected \u2014 the \`header\`, the \`nav\`, the \`footer\` the brief listed \u2014 and say in the justification what you did not find in it.`;
60382
60391
  var CAPTURE_RULE = `THE RENDERED PAGE MAY BE ON DISK. When a criterion's evidence is anchored under \`.ultra11y/pages/<id>/\`, the browser already ran: \`dom.html\`, \`styles.json\`, \`boxes.json\`, \`axtree.json\` and \`screen.png\` are there to read. \`needs-rendered-dom\` is refused on such a criterion \u2014 decide it from those files, or answer \`undecidable\` and say what the capture does not settle.`;
60383
60392
  var NEVER_GUESS_RULE = `Never guess. A criterion you cannot decide from real evidence stays \`manual\` with its reason \u2014 that is a valid, honest verdict, and it is worth more than a verdict the gate throws away.`;
60384
60393
  var SCOPE_RULE = `Rule ONLY on the criteria presented. Never introduce another \u2014 a verdict for a criterion nobody asked about is dropped, and under the fold it would otherwise overwrite what the deterministic engine already decided.`;
@@ -60588,6 +60597,7 @@ function applyRawVerdicts(items, verdicts) {
60588
60597
  var ALLOWED_TOOLS = "Read,Grep,Glob";
60589
60598
  var MAX_ATTEMPTS2 = 4;
60590
60599
  var DEFAULT_CLI_MODEL = "claude-haiku-4-5-20251001";
60600
+ var EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
60591
60601
  var DEFAULT_TIMEOUT_MS2 = 10 * 6e4;
60592
60602
  function realSpawn(argv, input, timeoutMs) {
60593
60603
  return new Promise((resolve17, reject) => {
@@ -60664,6 +60674,7 @@ function cliArgv(opts, items) {
60664
60674
  opts.model ?? DEFAULT_CLI_MODEL
60665
60675
  ];
60666
60676
  if (opts.maxBudgetUsd !== void 0) argv.push("--max-budget-usd", String(opts.maxBudgetUsd));
60677
+ if (opts.effort) argv.push("--effort", opts.effort);
60667
60678
  return argv;
60668
60679
  }
60669
60680
  function verdictsFromText(text) {
@@ -60726,6 +60737,7 @@ async function refuteBatchCli(items, prompt, opts, lang = "en") {
60726
60737
  opts.model ?? DEFAULT_CLI_MODEL
60727
60738
  ];
60728
60739
  if (opts.maxBudgetUsd !== void 0) argv.push("--max-budget-usd", String(opts.maxBudgetUsd));
60740
+ if (opts.effort) argv.push("--effort", opts.effort);
60729
60741
  return runCli2(argv, prompt, opts, (env) => {
60730
60742
  const structured = env.structured_output?.verdicts;
60731
60743
  const raw = Array.isArray(structured) ? structured : verdictsArrayFromText(env.result ?? "");
@@ -68889,7 +68901,7 @@ Usage:
68889
68901
  ultra11y orchestrate --run <dir> [--phase adjudicate|verify-report] [--eco] [--list] [--lang auto|en|fr]
68890
68902
  ultra11y fix <globs\u2026 | -> [--write] [--iterate] [--changed | --since <ref> | --staged] [--safe] [--include <glob>] [--exclude <glob>] [--ext <list>] [--only <ids>] [--jsx] [--json] [--lang auto|en|fr]
68891
68903
  ultra11y init [--hook] [--ci] [--baseline] [--fail-on blocking|major|minor]
68892
- ultra11y judge --in <audit.json> [--standard <pack>] [--runner api|cli] [--grain batch|criterion] [--max <n>] [--model <id>] [--max-budget-usd <n>] [--timeout <s>] [--out <dir>] [--apply] (adjudicate the manual criteria with a model \u2014 api needs ANTHROPIC_API_KEY; cli runs the Claude Code CLI and takes CLAUDE_CODE_OAUTH_TOKEN too)
68904
+ ultra11y judge --in <audit.json> [--standard <pack>] [--runner api|cli] [--grain batch|criterion] [--max <n>] [--model <id>] [--effort <level>] [--max-budget-usd <n>] [--timeout <s>] [--out <dir>] [--apply] (adjudicate the manual criteria with a model \u2014 api needs ANTHROPIC_API_KEY; cli runs the Claude Code CLI and takes CLAUDE_CODE_OAUTH_TOKEN too)
68893
68905
  ultra11y pack check <pack.json> [--guidance <g.json>] [--json] | pack scaffold
68894
68906
  ultra11y scan <url|file\u2026> [--runtime auto|local|docker] [--cwd <dir>] [--storage-state <file>] [--no-interact] [--interact-clicks] [--no-snapshot] [--merge <audit.json>] [--out <dir>] [--json]
68895
68907
  ultra11y scan --sample [--runtime \u2026] [--cwd <dir>] [--storage-state <file>] [--merge <audit.json>] [--json] (scan the .ultra11yrc.json page sample)
@@ -69122,6 +69134,8 @@ Options:
69122
69134
  --provider <id> tickets: auto|github|gitlab|jira. 'auto' reads ULTRA11Y_TICKET_PROVIDER,
69123
69135
  then .ultra11yrc.json, then the git remote (Jira is never auto-detected)
69124
69136
  --grain <mode> tickets: what one ticket is \u2014 criterion (default) | page | page-criterion | single | file
69137
+ --effort <level> judge (--runner cli): reasoning effort per invocation \u2014 low | medium |
69138
+ high | xhigh | max. Unset leaves the CLI's own default
69125
69139
  --transport <t> tickets: auto|cli|rest (mcp: stdio|http). 'auto' prefers the CLI (gh/glab),
69126
69140
  falling back to REST when only a token is available. Jira is REST-only
69127
69141
  --max-tickets <n> tickets: refuse to file more than n tickets in one run (default 200)
@@ -69325,6 +69339,7 @@ var VALUE_FLAGS2 = /* @__PURE__ */ new Set([
69325
69339
  "runner",
69326
69340
  "grain",
69327
69341
  "max-budget-usd",
69342
+ "effort",
69328
69343
  "timeout",
69329
69344
  "concurrency",
69330
69345
  "out",
@@ -71460,6 +71475,15 @@ async function cmdJudge(p) {
71460
71475
  console.error(`ultra11y judge: --grain '${grain}' is not a grain \u2014 expected 'batch' or 'criterion'.`);
71461
71476
  return 2;
71462
71477
  }
71478
+ const effort = typeof p.flags.effort === "string" ? p.flags.effort : void 0;
71479
+ if (effort !== void 0 && !EFFORT_LEVELS.includes(effort)) {
71480
+ console.error(`ultra11y judge: --effort '${effort}' is not a level \u2014 expected ${EFFORT_LEVELS.map((l) => `'${l}'`).join(", ")}.`);
71481
+ return 2;
71482
+ }
71483
+ if (effort !== void 0 && runner !== "cli") {
71484
+ console.error("ultra11y judge: --effort applies to `--runner cli` only \u2014 the Messages API has no session to set an effort on.");
71485
+ return 2;
71486
+ }
71463
71487
  const key2 = typeof p.flags["api-key"] === "string" && p.flags["api-key"] ? p.flags["api-key"] : apiKeyFromEnv();
71464
71488
  if (!key2 && runner === "api") {
71465
71489
  console.error(
@@ -71533,6 +71557,7 @@ async function cmdJudge(p) {
71533
71557
  // requests do. `--concurrency` overrides it for a runner with room.
71534
71558
  concurrency: runner === "cli" ? cliConcurrency : void 0,
71535
71559
  maxBudgetUsd: Number.isFinite(maxBudgetUsd) ? maxBudgetUsd : void 0,
71560
+ effort,
71536
71561
  timeoutMs: Number.isFinite(timeoutMs) ? timeoutMs : void 0,
71537
71562
  onCost: (usd) => {
71538
71563
  spent += usd;
@@ -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.33.1
7
+ version: 5.34.1
8
8
  ---
9
9
 
10
10
  # ultra11y — audit WCAG 2.2 AA and write accessible markup
@@ -6,7 +6,7 @@ import { join as join52, relative as relative5, 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.33.1";
9
+ var VERSION = "5.34.1";
10
10
  var SCHEMA_VERSION = 2;
11
11
  var INAPPLICABLE_STATUS = "C";
12
12
 
@@ -2293,7 +2293,7 @@ var wcag_universe_default = {
2293
2293
  wcagVersion: "2.2",
2294
2294
  source: "https://www.w3.org/TR/WCAG22/",
2295
2295
  criteriaSource: "https://github.com/w3c/wcag",
2296
- 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-24 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).",
2296
+ 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-25 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).",
2297
2297
  criteria: [
2298
2298
  {
2299
2299
  id: "1.1.1",
@@ -59367,6 +59367,12 @@ function auditFiles(audit2, cwd) {
59367
59367
  return /* @__PURE__ */ new Set();
59368
59368
  }
59369
59369
  }
59370
+ function offHarvestHint(criteriaId, evidence) {
59371
+ const anchors = [...new Set(evidence.map((e) => e.selector?.trim() || `${e.file}:${e.line}`).filter(Boolean))];
59372
+ const shown = anchors.slice(0, 4).join(", ");
59373
+ const rest = anchors.length > 4 ? `, +${anchors.length - 4}` : "";
59374
+ return anchors.length ? ` \u2014 this anchor is not among the evidence harvested for ${criteriaId}, so the snippet was verified literally rather than vouched for by the harvest. Cite one of: ${shown}${rest}.` : ` \u2014 ${criteriaId} was harvested no evidence, so nothing can vouch for a transcription here. If the criterion has no subject in scope, the verdict is NA with a justification and no citation to ground.`;
59375
+ }
59370
59376
  function tagOf2(x) {
59371
59377
  const fromSnippet = /<\s*([a-zA-Z][\w-]*)/.exec(x.snippet ?? "");
59372
59378
  if (fromSnippet) return fromSnippet[1].toLowerCase();
@@ -59538,8 +59544,8 @@ function applyAdjudication(audit2, adj, opts = {}) {
59538
59544
  }
59539
59545
  return engineNcCache.get(key2) ?? EMPTY_IDS;
59540
59546
  };
59541
- const toGround = (criteriaId, g, fallback) => {
59542
- const entry = { g, ...fallback ? { fallback } : {} };
59547
+ const toGround = (criteriaId, g, fallback, offHarvest) => {
59548
+ const entry = { g, ...fallback ? { fallback } : {}, ...offHarvest ? { offHarvest } : {} };
59543
59549
  const list = groundInputs.get(criteriaId);
59544
59550
  if (list) list.push(entry);
59545
59551
  else groundInputs.set(criteriaId, [entry]);
@@ -59584,7 +59590,7 @@ function applyAdjudication(audit2, adj, opts = {}) {
59584
59590
  anchor.representative ? { file: anchor.at.file, line: anchor.at.line, selector: anchor.at.selector ?? c2.selector, snippet: anchor.at.snippet } : { file: c2.file, line: c2.line, selector: anchor.at.selector ?? c2.selector }
59585
59591
  );
59586
59592
  } else {
59587
- toGround(it.criteriaId, cite);
59593
+ toGround(it.criteriaId, cite, void 0, anchor ? void 0 : offHarvestHint(it.criteriaId, it.evidence));
59588
59594
  }
59589
59595
  }
59590
59596
  }
@@ -59644,7 +59650,7 @@ function applyAdjudication(audit2, adj, opts = {}) {
59644
59650
  }
59645
59651
  const grounding = { grounded: 0, moved: 0, failed: 0, issues: [] };
59646
59652
  for (const [criteriaId, inputs] of groundInputs) {
59647
- for (const { g, fallback } of inputs) {
59653
+ for (const { g, fallback, offHarvest } of inputs) {
59648
59654
  let r = groundFinding(g, { cwd: opts.cwd });
59649
59655
  if (!r.ok && fallback) {
59650
59656
  const viaAnchor = groundFinding(fallback, { cwd: opts.cwd });
@@ -59656,8 +59662,9 @@ function applyAdjudication(audit2, adj, opts = {}) {
59656
59662
  } else {
59657
59663
  grounding.failed++;
59658
59664
  if (r.issue) {
59659
- grounding.issues.push(r.issue);
59660
- blame(criteriaId, r.issue);
59665
+ const issue = offHarvest && r.issue.startsWith("cited snippet not found") ? `${r.issue}${offHarvest}` : r.issue;
59666
+ grounding.issues.push(issue);
59667
+ blame(criteriaId, issue);
59661
59668
  }
59662
59669
  }
59663
59670
  }
@@ -60378,7 +60385,9 @@ var VERDICT_KINDS = `Rule it (the apply gate is FAIL-CLOSED \u2014 a verdict mis
60378
60385
  - \`NC\` (non-conforming) \u2014 REQUIRES \`findings\`: at least one groundable \`{ file, line, selector?, message, snippet?, severity?, normativeRef }\` pointing at REAL source. The fold re-grounds every finding; an invented file:line is rejected, and so is a finding with no \`file\` at all. \`normativeRef\` MUST cite the precise failed test \u2014 under a country standard, one of the criterion's OWN numbered tests, listed in its brief under \xAB tests to rule on \xBB. A WCAG id looks alike, denotes an unrelated test, and is rejected.
60379
60386
  - \`NA\` (not applicable) \u2014 REQUIRES \`justification\`, AND \`citations[]\` whenever evidence WAS presented, to say which of those items fall outside the criterion's scope.
60380
60387
  - \`manual\` (still undecidable) \u2014 REQUIRES \`reason\`: \`needs-rendered-dom\` (only a rendered DOM can decide it, and no capture in this run carries its subject) or \`undecidable\` (the evidence cannot settle it either way).`;
60381
- var ABSENCE_RULE = `AN NC SHAPED LIKE AN ABSENCE IS STILL ANCHORED. \xAB No second navigation system \xBB, \xAB no search engine \xBB, \xAB no error message suggests the expected format \xBB \u2014 an absence is OBSERVED somewhere: cite the element and the page you observed it on. And when the criterion's subject exists nowhere in the audited scope, the verdict is \`NA\` with its justification, never \`NC\`.`;
60388
+ var ABSENCE_RULE = `AN NC SHAPED LIKE AN ABSENCE IS STILL ANCHORED. \xAB No second navigation system \xBB, \xAB no search engine \xBB, \xAB no error message suggests the expected format \xBB \u2014 an absence is OBSERVED somewhere: cite the element and the page you observed it on. And when the criterion's subject exists nowhere in the audited scope, the verdict is \`NA\` with its justification, never \`NC\`.
60389
+
60390
+ AND CITE FROM THIS CRITERION'S OWN ANCHORS, NOT THE THING YOU ARE RULING OUT. An absence pulls you toward the element you are arguing ABOUT \u2014 the search form that is not a site search engine, the menu that is not a second navigation system \u2014 and that element is, precisely because it is off-topic, absent from what this criterion was harvested. A citation on one of the criterion's own anchors is vouched for by the harvest; one outside it has its snippet verified LITERALLY, character for character, and a retyping then fails. Cite the region you inspected \u2014 the \`header\`, the \`nav\`, the \`footer\` the brief listed \u2014 and say in the justification what you did not find in it.`;
60382
60391
  var CAPTURE_RULE = `THE RENDERED PAGE MAY BE ON DISK. When a criterion's evidence is anchored under \`.ultra11y/pages/<id>/\`, the browser already ran: \`dom.html\`, \`styles.json\`, \`boxes.json\`, \`axtree.json\` and \`screen.png\` are there to read. \`needs-rendered-dom\` is refused on such a criterion \u2014 decide it from those files, or answer \`undecidable\` and say what the capture does not settle.`;
60383
60392
  var NEVER_GUESS_RULE = `Never guess. A criterion you cannot decide from real evidence stays \`manual\` with its reason \u2014 that is a valid, honest verdict, and it is worth more than a verdict the gate throws away.`;
60384
60393
  var SCOPE_RULE = `Rule ONLY on the criteria presented. Never introduce another \u2014 a verdict for a criterion nobody asked about is dropped, and under the fold it would otherwise overwrite what the deterministic engine already decided.`;
@@ -60588,6 +60597,7 @@ function applyRawVerdicts(items, verdicts) {
60588
60597
  var ALLOWED_TOOLS = "Read,Grep,Glob";
60589
60598
  var MAX_ATTEMPTS2 = 4;
60590
60599
  var DEFAULT_CLI_MODEL = "claude-haiku-4-5-20251001";
60600
+ var EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
60591
60601
  var DEFAULT_TIMEOUT_MS2 = 10 * 6e4;
60592
60602
  function realSpawn(argv, input, timeoutMs) {
60593
60603
  return new Promise((resolve17, reject) => {
@@ -60664,6 +60674,7 @@ function cliArgv(opts, items) {
60664
60674
  opts.model ?? DEFAULT_CLI_MODEL
60665
60675
  ];
60666
60676
  if (opts.maxBudgetUsd !== void 0) argv.push("--max-budget-usd", String(opts.maxBudgetUsd));
60677
+ if (opts.effort) argv.push("--effort", opts.effort);
60667
60678
  return argv;
60668
60679
  }
60669
60680
  function verdictsFromText(text) {
@@ -60726,6 +60737,7 @@ async function refuteBatchCli(items, prompt, opts, lang = "en") {
60726
60737
  opts.model ?? DEFAULT_CLI_MODEL
60727
60738
  ];
60728
60739
  if (opts.maxBudgetUsd !== void 0) argv.push("--max-budget-usd", String(opts.maxBudgetUsd));
60740
+ if (opts.effort) argv.push("--effort", opts.effort);
60729
60741
  return runCli2(argv, prompt, opts, (env) => {
60730
60742
  const structured = env.structured_output?.verdicts;
60731
60743
  const raw = Array.isArray(structured) ? structured : verdictsArrayFromText(env.result ?? "");
@@ -68889,7 +68901,7 @@ Usage:
68889
68901
  ultra11y orchestrate --run <dir> [--phase adjudicate|verify-report] [--eco] [--list] [--lang auto|en|fr]
68890
68902
  ultra11y fix <globs\u2026 | -> [--write] [--iterate] [--changed | --since <ref> | --staged] [--safe] [--include <glob>] [--exclude <glob>] [--ext <list>] [--only <ids>] [--jsx] [--json] [--lang auto|en|fr]
68891
68903
  ultra11y init [--hook] [--ci] [--baseline] [--fail-on blocking|major|minor]
68892
- ultra11y judge --in <audit.json> [--standard <pack>] [--runner api|cli] [--grain batch|criterion] [--max <n>] [--model <id>] [--max-budget-usd <n>] [--timeout <s>] [--out <dir>] [--apply] (adjudicate the manual criteria with a model \u2014 api needs ANTHROPIC_API_KEY; cli runs the Claude Code CLI and takes CLAUDE_CODE_OAUTH_TOKEN too)
68904
+ ultra11y judge --in <audit.json> [--standard <pack>] [--runner api|cli] [--grain batch|criterion] [--max <n>] [--model <id>] [--effort <level>] [--max-budget-usd <n>] [--timeout <s>] [--out <dir>] [--apply] (adjudicate the manual criteria with a model \u2014 api needs ANTHROPIC_API_KEY; cli runs the Claude Code CLI and takes CLAUDE_CODE_OAUTH_TOKEN too)
68893
68905
  ultra11y pack check <pack.json> [--guidance <g.json>] [--json] | pack scaffold
68894
68906
  ultra11y scan <url|file\u2026> [--runtime auto|local|docker] [--cwd <dir>] [--storage-state <file>] [--no-interact] [--interact-clicks] [--no-snapshot] [--merge <audit.json>] [--out <dir>] [--json]
68895
68907
  ultra11y scan --sample [--runtime \u2026] [--cwd <dir>] [--storage-state <file>] [--merge <audit.json>] [--json] (scan the .ultra11yrc.json page sample)
@@ -69122,6 +69134,8 @@ Options:
69122
69134
  --provider <id> tickets: auto|github|gitlab|jira. 'auto' reads ULTRA11Y_TICKET_PROVIDER,
69123
69135
  then .ultra11yrc.json, then the git remote (Jira is never auto-detected)
69124
69136
  --grain <mode> tickets: what one ticket is \u2014 criterion (default) | page | page-criterion | single | file
69137
+ --effort <level> judge (--runner cli): reasoning effort per invocation \u2014 low | medium |
69138
+ high | xhigh | max. Unset leaves the CLI's own default
69125
69139
  --transport <t> tickets: auto|cli|rest (mcp: stdio|http). 'auto' prefers the CLI (gh/glab),
69126
69140
  falling back to REST when only a token is available. Jira is REST-only
69127
69141
  --max-tickets <n> tickets: refuse to file more than n tickets in one run (default 200)
@@ -69325,6 +69339,7 @@ var VALUE_FLAGS2 = /* @__PURE__ */ new Set([
69325
69339
  "runner",
69326
69340
  "grain",
69327
69341
  "max-budget-usd",
69342
+ "effort",
69328
69343
  "timeout",
69329
69344
  "concurrency",
69330
69345
  "out",
@@ -71460,6 +71475,15 @@ async function cmdJudge(p) {
71460
71475
  console.error(`ultra11y judge: --grain '${grain}' is not a grain \u2014 expected 'batch' or 'criterion'.`);
71461
71476
  return 2;
71462
71477
  }
71478
+ const effort = typeof p.flags.effort === "string" ? p.flags.effort : void 0;
71479
+ if (effort !== void 0 && !EFFORT_LEVELS.includes(effort)) {
71480
+ console.error(`ultra11y judge: --effort '${effort}' is not a level \u2014 expected ${EFFORT_LEVELS.map((l) => `'${l}'`).join(", ")}.`);
71481
+ return 2;
71482
+ }
71483
+ if (effort !== void 0 && runner !== "cli") {
71484
+ console.error("ultra11y judge: --effort applies to `--runner cli` only \u2014 the Messages API has no session to set an effort on.");
71485
+ return 2;
71486
+ }
71463
71487
  const key2 = typeof p.flags["api-key"] === "string" && p.flags["api-key"] ? p.flags["api-key"] : apiKeyFromEnv();
71464
71488
  if (!key2 && runner === "api") {
71465
71489
  console.error(
@@ -71533,6 +71557,7 @@ async function cmdJudge(p) {
71533
71557
  // requests do. `--concurrency` overrides it for a runner with room.
71534
71558
  concurrency: runner === "cli" ? cliConcurrency : void 0,
71535
71559
  maxBudgetUsd: Number.isFinite(maxBudgetUsd) ? maxBudgetUsd : void 0,
71560
+ effort,
71536
71561
  timeoutMs: Number.isFinite(timeoutMs) ? timeoutMs : void 0,
71537
71562
  onCost: (usd) => {
71538
71563
  spent += usd;
package/src/adjudicate.ts CHANGED
@@ -484,6 +484,22 @@ function auditFiles(audit: AuditResult, cwd?: string): Set<string> {
484
484
  * criterion was never given. And it is only half the gate — `groundFinding` still has to find
485
485
  * the cited content in the real source, so a plausible-looking file:line in the right
486
486
  * neighbourhood proves nothing on its own. */
487
+ /** Why a citation was held to the literal check, and what to cite instead.
488
+ *
489
+ * Appended to a `cited snippet not found` refusal when the citation missed THIS criterion's
490
+ * harvest. Without it the message names a symptom — a transcription that did not match — and
491
+ * hides the cause, which is that the citation left the evidence the criterion was shown. The
492
+ * two are fixed differently: one asks the adjudicator to copy more carefully, the other asks
493
+ * it to cite something else entirely. */
494
+ export function offHarvestHint(criteriaId: string, evidence: Evidence[]): string {
495
+ const anchors = [...new Set(evidence.map((e) => e.selector?.trim() || `${e.file}:${e.line}`).filter(Boolean))];
496
+ const shown = anchors.slice(0, 4).join(", ");
497
+ const rest = anchors.length > 4 ? `, +${anchors.length - 4}` : "";
498
+ return anchors.length
499
+ ? ` — this anchor is not among the evidence harvested for ${criteriaId}, so the snippet was verified literally rather than vouched for by the harvest. Cite one of: ${shown}${rest}.`
500
+ : ` — ${criteriaId} was harvested no evidence, so nothing can vouch for a transcription here. If the criterion has no subject in scope, the verdict is NA with a justification and no citation to ground.`;
501
+ }
502
+
487
503
  /** The tag a citation or an anchor is about, from its snippet first and its selector second.
488
504
  * Lowercased — HTML tag names are case-insensitive. Undefined when neither says. */
489
505
  function tagOf(x: { snippet?: string; selector?: string }): string | undefined {
@@ -789,7 +805,7 @@ export function applyAdjudication(
789
805
  // flat list) for the same reason as `blame`: a failed citation has to condemn its own
790
806
  // criterion and no other.
791
807
  type Ground = { file: string; line: number; selector?: string; snippet?: string };
792
- const groundInputs = new Map<string, { g: Ground; fallback?: Ground }[]>();
808
+ const groundInputs = new Map<string, { g: Ground; fallback?: Ground; offHarvest?: string }[]>();
793
809
  // Memoised: only a citation that missed its criterion's own anchors ever asks for it.
794
810
  let scopeCache: Set<string> | undefined;
795
811
  const { citationDrift } = adjudicationLimits(opts.cwd);
@@ -819,8 +835,9 @@ export function applyAdjudication(
819
835
  criteriaId: string,
820
836
  g: { file: string; line: number; selector?: string; snippet?: string },
821
837
  fallback?: { file: string; line: number; selector?: string; snippet?: string },
838
+ offHarvest?: string,
822
839
  ) => {
823
- const entry = { g, ...(fallback ? { fallback } : {}) };
840
+ const entry = { g, ...(fallback ? { fallback } : {}), ...(offHarvest ? { offHarvest } : {}) };
824
841
  const list = groundInputs.get(criteriaId);
825
842
  if (list) list.push(entry);
826
843
  else groundInputs.set(criteriaId, [entry]);
@@ -948,7 +965,30 @@ export function applyAdjudication(
948
965
  : { file: c.file, line: c.line, selector: anchor.at.selector ?? c.selector },
949
966
  );
950
967
  } else {
951
- toGround(it.criteriaId, cite);
968
+ // WHY THE STRICT PATH APPLIED, said at the point it applies.
969
+ //
970
+ // The citation belongs (it is in a file this audit read) but it is not on an anchor
971
+ // THIS criterion was shown, so the paragraph above cannot let the harvest vouch for
972
+ // the transcription — and the refusal that follows says only `cited snippet not
973
+ // found`, which reads as « you mistyped » when the actionable fact is « you cited
974
+ // outside this criterion's evidence, so your retyping was verified literally ».
975
+ //
976
+ // Measured: that message sent a reader diagnosing RGAA 12.5 to the wrong conclusion
977
+ // — a fold bug — with this source open. The risk is not the lost criterion, it is
978
+ // the « fix » that would follow: relaxing the strict path is exactly the hole the
979
+ // two bounds above exist to close. So the cause travels with the refusal.
980
+ //
981
+ // It bites hardest where the criterion's subject is an ABSENCE. 12.1 and 12.5 are
982
+ // the repeat offenders the adjudicator prompt already names: asked to prove there is
983
+ // no search engine, a model cites the search form it is arguing about — which is, by
984
+ // definition, not among the anchors harvested for a criterion about navigation.
985
+ // ONLY when the citation is genuinely off-harvest. Landing on an anchor and then
986
+ // claiming a different KIND of element (an <img> cited as a <video>) also reaches
987
+ // this branch, and telling that reader « not among the harvested evidence » would
988
+ // be false: the anchor is right there, it is the element that is wrong. That case
989
+ // keeps the bare refusal, which already says the honest thing — this markup is not
990
+ // in the file.
991
+ toGround(it.criteriaId, cite, undefined, anchor ? undefined : offHarvestHint(it.criteriaId, it.evidence));
952
992
  }
953
993
  }
954
994
  }
@@ -1069,7 +1109,7 @@ export function applyAdjudication(
1069
1109
  // flat `groundItems` produced.
1070
1110
  const grounding: GroundingSummary = { grounded: 0, moved: 0, failed: 0, issues: [] };
1071
1111
  for (const [criteriaId, inputs] of groundInputs) {
1072
- for (const { g, fallback } of inputs) {
1112
+ for (const { g, fallback, offHarvest } of inputs) {
1073
1113
  let r = groundFinding(g, { cwd: opts.cwd });
1074
1114
  // The harvested anchor vouches for a citation the file could not confirm on its own —
1075
1115
  // an adjudicator's retyping of an element it did read. `moved` either way: what
@@ -1084,8 +1124,12 @@ export function applyAdjudication(
1084
1124
  } else {
1085
1125
  grounding.failed++;
1086
1126
  if (r.issue) {
1087
- grounding.issues.push(r.issue);
1088
- blame(criteriaId, r.issue);
1127
+ // The cause travels with the refusal, and only when it IS the cause: a citation that
1128
+ // never reached the literal check (a missing file, a line out of range) is not an
1129
+ // off-harvest problem, and saying so would send the reader chasing the wrong thing.
1130
+ const issue = offHarvest && r.issue.startsWith("cited snippet not found") ? `${r.issue}${offHarvest}` : r.issue;
1131
+ grounding.issues.push(issue);
1132
+ blame(criteriaId, issue);
1089
1133
  }
1090
1134
  }
1091
1135
  }
package/src/agent-cli.ts CHANGED
@@ -39,6 +39,12 @@ const MAX_ATTEMPTS = 4;
39
39
  * states a model rather than inheriting one. */
40
40
  export const DEFAULT_CLI_MODEL = "claude-haiku-4-5-20251001";
41
41
 
42
+ /** The levels `claude --effort` accepts. Enumerated here so a caller's typo is refused with a
43
+ * message instead of forwarded: this CLI treats an unrecognised value the way it treats an
44
+ * unrecognised flag — silently — and a ceiling that is silently nothing is the exact failure
45
+ * `--max-turns` already cost this repo once. */
46
+ export const EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
47
+
42
48
  /** What `claude -p --output-format json` puts on stdout. Only the fields this reads. */
43
49
  interface CliEnvelope {
44
50
  /** Where `--json-schema` puts the validated object. Present since 2.1.x; `result` carries
@@ -168,6 +174,9 @@ export function cliArgv(opts: LlmOptions, items: AdjudicationItem[]): string[] {
168
174
  // a turn budget and be an unbounded run. The bounds that are real: `--max-budget-usd`, which
169
175
  // the CLI documents, and the wall-clock kill in `judgeBatchCli`, which is ours.
170
176
  if (opts.maxBudgetUsd !== undefined) argv.push("--max-budget-usd", String(opts.maxBudgetUsd));
177
+ // `--effort` IS documented by this CLI, unlike `--max-turns` above — so it is passed, and a
178
+ // caller can raise how hard the model thinks without also having to change the model.
179
+ if (opts.effort) argv.push("--effort", opts.effort);
171
180
  return argv;
172
181
  }
173
182
 
@@ -274,6 +283,9 @@ export async function refuteBatchCli(items: VerifyItem[], prompt: string, opts:
274
283
  opts.model ?? DEFAULT_CLI_MODEL,
275
284
  ];
276
285
  if (opts.maxBudgetUsd !== undefined) argv.push("--max-budget-usd", String(opts.maxBudgetUsd));
286
+ // `--effort` IS documented by this CLI, unlike `--max-turns` above — so it is passed, and a
287
+ // caller can raise how hard the model thinks without also having to change the model.
288
+ if (opts.effort) argv.push("--effort", opts.effort);
277
289
  return runCli(argv, prompt, opts, (env) => {
278
290
  const structured = (env.structured_output as { verdicts?: unknown } | undefined)?.verdicts;
279
291
  const raw = Array.isArray(structured) ? structured : verdictsArrayFromText(env.result ?? "");
package/src/cli.ts CHANGED
@@ -64,7 +64,7 @@ import {
64
64
  type AdjudicationFile,
65
65
  } from "./adjudicate.js";
66
66
  import { entriesFrom, isLedger, ledgerPath, mergeLedger, readLedger, replayLedger, unreadableCaptures, type VerdictLedger, writeLedger } from "./ledger.js";
67
- import { DEFAULT_CLI_MODEL, judgeBatchCli, refuteBatchCli } from "./agent-cli.js";
67
+ import { DEFAULT_CLI_MODEL, EFFORT_LEVELS, judgeBatchCli, refuteBatchCli } from "./agent-cli.js";
68
68
  import { BATCH_SIZE, apiKeyFromEnv, applyRawVerdicts, judgeAll, modelFromEnv, type LlmOptions } from "./llm.js";
69
69
  import { runScan, runScanMany, runCrawlScan, runSampleScan, mergeDynamic, mergeSnapshotAudit, cleanDynamic, dockerAvailable } from "./scan.js";
70
70
  import { runScanLocal, runScanManyLocal, runCrawlScanLocal, runSampleScanLocal, localAvailable, localTierStatus } from "./scan-local.js";
@@ -142,7 +142,7 @@ Usage:
142
142
  ultra11y orchestrate --run <dir> [--phase adjudicate|verify-report] [--eco] [--list] [--lang auto|en|fr]
143
143
  ultra11y fix <globs… | -> [--write] [--iterate] [--changed | --since <ref> | --staged] [--safe] [--include <glob>] [--exclude <glob>] [--ext <list>] [--only <ids>] [--jsx] [--json] [--lang auto|en|fr]
144
144
  ultra11y init [--hook] [--ci] [--baseline] [--fail-on blocking|major|minor]
145
- ultra11y judge --in <audit.json> [--standard <pack>] [--runner api|cli] [--grain batch|criterion] [--max <n>] [--model <id>] [--max-budget-usd <n>] [--timeout <s>] [--out <dir>] [--apply] (adjudicate the manual criteria with a model — api needs ANTHROPIC_API_KEY; cli runs the Claude Code CLI and takes CLAUDE_CODE_OAUTH_TOKEN too)
145
+ ultra11y judge --in <audit.json> [--standard <pack>] [--runner api|cli] [--grain batch|criterion] [--max <n>] [--model <id>] [--effort <level>] [--max-budget-usd <n>] [--timeout <s>] [--out <dir>] [--apply] (adjudicate the manual criteria with a model — api needs ANTHROPIC_API_KEY; cli runs the Claude Code CLI and takes CLAUDE_CODE_OAUTH_TOKEN too)
146
146
  ultra11y pack check <pack.json> [--guidance <g.json>] [--json] | pack scaffold
147
147
  ultra11y scan <url|file…> [--runtime auto|local|docker] [--cwd <dir>] [--storage-state <file>] [--no-interact] [--interact-clicks] [--no-snapshot] [--merge <audit.json>] [--out <dir>] [--json]
148
148
  ultra11y scan --sample [--runtime …] [--cwd <dir>] [--storage-state <file>] [--merge <audit.json>] [--json] (scan the .ultra11yrc.json page sample)
@@ -375,6 +375,8 @@ Options:
375
375
  --provider <id> tickets: auto|github|gitlab|jira. 'auto' reads ULTRA11Y_TICKET_PROVIDER,
376
376
  then .ultra11yrc.json, then the git remote (Jira is never auto-detected)
377
377
  --grain <mode> tickets: what one ticket is — criterion (default) | page | page-criterion | single | file
378
+ --effort <level> judge (--runner cli): reasoning effort per invocation — low | medium |
379
+ high | xhigh | max. Unset leaves the CLI's own default
378
380
  --transport <t> tickets: auto|cli|rest (mcp: stdio|http). 'auto' prefers the CLI (gh/glab),
379
381
  falling back to REST when only a token is available. Jira is REST-only
380
382
  --max-tickets <n> tickets: refuse to file more than n tickets in one run (default 200)
@@ -582,6 +584,7 @@ const VALUE_FLAGS = new Set([
582
584
  "runner",
583
585
  "grain",
584
586
  "max-budget-usd",
587
+ "effort",
585
588
  "timeout",
586
589
  "concurrency",
587
590
  "out",
@@ -3561,6 +3564,21 @@ async function cmdJudge(p: ParsedArgs): Promise<number> {
3561
3564
  return 2;
3562
3565
  }
3563
3566
 
3567
+ // HOW HARD THE MODEL IS ASKED TO THINK, which on this worklist is not a detail: what reaches
3568
+ // this tier is what no engine could decide. Validated rather than forwarded, because the CLI
3569
+ // SWALLOWS AN UNKNOWN VALUE the same way it swallows an unknown flag — `--effort hgih` would
3570
+ // read as a setting and be nothing at all, which is the failure mode this repo already ate
3571
+ // once with `--max-turns`. Refused here, loudly, while the message can still reach a human.
3572
+ const effort = typeof p.flags.effort === "string" ? (p.flags.effort as string) : undefined;
3573
+ if (effort !== undefined && !EFFORT_LEVELS.includes(effort)) {
3574
+ console.error(`ultra11y judge: --effort '${effort}' is not a level — expected ${EFFORT_LEVELS.map((l) => `'${l}'`).join(", ")}.`);
3575
+ return 2;
3576
+ }
3577
+ if (effort !== undefined && runner !== "cli") {
3578
+ console.error("ultra11y judge: --effort applies to `--runner cli` only — the Messages API has no session to set an effort on.");
3579
+ return 2;
3580
+ }
3581
+
3564
3582
  const key = typeof p.flags["api-key"] === "string" && p.flags["api-key"] ? (p.flags["api-key"] as string) : apiKeyFromEnv();
3565
3583
  if (!key && runner === "api") {
3566
3584
  console.error(
@@ -3684,6 +3702,7 @@ async function cmdJudge(p: ParsedArgs): Promise<number> {
3684
3702
  // requests do. `--concurrency` overrides it for a runner with room.
3685
3703
  concurrency: runner === "cli" ? cliConcurrency : undefined,
3686
3704
  maxBudgetUsd: Number.isFinite(maxBudgetUsd) ? maxBudgetUsd : undefined,
3705
+ effort,
3687
3706
  timeoutMs: Number.isFinite(timeoutMs) ? timeoutMs : undefined,
3688
3707
  onCost: (usd) => {
3689
3708
  spent += usd;
@@ -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-24 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-25 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
@@ -71,6 +71,15 @@ export interface LlmOptions {
71
71
  /** Dollar ceiling handed to the CLI backend — the bound that survives whatever a turn
72
72
  * happens to cost, which is what a CI job actually wants to cap. */
73
73
  maxBudgetUsd?: number;
74
+ /** Reasoning effort handed to the CLI backend (`--effort`: low, medium, high, xhigh, max).
75
+ *
76
+ * The criteria this tier rules on are the ones no engine can decide — alt relevance, link
77
+ * purpose in context, reading order — so how hard the model is asked to think is a real
78
+ * lever on the verdict, and one a caller should be able to move without also changing the
79
+ * model. Unlike `--max-turns`, `--effort` IS a flag of this CLI, so it is passed rather
80
+ * than refused. The Messages backend ignores it: effort is a session notion there, not a
81
+ * request parameter. */
82
+ effort?: string;
74
83
  /** Called with each invocation's real cost, so a run can report what it spent instead of
75
84
  * leaving the reader to find it in a provider dashboard. */
76
85
  onCost?: (usd: number) => void;
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.33.1";
3
+ export const VERSION = "5.34.1";
4
4
  export const SCHEMA_VERSION = 2;
5
5
 
6
6
  // Lang = the UI FRAME's language — the `L` localization tables each module (report,
@@ -27,7 +27,9 @@ export const VERDICT_KINDS = `Rule it (the apply gate is FAIL-CLOSED — a verdi
27
27
  - \`manual\` (still undecidable) — REQUIRES \`reason\`: \`needs-rendered-dom\` (only a rendered DOM can decide it, and no capture in this run carries its subject) or \`undecidable\` (the evidence cannot settle it either way).`;
28
28
 
29
29
  /** An absence is observed somewhere. Measured: 12.1 and 12.5 lost to this on one run. */
30
- export const ABSENCE_RULE = `AN NC SHAPED LIKE AN ABSENCE IS STILL ANCHORED. « No second navigation system », « no search engine », « no error message suggests the expected format » — an absence is OBSERVED somewhere: cite the element and the page you observed it on. And when the criterion's subject exists nowhere in the audited scope, the verdict is \`NA\` with its justification, never \`NC\`.`;
30
+ export const ABSENCE_RULE = `AN NC SHAPED LIKE AN ABSENCE IS STILL ANCHORED. « No second navigation system », « no search engine », « no error message suggests the expected format » — an absence is OBSERVED somewhere: cite the element and the page you observed it on. And when the criterion's subject exists nowhere in the audited scope, the verdict is \`NA\` with its justification, never \`NC\`.
31
+
32
+ AND CITE FROM THIS CRITERION'S OWN ANCHORS, NOT THE THING YOU ARE RULING OUT. An absence pulls you toward the element you are arguing ABOUT — the search form that is not a site search engine, the menu that is not a second navigation system — and that element is, precisely because it is off-topic, absent from what this criterion was harvested. A citation on one of the criterion's own anchors is vouched for by the harvest; one outside it has its snippet verified LITERALLY, character for character, and a retyping then fails. Cite the region you inspected — the \`header\`, the \`nav\`, the \`footer\` the brief listed — and say in the justification what you did not find in it.`;
31
33
 
32
34
  /** `needs-rendered-dom` is a claim about the RUN, not a way out of a hard criterion. */
33
35
  export const CAPTURE_RULE = `THE RENDERED PAGE MAY BE ON DISK. When a criterion's evidence is anchored under \`.ultra11y/pages/<id>/\`, the browser already ran: \`dom.html\`, \`styles.json\`, \`boxes.json\`, \`axtree.json\` and \`screen.png\` are there to read. \`needs-rendered-dom\` is refused on such a criterion — decide it from those files, or answer \`undecidable\` and say what the capture does not settle.`;