# Document Code

> Write and improve code documentation. Use whenever asked to document code, add comments, write a doc comment, annotate a function, document a package, explain a file's purpose, or clean up unclear or outdated comments — however phrased ("doc this"). Applies to Go, Bash/shell, YAML, Svelte, SQL, TypeScript, and other source or config files. Invoke before writing any documentation, even for one function.

- Skill: `a-novel-kit/document-code` (Agent Skill)
- Install (CLI): `npx skillmds@latest add a-novel-kit/document-code`
- Raw SKILL.md: https://api.skillmd.com/api/skills/a-novel-kit/document-code/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: a-novel-kit (https://skillmd.com/u/a-novel-kit)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/a-novel-kit/document-code

---


# Code Documentation Skill

This skill governs how to write, improve, and maintain code documentation. Help the reader understand
**what** something does and **how to use it**, not how it is implemented.

**Document at the element level.** Every exposed element — type, function, target, value, field —
documents itself directly; never describe elements in an aggregate block above them. A section header
listing its targets, a type doc enumerating its variants, a struct doc itemizing its fields all put the
doc in the wrong place: the reader looks at the element and finds no comment, while the description sits
paragraphs above with all the others. This applies everywhere — pnpm scripts, enum values, interface
methods, struct fields.

---

## What to Document

Document these file types:

- **Language source files**: `.go`, `.ts`, `.svelte`, `.sql`, `.sh`, `.bash`, `.proto`, etc.
- **General configuration files**: `.yaml`, `.toml`, `.json` (when they encode project logic, not tool config)

Do **not** document these:

- Tool-specific configuration files: `.golangci.yaml`, `.eslintrc`, `.prettierrc`, `renovate.json`, etc.
  Their documentation belongs to the tool, not the codebase.
- Auto-generated files (protobuf output in `protogen/`, mocks, migrations generated by tooling).

For `.proto` files, the comment conventions on `service`, `rpc`, `message`, `field`, and `enum` values
belong to the **`write-proto` skill**: apply this skill's general principles (accuracy, no redundancy,
intent over mechanics) and follow write-proto for proto-specific comment style and placement.

For `openapi.yaml`, field descriptions, examples, and spec-level rules belong to the **`write-openapi`
skill**: apply this skill to prose quality, write-openapi to every spec-structural decision (status
codes, schema shape, breaking changes).

---

## What deserves a comment at all

Most code deserves none. A comment must clear one of two bars:

1. **It describes the repo's own logic** — type and method docs, package roles, domain
   invariants (the "Documentation Priorities" below).
2. **It explains an unusual behavior specific to this repo** — a constraint, workaround, or
   ordering the reader cannot extract from the code in front of them.

Orientation is part of the first bar. In a long function or file, one line naming each logical
movement lets a contributor grasp the shape before reading it. Pace those deliberately: one orienting
line per movement, nothing inside it unless a line is genuinely surprising. Clustered or line-by-line
comments bury the signal as badly as none at all.

Everything else is noise. In particular:

- **Platform-wide conventions don't get repeated per repo.** A behavior that is unusual but holds
  across the organization (e.g., compose files never declare sibling services because the tooling
  resolves them) belongs ONCE in the tool that implements it or in the org-level documentation; copies
  in each repo drift. If a reader could be surprised, fix the central doc, not the local file.
- **The global picture belongs to the project docs.** The architecture, the layer split, the reason
  the system is shaped this way — README and CONTRIBUTING own that (see `write-project-docs`); a code
  comment re-explaining it duplicates a doc that then drifts. Code comments carry the _local_ detail
  those files can't: the invariant at the line that enforces it, why this call precedes that one.
- **Nothing the code already says.** If reading the surrounding lines answers the question, the
  comment restates; delete it. `depends_on` already shows ordering — only the _why_ of a
  surprising dependency is comment-worthy.
- **No external dependencies.** A comment must stay meaningful from the git history alone. Never
  reference a PR, review thread, or temporary plan document. The one exception: linking an issue
  for a patch or temporary workaround, where the issue _is_ the tracking artifact. Write it as the
  full URL — `https://github.com/<org>/<repo>/issues/<n>` — and keep the marker short, ending it
  with `Tracked in <url>`. In source a bare `#N` is not a hyperlink and names no repo, while a full
  URL pastes straight from the editor into a browser. (On GitHub itself `#N` auto-links, so this
  applies to code.)

