ultra11y 5.9.0 → 5.9.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.9.0",
13
+ "version": "5.9.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.9.0",
4
+ "version": "5.9.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.9.0",
4
+ "version": "5.9.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.9.0";
30
+ const ULTRA11Y_PLUGIN_VERSION = "5.9.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.9.0",
3
+ "version": "5.9.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 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.9.0";
9
+ var VERSION = "5.9.1";
10
10
  var SCHEMA_VERSION = 2;
11
11
 
12
12
  // src/audit.ts
@@ -57794,10 +57794,11 @@ function applyAdjudication(audit2, adj, opts = {}) {
57794
57794
  let scopeCache;
57795
57795
  const { citationDrift } = adjudicationLimits(opts.cwd);
57796
57796
  const scopeFiles = () => scopeCache ??= auditFiles(audit2, opts.cwd);
57797
- const toGround = (criteriaId, g) => {
57797
+ const toGround = (criteriaId, g, fallback) => {
57798
+ const entry = { g, ...fallback ? { fallback } : {} };
57798
57799
  const list = groundInputs.get(criteriaId);
57799
- if (list) list.push(g);
57800
- else groundInputs.set(criteriaId, [g]);
57800
+ if (list) list.push(entry);
57801
+ else groundInputs.set(criteriaId, [entry]);
57801
57802
  };
57802
57803
  for (const it of adj.items) {
57803
57804
  const v = it.verdict;
@@ -57831,18 +57832,15 @@ function applyAdjudication(audit2, adj, opts = {}) {
57831
57832
  blame(it.criteriaId, `criterion ${it.criteriaId}: citation ${c2.file}:${c2.line} is not among this criterion's harvested evidence (fabricated?)`);
57832
57833
  }
57833
57834
  const anchor = anchorFor(it.evidence, c2, citationDrift);
57834
- if (anchor && !recognisablySame(c2, anchor.at)) {
57835
- blame(
57835
+ const cite = { file: c2.file, line: c2.line, selector: c2.selector, snippet: c2.snippet };
57836
+ if (anchor && recognisablySame(c2, anchor.at)) {
57837
+ toGround(
57836
57838
  it.criteriaId,
57837
- `criterion ${it.criteriaId}: citation ${c2.file}:${c2.line} does not describe the element harvested there (${anchor.at.snippet ? `\`${anchor.at.snippet.slice(0, 80)}\`` : `<${tagOf2(anchor.at)}>`}) \u2014 cite the element you actually read, copying its \`snippet\` from the brief`
57839
+ cite,
57840
+ 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 }
57838
57841
  );
57839
- }
57840
- if (anchor?.representative) {
57841
- toGround(it.criteriaId, { file: anchor.at.file, line: anchor.at.line, selector: anchor.at.selector ?? c2.selector, snippet: anchor.at.snippet });
57842
- } else if (anchor) {
57843
- toGround(it.criteriaId, { file: c2.file, line: c2.line, selector: anchor.at.selector ?? c2.selector });
57844
57842
  } else {
57845
- toGround(it.criteriaId, { file: c2.file, line: c2.line, selector: c2.selector, snippet: c2.snippet });
57843
+ toGround(it.criteriaId, cite);
57846
57844
  }
57847
57845
  }
57848
57846
  }
@@ -57869,8 +57867,12 @@ function applyAdjudication(audit2, adj, opts = {}) {
57869
57867
  }
57870
57868
  const grounding = { grounded: 0, moved: 0, failed: 0, issues: [] };
