1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176# Contributing to Equalify Iris
Thanks for helping make document remediation more accessible! Iris is Open Source under
**AGPL-3.0** (see [LICENSE](LICENSE)) and maintained by [Equalify Inc.](https://equalify.app/),
the **University of Illinois Chicago**, and **California State University**.
By participating you agree to our [Code of Conduct](CODE_OF_CONDUCT.md).
## Ways to contribute
- **Report an accessibility barrier** โ in the app/demo or in the HTML Iris produces. Use the
**Accessibility issue** template. These are our highest priority.
- **Report a bug / request a feature** โ use the matching issue template.
- **Improve a prompt** โ the agent files are in [`agents/`](agents/), and `agents/page.md`
renders every page, so a PR that sharpens it changes the product. (The Reader and Copy Editor
prompts are code, in `src/pipeline/review.ts`.) Iris also opens `New agent suggestion: <type>`
issues when it meets content a specialist would handle better, and you're welcome to open one
yourself โ but note that `agents/` is not a directory of content types: a file added for a type
the whole-page pass already covers is never loaded โ the
[design notes](docs/design-notes.md#the-pipelines-shape) say why, under "One agent per page, not
one per content type". `chartDataAgent.md` is the shape that earns its place.
- **Code** โ bug fixes and improvements via pull request.
A well-written issue may get a pull request without you doing anything else. A scheduled workflow
ranks the open issues SunโWed and opens one PR for the most pressing one it can finish well
([details](docs/ci.md#scheduled-issue-triage)) โ accessibility barriers rank first, and small
user-visible fixes reported against the demo rank well because they review cleanly. It never
touches an issue labelled `no-auto-pr`, never files a second PR for an issue it has already tried,
and stops entirely when nothing is eligible. If you'd rather own the fix yourself, say so on the
issue and add that label.
**You get the credit for it.** A PR from that workflow names you in its body and carries a
`Co-authored-by` trailer for your account on the commit, so the merged commit is attributed to you
as well as to the bot that typed it โ the report is the contribution. The trailer uses your
GitHub `users.noreply` address, never your real email.
**It also stays off any issue that already has an open PR โ including yours.** Open a PR for an
issue and the workflow leaves it alone; if every open issue has one, it opens nothing at all. Two
things claim an issue: **`Closes #<n>` in your PR body** (which also closes the issue on merge, so
this is the one to use) or an `issue-<n>` in your branch name. Merely mentioning `#<n>` in prose
does *not* claim it โ too many PRs reference issues they aren't fixing โ so if you are working on
something, say `Closes #<n>` and the robot will stay out of your way.
## Development
Requires **Node 24+** (runs TypeScript directly; uses built-in `node:sqlite`), **git**, and โ
for PDF uploads โ **poppler-utils** (`brew install poppler` / `apt-get install poppler-utils`).
```bash
npm install
cp .env.example .env # a GitHub PAT (required) + a model provider key
cp config.example.yaml config.yaml
npm start # http://localhost:8080 (app at /, API under /v1)
```
Before opening a PR:
```bash
npm run typecheck # tsc --noEmit
npm test # the unit suite (node --test; run it through npm, see below)
./test/e2e.sh # full API lifecycle against mock GitHub + mock model (needs jq)
```
Run the unit suite through `npm`, not as a bare `node --test`. The script carries two
things a bare run does not:
- **A second reporter** (`test/spec-with-signals.mjs`) that prints `signal` and `exitCode`
when a test file's *process* dies. Node's default reporter shows that as `โ some.test.ts`
and `'test failed'` โ identical to a failed assertion, with nothing on stderr โ and the
tests after the death simply never run, so the pass count reads clean while being short.
- **`--no-sparkplug`**, which avoids a V8 bug that segfaults test children inside the
garbage collector roughly once in ten full runs on macOS arm64. The crash needs code
Sparkplug generates, so turning that tier off removes the path; it cost nothing
measurable here (two runs each, 55.8 s either way). It is on the test script only: `npm start`
and `npm run dev` keep the Sparkplug path on purpose, because one dev server dying is loud,
where a dead test child reads as a clean run with a short count. **Drop the flag when
[nodejs/node#65753](https://github.com/nodejs/node/pull/65753) ships in a 24.x release** โ
it is the backport of the V8 fix, and no released 24.x has it yet.
Both exist because of #405, which has the crash stack and the evidence.
The demo page must stay accessible โ it's audited with the project's own axe-core lint and
should report **0 violations**.
## Documentation
**Docs here are written in concise plain language. That is a requirement, not a preference.** It
covers `README.md`, everything under `docs/`, the comments in `config.example.yaml`, and the agent
prompts in `agents/` โ every one of them is read by someone deciding whether to trust Iris with a
document, and the prompts are read by a model as well.
What it asks for:
- **One idea per sentence.** If a sentence needs a second read, split it.
- **The claim first, the caveat after.** Never the reverse.
- **A number instead of an adjective.** "About 10.7ยข a page" beats "cost-effective". Anything you
assert about behaviour should be checkable against code, a test, or a named benchmark round.
- **No jargon without a gloss on first use**, and no new term where a plain one exists.
- **No repetition.** A paragraph that restates the one above it gets deleted, not softened.
- **One job per document, and no restating another one.** A claim lives in the document whose job it
is, and the others link to it โ that is what keeps this set DRY and cheap to correct, since a claim
written twice is a claim that goes stale in one place.
[README ยง Working on Iris](README.md#working-on-iris--including-if-you-are-an-ai-agent) says which
document holds what.
- **Shorter over completer.** A page nobody finishes documents nothing.
Two things this is not. It is not a ban on detail โ an exact number, a file path, or a caveat that
saves a reader an hour all belong in. And it is not about formatting: there is no linter here, and
nothing checks heading style, line length or word choice.
**Who checks it:** the automated review, then a maintainer. On a PR touching those files the
reviewer quotes a sentence that breaks one of the rules above and names the rule, as a
[non-blocking note](#what-the-automated-review-will-say) โ at most three, the worst ones. It is the
only prose it comments on, and a note is not a merge gate. It does not judge whether a document
earns its length: that is the last rule above, and it stays with the maintainer.
A PR that only makes an existing doc plainer is welcome, with no code change attached.
## Pull requests
- Branch from `main`, keep PRs focused, and describe the change + how you tested it.
- **`main` is protected: every change lands as a merged pull request.** Direct pushes are
refused for everyone โ maintainers included โ and force-pushes and branch deletion are
blocked. **@bbertucc** merges; a review from them is requested automatically
([CODEOWNERS](.github/CODEOWNERS)).
- **No formal approval is required to merge**, so don't wait for a green review checkmark that
never comes. The gate is a maintainer reading the PR, not a count of approvals โ GitHub won't
let anyone approve their own work, and requiring approvals would block the maintainer's
changes rather than yours.
- Match the surrounding code style (the codebase favors small, well-commented modules).
- New runtime dependencies should be justified โ Iris aims to stay portable and lightweight.
- AGPL-3.0: contributions are licensed under the same terms.
### What the automated review will say
Your PR gets a review from Claude in CI before a maintainer reads it
([details](docs/ci.md#automated-code-review)). Useful things to know:
- **It runs the checks itself** and quotes their real output, so a failing `typecheck` or `e2e`
comes back as a blocking finding with the relevant lines.
- **It will not nit-pick style, formatting or naming.** There's no linter or formatter in this
repo on purpose. It's also told not to suggest alternatives when your approach is correct, and
not to raise pre-existing issues your PR doesn't touch. If it does one of those anyway, that's a
bug in the prompt โ say so on the PR.
- **One exception: docs prose.** On a PR touching the files the [Documentation](#documentation)
section covers, it checks prose against those rules โ quoting the sentence, naming the rule, three
at most, always as a non-blocking note. The list of files lives in that section, not in this
bullet, so this bullet cannot fall out of date with it. Not in scope even there: heading style,
line length, Markdown layout, or wording it would have chosen differently.
- **`### Non-blocking notes` means "merge-ready".** A finding only blocks if something reaches it
on input the code accepts today; real-but-unreachable findings are notes on an *approval*. You
don't have to resolve them to merge, and you don't have to argue your way out of them.
- **Its verdict is advisory, and its check is not a merge gate.** A human merges. The check is
deliberately not required on `main`, because it could not be a trustworthy gate: a PR touching
only ignored paths never triggers it at all, and a fork PR's skipped job counts as satisfied
with nothing having reviewed the code. If you think a blocking finding is wrong, reply on the
PR โ the reviewer sees its earlier reviews on re-runs, but it is the maintainer you're actually
talking to.
- **Fork PRs aren't reviewed automatically** (a fork PR gets no CI secrets). A maintainer
dispatches the review manually; nothing is needed from you.
- **A PR touching `.github/workflows/**` gets a CI-security review first.** The workflows are part
of the app, so expect questions about `permissions:`, secrets reaching PR-authored code, and
`${{ }}` in `run:` blocks before anything about accessibility. Changing `code-review.yml` itself
also works now, with one visible difference: the review posts as **github-actions[bot]** rather
than claude[bot], and the PR gets a comment explaining why and asking for a human read as well.
- **`iris-auto/*` PRs aren't reviewed automatically either.** Those come from the scheduled triage
workflow, and GitHub doesn't start workflow runs for events raised by `GITHUB_TOKEN`. Each one
carries a comment saying so with the dispatch command. Nothing is needed from contributors.
## Architecture (orientation)
`src/pipeline` (extraction โ assembly โ review), `src/providers` (LLM provider abstraction),
`src/routes` (the `/v1` API), `src/auth` (the deployment's one GitHub identity, and the API gate),
`agents/` (the agent prompt files).
See [README.md](README.md) and [docs/API.md](docs/API.md).