The test, with "document the code as it is" below: a new contributor with only this repo checked out
today — does the comment teach them something true, local, and not already on their screen?

---

## Prose economy

Maximize meaning per word. Every sentence carries an idea the reader needs — rationale first,
mechanics only when surprising. Exhaustive and terse are not opposites: cover everything that
matters, in as few words as it takes, and stop.

- Lead with _why_; the code already shows _what_.
- Cut hedges, preambles, and ceremony ("note that", "in order to", "it is important to").
- **Stay sharp. Cut the sugar.** Softening costs words and buys nothing: a comment is read by someone
  who needs the fact, not reassurance. Drop the throat-clearing ("on purpose", "deliberately", "it is
  worth noting", "the case that matters"), the emphasis that adds no information (`MUST`, `NEVER`, and
  the rest of the shouted words), and the build-up that delays the point ("two is nobody's intended
  answer"). Assert the fact and move on.
- One precise sentence beats three approximate ones. If a comment reads well aloud, it ships.
- **Default to plain words.** Prefer the common word and the short one. Reach for an advanced or
  technical word only when it earns its place — when it replaces a whole clause or removes a real
  ambiguity.
- **Write plain sentences, not labels.** State the point as a subject-verb-object sentence and keep
  that order; a fronted object or a cleft makes the reader unpack it first. A rhetorical label
  ("The reason:", "Note:", "Why this matters:") is ceremony — delete it and let the sentence carry
  the idea: "This runs first because the cache is cold", not "Why this runs first: the cache."
- **Give every layer one job.** A heading makes the task easy to find, prose supplies context or a
  decision rule, code carries the mechanics, and a comment records a hidden constraint. Do not restate
  one fact across those layers or present a value derived from itself as verification. After
  `## Check the account` and `tool account`, "This prints the account" adds nothing; say "Continue only
  if it names the intended account." Delete any sentence, comment, or check whose removal loses no fact
  or decision.
- **Name a thing once, and keep the name.** Use one term for one concept. Do not alternate synonyms
  ("the cache," then "the store") for the same thing, or the reader stops to check whether they differ.
  An abstract label earns its place only when tied to something the reader can hold: "the intention" is
  vague until it is "the specification behind your idea" — name it that, then carry "specification"
  through. Where the domain already has a precise term, use it and resist paraphrasing it for warmth,
  because a paraphrase can carry a connotation the precise term rules out: a "convoy" may lose
  stragglers and still be one, where an "atomic landing" is all-or-none by name. When a name doubles as
  a common word, name its category on first use — "a dedicated Tracking status" reads as a place on the
  board where "Tracking" alone reads as an activity — and use it bare afterwards. When one concept spans
  several concrete forms, name the umbrella too, and never reuse that word in its ordinary sense a line
  later.
- **Make every reference land on something the reader already holds.** Keep a pronoun beside its one
  possible antecedent: "comprehension and judgment are the whole job, so spend your attention there"
  beats "so spend them there", where "them" reaches back past "the whole job" for its noun. Name earlier
  content by a plain description rather than an abstract handle the reader has to decode; a handle earns
  its place only once you have attached it to the thing.
- **Concision is careful word choice, not dropped grammar.** Keep sentences fully formed — subject and
  verb — rather than clipping them to fragments. A doc that is short but cryptic has failed.
- **Write the choice, not the rejected alternative.** State what holds and stop. The contrast form
  ("X is this, not that", "we use A rather than B") spends a clause on something the reader was never
  going to do, and leaves the rejected idea sitting in their head next to the real one. Write "Values
  are stored as JSONB", not "Values are stored as JSONB, never EAV". Keep a counter-example only where
  the wrong path is the one a reader would otherwise take; then give it once, plainly.
- **A counterfactual is that same defect in other grammar.** A sentence describing what _would_ happen
  under an implementation nobody wrote defends the code instead of describing it: "a fixed counter
  would repeat them", "a nested transaction would inherit the caller's isolation", "a redacted
  placeholder would still leak the length". State the property that holds — "Both must differ", "A
  redaction still carries its length" — and drop the rest. Three uses of "would" survive this rule.
  A **previewed action** the code shows without taking: "renderPruneImpact names the rulesets a plan's
  prune op would delete" documents what a dry run displays. A **condition that genuinely occurs**:
  "git refuses when incoming commits would clobber a locally-modified file" is git's real behavior and
  the code branches on it. And the wrong-path counter-example above: "depend on the typed structs
  declared here rather than reading os.Getenv directly" names the call a reader would otherwise reach
  for. When the conditional describes what the reader is shown or a condition that occurs, keep it.
- **Prefer affirmative sentences to clause chains.** Give a rule its own sentence rather than trailing
  it off another as "…, so X must Y", and split a chain of subordinate clauses into plain statements.
  "…the MAC and ENC keys of an AES-CBC-HMAC key, so they must differ" becomes "…the MAC and ENC keys
  of an AES-CBC-HMAC key. Both must differ." This is not license to shred prose into fragments — the
  aim is statements a reader takes in one at a time, and the fully-formed-sentence rule above still
  holds.

This applies to every prose surface we write — doc comments, package docs, README sections, PR
descriptions (see `open-pull-request`), and planning issues (see `plan-feature`). Load this section
before writing prose on any of them, including surfaces this skill does not otherwise own.

---

## Documentation Priorities

### 1. Exported / public symbols (highest priority)

Write with the **end-user in mind**: the reader is using this API, not maintaining it.

- Describe what the symbol does and how to use it. Keep it relevant and direct.
- Do not explain internal implementation details.
- Do not restate the name or the obvious (e.g., `// Foo returns foo.` is useless).
- **Every exported type needs a type-level doc**, including parameter and option structs
  (`FooRequest`, `FooOptions`, etc.) — even when every field already has its own comment.
  Field docs explain the fields; the type doc explains the purpose of the whole.

### 2. Unexported / internal symbols

Write with the **maintainer in mind**: the reader must grasp intent and usage well enough to modify it
safely.

- Same principles: what it does, how it's used — not how it works internally.
- Less critical than exported symbols; be more concise.

### 3. Inline technical comments

Only add inline comments when the logic is genuinely non-obvious. Ask: would a competent developer in
this language understand the line without one? If yes, skip it. Standard language constructs, external
library behavior, and restatements of the code are all noise — see "What to Avoid".

### 4. Interface vs. implementation

When a type has both an interface and a concrete implementation, **document the interface thoroughly** —
that is what users read. The implementation doc serves maintainers: keep it terse, or omit it when the
interface doc already covers everything. Never duplicate the same explanation across both.

### 5. Package-level documentation

When a package's purpose isn't self-evident, add a package-level doc comment explaining its
**architectural role**: where it sits in the codebase, what layer it belongs to, and how it relates to
adjacent packages. Avoid listing features — those change; the role usually doesn't.

Understand the package's place in the overall architecture before writing its doc. If you're uncertain
(is this a utility layer, a business-logic layer, something else?), **ask the developer** rather than
guessing. A wrong package doc is worse than none.

Each language has a canonical place for the package doc — always use it:

- **Go**: a dedicated `doc.go` file at the package root — see the Go conventions below.
- **Shell / other imperative files**: the top-level comment block in the entry file (after the shebang).

---

## Imperative Files (Scripts, Main Entrypoints)

For files that describe a sequence of actions (shell scripts, `main.go`, job runners, etc.):

1. **Top-level package comment**: explain what the file does as a whole.
   - In Go: a **package-level doc comment** placed before `package main` (or `package foo`), never a
     comment on `func main()` — Go tooling and doc generators read that as a function doc, not the
     package description.
   - In shell: place it after the shebang (`#!/...`) line.
   - Keep it brief — one to three sentences. For `cmd/` packages with multiple entry points
     (e.g., grpc vs. rest, dev vs. prod), always explain which role this particular command serves
     and cross-reference the others.

2. **Step comments**: when the file has distinct logical phases, add a short comment before each to
   orient the reader. If a phase hides a non-obvious sub-step (a multi-step dependency wiring chain,
   say), add one line on that sub-group's purpose, not its mechanics.

3. **Section separators**: use comment blocks (e.g., `# ---- Section Name ----`) to visually separate
   major logical steps when the file is long enough to benefit from it.

---

## Language-Specific Conventions

### Go

- Exported symbols: doc comment directly above the declaration, with the symbol name near the start —
  as the plain subject (`// Foo holds...`) or with a natural article (`// A Foo holds...`,
  `// The Foo interface...`). Readers scanning `go doc` output orient on it, so never omit the name:
  `// Holds...` and `// This holds...` are wrong.
- Package doc: library packages, always a dedicated `doc.go` at the package root — never inline in a
  source file, not the first file alphabetically, not a random one. Command packages (`package main`),
  directly before `package main` in `main.go` — the idiomatic location; a `doc.go` is unusual there.
- Errors: always document exported error variables (`ErrFoo`). Callers check them by identity, so say
  what condition causes the error to be returned rather than restating the name. Say whether the error
  is returned or only logged — the distinction matters to callers.
- Enums (typed string/int constants): the type doc says what the enum conceptually represents and how
  it's used; never describe the values there. Document each constant individually, explaining what that
  specific value means or selects.
- Interfaces: document the interface type and its methods. Implementations can reference the interface
  instead of duplicating doc.
- Cross-references: use `[Symbol]`, `[Type.Field]`, or `[package.Symbol]` godoc link syntax. Never link
  to an unexported symbol from outside its package, or to any symbol in an `internal` package the
  current package cannot import — neither resolves, and both signal inaccurate coupling. Describe the
  relationship in prose instead.
- Language: Go returns errors — never write "throws", "raises", or "panics" when the code just returns an error.

### Shell (bash/sh)

- Shebang first, then a top-level comment block explaining the script's purpose and usage.
- Use `# ---- Section ----` style separators for distinct phases.
- Comment variables when their purpose or expected format isn't obvious.

### YAML (config files)

- Add inline comments to explain non-obvious fields, valid values, or important constraints.
- Do not comment fields that are self-explanatory from the key name.

### TypeScript / Svelte

- JSDoc (`/** ... */`) for exported functions, types, and components.
- For Svelte components: briefly describe the component's role at the top if it's not obvious.

---

## Domain Concepts and Specialized Knowledge

Public APIs often work with domain concepts, serialization formats, or industry standards a competent
developer may not know. Explain the concept rather than assume familiarity, so a reader can use the API
without looking up external specs first.

### Where and how to introduce concepts

Introduce a concept **once**, at its first appearance in the public API — typically the package doc
or the first exported type that depends on it. Later uses take the term for granted.

An introduction answers: _what is it, and what does the caller do with it?_ One or two sentences are
usually enough. Name the standard (RFC, spec) that governs the format; the reader can look up the rest.

### Serialized and encoded values

When a `string`, `[]byte`, or other primitive field holds a structured or encoded value, document:

- What it contains semantically (e.g., "a signed JWT token")
- What format it uses (e.g., "compact JWT — base64url-encoded header.payload.signature")
- If non-obvious, how to produce or consume that value (e.g., "returned by the SignToken RPC")

`Token string` documented as `// The JWT token.` tells the caller nothing actionable;
`// Token is the signed compact JWT (base64url header.payload.signature) returned by Sign.` does.

### Generic type parameters

When a type parameter must satisfy a domain contract, state that contract in terms the caller can act
on, not just "must match X". Common patterns:

- Serialization contract: "C must be JSON-serializable and match the struct embedded at signing time"
- Protocol contract: "T must implement proto.Message"
- Schema contract: "Row must have fields matching the SQL columns selected by this query"

### Trust and security boundaries

When an API has security implications — what data leaves the process, which side holds secrets, what
operations happen locally vs. remotely — document them in the package doc or at the primary entry
point. Callers need this to reason about correctness and trust:

- "Signing is performed server-side; private keys never leave the service."
- "Verification is done locally using cached public keys — no network call per token."
- "The private key is generated and stored in memory only; it is never persisted."

---

## Document the code as it is, not the change that produced it

A comment describes the code in its current form — never the edit, migration, or plan that created it.
The reader has no access to what the code used to be, and references to that past rot the moment the
change merges.

The test: **if this were the first commit introducing the file — not a modification of something
already there — would you still write this comment?** If not, delete it or rewrite it in the
present tense. This rules out:

- **Change narrative**: "now that X is removed", "the new model", "predates the redesign", "was
  previously", "had to go", "dropped in the rewrite". State what the code does, not what it
  stopped doing.
- **Comparisons to deleted code**: "matches the old bash script", "the Go port of `foo.sh`", "same
  default the script used". The thing compared against is gone; the comparison points at nothing.
- **Pointers that send the reader outside the code**: "per spec §6.4", "see PLAN.md", "see #50", "the
  Stage-2 gate", "Gate-2 override". Transient ones rot when the branch merges; durable ones (an issue
  or epic link) persist but still force the reader out of the file to reconstruct the rationale, and a
  planning label like "Stage 2" goes meaningless once the work ships. Fold the rationale into the
  comment itself and write it as if the plan that motivated the code never existed.
