JSDoc/TSDoc conventions for @fuzdev packages.
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.
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
*/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).
@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, 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.
*/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.
*/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
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.
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
- 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.
*/@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
* 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>;
}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);
* ```
*/@deprecatedInclude 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.
*/@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.
/**
* 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. */@sinceSupported by the parser but not currently used. Use when versioning matters.
/**
* Generates a UUID v4.
* @since 1.5.0
*/@defaultDocuments 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.
@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, β¦) 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
*/@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>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).
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:
- 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 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.tsTop-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).
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`
*/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 β 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
*//**
* 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>> => {
// ...
};/**
* 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:
<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.
/**
* 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 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