JSDoc/TSDoc conventions for @fuzdev packages.
Doc comments flow through a three-stage pipeline:
svelte-docinfo analysis — extracts JSDoc/TSDoc from the TypeScript AST
into per-declaration metadatasvelte-docinfo Vite plugin — exposes module/declaration metadata
through the virtual:svelte-docinfo module at build/dev timemdz renders docs with auto-linking — backticked identifiers become
clickable API-doc linksWrite standard JSDoc with the tags below, wrap identifier references in backticks, and the system handles the rest.
Don't restate the function name. Explain why this exists, what problem it solves, and its role in the system — what depends on it, what it enables.
// Weak — restates the function name and types
/** Creates a new session. */
export const create_session = (deps: QueryDeps, account_id: AccountId): Session => {/* ... */};
// Strong — explains 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
*/A wrong or filler comment costs more than it adds. Four patterns recur in real audits.
1. Helper-contract @throws at every callsite. When a function
delegates a failure to an internal helper or external engine, document
the contract on the helper — not on every caller.
// 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, constraint violations, or connection failures */
// Strong — contract lives on the helper
// (in assert_row.ts)
/** @throws Error if `row` is undefined */2. @mutates X - <verb that mirrors the function name>. A tag that
adds no scope beyond the name + description is filler. A @mutates earns its
line when it surfaces *what would surprise a reader*: specific tables/columns,
cross-table cascades, fire-and-forget effects, context keys consumed by
downstream middleware, counter or rate-limiter state.
// Weak — set_session_cookie already says it
/**
* Set the session cookie on a response.
* @mutates `c` - writes the `Set-Cookie` header
*/
// Useful — names columns / scopes / cross-table cascade / non-obvious side channel
/** @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 */
/** @mutates drift counters - bumps `audit_unknown_event_type_failures` on mismatch */3. Duplicate sentence — @returns + prose saying the same thing.
// Weak — two sentences, one fact
/**
* @returns cleanup function that deactivates and hides the sidebar
*
* The returned disposer hides and disables on cleanup.
*/Pick one phrasing.
4. Verbose prose / useless detail. Filler that pads without signal. Recurring shapes:
@param X - the X — description adds nothing beyond the
parameter name and type. Drop the line; the signature is enough. A
qualifier ("the X to <verb>", a format hint, an edge-case note) is
usually worth keeping.// Weak — every line restates the parameter name + type
/**
* @param specs - route specs to register
* @param method - HTTP method
* @param path - request path
* @returns matching route spec, or `undefined`
*/
// Strong — keep `@param`/`@returns` only when they add a qualifier
// beyond the signature (constraint, format, edge-case behavior)
/**
* @param path - request path (exact or with concrete param values)
*/@mutates and @throws are terse fragments — `@mutates <target> -
<verb> <scope>`, not full sentences. 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); long prose without that payoff is the pattern to flag.
/**
* Multi-repo publishing pipeline.
*
* Steps:
* 1. **Sort** — `compute_topological_order` determines publish order
* 2. **Changeset** — `predict_next_version` simulates version bumps
* 3. **Publish** — `publish_package` publishes and waits for propagation
* 4. **Update** — `update_dependents` bumps downstream version ranges
*
* @module
*/Name the algorithm so readers can look it up; note rationale for non-obvious parameter choices.
/**
* Computes topological sort order for dependency graph.
*
* Uses Kahn's algorithm with alphabetical ordering within tiers for
* deterministic results.
*
* @param exclude_dev - If true, excludes dev dependencies to break cycles.
* Publishing uses exclude_dev=true to handle circular dev deps.
*/Focus on:
Skip:
When a symbol has non-obvious semantics — wire shape, invariants, ordering
constraints, failure modes — the explanation belongs on the symbol's TSDoc
(or its return type's), not in downstream CLAUDE.md or architecture docs.
mdz renders TSDoc through the virtual:svelte-docinfo pipeline, so the detail
stays one hop from the code and moves when the code moves.
CLAUDE.md entries should read as one-line pointers: symbol name plus a short hook. Three sentences about what a function returns or how it interacts with sibling symbols belong in source TSDoc. The failure mode is drift: CLAUDE.md prose goes stale living far from the code it describes, while TSDoc on the same symbol stays current because it's visible during the edit.
Complete sentences ending in a period. Separate summary from details with a blank line:
/**
* Formats a person's name in display order.
*
* Combines first and last names, handling edge cases like hyphenated or
* compound surnames. See `format_person_parts` for splitting.
*/@paramFormat: @param name - description
@param foo - the value to clamp) and sentence-style (@param foo - The value to clamp.) are accepted; pick one per file. Acronyms (CSS, HTML, URL) and proper names (Zod, Fisher-Yates) stay capitalized regardless./**
* Parses a semantic version string.
* @param version_string - version to parse (format: "major.minor.patch")
* @param allow_prerelease - allow versions with prerelease suffixes like "1.0.0-alpha"
*/Multi-sentence descriptions read as sentences and wrap with continuation
indentation — see the exclude_dev example under
Name algorithms.
@returnsUse @returns (not @return). Same capitalization rules as @param.
/**
* Gets the current time.
* @returns the current `Date` in milliseconds since epoch
*/For async functions, describe what the Promise resolves to, not the Promise itself.
@throwsPreferred: @throws ErrorType description — error type as first word, description follows. Pick a class even if it's just Error.
/**
* @throws Error if task with given name doesn't exist
* @throws TaskError if production cycles detected
*/The bare form (@throws description) and curly-brace form (@throws {ErrorType} description) also parse but are not preferred.
@exampleCode must be in fenced code blocks for syntax highlighting — mdz renders
examples as markdown.
/**
* Convert raw TSDoc `@see` content to mdz format for rendering.
*
* @param content - raw `@see` tag content in TSDoc format
* @returns mdz-formatted string ready for `Mdz` component
*
* @example
* ```typescript
* mdz_from_tsdoc('{@link https://fuz.dev|API Docs}')
* // → '[API Docs](https://fuz.dev)'
*
* mdz_from_tsdoc('{@link SomeType}')
* // → '`SomeType`'
* ```
*/Interface fields can have inline @example tags:
export interface ModuleSourceOptions {
/**
* Source directory paths to include, relative to `project_root`.
*
* @example
* ```typescript
* ['src/lib'] // single source directory
* ```
* @example
* ```typescript
* ['src/lib', 'src/routes'] // multiple directories
* ```
*/
source_paths: Array<string>;
}Give the reader a clear mental model of how to use the API:
@example tags for variants// => or // → comments to show return values inline// Good — shows input and return value
/**
* @example
* ```ts
* get_component_name('components/Button.svelte') // => 'Button'
* ```
*/
// Good — shows the pattern that motivates the API
/**
* @example
* ```ts
* if (is_kind(declaration, 'function')) {
* declaration.parameters; // narrowed to FunctionDeclarationJson
* declaration.return_type; // accessible after narrowing
* }
* ```
*/
// Weak — doesn't show what the function does or returns
/**
* @example
* ```ts
* process_data(input);
* ```
*/@deprecatedInclude migration guidance with backtick-linked replacement. Rarely used — the "no backwards compatibility" policy means deprecated code is usually deleted.
/**
* Legacy way to process data.
* @deprecated Use `process_data_v2` instead for better performance.
*/@seeThree patterns:
External URLs — {@link} for display text, bare URL when self-explanatory:
/** @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Element/contextmenu_event} */
/** @see {@link https://tools.ietf.org/html/rfc5322|RFC 5322} */
/** @see https://github.com/colinhacks/zod#brand */Sibling modules — module path relative to src/lib/ for cross-references
within a package. See Module path format for the exact
shape.
// src/lib/actions/action_spec.ts — from fuz_app
/**
* Action spec types — the canonical source of truth for action contracts.
*
* Action specs define method, kind, auth, side effects, and input/output
* schemas. Bridge functions in `actions/action_bridge.ts` derive `RouteSpec`
* and `EventSpec` from them.
*
* @see `actions/action_rpc.ts` for the JSON-RPC dispatcher
* @see `actions/register_action_ws.ts` for the WebSocket dispatcher
*
* @module
*/Note the nested modules use the full lib-relative path
(actions/action_rpc.ts, not action_rpc.ts).
Identifiers — wrap in backticks (not {@link}):
/** @see `each_concurrent` for the side-effect variant that skips result collection */
/** @see `format_number` in `maths.ts` for the underlying implementation. */@sinceSupported by the parser but not currently used (@since 1.5.0). Use when
versioning matters.
@defaultDocuments default values for interface fields and component props — place it on the field's doc comment:
/**
* Index 0 is under 1 is under 2 — the topmost dialog is last in the array.
* @default 0
*/
index?: number;See Svelte components for a full $props() block.
@nodocs (non-standard)Excludes from docs generation and flat namespace validation. Implemented by
svelte-docinfo — a tagged declaration is dropped from the analysis output
and skipped by duplicate checking. Use for build-system internals (Gro
Args/task, generated gen exports) or to resolve flat-namespace
collisions.
/** @nodocs */
export const Args = z.object({...});
/** @nodocs */
export const task: Task<typeof Args> = {...};Never @nodocs a symbol that external consumers import and use directly.
If it's part of the public API, rename one side of the collision instead —
hiding the primary surface from the flat namespace also hides it from
generated docs and tomes, silently breaking downstream documentation.
See SKILL.md §Flat Namespace for which side to rename.
@mutates (non-standard)Documents mutations to parameters or external state. Supported by fuz_ui's
tsdoc_helpers.ts.
Preferred form: @mutates target - description. The description is
the value-add — it tells the reader *what* changes and, when non-obvious,
*why or when*. Without it the tag duplicates the function name and
signature.
A bare backtick form (`` @mutates target ``, no description) parses but
is discouraged: if the mutation needs no description, the tag adds little
too. When you write @mutates, make the description carry weight.
Same capitalization rules as @param. Document mutations visible outside
the function; internal locals, closure state, and pull-based lazy caches
that consumers don't observe are out of scope.
@mutates this is warranted on class methodsStateful classes mutate by design — that's the point. Tagging *every*
state-changing method (add, remove, clear, set, release,
acquire, …) is noise: the method name already names the mutation.
@mutates this[.field] - description earns its line on a class method
when the mutation isn't obvious from the method name. Recurring shapes:
Logger.clear_colors_override resets the
override AND invalidates four cached prefix strings.attach_error_handler sets
#error_handler AND subscribes to process.uncaughtException.ProcessRegistry.spawn is named after spawning, but also adds the
child to this.processes for later despawn_all.LruMap.get reorders the
recency list.A method whose name fully communicates the mutation (set foo,
clear_console_override, Counter.increment, LruMap.delete) does NOT
need the tag.
Ranking when the tag *is* warranted: `@mutates this.specific_field -
description (best, names the field) > @mutates this - description`
(generic but at least carries reasoning) > `` @mutates this `` (bare,
discouraged) > omit (correct when the name says it all).
/**
* Shuffles an array in place using the Fisher-Yates algorithm.
* @param array - the array to shuffle
* @mutates array - randomly reorders elements in place
*/
export function shuffle<T>(array: T[]): T[] {
// ...
}/**
* Apply named middleware specs to a Hono app.
*
* @param specs - middleware specs to apply
* @mutates app - registers each spec's middleware on the app
*/@moduleMarks a module-level doc comment. Place at end of comment block. Works in
.ts files and .svelte components.
<script lang="ts">
/**
* @see {@link https://www.w3.org/WAI/ARIA/apg/patterns/alert/}
*
* @module
*/
</script>@param (in source parameter order)@returns@mutates@throws@example@deprecated@see@since@default@nodocs@mutates goes after @returns (or after @param if no return).
Backtick-wrapped identifiers auto-link to API docs. Unmatched references
fall through to plain <code>.
Wrap every mention of an exported identifier, module filename, or type name in backticks.
/**
* Wraps `LibraryJson` with computed properties and provides the root
* of the API documentation hierarchy: `Library` → `Module` → `Declaration`.
*
* @see `module.svelte.ts` for `Module` class
* @see `declaration.svelte.ts` for `Declaration` class
*/What to wrap:
tsdoc_parse", "shuffle"ModuleJson", "SourceFileInfo"Library", "Declaration"module_helpers.ts", "actions/composables.ts",
"DocsLink.svelte" — see Module path format@param", "@returns"Module references must use the canonical path that Library.module_by_path
indexes — the src/lib/-relative path with the source extension. Anything
else falls through to plain <code> and the auto-link silently breaks.
// GOOD — lib-relative path with source extension
/** @see `actions/action_rpc.ts` for the JSON-RPC dispatcher */
/** Wraps `LibraryJson`. @see `module.svelte.ts` for the `Module` class */
// BAD — relative `./` prefix doesn't match canonical paths
/** Dispatch through `action_rpc` from `./action_rpc.js` here */
// BAD — `.js` runtime extension doesn't match the indexed `.ts` source path
/** @see `action_rpc.js` for the JSON-RPC dispatcher */
// BAD — bare filename of a nested module ambiguous and won't resolve
/** @see `action_rpc.ts` */ // breaks if the file is at actions/action_rpc.ts
// BAD — redundant `src/lib/` prefix; collapse to the bare lib-relative form
/** @see `src/lib/actions/action_rpc.ts` */ // should be `actions/action_rpc.ts`Top-level files (e.g., src/lib/tome.ts) match by bare filename
("tome.ts"). Nested files (e.g., src/lib/actions/action_rpc.ts)
require the full sub-path ("actions/action_rpc.ts"). When in doubt,
include the directory — the longer form always works.
Never reference outside the repo from TSDoc. Source comments render into
the published API docs, where the shipped package stands alone — a bare ../
path to another repo (or an absolute workspace path) becomes a dead link. Keep TSDoc
references repo-local. Attribute external inspiration in prose without a
navigable path, or link a full URL; a backticked literal stays an escape hatch
(see Path references §2). Cross-repo *code* references
use the import-specifier form (@scope/pkg/foo.ts), not a relative path.
The canonical format is documented on Module.path in module.svelte.ts
(fuz_ui).
Paths starting with / after whitespace auto-link as internal navigation.
Gotcha — API route lists: /word patterns auto-link, including HTTP
routes. Bare paths create broken links that fail SvelteKit prerender:
// BAD — mdz auto-links /login as internal route, breaks prerender
/**
* - POST /login
* - GET /session
*/
// GOOD — backtick-wrapped renders as <code>, not <a>
/**
* - `POST /login`
* - `GET /session`
*/References are case-sensitive. "library" will NOT match Library.
Prioritize @module for modules with design rationale, pipeline stages, or
cross-references.
Basic:
/**
* Module path and metadata helpers.
*
* Provides utilities for working with source module paths, file types,
* and import relationships in the package generation system.
*
* @module
*/Design sections with ## headings for complex modules:
/**
* TSDoc/JSDoc parsing helpers using the TypeScript Compiler API.
*
* ## Design
*
* Pure extraction approach: extracts documentation as-is with minimal
* transformation, preserving source intent. Works around TypeScript
* Compiler API quirks where needed.
*
* ## Tag support
*
* Supports a subset of standard TSDoc tags:
* `@param`, `@returns`, `@throws`, `@example`, `@deprecated`, `@see`,
* `@since`, `@nodocs`.
*
* ## Behavioral notes
*
* Due to TS Compiler API limitations:
* - `@throws` tags have `{Type}` stripped by TS API; fallback regex
* extracts first word as error type
* - TS API strips URL protocols from `@see` tag text; we use
* `getText()` to preserve original format
*
* @module
*/Pipeline stages — combine the numbered-steps form
(Document workflows) with a @see
cluster in a single @module comment.
// src/lib/async.ts — from fuz_util
/**
* Maps over items with controlled concurrency, preserving input order.
*
* @param concurrency - maximum number of concurrent operations
* @param signal - optional `AbortSignal` to cancel processing
* @returns array of results in same order as input
* @throws Error if `concurrency < 1`
*
* @example
* ```ts
* const results = await map_concurrent(
* file_paths,
* 5, // max 5 concurrent reads
* async (path) => readFile(path, 'utf8'),
* );
* ```
*/
export const map_concurrent = async <T, R>(
items: Iterable<T>,
concurrency: number,
fn: (item: T, index: number) => Promise<R> | R,
signal?: AbortSignal,
): Promise<Array<R>> => {
// ...
};/**
* Rich runtime representation of a library.
*
* Wraps `LibraryJson` with computed properties and provides the root
* of the API documentation hierarchy: `Library` → `Module` → `Declaration`.
*
* @see `module.svelte.ts` for `Module` class
* @see `declaration.svelte.ts` for `Declaration` class
*/
export class Library {
/**
* URL path prefix for multi-package documentation sites.
* Prepended to `/docs/api/` paths in `Module.url_api` and
* `Declaration.url_api`. Default `''` preserves single-package behavior.
*/
readonly url_prefix: string;
/**
* All modules as rich `Module` instances.
*/
modules = $derived(/* ... */);
/**
* Search declarations by query string with multi-term AND logic.
*/
search_declarations(query: string): Array<Declaration> {
// ...
}
}/**
* File information for source analysis.
*
* Can be constructed from Gro's `Disknode` or from plain file system access.
* This abstraction enables non-Gro usage while keeping Gro support via adapter.
*
* Note: `content` is required to keep analysis functions pure (no hidden I/O).
*/
export interface SourceFileInfo {
/** Absolute path to the file. */
id: string;
/** File content (required - analysis functions don't read from disk). */
content: string;
/**
* Pre-resolved absolute file paths of modules this file imports (opt-in).
* When supplied, the session treats this as authoritative and skips its
* own lex+resolve pass. Only include resolved local imports — node_modules
* paths are filtered out at storage time by `isSource` either way.
*/
dependencies?: ReadonlyArray<string>;
// Reverse edges (`dependents`) are not a caller input — computed
// internally by `computeDependents` from forward edges of the owned set.
}Document props inline in the $props() type annotation. For obvious props
with no default, a comment is optional — focus on behavior, constraints,
and non-obvious defaults.
<script lang="ts">
const {
container,
layout = 'centered',
index = 0,
active = true,
content_selector = '.pane',
onclose,
children,
}: {
container?: HTMLElement;
/**
* @default 'centered'
*/
layout?: DialogLayout;
/**
* Index 0 is under 1 is under 2 — the topmost dialog
* is last in the array.
* @default 0
*/
index?: number;
/**
* @default true
*/
active?: boolean;
/**
* If provided, prevents clicks that would close the dialog
* from bubbling past any elements matching this selector.
* @default '.pane'
*/
content_selector?: string | null;
onclose?: () => void;
children: Snippet<[close: (e?: Event) => void]>;
} = $props();
</script>/**
* Analyzer type for source files.
*
* - `'typescript'` - TypeScript/JS files analyzed via TypeScript Compiler API
* - `'svelte'` - Svelte components analyzed via svelte2tsx + TypeScript Compiler API
*/
export type AnalyzerType = 'typescript' | 'svelte';A wrong doc comment is worse than a missing one: it looks authoritative, so readers trust it and propagate the mistake. Coverage (presence) is the easy axis; correctness (currency) is the failure mode that actually matters. When refactoring a public API — changing signatures, adding fields to return types, tightening error semantics, or renaming constants — re-read the TSDoc on every touched symbol before shipping.
Common drift patterns to watch for:
@throws vs return shape — function declares @throws but the body
returns null/undefined on the same failure path (or vice versa). The
highest-value contradiction because callers branch on it@param list no longer matches parameter order, or
names refer to renamed argumentserror.data.reason was added, but @throws still names the old one@see some_helper.ts points at a file that was
moved, merged, or deleted