agentskills.codes

This skill is the practical companion to the project's TSDoc rules. It shows what good TSDoc looks like in *this* codebase, points at real files to mine for patterns, and calls out common gaps so you can fix them when you touch a file.

Install

mkdir -p .claude/skills/tsdocs && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/17244" && unzip -o skill.zip -d .claude/skills/tsdocs && rm skill.zip

Installs to .claude/skills/tsdocs

Activation

This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.

This skill is the practical companion to the project's TSDoc rules. It shows what good TSDoc looks like in *this* codebase, points at real files to mine for patterns, and calls out common gaps so you can fix them when you touch a file.
235 chars · catalog description✓ has a “when” trigger

About this skill

TSDocs Skill

Overview

This skill is the practical companion to the project's TSDoc rules. It shows what good TSDoc looks like in this codebase, points at real files to mine for patterns, and calls out common gaps so you can fix them when you touch a file.

TSDoc vs JSDoc. TSDoc is a documentation comment standard maintained by Microsoft, designed specifically for TypeScript. JSDoc predates it and was built for JavaScript — its tags often double as type annotations (@param {string}), which is redundant when the language already has types. TSDoc drops type-in-comment annotations and standardizes the tag grammar so tools (API Extractor, VS Code, ESLint plugins) parse the same comments the same way. This project uses TSDoc so editor hovers, future generated docs, and lint rules all agree.

Why TSDoc matters here. It powers three concrete things:

  1. Editor hovers — every @param, @returns, @throws you write shows up when a teammate hovers over the call site in VS Code.
  2. Generated docs — if/when we publish API reference pages, TSDoc is what gets extracted.
  3. Reviewer attention — a missing @throws is the single most common reason a caller forgets to handle an error.

Authoritative reference

The canonical TSDoc rules for this repo live in .github/instructions/documentation.instructions.md — explicitly marked SINGLE SOURCE OF TRUTH. This skill defers to that file for any conflict. If something here disagrees with the instructions file, the instructions file wins and this skill should be updated.

