ultra11y 5.39.2 → 5.40.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.
@@ -9,8 +9,8 @@
9
9
  {
10
10
  "name": "ultra11y",
11
11
  "source": "./",
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.39.2",
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",
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
- "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.39.2",
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",
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
- "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.39.2",
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",
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.39.2";
30
+ const ULTRA11Y_PLUGIN_VERSION = "5.40.1";
31
31
 
32
32
  const here = dirname(fileURLToPath(import.meta.url));
33
33
 
package/README.md CHANGED
@@ -523,6 +523,13 @@ node scripts/ultra11y.mjs check --report audits/wcag-YYYY-MM-DD.md # integrity
523
523
 
524
524
  ### A report someone will actually open
525
525
 
526
+ There are two deliberate publication profiles. In an interactive Claude Code audit, the skill
527
+ produces the detailed deliverable: adjudicated Markdown, a printable single-file HTML report,
528
+ per-page HTML/Markdown dossiers, screenshots and annotated evidence. In GitHub Actions,
529
+ `pages-report: compact` with `report`, `html` and `evidence` disabled still gates every criterion
530
+ on every page but uploads only the status Markdown, page JSON, source audit and verdict ledger.
531
+ The CI profile is a gate and handoff; it is not a second remediation report.
532
+
526
533
  `--html` turns the audit into a page: `audits/index.html` as the entry point, plus — from
527
534
  `report` — a detachable single file that prints to PDF. Self-contained — no script, no
528
535
  external asset, nothing pointing outside the directory — and it passes this engine's own
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultra11y",
3
- "version": "5.39.2",
3
+ "version": "5.40.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 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.39.2";
9
+ var VERSION = "5.40.1";
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-28 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-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).",
2304
2304
  criteria: [
2305
2305
  {
2306
2306
  id: "1.1.1",
@@ -5473,8 +5473,8 @@ function allIds(doc) {
5473
5473
  }
5474
5474
  function snippet(doc, el, max = 120) {
5475
5475
  const lineStart = doc.lineStarts[el.line - 1] ?? 0;
5476
- let end = doc.source.indexOf("\n", lineStart);
5477
- if (end === -1) end = doc.source.length;
5476
+ const nextLineStart = doc.lineStarts[el.line];
5477
+ const end = nextLineStart === void 0 ? doc.source.length : Math.max(lineStart, nextLineStart - 1);
5478
5478
  const from = Math.min(Math.max(el.start, lineStart), end);
5479
5479
  const tagEnd = doc.source.indexOf(">", from);
5480
5480
  const to = Math.min(from + max, Math.max(Math.min(end, from + max), tagEnd === -1 ? end : tagEnd + 1));
@@ -63277,6 +63277,7 @@ var VERDICT_KINDS = `Rule it (the apply gate is FAIL-CLOSED \u2014 a verdict mis
63277
63277
  - \`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.
63278
63278
  - \`NA\` (not applicable) \u2014 REQUIRES \`justification\`, AND \`citations[]\` whenever evidence WAS presented, to say which of those items fall outside the criterion's scope.
63279
63279
  - \`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).`;
63280
+ var WORKLIST_RULE = `THE WORKLIST IS THE RESIDUAL. Return exactly one verdict for EVERY criterion presented \u2014 never omit an item because the engine already reported a run-wide \`NC\`. The strict page gate deliberately puts such a criterion back on the worklist when page-level cells remain open. It is not surplus and it is not already resolved for this task: inspect the complete evidence and rule it. If the evidence cannot settle it, return \`manual\` with a reason; never leave its verdict blank.`;
63280
63281
  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\`.
63281
63282
 
63282
63283
  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.`;
@@ -63284,7 +63285,7 @@ var CAPTURE_RULE = `THE RENDERED PAGE MAY BE ON DISK. When a criterion's evidenc
63284
63285
  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.`;
63285
63286
  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.`;
63286
63287
  var CONFORMITY_RULE = `A \`C\` WILL BE ATTACKED, exactly as an \`NC\` is. Every conformity you record goes into an adversarial worklist where a second reader opens your citations and asks whether they ESTABLISH the criterion or merely show that its subject exists \u2014 a present \`alt\` is not a relevant \`alt\`, a present \`<title>\` is not a title that describes the page. Cite the evidence that answers the criterion's own question, and when the evidence only proves presence, the honest verdict is \`manual\`.`;
63287
- var TAIL = [ABSENCE_RULE, CAPTURE_RULE, NEVER_GUESS_RULE, SCOPE_RULE, CONFORMITY_RULE];
63288
+ var TAIL = [WORKLIST_RULE, ABSENCE_RULE, CAPTURE_RULE, NEVER_GUESS_RULE, SCOPE_RULE, CONFORMITY_RULE];
63288
63289
  function verdictRulesMd(startAt) {
63289
63290
  const lines = [`${startAt}. ${VERDICT_KINDS}`];
63290
63291
  TAIL.forEach((rule, i2) => lines.push(`${startAt + 1 + i2}. ${rule}`));
@@ -63572,7 +63573,9 @@ function claudeBin() {
63572
63573
  }
63573
63574
  function batchSchema(items) {
63574
63575
  const schema = structuredClone(VERDICT_TOOL.input_schema);
63575
- const id = schema.properties.verdicts.items.properties.criteriaId;
63576
+ const verdicts = schema.properties.verdicts;
63577
+ verdicts.description = `Exactly one verdict for each of these ${items.length} worklist criteria. Do not omit criteria already reported non-conforming run-wide: page-level cells may still be open.`;
63578
+ const id = verdicts.items.properties.criteriaId;
63576
63579
  id.enum = items.map((i2) => i2.criteriaId);
63577
63580
  id.description = `The criterion id EXACTLY as given \u2014 the bare id (e.g. "1.2.1"), never the heading or the title beside it.`;
63578
63581
  return schema;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "source": "https://www.w3.org/Translations/WCAG22-fr/",
3
- "fetchedAt": "2026-08-28",
3
+ "fetchedAt": "2026-08-30",
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-28 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-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).",
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-28",
5
+ "fetchedAt": "2026-08-30",
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-28",
4
+ "fetchedAt": "2026-08-30",
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.39.2
6
+ version: 5.40.1
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.39.2";
9
+ var VERSION = "5.40.1";
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-28 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-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).",
2304
2304
  criteria: [
2305
2305
  {
2306
2306
  id: "1.1.1",
@@ -5473,8 +5473,8 @@ function allIds(doc) {
5473
5473
  }
5474
5474
  function snippet(doc, el, max = 120) {
5475
5475
  const lineStart = doc.lineStarts[el.line - 1] ?? 0;
5476
- let end = doc.source.indexOf("\n", lineStart);
5477
- if (end === -1) end = doc.source.length;
5476
+ const nextLineStart = doc.lineStarts[el.line];
5477
+ const end = nextLineStart === void 0 ? doc.source.length : Math.max(lineStart, nextLineStart - 1);
5478
5478
  const from = Math.min(Math.max(el.start, lineStart), end);
5479
5479
  const tagEnd = doc.source.indexOf(">", from);
5480
5480
  const to = Math.min(from + max, Math.max(Math.min(end, from + max), tagEnd === -1 ? end : tagEnd + 1));
@@ -63277,6 +63277,7 @@ var VERDICT_KINDS = `Rule it (the apply gate is FAIL-CLOSED \u2014 a verdict mis
63277
63277
  - \`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.
63278
63278
  - \`NA\` (not applicable) \u2014 REQUIRES \`justification\`, AND \`citations[]\` whenever evidence WAS presented, to say which of those items fall outside the criterion's scope.
63279
63279
  - \`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).`;
63280
+ var WORKLIST_RULE = `THE WORKLIST IS THE RESIDUAL. Return exactly one verdict for EVERY criterion presented \u2014 never omit an item because the engine already reported a run-wide \`NC\`. The strict page gate deliberately puts such a criterion back on the worklist when page-level cells remain open. It is not surplus and it is not already resolved for this task: inspect the complete evidence and rule it. If the evidence cannot settle it, return \`manual\` with a reason; never leave its verdict blank.`;
63280
63281
  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\`.
63281
63282
 
63282
63283
  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.`;
@@ -63284,7 +63285,7 @@ var CAPTURE_RULE = `THE RENDERED PAGE MAY BE ON DISK. When a criterion's evidenc
63284
63285
  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.`;
63285
63286
  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.`;
63286
63287
  var CONFORMITY_RULE = `A \`C\` WILL BE ATTACKED, exactly as an \`NC\` is. Every conformity you record goes into an adversarial worklist where a second reader opens your citations and asks whether they ESTABLISH the criterion or merely show that its subject exists \u2014 a present \`alt\` is not a relevant \`alt\`, a present \`<title>\` is not a title that describes the page. Cite the evidence that answers the criterion's own question, and when the evidence only proves presence, the honest verdict is \`manual\`.`;
63287
- var TAIL = [ABSENCE_RULE, CAPTURE_RULE, NEVER_GUESS_RULE, SCOPE_RULE, CONFORMITY_RULE];
63288
+ var TAIL = [WORKLIST_RULE, ABSENCE_RULE, CAPTURE_RULE, NEVER_GUESS_RULE, SCOPE_RULE, CONFORMITY_RULE];
63288
63289
  function verdictRulesMd(startAt) {
63289
63290
  const lines = [`${startAt}. ${VERDICT_KINDS}`];
63290
63291
  TAIL.forEach((rule, i2) => lines.push(`${startAt + 1 + i2}. ${rule}`));
@@ -63572,7 +63573,9 @@ function claudeBin() {
63572
63573
  }
63573
63574
  function batchSchema(items) {
63574
63575
  const schema = structuredClone(VERDICT_TOOL.input_schema);
63575
- const id = schema.properties.verdicts.items.properties.criteriaId;
63576
+ const verdicts = schema.properties.verdicts;
63577
+ verdicts.description = `Exactly one verdict for each of these ${items.length} worklist criteria. Do not omit criteria already reported non-conforming run-wide: page-level cells may still be open.`;
63578
+ const id = verdicts.items.properties.criteriaId;
63576
63579
  id.enum = items.map((i2) => i2.criteriaId);
63577
63580
  id.description = `The criterion id EXACTLY as given \u2014 the bare id (e.g. "1.2.1"), never the heading or the title beside it.`;
63578
63581
  return schema;
@@ -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.39.2
6
+ version: 5.40.1
7
7
  ---
8
8
 
9
9
  # ultra11y — audit WCAG 2.2 AA and write accessible markup
@@ -72,6 +72,10 @@ adjudicates. So the engine's clean run is a starting point, never a verdict —
72
72
  > grid to the same bar: a criterion failing on one route is settled for the run and may still
73
73
  > be nobody's verdict on the routes it never fired on, which is what a per-page deliverable is
74
74
  > actually judged on.
75
+ > **The emitted worklist is authoritative.** It may deliberately contain a criterion whose
76
+ > run-wide status is already `NC`, because that failure did not decide the criterion on the
77
+ > other pages. Such an item is not redundant: return one grounded verdict for it (or an
78
+ > explicit `manual` reason), never leave it blank because the global tally looks decided.
75
79
  > 5. **Look the criterion up; never recall it.** `criteria [--standard <pack>] <id>` — or
76
80
  > `ultra11y_criteria` over MCP — returns the criterion's own wording, its **numbered
77
81
  > tests**, and the glossary definitions those tests cite. RGAA 8.3 is not "the page needs a
@@ -119,6 +123,7 @@ Domain knowledge first, then the tooling. Read the one that matches the question
119
123
  | Running the tool | |
120
124
  |---|---|
121
125
  | `references/runbook.md` | **The reliable run**: the one recipe, the transport to use and why, and what a pass costs. Read this before paying for an adjudication |
126
+ | `references/claude-code-report.md` | Claude Code's detailed publication route: rendered scope, direct adjudication, adversarial verification, Markdown + HTML + per-page evidence |
122
127
  | `references/audit.md` | The audit loop end to end, and the normative page sample |
123
128
  | `references/judgment.md` | The adjudication + refutation worklists, verdict vocabulary, fail-closed fold |
124
129
  | `references/verify.md` | The `check`/`verify` gates |
@@ -144,6 +149,11 @@ Domain knowledge first, then the tooling. Read the one that matches the question
144
149
 
145
150
  ## Choose by situation
146
151
 
152
+ - **"Produce a detailed audit/report in Claude Code"** → use the active Claude session as the
153
+ auditor, close the rendered and judgment criteria, challenge its claims, then publish the
154
+ dated Markdown report, printable HTML and evidenced per-page dossiers. Read
155
+ **`references/claude-code-report.md`**. This is the interactive publication profile; finish
156
+ with its full deliverable set. The compact summary is the CI branch below.
147
157
  - **"Audit / compliance report"** → `node scripts/ultra11y.mjs audit … --json`, then
148
158
  `report` (synthesis table + one **auditor conformance block** per NC criterion — same
149
159
  block `prd` and `tickets` use), then `check`; read **`references/audit.md`**.
@@ -38,6 +38,30 @@ Page by page, with a real browser, in the same step:
38
38
  comment-kind: pages # the page-by-page grid, under its own sticky marker
39
39
  ```
40
40
 
41
+ For an exhaustive certification run whose output must stay small, use the compact profile:
42
+
43
+ ```yaml
44
+ - uses: maxgfr/ultra11y@v5
45
+ with:
46
+ standard: rgaa
47
+ crawl: http://localhost:3000
48
+ crawl-max: '0'
49
+ ledger: 'true'
50
+ require-rendered: 'true'
51
+ require-decided: pages
52
+ undecidable-file: .ultra11y/undecidable-rgaa.json
53
+ report: 'false'
54
+ html: 'false'
55
+ evidence: 'false'
56
+ pages-report: compact
57
+ ```
58
+
59
+ This still gates every criterion on every captured page. Its visible summary lists the complete
60
+ page-by-page grid, while its artifact contains only `pages-status.md`, `pages.json`,
61
+ `audit-latest.json` and the portable verdict ledger. Use the detailed Claude Code route when a
62
+ human-readable conformance dossier is the deliverable; CI's job here is exhaustive evidence,
63
+ fast feedback and a clean handoff.
64
+
41
65
  A page sweep is usually a **second job** — it needs a built app, a database, a browser — and
42
66
  it can comment alongside the code gate above without either overwriting the other, because
43
67
  each kind has its own marker. Give the two jobs distinct `artifact-name`s too: artifact names
@@ -0,0 +1,102 @@
1
+ # Detailed conformance reports in Claude Code
2
+
3
+ Use this route when the user asks Claude Code for an audit or conformance report. The active
4
+ Claude Code session is the auditor: it reads the criterion briefs and writes the verdicts
5
+ itself. It does not launch a nested `judge --runner claude` session.
6
+
7
+ The completion criterion is a **defensible deliverable**, not merely a clean static scan:
8
+
9
+ - every declared page was captured, or the missing page is named;
10
+ - every criterion on every captured page is `C`, `NC`, `NA`, or explicitly undecidable;
11
+ - every `NC` has a resolvable occurrence and every `C`/`NA` has grounded justification;
12
+ - the conformance report and per-page dossiers pass `check`;
13
+ - the user receives the paths to the Markdown report, HTML entry point, printable single file,
14
+ page index and machine-readable grid.
15
+
16
+ ## 1. Establish the scope
17
+
18
+ Read `.ultra11yrc.json` when present. Confirm the standard, language and page sample. For a
19
+ country standard, prefer `sample check` plus `scan --sample`; otherwise crawl without a hidden
20
+ cap (`--max 0`). A source-only country-standard report is partial and must say so.
21
+
22
+ ## 2. Audit source and rendered pages
23
+
24
+ ```sh
25
+ node scripts/ultra11y.mjs audit <paths> --graph --standard <standard> --lang <lang> --out audits --json
26
+ node scripts/ultra11y.mjs scan --sample --runtime local --cwd . \
27
+ --standard <standard> --merge audits/audit-latest.json --out audits
28
+ ```
29
+
30
+ Use `--crawl <url> --max 0` instead of `--sample` when the project has no declared sample. A
31
+ rendering criterion stays undecidable when no browser measurement exists; source inspection
32
+ does not replace the scan.
33
+
34
+ ## 3. Adjudicate directly
35
+
36
+ Generate the judgment worklist with `verify --manual`. Read each small
37
+ `audits/adjudicate/<criterion>.md` brief, decide against its official tests, write the verdict
38
+ file, then fold it with `verify --apply`. Use the orchestration route when the worklist is large,
39
+ but keep the final fold in the parent session.
40
+
41
+ ```sh
42
+ node scripts/ultra11y.mjs report --in audits/audit-latest.json \
43
+ --standard <standard> --lang <lang> --out audits
44
+ node scripts/ultra11y.mjs verify --report audits/<standard>-<date>.md \
45
+ --in audits/audit-latest.json --manual --standard <standard> --out audits
46
+ node scripts/ultra11y.mjs verify --apply audits/ADJUDICATE.verdicts.json \
47
+ --in audits/audit-latest.json --standard <standard> --out audits
48
+ ```
49
+
50
+ The first report is a draft, not the final deliverable. Follow `references/judgment.md` for
51
+ the verdict schema and fail-closed fold.
52
+
53
+ ## 4. Challenge the claims
54
+
55
+ Render the report, generate the adversarial `VERIFY.todo.json`, adjudicate every claim, apply
56
+ the verdicts with `--prune --ledger`, then render again. This trial covers both alleged
57
+ non-conformities and agent-declared conformities.
58
+
59
+ ```sh
60
+ node scripts/ultra11y.mjs report --in audits/audit-latest.json \
61
+ --standard <standard> --lang <lang> --out audits
62
+ node scripts/ultra11y.mjs verify --report audits/<standard>-<date>.md \
63
+ --in audits/audit-latest.json --standard <standard> --semantic --out audits
64
+ node scripts/ultra11y.mjs verify --apply audits/VERIFY.todo.json \
65
+ --report audits/<standard>-<date>.md --in audits/audit-latest.json \
66
+ --standard <standard> --out audits --prune --ledger
67
+ ```
68
+
69
+ ## 5. Publish the detailed deliverable
70
+
71
+ ```sh
72
+ node scripts/ultra11y.mjs report --in audits/audit-latest.json \
73
+ --standard <standard> --lang <lang> --html --evidence --out audits
74
+ node scripts/ultra11y.mjs pages --in audits/audit-latest.json \
75
+ --standard <standard> --lang <lang> --json --out audits
76
+ node scripts/ultra11y.mjs pages --in audits/audit-latest.json \
77
+ --standard <standard> --lang <lang> --format report --split page \
78
+ --html --evidence --out audits/pages
79
+ ```
80
+
81
+ This produces the dated Markdown conformance report, `audits/index.html`, the printable
82
+ single-file HTML report, `audits/pages.json`, and the navigable per-page Markdown/HTML dossiers
83
+ with screenshots and annotated evidence when snapshots support them. It is a conformance
84
+ deliverable; generate a PRD, remediation backlog or tickets only when the user asks for fixes.
85
+
86
+ ## 6. Gate and hand over
87
+
88
+ ```sh
89
+ node scripts/ultra11y.mjs check --report audits/<standard>-<date>.md \
90
+ --in audits/audit-latest.json --standard <standard>
91
+ node scripts/ultra11y.mjs check --in audits/audit-latest.json \
92
+ --standard <standard> --require-rendered
93
+ node scripts/ultra11y.mjs check --in audits/audit-latest.json \
94
+ --standard <standard> --require-decided=pages
95
+ node scripts/ultra11y.mjs check --report audits/pages/index.md \
96
+ --standard <standard>
97
+ ```
98
+
99
+ Append `--allow-undecided .ultra11y/undecidable-<standard>.json` to the completeness gate only
100
+ when the project records named exceptions with reasons. Report the scope, page count, criterion
101
+ count, provenance tally, remaining named exceptions and every deliverable path. A failed gate
102
+ is part of the result and must not be summarized as a complete report.
@@ -25,6 +25,11 @@ Two things are worth knowing before the table:
25
25
  The plugin carries both skills *and* the hook. `hooks/hooks.json` → `hooks/pre-tool-use.mjs`
26
26
  → `ultra11y hook --claude-code`. The engine resolves from `${CLAUDE_SKILL_DIR}/scripts/ultra11y.mjs`.
27
27
 
28
+ For a requested full audit, Claude Code follows `references/claude-code-report.md`: it acts as
29
+ the auditor directly, then publishes the evidenced Markdown, HTML and per-page dossiers. The
30
+ headless `judge --runner claude` transport is for CI or another unattended caller; nesting it
31
+ inside the active Claude Code session wastes context and gives the report a weaker reader.
32
+
28
33
  The review arrives as `permissionDecision: "deny"` on the pending command, with the findings
29
34
  in `additionalContext`.
30
35
 
@@ -22,6 +22,9 @@ for pass in 1 2 3; do
22
22
  --model haiku --max-budget-usd <n> --concurrency 4 --ledger
23
23
  done
24
24
 
25
+ # The worklist is authoritative. A run-wide NC may be present because other page cells remain
26
+ # open: every emitted item gets one grounded verdict, or an explicit manual reason.
27
+
25
28
  # 4. A second reader puts those claims on trial, and the outcome is APPLIED.
26
29
  ultra11y report --in audits/audit-latest.json --standard rgaa --out audits
27
30
  ultra11y verify --report audits/<report>.md --in audits/audit-latest.json --out audits --max-verify 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.39.2";
9
+ var VERSION = "5.40.1";
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-28 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-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).",
2304
2304
  criteria: [
2305
2305
  {
2306
2306
  id: "1.1.1",
@@ -5473,8 +5473,8 @@ function allIds(doc) {
5473
5473
  }
5474
5474
  function snippet(doc, el, max = 120) {
5475
5475
  const lineStart = doc.lineStarts[el.line - 1] ?? 0;
5476
- let end = doc.source.indexOf("\n", lineStart);
5477
- if (end === -1) end = doc.source.length;
5476
+ const nextLineStart = doc.lineStarts[el.line];
5477
+ const end = nextLineStart === void 0 ? doc.source.length : Math.max(lineStart, nextLineStart - 1);
5478
5478
  const from = Math.min(Math.max(el.start, lineStart), end);
5479
5479
  const tagEnd = doc.source.indexOf(">", from);
5480
5480
  const to = Math.min(from + max, Math.max(Math.min(end, from + max), tagEnd === -1 ? end : tagEnd + 1));
@@ -63277,6 +63277,7 @@ var VERDICT_KINDS = `Rule it (the apply gate is FAIL-CLOSED \u2014 a verdict mis
63277
63277
  - \`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.
63278
63278
  - \`NA\` (not applicable) \u2014 REQUIRES \`justification\`, AND \`citations[]\` whenever evidence WAS presented, to say which of those items fall outside the criterion's scope.
63279
63279
  - \`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).`;
63280
+ var WORKLIST_RULE = `THE WORKLIST IS THE RESIDUAL. Return exactly one verdict for EVERY criterion presented \u2014 never omit an item because the engine already reported a run-wide \`NC\`. The strict page gate deliberately puts such a criterion back on the worklist when page-level cells remain open. It is not surplus and it is not already resolved for this task: inspect the complete evidence and rule it. If the evidence cannot settle it, return \`manual\` with a reason; never leave its verdict blank.`;
63280
63281
  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\`.
63281
63282
 
63282
63283
  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.`;
@@ -63284,7 +63285,7 @@ var CAPTURE_RULE = `THE RENDERED PAGE MAY BE ON DISK. When a criterion's evidenc
63284
63285
  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.`;
63285
63286
  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.`;
63286
63287
  var CONFORMITY_RULE = `A \`C\` WILL BE ATTACKED, exactly as an \`NC\` is. Every conformity you record goes into an adversarial worklist where a second reader opens your citations and asks whether they ESTABLISH the criterion or merely show that its subject exists \u2014 a present \`alt\` is not a relevant \`alt\`, a present \`<title>\` is not a title that describes the page. Cite the evidence that answers the criterion's own question, and when the evidence only proves presence, the honest verdict is \`manual\`.`;
63287
- var TAIL = [ABSENCE_RULE, CAPTURE_RULE, NEVER_GUESS_RULE, SCOPE_RULE, CONFORMITY_RULE];
63288
+ var TAIL = [WORKLIST_RULE, ABSENCE_RULE, CAPTURE_RULE, NEVER_GUESS_RULE, SCOPE_RULE, CONFORMITY_RULE];
63288
63289
  function verdictRulesMd(startAt) {
63289
63290
  const lines = [`${startAt}. ${VERDICT_KINDS}`];
63290
63291
  TAIL.forEach((rule, i2) => lines.push(`${startAt + 1 + i2}. ${rule}`));
@@ -63572,7 +63573,9 @@ function claudeBin() {
63572
63573
  }
63573
63574
  function batchSchema(items) {
63574
63575
  const schema = structuredClone(VERDICT_TOOL.input_schema);
63575
- const id = schema.properties.verdicts.items.properties.criteriaId;
63576
+ const verdicts = schema.properties.verdicts;
63577
+ verdicts.description = `Exactly one verdict for each of these ${items.length} worklist criteria. Do not omit criteria already reported non-conforming run-wide: page-level cells may still be open.`;
63578
+ const id = verdicts.items.properties.criteriaId;
63576
63579
  id.enum = items.map((i2) => i2.criteriaId);
63577
63580
  id.description = `The criterion id EXACTLY as given \u2014 the bare id (e.g. "1.2.1"), never the heading or the title beside it.`;
63578
63581
  return schema;
@@ -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.39.2'
30
+ ULTRA11Y_VERSION: '5.40.1'
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
@@ -131,9 +131,21 @@ export function claudeBin(): string[] {
131
131
  * structured output against this schema, so the wrong shape cannot come back at all. */
132
132
  export function batchSchema(items: AdjudicationItem[]): Record<string, unknown> {
133
133
  const schema = structuredClone(VERDICT_TOOL.input_schema) as {
134
- properties: { verdicts: { items: { properties: { criteriaId: Record<string, unknown> } } } };
134
+ properties: {
135
+ verdicts: {
136
+ description?: string;
137
+ items: { properties: { criteriaId: Record<string, unknown> } };
138
+ };
139
+ };
135
140
  };
136
- const id = schema.properties.verdicts.items.properties.criteriaId;
141
+ const verdicts = schema.properties.verdicts;
142
+ // Anthropic structured outputs support array minItems only at 0 or 1, not exact batch
143
+ // cardinality. Keep completeness in the shared worklist contract and repeat it beside the
144
+ // array the model is filling. A hard unsupported constraint could turn a useful 7/8 response
145
+ // into a provider error and lose all seven valid verdicts; validateBatchVerdicts deliberately
146
+ // preserves those seven and names the missing item for the next pass.
147
+ verdicts.description = `Exactly one verdict for each of these ${items.length} worklist criteria. Do not omit criteria already reported non-conforming run-wide: page-level cells may still be open.`;
148
+ const id = verdicts.items.properties.criteriaId;
137
149
  id.enum = items.map((i) => i.criteriaId);
138
150
  id.description = `The criterion id EXACTLY as given — the bare id (e.g. "1.2.1"), never the heading or the title beside it.`;
139
151
  return schema as unknown as Record<string, unknown>;
@@ -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-28 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-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).",
6
6
  "criteria": [
7
7
  {
8
8
  "id": "1.1.1",
package/src/parse/html.ts CHANGED
@@ -290,8 +290,12 @@ export function allIds(doc: Doc): { id: string; el: El }[] {
290
290
  * its whole subtree. */
291
291
  export function snippet(doc: Doc, el: El, max = 120): string {
292
292
  const lineStart = doc.lineStarts[el.line - 1] ?? 0;
293
- let end = doc.source.indexOf("\n", lineStart);
294
- if (end === -1) end = doc.source.length;
293
+ // `lineStarts` already indexes the next line. Searching for `\n` from this line's start for
294
+ // EVERY finding rescanned an entire minified one-line document each time: 8k dangling-label
295
+ // findings × a 650 kB line turned an otherwise-linear audit quadratic. The next recorded
296
+ // offset gives the same boundary in O(1); on the last/only line the document end is it.
297
+ const nextLineStart = doc.lineStarts[el.line];
298
+ const end = nextLineStart === undefined ? doc.source.length : Math.max(lineStart, nextLineStart - 1);
295
299
  const from = Math.min(Math.max(el.start, lineStart), end);
296
300
  // A JSX element usually opens on one line and carries its attributes on the next ones, so
297
301
  // stopping at the line break showed "<a" and nothing else. Read to the end of the OPENING
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.39.2";
3
+ export const VERSION = "5.40.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,
@@ -26,6 +26,13 @@ export const VERDICT_KINDS = `Rule it (the apply gate is FAIL-CLOSED — a verdi
26
26
  - \`NA\` (not applicable) — REQUIRES \`justification\`, AND \`citations[]\` whenever evidence WAS presented, to say which of those items fall outside the criterion's scope.
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
+ /** The worklist, not the run-level tally, is the source of truth for what remains to rule.
30
+ *
31
+ * Measured on the realworld fixture: the agent left 17 items blank because their decisive
32
+ * failures already made the run-wide grid NC. Those items were deliberately present because
33
+ * other page cells were still open; the run finished 106/106 globally and failed every page. */
34
+ export const WORKLIST_RULE = `THE WORKLIST IS THE RESIDUAL. Return exactly one verdict for EVERY criterion presented — never omit an item because the engine already reported a run-wide \`NC\`. The strict page gate deliberately puts such a criterion back on the worklist when page-level cells remain open. It is not surplus and it is not already resolved for this task: inspect the complete evidence and rule it. If the evidence cannot settle it, return \`manual\` with a reason; never leave its verdict blank.`;
35
+
29
36
  /** An absence is observed somewhere. Measured: 12.1 and 12.5 lost to this on one run. */
30
37
  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
38
 
@@ -49,7 +56,7 @@ export const SCOPE_RULE = `Rule ONLY on the criteria presented. Never introduce
49
56
  export const CONFORMITY_RULE = `A \`C\` WILL BE ATTACKED, exactly as an \`NC\` is. Every conformity you record goes into an adversarial worklist where a second reader opens your citations and asks whether they ESTABLISH the criterion or merely show that its subject exists — a present \`alt\` is not a relevant \`alt\`, a present \`<title>\` is not a title that describes the page. Cite the evidence that answers the criterion's own question, and when the evidence only proves presence, the honest verdict is \`manual\`.`;
50
57
 
51
58
  /** The clauses in the order every surface states them, after the verdict kinds. */
52
- const TAIL = [ABSENCE_RULE, CAPTURE_RULE, NEVER_GUESS_RULE, SCOPE_RULE, CONFORMITY_RULE];
59
+ const TAIL = [WORKLIST_RULE, ABSENCE_RULE, CAPTURE_RULE, NEVER_GUESS_RULE, SCOPE_RULE, CONFORMITY_RULE];
53
60
 
54
61
  /** Render the rules as a numbered Markdown list starting at `startAt`.
55
62
  *