Workflow · TypeScript 6.0 · TypeDoc 0.28 · TSDoc · Docusaurus 3.10 · MADR 4.0
Documentation
Explain why, keep docs next to code, current or deleted.
Updated 5 Jul 2026 · CC0
AGENTS.mdrepo rootYou own the documentation for this TypeScript/Node codebase: READMEs, TSDoc API comments, ADRs, changelogs, and the docs site. "Good" means a newcomer can go from clone to running tests in five minutes by copy-pasting, every public API has a docstring a tool can render, every significant decision has an ADR, and no doc lies — stale docs are deleted, not left to rot. Docs ship in the same PR as the code they describe.
Stack
- Language/runtime: TypeScript 6.0 (
typescript@6.0.3), Node.js 24 LTS. TS 7.0 (the Go-basedtsgocompiler) is at RC — do not pin it yet; keep advice valid for the stable 6.0 checker. - API reference generator: TypeDoc
0.28.xwithtypedoc-plugin-markdown@4when emitting Markdown into a docs site. Never hand-maintain an API table — generate it. - Doc-comment standard: TSDoc (
@microsoft/tsdoc@0.16) enforced byeslint-plugin-tsdoc. Use TSDoc tags, not loose JSDoc type annotations (types live in the TS signature, not the comment). - Docs site: Docusaurus
3.10.x(@docusaurus/core@3.10) for TS/JS projects. For Python-heavy repos use MkDocs Material9.7.x— but note it is in maintenance mode (feature-frozen since 9.7.0, security fixes through Nov 2026); Zensical is its successor, not yet 1.0, so do not adopt it for production docs. - Prose linter: Vale
3.xwith a vocabulary and one style package (Google or Microsoft Writing Style Guide) in.vale.ini. - Markdown linter/formatter:
markdownlint-cli2@0.23for content rules, Prettier3.xfor layout. They do overlap on stylistic rules (list indentation MD004/MD007/MD030, blank lines MD012, trailing whitespace MD009), which fight each other — so disable markdownlint's Prettier-owned formatting rules via its built-in preset ("extends": "./node_modules/markdownlint/style/prettier.json"in.markdownlint-cli2.jsonc) and let Prettier own all whitespace/wrapping. Markdownlint then enforces only content rules Prettier can't (MD024 duplicate headings, MD033 raw HTML, MD034 bare URLs, MD040 fenced-block language). - Link checker:
lychee(Rust, fast) in CI to catch dead links and anchors. - Changelog/versioning: Changesets (
@changesets/cli@2.31) producing a Keep a Changelog 1.1.0-formattedCHANGELOG.md, SemVer 2.0.0. - ADRs: MADR 4.0.0 template. Optionally manage with
log4brainsfor a browsable ADR site. - Diagrams: Mermaid
11.xfenced blocks (```mermaid) — versioned as text, rendered natively by GitHub and Docusaurus. Model architecture with the C4 model (Context → Container → Component). - Doc architecture: organize the docs site by Diátaxis — tutorials, how-to guides, reference, explanation — as four distinct top-level sections.
Project conventions
README.md # entry point; quickstart + orientation only
CHANGELOG.md # Keep a Changelog, human-curated via Changesets
CONTRIBUTING.md # how to build/test/submit; linked from README
SECURITY.md # vuln reporting policy
LICENSE
.changeset/ # pending changeset markdown files
docs/
tutorials/ # learning-oriented (Diátaxis)
how-to/ # task-oriented
reference/ # information-oriented (TypeDoc output lands here)
explanation/ # understanding-oriented
adr/
0001-record-architecture-decisions.md
NNNN-short-title.md
diagrams/ # .mmd source + C4 models
.vale.ini
.markdownlint-cli2.jsonc
- File names: kebab-case
.md. ADRs are zero-padded, monotonic, immutable IDs:0007-use-changesets.md. Never renumber or reuse an ID. - One H1 per file, matching the file's topic; sentence case headings (
## Error handling, not## Error Handling). - Links are relative within the repo (
../adr/0007-use-changesets.md), never absolutegithub.com/...URLs that break on forks and rename. Link to headings by their generated anchor. - Docs live next to the code they describe and are versioned in the same repo. A package's README sits in that package's folder in a monorepo.
- Fenced code blocks are always tagged with a language (
```ts,```bash,```json) — untagged blocks skip syntax highlighting and prose-lint exclusion. - Prose lints on: keep sentences under ~25 words, active voice, second person for instructions ("Run
npm test"), present tense.
README
The README orients a newcomer and gets them running. It is not the manual — deep content goes in docs/.
Required sections, in order:
One-line description + optional badges (CI status, npm version, license). No marketing paragraphs.
Prerequisites — exact versions:
Node.js >= 24,pnpm >= 10(pin the exact line viapackageManagerinpackage.json). State them; do not assume.Quickstart — copy-pasteable, in order, that actually works on a clean clone:
git clone https://github.com/org/repo.git cd repo pnpm install cp .env.example .env # then fill in the values documented below pnpm dev # starts on http://localhost:3000 pnpm testConfiguration / environment — every env var in a table: name, required?, default, description. Ship a committed
.env.examplewith every key present and dummy/placeholder values — never real secrets.Common tasks — the 4-6 commands a contributor runs daily (
pnpm build,pnpm lint,pnpm typecheck,pnpm changeset).Contributing — one line linking to
CONTRIBUTING.md.License — one line linking to
LICENSE.
Rules:
- Every command in the README is executed by CI (see Testing) so it can never silently rot.
- Do not paste API reference into the README — link to the generated TypeDoc reference.
- If a command needs a running service (DB, Redis), give the exact
docker compose upline, not "make sure Postgres is running". - Keep it under ~150 lines. When it grows past that, move detail into
docs/how-to/.
Code comments
Comment why, never what. The code already says what it does; the comment explains the reason a reader cannot recover from the code.
Document non-obvious decisions, gotchas, invariants, units, and edge cases:
// Stripe rounds half-up; we round half-even to match the ledger, so a // 2.5-cent fee becomes 2, not 3. Diverging here caused reconciliation drift. const fee = bankersRound(rawFee); // timeout is milliseconds, not seconds — the upstream SDK takes ms. const client = new Client({ timeout: 30_000 });State invariants a reader must not break:
// INVARIANT: entries stays sorted by ts ascending; binarySearch below depends on it.Delete commented-out code. Git is the history. A block of
// const old = ...is never acceptable in a diff.No comments that restate the signature:
// increments the counteraboveincrement()is noise — delete it.Use
// TODO(username): reason + linkwith an issue reference, never a bare// TODO. Bare TODOs are unactionable and accumulate.Prefer a well-named function/constant over a comment when the comment only exists because the code is unclear.
API docs
Every exported symbol (functions, classes, public methods, types, constants) carries a TSDoc comment. Internal/unexported code does not need one unless the logic is subtle. eslint-plugin-tsdoc fails the build on malformed tags.
Do not repeat types in the comment — TypeScript already has them. Document purpose, meaning, failure modes, and an example.
/**
* Charges a customer's saved payment method for a one-off amount.
*
* Idempotent per `idempotencyKey`: repeated calls with the same key return the
* original charge instead of double-charging.
*
* @param customerId - Stripe customer id (`cus_...`).
* @param amount - Amount in the smallest currency unit (cents for USD).
* @param idempotencyKey - Caller-supplied unique key; reuse to retry safely.
* @returns The settled {@link Charge}, or `pending` if async capture is on.
* @throws {PaymentDeclinedError} When the issuer declines the charge.
* @throws {RateLimitError} When Stripe returns HTTP 429.
*
* @example
* ```ts
* const charge = await chargeCustomer("cus_123", 4_999, crypto.randomUUID());
* ```
*
* @public
*/
export async function chargeCustomer(
customerId: string,
amount: number,
idempotencyKey: string,
): Promise<Charge> { ... }
- Use the standard TSDoc tags:
@param,@returns,@throws,@example,@remarks,@deprecated, and the release tags@public/@internal/@beta. @deprecatedmust say what to use instead and since when:@deprecated Since 3.2 — use {@link chargeCustomer} instead.- Link symbols with
{@link OtherThing}so TypeDoc cross-references resolve. - Every
@examplemust compile — CI extracts and type-checks fencedtsexamples. - The first sentence is the summary line TypeDoc shows in listings — make it a complete, standalone sentence.
Architecture
Record every architecturally significant decision as an ADR using MADR 4.0. "Significant" = hard to reverse, or affects structure, dependencies, interfaces, or how teams build (choosing a DB, an auth model, a rendering strategy).
MADR 4.0 skeleton:
---
status: accepted # proposed | rejected | accepted | superseded by ADR-0012
date: 2026-07-05
decision-makers: [issam]
consulted: [backend-team]
informed: [all-eng]
---
# Use Changesets for versioning
## Context and Problem Statement
We publish 4 packages from one monorepo and hand-edited changelogs drift...
## Considered Options
- Changesets
- semantic-release
- Manual SemVer + hand-written CHANGELOG
## Decision Outcome
Chosen "Changesets" because it fits monorepos and keeps a human-curated changelog...
### Consequences
- Good: per-package versioning, PR-reviewable changelog entries.
- Bad: contributors must remember `pnpm changeset`; enforced via CI check.
Rules:
ADRs are immutable once accepted. To change a decision, write a new ADR and set the old one's
statustosuperseded by ADR-NNNN. Never edit history.ADR-0001 is always "Record architecture decisions" (the meta-decision to keep ADRs).
Keep the big picture in Mermaid C4 diagrams versioned as text, not exported PNGs (PNGs go stale and can't be diffed):
flowchart LR user([User]) --> web[Next.js app] web --> api[API service] api --> db[(Postgres)]Put the system-context diagram in
docs/explanation/architecture.md; link ADRs from it. A diagram shows structure; ADRs explain why.
Changelog
Maintain CHANGELOG.md in Keep a Changelog 1.1.0 format, generated from Changesets. Write changelog entries for humans reading a release, not a raw git log.
- Every user-facing PR adds a changeset:
pnpm changeset, pick the bump (patch/minor/major), write one clear sentence in the imperative. CI fails PRs that changesrc/with no changeset. - Group under the fixed headings:
Added,Changed,Deprecated,Removed,Fixed,Security. Keep an## [Unreleased]section at the top. - Entries describe impact, not internals: "Fixed timezone offset in invoice PDFs" — not "patched
formatDatein utils.ts". - SemVer strictly: breaking change → major, additive → minor, fix → patch. Never quietly break in a minor.
- Never rewrite released changelog entries; append corrections.
Keep every doc current in the same PR as the change, or delete it. A stale README, a lying docstring, or a broken quickstart is worse than none — it destroys trust. If you touch a public API, update its TSDoc and any how-to that references it in the same diff.
Testing
Docs are tested like code — a broken doc fails CI:
- Markdown lint:
markdownlint-cli2 "**/*.md"andprettier --check "**/*.md". - Prose lint:
vale docs/ README.mdagainst the chosen style package. - Dead links/anchors:
lychee --offline docs/ README.mdfor internal, a periodic online run for external. - Runnable quickstart: a CI job that runs the README's install→run→test commands verbatim on a clean checkout. If it drifts, the build breaks.
- Compilable examples: extract fenced
tsblocks andtsc --noEmitthem so no doc shows code that doesn't compile. - Docs site build:
docusaurus buildmust pass withonBrokenLinks: 'throw'andonBrokenAnchors: 'throw'indocusaurus.config.ts. - TSDoc validity:
eslintwitheslint-plugin-tsdocenabled fails on malformed doc comments.
Security
- Never put real secrets in docs, examples, or
.env.example. Use obvious placeholders:STRIPE_KEY=sk_test_xxx,sk_live_REPLACE_ME. Scan with a secret scanner (gitleaks) in CI before publishing. - Redact internal hostnames, IPs, employee names, and customer data from examples and diagrams. Use
example.com,cus_123. - Ship and maintain
SECURITY.md: how to report a vulnerability, supported versions, response SLA. Do not disclose unpatched vulns in the public changelog until fixed. - The docs site is a real app — pin and audit its deps (
pnpm audit), and disable directory listing / source maps in the published build. - Do not document internal-only endpoints, admin routes, or feature flags in public docs.
- License every code snippet you copy in; attribute third-party diagrams.
Do
- Update docs in the same PR as the code change; treat a missing doc update as a failing review.
- Write for a newcomer; prefer a concrete, copy-pasteable example over a paragraph of prose.
- Generate the API reference with TypeDoc; keep it in
docs/reference/, regenerated in CI. - Make every command in README/CONTRIBUTING actually runnable and CI-verified.
- Give ADRs immutable numbers and supersede rather than edit.
- Keep diagrams as Mermaid/C4 text so they diff and never desync from reality.
- Delete a doc the moment it stops being true.
Avoid
- JSDoc
@param {string} nametype annotations in TS — the type is in the signature; use bare TSDoc@param name - .... - Hand-written API tables — they rot; generate with TypeDoc instead.
- Commented-out code and bare
// TODO— delete the former; issue-link the latter. - Comments that restate the code (
// loop over users) — comment the why or nothing. - Exported PNG/JPEG architecture diagrams — use versioned Mermaid text; binary images go stale and can't be reviewed.
## [1.4.0] - see git logchangelogs — write human, impact-focused entries via Changesets, not a dump of commit subjects.- Absolute in-repo links (
https://github.com/org/repo/blob/main/...) — use relative paths that survive forks and renames. - A README that documents commands nobody runs — if
npm startno longer exists, the README is a bug. - Adopting Zensical or TS 7.0 for production yet — both are pre-stable as of this stack; stay on MkDocs Material 9.7 / Docusaurus 3.10 and TypeScript 6.0.
@deprecatedwith no replacement — always name the successor and the version.
When you code
- Ship the doc change in the same diff as the code. If the PR changes a public API and its TSDoc is untouched, that is incomplete, not done.
- Keep doc diffs small and focused; do not reflow an entire file's Markdown while making a one-line fix (it hides the real change in whitespace noise).
- Before opening a PR, run the doc gate locally:
pnpm typecheck && pnpm lint && markdownlint-cli2 "**/*.md" && prettier --check "**/*.md" && vale docs/ && lychee --offline docs/ README.md. - After changing any exported symbol, regenerate TypeDoc and confirm the reference builds.
- Ask before: changing the docs framework or its major version; restructuring the top-level
docs/taxonomy; deleting an ADR (you supersede, never delete); or removing a documented public API (that is a breaking change needing a major bump). - When a decision is architecturally significant, write the ADR first — the PR that implements it links to it.
- If you find a doc that lies, fix it or delete it in the current PR; do not leave known-false documentation behind.
Drop it in your repo
Save these rules as AGENTS.md, CLAUDE.md, .cursorrules, .windsurfrules or .github/copilot-instructions.md — your agent instantly codes to the same standard on TypeScript 6.0 · TypeDoc 0.28 · TSDoc · Docusaurus 3.10 · MADR 4.0.