ultra11y 5.43.1 → 5.43.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,7 +10,7 @@
10
10
  "name": "ultra11y",
11
11
  "source": "./",
12
12
  "description": "Automatic accessibility review before commit, push and pull request, plus detailed evidenced Markdown, HTML and per-page WCAG 2.2 AA / RGAA reports in Claude Code.",
13
- "version": "5.43.1",
13
+ "version": "5.43.2",
14
14
  "author": { "name": "maxgfr" },
15
15
  "homepage": "https://github.com/SocialGouv/ultra11y",
16
16
  "repository": "https://github.com/SocialGouv/ultra11y",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ultra11y",
3
3
  "description": "Accessibility for WCAG 2.2 AA and country standards (RGAA…): automatic review before commit, push and pull request, plus detailed evidenced Markdown, HTML and per-page conformance reports in interactive agent sessions.",
4
- "version": "5.43.1",
4
+ "version": "5.43.2",
5
5
  "author": { "name": "maxgfr" },
6
6
  "homepage": "https://github.com/SocialGouv/ultra11y",
7
7
  "repository": "https://github.com/SocialGouv/ultra11y",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ultra11y",
3
3
  "description": "Accessibility for WCAG 2.2 AA and country standards (RGAA…): automatic review before commit, push and pull request, plus detailed evidenced Markdown, HTML and per-page conformance reports in interactive agent sessions.",
4
- "version": "5.43.1",
4
+ "version": "5.43.2",
5
5
  "author": { "name": "maxgfr", "url": "https://github.com/maxgfr" },
6
6
  "homepage": "https://github.com/SocialGouv/ultra11y",
7
7
  "repository": "https://github.com/SocialGouv/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.43.1";
30
+ const ULTRA11Y_PLUGIN_VERSION = "5.43.2";
31
31
 
32
32
  const here = dirname(fileURLToPath(import.meta.url));
33
33
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultra11y",
3
- "version": "5.43.1",
3
+ "version": "5.43.2",
4
4
  "description": "Audit HTML/CSS/JSX against WCAG 2.2 AA and produce a dated compliance report, or author/review accessible markup (native-HTML-first). A skills.sh agent skill: an install-free static engine (bundled, no npm install) plus the agent's judgment, with check/verify gates against hallucinated non-conformities, and pluggable in-repo standards packs (RGAA France first; Section 508 & EN 301 549 next).",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.33.0",