- **Removal notes**: "placeholder removed — see above", or any comment whose only content is that
  something used to be here. Absence needs no monument.

Keep the rationale, drop the history. _"Uses `--no-deps` to avoid podman-compose's broken
`depends_on` wait"_ is durable; _"mirrors the lessons from the old CLI's `--no-deps` handling"_ carries
the same fact but only parses for someone who remembers the old CLI. The prior bar still applies:
comment only when the code doesn't supply the context — a durable comment that restates the code is
still noise.

A **placeholder** pending planned work is the one case where the future is worth naming — as a
self-contained `TODO`/`FIXME` on the provisional line, describing what is missing in its own terms
(_"TODO: always returns success; compute the real result"_), not a prose comment that only parses with
the roadmap open. The surrounding doc still describes what the code does _today_, cleanly, as if the
placeholder were the intended design.

---

## Sweeping comments across a codebase

A cleanup pass over existing comments — one repo or many — behaves differently from documenting new
code, and fails in its own ways.

**Scope the check to the tree, not to your diff.** Searching your own diff answers "did I change this
correctly?" It cannot answer "did I find everything?" The moment the bar tightens mid-sweep — a rule
added after reviewing the first pass — every comment the earlier pass deliberately left alone is
reclassified, and only a full re-scan surfaces them. Re-scan the whole tree whenever the rules change,
and measure the tree you are sweeping: a `grep -r` over a sibling checkout reports whatever
branch that checkout happens to be parked on, not your baseline.

