Corrigenda: the things to be corrected.
Point at what a page got wrong, on the page itself. What arrives is the evidence rather than a description of it — the element, the CSS rules that styled it, what the browser was complaining about at the time, and a screenshot if you asked for one.
It is a capture tool for the people who run a site, not a public feedback box: the reporter is already logged in, reports land on disk, and somebody reads them when they choose to. No mail, no queue, no moderation.
Pick the thing that is wrong, say what is wrong with it, and press send. The switches are what turns a complaint into evidence, and they are yours to change before it goes: the panel says how many kilobytes that adds up to, and will show you the whole payload first.
It pairs well with an agent. Evidence is the difference between a
report somebody has to reproduce first and one a program can act on: a
selector and an XPath to find the element again, every rule that
matched it with the stylesheet and cascade layer it came from, the
computed values, the errors and failed loads, the viewport and colour
scheme it was all seen under. Point an AI at the JSON interface and it
can read what came in, reproduce the defect under the conditions the
report names, fix it where the rule lives, and write down what it did
with an after-picture beside the original. It can claim proposed;
calling something fixed stays a person's word, after their own look.
Nothing needs installing for that — the service serves the manual and
the schema an agent reads. For a program, and
AGENT-USAGE.md.
Ruby 3.4 and bundler. Three lines and the widget is in front of you:
bundle install
cp deploy/corrigenda-template.yml deploy/corrigenda.yml
./run -f -p 9393
Open http://127.0.0.1:9393/fixture.html. The widget opens on its own
menu — it was loaded on purpose, so the menu is what you came for —
and Debug in the corner is the way back in after closing it. File a
report about the test page: pick a kind, pick an element, type a
sentence, send. It lands in /var/tmp/corrigenda-fixture and shows up
at http://127.0.0.1:9393/review/. The template needs no editing for
this — -f serves the fixture page and drops the site allowlist, which
is why it is a playground and not a deployment.
Three things it does not do yet, in the order you will want them:
- Put the widget on a real site. The landing page at the mount carries a bookmarklet — drag it to the toolbar and it loads the widget on whatever page you are looking at, no install on the site at all. Below in Getting a report in.
- Answer somewhere permanent. Edit
deploy/corrigenda.yml— the store, the endpoint, who may reach it — and letdeploy/macrowrite the Apache configuration from it rather than writing it twice. deploy/README.md has the wiring. - Let a program read the reports.
api: truein the config, and/apianswers JSON that describes itself. For a program, and the manual an agent is handed is AGENT-USAGE.md.
Whatever you change, this says whether it still works:
bundle exec rake test the endpoint, ~2 seconds
bundle exec rake test:browser the widget, in real browsers
A widget (one JavaScript file, CSS inlined, living in a shadow root), a small Sinatra service that receives and stores reports and shows them back, and an optional browser add-on that improves one thing: the screenshot.
client/corrigenda.js the widget
lib/, views/ the service and its two pages
extension/ the add-on
deploy/ config and the Apache macro generated from it
Reports are directories, one per report, under a store you name — the
report as JSON, the screenshot beside it, a state file, and a
journal.jsonl of what has been done about it — plus one
index.jsonl line for listing. There is no database, and at tens of
reports a month there does not need to be.
Three ways, in the order they cost you anything:
- The bookmarklet. The landing page carries one; drag it to the toolbar and it loads the widget on whatever page you are looking at. Nothing has to be installed on the site.
- The site loads it. A MoXoW site says
corrigenda="https://tools.example.com|<who>"in its framework declaration, and the widget is on the page before its own images and scripts have finished failing — so those failures reach the report. - The add-on, alongside either of the above. A page can only photograph itself through the browser's screen-sharing permission, which cannot crop to an element or black out a password field. The add-on takes the picture through the browser's own capture API instead. See extension/README.md.
What a report carries follows the kind it is. A defect arrives with the evidence around it — the element, the rules, what the browser was complaining about, and the conditions it was all seen under. An idea does not: a suggestion or a question is a sentence somebody wants read, and one that asked for nothing sends nothing but its message and the page it is about — no browser, no viewport, no language or timezone. Switch a channel on and the conditions come back with it, because evidence nobody can place is not evidence. If the idea turns out to be about something on the page, the element switches are also the way to point at it: press one and the picker opens.
A screenshot is optional, and you choose how much of the page it holds. Cropping to the picked element keeps a 16-pixel margin around it — a defect is usually a relationship with whatever sits next to the element, and a crop at its own edges throws that away. On a high-DPI screen the stored image shows that margin at the screen's own scale, so it looks like more than sixteen. Form fields are blacked out before the image is encoded, and nothing off-screen is ever captured.
Ruby 3.4, Sinatra 4.2, Puma 8. Gems install into vendor/. The
quick start above is the same thing with the editing
skipped; this is the line that was skipped:
$EDITOR deploy/corrigenda.yml # the store, the endpoint, who may reach it
./run # no -f: the configured socket, the real store
The config is the deployment: where reports are written, the socket Apache proxies to, which sites may be reported on, how long a report is kept. It is not tracked, and the Apache configuration is generated from it rather than written twice — deploy/README.md has the whole wiring, including why the service starts by hand.
The review UI is at /review behind the same login as the endpoint.
A report is triaged from its row in the listing as well as from its own
page. Two controls, because they answer two questions: a list of
open, proposed, fixed or wontfix says what happened to the defect —
it shows the state the report is in, in that state's colour, and the
button beside it applies what you pick — and an archived switch says
whether anybody still wants it in front of them. A morning's reports do
not mean opening each one.
proposed is the claim before the confirmation: whoever did the work —
usually an agent — believes the defect gone, and the report waits for
your look before anyone calls it fixed. A blue row in the listing is
work claiming to be done; the journal beside it says what was done and
shows the after-picture to check it by.
Archiving keeps everything: the switch goes back the other way, from the archived list or from the report. Deleting is on the report's own page, at the far end of the row, and asks again.
From a terminal on the host, the same store, without the proxy:
rake data:list the working list
rake data:list ALL=1 with the archived ones
rake data:show ID=<report> one report in full
rake data:status ID=<report> what happened to it…
rake data:status ID=<report> SET=fixed NOTE="what you changed"
rake data:status ID=<report> NOTE="…" SHOT=after.webp
rake data:archive ID=<report> done looking at it (UNDO=1 to undo)
A note can carry a picture: SHOT= files it beside the report, and the
report's page shows it on that line of the trail with the original
screenshot beside it on request. That is the comparison worth having
before anyone believes a report is fixed — the same page, before and
after, rather than a sentence saying so. VIEWPORT= and SCHEME= ride
with SHOT= and say what that picture was really taken at; the page
prints them beside the conditions the report names, so a comparison
made at another width is one a reader can see rather than one they have
to trust.
The listing carries a six-character column for what a report holds —
E element, R css rules, C computed styles, D diagnostics,
A accessibility, S screenshot, a dot where a channel is missing.
The same letters appear on the review UI's chips.
Optional, and absent unless the config asks for it: a JSON interface
under /api, for a reader that is a program — an agent asked to fix
what was reported, a script that files a ticket. Switch it on with
api: true, and it lists reports, hands over one in full, and serves
the screenshot as an image. It describes itself at /api/, so a client
that knows nothing else can start there.
curl --unix-socket /var/run/corrigenda/corrigenda.sock \
http://localhost/api/reports
It describes itself: openapi.yaml, served at
/api/openapi.json and rendered at /apidocs, where the masthead
grows an API tab on a deployment that has switched it on.
llms.txt at the mount is the convention-shaped pointer to the same
thing, for an agent that arrives knowing nothing else; generated, so
it says the interface is off rather than linking a dead route.
Reading needs nothing further. Changing anything is listed, one grant at a time:
api:
write: [journal, archive, state]journal adds a line to a report's trail, archive takes it out of
the working list or puts it back, state says what happened to the
defect. Three rather than one because they are wrong in three ways: a
journal line is additive, so a wrong one costs noise; archiving is
reversible; a state is a claim somebody will trust and stop checking
behind. write: [journal] is where to start. write: true is all of
them. Deleting a report is not offered at any setting.
token: <secret> adds a Bearer token on top of whatever Apache already
asked for. With no api: key, every path under /api answers 404
rather than 403 — a route that is switched off should not advertise
that it exists.
The landing page and the service's own startup line both say which of those a deployment allows, so you never have to read the config to find out what is switched on.
Nothing here needs a plugin or an integration: the interface
describes itself, so an agent with a shell has enough. A running
service prints the prompt for you — the landing page carries one
written for that deployment, with the socket it is actually
listening on, a site it actually accepts reports about, and a last
paragraph that says what the agent is allowed to do there. Copy it
from the page rather than from here, where it could only say
<site>.
Three things worth keeping if you write your own: point it at the
interface's own root first, so it learns what it may do rather than
discovering a 403 halfway through — and write that address in full,
because only the socket sees this app unmounted; through the web it is
<endpoint>/api/, and a bare /api/ is a path on the host's root
where nothing answers. Say what fixed means to you — otherwise
it means "I changed something", which is a different claim and the
one you will be reading later. And ask for the picture, since a
screenshot taken after the work sits beside the one the report came
with and settles the claim without you opening the site. An agent
handed no prompt at all still has a front door: llms.txt at the
mount says what this is and points at both the manual and the schema.
An agent given write: [journal] and nothing else is a good way to
start: it tells you what it found and what it tried, and every state
on the board still moved because a person moved it.
The protocol itself — the review loop, what fixed means, the journal
voice, what is never the agent's to do — is written once, in
AGENT-USAGE.md, so a prompt can stay short and the
deployment's own page can stay the authority on its specifics. A
running service serves it at /agent/usage.md, which is where the
prompt sends the agent — only the socket is needed, no checkout.
/agent/ is the directory for anything written for one; llms.txt
stays at the mount, because a convention with a fixed spelling is only
found where it is looked for.
Nothing expires unless the config says it should:
retention:
archived: 90 # days since somebody archived it
any: 365 # days since filing, whatever its state
rake data:purge:show what that would take, and take nothing
rake data:purge take it
bundle exec rake test the endpoint, ~2 seconds
bundle exec rake test:browser the widget, in real Firefox and Chromium
The second half exists because the picker, the CSSOM walk, the sanitiser and the CORS dance only exist in a browser — see test/browser/README.md.
| DESIGN.md | what it captures and why, the payload, the storage model — the reasoning under all of it |
| HISTORY.md | approaches taken and abandoned, and the failures that shaped what is here |
| openapi.yaml | the JSON interface in full — the only description of it, rendered at /apidocs |
| AGENT-USAGE.md | using a deployment as an agent: the review loop, the journal, what is never yours |
| deploy/README.md | installing it, the Apache wiring, retention in cron |
| extension/README.md | the add-on: build, install, permissions |
| test/browser/README.md | the browser checks and what each covers |
| CLAUDE.md | conventions and host quirks, for agents working in this repo |
MIT — LICENSE.
SPDX-License-Identifier: MIT is carried by the files that travel
away from this repository on their own: client/corrigenda.js, which
any site may serve to its visitors, and the add-on's background.js
and content.js, which end up installed in a browser. The package
takes a copy of the licence with it for the same reason — a
WebExtension manifest has no field to name one. Everything else is
covered by the file above, where a reader who has the repository will
look.

