Skip to content

Comments

When a file needs a purpose comment, and what a useful comment says

The goal is not more comments. It is comments in the places where a reader cannot recover the intent from the code.

Write a file-purpose comment when

The file meets any of these:

  • It holds a security boundary.
  • It holds a lifecycle rule, such as start and stop order, or a lease.
  • The order of operations matters and the code does not show why.
  • It runs past about 150 lines.
  • Its name does not predict its contents, and renaming it is not an option yet.

A useful file-purpose comment usually needs more than one sentence. It says why this file is a separate unit, what invariant it holds, and what a caller must not do.

Do not write a comment that restates the file name with a different domain term. A file called rate-limit.ts does not need a comment saying it limits rates.

Comment a function when

It has multiple phases, external effects, a security boundary, a lifecycle rule, or non-obvious fallback behavior.

Explain why a rule exists or why an operation order matters. Do not translate identifiers or individual statements into prose.

Add JSDoc to an exported API when its types do not fully describe its behavior, failures, or side effects.

JSDoc for backend declarations

When adding or changing backend modules, document every named service, Layer, type, function, helper, schema, and configuration description, including private declarations. Document each contract method. Apply this rule to app backend modules, owned scripts, and reusable test support too. Ordinary local variables, anonymous callbacks, and individual declarative schema fields do not need JSDoc. Generated code, migrations, entry-point re-exports, and ordinary test declarations remain exempt.

State the declaration's purpose first. Describe meaningful inputs, results, expected failures, side effects, and guarantees where applicable. A short pure helper can need only one sentence. Avoid parameter tags that repeat TypeScript types. Document method behavior once at its contract; implementation comments explain additional ordering or safety constraints. Describe expected Effect failures as failures in the error channel, not as JavaScript @throws.

For a service, explain the capability it owns and what callers can rely on. For a Layer, name the captured dependencies and distinguish construction from operation work. For a repository, explain transaction participation and the boundary between stored facts and authorization.

The JSDoc on ProjectAccess.layer in packages/services/lib/projects/access/service.ts provides a worked example:

/**
 * Captures ProjectRepository and AccessPolicy without reading stored access facts.
 * Each operation checks current authority and keeps its projection within one request.
 */

When reviewing a backend change, check every named declaration for coverage and accuracy. Apply this rule to changed modules; it does not require an unrelated repository-wide documentation pass.

Security code must say what it defends against

This is the rule that matters most, because a later reader who cannot see the threat will simplify the defense away.

packages/auth/lib/email-sign-in-response.ts rewrites a 403 EMAIL_NOT_VERIFIED into a 401 INVALID_EMAIL_OR_PASSWORD. Read without context that looks like a bug, and the obvious cleanup is to return the accurate status. It is a user-enumeration defense: the accurate status tells an attacker that the address is registered.

packages/db/lib/repositories/rate-limit.ts deletes old rows when Number.parseInt(key.slice(0, 2), 16) < 4. Nothing in the expression says why. The comment above it states the threat first: without the prune, the table grows with every distinct key. It then states the mechanism: calls for about 1 in 64 keys also delete rows older than RETENTION_SECONDS. The HMAC fixes that choice for each key.

State the threat, then the mechanism. One or two sentences.

Copy these files

FileWhy it is the model
apps/concourse/proxy.tsExplains why at each security decision, not what
apps/concourse/lib/auth/framework/session.tsExplains session gates, request-bound reads, and refusals
packages/db/lib/auditing/service.tsExplains operation order and what breaks if it changes
packages/auth/lib/client.tsRecords a compiler constraint a reader can otherwise undo

Study the last one. It stops a future reader from a change that looks correct and is not. That is the highest value a comment reaches.

Do not comment

Generated files, simple barrels, manifests, individual declarative configuration fields, ordinary tests, and thin user interface wrappers. Named backend configuration descriptions and schemas follow the JSDoc rule above.

Never add placeholder, history, or TODO comments. If work is outstanding, it belongs in an issue, not in the source.

Keep comments true

A wrong comment is worse than no comment. When you change code, fix the comments around it in the same change. Reject a diff that leaves a comment that describes the old behavior.

Language

Write comments in pragmatic Simple English: short sentences, active voice, one idea per sentence. Most readers of this repository read English as a second language, and every agent reads it literally.

How this is checked

Code review, deliberately. There is no comment lint rule and there will not be one. A rule that counts comments produces comments that restate code, which is the failure this page exists to prevent.

Reviewers: check comment quality on every diff that touches a file matching the triggers above.