πŸ“œ/skills/fuz-stack/references/tsdoc-comments
  • docs
  • skills
  • fuz-stack
    • Async Patterns
    • Code Generation
    • Common Utilities
    • CSS Patterns
    • Dependency Injection
    • Documentation System
    • Rust Patterns for the Fuz Ecosystem
    • Rust Performance Patterns
    • Svelte 5 Patterns
    • Task Patterns
    • Testing Patterns
    • TSDoc Comment Style Guide
    • 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. fuz_ui analysis β€” tsdoc_helpers.ts, ts_helpers.ts, svelte_helpers.ts extract JSDoc/TSDoc from the TypeScript AST into per-declaration metadata 2. Gro gen tasks β€” library.gen.ts emits library.json and library.ts with module and declaration metadata 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 and what problem it solves.

// 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 is worse than no comment. Four patterns recur in real audits.

1. Helper-contract @throws at every callsite. When your function delegates a failure to an internal helper or an 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 adding signal. Specific shapes that recur:

- Filler @param X - the X β€” when the 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 β€” when 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) */

Long prose without security, ordering, or invariant rationale is the shape to flag β€” multi-paragraph descriptions are *earned* (see Voice).

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

Explain system context

State the function's role in the larger system β€” what depends on it, what it enables.

/** * Waits for package version to propagate to NPM registry. * * Critical for multi-repo publishing: ensures published packages are available * before updating dependent packages. */

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 library.json 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. If you're writing three sentences about what a function returns or how it interacts with sibling symbols, that content belongs in source TSDoc. The failure mode is drift: CLAUDE.md prose goes stale because it lives far from the code it describes, while TSDoc on the same symbol often 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 and stay consistent within a 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:

/** * Computes topological sort order for dependency graph. * @param exclude_dev - If true, excludes dev dependencies to break cycles. * Publishing uses exclude_dev=true to handle circular dev deps. */

@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 * tsdoc_see_to_mdz('{@link https://fuz.dev|API Docs}') * // β†’ '[API Docs](https://fuz.dev)' * * tsdoc_see_to_mdz('{@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

Focus on giving 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 * } * ``` */ // Good β€” shows minimal setup and the typical workflow /** * @example * ```ts * const {modules, diagnostics} = await analyze_from_files({ * project_root: process.cwd(), * }); * if (diagnostics.has_errors()) { * for (const err of diagnostics.errors()) { * console.error(format_diagnostic(err)); * } * } * ``` */ // 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 β€” "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.

/** * Gro-specific library metadata generation. * * @see library_generate.ts for the generic generation entry point * @see library_pipeline.ts for pipeline helpers * @see library_output.ts for output file generation * * @module */

For nested modules, use the full lib-relative path:

/** @see `actions/composables.ts` for the action set to spread here */

Identifiers β€” wrap in backticks (not {@link}):

/** @see `tsdoc_parse` for the extraction step */ /** @see `format_number` in `maths.ts` for the underlying implementation. */

@since

Supported by the parser but not currently used. Use when versioning matters.

/** * Generates a UUID v4. * @since 1.5.0 */

@default

Documents default values for interface fields and component props:

const { layout = 'centered', index = 0, content_selector = '.pane', }: { /** * @default 'centered' */ layout?: DialogLayout; /** * Index 0 is under 1 is under 2 β€” the topmost dialog is last in the array. * @default 0 */ index?: number; /** * If provided, prevents clicks that would close the dialog * from bubbling past any elements matching this selector. * @default '.pane' */ content_selector?: string | null; } = $props();

@nodocs (non-standard)

Excludes from docs generation and flat namespace validation. Supported by fuz_ui's tsdoc_helpers.ts and svelte-docinfo. 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, which silently breaks 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 information the function name and signature already carry.

A bare backtick form (`` @mutates target ``, no description) parses but is discouraged: if the mutation is obvious enough that no description is warranted, the tag itself is also adding little. If 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, …) produces 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 in addition to 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 purposes. 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/composables.ts` for the action set to spread here */ /** Wraps `LibraryJson`. @see `module.svelte.ts` for the `Module` class */ // BAD β€” relative `./` prefix doesn't match canonical paths /** Spread `composable_actions` from `./composables.js` here */ // BAD β€” `.js` runtime extension doesn't match the indexed `.ts` source path /** @see `composables.js` for the bundled action set */ // BAD β€” bare filename of a nested module ambiguous and won't resolve /** @see `composables.ts` */ // breaks if the file is at actions/composables.ts

Top-level files (e.g., src/lib/Alert.ts) match by bare filename ("Alert.ts"). Nested files (e.g., src/lib/actions/composables.ts) require the full sub-path ("actions/composables.ts"). When in doubt, include the directory β€” the longer form always works.

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 get auto-linked, 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 β€” combines numbered steps with @see cross-references (see also the Document workflows with numbered steps pattern above):

/** * Library metadata generation pipeline. * * Pipeline stages: * 1. **Collection** β€” `library_collect_source_files` gathers and filters * 2. **Analysis** β€” `library_analyze_module` extracts metadata * 3. **Validation** β€” `library_find_duplicates` checks flat namespace * 4. **Transformation** β€” `library_merge_re_exports` resolves re-exports * 5. **Output** β€” `library_sort_modules` prepares deterministic output * * @see library_generate.ts for the main generation entry point * @see library_analysis.ts for module-level analysis * @see library_output.ts for output file generation * @see library_gen.ts for Gro-specific integration * * @module */

Design philosophy:

/** * mdz β€” minimal markdown dialect for Fuz documentation. * * ## Design philosophy * * - **False negatives over false positives**: When in doubt, treat as * plain text. * - **One way to do things**: Single unambiguous syntax per feature. * - **Explicit over implicit**: Clear delimiters avoid ambiguity. * - **Simple over complete**: Prefer simple parsing rules. * * @module */

Functions

/** * Find duplicate declaration names across modules. * * Returns a `Map` of declaration names to their full metadata * (only includes duplicates). Callers decide how to handle duplicates * (throw, warn, ignore). * * @example * const duplicates = library_find_duplicates(source_json); * if (duplicates.size > 0) { * for (const [name, occurrences] of duplicates) { * console.error(`"${name}" found in:`); * for (const {declaration, module} of occurrences) { * console.error(` - ${module}:${declaration.source_line}`); * } * } * } */ export const library_find_duplicates = ( source_json: SourceJson, ): Map<string, Array<DuplicateInfo>> => { // ... };

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:

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

For obvious props with no default, a comment is optional. Focus on behavior, constraints, and non-obvious defaults.

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