In particular, the instructions file owns:

  • The proportionality rule (TSDoc lines ≤ function body lines).
  • The right-sizing table (≤10 LOC / 11–30 LOC / >30 LOC).
  • The canonical TSDoc Tags Reference (when to include @param, @returns, @throws, @example, @remarks, @see, @internal, @public, @deprecated).
  • Inline comment prefix conventions (// PERF:, // NOTE:, // CRITICAL:, // TODO:, // FIXME:).
  • The anti-pattern catalogue.

Read it first. This file is examples.

Anatomy of a good TSDoc comment in this repo

A clean, working example: requireUserContext() in src/lib/auth/context.ts:

/**
 * Like {@link getUserContext} but throws {@link UnauthorizedError} when the
 * request is unauthenticated. Use this inside API route handlers.
 */
export async function requireUserContext(): Promise<UserContext> {
  const ctx = await getUserContext();
  if (!ctx) throw new UnauthorizedError();
  return ctx;
}

What this gets right:

  • Proportional. Two lines of prose for a three-line function — the instructions file's hard rule (TSDoc lines ≤ body lines) is satisfied.
  • Intent, not mechanics. It says what calling this means for you ("throws when unauthenticated, use in API routes"), not "calls getUserContext and checks if null".
  • {@link} cross-refs to related symbols so hover docs are navigable.
  • No @param / @returns — there are no params, and the return type Promise<UserContext> is self-documenting. The instructions file explicitly allows skipping these when the signature speaks for itself.
  • No @throws placeholder. It mentions UnauthorizedError in prose via {@link} — for a two-line summary that's enough. (For richer surfaces, see withUserGuards below.)

A longer, fully-tagged example: withUserGuards() in src/lib/security/guard.ts:

/**
 * Apply auth + rate-limit + concurrent-cap + audit logging around `work`.
 *
 * @throws {@link RateLimitedError} when the user is over the rate limit.
 * @throws {@link TooManyConcurrentSessionsError} when the user is over the
 *   concurrent-session cap.
 * @throws {@link UnauthorizedError} when the request is unauthenticated.
 */
export async function withUserGuards<T>(
  opts: GuardOptions,
  work: (ctx: UserContext) => Promise<T>,
): Promise<T> {

This pattern — one-line summary + a @throws per failure mode + an @example block at the file or symbol level — is the right shape for any exported function that can throw. Note the file-level @example directly above the export shows the canonical call site for reviewers, without bloating each function's own comment.

Required tags by export kind

The canonical rules are in documentation.instructions.md § TSDoc Tags Reference. Quoting the operative line:

| @param | Always for all parameters | Parameter name is self-documenting AND ≤10 LOC function | | @returns | Non-void functions | Return type is obvious (e.g., getName(): string) | | @throws | Function can throw | Function never throws |

Practical translation by export kind:

Export kindSummary@param@returns@throws@example@deprecated
Exported functionRequiredYes, unless self-documenting & ≤10 LOCYes, unless return type is obviousOne per throwable errorFor non-obvious APIs / public surfaceOnly if phasing out (include migration path)
Exported classRequired (one summary on the class)On the constructor and each public methodOn each non-void methodPer throwable methodOn the class for canonical usageIf the class is being removed
Exported interfaceRequiredTSDoc each field (one-liner is fine)n/an/aIf the shape is non-obviousPer-field if individual fields are being removed
Exported type aliasSingle-line TSDoc is usually enoughn/an/an/aOnly for non-obvious unions/discriminantsRare
Exported constantSingle-line TSDoc explaining intent / unitsn/an/an/aIf usage is non-obviousRare

Defer to the instructions file's right-sizing table for how much prose. The table above only covers which tags — length is a separate axis.

Interface field documentation

The codebase already does this well. From src/lib/security/rate-limit.ts:

export interface RateLimitResult {
  /** Whether the request is allowed under the current window. */
  allowed: boolean;
  /** Milliseconds the caller should wait before retrying. Only present when blocked. */
  retryAfterMs?: number;
}

One-line TSDoc per field. The optional retryAfterMs mentions when it's present — that's the kind of intent-over-mechanics detail that's worth the line.

What NOT to document

Don't write TSDoc that:

  1. Restates the type. @param userId - The user id, a string. adds nothing the signature didn't already say. Write what role the value plays, not its type.
  2. Narrates the implementation. // Loop through buckets and filter expired entries is what the code itself shows. The instructions file's anti-pattern table calls this out explicitly.
  3. Lists obvious behaviour. getUserById(id: string): User | null does not need "Returns the user with the given id, or null if not found." That's the signature in prose.
  4. Is a placeholder. TODO: write docs is worse than nothing — it signals to readers "stop reading, this is unfinished." Either write it or omit the comment.
  5. Documents private helpers. Internal helpers (cleanupExpired in rate-limit.ts) don't need TSDoc — a brief inline // NOTE: is enough when intent isn't obvious. Mark testing-only exports with @internal (see __resetRateLimitState).

Examples of well-documented exports in this codebase

Three real exports worth mining when you write or review TSDoc. Each shows a different right-sized shape, from one-liner field docs through to fully-tagged thrower contracts. (These were rough spots earlier in the project's life; H5 brought them up to standard. They are now the benchmark, not the cautionary tale.)

1. src/lib/auth/token-store.tsTokenStore interface methods

Every method on the TokenStore contract documents its edge-case behaviour, because the contract is the Liskov boundary between the in-memory and Cosmos implementations. Notice how each method explains the observable corners — what "not found" looks like, whether the operation is idempotent — rather than restating the signature:

export interface TokenStore {
  /**
   * Look up the stored token for `userId`.
   *
   * @param userId - Stable GitHub numeric ID as a string.
   * @returns The {@link StoredToken}, or `null` when no record exists for
   *   the user **or** when the stored record has already expired
   *   (`expiresAt * 1000 <= now`). Callers must treat "not found" and
   *   "expired" identically — both mean "refresh / re-auth required".
   *   Never throws for missing users.
   */
  getToken(userId: string): Promise<StoredToken | null>;

  /**
   * Persist `token` for `userId`.
   *
   * @param userId - Stable GitHub numeric ID as a string.
   * @param token - The token payload to store. Overwrites (upserts) any
   *   previous record for the same `userId`; there is no separate update
   *   path.
   */
  setToken(userId: string, token: StoredToken): Promise<void>;

  /**
   * Remove the token record for `userId`.
   *
   * @param userId - Stable GitHub numeric ID as a string.
   * @remarks Idempotent: deleting a `userId` with no stored token is a
   *   successful no-op, not an error.
   */
  deleteToken(userId: string): Promise<void>;
}

What this gets right: every "what does the caller do with the return value?" question is answered at the contract level, not deferred to "see the implementation".

2. src/lib/security/rate-limit.tscheckRateLimit

This function is a good template for "function with both a return value and a side effect". The @returns tag describes both arms of the discriminated result, and the prose calls out the side effect explicitly so callers know that asking is also recording:

/**
 * Check whether a user may make anot

---

*Content truncated.*

Search skills

Search the agent skills registry