ultra11y 5.14.1 → 5.14.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.14.1",
13
+ "version": "5.14.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.14.1",
4
+ "version": "5.14.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.14.1",
4
+ "version": "5.14.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.14.1";
30
+ const ULTRA11Y_PLUGIN_VERSION = "5.14.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.14.1",
3
+ "version": "5.14.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.14.1";
9
+ var VERSION = "5.14.3";
10
10
  var SCHEMA_VERSION = 2;
11
11
  var INAPPLICABLE_STATUS = "C";
12
12
 
@@ -19918,6 +19918,7 @@ function isUrlPath(file) {
19918
19918
  return /^https?:\/\//i.test(file);
19919
19919
  }
19920
19920
  function repoRelative(file, baseDir) {
19921
+ if (typeof file !== "string" || !file) return "";
19921
19922
  const posix3 = file.split("\\").join("/").replace(/^\.\//, "");
19922
19923
  const base = baseDir.split("\\").join("/").replace(/\/+$/, "");
19923
19924
  if (base && posix3.startsWith(`${base}/`)) return posix3.slice(base.length + 1);
@@ -56863,12 +56864,27 @@ function sectionBody(md, n) {
56863
56864
  function checkSampleCaptured(root = ".", lang = "en") {
56864
56865
  const fr = lang === "fr";
56865
56866
  let declared = [];
56867
+ const rc = join34(root, ".ultra11yrc.json");
56868
+ let raw;
56866
56869
  try {
56867
- const cfg = JSON.parse(readFileSync18(join34(root, ".ultra11yrc.json"), "utf8"));
56868
- declared = cfg.sample?.pages ?? [];
56870
+ raw = readFileSync18(rc, "utf8");
56869
56871
  } catch {
56870
56872
  return { ok: true, issues: [], missing: [], declared: 0, captured: 0 };
56871
56873
  }
56874
+ try {
56875
+ const cfg = JSON.parse(raw);
56876
+ declared = cfg.sample?.pages ?? [];
56877
+ } catch (e) {
56878
+ return {
56879
+ ok: false,
56880
+ issues: [
56881
+ fr ? `.ultra11yrc.json est pr\xE9sent mais illisible (${e instanceof Error ? e.message : String(e)}) \u2014 impossible de savoir quelles pages ce run devait couvrir. La porte de couverture ne peut pas r\xE9pondre, et ne r\xE9pondra donc pas \xAB couvert \xBB.` : `.ultra11yrc.json is present but unreadable (${e instanceof Error ? e.message : String(e)}) \u2014 there is no way to know which pages this run was meant to cover. The coverage gate cannot answer, so it does not answer "covered".`
56882
+ ],
56883
+ missing: [],
56884
+ declared: 0,
56885
+ captured: 0
56886
+ };
56887
+ }
56872
56888
  if (!declared.length) return { ok: true, issues: [], missing: [], declared: 0, captured: 0 };
56873
56889
  let captured = /* @__PURE__ */ new Set();
56874
56890
  try {
@@ -58072,6 +58088,13 @@ function applyAdjudication(audit2, adj, opts = {}) {
58072
58088
  } else if (v === "NC") {
58073
58089
  if (!it.findings || it.findings.length === 0) blame(it.criteriaId, `criterion ${it.criteriaId}: an NC verdict requires at least one groundable finding`);
58074
58090
  for (const f of it.findings ?? []) {
58091
+ if (typeof f.file !== "string" || !f.file.trim()) {
58092
+ blame(
58093
+ it.criteriaId,
58094
+ `criterion ${it.criteriaId}: an NC finding must name the file it was observed in \u2014 nobody can act on a non-conformity with no location`
58095
+ );
58096
+ continue;
58097
+ }
58075
58098
  if (!f.normativeRef || !f.normativeRef.trim()) {
58076
58099
  blame(it.criteriaId, `criterion ${it.criteriaId}: an NC finding requires a normativeRef citing the failed test of the active standard`);
58077
58100
  } else if (!normativeRefResolves(f.normativeRef, adj.standard, isCore(adj.standard) ? void 0 : it.criteriaId)) {
@@ -58088,7 +58111,13 @@ function applyAdjudication(audit2, adj, opts = {}) {
58088
58111
  } else {
58089
58112
  blame(it.criteriaId, `criterion ${it.criteriaId}: unknown verdict "${String(v)}" \u2014 expected one of ${VERDICTS.join(" | ")}`);
58090
58113
  }
58091
- for (const rec of it.recommendations ?? []) toGround(it.criteriaId, { file: rec.file, line: rec.line, selector: rec.selector, snippet: rec.snippet });
58114
+ for (const rec of it.recommendations ?? []) {
58115
+ if (typeof rec.file !== "string" || !rec.file.trim()) {
58116
+ blame(it.criteriaId, `criterion ${it.criteriaId}: a recommendation must name the file it was observed in`);
58117
+ continue;
58118
+ }
58119
+ toGround(it.criteriaId, { file: rec.file, line: rec.line, selector: rec.selector, snippet: rec.snippet });
58120
+ }
58092
58121
  }
58093
58122
  const grounding = { grounded: 0, moved: 0, failed: 0, issues: [] };
58094
58123
  for (const [criteriaId, inputs] of groundInputs) {
@@ -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.14.1
8
+ version: 5.14.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.14.1";
9
+ var VERSION = "5.14.3";
10
10
  var SCHEMA_VERSION = 2;
11
11
  var INAPPLICABLE_STATUS = "C";
12
12
 
@@ -19918,6 +19918,7 @@ function isUrlPath(file) {
19918
19918
  return /^https?:\/\//i.test(file);
19919
19919
  }
19920
19920
  function repoRelative(file, baseDir) {
19921
+ if (typeof file !== "string" || !file) return "";
19921
19922
  const posix3 = file.split("\\").join("/").replace(/^\.\//, "");
19922
19923
  const base = baseDir.split("\\").join("/").replace(/\/+$/, "");
19923
19924
  if (base && posix3.startsWith(`${base}/`)) return posix3.slice(base.length + 1);
@@ -56863,12 +56864,27 @@ function sectionBody(md, n) {
56863
56864
  function checkSampleCaptured(root = ".", lang = "en") {
56864
56865
  const fr = lang === "fr";
56865
56866
  let declared = [];
56867
+ const rc = join34(root, ".ultra11yrc.json");
56868
+ let raw;
56866
56869
  try {
56867
- const cfg = JSON.parse(readFileSync18(join34(root, ".ultra11yrc.json"), "utf8"));
56868
- declared = cfg.sample?.pages ?? [];
56870
+ raw = readFileSync18(rc, "utf8");
56869
56871
  } catch {
56870
56872
  return { ok: true, issues: [], missing: [], declared: 0, captured: 0 };
56871
56873
  }
56874
+ try {
56875
+ const cfg = JSON.parse(raw);
56876
+ declared = cfg.sample?.pages ?? [];
56877
+ } catch (e) {
56878
+ return {
56879
+ ok: false,
56880
+ issues: [
56881
+ fr ? `.ultra11yrc.json est pr\xE9sent mais illisible (${e instanceof Error ? e.message : String(e)}) \u2014 impossible de savoir quelles pages ce run devait couvrir. La porte de couverture ne peut pas r\xE9pondre, et ne r\xE9pondra donc pas \xAB couvert \xBB.` : `.ultra11yrc.json is present but unreadable (${e instanceof Error ? e.message : String(e)}) \u2014 there is no way to know which pages this run was meant to cover. The coverage gate cannot answer, so it does not answer "covered".`
56882
+ ],
56883
+ missing: [],
56884
+ declared: 0,
56885
+ captured: 0
56886
+ };
56887
+ }
56872
56888
  if (!declared.length) return { ok: true, issues: [], missing: [], declared: 0, captured: 0 };
56873
56889
  let captured = /* @__PURE__ */ new Set();
56874
56890
  try {
@@ -58072,6 +58088,13 @@ function applyAdjudication(audit2, adj, opts = {}) {
58072
58088
  } else if (v === "NC") {
58073
58089
  if (!it.findings || it.findings.length === 0) blame(it.criteriaId, `criterion ${it.criteriaId}: an NC verdict requires at least one groundable finding`);
58074
58090
  for (const f of it.findings ?? []) {
58091
+ if (typeof f.file !== "string" || !f.file.trim()) {
58092
+ blame(
58093
+ it.criteriaId,
58094
+ `criterion ${it.criteriaId}: an NC finding must name the file it was observed in \u2014 nobody can act on a non-conformity with no location`
58095
+ );
58096
+ continue;
58097
+ }
58075
58098
  if (!f.normativeRef || !f.normativeRef.trim()) {
58076
58099
  blame(it.criteriaId, `criterion ${it.criteriaId}: an NC finding requires a normativeRef citing the failed test of the active standard`);
58077
58100
  } else if (!normativeRefResolves(f.normativeRef, adj.standard, isCore(adj.standard) ? void 0 : it.criteriaId)) {
@@ -58088,7 +58111,13 @@ function applyAdjudication(audit2, adj, opts = {}) {
58088
58111
  } else {
58089
58112
  blame(it.criteriaId, `criterion ${it.criteriaId}: unknown verdict "${String(v)}" \u2014 expected one of ${VERDICTS.join(" | ")}`);
58090
58113
  }
58091
- for (const rec of it.recommendations ?? []) toGround(it.criteriaId, { file: rec.file, line: rec.line, selector: rec.selector, snippet: rec.snippet });
58114
+ for (const rec of it.recommendations ?? []) {
58115
+ if (typeof rec.file !== "string" || !rec.file.trim()) {
58116
+ blame(it.criteriaId, `criterion ${it.criteriaId}: a recommendation must name the file it was observed in`);
58117
+ continue;
58118
+ }
58119
+ toGround(it.criteriaId, { file: rec.file, line: rec.line, selector: rec.selector, snippet: rec.snippet });
58120
+ }
58092
58121
  }
58093
58122
  const grounding = { grounded: 0, moved: 0, failed: 0, issues: [] };
58094
58123
  for (const [criteriaId, inputs] of groundInputs) {
@@ -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.14.1
7
+ version: 5.14.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.14.1";
9
+ var VERSION = "5.14.3";
10
10
  var SCHEMA_VERSION = 2;
11
11
  var INAPPLICABLE_STATUS = "C";
12
12
 
@@ -19918,6 +19918,7 @@ function isUrlPath(file) {
19918
19918
  return /^https?:\/\//i.test(file);
19919
19919
  }
19920
19920
  function repoRelative(file, baseDir) {
19921
+ if (typeof file !== "string" || !file) return "";
19921
19922
  const posix3 = file.split("\\").join("/").replace(/^\.\//, "");
19922
19923
  const base = baseDir.split("\\").join("/").replace(/\/+$/, "");
19923
19924
  if (base && posix3.startsWith(`${base}/`)) return posix3.slice(base.length + 1);
@@ -56863,12 +56864,27 @@ function sectionBody(md, n) {
56863
56864
  function checkSampleCaptured(root = ".", lang = "en") {
56864
56865
  const fr = lang === "fr";
56865
56866
  let declared = [];
56867
+ const rc = join34(root, ".ultra11yrc.json");
56868
+ let raw;
56866
56869
  try {
56867
- const cfg = JSON.parse(readFileSync18(join34(root, ".ultra11yrc.json"), "utf8"));
56868
- declared = cfg.sample?.pages ?? [];
56870
+ raw = readFileSync18(rc, "utf8");
56869
56871
  } catch {
56870
56872
  return { ok: true, issues: [], missing: [], declared: 0, captured: 0 };
56871
56873
  }
56874
+ try {
56875
+ const cfg = JSON.parse(raw);
56876
+ declared = cfg.sample?.pages ?? [];
56877
+ } catch (e) {
56878
+ return {
56879
+ ok: false,
56880
+ issues: [
56881
+ fr ? `.ultra11yrc.json est pr\xE9sent mais illisible (${e instanceof Error ? e.message : String(e)}) \u2014 impossible de savoir quelles pages ce run devait couvrir. La porte de couverture ne peut pas r\xE9pondre, et ne r\xE9pondra donc pas \xAB couvert \xBB.` : `.ultra11yrc.json is present but unreadable (${e instanceof Error ? e.message : String(e)}) \u2014 there is no way to know which pages this run was meant to cover. The coverage gate cannot answer, so it does not answer "covered".`
56882
+ ],
56883
+ missing: [],
56884
+ declared: 0,
56885
+ captured: 0
56886
+ };
56887
+ }
56872
56888
  if (!declared.length) return { ok: true, issues: [], missing: [], declared: 0, captured: 0 };
56873
56889
  let captured = /* @__PURE__ */ new Set();
56874
56890
  try {
@@ -58072,6 +58088,13 @@ function applyAdjudication(audit2, adj, opts = {}) {
58072
58088
  } else if (v === "NC") {
58073
58089
  if (!it.findings || it.findings.length === 0) blame(it.criteriaId, `criterion ${it.criteriaId}: an NC verdict requires at least one groundable finding`);
58074
58090
  for (const f of it.findings ?? []) {
58091
+ if (typeof f.file !== "string" || !f.file.trim()) {
58092
+ blame(
58093
+ it.criteriaId,
58094
+ `criterion ${it.criteriaId}: an NC finding must name the file it was observed in \u2014 nobody can act on a non-conformity with no location`
58095
+ );
58096
+ continue;
58097
+ }
58075
58098
  if (!f.normativeRef || !f.normativeRef.trim()) {
58076
58099
  blame(it.criteriaId, `criterion ${it.criteriaId}: an NC finding requires a normativeRef citing the failed test of the active standard`);
58077
58100
  } else if (!normativeRefResolves(f.normativeRef, adj.standard, isCore(adj.standard) ? void 0 : it.criteriaId)) {
@@ -58088,7 +58111,13 @@ function applyAdjudication(audit2, adj, opts = {}) {
58088
58111
  } else {
58089
58112
  blame(it.criteriaId, `criterion ${it.criteriaId}: unknown verdict "${String(v)}" \u2014 expected one of ${VERDICTS.join(" | ")}`);
58090
58113
  }
58091
- for (const rec of it.recommendations ?? []) toGround(it.criteriaId, { file: rec.file, line: rec.line, selector: rec.selector, snippet: rec.snippet });
58114
+ for (const rec of it.recommendations ?? []) {
58115
+ if (typeof rec.file !== "string" || !rec.file.trim()) {
58116
+ blame(it.criteriaId, `criterion ${it.criteriaId}: a recommendation must name the file it was observed in`);
58117
+ continue;
58118
+ }
58119
+ toGround(it.criteriaId, { file: rec.file, line: rec.line, selector: rec.selector, snippet: rec.snippet });
58120
+ }
58092
58121
  }
58093
58122
  const grounding = { grounded: 0, moved: 0, failed: 0, issues: [] };
58094
58123
  for (const [criteriaId, inputs] of groundInputs) {
package/src/adjudicate.ts CHANGED
@@ -879,6 +879,18 @@ export function applyAdjudication(
879
879
  } else if (v === "NC") {
880
880
  if (!it.findings || it.findings.length === 0) blame(it.criteriaId, `criterion ${it.criteriaId}: an NC verdict requires at least one groundable finding`);
881
881
  for (const f of it.findings ?? []) {
882
+ // AN NC NOBODY CAN OPEN IS NOT A FINDING. Checked before the normativeRef, because a
883
+ // finding with no file cannot be grounded at all — there is nothing to go and read.
884
+ // Measured once: a model returned an NC with no `file`, the fold minted it anyway, and
885
+ // it reached `repoRelative`, which crashed on `undefined.split` and took SARIF, the
886
+ // annotations, the comment, the report, the HTML and the artifact upload with it.
887
+ if (typeof f.file !== "string" || !f.file.trim()) {
888
+ blame(
889
+ it.criteriaId,
890
+ `criterion ${it.criteriaId}: an NC finding must name the file it was observed in — nobody can act on a non-conformity with no location`,
891
+ );
892
+ continue;
893
+ }
882
894
  // FAIL-CLOSED: every NC finding must cite a precise, resolvable test of the active
883
895
  // standard. A good practice with no normative test is a recommendation, not an NC.
884
896
  if (!f.normativeRef || !f.normativeRef.trim()) {
@@ -905,7 +917,15 @@ export function applyAdjudication(
905
917
  // Recommendations are independent of the verdict (a C criterion may still carry a good
906
918
  // practice) and are grounded exactly like an NC finding — no normativeRef required, as
907
919
  // a recommendation has no normative test by definition.
908
- for (const rec of it.recommendations ?? []) toGround(it.criteriaId, { file: rec.file, line: rec.line, selector: rec.selector, snippet: rec.snippet });
920
+ for (const rec of it.recommendations ?? []) {
921
+ // Same contract, same reason: a recommendation is rendered through the very surfaces an
922
+ // NC is, so one with no location breaks them just as thoroughly.
923
+ if (typeof rec.file !== "string" || !rec.file.trim()) {
924
+ blame(it.criteriaId, `criterion ${it.criteriaId}: a recommendation must name the file it was observed in`);
925
+ continue;
926
+ }
927
+ toGround(it.criteriaId, { file: rec.file, line: rec.line, selector: rec.selector, snippet: rec.snippet });
928
+ }
909
929
  }
910
930
 
911
931
  // Content-level grounding of every agent NC finding, every C/NA citation, and every
package/src/check.ts CHANGED
@@ -362,14 +362,36 @@ export interface SampleCoverage {
362
362
  export function checkSampleCaptured(root = ".", lang: Lang = "en"): SampleCoverage {
363
363
  const fr = lang === "fr";
364
364
  let declared: { id: string; name?: string; url?: string }[] = [];
365
+ const rc = join(root, ".ultra11yrc.json");
366
+ let raw: string;
365
367
  try {
366
- const cfg = JSON.parse(readFileSync(join(root, ".ultra11yrc.json"), "utf8")) as {
367
- sample?: { pages?: { id: string; name?: string; url?: string }[] };
368
- };
369
- declared = cfg.sample?.pages ?? [];
368
+ raw = readFileSync(rc, "utf8");
370
369
  } catch {
370
+ // NO CONFIG AT ALL — nothing is declared, so there is nothing to answer for. This is the
371
+ // one silence that may pass: a gate that fails on its own absence is a gate that gets
372
+ // turned off.
371
373
  return { ok: true, issues: [], missing: [], declared: 0, captured: 0 };
372
374
  }
375
+ try {
376
+ const cfg = JSON.parse(raw) as { sample?: { pages?: { id: string; name?: string; url?: string }[] } };
377
+ declared = cfg.sample?.pages ?? [];
378
+ } catch (e) {
379
+ // A CONFIG THAT EXISTS AND CANNOT BE READ is the opposite case, and it must not pass. The
380
+ // file is where the sample is declared, so an unparseable one means this gate does not know
381
+ // what the run was supposed to cover — and answering "covered" to that is how a gate
382
+ // disarms itself at exactly the moment something is wrong.
383
+ return {
384
+ ok: false,
385
+ issues: [
386
+ fr
387
+ ? `.ultra11yrc.json est présent mais illisible (${e instanceof Error ? e.message : String(e)}) — impossible de savoir quelles pages ce run devait couvrir. La porte de couverture ne peut pas répondre, et ne répondra donc pas « couvert ».`
388
+ : `.ultra11yrc.json is present but unreadable (${e instanceof Error ? e.message : String(e)}) — there is no way to know which pages this run was meant to cover. The coverage gate cannot answer, so it does not answer "covered".`,
389
+ ],
390
+ missing: [],
391
+ declared: 0,
392
+ captured: 0,
393
+ };
394
+ }
373
395
  if (!declared.length) return { ok: true, issues: [], missing: [], declared: 0, captured: 0 };
374
396
 
375
397
  let captured = new Set<string>();
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.14.1";
3
+ export const VERSION = "5.14.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,
package/src/util.ts CHANGED
@@ -36,6 +36,13 @@ export function isUrlPath(file: string): boolean {
36
36
  * under `baseDir`; leave it as-is when it does not, rather than inventing a path that
37
37
  * resolves nowhere. */
38
38
  export function repoRelative(file: string, baseDir: string): string {
39
+ // A finding with no file at all. It should never reach here — the adjudication fold refuses
40
+ // an NC that names none — but an AuditResult can also be hand-written or produced by an
41
+ // external tool's adapter, and neither goes through that gate. Measured once: one such
42
+ // finding threw here and took SARIF, the annotations, the pull-request comment, the report,
43
+ // the HTML and the artifact upload with it, because every one of those steps runs after.
44
+ // A rendering surface that explodes on bad input tells its reader nothing at all.
45
+ if (typeof file !== "string" || !file) return "";
39
46
  const posix = file.split("\\").join("/").replace(/^\.\//, "");
40
47
  const base = baseDir.split("\\").join("/").replace(/\/+$/, "");
41
48
  if (base && posix.startsWith(`${base}/`)) return posix.slice(base.length + 1);