**A pattern search locates candidates; reading classifies them.** Every mechanical scan is a proxy —
`would`, `rather than`, `, not ` — and the worst findings match none of them. Change narrative
("the defect lived on the read path", "fixed in v1.4.5"), aggregate blocks describing the elements
below them, and docs that contradict the code are all found by reading the block the grep dragged into
view, never by the grep itself.

**Expect to find comments that are wrong.** Trimming forces someone to read the code underneath, which
is why a verbosity sweep routinely turns up inverted parameter descriptions, references to helpers
that no longer exist, and doc blocks attached to the wrong function. Those are the valuable output.
Fix the comment, leave the code, and call them out separately from the prose changes — a reviewer
skims rewording and scrutinizes a corrected claim.

**Comments in a generating source must be regenerated.** Editing a `.proto` comment changes the
generated Go, and the `generated-go` job fails on the drift. Run the repo's generate script and commit
the output as its own `chore(gen)` commit — `git-conventions` forbids mixing types, and a `docs`
commit carrying regenerated files hides the reason they changed.

**A comment-only diff is not always a comment-only diff.** Removing a trailing comment lets gofmt
re-align a struct or const block, and removing the comment above a single-element `var (…)` group
makes gofumpt collapse it. Both are the formatter asserting itself rather than an edit to the code, and
both belong in the PR description — an unexplained non-comment hunk costs a reviewer more than it saves.