57871
57869
  for (const [criteriaId, inputs] of groundInputs) {
57872
- for (const input of inputs) {
57873
- const r = groundFinding(input, { cwd: opts.cwd });
57870
+ for (const { g, fallback } of inputs) {
57871
+ let r = groundFinding(g, { cwd: opts.cwd });
57872
+ if (!r.ok && fallback) {
57873
+ const viaAnchor = groundFinding(fallback, { cwd: opts.cwd });
57874
+ if (viaAnchor.ok) r = { ok: true, moved: true };
57875
+ }
57874
57876
  if (r.ok) {
57875
57877
  grounding.grounded++;
57876
57878
  if (r.moved) grounding.moved++;
@@ -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.9.0
8
+ version: 5.9.1
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.9.0";
9
+ var VERSION = "5.9.1";
10
10
  var SCHEMA_VERSION = 2;
11
11
 
12
12
  // src/audit.ts
@@ -57794,10 +57794,11 @@ function applyAdjudication(audit2, adj, opts = {}) {
57794
57794
  let scopeCache;
57795
57795
  const { citationDrift } = adjudicationLimits(opts.cwd);
57796
57796
  const scopeFiles = () => scopeCache ??= auditFiles(audit2, opts.cwd);
57797
- const toGround = (criteriaId, g) => {
57797
+ const toGround = (criteriaId, g, fallback) => {
57798
+ const entry = { g, ...fallback ? { fallback } : {} };
57798
57799
  const list = groundInputs.get(criteriaId);
57799
- if (list) list.push(g);
57800
- else groundInputs.set(criteriaId, [g]);
57800
+ if (list) list.push(entry);
57801
+ else groundInputs.set(criteriaId, [entry]);
57801
57802
  };
57802
57803
  for (const it of adj.items) {
57803
57804
  const v = it.verdict;
@@ -57831,18 +57832,15 @@ function applyAdjudication(audit2, adj, opts = {}) {
57831
57832
  blame(it.criteriaId, `criterion ${it.criteriaId}: citation ${c2.file}:${c2.line} is not among this criterion's harvested evidence (fabricated?)`);
57832
57833
  }
57833
57834
  const anchor = anchorFor(it.evidence, c2, citationDrift);
57834
- if (anchor && !recognisablySame(c2, anchor.at)) {
57835
- blame(
57835
+ const cite = { file: c2.file, line: c2.line, selector: c2.selector, snippet: c2.snippet };
57836
+ if (anchor && recognisablySame(c2, anchor.at)) {
57837
+ toGround(
57836
57838
  it.criteriaId,
57837
- `criterion ${it.criteriaId}: citation ${c2.file}:${c2.line} does not describe the element harvested there (${anchor.at.snippet ? `\`${anchor.at.snippet.slice(0, 80)}\`` : `<${tagOf2(anchor.at)}>`}) \u2014 cite the element you actually read, copying its \`snippet\` from the brief`
57839
+ cite,
57840
+ 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 }
57838
57841
  );
57839
- }
57840
- if (anchor?.representative) {
57841
- toGround(it.criteriaId, { file: anchor.at.file, line: anchor.at.line, selector: anchor.at.selector ?? c2.selector, snippet: anchor.at.snippet });
57842
- } else if (anchor) {
57843
- toGround(it.criteriaId, { file: c2.file, line: c2.line, selector: anchor.at.selector ?? c2.selector });
57844
57842
  } else {
57845
- toGround(it.criteriaId, { file: c2.file, line: c2.line, selector: c2.selector, snippet: c2.snippet });
57843
+ toGround(it.criteriaId, cite);
57846
57844
  }
57847
57845
  }
57848
57846
  }
@@ -57869,8 +57867,12 @@ function applyAdjudication(audit2, adj, opts = {}) {
57869
57867
  }
57870
57868
  const grounding = { grounded: 0, moved: 0, failed: 0, issues: [] };
57871
57869
  for (const [criteriaId, inputs] of groundInputs) {
57872
- for (const input of inputs) {
57873
- const r = groundFinding(input, { cwd: opts.cwd });
57870
+ for (const { g, fallback } of inputs) {
57871
+ let r = groundFinding(g, { cwd: opts.cwd });
57872
+ if (!r.ok && fallback) {
57873
+ const viaAnchor = groundFinding(fallback, { cwd: opts.cwd });
57874
+ if (viaAnchor.ok) r = { ok: true, moved: true };
57875
+ }
57874
57876
  if (r.ok) {
57875
57877
  grounding.grounded++;
57876
57878
  if (r.moved) grounding.moved++;
@@ -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.9.0
7
+ version: 5.9.1
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.9.0";
9
+ var VERSION = "5.9.1";
10
10
  var SCHEMA_VERSION = 2;
11
11
 
12
12
  // src/audit.ts
@@ -57794,10 +57794,11 @@ function applyAdjudication(audit2, adj, opts = {}) {
57794
57794
  let scopeCache;
57795
57795
  const { citationDrift } = adjudicationLimits(opts.cwd);
57796
57796
  const scopeFiles = () => scopeCache ??= auditFiles(audit2, opts.cwd);
57797
- const toGround = (criteriaId, g) => {
57797
+ const toGround = (criteriaId, g, fallback) => {
57798
+ const entry = { g, ...fallback ? { fallback } : {} };
57798
57799
  const list = groundInputs.get(criteriaId);
57799
- if (list) list.push(g);
57800
- else groundInputs.set(criteriaId, [g]);
57800
+ if (list) list.push(entry);
57801
+ else groundInputs.set(criteriaId, [entry]);
57801
57802
  };
57802
57803
  for (const it of adj.items) {
57803
57804
  const v = it.verdict;
@@ -57831,18 +57832,15 @@ function applyAdjudication(audit2, adj, opts = {}) {
57831
57832
  blame(it.criteriaId, `criterion ${it.criteriaId}: citation ${c2.file}:${c2.line} is not among this criterion's harvested evidence (fabricated?)`);
57832
57833
  }
57833
57834
  const anchor = anchorFor(it.evidence, c2, citationDrift);
57834
- if (anchor && !recognisablySame(c2, anchor.at)) {
57835
- blame(
57835
+ const cite = { file: c2.file, line: c2.line, selector: c2.selector, snippet: c2.snippet };
57836
+ if (anchor && recognisablySame(c2, anchor.at)) {
57837
+ toGround(
57836
57838
  it.criteriaId,
57837
- `criterion ${it.criteriaId}: citation ${c2.file}:${c2.line} does not describe the element harvested there (${anchor.at.snippet ? `\`${anchor.at.snippet.slice(0, 80)}\`` : `<${tagOf2(anchor.at)}>`}) \u2014 cite the element you actually read, copying its \`snippet\` from the brief`
57839
+ cite,
57840
+ 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 }
57838
57841
  );
57839
- }
57840
- if (anchor?.representative) {
57841
- toGround(it.criteriaId, { file: anchor.at.file, line: anchor.at.line, selector: anchor.at.selector ?? c2.selector, snippet: anchor.at.snippet });
57842
- } else if (anchor) {
57843
- toGround(it.criteriaId, { file: c2.file, line: c2.line, selector: anchor.at.selector ?? c2.selector });
57844
57842
  } else {
57845
- toGround(it.criteriaId, { file: c2.file, line: c2.line, selector: c2.selector, snippet: c2.snippet });
57843
+ toGround(it.criteriaId, cite);
57846
57844
  }
57847
57845
  }
57848
57846
  }
@@ -57869,8 +57867,12 @@ function applyAdjudication(audit2, adj, opts = {}) {
57869
57867
  }
57870
57868
  const grounding = { grounded: 0, moved: 0, failed: 0, issues: [] };
57871
57869
  for (const [criteriaId, inputs] of groundInputs) {
57872
- for (const input of inputs) {
57873
- const r = groundFinding(input, { cwd: opts.cwd });
57870
+ for (const { g, fallback } of inputs) {
57871
+ let r = groundFinding(g, { cwd: opts.cwd });
57872
+ if (!r.ok && fallback) {
57873
+ const viaAnchor = groundFinding(fallback, { cwd: opts.cwd });
57874
+ if (viaAnchor.ok) r = { ok: true, moved: true };
57875
+ }
57874
57876
  if (r.ok) {
57875
57877
  grounding.grounded++;
57876
57878
  if (r.moved) grounding.moved++;
package/src/adjudicate.ts CHANGED
@@ -732,15 +732,21 @@ export function applyAdjudication(
732
732
  // Per-item fail-closed validation. Grounding inputs are collected PER ITEM (not into one
733
733
  // flat list) for the same reason as `blame`: a failed citation has to condemn its own
734
734
  // criterion and no other.
735
- const groundInputs = new Map<string, { file: string; line: number; selector?: string; snippet?: string }[]>();
735
+ type Ground = { file: string; line: number; selector?: string; snippet?: string };
736
+ const groundInputs = new Map<string, { g: Ground; fallback?: Ground }[]>();
736
737
  // Memoised: only a citation that missed its criterion's own anchors ever asks for it.
737
738
  let scopeCache: Set<string> | undefined;
738
739
  const { citationDrift } = adjudicationLimits(opts.cwd);
739
740
  const scopeFiles = (): Set<string> => (scopeCache ??= auditFiles(audit, opts.cwd));
740
- const toGround = (criteriaId: string, g: { file: string; line: number; selector?: string; snippet?: string }) => {
741
+ const toGround = (
742
+ criteriaId: string,
743
+ g: { file: string; line: number; selector?: string; snippet?: string },
744
+ fallback?: { file: string; line: number; selector?: string; snippet?: string },
745
+ ) => {
746
+ const entry = { g, ...(fallback ? { fallback } : {}) };
741
747
  const list = groundInputs.get(criteriaId);
742
- if (list) list.push(g);
743
- else groundInputs.set(criteriaId, [g]);
748
+ if (list) list.push(entry);
749
+ else groundInputs.set(criteriaId, [entry]);
744
750
  };
745
751
  for (const it of adj.items) {
746
752
  const v = it.verdict;
@@ -839,19 +845,33 @@ export function applyAdjudication(
839
845
  //
840
846
  // Anything OUTSIDE the harvest keeps the strict check below, unchanged. That is where
841
847
  // a fabricated location would hide, and this must not become a way to launder one.
848
+ // THE CITATION FIRST, THE ANCHOR ONLY AS A FALLBACK — and the order is the whole
849
+ // point.
850
+ //
851
+ // Whatever the agent wrote is checked against the real file, exactly as before. That
852
+ // is the strongest proof there is, and it covers the case the drift window exists
853
+ // for: RGAA 5.2 asks about a table's summary, the harvest anchors the `<table>`, and
854
+ // the honest citation is the `<caption>` a line below. Grounding the anchor INSTEAD
855
+ // refused precisely that — measured on a real run, 5.2, 5.4, 5.5, 11.6, 11.7 and
856
+ // 11.9 all died on it, each a correct citation of a neighbour.
857
+ //
858
+ // Only when the citation does not ground — which is what a RETYPING looks like, and
859
+ // on a one-line snapshot that is most of them — does the harvested anchor stand in.
860
+ // It is authoritative there: the engine read it out of the file itself. And only
861
+ // then does recognisability have anything to say, because only then is the anchor
862
+ // being used to vouch for something the file did not confirm on its own.
842
863
  const anchor = anchorFor(it.evidence, c, citationDrift);
843
- if (anchor && !recognisablySame(c, anchor.at)) {
844
- blame(
864
+ const cite = { file: c.file, line: c.line, selector: c.selector, snippet: c.snippet };
865
+ if (anchor && recognisablySame(c, anchor.at)) {
866
+ toGround(
845
867
  it.criteriaId,
846
- `criterion ${it.criteriaId}: citation ${c.file}:${c.line} does not describe the element harvested there (${anchor.at.snippet ? `\`${anchor.at.snippet.slice(0, 80)}\`` : `<${tagOf(anchor.at)}>`}) — cite the element you actually read, copying its \`snippet\` from the brief`,
868
+ cite,
869
+ anchor.representative
870
+ ? { file: anchor.at.file, line: anchor.at.line, selector: anchor.at.selector ?? c.selector, snippet: anchor.at.snippet }
871
+ : { file: c.file, line: c.line, selector: anchor.at.selector ?? c.selector },
847
872
  );
848
- }
849
- if (anchor?.representative) {
850
- toGround(it.criteriaId, { file: anchor.at.file, line: anchor.at.line, selector: anchor.at.selector ?? c.selector, snippet: anchor.at.snippet });
851
- } else if (anchor) {
852
- toGround(it.criteriaId, { file: c.file, line: c.line, selector: anchor.at.selector ?? c.selector });
853
873
  } else {
854
- toGround(it.criteriaId, { file: c.file, line: c.line, selector: c.selector, snippet: c.snippet });
874
+ toGround(it.criteriaId, cite);
855
875
  }
856
876
  }
857
877
  }
@@ -893,8 +913,15 @@ export function applyAdjudication(
893
913
  // flat `groundItems` produced.
894
914
  const grounding: GroundingSummary = { grounded: 0, moved: 0, failed: 0, issues: [] };
895
915
  for (const [criteriaId, inputs] of groundInputs) {
896
- for (const input of inputs) {
897
- const r = groundFinding(input, { cwd: opts.cwd });
916
+ for (const { g, fallback } of inputs) {
917
+ let r = groundFinding(g, { cwd: opts.cwd });
918
+ // The harvested anchor vouches for a citation the file could not confirm on its own —
919
+ // an adjudicator's retyping of an element it did read. `moved` either way: what
920
+ // grounded is not verbatim what was cited.
921
+ if (!r.ok && fallback) {
922
+ const viaAnchor = groundFinding(fallback, { cwd: opts.cwd });
923
+ if (viaAnchor.ok) r = { ok: true, moved: true };
924
+ }
898
925
  if (r.ok) {
899
926
  grounding.grounded++;
900
927
  if (r.moved) grounding.moved++;
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.9.0";
3
+ export const VERSION = "5.9.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,