Imported from timothyjordan/a14y (
AGENTS.md). Install upstream withnpx skills add timothyjordan/a14y. Copyright stays with the author.
AGENTS.md — a14y (Agent Readability)
Machine-readable invariants for coding agents working in this repo
(Claude Code, Codex, Cursor, OpenCode, etc.). For the longer
human-readable version, see CONTRIBUTING.md.
Hard invariants
- Published scorecards are an immutable contract. Once a
scorecard version ships, it never changes — every consumer pinned
to that version must keep getting the same score forever. This
means:
- Never modify
packages/core/src/scorecard/v0_*.ts. - Never mutate an existing
'1.x.x'entry under a check'simplementationsmap. To change behavior, add a new entry ('1.1.0') and pin it indraft.ts. - The contribution surface is
packages/core/src/scorecard/draft.tsplus newimplementationsentries — never the past.
- Never modify
- Never hand-edit a
README.md. All user-facing READMEs are generated fromdocs/templates/anddocs/fragments/. Edit the sources and runnpm run docs. - Never commit directly to
main. Branch asfeature/...,fix/...,refactor/...,docs/..., orchore/.... - Use Conventional Commits.
The release pipeline reads commit prefixes to decide version bumps.
chore:,docs:,ci:,test:do not trigger a release. - Frontmatter
idinsrc/content/checks/<id>.mdmust equal the filename and the pin indraft.ts. The coverage gate fails the build otherwise. - If your change touches
draft.ts, therefresh-draft-diffworkflow reconcilesdraft-changes.jsonafter you merge. That JSON holds the attribution data the docs site reads to render each diff entry on/scorecards/draft/and/scorecards/draft/changes/. The workflow runs on push tomain(i.e. after your PR merges), not on the PR branch, because an auto-commit-back to the contributor branch is structurally broken for fork PRs (read-onlyGITHUB_TOKEN). It regenerates the JSON against the live net diff and opens a separate follow-up PR (chore/refresh-draft-diff) that attributes new entries to the PR that triggered it; a maintainer reviews and merges that follow-up like any other change (there is no auto-merge). You do not need to run anything on your own PR. To preview the would-be JSON before merging, runnode scripts/refresh-draft-diff.mjs --localand eyeball that it reflects the net diff. If the post-merge run fails for infra reasons, trigger a catch-up fromActions → Refresh draft diff → Run workflow. - Non-trivial scorecard changes ship docs-first. Split into two
PRs: a spec PR (the check's
.mdpage + thedraft.tspin, plus a stub'1.1.0'implementation copying the prior version when updating an existing check, so the pin resolves and tests stay green) lands and merges first; an implementation PR then replaces the stub with the real'1.1.0'handler. Rationale: prevents the spec PR and impl PR from pulling against each other while reviewers negotiate the contract. See CONTRIBUTING → Docs-first for scorecard changes.
Canonical commands
npm install
npm run build
npm test # every workspace + readme builder
npm test --workspace @a14y/core # registry + check engine
npm test --workspace @a14y/docs # mirror integration + coverage gate
npm run docs # regenerate READMEs from docs/
Use the space form (--workspace @a14y/core), not the = form, to
match the rest of the repo.
Where things live
| Want to change… | Edit |
|---|---|
| A check's logic | packages/core/src/checks/{site,page}/<file>.ts (camelCase file name; one file may declare several related check ids — e.g. llmsTxt.ts declares llms-txt.exists, .content-type, .non-empty, .md-extensions). Add a new '1.x.x' entry under implementations; do not mutate existing ones. |
| Which checks are on the rubric | packages/core/src/scorecard/draft.ts |
| Wire a new check into the registry | packages/core/src/scorecard/_imports.ts |
| A check's docs page | packages/apps/docs/src/content/checks/<id>.md |
| Prose pages (glossary, privacy, scorecards intro/tail) | packages/apps/docs/src/content/pages/*.md |
Landing page or /spec/ design |
packages/apps/docs/src/pages/{index,spec}.astro |
| HTML→markdown mirror rules | packages/apps/docs/src/lib/html-to-markdown.ts (+ test) |
| README content | docs/templates/ and docs/fragments/, then npm run docs |
Adding a new check (recipe)
For non-trivial additions, split into spec + impl PRs (see invariant #7). The steps below are the impl PR.
- Add
packages/core/src/checks/{site,page}/<file>.tswith animplementationsmap keyed by'1.0.0'. The file name is camelCase; the check ids inside it are kebab-case and scoped (e.g.llms-txt.exists). - Import it in
packages/core/src/scorecard/_imports.ts. - Pin it in
packages/core/src/scorecard/draft.ts. - Add
packages/apps/docs/src/content/checks/<id>.mdwith required frontmatter (id,title,group,scope,why). - Run
npm test --workspace @a14y/coreandnpm test --workspace @a14y/docs.
Updating an existing check (recipe)
For non-trivial bumps, split into spec + impl PRs (see invariant #7). The steps below are the impl PR — the spec PR already shipped the
.mdpage, thedraft.tspin bump, and a stub'1.1.0'copy of the prior handler; here you replace that stub with real logic.
- In
packages/core/src/checks/{site,page}/<file>.ts, add a new'1.1.0'(or higher) entry toimplementations. Leave the existing'1.0.0'entry untouched — frozen scorecards reference it by version. - Bump the pin in
packages/core/src/scorecard/draft.ts. - Update
packages/apps/docs/src/content/checks/<id>.mdif behavior changed. - Run the same test commands as above.
Definition of done
Before opening a PR, confirm:
- Affected workspace tests pass.
- No
v0_*.tsfile was modified. - If a check changed:
draft.tspin, content file, and_imports.tsare all consistent. - If
.astroor global CSS changed: class-name audit diff is empty or intentional (see CONTRIBUTING → "Visual / design parity"). - If anything under
docs/templates/ordocs/fragments/changed:npm run docswas run and the regenerated READMEs are committed. - If
draft.tschanged: you previewed the reconciledpackages/core/src/scorecard/draft-changes.jsonwithnode scripts/refresh-draft-diff.mjs --localand it reflects the net diff. Therefresh-draft-diffworkflow opens the attributed follow-up PR after this PR merges tomain. See CONTRIBUTING → Diff refresh workflow. - If non-trivial rubric change: docs-first split was used — the spec PR landed before this impl PR (see invariant #7).
- Commits follow Conventional Commits.
Further reading
CONTRIBUTING.md— full contribution guide.RELEASING.md— release flow, Conventional Commits scopes, release-please mechanics.TESTING.md— manual smoke-test recipes for the CLI, Chrome extension, and docs site.