// Add or revise source-level JSDoc for Shift APIs. Use this skill before writing or editing documentation comments for exported classes, methods, constructors, domain data structures, render frames, reactive state, or any API where caller intent, side effects, lifetime, ownership, or nullability are easy to misunderstand.
Add or revise source-level JSDoc for Shift APIs. Use this skill before writing or editing documentation comments for exported classes, methods, constructors, domain data structures, render frames, reactive state, or any API where caller intent, side effects, lifetime, ownership, or nullability are easy to misunderstand.
/jsdoc — Source API Contracts
Write JSDoc as the stable public contract a caller needs without reading the implementation.
JSDoc comments must sit immediately before the documented symbol and use /** ... */ so tooling can parse them. Follow the standard tag vocabulary from https://jsdoc.app/about-getting-started, adapted for TypeScript source.
Why this matters
VS Code hover renders the first line in bold and the rest as body. A one-sentence contract on line 1 is the single highest-leverage thing you can write.
TypeScript already encodes shape. JSDoc is for what types can't say: ownership, lifetime, mutation, side effects, side-channel reactivity, nullability semantics, performance class, call ordering.
Code review is the second reader. Reviewers should be able to judge a call site against the doc without opening the implementation.
Runbook
Identify the audience: caller, implementer, renderer, tool author, or maintainer.
State the stable contract in one short opening sentence.
Add details only for ownership, lifetime, reactivity, mutation, side effects, nullability, performance, or call ordering. Put long detail under @remarks.
Use tags for callable APIs:
Add @param for every public constructor, method, or function parameter, and make the text describe the parameter's role, constraint, ownership, or valid range.
Add @returns when a method returns a value, nullable result, created object, snapshot, or read-only view.
Add @throws {ErrorType} when … for every observable failure mode (custom error class, semantic Error).
Do not add @returns void; describe the side effect in prose instead.
Cross-reference siblings with @see {@link OtherSymbol} (one tag per related symbol).
Add @example only when the intended call flow, ordering, or output is not obvious from the signature.
Re-read the comment and delete implementation trivia, current call-site anecdotes, and unstable examples.
Hard Style Rules
One-line contract. First sentence is a verb phrase (Returns…, Applies…, Triggers…), ending with a period. No "This function…".
@remarks for the long explanation. If you need more than one sentence of context, demote it under @remarks so the hover summary stays clean.
@param name - description documents meaning, not type. For public callable APIs, include the tag and make it earn its place by describing role, constraint, ownership, valid range, or call-order semantics.
@returns documents meaning, never "void". Drop the tag entirely for void returns; describe the side effect in the summary instead. Use @returns to clarify ownership ("a fresh array; caller owns it"), nullability semantics ("null when the glyph has no contours, not when it's missing"), or that the result is a snapshot vs a live reactive view.
Document side effects, lifetime, and reactivity. TypeScript can't encode "runs after render", "mutates the Glyph signal", "JS-only — does not call NAPI", "transfers ownership of the buffer". That is exactly what JSDoc is for.
Stable terms over current implementation names. Document the concept, not today's wiring.
No warnings, no scolding. State the contract directly.
Do not document private helpers unless they encode a non-obvious invariant.
Do not name current callers ("used by FooManager") — rots fast.
Never use JSDoc as a TODO list. That belongs in commits, issues, or // TODO comments.
What To Document
Document where the type signature is silent. If the type fully encodes the contract, write nothing. Otherwise, prioritize these dimensions:
Effects — purity, mutation of arguments, mutation of shared state, I/O.
Ownership — who owns the return value, who may mutate it, aliasing with internal state.
Identity vs value — handles/refs/IDs that look like the loaded object but aren't; snapshots vs live views.
Nullability semantics — what null / undefined / empty actually means (absent, error, not-yet-loaded, end-of-stream).
Resolution semantics — strict vs fallback, find vs find-or-create, exact vs nearest.
Lifetime and ordering — preconditions, disposal, idempotence, what makes the result go stale.
Failure modes — which errors, under which conditions; whether failure is observable or swallowed.
In TypeScript files, omit JSDoc type annotations. Let TypeScript own the type; let JSDoc own meaning.
/**
* Snapshot of state required to redraw the scene layer.
*
* Building this frame establishes the reactive dependencies for the scene
* output. Drawing code consumes the frame as plain data.
*
* @paramdependencies - Values that invalidate or describe one scene redraw.
*/constructor(dependencies: SceneFrameDependencies) {}
For functions with multiple parameters, document each parameter by role:
/**
* Converts a screen-space pointer into editor coordinate spaces.
*
* @paramscreen - Pointer position in canvas pixels.
* @paramdrawOffset - Glyph-local offset applied by the current editor view.
* @returns Coordinates in screen, scene, and glyph-local space.
*/functionresolveCoordinates(screen: Point2D,
drawOffset: Point2D,
): Coordinates {}
When failure paths are observable, document them with @throws:
/**
* Loads a glyph by handle. Resolves once the source is hydrated.
*
* @paramhandle - identity returned by {@link glyphHandleForUnicode}.
* @returns the loaded glyph; never a {@link GlyphRef}.
* @throws {GlyphNotFoundError} when the handle does not resolve in the active font.
* @see {@link glyphHandleForUnicode}
*/asyncfunctionloadGlyph(handle: GlyphHandle): Promise<Glyph> {}
When deprecating, name the replacement:
/**
* @deprecated Use {@link draft.setPositions} — `bridge.setNodePositions` sends
* one NAPI call per point and causes ~450ms frames on dense glyphs.
*/functionsetNodePositions(updates: NodePositionUpdate[]): void {}
Examples — the rules
Examples must be runnable assertions, not decoration.
Always fenced and language-tagged with ```ts. VS Code highlights inside fences.
Self-contained. Include imports. The reader should be able to paste the snippet and have it compile.
Show expected output with a // Output: or // => comment when the value carries the lesson.
Short. 8–12 lines is the typical good length; 25 is the ceiling. If it doesn't fit, the example is the wrong shape.
One concept per @example. Multiple @example blocks are fine and better than one mega-block.
A good example for a Shift API:
/**
* Begins a JS-only edit of the active glyph. Pair with {@link GlyphDraft.finish}
* to persist, or {@link GlyphDraft.discard} to revert.
*
* @returns a draft scoped to the active glyph; `null` when no glyph is loaded.
*
* @example
* ```ts
* const draft = editor.createDraft();
* if (!draft) return;
*
* for (const update of dragFrame) {
* draft.setPositions(update); // JS-only; no NAPI
* }
*
* draft.finish("translate"); // syncs once, records undo
* ```
*/createDraft(): GlyphDraft | null {}
When TS inference is non-obvious, annotate the type position inline (Effect pattern):
Avoid examples that depend on hidden setup, test fixtures, or implicit globals.
Overloads
Per-overload JSDoc when parameter meanings differ. (This is the TS standard-library convention.) Copy the contract on each signature; do not put one block on the implementation signature.
Top-overload JSDoc only when the contract is identical and only the type shape differs. The implementation signature stays bare.
/**
* Resolves a glyph from its Unicode codepoint.
* @paramcodepoint - the Unicode scalar value.
*/functionglyphFor(codepoint: number): Glyph | null;
/**
* Resolves a glyph from its handle.
* @paramhandle - identity from a prior lookup; cheaper than codepoint resolution.
*/functionglyphFor(handle: GlyphHandle): Glyph | null;
functionglyphFor(arg: number | GlyphHandle): Glyph | null {
/* impl */
}
Anti-Patterns (bad → good)
@returns void
// ❌/** @returns void */clear(): void {}
// ✅ describe the side effect; drop @returns entirely/** Clears all queued render frames. Idempotent. */clear(): void {}
Re-stating the signature in prose
// ❌/**
* @paramglyph - the glyph
* @paramindex - the index
*/// ✅ document the constraint/**
* @paramglyph - must be loaded (not a {@link GlyphRef}).
* @paramindex - zero-based contour index; -1 selects the outer hull.
*/
Multi-paragraph summary
// ❌ VS Code hover becomes a wall of text/**
* This function is used to set positions. It is part of the GlyphDraft API and
* is used during drag operations. It does not call NAPI. It must be paired
* with either finish() or discard().
*/// ✅ one-line contract + @remarks/**
* Updates JS-side glyph positions; pair with {@link finish} or {@link discard}.
*
* @remarks
* JS-only — does not call NAPI. Use during drag hot path; call `finish()` once
* at gesture end to sync to Rust, or `discard()` to revert.
*/
Naming current callers
// ❌ rots fast/** Called by GlyphSidebar and TransformPanel. */// ✅ describe what it produces/** Returns the active glyph's tight bounds, accounting for sidebearings. */
Bare @deprecated
// ❌/** @deprecated */functionoldThing() {}
// ✅ name the replacement or the reason/** @deprecated Use {@link newThing} — removes the legacy 2D-only path. */functionoldThing() {}
@example for trivial calls
// ❌ noise/**
* @example
* ```ts
* const id = glyph.id;
* ```
*/getid(): string {}
// ✅ no @example; the name is the whole storygetid(): string {}