Doc comments are extracted by svelte-docinfo (TypeScript AST → per-declaration
metadata, exposed via virtual:svelte-docinfo) and rendered by mdz, which
autolinks backticked identifiers to API docs. Write standard JSDoc with the
tags below, backtick identifier references, and the system handles the rest.
Don't restate the function name. Explain why this exists, what problem it solves, what depends on it.
// Weak — restates the name and types
/** Creates a new session. */
// Strong — purpose and rationale
/**
* Predicts the next version by analyzing all changesets in a repo.
*
* Critical for dry-run mode accuracy — allows simulating publishes without
* actually running `gro publish` which consumes changesets.
*
* @returns predicted version and bump type, or null if no changesets
*/Name algorithms so readers can look them up ("Uses Kahn's algorithm with
alphabetical ordering within tiers for deterministic results") and give
rationale for non-obvious parameter choices. Multi-step workflows get a
numbered list (1. **Sort** — …), usually in a @module comment paired with a
@see cluster for pipeline stages.
A wrong or filler comment costs more than it adds. Four patterns recur in audits:
1. Helper-contract @throws at every callsite. Document the contract on
the helper, not on every caller that delegates to it.
// Weak — same internal invariant repeated on every create_* query
/** @throws Error if the INSERT does not return a row (failed `assert_row` invariant) */
// Weak — generic driver error true of every SQL call
/** @throws Error propagated from the underlying driver on syntax errors, ... */
// Strong — lives on assert_row itself
/** @throws Error if `row` is undefined */2. @mutates X - <verb that mirrors the function name>. The tag earns its
line only when it surfaces what would surprise a reader: specific
tables/columns, cross-table cascades, fire-and-forget effects, context keys
consumed downstream, counter or rate-limiter state.
// Weak — set_session_cookie already says it
/** @mutates `c` - writes the `Set-Cookie` header */
// Useful — names columns / cascades / side channels
/** @mutates `app_settings` row - sets `open_signup`, `updated_at`, `updated_by` */
/** @mutates `permit_offer` siblings - stamps `superseded_at` on every other pending offer for the tuple */
/** @mutates Hono context - sets REQUEST_CONTEXT_KEY, CREDENTIAL_TYPE_KEY, AUTH_API_TOKEN_ID_KEY */3. Duplicate sentence — a @returns and a prose sentence saying the same
thing. Pick one.
4. Filler. @param X - the X (drop it, but keep a qualifier: format,
constraint, edge case); step-by-step narration of self-evident behavior; hedges
("simply", "just", "essentially", "should never happen"); "useful for" bullet
lists that repeat the description.
// Weak — every line restates name + type
/**
* @param specs - route specs to register
* @param method - HTTP method
* @returns matching route spec, or `undefined`
*/
// Strong — only the line that adds a qualifier
/** @param path - request path (exact or with concrete param values) */@mutates and @throws are terse fragments —
@mutates <target> - <verb> <scope>. Backticks on every
table/column/symbol/constant name are house style. Multi-paragraph descriptions
are earned by security or invariant rationale (TOCTOU, fail-closed,
sibling-supersede, ordering, init order). A union type alias documents its
members as a bullet list of backticked literals with - description.
Non-obvious semantics — wire shape, invariants, ordering, failure modes — belong on the symbol's TSDoc, not in a downstream CLAUDE.md. CLAUDE.md entries are one-line pointers: symbol name plus a hook. CLAUDE.md prose drifts because it lives far from the code; TSDoc on the symbol is visible during the edit.
Complete sentences ending in a period; blank line between summary and detail.
@param@param name - description — hyphen separator, source parameter order,
identifiers backticked. Single-sentence descriptions are lowercase fragments
with no period (@param foo - the value to clamp); multi-sentence descriptions
are capitalized sentences with periods, continuation lines indented
(@param exclude_dev - If true, excludes dev dependencies to break cycles. then
* Publishing uses exclude_dev=true …). Acronyms and proper names (CSS, Zod,
Fisher-Yates) stay capitalized. A legacy sentence-style file may stay internally
consistent until touched.
@param options.field - description documents a sub-property. Matching is by
parameter name, so destructured params (fn({a, b}: T) — TS names it __0)
can't be documented; name the parameter if it needs docs.
@returns@returns (not @return); same capitalization rules as @param. For async
functions describe what the Promise resolves to, not the Promise.
@throws@throws ErrorType description — type as first word, even if just Error
(@throws TaskError if production cycles detected). svelte-docinfo always
reads the unbraced first word as the type, so a description-only
@throws if … records if as the type, and a lone @throws Error (no
description) records no type. {ErrorType} parses exactly but isn't house
style.
@exampleFenced code blocks (mdz renders examples as markdown); show the common case
first, additional @example tags for variants; // => or // → comments for
return values — a bare call with no visible output teaches nothing. Constants
and simple predicates don't need examples. Interface fields can carry inline
@example tags too.
/**
* @example
* ```typescript
* mdz_from_tsdoc('{@link SomeType}')
* // → '`SomeType`'
* ```
*/@deprecatedRarely used — the no-backwards-compatibility policy means deprecated code is deleted. When used, include the backticked replacement.
@see{@link url|text} for display text, bare URL when
self-explanatoryactions/action_rpc.ts for the JSON-RPC dispatcher ``); see
Module path format{@link}
(`` @see each_concurrent for the side-effect variant ``)@sinceParsed, not currently used.
@defaultDocuments defaults for interface fields and component props; place it on the field's own doc comment:
/**
* How the content is aligned in the viewport.
* @default 'center'
*/
align?: DialogAlign;@internalNot-stable public API (standard TSDoc semantics). A marker, not an exclusion —
extracted as internalMessage, the declaration stays fully documented so
consumers can badge or filter (fuz_ui's DeclarationDetail shows an
internal badge plus the message; nothing filters on it yet). Trailing prose is
kept: say who uses it or why it's internal. Use it for power-user-importable
internals that should stay documented (deep extractor modules, orchestration
seams); to remove a symbol from docs entirely, use @nodocs.
/** @internal Used by `analyzeCore` and the test harnesses — not stable API. */@nodocs (non-standard)Drops the declaration from analysis output and duplicate checking. Dominant
use: exported-but-internal plumbing forced by the no-barrels convention (mdz
is the heaviest user); also build-system internals (Gro Args/task, gen
exports) and flat-namespace collisions.
/** @nodocs */
export const Args = s.object({...});Never @nodocs a symbol external consumers import. Rename one side of the
collision instead (SKILL.md §Flat Namespace) — hiding the primary surface also
hides it from generated docs and tomes.
@mutates (non-standard)@mutates target - description. Everything before the first - is the
target — a parameter, a path (this.field), or a multi-word reference
(`` permit_offer siblings ``); backticks in the target are stripped by the
parser. Same capitalization as @param. Document mutations visible outside the
function; locals, closure state, and pull-based caches are out of scope. A
bare `` @mutates target `` with no description parses but is discouraged —
if the mutation needs no description, the tag adds little.
On class methods, stateful classes mutate by design; tagging every
add/remove/clear/set is noise. @mutates this[.field] - description
earns its line when the mutation isn't obvious from the name:
Logger.clear_colors_override also
invalidates four cached prefix stringsattach_error_handler also subscribes to
process.uncaughtExceptionProcessRegistry.spawn also records the child for
despawn_allLruMap.get reorders the recency listRanking when warranted: @mutates this.field - description >
@mutates this - description > bare > omit (correct when the name says it all).
/**
* Shuffles an array in place using the Fisher-Yates algorithm.
* @mutates array - randomly reorders elements in place
*/@moduleModule-level doc comment, tag at the end of the block; works in .ts and in a
.svelte component's <script>. Prioritize it for modules with design
rationale, pipeline stages, or cross-references; ## headings (## Design,
## Behavioral notes) for complex modules.
description → @param (source order) → @returns → @mutates → @throws →
@example → @deprecated → @see → @since → @default → @internal →
@nodocs
The parser discards these (svelte-docinfo emits misplaced_tag for the
overload and @module cases, but no consumer imports diagnostics today):
@example, @deprecated, @internal, @since, @see,
@throws, @mutates, @default, @nodocs) on a non-primary overload
signature — put them on the first overload signature, whose JSDoc documents
the symbol (@param/@returns are per-signature)@nodocs inside a @module comment — use the analyzer's exclude patterns
to skip a module@nodocs on a member (class, interface, type alias, enum) or a component
prop — still emitted, no warning; it applies to top-level declarations and
export statements@default on a top-level function (dropped silently) — it lands on
variables, members of interfaces/type aliases/classes (callable members
included), and component props@defaultValue / @defaultvalue / @return do take effect — they parse as
synonyms of @default / @returns — but aren't house style.
Backticked identifiers autolink; unmatched references fall through to plain
<code>. Autolinking applies in main descriptions, @param, @returns,
@example, and @see; @throws and @mutates render as plain text, so
backticks there display literally (still house style — consistency wins).
References are case-sensitive (`` library `` won't match Library).
Wrap every mention of an exported identifier, module filename, or type name
in backticks — functions, types, classes, module paths (module_helpers.ts,
actions/composables.ts, DocsLink.svelte), tag names in prose (@param),
enums and constants.
Module references must use the path Library.module_by_path indexes: the
src/lib/-relative path with the source extension. Anything else silently
falls through.
// GOOD
/** @see `actions/action_rpc.ts` for the JSON-RPC dispatcher */
// BAD — `./` prefix
/** … from `./action_rpc.js` … */
// BAD — `.js` runtime extension
/** @see `action_rpc.js` */
// BAD — bare filename of a nested module (breaks when the file is at actions/action_rpc.ts)
/** @see `action_rpc.ts` */
// BAD — redundant `src/lib/`
/** @see `src/lib/actions/action_rpc.ts` */Top-level files match by bare filename (tome.ts); nested files need the
sub-path. When in doubt, include the directory. The canonical format is
documented on Module.path in fuz_ui's module.svelte.ts.
Never reference outside the repo from TSDoc — the published API docs stand alone, so an out-of-repo path is a dead link (./path-references §4).
/word after whitespace autolinks as internal navigation — including HTTP
routes, which then break prerender. Backtick them:
/** BAD — mdz autolinks /login and breaks prerender: - POST /login */
/** GOOD — renders as <code>: - `POST /login` */Document props inline in the $props() type annotation. Obvious props with no
default need no comment; focus on behavior, constraints, and non-obvious
defaults.
<script lang="ts">
// fuz_ui Dialog.svelte (abridged)
const {
show = true,
dismissable = true,
onbeforeclose,
children,
...rest
}: Omit<SvelteHTMLElements['dialog'], 'children' | 'onclose'> & {
/**
* Whether the dialog is shown. When the `<dialog>` mounts it opens via
* `showModal()`; when it unmounts it closes.
* @default true
*/
show?: boolean;
/**
* Whether clicking outside the content closes the dialog. `Escape`
* closes it regardless of this.
* @default true
*/
dismissable?: boolean;
/**
* Called before a user-initiated close. Return `false` to veto.
*/
onbeforeclose?: () => boolean | void;
/** Rendered inside the overlay. Receives the `DialogContext` (e.g. `{close}`). */
children: Snippet<[dialog: DialogContext]>;
} = $props();
</script>A wrong doc comment is worse than a missing one — readers trust it and propagate the mistake. When refactoring a public API, re-read the TSDoc on every touched symbol. Common drift:
@throws vs return shape — declares @throws but the body returns
null/undefined on that path (or vice versa); highest-value because
callers branch on it@param order or names no longer matcherror.data.reason added, but @throws names the old one@see some_helper.ts points at a moved or deleted file