@binclusive/a11y 0.1.0

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.
Files changed (80) hide show
  1. package/README.md +285 -0
  2. package/bin/a11y.mjs +36 -0
  3. package/bin/diff-scope.mjs +29 -0
  4. package/data/baseline-rules.json +892 -0
  5. package/package.json +68 -0
  6. package/src/agent-lane.ts +138 -0
  7. package/src/agents-block.ts +157 -0
  8. package/src/baseline/gen-baseline.ts +166 -0
  9. package/src/cli.ts +1026 -0
  10. package/src/collect-dom.ts +119 -0
  11. package/src/collect-liquid.ts +103 -0
  12. package/src/collect-swift.ts +227 -0
  13. package/src/collect-unity.ts +99 -0
  14. package/src/collect.ts +54 -0
  15. package/src/commands.ts +314 -0
  16. package/src/config-scan.ts +177 -0
  17. package/src/contract.ts +355 -0
  18. package/src/core.ts +546 -0
  19. package/src/detect-stack.ts +207 -0
  20. package/src/diff-scope-cli.ts +12 -0
  21. package/src/diff-scope.ts +150 -0
  22. package/src/emit-contract.ts +181 -0
  23. package/src/enforce.ts +1125 -0
  24. package/src/eslint-plugin-jsx-a11y.d.ts +11 -0
  25. package/src/evidence.ts +308 -0
  26. package/src/github-identity.ts +201 -0
  27. package/src/hook.ts +242 -0
  28. package/src/impact-gate.ts +107 -0
  29. package/src/imports-resolve.ts +248 -0
  30. package/src/index.ts +183 -0
  31. package/src/liquid-ast.ts +203 -0
  32. package/src/liquid-rules.ts +691 -0
  33. package/src/mcp.ts +363 -0
  34. package/src/module-scope.ts +137 -0
  35. package/src/phone-home.ts +470 -0
  36. package/src/pr-comment.ts +250 -0
  37. package/src/pr-summary-cli.ts +182 -0
  38. package/src/pr-summary.ts +291 -0
  39. package/src/registry.ts +605 -0
  40. package/src/reporter/contract.ts +154 -0
  41. package/src/reporter/finding.ts +87 -0
  42. package/src/reporter/github-adapter.ts +183 -0
  43. package/src/reporter/null-adapter.ts +51 -0
  44. package/src/reporter/registry.ts +22 -0
  45. package/src/reporter-cli.ts +48 -0
  46. package/src/resolve-components.ts +579 -0
  47. package/src/runner/budget.ts +90 -0
  48. package/src/runner/codegraph-lookup.test.ts +93 -0
  49. package/src/runner/codegraph-lookup.ts +197 -0
  50. package/src/runner/index.ts +86 -0
  51. package/src/runner/lookup.ts +69 -0
  52. package/src/runner/provider.ts +72 -0
  53. package/src/runner/providers/anthropic.ts +168 -0
  54. package/src/runner/providers/openai.ts +191 -0
  55. package/src/runner/reasoner.ts +73 -0
  56. package/src/runner/reasoning/index.ts +53 -0
  57. package/src/runner/reasoning/prompt.ts +200 -0
  58. package/src/runner/reasoning/react.ts +149 -0
  59. package/src/runner/reasoning/shopify.ts +214 -0
  60. package/src/runner/reasoning/skills-reasoner.ts +117 -0
  61. package/src/runner/reasoning/types.ts +99 -0
  62. package/src/runner/runner.ts +203 -0
  63. package/src/sarif.ts +148 -0
  64. package/src/source-identity.ts +0 -0
  65. package/src/source-trace.ts +814 -0
  66. package/src/suggest.ts +328 -0
  67. package/src/suppression-ranges.ts +354 -0
  68. package/src/suppressor-map.ts +189 -0
  69. package/src/suppressors.ts +284 -0
  70. package/src/tsconfig-aliases.ts +155 -0
  71. package/src/unity-ast.ts +331 -0
  72. package/src/unity-findings.ts +91 -0
  73. package/src/unity-guid-registry.ts +82 -0
  74. package/src/unity-label-resolve.ts +249 -0
  75. package/src/unity-rule-color-only.ts +127 -0
  76. package/src/unity-rule-missing-label.ts +156 -0
  77. package/src/unity-rules-baseline.ts +273 -0
  78. package/src/wcag-map.ts +35 -0
  79. package/src/wcag-tags.ts +35 -0
  80. package/src/workspace-resolve.ts +405 -0
