ultra11y 5.5.2 → 5.5.3

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.5.2",
13
+ "version": "5.5.3",
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.5.2",
4
+ "version": "5.5.3",
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.5.2",
4
+ "version": "5.5.3",
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.5.2";
30
+ const ULTRA11Y_PLUGIN_VERSION = "5.5.3";
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.5.2",
3
+ "version": "5.5.3",
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 join51, 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.5.2";
9
+ var VERSION = "5.5.3";
10
10
  var SCHEMA_VERSION = 2;
11
11
 
12
12
  // src/audit.ts
@@ -57182,7 +57182,7 @@ function adjudicationText(sc, lang) {
57182
57182
 
57183
57183
  // src/adjudicate.ts
57184
57184
  var ADJUDICATE_MAX_EVIDENCE_CLASSES = 1200;
57185
- var ALSO_AT_MAX = 8;
57185
+ var ALSO_AT_SHOWN = 8;
57186
57186
  function adjudicationContract() {
57187
57187
  return {
57188
57188
  verdicts: [...VERDICTS],
@@ -57225,7 +57225,7 @@ function collapse2(harvested) {
57225
57225
  return {
57226
57226
  ...rep.ev,
57227
57227
  ...group.length > 1 ? { occurrences: group.length } : {},
57228
- ...others.length ? { alsoAt: others.slice(0, ALSO_AT_MAX).map((x) => `${x.ev.file}:${x.ev.line}`) } : {},
57228
+ ...others.length ? { alsoAt: others.map((x) => `${x.ev.file}:${x.ev.line}`) } : {},
57229
57229
  ...pages.length ? { pages } : {}
57230
57230
  };
57231
57231
  });
@@ -57288,6 +57288,27 @@ function buildAdjudicationWorklist(audit2, opts = {}) {
57288
57288
  (r) => blankItem(r.criteriaId, r.automatability, scTitle(r.criteriaId) ?? void 0, harvestSubjects(subjectsForSc(r.criteriaId), docs))
57289
57289
  );
57290
57290
  }
57291
+ function auditFiles(audit2, cwd) {
57292
+ const inputs = audit2.scope.inputs.filter((i2) => i2 !== "-" && i2 !== "<stdin>");
57293
+ if (!inputs.length) return /* @__PURE__ */ new Set();
57294
+ try {
57295
+ void cwd;
57296
+ return new Set(discover(inputs, {}).files);
57297
+ } catch {
57298
+ return /* @__PURE__ */ new Set();
57299
+ }
57300
+ }
57301
+ var CITE_DRIFT = 10;
57302
+ function cites0(anchors, c2) {
57303
+ for (const a of anchors) {
57304
+ const at = a.lastIndexOf(":");
57305
+ if (at < 0) continue;
57306
+ if (a.slice(0, at) !== c2.file) continue;
57307
+ const line = Number.parseInt(a.slice(at + 1), 10);
57308
+ if (Number.isFinite(line) && Math.abs(line - c2.line) <= CITE_DRIFT) return true;
57309
+ }
57310
+ return false;
57311
+ }
57291
57312
  var NC_SEVERITY_DEFAULT = "majeur";
57292
57313
  var MANUAL_REASONS = /* @__PURE__ */ new Set(["needs-rendered-dom", "undecidable"]);
57293
57314
  var VERDICTS = ["C", "NC", "NA", "manual"];
@@ -57374,6 +57395,8 @@ function applyAdjudication(audit2, adj, opts = {}) {
57374
57395
  }
57375
57396
  }
57376
57397
  const groundInputs = /* @__PURE__ */ new Map();
57398
+ let scopeCache;
57399
+ const scopeFiles = () => scopeCache ??= auditFiles(audit2, opts.cwd);
57377
57400
  const toGround = (criteriaId, g) => {
57378
57401
  const list = groundInputs.get(criteriaId);
57379
57402
  if (list) list.push(g);
@@ -57399,15 +57422,15 @@ function applyAdjudication(audit2, adj, opts = {}) {
57399
57422
  `criterion ${it.criteriaId}: a C verdict needs evidence to cite, and none was harvested for this criterion \u2014 record "manual" (reason "undecidable"), or "NA" if nothing in scope is concerned`
57400
57423
  );
57401
57424
  }
