Code Comment Policy
Comments should explain information that the code cannot express clearly: rationale, constraints, invariants, compatibility behavior, security boundaries, or surprising ordering requirements. Do not translate the next statement into English.
Keep a comment when removing it would hide useful context, such as:
- why an operation must happen before or after another operation;
- a provider or platform quirk that the implementation works around;
- a security, cache, interaction-timing, or compatibility constraint;
- a non-obvious fallback and the condition that makes it safe;
- a suppression with a concrete reason; or
- an ordered JSDoc procedure whose order is part of the exported contract.
// Discord rejects a second acknowledgement, so modal branches return before deferral.if (opensModal) return showModal();Remove or rewrite
Section titled “Remove or rewrite”Remove comments that only name the statement below them:
// Parse and validate composite-key formatconst parsedKey = parseCompositeKey(compositeKey);Prefer a clear function or variable name. If important context exists, state that context instead of narrating the operation.
Also avoid:
- procedural labels such as
// 1. Parse the valueor// 5c-2. Build the menu; - prompt-style scaffolding such as
// Rule 3: Validate input; - decorative section banners;
- commented-out code; and
- prose em dashes, en dashes, or spaced double hyphens. Use punctuation that makes the relationship explicit.
Numbered JSDoc lists remain valid when they describe a genuinely ordered public contract. Ordinary line comments should not carry step numbers.
No meta commentary
Section titled “No meta commentary”A comment states a constraint the reader must respect. It is not a record of the work that produced the line. Keep out:
- Incident and changelog narrative. “this was the biggest consumer before we fixed it”, “added after last quarter’s outage”. Git history already holds this, and the comment cannot be kept accurate.
- Point-in-time measurements. Concrete counts, sizes, or timings sampled once. They read as current facts and silently become false. State the constraint, not the sample.
- Commentary about the comment. “Two exclusions are load-bearing”, “Note the following three points”, “Important:”. If the reader must count the parts, write fewer parts.
- Justification aimed at a reviewer. Arguing why a change is safe belongs in the PR description; a later maintainer only needs the rule that must hold.
Write the constraint that would still be true a year from now:
// Never sweep the client's own member: discord.js resolves permissions through it.not the story of how it was found:
// While chasing a memory leak we measured this cache and it dominated the heap, so we added a// sweeper. Note that two exclusions are load-bearing here, the first being that discord.js// resolves permissions through the client's own member.Length is the usual symptom. If a comment runs past two or three lines, the surplus is almost always narrative rather than constraint.
The same restraint applies to this page and every other doc: examples of bad comments should be illustrative, not transcribed from production. Deployment sizes, record counts, and host details do not belong in a public repository. See Docs Authoring (“Audience: Guide or Runbook”).
JSDoc tags
Section titled “JSDoc tags”JSDoc predates TypeScript, where @param {string} name was the only way to state a type.
The signature carries that now, so a tag that repeats the parameter name or its type adds
nothing and goes stale independently of the code.
Remove tags that restate the signature:
/** * Extract image URLs from a Brave image search response. * @param response - Image search API response // the type already says this * @returns Promise<string[]> // so does the return type */Keep tags that carry what the type cannot:
/** * @param modes - Empty when the provider reports no capabilities * @returns Comma-joined list, or empty string when no modes are supported * @throws {NvidiaImageModelUnavailableError} When the codename has no registered spec */Units, ranges, valid values, nullability the type does not encode, failure behavior, ordering and lifecycle guarantees, side effects, and cancellation or idempotency expectations all earn a tag.
A partial tag list is the expected result, not an oversight. Documenting one parameter and leaving two undocumented means those two were self-explanatory. Do not “complete” a block by adding tags that restate the signature, and do not delete a documented tag because its neighbours have none.
The same rule applies to the summary line above the tags. Build system prompt for LLM over
buildSystemPrompt() is the identifier in English, so the block goes. Keep it when it
defines a word the name leaves ambiguous:
/** * Finds the most active text channel that's accessible to the bot */export async function findBestChannel(guild: Guild, client: Client): Promise<TextChannel | null>findBestChannel never says what “best” measures. The summary names the ranking metric and
the filter, so it stays.
checkCommentPolicy.ts enforces the exact tag case as jsdoc-restatement, comparing only
after normalization and never on substrings. Summary echoes are heuristic and surface under
obvious-narration: a warning during a full audit, an error once the line is in your diff.
Judgment cases stay with review.
Treat findings as review prompts
Section titled “Treat findings as review prompts”The audit is a heuristic reviewer, not a deletion checklist. A zero-warning result is useful only when the remaining code still explains its non-obvious constraints. Do not make the counter reach zero by deleting rationale, truncating a multi-line explanation, adding broad exceptions, or leaving an empty JSDoc block.
For every finding:
- Read the complete comment or JSDoc block and the code it describes.
- Remove the comment only when the code already expresses everything it says.
- Rewrite the block when it mixes narration with rationale, keeping the constraint, compatibility behavior, security boundary, or ordering requirement.
- Re-read the surrounding paragraph after editing. Remove vacated divider lines and JSDoc gaps, and make sure no continuation became a sentence fragment.
- Review the final diff as prose before running the audit again.
orphaned-comment catches provable partial-cleanup damage such as an indented continuation
without an opening line, a bare divider remnant, or a completely empty JSDoc block. It cannot
decide whether deleted context was valuable, so human diff review remains required.
Maintainer audit
Section titled “Maintainer audit”bun run audit-commentsaudit-comments reports subjective narration candidates across the existing tree without
failing. It runs as a non-blocking warning under the Documentation section of bun run vl,
so contributors can see policy drift without needing to resolve heuristic findings as part
of unrelated work. It remains separate from the normal test runner.
Maintainers can invoke scripts/checks/checkCommentPolicy.ts directly for deterministic
checks or pass --staged or --base <ref> to focus the narration heuristic on changed
lines. The command prints this policy guide before its findings so contributors have the
editing criteria beside the report. Its focused self-test is also manual:
bun test ./scripts/checks/commentPolicy.test.tsExceptions
Section titled “Exceptions”Literal syntax and live-rule references sometimes contain text that resembles a violation.
Record only those narrow cases in
scripts/checks/comment-policy-exceptions.json, including the exact comment and a reason.
The checker reports an exception as stale once the matching comment disappears.
Do not use the exception file as a catalogue of comments removed in past cleanups. Git history is the durable record for those edits; the exception list exists only for current, intentional violations.