@@ -6,7 +6,7 @@ import { join as join52, relative as relative6, resolve as resolve17, 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.43.1";
9
+ var VERSION = "5.43.2";
10
10
  var SCHEMA_VERSION = 2;
11
11
  var INAPPLICABLE_STATUS = "C";
12
12
 
@@ -62643,9 +62643,12 @@ var resumableItemKey = (it) => {
62643
62643
  return `${it.kind ?? "nc"}|${it.criteriaId}|${canonicalFile(it.file)}|${it.line}|${it.selector}|${it.claim}|${citationSetKey(it)}`;
62644
62644
  };
62645
62645
  var legacyConformityKey = (criteriaId, file, line, selector, claim) => `${criteriaId}|${canonicalFile(file)}|${line}|${selector}|${claim}`;
62646
- function verifyGroundingInputs(items) {
62646
+ function verifyGroundingInputs(items, fallbackFor) {
62647
62647
  return items.flatMap(
62648
- (it) => it.kind === "c" && it.citations?.length ? it.citations.map((cite) => ({ file: cite.file, line: cite.line, selector: cite.selector, snippet: cite.snippet })) : [{ file: it.file, line: it.line, selector: it.selector, snippet: it.snippet }]
62648
+ (it) => it.kind === "c" && it.citations?.length ? it.citations.map((cite) => {
62649
+ const fallback = fallbackFor?.(it, cite);
62650
+ return { file: cite.file, line: cite.line, selector: cite.selector, snippet: cite.snippet, ...fallback ? { fallback } : {} };
62651
+ }) : [{ file: it.file, line: it.line, selector: it.selector, snippet: it.snippet }]
62649
62652
  );
62650
62653
  }
62651
62654
  var PASSING = /* @__PURE__ */ new Set(["supported", "partial"]);
@@ -62783,7 +62786,11 @@ function groundFinding(g, opts = {}) {
62783
62786
  function groundItems(items, opts = {}) {
62784
62787
  const s = { grounded: 0, moved: 0, failed: 0, issues: [] };
62785
62788
  for (const it of items) {
62786
- const r = groundFinding(it, opts);
62789
+ let r = groundFinding(it, opts);
62790
+ if (!r.ok && it.fallback) {
62791
+ const viaAnchor = groundFinding(it.fallback, opts);
62792
+ if (viaAnchor.ok) r = { ok: true, moved: true };
62793
+ }
62787
62794
  if (r.ok) {
62788
62795
  s.grounded++;
62789
62796
  if (r.moved) s.moved++;
@@ -64210,6 +64217,23 @@ function buildAdjudicationWorklist(audit2, opts = {}) {
64210
64217
  (r) => blankItem(r.criteriaId, r.automatability, scTitle(r.criteriaId) ?? void 0, harvestSubjects(subjectsForSc2(r.criteriaId), docs), limits)
64211
64218
  );
64212
64219
  }
64220
+ function evidenceForCriteria(audit2, criteriaIds, opts = {}) {
64221
+ const limits = adjudicationLimits(opts.cwd);
64222
+ const docs = docsForAudit(audit2, opts.cwd);
64223
+ const standard = opts.standard;
64224
+ const pack = standard !== void 0 && !isCore(standard) ? loadPack(standard) : void 0;
64225
+ const candidates2 = new Map(pack && standard ? derivePackResults(audit2, standard).map((pc) => [pc.id, pc.candidateFindings ?? []]) : []);
64226
+ const out2 = /* @__PURE__ */ new Map();
64227
+ for (const id of criteriaIds) {
64228
+ if (out2.has(id)) continue;
64229
+ const crit = pack ? getCriterion(pack, id) : void 0;
64230
+ const subjects = pack && standard ? subjectsForPackCriterion2(standard, id, crit?.wcag ?? []) : subjectsForSc2(id);
64231
+ let evidence = collapse2(harvestSubjects(subjects, docs), limits).evidence;
64232
+ if (!evidence.length) evidence = candidateFindingEvidence(candidates2.get(id) ?? [], limits.maxClasses);
64233
+ out2.set(id, evidence);
64234
+ }
64235
+ return out2;
64236
+ }
64213
64237
  function unrenderedResidual(audit2, items) {
64214
64238
  const audited = audit2.scope.pagesAudited;
64215
64239
  const readSomePage = audited === void 0 ? (audit2.scope.pages ?? []).length > 0 : audited.length > 0;
@@ -64336,6 +64360,14 @@ function cites0(anchors, c2, drift) {
64336
64360
  }
64337
64361
  return false;
64338
64362
  }
64363
+ function evidenceFallback(evidence, c2, drift) {
64364
+ const anchor = anchorFor(evidence, c2, drift);
64365
+ if (!anchor || !recognisablySame(c2, anchor.at)) return { onAnchor: anchor !== void 0 };
64366
+ return {
64367
+ onAnchor: true,
64368
+ fallback: 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 }
64369
+ };
64370
+ }
64339
64371
  var NC_SEVERITY_DEFAULT = "majeur";
64340
64372
  var MANUAL_REASONS = /* @__PURE__ */ new Set(["needs-rendered-dom", "undecidable"]);
64341
64373
  var VERDICTS = ["C", "NC", "NA", "manual"];
@@ -64485,16 +64517,12 @@ function applyAdjudication(audit2, adj, opts = {}) {
64485
64517
  if (!cites0(anchors, c2, citationDrift) && !inAuditScope(c2.file)) {
64486
64518
  blame(it.criteriaId, `criterion ${it.criteriaId}: citation ${c2.file}:${c2.line} is not among this criterion's harvested evidence (fabricated?)`);
64487
64519
  }
64488
- const anchor = anchorFor(it.evidence, c2, citationDrift);
64489
64520
  const cite = { file: c2.file, line: c2.line, selector: c2.selector, snippet: c2.snippet };
64490
- if (anchor && recognisablySame(c2, anchor.at)) {
64491
- toGround(
64492
- it.criteriaId,
64493
- cite,
64494
- 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 }
64495
- );
64521
+ const { fallback, onAnchor } = evidenceFallback(it.evidence, c2, citationDrift);
64522
+ if (fallback) {
64523
+ toGround(it.criteriaId, cite, fallback);
64496
64524
  } else {
64497
- toGround(it.criteriaId, cite, void 0, anchor ? void 0 : offHarvestHint(it.criteriaId, it.evidence));
64525
+ toGround(it.criteriaId, cite, void 0, onAnchor ? void 0 : offHarvestHint(it.criteriaId, it.evidence));
64498
64526
  }
64499
64527
  }
64500
64528
  }
@@ -74398,6 +74426,11 @@ Options:
74398
74426
  (requires --report \u2014 coverage is re-derived from the report, uncapped)
74399
74427
  Also accepts an ADJUDICATION (--in <audit.json>), or a VERDICT LEDGER,
74400
74428
  which it REPLAYS onto the audit with no model in the loop
74429
+ Retained conformities are grounded the way the fold grounded them:
74430
+ a citation that is one of its criterion's own harvested anchors
74431
+ (--in <audit.json>) falls back to that anchor, so a retyping the
74432
+ fold accepted is not refused again at trial; without the audit the
74433
+ cited snippet must match the source literally
74401
74434
  --ledger <file> verify --apply / judge --apply: record the verdicts the fold ACCEPTED,
74402
74435
  so a later run replays them for free (default:
74403
74436
  .ultra11y/verdicts/<standard>.json). Replay re-derives the evidence
@@ -76267,7 +76300,7 @@ function cmdVerify(p) {
76267
76300
  const expected = [...expectedNc, ...buildConformityWorklist(conformityClaimsFor(p, standard, lang), expectedNc.length, Number.POSITIVE_INFINITY)];
76268
76301
  const r = applyVerdicts(items, expected);
76269
76302
  const passing = items.filter((it) => typeof it.verdict === "string" && ["supported", "partial"].includes(it.verdict.trim().toLowerCase()));
76270
- const grounding = groundItems(verifyGroundingInputs(passing));
76303
+ const grounding = groundItems(verifyGroundingInputs(passing, trialAnchorFallbacks(p, standard, passing)));
76271
76304
  const pruned = p.flags.prune === true ? applyPrune(p, standard, items, lang) : void 0;
76272
76305
  const ok = (pruned ? r.unadjudicated + r.invalid + r.missing === 0 : r.ok) && grounding.failed === 0;
76273
76306
  if (p.flags.json) console.log(JSON.stringify({ ...r, ok, grounding, ...pruned ? { pruned } : {} }, null, 2));
@@ -76425,6 +76458,21 @@ function cmdVerifyWorklist(p, langIn) {
76425
76458
  );
76426
76459
  return 0;
76427
76460
  }
76461
+ function trialAnchorFallbacks(p, standard, passing) {
76462
+ const ids = [...new Set(passing.filter((it) => it.kind === "c").map((it) => it.criteriaId))];
76463
+ const inFlag = p.flags.in;
76464
+ if (!ids.length || typeof inFlag !== "string" || !inFlag || inFlag === "-") return void 0;
76465
+ let audit2;
76466
+ try {
76467
+ audit2 = unwrapAudit(JSON.parse(readText(inFlag)));
76468
+ } catch {
76469
+ return void 0;
76470
+ }
76471
+ const cwd = typeof p.flags.cwd === "string" ? p.flags.cwd : void 0;
76472
+ const evidence = evidenceForCriteria(audit2, ids, { cwd, standard });
76473
+ const { citationDrift } = adjudicationLimits(cwd);
76474
+ return (it, cite) => evidenceFallback(evidence.get(it.criteriaId) ?? [], cite, citationDrift).fallback;
76475
+ }
76428
76476
  function conformityClaimsFor(p, standard, lang) {
76429
76477
  if (p.flags.conformities === false || p.flags["no-conformities"] === true) return [];
76430
76478
  const named3 = typeof p.flags.conformities === "string" && p.flags.conformities ? p.flags.conformities : void 0;
@@ -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.43.1
6
+ version: 5.43.2
7
7
  ---
8
8
 
9
9
  # review-a11y — accessibility review of changed code
@@ -6,7 +6,7 @@ import { join as join52, relative as relative6, resolve as resolve17, 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.43.1";
9
+ var VERSION = "5.43.2";
10
10
  var SCHEMA_VERSION = 2;
11
11
  var INAPPLICABLE_STATUS = "C";
12
12
 
@@ -62643,9 +62643,12 @@ var resumableItemKey = (it) => {
62643
62643
  return `${it.kind ?? "nc"}|${it.criteriaId}|${canonicalFile(it.file)}|${it.line}|${it.selector}|${it.claim}|${citationSetKey(it)}`;
62644
62644
  };
62645
62645
  var legacyConformityKey = (criteriaId, file, line, selector, claim) => `${criteriaId}|${canonicalFile(file)}|${line}|${selector}|${claim}`;
62646
- function verifyGroundingInputs(items) {
62646
+ function verifyGroundingInputs(items, fallbackFor) {
62647
62647
  return items.flatMap(
62648
- (it) => it.kind === "c" && it.citations?.length ? it.citations.map((cite) => ({ file: cite.file, line: cite.line, selector: cite.selector, snippet: cite.snippet })) : [{ file: it.file, line: it.line, selector: it.selector, snippet: it.snippet }]
62648
+ (it) => it.kind === "c" && it.citations?.length ? it.citations.map((cite) => {
62649
+ const fallback = fallbackFor?.(it, cite);
62650
+ return { file: cite.file, line: cite.line, selector: cite.selector, snippet: cite.snippet, ...fallback ? { fallback } : {} };
62651
+ }) : [{ file: it.file, line: it.line, selector: it.selector, snippet: it.snippet }]
62649
62652
  );
62650
62653
  }
62651
62654
  var PASSING = /* @__PURE__ */ new Set(["supported", "partial"]);
@@ -62783,7 +62786,11 @@ function groundFinding(g, opts = {}) {
62783
62786
  function groundItems(items, opts = {}) {
62784
62787
  const s = { grounded: 0, moved: 0, failed: 0, issues: [] };
62785
62788
  for (const it of items) {
62786
- const r = groundFinding(it, opts);
62789
+ let r = groundFinding(it, opts);
62790
+ if (!r.ok && it.fallback) {
62791
+ const viaAnchor = groundFinding(it.fallback, opts);
62792
+ if (viaAnchor.ok) r = { ok: true, moved: true };
62793
+ }
62787
62794
  if (r.ok) {
62788
62795
  s.grounded++;
62789
62796
  if (r.moved) s.moved++;
@@ -64210,6 +64217,23 @@ function buildAdjudicationWorklist(audit2, opts = {}) {
64210
64217
  (r) => blankItem(r.criteriaId, r.automatability, scTitle(r.criteriaId) ?? void 0, harvestSubjects(subjectsForSc2(r.criteriaId), docs), limits)
64211
64218
  );
64212
64219
  }
64220
+ function evidenceForCriteria(audit2, criteriaIds, opts = {}) {
64221
+ const limits = adjudicationLimits(opts.cwd);
64222
+ const docs = docsForAudit(audit2, opts.cwd);
64223
+ const standard = opts.standard;
64224
+ const pack = standard !== void 0 && !isCore(standard) ? loadPack(standard) : void 0;
64225
+ const candidates2 = new Map(pack && standard ? derivePackResults(audit2, standard).map((pc) => [pc.id, pc.candidateFindings ?? []]) : []);
64226
+ const out2 = /* @__PURE__ */ new Map();
64227
+ for (const id of criteriaIds) {
64228
+ if (out2.has(id)) continue;
64229
+ const crit = pack ? getCriterion(pack, id) : void 0;
64230
+ const subjects = pack && standard ? subjectsForPackCriterion2(standard, id, crit?.wcag ?? []) : subjectsForSc2(id);
64231
+ let evidence = collapse2(harvestSubjects(subjects, docs), limits).evidence;
64232
+ if (!evidence.length) evidence = candidateFindingEvidence(candidates2.get(id) ?? [], limits.maxClasses);
64233
+ out2.set(id, evidence);
64234
+ }
64235
+ return out2;
64236
+ }
64213
64237
  function unrenderedResidual(audit2, items) {
64214
64238
  const audited = audit2.scope.pagesAudited;
64215
64239
  const readSomePage = audited === void 0 ? (audit2.scope.pages ?? []).length > 0 : audited.length > 0;
@@ -64336,6 +64360,14 @@ function cites0(anchors, c2, drift) {
64336
64360
  }
64337
64361
  return false;
64338
64362
  }
64363
+ function evidenceFallback(evidence, c2, drift) {
64364
+ const anchor = anchorFor(evidence, c2, drift);
64365
+ if (!anchor || !recognisablySame(c2, anchor.at)) return { onAnchor: anchor !== void 0 };
64366
+ return {
64367
+ onAnchor: true,
64368
+ fallback: 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 }
64369
+ };
64370
+ }
64339
64371
  var NC_SEVERITY_DEFAULT = "majeur";
64340
64372
  var MANUAL_REASONS = /* @__PURE__ */ new Set(["needs-rendered-dom", "undecidable"]);
64341
64373
  var VERDICTS = ["C", "NC", "NA", "manual"];
@@ -64485,16 +64517,12 @@ function applyAdjudication(audit2, adj, opts = {}) {
64485
64517
  if (!cites0(anchors, c2, citationDrift) && !inAuditScope(c2.file)) {
64486
64518
  blame(it.criteriaId, `criterion ${it.criteriaId}: citation ${c2.file}:${c2.line} is not among this criterion's harvested evidence (fabricated?)`);
64487
64519
  }
64488
- const anchor = anchorFor(it.evidence, c2, citationDrift);
64489
64520
  const cite = { file: c2.file, line: c2.line, selector: c2.selector, snippet: c2.snippet };
64490
- if (anchor && recognisablySame(c2, anchor.at)) {
64491
- toGround(
64492
- it.criteriaId,
64493
- cite,
64494
- 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 }
64495
- );
64521
+ const { fallback, onAnchor } = evidenceFallback(it.evidence, c2, citationDrift);
64522
+ if (fallback) {
64523
+ toGround(it.criteriaId, cite, fallback);
64496
64524
  } else {
64497
- toGround(it.criteriaId, cite, void 0, anchor ? void 0 : offHarvestHint(it.criteriaId, it.evidence));
64525
+ toGround(it.criteriaId, cite, void 0, onAnchor ? void 0 : offHarvestHint(it.criteriaId, it.evidence));
64498
64526
  }
64499
64527
  }
64500
64528
  }
@@ -74398,6 +74426,11 @@ Options:
74398
74426
  (requires --report \u2014 coverage is re-derived from the report, uncapped)
74399
74427
  Also accepts an ADJUDICATION (--in <audit.json>), or a VERDICT LEDGER,
74400
74428
  which it REPLAYS onto the audit with no model in the loop
74429
+ Retained conformities are grounded the way the fold grounded them:
74430
+ a citation that is one of its criterion's own harvested anchors
74431
+ (--in <audit.json>) falls back to that anchor, so a retyping the
74432
+ fold accepted is not refused again at trial; without the audit the
74433
+ cited snippet must match the source literally
74401
74434
  --ledger <file> verify --apply / judge --apply: record the verdicts the fold ACCEPTED,
74402
74435
  so a later run replays them for free (default:
74403
74436
  .ultra11y/verdicts/<standard>.json). Replay re-derives the evidence
@@ -76267,7 +76300,7 @@ function cmdVerify(p) {
76267
76300
  const expected = [...expectedNc, ...buildConformityWorklist(conformityClaimsFor(p, standard, lang), expectedNc.length, Number.POSITIVE_INFINITY)];
76268
76301
  const r = applyVerdicts(items, expected);
76269
76302
  const passing = items.filter((it) => typeof it.verdict === "string" && ["supported", "partial"].includes(it.verdict.trim().toLowerCase()));
76270
- const grounding = groundItems(verifyGroundingInputs(passing));
76303
+ const grounding = groundItems(verifyGroundingInputs(passing, trialAnchorFallbacks(p, standard, passing)));
76271
76304
  const pruned = p.flags.prune === true ? applyPrune(p, standard, items, lang) : void 0;
76272
76305
  const ok = (pruned ? r.unadjudicated + r.invalid + r.missing === 0 : r.ok) && grounding.failed === 0;
76273
76306
  if (p.flags.json) console.log(JSON.stringify({ ...r, ok, grounding, ...pruned ? { pruned } : {} }, null, 2));
@@ -76425,6 +76458,21 @@ function cmdVerifyWorklist(p, langIn) {
76425
76458
  );
76426
76459
  return 0;
76427
76460
  }
76461
+ function trialAnchorFallbacks(p, standard, passing) {
76462
+ const ids = [...new Set(passing.filter((it) => it.kind === "c").map((it) => it.criteriaId))];
76463
+ const inFlag = p.flags.in;
76464
+ if (!ids.length || typeof inFlag !== "string" || !inFlag || inFlag === "-") return void 0;
76465
+ let audit2;
76466
+ try {
76467
+ audit2 = unwrapAudit(JSON.parse(readText(inFlag)));
76468
+ } catch {
76469
+ return void 0;
76470
+ }
76471
+ const cwd = typeof p.flags.cwd === "string" ? p.flags.cwd : void 0;
76472
+ const evidence = evidenceForCriteria(audit2, ids, { cwd, standard });
76473
+ const { citationDrift } = adjudicationLimits(cwd);
76474
+ return (it, cite) => evidenceFallback(evidence.get(it.criteriaId) ?? [], cite, citationDrift).fallback;
76475
+ }
76428
76476
  function conformityClaimsFor(p, standard, lang) {
76429
76477
  if (p.flags.conformities === false || p.flags["no-conformities"] === true) return [];
76430
76478
  const named3 = typeof p.flags.conformities === "string" && p.flags.conformities ? p.flags.conformities : void 0;
@@ -3,7 +3,7 @@ name: ultra11y
3
3
  description: "Use to AUDIT a repository, site, or page against WCAG 2.2 AA or a country standard such as RGAA; produce a dated conformance report, criterion grid, PRD backlog, or tickets; AUTHOR accessible markup; fix accessibility; or wire a repo gate and rendered-DOM capture pipeline. The bundled install-free engine runs 93 static checks tied to success criteria, while the agent adjudicates judgment criteria and routes rendering criteria to browser scans; check/verify reject invented non-conformities. Use `review-a11y` instead for a review scoped only to a diff, branch, or PR."
4
4
  license: MIT
5
5
  metadata:
6
- version: 5.43.1
6
+ version: 5.43.2
7
7
  ---
8
8
 
9
9
  # ultra11y — audit WCAG 2.2 AA and write accessible markup
@@ -55,10 +55,16 @@ so an agent can fill verdicts in-memory). **Prioritise `preliminary` findings**
55
55
  source, see `references/false-positives.md`): refute the ones the rendered DOM disproves.
56
56
  Then apply the gate:
57
57
  ```
58
- node scripts/ultra11y.mjs verify --apply VERIFY.todo.json
58
+ node scripts/ultra11y.mjs verify --apply VERIFY.todo.json --report audits/wcag-YYYY-MM-DD.md --in audit.json
59
59
  ```
60
- The gate fails (non-zero) if any entry is `refuted`, `unsupported`, or unadjudicated. Goal: no
61
- fabricated non-conformity survives into the final report.
60
+ The gate fails (non-zero) if any entry is `refuted`, `unsupported`, or unadjudicated. Every
61
+ retained claim is also re-grounded against the cited source. A conformity is grounded the way
62
+ the fold grounded it: a citation that is one of its criterion's own harvested anchors falls
63
+ back to that anchor (the engine read its snippet out of the file), so a retyping the fold
64
+ accepted at `verify --apply <adjudication>` is not refused again at trial — pass `--in
65
+ <audit.json>` so the harvest can be re-derived; a citation that is NOT one of the criterion's
66
+ anchors must still match the source literally. Goal: no fabricated non-conformity survives
67
+ into the final report, and no accepted conformity is re-refused one gate later.
62
68
 
63
69
  ## `verify --manual` — adjudicate the residual (judgment / needs-rendering) criteria
64
70
 
@@ -6,7 +6,7 @@ import { join as join52, relative as relative6, resolve as resolve17, 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.43.1";
9
+ var VERSION = "5.43.2";
10
10
  var SCHEMA_VERSION = 2;
11
11
  var INAPPLICABLE_STATUS = "C";
12
12
 
@@ -62643,9 +62643,12 @@ var resumableItemKey = (it) => {
62643
62643
  return `${it.kind ?? "nc"}|${it.criteriaId}|${canonicalFile(it.file)}|${it.line}|${it.selector}|${it.claim}|${citationSetKey(it)}`;
62644
62644
  };
62645
62645
  var legacyConformityKey = (criteriaId, file, line, selector, claim) => `${criteriaId}|${canonicalFile(file)}|${line}|${selector}|${claim}`;
62646
- function verifyGroundingInputs(items) {
62646
+ function verifyGroundingInputs(items, fallbackFor) {
62647
62647
  return items.flatMap(
62648
- (it) => it.kind === "c" && it.citations?.length ? it.citations.map((cite) => ({ file: cite.file, line: cite.line, selector: cite.selector, snippet: cite.snippet })) : [{ file: it.file, line: it.line, selector: it.selector, snippet: it.snippet }]
62648
+ (it) => it.kind === "c" && it.citations?.length ? it.citations.map((cite) => {
62649
+ const fallback = fallbackFor?.(it, cite);
62650
+ return { file: cite.file, line: cite.line, selector: cite.selector, snippet: cite.snippet, ...fallback ? { fallback } : {} };
62651
+ }) : [{ file: it.file, line: it.line, selector: it.selector, snippet: it.snippet }]
62649
62652
  );
62650
62653
  }
62651
62654
  var PASSING = /* @__PURE__ */ new Set(["supported", "partial"]);
@@ -62783,7 +62786,11 @@ function groundFinding(g, opts = {}) {
62783
62786
  function groundItems(items, opts = {}) {
62784
62787
  const s = { grounded: 0, moved: 0, failed: 0, issues: [] };
62785
62788
  for (const it of items) {
62786
- const r = groundFinding(it, opts);
62789
+ let r = groundFinding(it, opts);
62790
+ if (!r.ok && it.fallback) {
62791
+ const viaAnchor = groundFinding(it.fallback, opts);
62792
+ if (viaAnchor.ok) r = { ok: true, moved: true };
62793
+ }
62787
62794
  if (r.ok) {
62788
62795
  s.grounded++;
62789
62796
  if (r.moved) s.moved++;
@@ -64210,6 +64217,23 @@ function buildAdjudicationWorklist(audit2, opts = {}) {
64210
64217
  (r) => blankItem(r.criteriaId, r.automatability, scTitle(r.criteriaId) ?? void 0, harvestSubjects(subjectsForSc2(r.criteriaId), docs), limits)
64211
64218
  );
64212
64219
  }
64220
+ function evidenceForCriteria(audit2, criteriaIds, opts = {}) {
64221
+ const limits = adjudicationLimits(opts.cwd);
64222
+ const docs = docsForAudit(audit2, opts.cwd);
64223
+ const standard = opts.standard;
64224
+ const pack = standard !== void 0 && !isCore(standard) ? loadPack(standard) : void 0;
64225
+ const candidates2 = new Map(pack && standard ? derivePackResults(audit2, standard).map((pc) => [pc.id, pc.candidateFindings ?? []]) : []);
64226
+ const out2 = /* @__PURE__ */ new Map();
64227
+ for (const id of criteriaIds) {
64228
+ if (out2.has(id)) continue;
64229
+ const crit = pack ? getCriterion(pack, id) : void 0;
64230
+ const subjects = pack && standard ? subjectsForPackCriterion2(standard, id, crit?.wcag ?? []) : subjectsForSc2(id);
64231
+ let evidence = collapse2(harvestSubjects(subjects, docs), limits).evidence;
64232
+ if (!evidence.length) evidence = candidateFindingEvidence(candidates2.get(id) ?? [], limits.maxClasses);
64233
+ out2.set(id, evidence);
64234
+ }
64235
+ return out2;
64236
+ }
64213
64237
  function unrenderedResidual(audit2, items) {
64214
64238
  const audited = audit2.scope.pagesAudited;
64215
64239
  const readSomePage = audited === void 0 ? (audit2.scope.pages ?? []).length > 0 : audited.length > 0;
@@ -64336,6 +64360,14 @@ function cites0(anchors, c2, drift) {
64336
64360
  }
64337
64361
  return false;
64338
64362
  }
64363
+ function evidenceFallback(evidence, c2, drift) {
64364
+ const anchor = anchorFor(evidence, c2, drift);
64365
+ if (!anchor || !recognisablySame(c2, anchor.at)) return { onAnchor: anchor !== void 0 };
64366
+ return {
64367
+ onAnchor: true,
64368
+ fallback: 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 }
64369
+ };
64370
+ }
64339
64371
  var NC_SEVERITY_DEFAULT = "majeur";
64340
64372
  var MANUAL_REASONS = /* @__PURE__ */ new Set(["needs-rendered-dom", "undecidable"]);
64341
64373
  var VERDICTS = ["C", "NC", "NA", "manual"];
@@ -64485,16 +64517,12 @@ function applyAdjudication(audit2, adj, opts = {}) {
64485
64517
  if (!cites0(anchors, c2, citationDrift) && !inAuditScope(c2.file)) {
64486
64518
  blame(it.criteriaId, `criterion ${it.criteriaId}: citation ${c2.file}:${c2.line} is not among this criterion's harvested evidence (fabricated?)`);
64487
64519
  }
64488
- const anchor = anchorFor(it.evidence, c2, citationDrift);
64489
64520
  const cite = { file: c2.file, line: c2.line, selector: c2.selector, snippet: c2.snippet };
64490
- if (anchor && recognisablySame(c2, anchor.at)) {
64491
- toGround(
64492
- it.criteriaId,
64493
- cite,
64494
- 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 }
64495
- );
64521
+ const { fallback, onAnchor } = evidenceFallback(it.evidence, c2, citationDrift);
64522
+ if (fallback) {
64523
+ toGround(it.criteriaId, cite, fallback);
64496
64524
  } else {
64497
- toGround(it.criteriaId, cite, void 0, anchor ? void 0 : offHarvestHint(it.criteriaId, it.evidence));
64525
+ toGround(it.criteriaId, cite, void 0, onAnchor ? void 0 : offHarvestHint(it.criteriaId, it.evidence));
64498
64526
  }
64499
64527
  }
64500
64528
  }
@@ -74398,6 +74426,11 @@ Options:
74398
74426
  (requires --report \u2014 coverage is re-derived from the report, uncapped)
74399
74427
  Also accepts an ADJUDICATION (--in <audit.json>), or a VERDICT LEDGER,
74400
74428
  which it REPLAYS onto the audit with no model in the loop
74429
+ Retained conformities are grounded the way the fold grounded them:
74430
+ a citation that is one of its criterion's own harvested anchors
74431
+ (--in <audit.json>) falls back to that anchor, so a retyping the
74432
+ fold accepted is not refused again at trial; without the audit the
74433
+ cited snippet must match the source literally
74401
74434
  --ledger <file> verify --apply / judge --apply: record the verdicts the fold ACCEPTED,
74402
74435
  so a later run replays them for free (default:
74403
74436
  .ultra11y/verdicts/<standard>.json). Replay re-derives the evidence
@@ -76267,7 +76300,7 @@ function cmdVerify(p) {
76267
76300
  const expected = [...expectedNc, ...buildConformityWorklist(conformityClaimsFor(p, standard, lang), expectedNc.length, Number.POSITIVE_INFINITY)];
76268
76301
  const r = applyVerdicts(items, expected);
76269
76302
  const passing = items.filter((it) => typeof it.verdict === "string" && ["supported", "partial"].includes(it.verdict.trim().toLowerCase()));
76270
- const grounding = groundItems(verifyGroundingInputs(passing));
76303
+ const grounding = groundItems(verifyGroundingInputs(passing, trialAnchorFallbacks(p, standard, passing)));
76271
76304
  const pruned = p.flags.prune === true ? applyPrune(p, standard, items, lang) : void 0;
76272
76305
  const ok = (pruned ? r.unadjudicated + r.invalid + r.missing === 0 : r.ok) && grounding.failed === 0;
76273
76306
  if (p.flags.json) console.log(JSON.stringify({ ...r, ok, grounding, ...pruned ? { pruned } : {} }, null, 2));
@@ -76425,6 +76458,21 @@ function cmdVerifyWorklist(p, langIn) {
76425
76458
  );
76426
76459
  return 0;
76427
76460
  }
76461
+ function trialAnchorFallbacks(p, standard, passing) {
76462
+ const ids = [...new Set(passing.filter((it) => it.kind === "c").map((it) => it.criteriaId))];
76463
+ const inFlag = p.flags.in;
76464
+ if (!ids.length || typeof inFlag !== "string" || !inFlag || inFlag === "-") return void 0;
76465
+ let audit2;
76466
+ try {
76467
+ audit2 = unwrapAudit(JSON.parse(readText(inFlag)));
76468
+ } catch {
76469
+ return void 0;
76470
+ }
76471
+ const cwd = typeof p.flags.cwd === "string" ? p.flags.cwd : void 0;
76472
+ const evidence = evidenceForCriteria(audit2, ids, { cwd, standard });
76473
+ const { citationDrift } = adjudicationLimits(cwd);
76474
+ return (it, cite) => evidenceFallback(evidence.get(it.criteriaId) ?? [], cite, citationDrift).fallback;
76475
+ }
76428
76476
  function conformityClaimsFor(p, standard, lang) {
76429
76477
  if (p.flags.conformities === false || p.flags["no-conformities"] === true) return [];
76430
76478
  const named3 = typeof p.flags.conformities === "string" && p.flags.conformities ? p.flags.conformities : void 0;
@@ -27,7 +27,7 @@ variables:
27
27
  ULTRA11Y_LANG: en
28
28
  # Exact by default: a CI report must say which engine produced it. The release script keeps
29
29
  # this pin aligned with the package version; override only for a deliberate upgrade test.
30
- ULTRA11Y_VERSION: '5.43.1'
30
+ ULTRA11Y_VERSION: '5.43.2'
31
31
  # Batches of 8 amortise the prompt; each accepted verdict is checkpointed before the next
32
32
  # batch, and later passes receive only the residue.
33
33
  ULTRA11Y_GRAIN: batch
package/src/adjudicate.ts CHANGED
@@ -524,6 +524,40 @@ export function buildAdjudicationWorklist(audit: AuditResult, opts: { cwd?: stri
524
524
  );
525
525
  }
526
526
 
527
+ /** The harvested evidence behind ARBITRARY criteria — including ones a prior fold already
528
+ * decided, which `buildAdjudicationWorklist` filters out on purpose (a prior agent decision
529
+ * is never reopened). The trial (`verify --apply … --report`) needs them anyway: it grounds
530
+ * a retained claim the way the fold grounded it, and the fold's evidence-anchor fallback
531
+ * vouches only for a citation that is one of the criterion's OWN harvested anchors.
532
+ *
533
+ * Same derivation, item for item, as the worklist the verdict was gated against — the worklist
534
+ * is a pure function of the audit, so re-reading the audit's inputs rebuilds the very anchor
535
+ * set the citation was checked against. When a criterion's harvest is empty, the candidate
536
+ * browser-rule findings stand in, exactly as the worklist's items carry them. */
537
+ export function evidenceForCriteria(
538
+ audit: AuditResult,
539
+ criteriaIds: readonly string[],
540
+ opts: { cwd?: string; standard?: StandardId } = {},
541
+ ): Map<string, Evidence[]> {
542
+ const limits = adjudicationLimits(opts.cwd);
543
+ const docs = docsForAudit(audit, opts.cwd);
544
+ const standard = opts.standard;
545
+ const pack = standard !== undefined && !isCore(standard) ? loadPack(standard) : undefined;
546
+ // Candidate findings only exist in pack mode, and a criterion's own `wcag` mapping decides
547
+ // its subjects the same way `buildAdjudicationWorklist` lets it (its OWN subjects first).
548
+ const candidates = new Map(pack && standard ? derivePackResults(audit, standard).map((pc) => [pc.id, pc.candidateFindings ?? []] as const) : []);
549
+ const out = new Map<string, Evidence[]>();
550
+ for (const id of criteriaIds) {
551
+ if (out.has(id)) continue;
552
+ const crit = pack ? getCriterion(pack, id) : undefined;
553
+ const subjects = pack && standard ? subjectsForPackCriterion(standard, id, crit?.wcag ?? []) : subjectsForSc(id);
554
+ let evidence = collapse(harvestSubjects(subjects, docs), limits).evidence;
555
+ if (!evidence.length) evidence = candidateFindingEvidence(candidates.get(id) ?? [], limits.maxClasses);
556
+ out.set(id, evidence);
557
+ }
558
+ return out;
559
+ }
560
+
527
561
  /** THE CRITERIA THIS RUN COULD NOT POSSIBLY HAVE DECIDED — because nothing was rendered.
528
562
  *
529
563
  * `applyAdjudication` already refuses a `needs-rendered-dom` verdict whose own evidence sits
@@ -768,6 +802,32 @@ function cites0(anchors: string[], c: { file: string; line: number }, drift: num
768
802
  return false;
769
803
  }
770
804
 
805
+ /** The evidence-anchor stand-in the fold grants a citation: the harvested anchor of THIS
806
+ * criterion the citation resolves to, grounding the anchor's own snippet — the engine read
807
+ * it out of the file, so it is there by construction. A sibling from `alsoAt` carries no
808
+ * snippet (it is a bare `file:line`), so its stand-in is the cited place itself. Undefined
809
+ * fallback when the citation is off-harvest or claims a different KIND of element than the
810
+ * anchor holds — those keep the strict literal check (`onAnchor` says which, because only a
811
+ * genuinely off-harvest citation earns the off-harvest explanation).
812
+ *
813
+ * SHARED WITH THE TRIAL on purpose: `verify --apply … --report` grounds a retained claim
814
+ * through the very same function, so a conformity the fold accepted is not refused one gate
815
+ * later for the retyping the fold already forgave. */
816
+ export function evidenceFallback(
817
+ evidence: Evidence[],
818
+ c: { file: string; line: number; selector?: string; snippet?: string },
819
+ drift: number,
820
+ ): { fallback?: { file: string; line: number; selector?: string; snippet?: string }; onAnchor: boolean } {
821
+ const anchor = anchorFor(evidence, c, drift);
822
+ if (!anchor || !recognisablySame(c, anchor.at)) return { onAnchor: anchor !== undefined };
823
+ return {
824
+ onAnchor: true,
825
+ fallback: anchor.representative
826
+ ? { file: anchor.at.file, line: anchor.at.line, selector: anchor.at.selector ?? c.selector, snippet: anchor.at.snippet }
827
+ : { file: c.file, line: c.line, selector: anchor.at.selector ?? c.selector },
828
+ };
829
+ }
830
+
771
831
  export interface ApplyAdjudicationResult {
772
832
  ok: boolean;
773
833
  audit: AuditResult;
@@ -1115,16 +1175,10 @@ export function applyAdjudication(
1115
1175
  // It is authoritative there: the engine read it out of the file itself. And only
1116
1176
  // then does recognisability have anything to say, because only then is the anchor
1117
1177
  // being used to vouch for something the file did not confirm on its own.
1118
- const anchor = anchorFor(it.evidence, c, citationDrift);
1119
1178
  const cite = { file: c.file, line: c.line, selector: c.selector, snippet: c.snippet };
1120
- if (anchor && recognisablySame(c, anchor.at)) {
1121
- toGround(
1122
- it.criteriaId,
1123
- cite,
1124
- anchor.representative
1125
- ? { file: anchor.at.file, line: anchor.at.line, selector: anchor.at.selector ?? c.selector, snippet: anchor.at.snippet }
1126
- : { file: c.file, line: c.line, selector: anchor.at.selector ?? c.selector },
1127
- );
1179
+ const { fallback, onAnchor } = evidenceFallback(it.evidence, c, citationDrift);
1180
+ if (fallback) {
1181
+ toGround(it.criteriaId, cite, fallback);
1128
1182
  } else {
1129
1183
  // WHY THE STRICT PATH APPLIED, said at the point it applies.
1130
1184
  //
@@ -1149,7 +1203,7 @@ export function applyAdjudication(
1149
1203
  // be false: the anchor is right there, it is the element that is wrong. That case
1150
1204
  // keeps the bare refusal, which already says the honest thing — this markup is not
1151
1205
  // in the file.
1152
- toGround(it.criteriaId, cite, undefined, anchor ? undefined : offHarvestHint(it.criteriaId, it.evidence));
1206
+ toGround(it.criteriaId, cite, undefined, onAnchor ? undefined : offHarvestHint(it.criteriaId, it.evidence));
1153
1207
  }
1154
1208
  }
1155
1209
  }
package/src/cli.ts CHANGED
@@ -62,6 +62,9 @@ import {
62
62
  applyAdjudication,
63
63
  hydrateAdjudication,
64
64
  unrenderedResidual,
65
+ adjudicationLimits,
66
+ evidenceFallback,
67
+ evidenceForCriteria,
65
68
  type AdjudicationFile,
66
69
  } from "./adjudicate.js";
67
70
  import {
@@ -431,6 +434,11 @@ Options:
431
434
  (requires --report — coverage is re-derived from the report, uncapped)
432
435
  Also accepts an ADJUDICATION (--in <audit.json>), or a VERDICT LEDGER,
433
436
  which it REPLAYS onto the audit with no model in the loop
437
+ Retained conformities are grounded the way the fold grounded them:
438
+ a citation that is one of its criterion's own harvested anchors
439
+ (--in <audit.json>) falls back to that anchor, so a retyping the
440
+ fold accepted is not refused again at trial; without the audit the
441
+ cited snippet must match the source literally
434
442
  --ledger <file> verify --apply / judge --apply: record the verdicts the fold ACCEPTED,
435
443
  so a later run replays them for free (default:
436
444
  .ultra11y/verdicts/<standard>.json). Replay re-derives the evidence
@@ -2929,9 +2937,13 @@ function cmdVerify(p: ParsedArgs): number {
2929
2937
  const expected = [...expectedNc, ...buildConformityWorklist(conformityClaimsFor(p, standard, lang), expectedNc.length, Number.POSITIVE_INFINITY)];
2930
2938
  const r = applyVerdicts(items, expected);
2931
2939
  // Content-level grounding of every verdict that passed adjudication: the cited
2932
- // file/line/snippet must still exist and match the source (see src/grounding.ts).
2940
+ // file/line/snippet must still exist and match the source (see src/grounding.ts). A
2941
+ // RETAINED CONFORMITY IS GROUNDED THE WAY THE FOLD GROUNDED IT: a citation that is one of
2942
+ // its criterion's own harvested anchors falls back to the anchor, so a claim `verify
2943
+ // --apply <adjudication>` accepted is not refused again here — on every later trial —
2944
+ // for the retyping the fold already forgave.
2933
2945
  const passing = items.filter((it) => typeof it.verdict === "string" && ["supported", "partial"].includes(it.verdict.trim().toLowerCase()));
2934
- const grounding = groundItems(verifyGroundingInputs(passing));
2946
+ const grounding = groundItems(verifyGroundingInputs(passing, trialAnchorFallbacks(p, standard, passing)));
2935
2947
  // --prune: APPLY the outcome instead of only reporting it. See src/refute.ts for why a
2936
2948
  // gate that can only go red is the wrong shape for a cheap adjudicator's run, and for the
2937
2949
  // asymmetry between withdrawing a non-conformity and withdrawing a conformity.
@@ -3168,6 +3180,45 @@ function cmdVerifyWorklist(p: ParsedArgs, langIn: Lang): number {
3168
3180
  return 0;
3169
3181
  }
3170
3182
 
3183
+ /** The fold's evidence-anchor fallback, rebuilt for the trial (`verify --apply … --report`).
3184
+ *
3185
+ * The fold grounds a citation that is one of its criterion's own harvested anchors against
3186
+ * THAT ANCHOR when the literal check fails — an adjudicator's retyping of an element it did
3187
+ * read. The trial of the same report must not refuse the claim one gate later for the
3188
+ * retyping the fold already forgave, so it re-derives the harvest from the audit under `--in`
3189
+ * (the worklist evidence is a pure function of the audit) and grants the same fallback,
3190
+ * through the very same `evidenceFallback` the fold runs. A citation that is NOT one of the
3191
+ * criterion's anchors still fails strictly: without the audit there is nothing to re-derive
3192
+ * and nothing is granted.
3193
+ *
3194
+ * Returns the per-citation resolver `verifyGroundingInputs` applies; undefined when there is
3195
+ * nothing to resolve (no conformity on trial, no `--in`, unreadable audit). */
3196
+ function trialAnchorFallbacks(
3197
+ p: ParsedArgs,
3198
+ standard: StandardId,
3199
+ passing: VerifyItem[],
3200
+ ):
3201
+ | ((
3202
+ it: VerifyItem,
3203
+ cite: { file: string; line: number; selector?: string; snippet?: string },
3204
+ ) => { file: string; line: number; selector?: string; snippet?: string } | undefined)
3205
+ | undefined {
3206
+ const ids = [...new Set(passing.filter((it) => it.kind === "c").map((it) => it.criteriaId))];
3207
+ const inFlag = p.flags.in;
3208
+ if (!ids.length || typeof inFlag !== "string" || !inFlag || inFlag === "-") return undefined;
3209
+ let audit: AuditResult;
3210
+ try {
3211
+ audit = unwrapAudit(JSON.parse(readText(inFlag))) as AuditResult;
3212
+ } catch {
3213
+ // The command that owns --in reports malformed input; the trial stays strict.
3214
+ return undefined;
3215
+ }
3216
+ const cwd = typeof p.flags.cwd === "string" ? (p.flags.cwd as string) : undefined;
3217
+ const evidence = evidenceForCriteria(audit, ids, { cwd, standard });
3218
+ const { citationDrift } = adjudicationLimits(cwd);
3219
+ return (it, cite) => evidenceFallback(evidence.get(it.criteriaId) ?? [], cite, citationDrift).fallback;
3220
+ }
3221
+
3171
3222
  /**
3172
3223
  * The claimed conformities to put on trial, from the verdict ledger (or an adjudication file).
3173
3224
  *
package/src/grounding.ts CHANGED
@@ -13,6 +13,11 @@ export interface GroundingInput {
13
13
  line: number;
14
14
  snippet?: string;
15
15
  selector?: string;
16
+ /** The criterion's harvested anchor standing in for this citation when the literal check
17
+ * fails — the evidence-anchor fallback the fold grants a citation that is one of the
18
+ * criterion's own harvested anchors (see src/adjudicate.ts). Optional; absent on the
19
+ * strict inputs, which must ground literally. */
20
+ fallback?: GroundingInput;
16
21
  }
17
22
 
18
23
  export interface GroundingVerdict {
@@ -121,7 +126,14 @@ export function groundFinding(g: GroundingInput, opts: { cwd?: string } = {}): G
121
126
  export function groundItems(items: GroundingInput[], opts: { cwd?: string } = {}): GroundingSummary {
122
127
  const s: GroundingSummary = { grounded: 0, moved: 0, failed: 0, issues: [] };
123
128
  for (const it of items) {
124
- const r = groundFinding(it, opts);
129
+ let r = groundFinding(it, opts);
130
+ // The harvested anchor vouches for a citation the file could not confirm on its own —
131
+ // an adjudicator's retyping of an element it did read. `moved` either way: what grounded
132
+ // is not verbatim what was cited (same contract as the fold, src/adjudicate.ts).
133
+ if (!r.ok && it.fallback) {
134
+ const viaAnchor = groundFinding(it.fallback, opts);
135
+ if (viaAnchor.ok) r = { ok: true, moved: true };
136
+ }
125
137
  if (r.ok) {
126
138
  s.grounded++;
127
139
  if (r.moved) s.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.43.1";
3
+ export const VERSION = "5.43.2";
4
4
  export const SCHEMA_VERSION = 2;
5
5
 
6
6
  // Lang = the UI FRAME's language — the `L` localization tables each module (report,
package/src/verify.ts CHANGED
@@ -465,11 +465,20 @@ const resumableItemKey = (it: VerifyItem): string => {
465
465
  const legacyConformityKey = (criteriaId: string, file: string, line: number, selector: string, claim: string): string =>
466
466
  `${criteriaId}|${canonicalFile(file)}|${line}|${selector}|${claim}`;
467
467
 
468
- /** Every source anchor a passing claim asks the grounding gate to re-open. */
469
- export function verifyGroundingInputs(items: VerifyItem[]): Array<{ file: string; line: number; selector?: string; snippet?: string }> {
468
+ /** Every source anchor a passing claim asks the grounding gate to re-open. `fallbackFor`
469
+ * optionally attaches, per cited conformity, the harvested anchor that may stand in when the
470
+ * literal check fails — the fold's evidence-anchor fallback (src/adjudicate.ts), so a claim
471
+ * the fold accepted is grounded the same way here. */
472
+ export function verifyGroundingInputs(
473
+ items: VerifyItem[],
474
+ fallbackFor?: (it: VerifyItem, cite: CriterionCitation) => { file: string; line: number; selector?: string; snippet?: string } | undefined,
475
+ ): Array<{ file: string; line: number; selector?: string; snippet?: string; fallback?: { file: string; line: number; selector?: string; snippet?: string } }> {
470
476
  return items.flatMap((it) =>
471
477
  it.kind === "c" && it.citations?.length
472
- ? it.citations.map((cite) => ({ file: cite.file, line: cite.line, selector: cite.selector, snippet: cite.snippet }))
478
+ ? it.citations.map((cite) => {
479
+ const fallback = fallbackFor?.(it, cite);
480
+ return { file: cite.file, line: cite.line, selector: cite.selector, snippet: cite.snippet, ...(fallback ? { fallback } : {}) };
481
+ })
473
482
  : [{ file: it.file, line: it.line, selector: it.selector, snippet: (it as { snippet?: string }).snippet }],
474
483
  );
475
484
  }