Last Updated: 2026-05-24
Status: Active Owning Core/Module: PF-75
Purpose
This standard defines how Encore OS enforces documentation comment quality and coverage for public TypeScript APIs. The goal is to improve maintainability and API clarity while enforcing durable standards for all in-scope exported APIs.Scope
In Scope (initial)
src/platform/**src/shared/**src/cores/**supabase/functions/**
Declaration types currently enforced for coverage
export function ...export class ...- exported
constinitialized with function expressions / arrow functions export const/functionwhose name matches/^use[A-Z]/(hooks, Phase 3)export const/functionwhose direct return is JSX (components, Phase 3)export interface ...(Phase 2)export type ...(Phase 2)
Initial exclusions
src/routes/**route/lazy-map filessrc/integrations/**generated integration files- test/spec/story files
- pure re-export barrels (
export { ... } from ...) - type-heavy mapping files:
**/types.ts**/types/**/*.ts**/index.types.ts
TSDoc Syntax Rule
As of Phase 1B.7 (v1.5.1),tsdoc/syntax is an ESLint error for
src/**/*.{ts,tsx}. Malformed TSDoc fails npm run lint:ci immediately, so
syntax debt can no longer accrue. The dedicated inventory command remains
available for ad-hoc audits:
Phase 1B.7: tsdoc/syntax promoted to error (shipped 2026-05-24)
tsdoc/syntax moved from off to error. The path to a clean promotion:
- Codemod (
scripts/codemods/fix-tsdoc-syntax.ts) — extended to escape{ } < >and stray@in doc-comment prose (it already handled}/>), plus escaping inside unfenced@examplebodies and a single-line/** @tag */exemption so real block tags are preserved. Every escape uses a(?<!\\)guard (idempotent). A repo-wide comment-only sweep (~700 files), combined with the commit-hook Biome reformat (which normalized whitespace-sensitive cases), cleared the bulk. fix-dotted-param— converted JSDoc@param parent.childdotted names into plain-text bullets under the parent@param.- Tag registration — 12 block/modifier tags registered in
tsdoc.json(@packageDocumentation,@experimental,@since,@updated,@owner,@route,@permission,@accessibility,@todo,@generated,@note,@size). - Residual cleanup — the long tail (unbalanced code spans, JSDoc
@param {type}, package refs like`@tanstack/react-table`) fixed by hand, comment-only.
tsdoc/syntax-specific count to 0 with no
eslint-disable. Phase 1B is complete (tag tsdoc-phase-1b-complete).
Measurement note: counttsdoc/syntaxin isolation via the JSON reporter filtered onruleId === "tsdoc/syntax". The default-formatter “N problems” line also includes every other configured rule (no-console,react-hooks/*, …) and will badly overstate the figure.
Coverage Audit Commands
Output reports
reports/audits/TSDOC_JSDOC_COVERAGE_REPORT.mdreports/audits/TSDOC_JSDOC_COVERAGE_REPORT.jsonreports/audits/TSDOC_JSDOC_COVERAGE_BASELINE.mdreports/audits/TSDOC_JSDOC_COVERAGE_BASELINE.json
Required Comment Quality
At minimum, public declarations should include:- A concise summary line that describes behavior,
@paramtags for non-obvious parameters,@returnswhen return semantics are non-trivial,@deprecatedwith migration guidance and sunset where applicable.
Quality Rubric
Per-declaration quality score (0–5)
Each in-scope declaration earns up to 5 points. Passing = ≥ 3. Below 3 = audit failure on changed files; backfill is tracked, not blocking.Filler detection
A doc-block fails the summary point when any of these are true:- body < 30 chars after trim
- body is the symbol name de-camel-cased with throwaway verbs (
Hook for X,Helper for X,Utility for X,Function that …) - body matches the denylist (
TODO,WIP,Documentation pending,Auto-generated, plain dashes)
isAdult) and simple getters whose name fully expresses behavior get a lower bar — summary ≥ 15 chars AND adds non-trivial info beyond the type signature earns the point.
Per-kind quality variant
- Function / class method / hook — full 5-point rubric.
- React component (functional, exported) — summary +
@spec+@example(mount snippet) +@paramfor props (one entry per prop OR a single block describing the props object) +@returnswaived. - Interface / type alias — summary +
@spec+ per-property@remarksfor non-obvious members, or@exampleshowing a populated value;@param/@returnswaived. - Exported const (data / config) — summary +
@spec+@exampleof intended consumption;@param/@returnswaived.
Tag Dictionary
Canonical, ESLint-enforced tag set. Required when applicable@spec <SPEC-ID>— owning spec; repeatable. Audit verifies the ID exists underspecs/.@param <name> <desc>— non-obvious params.@returns <desc>— non-trivial returns.@throws <Type> <when>— when the throw is part of the contract.
@example— code block; required for any export marked@publicand >0 params.@see <symbol|path|url>— related code, spec, integration doc.@deprecated <since> — <migration>— must include replacement or sunset.
@remarks— additional context.@internal— opts out of public-API quality enforcement (still counted toward presence; lower bar applies).@beta/@alpha— stability markers.@core <CORE>— explicit core override (otherwise inferred from path).
@summary(Storybook): configuretsdoc.jsonto recognize it as a known tag and confine it to*.stories.tsx. Clears the 853 + 848tsdoc-undefined-tagwarnings.- Inline
{@link X}with unescaped}and>in generics — fixed by a one-time codemod.
Phasing & Rollout
Phase 3: hooks + components (shipped 2026-05-23)
The audit parser now distinguishes:- Hooks — exported
functionorconstwhose name matches/^use[A-Z]/. Treated under the full 5-point rubric (identical to functions). - Components — exported
functionorconstwhose body directly returns JSX (JsxElement / JsxFragment / JsxSelfClosingElement, including conditional and parenthesized JSX). Treated under the component variant: summary +@spec+@example(mount snippet) + per-prop or props-object@param;@returnswaived.
- Hook name beats JSX return —
useFooreturning JSX is still a hook. - JSX return signals a component — regardless of name capitalization.
- HOCs (functions whose direct return is another function, not JSX)
remain
function. An opt-in@componentmodifier tag is deferred to a future phase.
docs/api/{core}.md) now emits ## Hooks and
## Components sections in spec §4.4 order, replacing the previous heuristic
(.tsx file + capitalized name) that conflated hooks and components into a
single bucket.
Phase 2: interfaces + types (shipped 2026-05-23)
The audit parser now recognizes two additional declaration kinds:- Interfaces —
export interface Foo { ... }. Scored under the interface/type variant of the rubric. - Type aliases —
export type Foo = .... Same variant as interfaces.
@param and @returns are
waived; summary + @spec + @example (or @see) carry the substitute point
for the missing call-signature checks. Per-property @remarks count as
@example for the “example/see” rubric point.
The AI-context surface (docs/api/{core}.md) now emits a ## Types & interfaces section as the first H2 (spec §4.4 ordering: Types & interfaces →
Hooks → Components → Functions & utilities → Classes). The section is
suppressed when a core has zero documented interfaces/types so we never leave
a dangling H2 behind.
Volume reality. Phase 2 added +2,744 declarations to the rubric
surface (2,182 interfaces + 562 type aliases), bringing the grand total from
11,528 to 14,272. This is smaller than the design spec’s projected +8,786
because the existing config.ts exclusions — **/types.ts, **/types/**,
**/index.types.ts — correctly keep interface-barrel modules out of scope.
Re-classifying those barrel modules as in-scope (only when they contain ≥ N
exported interfaces with non-trivial bodies) is a separate follow-up plan, not
a Phase 2 expansion. Hand-authored core-local types.ts files therefore remain
audit-invisible; backfill prioritizes the in-scope interfaces/types under
platform, hr, fa, edge-functions, ce, cl, fw, and pm (in
descending sub-3 count order).
Disambiguation rules (parser.ts header doc-block):
export interface Foo→kind: 'interface'regardless of declaration site (module body, namespace, ambient).export type Foo = ...→kind: 'type'. Type-only alias forms only;export type { Foo }re-exports remain barrels and are excluded.*/types.ts,*/types/**, and*/index.types.tsfiles remain excluded byconfig.ts(Phase 1A behavior, deliberately preserved for Phase 2).
AI-Context Surface
Three artifacts, all committed to git
No separate website. TypeDoc is not introduced; we reuse the audit parser so there is one source of truth.
MCP server (Phase B / Phase 4 Track A)
npm run mcp:serve starts a local HTTP MCP server
(scripts/mcp/api-surface-server.ts, default port 3399, registered in
.mcp.json as encore-api-surface) that exposes MCP tools over
docs/api/index.json:
For Cursor IDE usage, add the same endpoint in local .cursor/mcp.json (seed from .cursor/mcp.json.example).
lookup_symbol({ name, limit? })— resolve a symbol name/fragment to itsfile:line, core, spec links, and summary (exact match first, then case-insensitive substring).symbols_for_spec({ spec_id })— list the symbols implementing a spec ID.semantic_search({ query, top_k? })— optional natural-language symbol discovery when embeddings are configured.
index.json and hot-reloads after
npm run docs:api:generate. Query logic lives in the pure, unit-tested
scripts/mcp/index-store.ts. v1 is local/trusted use only (no auth, no
deployed instance).
Semantic search (Phase D / Phase 4 Track C)
semantic_search({ query, top_k? }) — a third tool on encore-api-surface —
does dense-vector search over the surface for paraphrased / “find the symbol
that does X” queries. The index is built by npm run docs:api:embeddings
(text-embedding-3-small via the Vercel AI Gateway, full-precision Float32
stored in Vercel Blob; inputs are PHI-pre-scanned before any upstream call) and
refreshed by the weekly Refresh API Embeddings workflow. Requires
AI_GATEWAY_API_KEY + BLOB_READ_WRITE_TOKEN (read from the environment only);
without them the build no-ops and semantic_search returns “unavailable” while
lookup_symbol/symbols_for_spec keep working. Pure cores
(scripts/mcp/embedding-store.ts k-NN, scripts/mcp/phi-scan.ts) are
unit-tested; all provider/Blob calls sit behind injected adapters so CI never
hits the network.
Per-symbol record (shared shape)
index.json, plus top-level reverse maps:
bySpec gives every implementer of a spec in O(1).
CI Policy
PR comment from the audit script summarizes deltas:
+2 decls improved (3→4, 2→5); 0 regressions.
Threshold Policy
Minimum thresholds (fordocs:comments:audit):
- Global minimum: 100%
- Area minimums:
- platform: 100%
- shared: 100%
- cores: 100%
- edge-functions: 100%
Current Coverage Status
As of the Phase 2 re-baseline (2026-05-23, after the parser addedinterface and type declaration kinds on top of the Phase 3 hook/component split):
- Files scanned: 9,187
- Declarations analyzed: 14,272
- Documented (presence): 11,111 / 14,272 (77.9%)
- Missing doc-block: 3,161
- Mean quality score (0–5): 1.83
Coverage by declaration kind (Phase 2 byKind rollup)
Grand total grew from 11,528 to 14,272 — Phase 2 adds 2,182 interfaces +
562 type aliases (+2,744 net) on top of the Phase 3 surface. The interface and
type kinds enter with low presence coverage (13.9% and 15.5%), pulling the
overall presence rate down from 93% to 77.9% and the mean score from 2.20 to
1.83. This is expected: every TSDoc-less interface contributes a fresh
score-0 record until backfill lands. The audit/quality gate continues to fire
only on changed files, so this baseline drop does not break CI; it is the new
floor for the Phase 2 backfill cadence to climb.
Score histogram (overall)
The “0” bucket grew from 807 to 3,161 because the newly in-scope interface and
type aliases overwhelmingly lack a leading TSDoc block. Backfill against the
sub-3 worklist (concentrated in
platform, hr, fa, edge-functions, ce,
cl, fw, pm — see the agent’s per-core priority table) will move these
records from 0 directly to 4 or 5 as the interface/type rubric variant awards
the @param/@returns checks as N/A.
Per-area means
Rubric histogram quirk
The rubric awards “N/A” automatically when a declaration has no parameters or when its return type is trivial. In practice this means that a declaration with a substantive summary jumps from 2 points (presence + summary) directly to 4 points (presence + summary + params N/A + returns N/A) without passing through 3. The only way to score 3 is to have a non-filler summary plus exactly one of (@spec, @param, @returns)
without the N/A backstop — rare in real code. Similarly, 5 requires a
non-trivial @spec linkage on top, which is rare until backfill lands.
This is a feature, not a bug: the rubric’s pass-threshold --min-score 3
filters in practice to “score 4 or 5,” which is the right pass/fail
boundary for the changed-files gate.
Prior coverage figure (now retired)
The previous standard’s “100% (8,201 / 8,201)” figure came from the legacy parser that only counted function/class/variable declarations and used a binaryhasDocumentationComment check. The new rubric pipeline
(a) captures the raw doc-block text via getDocCommentRaw, (b) finds
813 declarations the legacy parser missed or mis-counted. The 100%
figure is retired; the 92.9% figure is the new baseline.
References
scripts/audit/audit-doc-comment-coverage.tsspecs/pf/specs/PF-75-tsdoc-jsdoc-coverage-program.mddocs/superpowers/specs/2026-05-23-tsdoc-coverage-quality-ai-context-design.mdAGENTS.md(current: see docs/VERSIONS.md)constitution.md(current: see docs/VERSIONS.md)