Amended by 0128: the
documentation rules for classes and members are assumed there, pending the owner.
Amended 2026-10-21 (STA-215): the format is TSDoc, and members, tokens and the header are
required. See the amendment at the end of this page.
Context
Agents and new contributors read files cold. The cheapest durable defence against a wrong change is a header that says what the file is for and which rule governs it. Doc comments that restate a signature are noise and teach readers to skip comments.Decision
-
Every source file opens with a contract block before its imports:
Purpose:what the file is for.Boundary:what it declares and what it does not own.Runtime:one ofnode,browser,worker,react-native,isomorphic.Governing contract:the ADR or standard a change must respect.
.mjs) carry the same fields in their own comment syntax. The repository’s own config files already do this (# Stackra file contract:). -
Every exported symbol has TSDoc that says something the type does not: a purpose sentence always;
@paramand@returnswhen meaning, units or ownership are not in the type;@throwsfor each error a caller must handle;Side effects:for I/O and global mutation;Invariant:;@examplewhen use is not obvious;@deprecatedwith replacement and removal target. -
Types carry the weight first. No
any; generics get meaningful names; an exported type is its own documentation. - Boilerplate is a finding. A comment restating the signature fails review.
Consequences
- Reading cost per file drops; writing cost rises by a few lines.
/standards/code-styleis the operating form (header fields, tags, examples).
Enforcement
tools/scripts/doc-standard.mjs (test: tools/scripts/__tests__/doc-standard.test.mjs) runs in pnpm validate. It uses
the TypeScript compiler API to check the file contract header and the TSDoc presence rules of the amendment below, and it
rejects types in @param, @returns, @throws and @property. oxlint’s jsdoc/check-tag-names (in
packages/tooling/oxlint/src/base.jsonc) holds the tag names to TSDoc. oxlint 1.86 has no jsdoc/no-types, so the type
ban lives in the check. The check never judges wording: a block that restates the signature is a review finding.
Amendment 2026-10-21: TSDoc on members, tokens and the check
Context. The owner found exported interfaces, class methods and DI token files with no docblocks. Decision 2 asked for TSDoc on exported symbols only, and its check stayed planned, so nothing enforced it. The owner settled the format in STA-215. This amendment replaces decision 2 where the two differ. Decision.- The format is TSDoc, not JSDoc with types. Types never appear in a tag. Write
@param name - meaning, never@param {type} name; no typed@returns. TypeScript owns the types. - Required on:
- every exported symbol;
- every property and method signature of an exported interface or type literal;
- every public and protected member of an exported class (
privateand#members are internal); - every DI token constant made with
defineToken. The block says what resolves to the token and which package or app provides it.
- What the block says. A purpose sentence always.
@paramand@returnsonly where meaning, units or edge behaviour are not in the type (for example, “returns false and changes nothing on a repeat”).@throwsfor each typed error a caller must handle.@defaultValueon optional config properties.@exampleon public entry points.@remarksfor longer notes.@deprecatedwith the replacement and the removal target. - The header. Decision 1 applies to every
.tsand.tsxsource file inpackages/,services/andapps/, excluding tests, generated code and declaration files. ItsRuntime:field is one of the five values in decision 1.
Amendment 2026-10-21 (STA-219, L5): the frozen header and docblock fields
Context. The file-contract header and the TSDoc rules were in use, but their exact spelling was not frozen. Tools (tools/scripts/doc-standard.mjs, the scaffold templates, and the agents that write files) need one spelling to match.
Decision.
- The header prefix is frozen. The contract block is a comment whose first line is exactly
Stackra file contract:(the colon included, no other words on that line). In a.tsor.tsxfile it sits before the first import. In other files it uses that file’s comment syntax (a/* */block in.mjs,#in YAML and shell,//in JSONC). - The header field list is frozen, in this order:
Purpose:,Boundary:,Runtime:,Governing contract:. Each field starts its own line.Runtime:takes one ofnode,browser,worker,react-native,isomorphic. - The docblock field list is frozen. A TSDoc block uses these tags and no others for the cases of decision 2 of the
amendment above: a purpose sentence (no tag);
@param name - meaning;@returns;@throws;@defaultValue;@example;@remarks;@deprecatedwith the replacement and the removal target; and the two labelled linesSide effects:andInvariant:. A tag outside this list needs an ADR. - Typed tags are banned. No
{type}after@param,@returns,@throws,@propertyor@prop. TypeScript owns the type.
- Any header that uses another prefix, or another field name, fails the check: a spelling change is a new amendment.
- The scaffold templates write this header; a template that drifts is a finding for the code-standards steward.
tools/scripts/doc-standard.mjs (test tools/scripts/__tests__/doc-standard.test.mjs) checks the prefix,
the four header fields, the Runtime: value and the typed-tag ban. It runs in pnpm validate. The presence of the
docblock fields on members is checked by the same script under decision 2 of the 2026-10-21 amendment; the wording is
review, not a check.
Supersedes and sources
Supersedes: none. Amends: decision 2 of this page (by the amendment above). Freezes: the header and docblock field lists (STA-219). Sources: basaltadr/0011-documentation-and-source-comment-standard; figentra docs/constitution/documentation.md; owner ruling in STA-215.