package/src/sarif.ts ADDED
@@ -0,0 +1,148 @@
1
+ /**
2
+ * SARIF 2.1.0 renderer — GitHub code-scanning's finding interchange format.
3
+ * Harvested from the platform CLI's renderer (`@binclusive/cli`
4
+ * `src/output/sarif.ts`) and RETARGETED to the engine's rich LOCAL
5
+ * {@link EnrichedFinding}: a SARIF `physicalLocation` needs the `file`/`line` the
6
+ * metadata-only wire contract deliberately drops, so this is a LOCAL renderer
7
+ * that reads the source-anchored model directly — it never routes through the
8
+ * contract projection.
9
+ *
10
+ * Impact is the ONE contract enum: {@link impactToLevel} maps the four concrete
11
+ * axe impacts (plus `unknown`) onto SARIF's `error|warning|note`, and
12
+ * {@link evidenceImpact} supplies that value from the finding's resolved axe
13
+ * impact. So text and SARIF narrow through the same impact vocabulary.
14
+ */
15
+ import { relative } from "node:path";
16
+ import type { Impact } from "@binclusive/a11y-contract";
17
+ import { hasSelector, toContractProvenance } from "./emit-contract";
18
+ import { evidenceHelpUrl, evidenceImpact, type EnrichedFinding } from "./evidence";
19
+ import { type LocationOptions, resolveLocations } from "./source-identity";
20
+
21
+ /**
22
+ * The one contract-impact -> SARIF-level mapping. Exhaustive over the closed enum.
23
+ * `critical`/`serious` are the actionable errors; `moderate` a warning; `minor`
24
+ * and `unknown` (not judged) are notes — SARIF's lowest, honest floor.
25
+ */
26
+ export function impactToLevel(impact: Impact): "error" | "warning" | "note" {
27
+ switch (impact) {
28
+ case "critical":
29
+ case "serious":
30
+ return "error";
31
+ case "moderate":
32
+ return "warning";
33
+ case "minor":
34
+ case "unknown":
35
+ return "note";
36
+ }
37
+ }
38
+
39
+ // A SARIF physicalLocation anchors on an artifactLocation (the source file); a
40
+ // source-line region rides it when the finding has one. A rendered-DOM (axe)
41
+ // finding carries `file` = the page URL and `line` = 0, so it emits no region
42
+ // but maps its CSS `selector` to a logicalLocation instead.
43
+ interface SarifLocation {
44
+ physicalLocation: {
45
+ artifactLocation: { uri: string };
46
+ region?: { startLine: number };
47
+ };
48
+ logicalLocations?: Array<{ fullyQualifiedName: string; kind: "element" }>;
49
+ }
50
+
51
+ // GitHub code-scanning resolves a SARIF `uri` relative to the repo root, so when
52
+ // a workspace `root` is given the source-file uri is relativized against it — a
53
+ // scan of a staged mirror dir then still yields repo-relative `src/Foo.tsx`
54
+ // paths that annotate the right line of the PR diff. A rendered-DOM (axe)
55
+ // finding's `file` is the page URL, not a workspace path, so it is left as-is.
56
+ function locationUri(file: string, root: string | undefined): string {
57
+ if (root === undefined || /^https?:\/\//i.test(file)) return file;
58
+ return relative(root, file);
59
+ }
60
+
61
+ function findingLocations(f: EnrichedFinding, root: string | undefined): SarifLocation[] {
62
+ const location: SarifLocation = {
63
+ physicalLocation: {
64
+ artifactLocation: { uri: locationUri(f.file, root) },
65
+ ...(f.line > 0 ? { region: { startLine: f.line } } : {}),
66
+ },
67
+ };
68
+ if (hasSelector(f.selector)) {
69
+ location.logicalLocations = [{ fullyQualifiedName: f.selector, kind: "element" }];
70
+ }
71
+ return [location];
72
+ }
73
+
74
+ /**
75
+ * Render a SARIF 2.1.0 log over the LOCAL findings. `runId` names the run in
76
+ * `automationDetails` (e.g. a PR number or scan id). Rules are the deduped set
77
+ * of fired rule ids; each result carries its impact level, source location,
78
+ * and a `properties.provenance` tag (`deterministic` | `agent`) so the two
79
+ * checker lanes stay distinguishable once both feed SARIF. `opts.root`, when
80
+ * given, relativizes source-file uris against the scanned root — the form
81
+ * GitHub code-scanning needs to anchor annotations on the PR diff.
82
+ *
83
+ * A source result also carries `partialFingerprints.primaryLocationLineHash` so
84
+ * code-scanning tracks the alert across commits as lines move (ADR 0042's "free
85
+ * consistency win"): the hash is the SAME source-identity fingerprint the wire
86
+ * contract emits — see the results map for why they can never disagree.
87
+ */
88
+ export function formatSarif(
89
+ findings: readonly EnrichedFinding[],
90
+ runId: string,
91
+ opts: LocationOptions = {},
92
+ ): string {
93
+ const ruleIds = [...new Set(findings.map((f) => f.ruleId))];
94
+ const ruleById = new Map(findings.map((f) => [f.ruleId, f]));
95
+
96
+ // Resolve every finding's identity through the ONE hash function the wire
97
+ // contract uses (`resolveLocations`), so SARIF and the emitted identity can
98
+ // never disagree on a line's hash. Page findings resolve to a `page` location
99
+ // (no line hash) and honestly get no `primaryLocationLineHash`.
100
+ const located = resolveLocations(findings, opts);
101
+
102
+ const sarif = {
103
+ $schema:
104
+ "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json",
105
+ version: "2.1.0" as const,
106
+ runs: [
107
+ {
108
+ tool: {
109
+ driver: {
110
+ name: "Binclusive",
111
+ informationUri: "https://binclusive.io",
112
+ rules: ruleIds.map((id) => {
113
+ const f = ruleById.get(id);
114
+ const helpUri = f ? evidenceHelpUrl(f) : null;
115
+ return {
116
+ id,
117
+ ...(f ? { shortDescription: { text: f.message } } : {}),
118
+ ...(helpUri ? { helpUri } : {}),
119
+ };
120
+ }),
121
+ },
122
+ },
123
+ results: findings.map((f) => {
124
+ const loc = located.get(f);
125
+ return {
126
+ ruleId: f.ruleId,
127
+ level: impactToLevel(evidenceImpact(f) ?? "unknown"),
128
+ message: { text: f.message },
129
+ locations: findingLocations(f, opts.root),
130
+ // `<lineHash>:<index>` — the content hash lets code-scanning re-match the
131
+ // alert when the line moves; `index` disambiguates identical lines within a
132
+ // file, exactly as the finding identity does. A page finding has no source
133
+ // line, so it carries no fingerprint (never fabricate one).
134
+ ...(loc?.kind === "source"
135
+ ? { partialFingerprints: { primaryLocationLineHash: `${loc.lineHash}:${loc.index}` } }
136
+ : {}),
137
+ properties: { provenance: toContractProvenance(f.provenance) },
138
+ };
139
+ }),
140
+ automationDetails: {
141
+ id: `binclusive-a11y/${runId}`,
142
+ },
143
+ },
144
+ ],
145
+ };
146
+
147
+ return JSON.stringify(sarif, null, 2);
148
+ }
Binary file