Add JSDoc completeness gate for the cordis surface

gen-cordis-catalog now hard-errors (aggregated, not fail-fast) when an
event lacks description prose or a payload @param, or a public service
method lacks JSDoc, a @param per parameter, a @returns on a non-void
result, or an explicit return type annotation. The this receiver and the
trailing waterfall next are exempt on events (mode machinery owned by
@mode); a stale @param naming no real parameter errors, mirroring the
@mode contradiction check. parseJsDoc now ends prose at the first block
tag (standard JSDoc semantics), so the tags never change the rendered
catalog — only Source: line pointers moved.

Fills the ~139 gaps found across the 15 surface files, extends the spec
with negative-path fixtures for every new guard plus the exemptions,
records the decision as an implemented process RFC, and extends the
AGENTS.md typed-events bullet with the authoring rule. Runs inside
verify-cordis-catalog -> doc-sync, so CI and pre-push enforce it with
zero new wiring.
This commit is contained in:
Tianyi Cui
2026-07-04 19:06:35 +08:00
parent 734ffcc8a2
commit a29bbe1453
19 changed files with 593 additions and 73 deletions

View File

@@ -25,7 +25,7 @@ An agent was registered in the AgentRegistry and is ready to receive messages.
Types: [Agent](../core-data-structures/core.md)
Source: [`packages/core/agent/src/types.ts:233`](../../packages/core/agent/src/types.ts)
Source: [`packages/core/agent/src/types.ts:234`](../../packages/core/agent/src/types.ts)
#### `agent/disposed` — emit
@@ -37,7 +37,7 @@ An agent was disposed and removed from the registry; its fiber and any in-flight
Types: [Agent](../core-data-structures/core.md)
Source: [`packages/core/agent/src/types.ts:239`](../../packages/core/agent/src/types.ts)
Source: [`packages/core/agent/src/types.ts:241`](../../packages/core/agent/src/types.ts)
#### `agent/error` — emit
@@ -49,7 +49,7 @@ A step or turn errored. The loop reports a failure here (plus the logger) even w
Types: [Agent](../core-data-structures/core.md)
Source: [`packages/core/agent/src/types.ts:353`](../../packages/core/agent/src/types.ts)
Source: [`packages/core/agent/src/types.ts:389`](../../packages/core/agent/src/types.ts)
#### `agent/pre-step` — serial
@@ -63,7 +63,7 @@ Serial (awaited in registration order), not a waterfall: a listener mutates the
Types: [Agent](../core-data-structures/core.md)
Source: [`packages/core/agent/src/types.ts:305`](../../packages/core/agent/src/types.ts)
Source: [`packages/core/agent/src/types.ts:319`](../../packages/core/agent/src/types.ts)
#### `agent/prompt-submit` — waterfall
@@ -75,7 +75,7 @@ Waterfall: decide what happens to ONE drained queued message before it becomes a
Types: [Agent](../core-data-structures/core.md) · [ContentBlock](../core-data-structures/core.md) · [MessageSource](../core-data-structures/core.md)
Source: [`packages/core/agent/src/types.ts:315`](../../packages/core/agent/src/types.ts)
Source: [`packages/core/agent/src/types.ts:332`](../../packages/core/agent/src/types.ts)
#### `agent/queued` — emit
@@ -87,7 +87,7 @@ A message entered the agent's inbox (queued or steering). `source` is the resolv
Types: [Agent](../core-data-structures/core.md) · [ContentBlock](../core-data-structures/core.md) · [MessageSource](../core-data-structures/core.md)
Source: [`packages/core/agent/src/types.ts:252`](../../packages/core/agent/src/types.ts)
Source: [`packages/core/agent/src/types.ts:259`](../../packages/core/agent/src/types.ts)
#### `agent/request` — waterfall
@@ -99,7 +99,7 @@ Waterfall: mutate the fully-assembled GenerateOptions before the model call (hoo
Types: [Agent](../core-data-structures/core.md) · [GenerateOptions](../core-data-structures/core.md)
Source: [`packages/core/agent/src/types.ts:324`](../../packages/core/agent/src/types.ts)
Source: [`packages/core/agent/src/types.ts:345`](../../packages/core/agent/src/types.ts)
#### `agent/session-start` — emit
@@ -111,7 +111,7 @@ The agent's session lifecycle began, fired once before its first turn. `source`
Types: [Agent](../core-data-structures/core.md)
Source: [`packages/core/agent/src/types.ts:265`](../../packages/core/agent/src/types.ts)
Source: [`packages/core/agent/src/types.ts:274`](../../packages/core/agent/src/types.ts)
#### `agent/status` — emit
@@ -123,7 +123,7 @@ Agent status changed (`idle` ⇄ `running`, or → `disposed`). Drive lifecycle
Types: [Agent](../core-data-structures/core.md)
Source: [`packages/core/agent/src/types.ts:246`](../../packages/core/agent/src/types.ts)
Source: [`packages/core/agent/src/types.ts:250`](../../packages/core/agent/src/types.ts)
#### `agent/steering` — emit
@@ -135,7 +135,7 @@ Steering content was injected into a running turn.
Types: [Agent](../core-data-structures/core.md) · [ContentBlock](../core-data-structures/core.md) · [MessageSource](../core-data-structures/core.md)
Source: [`packages/core/agent/src/types.ts:347`](../../packages/core/agent/src/types.ts)
Source: [`packages/core/agent/src/types.ts:379`](../../packages/core/agent/src/types.ts)
#### `agent/step-result` — waterfall
@@ -147,7 +147,7 @@ Waterfall: post-process the assembled assistant Message before tool dispatch (va
Types: [Agent](../core-data-structures/core.md) · [Message](../core-data-structures/core.md)
Source: [`packages/core/agent/src/types.ts:330`](../../packages/core/agent/src/types.ts)
Source: [`packages/core/agent/src/types.ts:355`](../../packages/core/agent/src/types.ts)
#### `agent/turn-continuation` — waterfall
@@ -159,7 +159,7 @@ Waterfall: override the turn-continuation decision via a typed ContinuationDecis
Types: [Agent](../core-data-structures/core.md)
Source: [`packages/core/agent/src/types.ts:340`](../../packages/core/agent/src/types.ts)
Source: [`packages/core/agent/src/types.ts:368`](../../packages/core/agent/src/types.ts)
### `fs/*`
@@ -173,7 +173,7 @@ Single-slot decision: produce the optional version guard for the next FileSystem
Types: [FsTarget](../core-data-structures/filesystem.md) · [FsVersion](../core-data-structures/filesystem.md)
Source: [`packages/fs/fs/src/index.ts:119`](../../packages/fs/fs/src/index.ts)
Source: [`packages/fs/fs/src/index.ts:123`](../../packages/fs/fs/src/index.ts)
#### `fs/observed` — emit
@@ -185,7 +185,7 @@ Record that an actor observed a target at a version, after a successful read/wri
Types: [FsTarget](../core-data-structures/filesystem.md) · [FsVersion](../core-data-structures/filesystem.md)
Source: [`packages/fs/fs/src/index.ts:131`](../../packages/fs/fs/src/index.ts)
Source: [`packages/fs/fs/src/index.ts:138`](../../packages/fs/fs/src/index.ts)
#### `fs/write-intent` — waterfall
@@ -197,7 +197,7 @@ Single-slot decision: produce the write intent for the next FileSystem.writeText
Types: [FsTarget](../core-data-structures/filesystem.md) · [FsWriteIntent](../core-data-structures/filesystem.md)
Source: [`packages/fs/fs/src/index.ts:107`](../../packages/fs/fs/src/index.ts)
Source: [`packages/fs/fs/src/index.ts:109`](../../packages/fs/fs/src/index.ts)
### `llm/*`
@@ -211,7 +211,7 @@ Waterfall around every streaming model call (retry, caching, routing). Bound to
Types: [GenerateOptions](../core-data-structures/core.md) · [StreamChunk](../core-data-structures/llm-streaming.md)
Source: [`packages/llm/llm/src/index.ts:31`](../../packages/llm/llm/src/index.ts)
Source: [`packages/llm/llm/src/index.ts:32`](../../packages/llm/llm/src/index.ts)
### `session/*`
@@ -223,7 +223,7 @@ A session was created in the store.
'session/created'(session: Session): void
```
Source: [`packages/core/session/src/index.ts:35`](../../packages/core/session/src/index.ts)
Source: [`packages/core/session/src/index.ts:36`](../../packages/core/session/src/index.ts)
#### `session/event` — emit
@@ -235,7 +235,7 @@ An event was appended to a session log (sync, fire-and-forget). This is the per-
Types: [SessionEvent](../core-data-structures/core.md)
Source: [`packages/core/session/src/index.ts:41`](../../packages/core/session/src/index.ts)
Source: [`packages/core/session/src/index.ts:44`](../../packages/core/session/src/index.ts)
#### `session/flush` — parallel
@@ -245,7 +245,7 @@ Awaited durability checkpoint. The agent loop awaits `ctx.parallel('session/flus
'session/flush'(session: Session): Promise<void> | void
```
Source: [`packages/core/session/src/index.ts:50`](../../packages/core/session/src/index.ts)
Source: [`packages/core/session/src/index.ts:54`](../../packages/core/session/src/index.ts)
### `subagent/*`
@@ -257,7 +257,7 @@ A subagent run settled — emitted when SubagentRun.result resolves (any stop re
'subagent/end'(info: SubagentRunEndInfo): void
```
Source: [`packages/subagent/subagent/src/index.ts:75`](../../packages/subagent/subagent/src/index.ts)
Source: [`packages/subagent/subagent/src/index.ts:77`](../../packages/subagent/subagent/src/index.ts)
#### `subagent/start` — emit
@@ -267,7 +267,7 @@ A subagent run started — emitted after the provider is resolved and its capabi
'subagent/start'(info: SubagentRunInfo): void
```
Source: [`packages/subagent/subagent/src/index.ts:69`](../../packages/subagent/subagent/src/index.ts)
Source: [`packages/subagent/subagent/src/index.ts:70`](../../packages/subagent/subagent/src/index.ts)
### `system-prompt/*`
@@ -279,7 +279,7 @@ Waterfall around prompt assembly — mutate or extend the PromptAssembly (sectio
'system-prompt/assemble'(this: SystemPrompt, assembly: PromptAssembly, next: () => Promise<PromptAssembly>): Promise<PromptAssembly>
```
Source: [`packages/core/system-prompt/src/index.ts:24`](../../packages/core/system-prompt/src/index.ts)
Source: [`packages/core/system-prompt/src/index.ts:26`](../../packages/core/system-prompt/src/index.ts)
#### `system-prompt/change` — emit
@@ -289,7 +289,7 @@ A section or tool provider was registered or unregistered (the assembly inputs c
'system-prompt/change'(): void
```
Source: [`packages/core/system-prompt/src/index.ts:30`](../../packages/core/system-prompt/src/index.ts)
Source: [`packages/core/system-prompt/src/index.ts:32`](../../packages/core/system-prompt/src/index.ts)
### `tools/*`
@@ -301,7 +301,7 @@ A tool was registered or unregistered (the available tool set changed).
'tools/change'(): void
```
Source: [`packages/core/tools/src/index.ts:84`](../../packages/core/tools/src/index.ts)
Source: [`packages/core/tools/src/index.ts:87`](../../packages/core/tools/src/index.ts)
#### `tools/post-execute` — waterfall
@@ -313,7 +313,7 @@ Waterfall AFTER a tool runs — where hook plugins inspect the result and accept
Types: [ToolExecution](../core-data-structures/tools.md) · [ToolExecutionResult](../core-data-structures/tools.md)
Source: [`packages/core/tools/src/index.ts:79`](../../packages/core/tools/src/index.ts)
Source: [`packages/core/tools/src/index.ts:82`](../../packages/core/tools/src/index.ts)
#### `tools/pre-execute` — waterfall
@@ -325,7 +325,7 @@ Waterfall BEFORE a tool runs — the gate where sandbox, permission, and hook pl
Types: [ToolExecution](../core-data-structures/tools.md)
Source: [`packages/core/tools/src/index.ts:65`](../../packages/core/tools/src/index.ts)
Source: [`packages/core/tools/src/index.ts:66`](../../packages/core/tools/src/index.ts)
### `web/*`
@@ -444,7 +444,7 @@ abstract editText(target: FsTarget, edit: FsEditRequest, expected?: { version: F
Types: [FsEditOutcome](../core-data-structures/filesystem.md) · [FsEditRequest](../core-data-structures/filesystem.md) · [FsInfo](../core-data-structures/filesystem.md) · [FsTarget](../core-data-structures/filesystem.md) · [FsVersion](../core-data-structures/filesystem.md) · [FsWriteIntent](../core-data-structures/filesystem.md) · [FsWriteOutcome](../core-data-structures/filesystem.md)
Source: [`packages/fs/fs/src/index.ts:165`](../../packages/fs/fs/src/index.ts)
Source: [`packages/fs/fs/src/index.ts:172`](../../packages/fs/fs/src/index.ts)
### `ctx.llm` — `LlmService`
@@ -458,7 +458,7 @@ stream(options: GenerateOptions): AsyncIterable<StreamChunk>
Types: [GenerateOptions](../core-data-structures/core.md) · [StreamChunk](../core-data-structures/llm-streaming.md)
Source: [`packages/llm/llm/src/index.ts:69`](../../packages/llm/llm/src/index.ts)
Source: [`packages/llm/llm/src/index.ts:70`](../../packages/llm/llm/src/index.ts)
### `ctx.sessionPersistence` — `SessionPersistence` (abstract seam)
@@ -497,7 +497,7 @@ get(id: SessionId): Session | undefined
list(): Session[]
```
Source: [`packages/core/session/src/index.ts:323`](../../packages/core/session/src/index.ts)
Source: [`packages/core/session/src/index.ts:327`](../../packages/core/session/src/index.ts)
### `ctx.subagents` — `SubagentService`
@@ -510,7 +510,7 @@ list(): string[]
start(name: string, request: SubagentStartRequest): SubagentRun
```
Source: [`packages/subagent/subagent/src/index.ts:121`](../../packages/subagent/subagent/src/index.ts)
Source: [`packages/subagent/subagent/src/index.ts:123`](../../packages/subagent/subagent/src/index.ts)
### `ctx.systemPrompt` — `SystemPrompt`
@@ -522,7 +522,7 @@ tools(provider: () => ToolSchema[]): () => void
assemble(): Promise<PromptAssembly>
```
Source: [`packages/core/system-prompt/src/index.ts:71`](../../packages/core/system-prompt/src/index.ts)
Source: [`packages/core/system-prompt/src/index.ts:73`](../../packages/core/system-prompt/src/index.ts)
### `ctx.tools` — `ToolRegistry`
@@ -537,7 +537,7 @@ async execute(exec: ToolExecution): Promise<ToolExecutionResult>
Types: [ToolDefinition](../core-data-structures/tools.md) · [ToolExecution](../core-data-structures/tools.md) · [ToolExecutionResult](../core-data-structures/tools.md)
Source: [`packages/core/tools/src/index.ts:265`](../../packages/core/tools/src/index.ts)
Source: [`packages/core/tools/src/index.ts:268`](../../packages/core/tools/src/index.ts)
### `ctx.web` — `WebService`

View File

@@ -168,6 +168,7 @@ Do NOT write one for a mechanical or local choice (a variable name, a one-file r
| [Generated tool-schema catalog (boot-and-harvest)](implemented/process/2026-07-02-tool-schema-catalog.md) | 2026-07-02 |
| [Bilingual documentation via paired sibling files and a pairing gate](implemented/process/2026-07-02-bilingual-docs-and-pairing-gate.md) | 2026-07-02 |
| [Documentation tiers, budgets, and the ceiling gate](implemented/process/2026-07-04-doc-tiers-and-budgets.md) | 2026-07-04 |
| [JSDoc completeness gate for the cordis surface](implemented/process/2026-07-04-cordis-jsdoc-completeness-gate.md) | 2026-07-04 |
### Testing

View File

@@ -0,0 +1,33 @@
# RFC: JSDoc completeness gate for the cordis surface
Status: implemented (accepted 2026-07-04)
## Context
The [generated cordis catalog](2026-06-20-generated-cordis-catalog.md) already walks every harness `interface Events` member and every `ctx.<key>` service class with the TypeScript compiler API, and already hard-errors on a missing `@mode` tag — a forcing function that made dispatch modes impossible to leave undocumented. Nothing equivalent guarded the rest of the JSDoc: a service method could ship with no doc at all, and no event or method documented its parameters or return value individually. A survey at adoption found 5 public service methods with no JSDoc and roughly 139 missing `@param`/`@returns` entries across 15 files — on the product API spine (`ctx.bash`, `ctx.fs`, `ctx.sessions`, …) and the cross-plugin event payload contracts, exactly the surface where "what does this argument mean" is the question a plugin author asks the IDE.
The AGENTS.md rule ("every export has a JSDoc explaining semantics") is prose-checkable only by review; the repo's stated preference is to encode invariants in mechanical gates. The scope "cordis service functions and events" has a precise machine definition that only the catalog generator knows: events are the `interface Events` members inside `declare module 'cordis'`, and the service surface is the public methods of the class each `interface Context` key names. An ESLint rule cannot see that mapping; the generator computes it on every run.
## Decision
Extend `scripts/gen-cordis-catalog.ts` — the same walk, the same `@mode` precedent — to enforce JSDoc COMPLETENESS on everything it catalogs. `verify-cordis-catalog` runs inside `doc-sync`, which both CI and the lefthook pre-push hook already execute, so the gate needs zero new wiring (quality-gates principle: one source of truth).
The contract:
- **Events** need description prose plus a non-empty `@param` for every **payload parameter**. A payload parameter is a signature parameter that carries event data; the `this` receiver annotation and the trailing waterfall `next` are exempt — `next` is dispatch machinery whose semantics the `@mode waterfall` tag (and its structural cross-check) already owns, so restating it per event would be boilerplate. Documenting an exempt parameter anyway is allowed; only absence is checked.
- **Service classes** need class-level JSDoc, and every public method needs description prose, a non-empty `@param` per parameter, and a non-empty `@returns` unless the annotated return type is `void`/`Promise<void>` (where `@returns` stays optional — resolution timing can be worth documenting — but is never required).
- **Stale tags error**: an `@param` naming no real parameter is a violation, mirroring the `@mode`-contradicts-signature check. Tag descriptions must be non-empty; their semantic quality beyond that is review's job.
- **Explicitness the walk can check**: the gate is a pure-AST pass (no type checker), so a service method must annotate its return type (an inferred return cannot be classified) and surface parameters must be simple identifiers (a binding pattern has no name for `@param` to match).
- **Violations aggregate** into one error listing every offender — a remediation pass sees the whole list at once. The previously fail-fast `@mode` checks moved into the same aggregated report, with their message texts unchanged.
The tags are **enforcement-only**: `parseJsDoc` now ends description prose at the first block tag (standard JSDoc semantics, which also stops multi-line tag descriptions from leaking into the catalog as prose), so `@param`/`@returns` never change the rendered catalog. Rendering them — restructuring the services section into per-method entries — was considered and deliberately deferred: source JSDoc plus IDE hover is where method docs are consumed, and the catalog stays an index. No escape-hatch tag exists; the surface is small and curated (12 services, 57 methods, 27 events at adoption), and the point is that the check cannot be waved off.
Negative-path tests in `packages/core/agent/tests/gen-cordis-catalog.spec.ts` drive `collectEvents`/`collectServices` against synthetic fixtures to prove each guard fires and that the exemptions hold. The authoring rule lives in the root [AGENTS.md](../../../../AGENTS.md) conventions bullet alongside the `@mode` rule.
## Consequences
- A new event or service method cannot land with an undocumented parameter or result: the generator refuses to regenerate and `verify-cordis-catalog` fails pre-push and in CI. The ~139 gaps found at adoption were filled in the same change, so the gate landed green.
- The service surface must annotate return types explicitly and use identifier parameters. Neither constraint bound at adoption (every method already annotated; no destructured seam parameters existed); both are now load-bearing requirements a violating change will discover mechanically.
- The general AGENTS.md JSDoc rule ("one-liners when one line suffices") acquires a stricter carve-out on this surface: a one-line summary still suffices only when the method has no parameters and a void result.
- `@param` on `next` or `this` stays legal but unchecked — a deliberate asymmetry: the gate enforces the payload contract and refuses to demand boilerplate.
- The rendered catalog is unchanged by the tags (prose stops at the first block tag). If method-level rendering is wanted later, that is a catalog-design decision to take separately, not a gap in this gate.