/skills/fuz-stack/references/tsdoc-comments
  • docs
  • skills
  • fuz-stack
    • Async Patterns
    • Code Generation
    • Common Utilities
    • CSS Patterns
    • Dependency Injection
    • Documentation System
    • File Organization
    • mdz — Strict Markdown Dialect
    • Approved npm Dependencies
    • Path References in Documentation
    • Approved Rust Dependencies
    • Rust Patterns for the Fuz Ecosystem
    • Rust Performance Patterns
    • Rust Spine & Consumer Servers
    • Svelte 5 Patterns
    • Task Patterns
    • Testing Patterns
    • TSDoc Comment Style Guide
    • Twin Implementations (TS ↔ Rust)
    • Type Utilities
    • WASM Patterns for the Fuz Ecosystem
    • Zod Schemas
  • grimoire
  • tools
  • hash

TSDoc Comment Style Guide

JSDoc/TSDoc conventions for @fuzdev packages.

Overview

Doc comments flow through a three-stage pipeline:

  1. svelte-docinfo analysis — extracts JSDoc/TSDoc from the TypeScript AST into per-declaration metadata
  2. svelte-docinfo Vite plugin — exposes module/declaration metadata through the virtual:svelte-docinfo module at build/dev time
  3. mdz renders docs with auto-linking — backticked identifiers become clickable API-doc links

Write standard JSDoc with the tags below, wrap identifier references in backticks, and the system handles the rest.

Writing Good Documentation

Prioritize "why" over "what"

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 */

Conciseness — anti-patterns

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:

  • Filler @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.
  • Step-by-step narration of self-evident behavior — the function name + signature already tell the story.
  • Hedging filler — "simply", "just", "essentially", "basically", and "should never happen" almost always indicate filler. Cut the sentence or rewrite without the hedge.
  • Marketing "useful for" bullet lists that repeat the main description in different words.
// 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) */

Voice

@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.

Document workflows with numbered steps

/** * 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 algorithms and explain rationale

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. */

When to document

Focus on:

  • Public API surfaces — all exported functions consumers use
  • Complexity — where the "why" isn't obvious
  • Side effects — mutations, async operations, error conditions
  • Domain knowledge — business rules, algorithms

Skip:

  • Simple getters/setters with obvious behavior
  • Internal helpers with clear names
  • Code where TypeScript types provide sufficient documentation

CLAUDE.md is a map; TSDoc is the detail

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.

Tag Reference

Main description

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. */

@param

Format: @param name - description

  • Hyphen separator (per TSDoc spec)
  • Wrap type/identifier references in backticks
  • Must be in source parameter order
  • Capitalization and trailing periods follow normal English. Both fragment-style (@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.

@returns

Use @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.

@throws

Preferred: @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.

@example

Code 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>; }

Writing effective examples

Give the reader a clear mental model of how to use the API:

  • Show the most common use case first — additional @example tags for variants
  • Use // => or // → comments to show return values inline
  • For option objects, show the minimal required fields
  • For type narrowing helpers, show the pattern that makes the types useful
  • Constants and simple predicates don't need examples unless usage is non-obvious
  • Keep examples complete enough to understand without reading the implementation
// 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); * ``` */

@deprecated

Include 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. */

@see

Three 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. */

@since

Supported by the parser but not currently used (@since 1.5.0). Use when versioning matters.

@default

Documents 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.

When @mutates this is warranted on class methods

Stateful 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:

  • Cross-field invalidation — clearing one field also resets caches or derived state. Example: Logger.clear_colors_override resets the override AND invalidates four cached prefix strings.
  • Cross-resource side effects — the method registers/unregisters external listeners, file watchers, timers, or process handlers beyond mutating local state. Example: attach_error_handler sets #error_handler AND subscribes to process.uncaughtException.
  • Implicit tracking — the method name describes one action but the class also records it for lifecycle/cleanup. Example: ProcessRegistry.spawn is named after spawning, but also adds the child to this.processes for later despawn_all.
  • Surprising mutation on a query-shaped name — the method looks like a getter or pure query but mutates. Example: 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 */

@module

Marks 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>

Tag order

  1. Main description
  2. @param (in source parameter order)
  3. @returns
  4. @mutates
  5. @throws
  6. @example
  7. @deprecated
  8. @see
  9. @since
  10. @default
  11. @nodocs

@mutates goes after @returns (or after @param if no return).

Inter-linking with mdz

Backtick-wrapped identifiers auto-link to API docs. Unmatched references fall through to plain <code>.

Always link

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:

  • exported function names: "tsdoc_parse", "shuffle"
  • type and interface names: "ModuleJson", "SourceFileInfo"
  • class names: "Library", "Declaration"
  • module paths: "module_helpers.ts", "actions/composables.ts", "DocsLink.svelte" — see Module path format
  • tag names in prose: "@param", "@returns"
  • enum and constant names

Module path format

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).

Internal paths

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` */

Case sensitivity

References are case-sensitive. "library" will NOT match Library.

Documentation Patterns

Module-level documentation

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.

Functions

// 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>> => { // ... };

Classes

/** * 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> { // ... } }

Interfaces

/** * 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. }

Svelte components

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>

Type aliases

/** * 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';

Drift — Correctness Over Coverage

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
  • Signature changed — @param list no longer matches parameter order, or names refer to renamed arguments
  • Return shape widened — new fields on a returned type go undocumented on the function that produces them
  • Error semantics tightened — a thrown error class was replaced or a distinct error.data.reason was added, but @throws still names the old one
  • Cross-refs rotted — @see some_helper.ts points at a file that was moved, merged, or deleted