a11ign 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.
- package/LICENSE +661 -0
- package/README.md +140 -0
- package/dist/action/post-comment.d.ts +20 -0
- package/dist/action/post-comment.d.ts.map +1 -0
- package/dist/action/post-comment.js +93 -0
- package/dist/action/post-comment.js.map +1 -0
- package/dist/action/run.d.ts +2 -0
- package/dist/action/run.d.ts.map +1 -0
- package/dist/action/run.js +119 -0
- package/dist/action/run.js.map +1 -0
- package/dist/action/summary.d.ts +180 -0
- package/dist/action/summary.d.ts.map +1 -0
- package/dist/action/summary.js +355 -0
- package/dist/action/summary.js.map +1 -0
- package/dist/cli.d.ts +279 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1028 -0
- package/dist/cli.js.map +1 -0
- package/dist/fault-remediation.d.ts +87 -0
- package/dist/fault-remediation.d.ts.map +1 -0
- package/dist/fault-remediation.js +156 -0
- package/dist/fault-remediation.js.map +1 -0
- package/dist/forms/config.d.ts +69 -0
- package/dist/forms/config.d.ts.map +1 -0
- package/dist/forms/config.js +185 -0
- package/dist/forms/config.js.map +1 -0
- package/dist/forms/coverage.d.ts +51 -0
- package/dist/forms/coverage.d.ts.map +1 -0
- package/dist/forms/coverage.js +80 -0
- package/dist/forms/coverage.js.map +1 -0
- package/dist/forms/draft.d.ts +62 -0
- package/dist/forms/draft.d.ts.map +1 -0
- package/dist/forms/draft.js +165 -0
- package/dist/forms/draft.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/report.d.ts +48 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +322 -0
- package/dist/report.js.map +1 -0
- package/dist/scan/axe-results.d.ts +25 -0
- package/dist/scan/axe-results.d.ts.map +1 -0
- package/dist/scan/axe-results.js +111 -0
- package/dist/scan/axe-results.js.map +1 -0
- package/dist/scan/axe.d.ts +158 -0
- package/dist/scan/axe.d.ts.map +1 -0
- package/dist/scan/axe.js +203 -0
- package/dist/scan/axe.js.map +1 -0
- package/dist/scan/page-title.d.ts +3 -0
- package/dist/scan/page-title.d.ts.map +1 -0
- package/dist/scan/page-title.js +41 -0
- package/dist/scan/page-title.js.map +1 -0
- package/dist/scan/run-axe.d.ts +2 -0
- package/dist/scan/run-axe.d.ts.map +1 -0
- package/dist/scan/run-axe.js +39 -0
- package/dist/scan/run-axe.js.map +1 -0
- package/package.json +60 -0
package/README.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# `a11ign`
|
|
2
|
+
|
|
3
|
+
Drives a **real screen reader** through a real page and reports the WCAG 2.2 AA failures a rule scanner cannot
|
|
4
|
+
see — the ones that need to know what a blind user actually heard, and whether they could still finish the task.
|
|
5
|
+
|
|
6
|
+
**Alongside axe, never instead of it.** Both layers run; neither subsumes the other.
|
|
7
|
+
|
|
8
|
+
**Two ways in that work today.** No install and no Windows machine of your own — add the action to a
|
|
9
|
+
workflow:
|
|
10
|
+
|
|
11
|
+
```yaml
|
|
12
|
+
runs-on: windows-2022 # NVDA is Windows-only; the action fails fast and says so otherwise
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: a11ign/a11ign@main
|
|
16
|
+
with:
|
|
17
|
+
url: https://example.com/contact
|
|
18
|
+
task: Send an enquiry
|
|
19
|
+
fail-on: never # report first; gate when your team asks for it
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Or run it yourself from a checkout:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
git clone https://github.com/a11ign/a11ign.git
|
|
26
|
+
cd a11ign
|
|
27
|
+
npm install
|
|
28
|
+
npm run witness -- https://example.com --task "Find the opening hours"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**See the [top-level README](../../README.md) for which of those two paths is yours** — this file does not
|
|
32
|
+
repeat that decision.
|
|
33
|
+
|
|
34
|
+
> **`npx a11ign` does NOT work yet, and that is why it is not the first thing on this page.**
|
|
35
|
+
> `npx a11ign https://example.com --task "Find the opening hours"` is what the CLI *is*, and it
|
|
36
|
+
> works from a checkout — but no package has been pushed to npm, so run as typed it returns `E404` and
|
|
37
|
+
> teaches a reader only that the tool is broken. The name is still undecided (PLAN.md, B5); publishing is
|
|
38
|
+
> tracked as PLAN.md B7. It will move to the top of this file on the day it is true.
|
|
39
|
+
|
|
40
|
+
## What "a rule scanner cannot see" means, concretely
|
|
41
|
+
|
|
42
|
+
Measured against the University of Washington "Accessible University" demo — a third-party, expert-built
|
|
43
|
+
inaccessible page and its accessible twin:
|
|
44
|
+
|
|
45
|
+
| | before (inaccessible) | after (accessible) |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| screen-reader layer | 1.1.1, 1.1.1, 4.1.2, **2.4.4**, **1.3.1** | none |
|
|
48
|
+
| axe | 1.4.3, 3.1.1, 1.1.1, 4.1.2, 1.4.1, 2.5.8 | none |
|
|
49
|
+
|
|
50
|
+
Two findings only the screen-reader layer produced, quoting what a user hears:
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
2.4.4 Link Purpose heard: "click here, link"
|
|
54
|
+
1.3.1 Info & Relationships heard: "102 announcements, no heading among them"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
axe reports neither, and not by oversight: its `link-name` rule asks whether a link *has* an accessible name,
|
|
58
|
+
and "click here" has one. Meanwhile axe found four things a screen reader cannot perceive at all — contrast,
|
|
59
|
+
target size, language. That is the argument for running both, and the accessible twin being clean on both
|
|
60
|
+
matters more than either list.
|
|
61
|
+
|
|
62
|
+
## It will not tell you about anything visual
|
|
63
|
+
|
|
64
|
+
Contrast, focus-visible, reflow, target size: a screen reader cannot see them. Every report **says so** when
|
|
65
|
+
the rule layer did not run, because "we checked and found nothing" and "we did not check" must never look
|
|
66
|
+
alike. Reporting silence as a clean bill of health is the single most misleading thing this tool could do.
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npx a11ign <url> --no-axe # screen-reader layer only, and the report says so
|
|
70
|
+
npx a11ign <url> --axe-results axe.json # import a run you already did
|
|
71
|
+
npx a11ign <url> --json # machine-readable, for CI
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## You need a Windows worker
|
|
75
|
+
|
|
76
|
+
The capture runs on Windows, with NVDA, in an interactive desktop session — that is not a limitation to work
|
|
77
|
+
around, it is what makes the evidence real. Point the CLI at one:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
A11Y_WORKER=http://REDACTED-INTERNAL-ADDRESS:8765 npx a11ign <url> --task "..."
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**UTM is DEPRECATED — it was a testing path, not the fleet.** `@a11ign/worker-fleet` can still lease a
|
|
84
|
+
local UTM VM on macOS and put it back as it found it, and every UTM entry point now says so at runtime. Point
|
|
85
|
+
`A11Y_WORKER` at a Windows machine you have, or use the GitHub Action if you have none. See
|
|
86
|
+
`docs/getting-started.md` for setting a worker up, and `@a11ign/nvda-worker` for the worker itself.
|
|
87
|
+
|
|
88
|
+
## Judging what the screen reader heard
|
|
89
|
+
|
|
90
|
+
By default the judge is a small trained scorer shipped with this tool — no API key, no metered cost, no
|
|
91
|
+
network call for the judgment itself. Point it at a rented model instead if you want a second opinion or
|
|
92
|
+
do not trust the local one yet:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
JUDGE_BACKEND=openai OPENAI_API_KEY=sk-... npx a11ign <url> --task "..."
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`JUDGE_BACKEND=openai` speaks plain `/v1/chat/completions`, so it works against hosted OpenAI, Anthropic's
|
|
99
|
+
OpenAI-compatible endpoint, or a local server (Ollama, LM Studio, vLLM, llama.cpp) — set `JUDGE_BASE_URL`
|
|
100
|
+
to point it somewhere other than `api.openai.com`. `JUDGE_API_KEY` is accepted as a project-neutral alias
|
|
101
|
+
for `OPENAI_API_KEY` if you would rather not put a provider's name on the variable. Other knobs, all
|
|
102
|
+
optional: `JUDGE_MODEL` (which model to ask for), `JUDGE_TIMEOUT_MS` (default 120000), `JUDGE_REASONING`
|
|
103
|
+
(default `medium`, for models that support a reasoning-effort parameter). If a server rejects
|
|
104
|
+
constrained/structured JSON output, set `JUDGE_STRUCTURED=off` — most reject silently in ways that read as
|
|
105
|
+
a truncated or malformed response, so this is the first thing to try if `openai` backend responses look
|
|
106
|
+
corrupted against a self-hosted server.
|
|
107
|
+
|
|
108
|
+
`codex` and `anthropic` backends also exist, for comparison; none of the three rented backends is ever the
|
|
109
|
+
default.
|
|
110
|
+
|
|
111
|
+
## A page behind a consent wall is REFUSED, not reported
|
|
112
|
+
|
|
113
|
+
The screen reader gets held inside the modal, so the capture describes the dialog rather than the page.
|
|
114
|
+
|
|
115
|
+
**What the CLI actually does — this paragraph used to say "the run exits 2", and it does not.** It writes a
|
|
116
|
+
warning to stderr naming which kind of doubt it was, marks the result `captureVerified: false` with an
|
|
117
|
+
`unverifiedReason`, and **reports no findings rather than describing the dialog**. The report says so in
|
|
118
|
+
words. The run's exit code is decided by `--fail-on` as usual; the only thing here that exits 2 is a bad
|
|
119
|
+
`--forms` config. (Exit 2 on an unverified capture is `capture-check.mjs`, a lab harness, and this README
|
|
120
|
+
had inherited its behaviour.)
|
|
121
|
+
|
|
122
|
+
This is deliberate and it is the check that matters most in the whole tool: on one real site the census
|
|
123
|
+
found 793 links and 463 headings while the screen reader reached 1 heading and 0 links, and an earlier
|
|
124
|
+
version reported "No lived-experience findings" — for a page it had never seen. **Refusing to report is the
|
|
125
|
+
point; the exit code is not what does it.**
|
|
126
|
+
|
|
127
|
+
## Rendering the report yourself
|
|
128
|
+
|
|
129
|
+
```js
|
|
130
|
+
import { reportLines } from "a11ign";
|
|
131
|
+
|
|
132
|
+
console.log(reportLines(report).join("\n"));
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
`reportLines` and the `Report` type are the entire public API. Findings come back in the order a user meets
|
|
136
|
+
them — perceive, then navigate, then interact — because a finding about operating a control is not useful to
|
|
137
|
+
someone who could not perceive it.
|
|
138
|
+
|
|
139
|
+
The pieces underneath are packages in their own right: `@a11ign/judge` for the judgment,
|
|
140
|
+
`@a11ign/evidence` for the capture contract, `@a11ign/worker-fleet` for the VM lifecycle.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Does a real summary file exist at `path`? The ONE fact this module exists to check before choosing a
|
|
3
|
+
* message, rather than assuming the earlier step succeeded because this step was reached at all --
|
|
4
|
+
* `if: always()` on the caller means this runs whether or not anything upstream produced output.
|
|
5
|
+
* @param {string} path
|
|
6
|
+
* @returns {boolean}
|
|
7
|
+
*/
|
|
8
|
+
export declare function reportSummaryExists(path: string): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* The message to print when the PR comment could not be posted, as a pure function of whether a real
|
|
11
|
+
* report exists -- #493's own acceptance: the two states must never print the same sentence. `reportExists:
|
|
12
|
+
* true` means `gh pr comment` itself failed (network, permissions, rate limit) while a real report sits in
|
|
13
|
+
* the job summary, so the ORIGINAL sentence is honest there and stays. `reportExists: false` means there
|
|
14
|
+
* was never anything to post -- and pointing the reader at a job summary that was never written is worse
|
|
15
|
+
* than saying nothing, per #493's own framing: "they now doubt their own reading rather than the tool."
|
|
16
|
+
* @param {boolean} reportExists
|
|
17
|
+
* @returns {string}
|
|
18
|
+
*/
|
|
19
|
+
export declare function commentFailureMessage(reportExists: boolean): string;
|
|
20
|
+
//# sourceMappingURL=post-comment.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"post-comment.d.ts","sourceRoot":"","sources":["../../src/action/post-comment.ts"],"names":[],"mappings":"AAyCA;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEzD;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,YAAY,EAAE,OAAO,GAAG,MAAM,CAKnE"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// #493: "Could not post the PR comment. The job summary still has the report" used to print unconditionally
|
|
2
|
+
// on ANY `gh pr comment` failure -- including the one the V1 rehearsal hit on all three of its runs, where
|
|
3
|
+
// the build died before `run.ts` ever produced a summary at all. `run.ts` writes $GITHUB_STEP_SUMMARY and
|
|
4
|
+
// its `--summary-out` file in the SAME call, after the SAME early-exit checks (a missing/unreadable result,
|
|
5
|
+
// or a result with no verdict) -- so an absent summary FILE means the step summary is absent too, never
|
|
6
|
+
// just a different place the reader missed. This repo's own most-recorded fault class -- "a diagnostic that
|
|
7
|
+
// cannot report itself" -- pointed at a user instead of at us.
|
|
8
|
+
//
|
|
9
|
+
// Deliberately separate from `run.ts`, mirroring that file's own split from `action.yml`: the POLICY (which
|
|
10
|
+
// message, and whether the file genuinely exists) is testable here without a real `gh` call; the mechanics
|
|
11
|
+
// of actually posting stay in `main()`, which nothing but `action.yml` invokes.
|
|
12
|
+
//
|
|
13
|
+
// #567: NO WORKSPACE IMPORT, DELIBERATELY -- this file used to import `flagValue` from
|
|
14
|
+
// `@a11ign/worker-fleet/cli-flags`, which needs BOTH `node_modules` and a completed build
|
|
15
|
+
// (`pre-install-import-graph.test.ts`'s own header). This step runs `if: always()` in `action.yml`,
|
|
16
|
+
// specifically so it still reports when an EARLIER step failed -- and "Install a11ign" (the step that
|
|
17
|
+
// creates `node_modules` in the action's own checkout) is itself one of the steps that can fail or never
|
|
18
|
+
// run (measured live: the V1 rehearsal's #568, a `setup-node` cache step dying before "Install a11ign"
|
|
19
|
+
// ever starts). So the one step whose entire job is to report honestly on failure was, itself, silently
|
|
20
|
+
// unable to run under exactly the failure shape it exists to handle -- the identical class #535 fixed in
|
|
21
|
+
// `packages/guards/src/piped-exit-status-guard.mjs` the same day, missed here because this file was only ever tested
|
|
22
|
+
// inside this repo's own fully-installed checkout, never through the real composite-action path a
|
|
23
|
+
// consumer's workflow actually uses. `flagValue`'s own body is four lines; reproduced inline below rather
|
|
24
|
+
// than imported.
|
|
25
|
+
import { existsSync } from "node:fs";
|
|
26
|
+
import { execFileSync } from "node:child_process";
|
|
27
|
+
import { pathToFileURL } from "node:url";
|
|
28
|
+
/**
|
|
29
|
+
* `flagValue`'s own logic, reproduced rather than imported -- see this file's header for why. Pure and
|
|
30
|
+
* tiny enough that a second copy is cheaper than the workspace dependency it would otherwise require.
|
|
31
|
+
* @param {string[]} argv
|
|
32
|
+
* @param {string} name
|
|
33
|
+
* @returns {string | undefined}
|
|
34
|
+
*/
|
|
35
|
+
function flagValue(argv, name) {
|
|
36
|
+
const prefix = `--${name}=`;
|
|
37
|
+
const hit = argv.find((a) => a.startsWith(prefix));
|
|
38
|
+
return hit === undefined ? undefined : hit.slice(prefix.length);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Does a real summary file exist at `path`? The ONE fact this module exists to check before choosing a
|
|
42
|
+
* message, rather than assuming the earlier step succeeded because this step was reached at all --
|
|
43
|
+
* `if: always()` on the caller means this runs whether or not anything upstream produced output.
|
|
44
|
+
* @param {string} path
|
|
45
|
+
* @returns {boolean}
|
|
46
|
+
*/
|
|
47
|
+
export function reportSummaryExists(path) {
|
|
48
|
+
return existsSync(path);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The message to print when the PR comment could not be posted, as a pure function of whether a real
|
|
52
|
+
* report exists -- #493's own acceptance: the two states must never print the same sentence. `reportExists:
|
|
53
|
+
* true` means `gh pr comment` itself failed (network, permissions, rate limit) while a real report sits in
|
|
54
|
+
* the job summary, so the ORIGINAL sentence is honest there and stays. `reportExists: false` means there
|
|
55
|
+
* was never anything to post -- and pointing the reader at a job summary that was never written is worse
|
|
56
|
+
* than saying nothing, per #493's own framing: "they now doubt their own reading rather than the tool."
|
|
57
|
+
* @param {boolean} reportExists
|
|
58
|
+
* @returns {string}
|
|
59
|
+
*/
|
|
60
|
+
export function commentFailureMessage(reportExists) {
|
|
61
|
+
return reportExists
|
|
62
|
+
? "::warning::Could not post the PR comment. The job summary still has the report."
|
|
63
|
+
: "::warning::No report was produced -- the run failed before a result existed to summarize. "
|
|
64
|
+
+ "See the earlier step's own error above for what actually went wrong.";
|
|
65
|
+
}
|
|
66
|
+
function main() {
|
|
67
|
+
const arg = (name) => flagValue(process.argv, name);
|
|
68
|
+
const summaryPath = arg("summary");
|
|
69
|
+
const prNumber = arg("pr");
|
|
70
|
+
const repo = arg("repo");
|
|
71
|
+
if (!summaryPath || !prNumber || !repo) {
|
|
72
|
+
process.stderr.write("usage: tsx packages/cli/src/action/post-comment.ts --summary=<file> --pr=<n> --repo=<owner/name>\n");
|
|
73
|
+
process.exit(2);
|
|
74
|
+
}
|
|
75
|
+
// CHECKED, NOT ASSUMED -- the whole point of this file. A missing summary is not something `gh pr
|
|
76
|
+
// comment` needs to be asked about; there is nothing honest it could report by being run against a file
|
|
77
|
+
// that was never written.
|
|
78
|
+
if (!reportSummaryExists(summaryPath)) {
|
|
79
|
+
process.stderr.write(`${commentFailureMessage(false)}\n`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
execFileSync("gh", ["pr", "comment", prNumber, "--body-file", summaryPath, "--edit-last", "--create-if-none", "--repo", repo], { stdio: "inherit" });
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
process.stderr.write(`${commentFailureMessage(true)}\n`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// RUN ONLY WHEN INVOKED -- `pathToFileURL`, never a template literal, for the identical reason `run.ts`
|
|
90
|
+
// carries beside its own copy of this guard: concatenation does not percent-encode a space in the path.
|
|
91
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href)
|
|
92
|
+
main();
|
|
93
|
+
//# sourceMappingURL=post-comment.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"post-comment.js","sourceRoot":"","sources":["../../src/action/post-comment.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/action/run.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// The Action's entry point: read a witness run's JSON, write the summary, decide the exit code.
|
|
2
|
+
//
|
|
3
|
+
// tsx packages/cli/src/action/run.ts --result=run.json [--fail-on=never|any|blocker|serious|moderate|minor]
|
|
4
|
+
// [--summary-out=summary.md] [--marker=a11ign]
|
|
5
|
+
//
|
|
6
|
+
// Deliberately separate from `src/cli.ts`. The CLI's job is to capture and judge; this one's job is to
|
|
7
|
+
// present that to GitHub and decide whether the check passes. Keeping them apart means the Action's
|
|
8
|
+
// policy is testable (`summary.test.ts`) without a Windows runner, and a change to how findings are
|
|
9
|
+
// displayed cannot break how they are produced.
|
|
10
|
+
import { readFileSync, writeFileSync, appendFileSync } from "node:fs";
|
|
11
|
+
import { resolve } from "node:path";
|
|
12
|
+
import { pathToFileURL } from "node:url";
|
|
13
|
+
import { logLines, renderSummary, shouldFail } from "./summary.js";
|
|
14
|
+
import { taskVerdictLabel } from "@a11ign/judge";
|
|
15
|
+
import { announcedStateChanges } from "@a11ign/judge/rules";
|
|
16
|
+
import { flagValue } from "@a11ign/worker-fleet/cli-flags";
|
|
17
|
+
// audit §9 "argv parsing": this was its own copy of the fifteen-file idiom. `flagValue` is the shared,
|
|
18
|
+
// tested extraction; `?? fallback` stays here because defaulting is this call site's business, not the
|
|
19
|
+
// extractor's.
|
|
20
|
+
/**
|
|
21
|
+
* The Action's whole body, so importing this file does not RUN it.
|
|
22
|
+
*
|
|
23
|
+
* It was top-level code ending in `process.exit`, which meant nothing could import it — not a test, not
|
|
24
|
+
* another entry point — and `entry-points.test.ts` could not see that, because it discovers entry points
|
|
25
|
+
* from `package.json` scripts and this one is invoked by `action.yml`. Two blindnesses, one file: a guard
|
|
26
|
+
* that was never there, in a place the guard for it could not look.
|
|
27
|
+
*
|
|
28
|
+
* `process.exit` stays inside rather than becoming a returned code. This IS the process's last act, the
|
|
29
|
+
* exit codes are the Action's documented contract (2 = we could not look, 1 = findings met the
|
|
30
|
+
* threshold), and routing them through a return value would put a second place where that contract can be
|
|
31
|
+
* got wrong. What the wrapper buys is that the code no longer runs merely because somebody imported the
|
|
32
|
+
* module.
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* #1391: which state changes were announced correctly is the JUDGE's call, through the same gates as
|
|
36
|
+
* `4.1.2:state-change-silent`; the renderer only shows the list, the way it only shows the task label. A result
|
|
37
|
+
* written before the probe carries no `interaction`, and then there is nothing to show.
|
|
38
|
+
*/
|
|
39
|
+
function observedStateChanges(result) {
|
|
40
|
+
return announcedStateChanges(result.interaction?.stateChanges ?? []);
|
|
41
|
+
}
|
|
42
|
+
function main() {
|
|
43
|
+
const arg = (name, fallback) => flagValue(process.argv, name) ?? fallback;
|
|
44
|
+
const resultPath = arg("result");
|
|
45
|
+
if (!resultPath) {
|
|
46
|
+
process.stderr.write("usage: tsx packages/cli/src/action/run.ts --result=<file.json> [--fail-on=...] [--summary-out=...]\n");
|
|
47
|
+
process.exit(2);
|
|
48
|
+
}
|
|
49
|
+
const failOn = arg("fail-on", "never");
|
|
50
|
+
const marker = arg("marker", "a11ign");
|
|
51
|
+
let result;
|
|
52
|
+
try {
|
|
53
|
+
result = JSON.parse(readFileSync(resolve(resultPath), "utf8"));
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
// A capture that never produced JSON is an infrastructure failure, not a clean page. Failing loudly
|
|
57
|
+
// here is the difference between "your page is fine" and "we did not manage to look at it" — the
|
|
58
|
+
// distinction this whole project is built around.
|
|
59
|
+
process.stderr.write(`a11ign: could not read the run result at ${resultPath}: ${error.message}\n`);
|
|
60
|
+
process.exit(2);
|
|
61
|
+
}
|
|
62
|
+
if (!result?.verdict || !Array.isArray(result.verdict.findings)) {
|
|
63
|
+
process.stderr.write("a11ign: the run result has no verdict — the judge did not complete, so nothing was assessed.\n");
|
|
64
|
+
process.exit(2);
|
|
65
|
+
}
|
|
66
|
+
const label = taskVerdictLabel();
|
|
67
|
+
const markdown = renderSummary(result, {
|
|
68
|
+
marker, taskQuestion: label.question, isTaskClaim: label.isTaskClaim, stateChangesObserved: observedStateChanges(result),
|
|
69
|
+
});
|
|
70
|
+
// An unverified capture is an infrastructure failure, not a verdict about the page — so it exits 2, the
|
|
71
|
+
// same code used for "could not read the result". Green would say "we checked and it is fine"; red (1)
|
|
72
|
+
// would say "your page has a problem". Neither is true: we did not manage to look at it.
|
|
73
|
+
//
|
|
74
|
+
// The summary is written FIRST so the reader still gets the explanation. Found on gov.uk, where the
|
|
75
|
+
// capture read Edge's image-magnifier overlay, the retry warned three times, and the run reported a 4.1.2
|
|
76
|
+
// finding about the browser's own Zoom In / Rotate buttons.
|
|
77
|
+
const unverified = result.captureVerified === false;
|
|
78
|
+
// $GITHUB_STEP_SUMMARY is append-only and shared with other steps, so append rather than overwrite.
|
|
79
|
+
const stepSummary = process.env.GITHUB_STEP_SUMMARY;
|
|
80
|
+
if (stepSummary)
|
|
81
|
+
appendFileSync(stepSummary, `${markdown}\n`);
|
|
82
|
+
const summaryOut = arg("summary-out");
|
|
83
|
+
if (summaryOut)
|
|
84
|
+
writeFileSync(resolve(summaryOut), `${markdown}\n`, "utf8");
|
|
85
|
+
if (!stepSummary && !summaryOut)
|
|
86
|
+
process.stdout.write(`${markdown}\n`);
|
|
87
|
+
// The summary has already been written above, so this only decides the exit code. Writing it again here
|
|
88
|
+
// appended it TWICE to $GITHUB_STEP_SUMMARY, which is append-only.
|
|
89
|
+
if (unverified) {
|
|
90
|
+
process.stderr.write("a11ign: the capture could not be confirmed to have read the requested page; "
|
|
91
|
+
+ "reporting no findings. This is a failed measurement, not a clean page.\n");
|
|
92
|
+
process.exit(2);
|
|
93
|
+
}
|
|
94
|
+
const { findings } = result.verdict;
|
|
95
|
+
let fail;
|
|
96
|
+
try {
|
|
97
|
+
fail = shouldFail(findings, failOn);
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
// An unrecognised `fail-on` is a workflow typo, and the dangerous outcome is treating it as "never":
|
|
101
|
+
// the check goes green and nobody looks again. Refuse instead.
|
|
102
|
+
process.stderr.write(`a11ign: ${error.message}. Use never|any|blocker|serious|moderate|minor.\n`);
|
|
103
|
+
process.exit(2);
|
|
104
|
+
}
|
|
105
|
+
// #1363: the log is what a reader sees without opening the summary, so it says where the examination ended
|
|
106
|
+
// BEFORE it counts findings -- a bare count reads as a verdict about everything the run touched.
|
|
107
|
+
for (const line of logLines(result, failOn))
|
|
108
|
+
process.stderr.write(`${line}\n`);
|
|
109
|
+
if (fail) {
|
|
110
|
+
process.stderr.write(`a11ign: failing the check — asserted findings met the ${failOn} threshold.\n`);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// RUN ONLY WHEN INVOKED. `pathToFileURL`, never a template literal: concatenation does not
|
|
115
|
+
// percent-encode, so a checkout under a path containing a SPACE compares false, the guard never fires,
|
|
116
|
+
// and the Action exits 0 having assessed nothing -- which a workflow reads as a passing check.
|
|
117
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href)
|
|
118
|
+
main();
|
|
119
|
+
//# sourceMappingURL=run.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.js","sourceRoot":"","sources":["../../src/action/run.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render a witness run as Markdown for a GitHub job summary or PR comment, and decide pass/fail.
|
|
3
|
+
*
|
|
4
|
+
* Kept pure and separate from the Action's YAML so the output is testable without pushing a commit and
|
|
5
|
+
* waiting for a Windows runner. That matters more here than usual: the whole point of the Action is to
|
|
6
|
+
* tell someone their page has an accessibility problem, so a renderer that drops a finding, or a gate
|
|
7
|
+
* that passes when it should fail, is worse than no Action at all.
|
|
8
|
+
*/
|
|
9
|
+
export type Severity = "blocker" | "serious" | "moderate" | "minor";
|
|
10
|
+
import type { CaptureInteraction } from "@a11ign/evidence";
|
|
11
|
+
import type { Judgment } from "@a11ign/judge";
|
|
12
|
+
import type { announcedStateChanges } from "@a11ign/judge/rules";
|
|
13
|
+
export interface RunFinding {
|
|
14
|
+
issue: string;
|
|
15
|
+
wcag: string;
|
|
16
|
+
severity: Severity;
|
|
17
|
+
evidence: string;
|
|
18
|
+
confidence: number;
|
|
19
|
+
/**
|
|
20
|
+
* #1366: whether this finding ASSERTS the criterion (`conformance`) or REFERS it (absent or `secondary`) -- the
|
|
21
|
+
* judge's own `RequirementMapping`, taken from its `Judgment` type so it has one definition. The result JSON has
|
|
22
|
+
* carried it all along; this renderer dropped it, so a referral rated `serious` was logged as "1 serious".
|
|
23
|
+
*/
|
|
24
|
+
mapping?: Judgment["findings"][number]["mapping"];
|
|
25
|
+
}
|
|
26
|
+
export interface RunResult {
|
|
27
|
+
/**
|
|
28
|
+
* False when the capture could not be confirmed to have read the requested page.
|
|
29
|
+
*
|
|
30
|
+
* Not cosmetic. On gov.uk the capture read Edge's image-magnifier overlay, the retry fired three times
|
|
31
|
+
* and warned, and the run still reported a 4.1.2 finding about the browser's own Zoom In / Rotate
|
|
32
|
+
* buttons as if the site were at fault. Findings from an unverified capture are not merely unreliable,
|
|
33
|
+
* they can blame a page for its browser — so they are not shown at all.
|
|
34
|
+
*/
|
|
35
|
+
captureVerified?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Why the capture is unverified: `"wrong-content"` (it read something else) or `"contained"` (it read
|
|
38
|
+
* only part of the right page, almost always a consent or cookie modal holding the screen reader).
|
|
39
|
+
* Absent on older results, which are explained with the original wrong-content wording.
|
|
40
|
+
*/
|
|
41
|
+
captureUnverifiedReason?: "wrong-content" | "contained";
|
|
42
|
+
url: string;
|
|
43
|
+
task: string;
|
|
44
|
+
screenReader: string;
|
|
45
|
+
transcript?: string[];
|
|
46
|
+
/** null when the rule layer did not run — NOT the same as running and finding nothing. */
|
|
47
|
+
ruleBased: {
|
|
48
|
+
impact: string;
|
|
49
|
+
wcag: string[];
|
|
50
|
+
rule: string;
|
|
51
|
+
help: string;
|
|
52
|
+
nodes?: {
|
|
53
|
+
target?: unknown[];
|
|
54
|
+
}[];
|
|
55
|
+
}[] | null;
|
|
56
|
+
verdict: {
|
|
57
|
+
taskCompletable: boolean;
|
|
58
|
+
summary: string;
|
|
59
|
+
findings: RunFinding[];
|
|
60
|
+
confidence: number;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Per-criterion ACT outcomes, when the result carries them.
|
|
64
|
+
*
|
|
65
|
+
* `cli.ts --json` has emitted these all along and this renderer dropped them, so the most PUBLIC output
|
|
66
|
+
* this tool produces — a comment on somebody's pull request, in bold — could not say `cantTell`. A page
|
|
67
|
+
* where six of the ten covered criteria came back undetermined rendered identically to one where all ten
|
|
68
|
+
* passed. The CLI prints the tally with "Neither is clean"; the PR comment could not.
|
|
69
|
+
*
|
|
70
|
+
* Optional because an older result JSON has none, and absent must render as SILENCE rather than as a
|
|
71
|
+
* tally of zeroes — a fabricated "0 untested" would be worse than the omission it replaced.
|
|
72
|
+
*/
|
|
73
|
+
outcomes?: {
|
|
74
|
+
criterion: string;
|
|
75
|
+
outcome: string;
|
|
76
|
+
reason: string;
|
|
77
|
+
}[];
|
|
78
|
+
/**
|
|
79
|
+
* Where the examination ENDED because an activation took the browser off the page's site (#1363). Nothing
|
|
80
|
+
* after it was examined and no finding describes it. Absent on results whose activations stayed on the
|
|
81
|
+
* page, and on results written before the check existed.
|
|
82
|
+
*/
|
|
83
|
+
leftSite?: {
|
|
84
|
+
control: string;
|
|
85
|
+
to: string | null;
|
|
86
|
+
source: "recorded" | "derived";
|
|
87
|
+
} | null;
|
|
88
|
+
/**
|
|
89
|
+
* The disclosure probe's before/after pairs: a control as first announced, and what took focus after it was
|
|
90
|
+
* activated. Declared for #1391 only -- `run.ts` reads it to compute which changes were announced correctly --
|
|
91
|
+
* and TYPED FROM `@a11ign/evidence`'s own `CaptureInteraction`, never restated, so the shape has one definition.
|
|
92
|
+
* The import is `import type`: erased at runtime, so this renderer still imports nothing when it runs. Absent on
|
|
93
|
+
* results written before the probe.
|
|
94
|
+
*/
|
|
95
|
+
interaction?: Partial<Pick<CaptureInteraction, "stateChanges">> | null;
|
|
96
|
+
/**
|
|
97
|
+
* WCAG §5.2's five conformance requirements, as `cli.ts --json` emits them (`@a11ign/evidence/conformance`).
|
|
98
|
+
* Declared for one sentence only (#1387): Requirement 2 names a capture that spanned more than one document,
|
|
99
|
+
* and until this field was declared the summary could not show it. Absent on older results, which say nothing.
|
|
100
|
+
*/
|
|
101
|
+
conformance?: {
|
|
102
|
+
number: number;
|
|
103
|
+
name: string;
|
|
104
|
+
establishes: string;
|
|
105
|
+
limitation: string;
|
|
106
|
+
}[];
|
|
107
|
+
}
|
|
108
|
+
export declare function documentsSpannedSentence(conformance: RunResult["conformance"]): string | null;
|
|
109
|
+
/** How many undetermined criteria rest on an examination known to be partial. Zero with no outcomes at all. */
|
|
110
|
+
export declare function partialExaminationCount(outcomes: RunResult["outcomes"]): number;
|
|
111
|
+
export type FailOn = "never" | "any" | Severity;
|
|
112
|
+
/**
|
|
113
|
+
* Should this run fail the check?
|
|
114
|
+
*
|
|
115
|
+
* `never` is the default deliberately. A tool that starts failing builds the day it is installed gets
|
|
116
|
+
* uninstalled; one that reports first, and fails when the team asks it to, gets adopted. The same
|
|
117
|
+
* reasoning is why the rule layer is opt-out rather than mandatory.
|
|
118
|
+
*/
|
|
119
|
+
export declare function shouldFail(findings: RunFinding[], failOn: FailOn): boolean;
|
|
120
|
+
/**
|
|
121
|
+
* #1388: IS THIS RULE-LAYER ROW ABOUT CONTENT INSIDE A FRAME?
|
|
122
|
+
*
|
|
123
|
+
* axe's `target` holds one selector per frame boundary it crossed, so a target of more than one entry points inside an
|
|
124
|
+
* iframe. It says the node is in A frame, never WHOSE: a same-origin frame is the author's own, and the result records no
|
|
125
|
+
* frame origin, so the marker claims no more than that. A single entry that is itself an array is a shadow-DOM path, not
|
|
126
|
+
* a frame. A row with no nodes (an older result) cannot be told, so it is not marked.
|
|
127
|
+
*/
|
|
128
|
+
export declare function insideFrame(row: NonNullable<RunResult["ruleBased"]>[number]): boolean;
|
|
129
|
+
/**
|
|
130
|
+
* #1391: ONE STATE CHANGE ANNOUNCED CORRECTLY -- exactly the judge's `announcedStateChanges` element type, taken from
|
|
131
|
+
* it by a type-only import (erased at runtime; this renderer still loads nothing), so the shape has one definition.
|
|
132
|
+
* `from` and `to` are the expandable-state words the rule reads; `control` and `after` are the two announcements.
|
|
133
|
+
*/
|
|
134
|
+
export type ObservedStateChange = ReturnType<typeof announcedStateChanges>[number];
|
|
135
|
+
export interface SummaryOptions {
|
|
136
|
+
/** Rows per table. Bounded because a job summary has a hard size limit and truncation must be stated. */
|
|
137
|
+
limit?: number;
|
|
138
|
+
/** Included as an HTML comment so a PR comment can be found and UPDATED rather than duplicated. */
|
|
139
|
+
marker?: string;
|
|
140
|
+
/**
|
|
141
|
+
* What `taskCompletable` should be LABELLED as, because it means different things per backend.
|
|
142
|
+
*
|
|
143
|
+
* Passed in rather than read from the environment here: this renderer is deliberately pure so the
|
|
144
|
+
* output is testable without a Windows runner, and reading process.env would end that.
|
|
145
|
+
*
|
|
146
|
+
* The default is the honest one for the shipped `local` scorer, which has no head for task completion
|
|
147
|
+
* and never sees the task — local-judge.ts computes `!findings.some(f => f.severity === "blocker")`
|
|
148
|
+
* and its own comment says claiming a task answer would be inventing one. This comment is posted on
|
|
149
|
+
* someone's PULL REQUEST in bold, so inventing it there is the worst place to do it. The LLM backends
|
|
150
|
+
* do read the task, and pass their own wording.
|
|
151
|
+
*/
|
|
152
|
+
taskQuestion?: string;
|
|
153
|
+
/**
|
|
154
|
+
* `taskVerdictLabel().isTaskClaim` (`@a11ign/judge`), passed in for the identical reason `taskQuestion`
|
|
155
|
+
* is: this renderer is pure. `report.ts`'s own `verdictHeadline` already makes this split -- a bare
|
|
156
|
+
* "Yes"/"No" only when `taskCompletable` really answers a question about the task (the LLM backends).
|
|
157
|
+
* For the shipped `local` scorer it is `!findings.some(f => f.severity === "blocker")`, so a report of
|
|
158
|
+
* six SERIOUS findings answered "**No blocking findings** Yes" until this flag existed: true, and
|
|
159
|
+
* indistinguishable from a clean page to anyone who did not read past the first line. Defaults to
|
|
160
|
+
* `false`, the honest default for the backend that ships.
|
|
161
|
+
*/
|
|
162
|
+
isTaskClaim?: boolean;
|
|
163
|
+
/**
|
|
164
|
+
* #1391: the state changes the screen reader announced correctly, computed by `run.ts` through the judge's
|
|
165
|
+
* `announcedStateChanges` -- the SAME gates as the `4.1.2:state-change-silent` rule, so this renderer never
|
|
166
|
+
* decides what counts. Passed in for the identical reason `taskQuestion` is: this renderer is pure. Absent or
|
|
167
|
+
* empty renders nothing, which is also what a result without the probe gets.
|
|
168
|
+
*/
|
|
169
|
+
stateChangesObserved?: readonly ObservedStateChange[];
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* The Action's log lines -- EXPORTED so a test drives the real wording (#1363).
|
|
173
|
+
*
|
|
174
|
+
* When the examination ended because an activation left the site, that is said FIRST. Rehearsal 2's log read
|
|
175
|
+
* `a11ign: 1 finding(s) (1 serious)` about a finding observed on youtube.com, and a reader of that line alone
|
|
176
|
+
* would have filed a 2.4.2 bug against the W3C.
|
|
177
|
+
*/
|
|
178
|
+
export declare function logLines(result: RunResult, failOn: FailOn): string[];
|
|
179
|
+
export declare function renderSummary(result: RunResult, options?: SummaryOptions): string;
|
|
180
|
+
//# sourceMappingURL=summary.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"summary.d.ts","sourceRoot":"","sources":["../../src/action/summary.ts"],"names":[],"mappings":"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"}
|