---

## Consistency and Quality

Wrong documentation is a liability. Every time you write or touch it:

- **Verify accuracy**: re-read the code and confirm the doc matches what it actually does. Watch
  initialization claims ("pre-fetches", "caches on creation") — check the constructor to see whether
  the behavior is eager or lazy.
- **Check consistency**: if a concept is described elsewhere (another file, the interface), use the
  same terminology.
- **Rephrase freely**: fix awkward, redundant, or misleading doc rather than preserving it out of
  caution. Rewrite entire sections if that's what it takes to make them clear.
- **Respect scope**: asked to document one function, don't refactor the whole file. Document what was
  asked, fix obvious inconsistencies nearby, and stop there.

---

## What to Avoid

- Redundant preambles: `// This function...` — the symbol name already acts as the subject. Same for
  field docs: `// Tell the service to X` when the field already implies a configuration value — state
  the behavior directly.
- Doc comments that omit the symbol name entirely — see the Go conventions above.
- Sentence fragments in doc comments — every top-level doc (type, func, var) should be a complete
  sentence. Field docs may be shorter, but avoid opening with a bare adjective like "Optional ..." or
  "Required ..."; a noun phrase ("Validation overrides to apply") is clearer and still concise.
- Explaining how the language works (readers know Go, shell, etc.).
- Paraphrasing external APIs: the reader can look those up.
- Documenting obvious setters/getters unless there's a real invariant to explain.
- Noise comments that just make the file longer without adding information.
- Copying the same description from an interface down to the struct method — reference or omit instead.
- **Tying a reusable component's docs to one caller's wiring**: an action input, exported function, or
  library parameter documents the _contract_ — the capability it needs — not how one consumer supplies
  it. _Bad_: _"the AGENT_BOT_CLIENT_ID secret"_. _Good_: _"a GitHub App with write access to Checks"_.
  Naming a specific secret or env var couples the general component to one deployment and misleads the
  next caller.