57402
- } else if (cites.length === 0) {
57425
+ } else if (cites.length === 0 && v === "C") {
57403
57426
  blame(
57404
57427
  it.criteriaId,
57405
- `criterion ${it.criteriaId}: a ${v} verdict must cite at least one of the ${it.evidence.length} evidence item(s) it was shown (citations: [{file, line, \u2026}])`
57428
+ `criterion ${it.criteriaId}: a C verdict must cite at least one of the ${it.evidence.length} evidence item(s) it was shown (citations: [{file, line, \u2026}])`
57406
57429
  );
57407
- } else {
57408
- const anchors = new Set(it.evidence.flatMap((e) => [`${e.file}:${e.line}`, ...e.alsoAt ?? []]));
57430
+ } else if (cites.length > 0) {
57431
+ const anchors = it.evidence.flatMap((e) => [`${e.file}:${e.line}`, ...e.alsoAt ?? []]);
57409
57432
  for (const c2 of cites) {
57410
- if (!anchors.has(`${c2.file}:${c2.line}`)) {
57433
+ if (!cites0(anchors, c2) && !scopeFiles().has(c2.file)) {
57411
57434
  blame(it.criteriaId, `criterion ${it.criteriaId}: citation ${c2.file}:${c2.line} is not among this criterion's harvested evidence (fabricated?)`);
57412
57435
  }
57413
57436
  toGround(it.criteriaId, { file: c2.file, line: c2.line, selector: c2.selector, snippet: c2.snippet });
@@ -57593,6 +57616,7 @@ var T2 = {
57593
57616
  occurrences: "occurrences",
57594
57617
  pagesWord: "page(s)",
57595
57618
  on: "sur",
57619
+ alsoAt: "aussi en",
57596
57620
  incomplete: "LECTURE INCOMPL\xC8TE \u2014 un \xAB C \xBB sera refus\xE9 sur ce crit\xE8re",
57597
57621
  none: "(aucune \xE9vidence automatique \u2014 d\xE9cidez depuis la source, ou laissez `manual` avec une raison)",
57598
57622
  questions: "\xC0 v\xE9rifier manuellement",
@@ -57621,6 +57645,7 @@ var T2 = {
57621
57645
  occurrences: "occurrences",
57622
57646
  pagesWord: "page(s)",
57623
57647
  on: "on",
57648
+ alsoAt: "also at",
57624
57649
  incomplete: "INCOMPLETE READING \u2014 a C will be refused on this criterion",
57625
57650
  none: "(no automatic evidence \u2014 decide from source, or leave `manual` with a reason)",
57626
57651
  questions: "To verify manually",
@@ -57676,7 +57701,8 @@ function formatAdjudication(items, lang = "en", standard = CORE2, opts = {}) {
57676
57701
  for (const e of it.evidence) {
57677
57702
  const extra = [
57678
57703
  e.occurrences && e.occurrences > 1 ? `\xD7${e.occurrences}` : "",
57679
- e.pages?.length ? `${s.on} ${e.pages.slice(0, 4).join(", ")}${e.pages.length > 4 ? "\u2026" : ""}` : ""
57704
+ e.pages?.length ? `${s.on} ${e.pages.slice(0, 4).join(", ")}${e.pages.length > 4 ? "\u2026" : ""}` : "",
57705
+ e.alsoAt?.length ? `${s.alsoAt} ${e.alsoAt.slice(0, ALSO_AT_SHOWN).join(", ")}${e.alsoAt.length > ALSO_AT_SHOWN ? "\u2026" : ""}` : ""
57680
57706
  ].filter(Boolean).join(" ");
57681
57707
  out2.push(`- \`${e.file}:${e.line}\` (\`${e.selector}\`)${extra ? ` [${extra}]` : ""}${e.note ? ` \u2014 ${e.note}` : ""}`);
57682
57708
  }
@@ -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.5.2
8
+ version: 5.5.3
9
9
  ---
10
10
 
11
11
  # review-a11y — accessibility review of changed code
@@ -6,7 +6,7 @@ import { join as join51, 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.5.2";
9
+ var VERSION = "5.5.3";
10
10
  var SCHEMA_VERSION = 2;
11
11
 
12
12
  // src/audit.ts
@@ -57182,7 +57182,7 @@ function adjudicationText(sc, lang) {
57182
57182
 
57183
57183
  // src/adjudicate.ts
57184
57184
  var ADJUDICATE_MAX_EVIDENCE_CLASSES = 1200;
57185
- var ALSO_AT_MAX = 8;
57185
+ var ALSO_AT_SHOWN = 8;
57186
57186
  function adjudicationContract() {
57187
57187
  return {
57188
57188
  verdicts: [...VERDICTS],
@@ -57225,7 +57225,7 @@ function collapse2(harvested) {
57225
57225
  return {
57226
57226
  ...rep.ev,
57227
57227
  ...group.length > 1 ? { occurrences: group.length } : {},
57228
- ...others.length ? { alsoAt: others.slice(0, ALSO_AT_MAX).map((x) => `${x.ev.file}:${x.ev.line}`) } : {},
57228
+ ...others.length ? { alsoAt: others.map((x) => `${x.ev.file}:${x.ev.line}`) } : {},
57229
57229
  ...pages.length ? { pages } : {}
57230
57230
  };
57231
57231
  });
@@ -57288,6 +57288,27 @@ function buildAdjudicationWorklist(audit2, opts = {}) {
57288
57288
  (r) => blankItem(r.criteriaId, r.automatability, scTitle(r.criteriaId) ?? void 0, harvestSubjects(subjectsForSc(r.criteriaId), docs))
57289
57289
  );
57290
57290
  }
57291
+ function auditFiles(audit2, cwd) {
57292
+ const inputs = audit2.scope.inputs.filter((i2) => i2 !== "-" && i2 !== "<stdin>");
57293
+ if (!inputs.length) return /* @__PURE__ */ new Set();
57294
+ try {
57295
+ void cwd;
57296
+ return new Set(discover(inputs, {}).files);
57297
+ } catch {
57298
+ return /* @__PURE__ */ new Set();
57299
+ }
57300
+ }
57301
+ var CITE_DRIFT = 10;
57302
+ function cites0(anchors, c2) {
57303
+ for (const a of anchors) {
57304
+ const at = a.lastIndexOf(":");
57305
+ if (at < 0) continue;
57306
+ if (a.slice(0, at) !== c2.file) continue;
57307
+ const line = Number.parseInt(a.slice(at + 1), 10);
57308
+ if (Number.isFinite(line) && Math.abs(line - c2.line) <= CITE_DRIFT) return true;
57309
+ }
57310
+ return false;
57311
+ }
57291
57312
  var NC_SEVERITY_DEFAULT = "majeur";
57292
57313
  var MANUAL_REASONS = /* @__PURE__ */ new Set(["needs-rendered-dom", "undecidable"]);
57293
57314
  var VERDICTS = ["C", "NC", "NA", "manual"];
@@ -57374,6 +57395,8 @@ function applyAdjudication(audit2, adj, opts = {}) {
57374
57395
  }
57375
57396
  }
57376
57397
  const groundInputs = /* @__PURE__ */ new Map();
57398
+ let scopeCache;
57399
+ const scopeFiles = () => scopeCache ??= auditFiles(audit2, opts.cwd);
57377
57400
  const toGround = (criteriaId, g) => {
57378
57401
  const list = groundInputs.get(criteriaId);
57379
57402
  if (list) list.push(g);
@@ -57399,15 +57422,15 @@ function applyAdjudication(audit2, adj, opts = {}) {
57399
57422
  `criterion ${it.criteriaId}: a C verdict needs evidence to cite, and none was harvested for this criterion \u2014 record "manual" (reason "undecidable"), or "NA" if nothing in scope is concerned`
57400
57423
  );
57401
57424
  }
57402
- } else if (cites.length === 0) {
57425
+ } else if (cites.length === 0 && v === "C") {
57403
57426
  blame(
57404
57427
  it.criteriaId,
57405
- `criterion ${it.criteriaId}: a ${v} verdict must cite at least one of the ${it.evidence.length} evidence item(s) it was shown (citations: [{file, line, \u2026}])`
57428
+ `criterion ${it.criteriaId}: a C verdict must cite at least one of the ${it.evidence.length} evidence item(s) it was shown (citations: [{file, line, \u2026}])`
57406
57429
  );
57407
- } else {
57408
- const anchors = new Set(it.evidence.flatMap((e) => [`${e.file}:${e.line}`, ...e.alsoAt ?? []]));
57430
+ } else if (cites.length > 0) {
57431
+ const anchors = it.evidence.flatMap((e) => [`${e.file}:${e.line}`, ...e.alsoAt ?? []]);
57409
57432
  for (const c2 of cites) {
57410
- if (!anchors.has(`${c2.file}:${c2.line}`)) {
57433
+ if (!cites0(anchors, c2) && !scopeFiles().has(c2.file)) {
57411
57434
  blame(it.criteriaId, `criterion ${it.criteriaId}: citation ${c2.file}:${c2.line} is not among this criterion's harvested evidence (fabricated?)`);
57412
57435
  }
57413
57436
  toGround(it.criteriaId, { file: c2.file, line: c2.line, selector: c2.selector, snippet: c2.snippet });
@@ -57593,6 +57616,7 @@ var T2 = {
57593
57616
  occurrences: "occurrences",
57594
57617
  pagesWord: "page(s)",
57595
57618
  on: "sur",
57619
+ alsoAt: "aussi en",
57596
57620
  incomplete: "LECTURE INCOMPL\xC8TE \u2014 un \xAB C \xBB sera refus\xE9 sur ce crit\xE8re",
57597
57621
  none: "(aucune \xE9vidence automatique \u2014 d\xE9cidez depuis la source, ou laissez `manual` avec une raison)",
57598
57622
  questions: "\xC0 v\xE9rifier manuellement",
@@ -57621,6 +57645,7 @@ var T2 = {
57621
57645
  occurrences: "occurrences",
57622
57646
  pagesWord: "page(s)",
57623
57647
  on: "on",
57648
+ alsoAt: "also at",
57624
57649
  incomplete: "INCOMPLETE READING \u2014 a C will be refused on this criterion",
57625
57650
  none: "(no automatic evidence \u2014 decide from source, or leave `manual` with a reason)",
57626
57651
  questions: "To verify manually",
@@ -57676,7 +57701,8 @@ function formatAdjudication(items, lang = "en", standard = CORE2, opts = {}) {
57676
57701
  for (const e of it.evidence) {
57677
57702
  const extra = [
57678
57703
  e.occurrences && e.occurrences > 1 ? `\xD7${e.occurrences}` : "",
57679
- e.pages?.length ? `${s.on} ${e.pages.slice(0, 4).join(", ")}${e.pages.length > 4 ? "\u2026" : ""}` : ""
57704
+ e.pages?.length ? `${s.on} ${e.pages.slice(0, 4).join(", ")}${e.pages.length > 4 ? "\u2026" : ""}` : "",
57705
+ e.alsoAt?.length ? `${s.alsoAt} ${e.alsoAt.slice(0, ALSO_AT_SHOWN).join(", ")}${e.alsoAt.length > ALSO_AT_SHOWN ? "\u2026" : ""}` : ""
57680
57706
  ].filter(Boolean).join(" ");
57681
57707
  out2.push(`- \`${e.file}:${e.line}\` (\`${e.selector}\`)${extra ? ` [${extra}]` : ""}${e.note ? ` \u2014 ${e.note}` : ""}`);
57682
57708
  }
@@ -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.5.2
7
+ version: 5.5.3
8
8
  ---
9
9
 
10
10
  # ultra11y — audit WCAG 2.2 AA and write accessible markup
@@ -6,7 +6,7 @@ import { join as join51, 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.5.2";
9
+ var VERSION = "5.5.3";
10
10
  var SCHEMA_VERSION = 2;
11
11
 
12
12
  // src/audit.ts
@@ -57182,7 +57182,7 @@ function adjudicationText(sc, lang) {
57182
57182
 
57183
57183
  // src/adjudicate.ts
57184
57184
  var ADJUDICATE_MAX_EVIDENCE_CLASSES = 1200;
57185
- var ALSO_AT_MAX = 8;
57185
+ var ALSO_AT_SHOWN = 8;
57186
57186
  function adjudicationContract() {
57187
57187
  return {
57188
57188
  verdicts: [...VERDICTS],
@@ -57225,7 +57225,7 @@ function collapse2(harvested) {
57225
57225
  return {
57226
57226
  ...rep.ev,
57227
57227
  ...group.length > 1 ? { occurrences: group.length } : {},
57228
- ...others.length ? { alsoAt: others.slice(0, ALSO_AT_MAX).map((x) => `${x.ev.file}:${x.ev.line}`) } : {},
57228
+ ...others.length ? { alsoAt: others.map((x) => `${x.ev.file}:${x.ev.line}`) } : {},
57229
57229
  ...pages.length ? { pages } : {}
57230
57230
  };
57231
57231
  });
@@ -57288,6 +57288,27 @@ function buildAdjudicationWorklist(audit2, opts = {}) {
57288
57288
  (r) => blankItem(r.criteriaId, r.automatability, scTitle(r.criteriaId) ?? void 0, harvestSubjects(subjectsForSc(r.criteriaId), docs))
57289
57289
  );
57290
57290
  }
57291
+ function auditFiles(audit2, cwd) {
57292
+ const inputs = audit2.scope.inputs.filter((i2) => i2 !== "-" && i2 !== "<stdin>");
57293
+ if (!inputs.length) return /* @__PURE__ */ new Set();
57294
+ try {
57295
+ void cwd;
57296
+ return new Set(discover(inputs, {}).files);
57297
+ } catch {
57298
+ return /* @__PURE__ */ new Set();
57299
+ }
57300
+ }
57301
+ var CITE_DRIFT = 10;
57302
+ function cites0(anchors, c2) {
57303
+ for (const a of anchors) {
57304
+ const at = a.lastIndexOf(":");
57305
+ if (at < 0) continue;
57306
+ if (a.slice(0, at) !== c2.file) continue;
57307
+ const line = Number.parseInt(a.slice(at + 1), 10);
57308
+ if (Number.isFinite(line) && Math.abs(line - c2.line) <= CITE_DRIFT) return true;
57309
+ }
57310
+ return false;
57311
+ }
57291
57312
  var NC_SEVERITY_DEFAULT = "majeur";
57292
57313
  var MANUAL_REASONS = /* @__PURE__ */ new Set(["needs-rendered-dom", "undecidable"]);
57293
57314
  var VERDICTS = ["C", "NC", "NA", "manual"];
@@ -57374,6 +57395,8 @@ function applyAdjudication(audit2, adj, opts = {}) {
57374
57395
  }
57375
57396
  }
57376
57397
  const groundInputs = /* @__PURE__ */ new Map();
57398
+ let scopeCache;
57399
+ const scopeFiles = () => scopeCache ??= auditFiles(audit2, opts.cwd);
57377
57400
  const toGround = (criteriaId, g) => {
57378
57401
  const list = groundInputs.get(criteriaId);
57379
57402
  if (list) list.push(g);
@@ -57399,15 +57422,15 @@ function applyAdjudication(audit2, adj, opts = {}) {
57399
57422
  `criterion ${it.criteriaId}: a C verdict needs evidence to cite, and none was harvested for this criterion \u2014 record "manual" (reason "undecidable"), or "NA" if nothing in scope is concerned`
57400
57423
  );
57401
57424
  }
57402
- } else if (cites.length === 0) {
57425
+ } else if (cites.length === 0 && v === "C") {
57403
57426
  blame(
57404
57427
  it.criteriaId,
57405
- `criterion ${it.criteriaId}: a ${v} verdict must cite at least one of the ${it.evidence.length} evidence item(s) it was shown (citations: [{file, line, \u2026}])`
57428
+ `criterion ${it.criteriaId}: a C verdict must cite at least one of the ${it.evidence.length} evidence item(s) it was shown (citations: [{file, line, \u2026}])`
57406
57429
  );
57407
- } else {
57408
- const anchors = new Set(it.evidence.flatMap((e) => [`${e.file}:${e.line}`, ...e.alsoAt ?? []]));
57430
+ } else if (cites.length > 0) {
57431
+ const anchors = it.evidence.flatMap((e) => [`${e.file}:${e.line}`, ...e.alsoAt ?? []]);
57409
57432
  for (const c2 of cites) {
57410
- if (!anchors.has(`${c2.file}:${c2.line}`)) {
57433
+ if (!cites0(anchors, c2) && !scopeFiles().has(c2.file)) {
57411
57434
  blame(it.criteriaId, `criterion ${it.criteriaId}: citation ${c2.file}:${c2.line} is not among this criterion's harvested evidence (fabricated?)`);
57412
57435
  }
57413
57436
  toGround(it.criteriaId, { file: c2.file, line: c2.line, selector: c2.selector, snippet: c2.snippet });
@@ -57593,6 +57616,7 @@ var T2 = {
57593
57616
  occurrences: "occurrences",
57594
57617
  pagesWord: "page(s)",
57595
57618
  on: "sur",
57619
+ alsoAt: "aussi en",
57596
57620
  incomplete: "LECTURE INCOMPL\xC8TE \u2014 un \xAB C \xBB sera refus\xE9 sur ce crit\xE8re",
57597
57621
  none: "(aucune \xE9vidence automatique \u2014 d\xE9cidez depuis la source, ou laissez `manual` avec une raison)",
57598
57622
  questions: "\xC0 v\xE9rifier manuellement",
@@ -57621,6 +57645,7 @@ var T2 = {
57621
57645
  occurrences: "occurrences",
57622
57646
  pagesWord: "page(s)",
57623
57647
  on: "on",
57648
+ alsoAt: "also at",
57624
57649
  incomplete: "INCOMPLETE READING \u2014 a C will be refused on this criterion",
57625
57650
  none: "(no automatic evidence \u2014 decide from source, or leave `manual` with a reason)",
57626
57651
  questions: "To verify manually",
@@ -57676,7 +57701,8 @@ function formatAdjudication(items, lang = "en", standard = CORE2, opts = {}) {
57676
57701
  for (const e of it.evidence) {
57677
57702
  const extra = [
57678
57703
  e.occurrences && e.occurrences > 1 ? `\xD7${e.occurrences}` : "",
57679
- e.pages?.length ? `${s.on} ${e.pages.slice(0, 4).join(", ")}${e.pages.length > 4 ? "\u2026" : ""}` : ""
57704
+ e.pages?.length ? `${s.on} ${e.pages.slice(0, 4).join(", ")}${e.pages.length > 4 ? "\u2026" : ""}` : "",
57705
+ e.alsoAt?.length ? `${s.alsoAt} ${e.alsoAt.slice(0, ALSO_AT_SHOWN).join(", ")}${e.alsoAt.length > ALSO_AT_SHOWN ? "\u2026" : ""}` : ""
57680
57706
  ].filter(Boolean).join(" ");
57681
57707
  out2.push(`- \`${e.file}:${e.line}\` (\`${e.selector}\`)${extra ? ` [${extra}]` : ""}${e.note ? ` \u2014 ${e.note}` : ""}`);
57682
57708
  }
package/src/adjudicate.ts CHANGED
@@ -60,9 +60,11 @@ export const ADJUDICATE_MAX_EVIDENCE_CLASSES = 1200;
60
60
  // especially confusing one, since nothing about the code got worse. Refusing to conclude is
61
61
  // honest only when something really was not looked at.
62
62
 
63
- /** Sibling anchors listed per class. Bounded for readability; `occurrences` carries the true
64
- * count, and the citation gate accepts any anchor of the class, listed or not. */
65
- const ALSO_AT_MAX = 8;
63
+ /** Sibling anchors RENDERED per class. The data keeps them all — the citation gate reads that
64
+ * list to decide whether an anchor belongs to the criterion, so a bound here would make the
65
+ * gate refuse real occurrences for being ninth. Measured on a real run: 31 of 37 refusals
66
+ * were citations of elements the criterion genuinely carried. Only the reading is bounded. */
67
+ const ALSO_AT_SHOWN = 8;
66
68
 
67
69
  /** How much there actually was to look at, so a reader can tell a complete reading from a
68
70
  * glance at the first few. */
@@ -84,7 +86,9 @@ export interface Evidence {
84
86
  /** Anchors sharing this content class — the same header link on 38 pages is one decision,
85
87
  * not 38. Absent when the class has a single anchor. */
86
88
  occurrences?: number;
87
- /** Some of the other anchors of this class, as `file:line`. Bounded (see ALSO_AT_MAX). */
89
+ /** Every other anchor of this class, as `file:line`. Complete on purpose: the citation gate
90
+ * reads it to decide membership, so a bound here would refuse real occurrences. The
91
+ * worklist renders only the first few (ALSO_AT_SHOWN). */
88
92
  alsoAt?: string[];
89
93
  /** Page ids this class appears on, when it was harvested from rendered captures. */
90
94
  pages?: string[];
@@ -240,7 +244,7 @@ function collapse(harvested: Harvested[]): { evidence: Evidence[]; population: E
240
244
  return {
241
245
  ...rep.ev,
242
246
  ...(group.length > 1 ? { occurrences: group.length } : {}),
243
- ...(others.length ? { alsoAt: others.slice(0, ALSO_AT_MAX).map((x) => `${x.ev.file}:${x.ev.line}`) } : {}),
247
+ ...(others.length ? { alsoAt: others.map((x) => `${x.ev.file}:${x.ev.line}`) } : {}),
244
248
  ...(pages.length ? { pages } : {}),
245
249
  };
246
250
  });
@@ -330,6 +334,44 @@ export function buildAdjudicationWorklist(audit: AuditResult, opts: { cwd?: stri
330
334
  );
331
335
  }
332
336
 
337
+ /** The files this audit actually read. Computed from the audit's own scope inputs, so it is
338
+ * the same set the harvest walked — `discover` only globs, it does not parse, so this costs
339
+ * little and is memoised per fold. */
340
+ function auditFiles(audit: AuditResult, cwd?: string): Set<string> {
341
+ const inputs = audit.scope.inputs.filter((i) => i !== "-" && i !== "<stdin>");
342
+ if (!inputs.length) return new Set();
343
+ try {
344
+ void cwd; // `discover` returns the same repo-relative paths the harvest anchors use
345
+ return new Set(discover(inputs, {}).files);
346
+ } catch {
347
+ return new Set();
348
+ }
349
+ }
350
+
351
+ /** Does this citation name evidence the criterion actually carries?
352
+ *
353
+ * Same file, and within the drift `groundFinding` already tolerates. Exact line equality was
354
+ * too literal to be useful: the harvest anchors a <table> at line 19, the agent cites the
355
+ * <caption> at line 20 — which is the very thing RGAA 5.4 asks about, inside the element it
356
+ * was shown — and the gate called it fabricated. Measured on a real run, that class of
357
+ * refusal cost more criteria than every other cause combined.
358
+ *
359
+ * What the check still catches is what it was written for: a citation pointing at a file the
360
+ * criterion was never given. And it is only half the gate — `groundFinding` still has to find
361
+ * the cited content in the real source, so a plausible-looking file:line in the right
362
+ * neighbourhood proves nothing on its own. */
363
+ const CITE_DRIFT = 10;
364
+ function cites0(anchors: string[], c: { file: string; line: number }): boolean {
365
+ for (const a of anchors) {
366
+ const at = a.lastIndexOf(":");
367
+ if (at < 0) continue;
368
+ if (a.slice(0, at) !== c.file) continue;
369
+ const line = Number.parseInt(a.slice(at + 1), 10);
370
+ if (Number.isFinite(line) && Math.abs(line - c.line) <= CITE_DRIFT) return true;
371
+ }
372
+ return false;
373
+ }
374
+
333
375
  export interface ApplyAdjudicationResult {
334
376
  ok: boolean;
335
377
  audit: AuditResult;
@@ -521,6 +563,9 @@ export function applyAdjudication(
521
563
  // flat list) for the same reason as `blame`: a failed citation has to condemn its own
522
564
  // criterion and no other.
523
565
  const groundInputs = new Map<string, { file: string; line: number; selector?: string; snippet?: string }[]>();
566
+ // Memoised: only a citation that missed its criterion's own anchors ever asks for it.
567
+ let scopeCache: Set<string> | undefined;
568
+ const scopeFiles = (): Set<string> => (scopeCache ??= auditFiles(audit, opts.cwd));
524
569
  const toGround = (criteriaId: string, g: { file: string; line: number; selector?: string; snippet?: string }) => {
525
570
  const list = groundInputs.get(criteriaId);
526
571
  if (list) list.push(g);
@@ -557,12 +602,21 @@ export function applyAdjudication(
557
602
  `criterion ${it.criteriaId}: a C verdict needs evidence to cite, and none was harvested for this criterion — record "manual" (reason "undecidable"), or "NA" if nothing in scope is concerned`,
558
603
  );
559
604
  }
560
- } else if (cites.length === 0) {
605
+ } else if (cites.length === 0 && v === "C") {
606
+ // Only a `C` must cite. A `C` says "everything here conforms", so it has to name what
607
+ // it cleared; an `NA` says "none of this is what the criterion is about", and there is
608
+ // nothing to clear — asking it to cite the very elements it just ruled out of scope is
609
+ // a contradiction, and one the engine does not hold ITSELF to: an engine-proved NA
610
+ // (src/audit.ts subjectMatterReason) carries a justification naming what was searched
611
+ // for, and no citations at all. Measured on a real run, six criteria were refused for
612
+ // this alone, each of them an honest NA.
613
+ //
614
+ // The justification is what keeps an NA falsifiable, and it is still required above.
561
615
  blame(
562
616
  it.criteriaId,
563
- `criterion ${it.criteriaId}: a ${v} verdict must cite at least one of the ${it.evidence.length} evidence item(s) it was shown (citations: [{file, line, …}])`,
617
+ `criterion ${it.criteriaId}: a C verdict must cite at least one of the ${it.evidence.length} evidence item(s) it was shown (citations: [{file, line, …}])`,
564
618
  );
565
- } else {
619
+ } else if (cites.length > 0) {
566
620
  // Each citation must name evidence THIS item carried. Grounding alone would only
567
621
  // prove the anchor exists somewhere in the tree; the pairing proves the agent ruled
568
622
  // on what it was actually given.
@@ -576,9 +630,22 @@ export function applyAdjudication(
576
630
  // exactly how RGAA 5.8, 9.2 and 11.1 were refused. Membership was the defect;
577
631
  // grounding — which proves the citation resolves against real source — is unchanged
578
632
  // and still runs on every one of them.
579
- const anchors = new Set(it.evidence.flatMap((e) => [`${e.file}:${e.line}`, ...(e.alsoAt ?? [])]));
633
+ const anchors = it.evidence.flatMap((e) => [`${e.file}:${e.line}`, ...(e.alsoAt ?? [])]);
580
634
  for (const c of cites) {
581
- if (!anchors.has(`${c.file}:${c.line}`)) {
635
+ // Two ways to belong, and the second one is what a good adjudicator does.
636
+ //
637
+ // The harvest anchors a rendered page (`.ultra11y/pages/<id>/dom.html`) because that
638
+ // is what proves the browser's output — but the place a reader goes to FIX it is the
639
+ // component that produced it, and that is what the agent naturally cites. Measured
640
+ // on a real run: 15 of the 16 citations still refused after the drift tolerance were
641
+ // in files this audit had read, pointing at the source behind the evidence. Refusing
642
+ // them called correct work fabricated.
643
+ //
644
+ // So a citation also belongs when it lands in a file the audit read. That is a real
645
+ // bound — a file outside the audited scope is still refused — and it is not the last
646
+ // one: `groundFinding` below still has to find the cited content at that file:line in
647
+ // the real source, which is what "not fabricated" actually means.
648
+ if (!cites0(anchors, c) && !scopeFiles().has(c.file)) {
582
649
  blame(it.criteriaId, `criterion ${it.criteriaId}: citation ${c.file}:${c.line} is not among this criterion's harvested evidence (fabricated?)`);
583
650
  }
584
651
  toGround(it.criteriaId, { file: c.file, line: c.line, selector: c.selector, snippet: c.snippet });
@@ -829,6 +896,7 @@ const T = {
829
896
  occurrences: "occurrences",
830
897
  pagesWord: "page(s)",
831
898
  on: "sur",
899
+ alsoAt: "aussi en",
832
900
  incomplete: "LECTURE INCOMPLÈTE — un « C » sera refusé sur ce critère",
833
901
  none: "(aucune évidence automatique — décidez depuis la source, ou laissez `manual` avec une raison)",
834
902
  questions: "À vérifier manuellement",
@@ -858,6 +926,7 @@ const T = {
858
926
  occurrences: "occurrences",
859
927
  pagesWord: "page(s)",
860
928
  on: "on",
929
+ alsoAt: "also at",
861
930
  incomplete: "INCOMPLETE READING — a C will be refused on this criterion",
862
931
  none: "(no automatic evidence — decide from source, or leave `manual` with a reason)",
863
932
  questions: "To verify manually",
@@ -1003,6 +1072,7 @@ export function formatAdjudication(items: AdjudicationItem[], lang: Lang = "en",
1003
1072
  const extra = [
1004
1073
  e.occurrences && e.occurrences > 1 ? `×${e.occurrences}` : "",
1005
1074
  e.pages?.length ? `${s.on} ${e.pages.slice(0, 4).join(", ")}${e.pages.length > 4 ? "…" : ""}` : "",
1075
+ e.alsoAt?.length ? `${s.alsoAt} ${e.alsoAt.slice(0, ALSO_AT_SHOWN).join(", ")}${e.alsoAt.length > ALSO_AT_SHOWN ? "…" : ""}` : "",
1006
1076
  ]
1007
1077
  .filter(Boolean)
1008
1078
  .join(" ");
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.5.2";
3
+ export const VERSION = "5.5.3";
4
4
  export const SCHEMA_VERSION = 2;
5
5
 
6
6
  // Lang = the UI FRAME's language — the `L` localization tables each module (report,