- **Enumerating anything in a list** — fields, parameters, behaviors, checks, steps, etc. Lists go stale
  as soon as an item changes, and they push implementation details into docs that should describe intent.
  _Bad_: _"It validates signature, expiry, issuer, audience, and subject against the config."_
  _Good_: _"It validates all token claims against the configuration registered for the given usage."_
  When an example helps, give one representative item, not a complete inventory.
- **Section-level enumerations**: describing elements in a block header above them — document each
  element directly, as stated at the top of this skill.
- **British English spelling**: use American English throughout ("initialize" not "initialise",
  "behavior" not "behaviour").
- **Incorrect acronym casing**: write acronyms as the language ecosystem conventionally styles them
  (`JSON` not `Json`, `DAO` not `dao`, `gRPC` not `grpc`, `URL` not `url`).
- **Referencing inaccessible symbols in cross-package links** — see the Go conventions above.
- **All-caps emphasis words**: avoid `MUST`, `SHOULD`, `OPTIONAL`, `NOT`, etc. in prose documentation.
  These RFC-style terms feel out of place in code docs; plain prose ("must", "only", "nil for...")
  reads better.
- **Contrastive framing where the positive alone would do**: `// Stored, not derived`,
  `// We use X rather than Y`, `// This is not a cache`. State the choice instead:
  `// Stored at publish time.` — see "Write the choice, not the rejected alternative" above.
- **Counterfactuals defending the implementation**: `// A fixed counter would repeat them`,
  `// a nested transaction would inherit the caller's isolation`. State the property that holds — see
  "A counterfactual is that same defect in other grammar" above, including the three cases where a
  conditional stays.
- **A rule trailing off another sentence**: `// …, so they must differ`. Give it its own sentence —
  see "Prefer affirmative sentences to clause chains" above.
- **Rhetorical labels standing in for a sentence**: `// Why this matters: ...`, `// Note: ...`,
  `// The reason: ...` — state the point plainly, as under "Write plain sentences, not labels" above.
- **Change narrative and references to deleted code or transient docs** — see "Document the code as
  it is, not the change that produced it" above.

---

## Process

1. **Read the file(s)** fully before writing anything.
2. **Understand the context**: for package-level doc, also look at adjacent packages (what layer is
   this? how does it relate to its neighbors?). If the architectural role is unclear, **ask the
   developer** before writing anything.
3. **Identify gaps**: missing doc on exported symbols, stale/incorrect comments, package doc absent.
4. **Check existing doc**: is it accurate? Consistent? Could it be said more clearly?
5. **Write or improve** following the priorities above.
6. **Re-read the result**: does every comment add value? Is it consistent with the rest of the file?

