From 5e4ac5e4726086a9b042e34e37f8863b44af2f94 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Tue, 7 Jul 2026 20:30:34 +0800 Subject: [PATCH] fix review findings: CI leaf-gate wiring, heritage return surface, AGENTS.md self-containedness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - run-gates.ts docSyncLeafGates() gains verify-export-jsdoc — CI lanes and the pre-push hook execute this leaf list, not the doc-sync npm script, so the gate was previously unenforced there (proven by SessionForkErrorCode landing undocumented via a master merge while checks stayed green; now documented). Same wiring gap fixed for master's verify-config-catalog, which was also missing from the list. - The heritage exemption now recovers the base's return surface: a void base return carried no @returns duty, so an override returning a concrete result documents it itself (annotated overrides run the standard check; unannotated ones are classified by the checker so faithful void overrides need no boilerplate annotation). Three new negative-path tests pin it; RFC and module doc updated. - AGENTS.md states each principle inline instead of citing RFCs (eight citations removed; high-level doc links kept) and the editing section now carries the self-containedness rule. - Generated catalogs/graphs regenerated for the shifted line pointers. --- AGENTS.md | 18 ++-- docs/cordis-catalog/services.md | 2 +- .../2026-07-06-export-surface-jsdoc-gate.md | 2 +- .../agent/tests/verify-export-jsdoc.spec.ts | 65 +++++++++++++++ packages/core/session/src/index.ts | 8 ++ scripts/run-gates.ts | 2 + scripts/verify-export-jsdoc.ts | 82 ++++++++++++++----- 7 files changed, 148 insertions(+), 31 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c5c6fcd5c8..57d46556fd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -83,21 +83,21 @@ Real-API tests and demos read `DEEPSEEK_API_KEY` (and optional `DEEPSEEK_BASE_UR - Every npm package is `@deepseek-ai/dsh-`; vendored packages keep upstream names and are `private: true`. `cordis` is a peerDependency (+ dev) of every harness package. - ESM everywhere (`"type": "module"`). Cross-package imports use package names, never relative paths; in-package relative imports use explicit `.ts` extensions. Dev/test/demo run unbuilt via tsx + the root tsconfig `paths` map; builds are for outside consumers only. - **Registrations are effects**: every contribution goes through `ctx.effect()` / `ctx.on()`; a registry's `register()` returns the disposer. -- **Typed events via declaration merging**; extensible unions use the merge-extensible-map pattern (`ContentBlockMap`, `SessionEventMap`, …). Every new event's JSDoc carries an `@mode` tag and a `@param` per payload parameter (`this`/trailing `next` exempt); every public service-class method documents each parameter and non-void return (`@param`/`@returns`) — the catalog generator hard-errors otherwise ([completeness RFC](docs/rfc/implemented/process/2026-07-04-cordis-jsdoc-completeness-gate.md)); mode semantics are in the [generated events catalog](docs/cordis-catalog/events.md) header ([catalog RFC](docs/rfc/implemented/process/2026-06-20-generated-cordis-catalog.md)). +- **Typed events via declaration merging**; extensible unions use the merge-extensible-map pattern (`ContentBlockMap`, `SessionEventMap`, …). Every new event's JSDoc carries an `@mode` tag and a `@param` per payload parameter (`this`/trailing `next` exempt); every public service-class method documents each parameter and non-void return (`@param`/`@returns`) — the catalog generator hard-errors otherwise; mode semantics are in the [generated events catalog](docs/cordis-catalog/events.md) header. - **Discriminated unions: `switch` on the tag**, not if-chains. Closed unions end with `default: assertNever(...)`; merge-extensible unions must NOT — handle known cases and fall through `default` with a comment. - **Waterfall listeners MUST call `next()`** to delegate; returning without it is the veto ([semantics](docs/cordis-primer.md#cordis-waterfall-semantics)). -- **Model-visible ⟺ logged**: anything that reaches a model request must be reconstructable from the session log; a new model-visible input requires a session event ([reconstructability RFC](docs/rfc/implemented/architecture/2026-07-05-reconstructable-requests.md)). +- **Model-visible ⟺ logged**: anything that reaches a model request must be reconstructable from the session log; a new model-visible input requires a session event. - **Plugins, not loop changes**: new behavior goes on the documented extension seams; changing `agent-loop` requires updating docs/architecture.md. -- **Capability seams are three packages** — interface / implementation / consumer ([capability seams](docs/rfc/implemented/architecture/2026-06-13-capability-seams.md)); don't split preemptively. +- **Capability seams are three packages** — interface / implementation / consumer; don't split preemptively. - **Explicit > implicit at package seams**: defaulting is an explicit `resolve(request): Spec` step in the owning implementation, never a hidden `?? default` inside `run()` (the `dsh-bash` request/spec split is the template). - **No hardcoded tunables in plugins**: anything two deployments could want different — timeouts, caps, model names, base URLs — is a defaulted, validated `Config` field, not a literal; a `DEFAULT_*` constant or test-only seam is not configurability. The test: changeable from `cordis.yml`, no code edit. Protocol/wire constants, external-spec values, security invariants stay hardcoded. -- **Opaque cross-boundary ids are branded** (`Branded` from `dsh-brand`), never bare `string` ([branded IDs](docs/rfc/implemented/architecture/2026-06-20-branded-ids.md)). +- **Opaque cross-boundary ids are branded** (`Branded` from `dsh-brand`), never bare `string`. - **An empty `catch` names what it swallows** and why nothing else can reach it; keep the `try` to one statement. - **Symmetry is usually more correct**: parallel values get parallel form; asymmetry smells of a missed extraction. -- **Tests document behavior, not golden truth**: a green test pins what the code DOES, not what it SHOULD do. Before preserving a behavior solely for its test, ask whether it is load-bearing; an artifact changes together with its test, with the why in the PR ([worked example](docs/rfc/implemented/simplification/2026-06-19-drop-mutable-session-summary.md)). -- **RFCs are proposals, not golden truth**: validate its premise against current code before implementing; friction is evidence of over-reach — amend on the way to `implemented/` ([worked example](docs/rfc/implemented/simplification/2026-06-20-public-agent-stop-surface.md)). +- **Tests document behavior, not golden truth**: a green test pins what the code DOES, not what it SHOULD do. Before preserving a behavior solely for its test, ask whether it is load-bearing; an artifact changes together with its test, with the why in the PR. +- **RFCs are proposals, not golden truth**: validate its premise against current code before implementing; friction is evidence of over-reach — amend on the way to `implemented/`. - **Testing policy** — [docs/testing.md](docs/testing.md). Transcript/UX changes need snapshots or a PR note. Snapshot fixtures must replay on macOS/Linux; avoid GNU/BSD-only commands (e.g. `sed -i`); fix fixtures, not normalizers. -- **A tool's ACP render intent is part of its design**, decided up front (`generic`/`terminal`/`diff`, `locations`); presentation methods are pure functions of `args` ([render-intent RFC](docs/rfc/implemented/architecture/2026-07-02-tool-render-intent-union.md), [cookbook](docs/cookbook/adding-a-tool.md)). +- **A tool's ACP render intent is part of its design**, decided up front (`generic`/`terminal`/`diff`, `locations`); presentation methods are pure functions of `args` ([cookbook](docs/cookbook/adding-a-tool.md)). - **A new capability seam, lifecycle shape, or transcript surface names its coverage at every tier (unit, e2e, snapshot) at plan time** and verifies the harness can express it — a gap is scheduled work, not a mid-build surprise. - **Merge PRs with merge commits** (`gh pr merge --merge`), never squash/rebase. **Never rewrite a pushed branch**; update a child by merging its parent down. **A review fix lands on the PR that introduced the issue, as a separate commit**, then merges down ([stacked-review guide](docs/cookbook/responding-to-pr-review-on-a-stack.md)). - TODO markers: `FIXME`/`TODO`/`XXX` by urgency ([semantics](docs/development.md)). @@ -109,13 +109,13 @@ Real-API tests and demos read `DEEPSEEK_API_KEY` (and optional `DEEPSEEK_BASE_UR ## Type safety and documentation -Everything compiles under `strict: true` with `noImplicitAny`; every remaining `any` carries a comment saying why a narrower type is infeasible. Every module has a module-level doc comment; every export (and non-obvious method) has a JSDoc explaining semantics — contracts, disposal, errors — not the name restated; internal helpers only where non-obvious; one-liners when one line suffices. The export half is mechanical: `verify-export-jsdoc` (in `doc-sync`) requires description prose on every package export plus `@param`/`@returns` (and an annotated return) on function-like ones — heritage-declared members, plugin-protocol slots, and constructors exempt ([export-gate RFC](docs/rfc/implemented/process/2026-07-06-export-surface-jsdoc-gate.md)). Lean toward the stricter lint rule and the extra mechanical gate: encode invariants in checks (`verify-*` scripts), preferring a narrow justified escape hatch over a rule left off globally. Type gymnastics are acceptable inside core packages when they buy plugin-author DX (the `defineTool` schema DSL is the canonical example). +Everything compiles under `strict: true` with `noImplicitAny`; every remaining `any` carries a comment saying why a narrower type is infeasible. Every module has a module-level doc comment; every export (and non-obvious method) has a JSDoc explaining semantics — contracts, disposal, errors — not the name restated; internal helpers only where non-obvious; one-liners when one line suffices. The export half is mechanical: `verify-export-jsdoc` (in `doc-sync`) requires description prose on every package export plus `@param`/`@returns` (and an annotated return) on function-like ones. Heritage-declared members, plugin-protocol slots, and constructors are exempt — their docs' one home is the seam declaration, the framework protocol, and the class doc respectively. Lean toward the stricter lint rule and the extra mechanical gate: encode invariants in checks (`verify-*` scripts), preferring a narrow justified escape hatch over a rule left off globally. Type gymnastics are acceptable inside core packages when they buy plugin-author DX (the `defineTool` schema DSL is the canonical example). Docs are part of every change: code changes update their README and JSDoc in the SAME change; a bilingual-pair edit updates the counterpart and re-records ([i18n contract](docs/i18n/README.md)). The writing rules — document the current state never the history, one physical line per paragraph, one home per fact — and the word-budget gate live in [docs/AGENTS.md](docs/AGENTS.md). ## Editing these instructions -`AGENTS.md` is the real file; `CLAUDE.md` is a symlink to it (root, `packages/`, `examples/`). Edit `AGENTS.md`, never the symlink. This file is budget-gated (`verify-doc-budgets`): condense first if it is possible without sacrificing clarity; truly needed additions may justify a ceiling raise. +`AGENTS.md` is the real file; `CLAUDE.md` is a symlink to it (root, `packages/`, `examples/`). Edit `AGENTS.md`, never the symlink. Keep it self-contained: state each principle inline instead of citing RFCs (they stay discoverable via the RFC index); linking high-level docs — architecture, testing, cookbooks — is fine. This file is budget-gated (`verify-doc-budgets`): condense first if it is possible without sacrificing clarity; truly needed additions may justify a ceiling raise. ## Vendoring policy diff --git a/docs/cordis-catalog/services.md b/docs/cordis-catalog/services.md index 4752ce2891..1d929542b8 100644 --- a/docs/cordis-catalog/services.md +++ b/docs/cordis-catalog/services.md @@ -164,7 +164,7 @@ list(): Session[] fork(source: SessionForkSource, boundary?: number, childSessionId?: SessionId): Session ``` -Source: [`packages/core/session/src/index.ts:397`](../../packages/core/session/src/index.ts) +Source: [`packages/core/session/src/index.ts:405`](../../packages/core/session/src/index.ts) ## `ctx.subagents` — `SubagentService` diff --git a/docs/rfc/implemented/process/2026-07-06-export-surface-jsdoc-gate.md b/docs/rfc/implemented/process/2026-07-06-export-surface-jsdoc-gate.md index 54c5c54e44..2e98808426 100644 --- a/docs/rfc/implemented/process/2026-07-06-export-surface-jsdoc-gate.md +++ b/docs/rfc/implemented/process/2026-07-06-export-surface-jsdoc-gate.md @@ -22,7 +22,7 @@ The contract by declaration kind: Three exemption families keep the gate from demanding boilerplate, in the spirit of the cordis gate's `this`/`next` exemptions (documenting an exempt name anyway is allowed; only absence goes unchecked): -- **Heritage members.** A class member whose name exists on an `extends`/`implements` heritage type is exempt: the seam declaration is the doc's one home, and the IDE inherits it on hover — re-documenting every `LocalBashExecutor.run` invites drift. The exemption stops where the override grows surface the base never documented: a protected-only base member does not exempt a public override, and parameters the base never names keep their `@param` duty (an underscore-prefixed rename of a base parameter — the deliberately-unused marker — is the same parameter). This is the one question the walk asks the TYPE CHECKER (heritage types live across package boundaries, resolved through the repo `paths` map); everything else stays pure AST, and the annotated-return requirement is kept for symmetry with the cordis gate (it bound nothing at adoption — every exported function was already annotated). +- **Heritage members.** A class member whose name exists on an `extends`/`implements` heritage type is exempt: the seam declaration is the doc's one home, and the IDE inherits it on hover — re-documenting every `LocalBashExecutor.run` invites drift. The exemption stops where the override grows surface the base never documented: a protected-only base member does not exempt a public override, parameters the base never names keep their `@param` duty (an underscore-prefixed rename of a base parameter — the deliberately-unused marker — is the same parameter), and a concrete result above a void base return keeps its `@returns` duty (an unannotated override's inferred return is classified by the checker, so a faithful void override needs no boilerplate annotation). Heritage lookups and that one return classification are the walk's only TYPE CHECKER questions (heritage types live across package boundaries, resolved through the repo `paths` map); everything else stays pure AST, and the annotated-return requirement is kept for symmetry with the cordis gate (it bound nothing at adoption — every exported function was already annotated). - **Plugin-protocol slots.** Top-level `name` / `inject` / `reusable` / `Config` consts and the `apply` entry, plus the same slots as statics on a plugin class, are framework protocol: their shape is fixed by cordis, and the module doc comment plus the `interface Config` carry the plugin's real semantics. - **Constructors**, mirroring the cordis gate: plugin classes are framework-constructed, and the class doc owns the story. diff --git a/packages/core/agent/tests/verify-export-jsdoc.spec.ts b/packages/core/agent/tests/verify-export-jsdoc.spec.ts index ae7dd1bf81..b699a72765 100644 --- a/packages/core/agent/tests/verify-export-jsdoc.spec.ts +++ b/packages/core/agent/tests/verify-export-jsdoc.spec.ts @@ -487,4 +487,69 @@ export class Impl extends Base { } `))).toEqual([expect.stringMatching(/exported class method 'Impl.run' .* is a binding pattern/)]) }) + + it('revives the @returns duty when an override grows a concrete result over a void base', () => { + const voidBase = ` +/** Seam. */ +export abstract class Base { + /** Do it (fire-and-forget). */ + abstract run(): void +} +` + expect(collectExportJsdocViolations(make(`${voidBase} +/** Impl. */ +export class Impl extends Base { + override run(): number { return 1 } +} +`))).toEqual([expect.stringMatching(/exported class method 'Impl.run' .* is missing @returns \(return type: number\)\./)]) + expect(collectExportJsdocViolations(make(`${voidBase} +/** Impl. */ +export class Impl extends Base { + /** + * Do it and count. + * @returns how many were done. + */ + override run(): number { return 1 } +} +`))).toEqual([]) + }) + + it('classifies an unannotated override return over a void base via the checker', () => { + const voidBase = ` +/** Seam. */ +export abstract class Base { + /** Do it (fire-and-forget). */ + abstract run(): void +} +` + expect(collectExportJsdocViolations(make(`${voidBase} +/** Impl. */ +export class Impl extends Base { + override run() { return 1 } +} +`))).toEqual([expect.stringMatching(/exported class method 'Impl.run' .* non-void result its heritage declaration does not document/)]) + expect(collectExportJsdocViolations(make(`${voidBase} +/** Impl (faithful void, no annotation needed). */ +export class Impl extends Base { + override run() {} +} +`))).toEqual([]) + }) + + it('keeps the full exemption when the base return already carries the @returns duty', () => { + expect(collectExportJsdocViolations(make(` +/** Seam. */ +export abstract class Base { + /** + * Count things. + * @returns the count. + */ + abstract run(): number +} +/** Impl. */ +export class Impl extends Base { + override run(): number { return 1 } +} +`))).toEqual([]) + }) }) diff --git a/packages/core/session/src/index.ts b/packages/core/session/src/index.ts index b98bcd8d84..cd9e1828b2 100644 --- a/packages/core/session/src/index.ts +++ b/packages/core/session/src/index.ts @@ -373,6 +373,14 @@ export class Session { /** A fork source: either the live session object or its live store id. */ export type SessionForkSource = Session | SessionId +/** + * Rejection codes for session forking: the fork source id is unknown to the + * live store (`SESSION_NOT_FOUND`) or names a session object that is not the + * store's live instance (`SESSION_NOT_LIVE`); the requested child id is + * already taken (`SESSION_ALREADY_EXISTS`); the boundary is not a contiguous + * existing seq (`INVALID_BOUNDARY`); or the boundary event is not a + * `turn/end` — a fork must cut on a closed turn (`OPEN_TURN`). + */ export type SessionForkErrorCode = | 'SESSION_NOT_FOUND' | 'SESSION_NOT_LIVE' diff --git a/scripts/run-gates.ts b/scripts/run-gates.ts index 195e188858..6c844f842a 100644 --- a/scripts/run-gates.ts +++ b/scripts/run-gates.ts @@ -257,7 +257,9 @@ function docSyncLeafGates(): Gate[] { return [ pnpmScript('doc-typecheck', 'doc-typecheck'), pnpmScript('cordis-catalog', 'verify-cordis-catalog', { label: 'cordis catalog' }), + pnpmScript('export-jsdoc', 'verify-export-jsdoc', { label: 'export jsdoc' }), pnpmScript('tool-catalog', 'verify-tool-catalog', { label: 'tool catalog' }), + pnpmScript('config-catalog', 'verify-config-catalog', { label: 'config catalog' }), pnpmScript('persistence-catalog', 'verify-persistence-catalog', { label: 'persistence catalog' }), pnpmScript('doc-graphs', 'verify-doc-graphs', { label: 'doc graphs' }), pnpmScript('markdown-wrap', 'verify-md-wrap', { label: 'markdown wrap' }), diff --git a/scripts/verify-export-jsdoc.ts b/scripts/verify-export-jsdoc.ts index 46e2f82f2b..779888ab90 100644 --- a/scripts/verify-export-jsdoc.ts +++ b/scripts/verify-export-jsdoc.ts @@ -36,9 +36,11 @@ * the doc's one home, the IDE inherits it, and re-documenting every * implementation invites drift — UNLESS the override grows surface the * base never documented: a protected-only base member does not exempt a - * public override, and parameters the base never names keep their `@param` - * duty. This is the one question the walk asks the TYPE CHECKER (heritage - * members live across package boundaries); everything else is pure AST. + * public override, parameters the base never names keep their `@param` + * duty, and a concrete result above a void base return keeps its + * `@returns` duty. Heritage members (and classifying an unannotated + * override's inferred return above a void base) are the questions the walk + * asks the TYPE CHECKER; everything else is pure AST. * Constructors are exempt like the cordis gate's: plugin classes are * framework-constructed, and the class doc owns the story. * - Exported interfaces, type aliases, enums: description prose on the @@ -165,29 +167,31 @@ function callableAnnotation(type: ts.TypeNode): ts.SignatureDeclarationBase | 'r * is the doc's one home (the IDE inherits it on hover) and the member needs no * doc of its own — EXCEPT where the override grows public surface the base * never documented: a base member that is protected on every declaration does - * not exempt a public override (consumers could not call it before), and + * not exempt a public override (consumers could not call it before); * parameters the base never names keep their own `@param` duty (the caller * reads the seam doc, which cannot describe them; an underscore-prefixed * rename of a base parameter — the deliberately-unused marker — is the same - * parameter, not new surface). Static members are looked - * up on the base CONSTRUCTOR type (only an `extends` expression has one; an - * unresolvable or interface expression yields no property and therefore no - * exemption). + * parameter, not new surface); and a void base return carried no `@returns` + * duty, so an override returning a concrete result documents it itself. + * Static members are looked up on the base CONSTRUCTOR type (only an + * `extends` expression has one; an unresolvable or interface expression + * yields no property and therefore no exemption). * @param cls - the class whose heritage to search. * @param name - the member name to look up. * @param staticSide - whether to search the constructor side instead of the instance side. * @param checker - the program's type checker. * @returns null when no exemption applies; otherwise the parameter names the - * base declarations carry (`baseParams: null` means the base's parameters are - * not syntactically recoverable — a complex heritage type — and the member is - * exempt in full). + * base declarations carry (`baseParams: null` when not syntactically + * recoverable — a complex heritage type — exempting all parameters) plus + * whether every recoverable base return annotation is `void`-like + * (`baseVoidReturn: null` when none is recoverable, exempting the result). */ function heritageExemption( cls: ts.ClassDeclaration, name: string, staticSide: boolean, checker: ts.TypeChecker, -): { baseParams: Set | null } | null { +): { baseParams: Set | null; baseVoidReturn: boolean | null } | null { const isProtected = (d: ts.Declaration): boolean => (ts.canHaveModifiers(d) ? ts.getModifiers(d) : undefined)?.some(m => m.kind === ts.SyntaxKind.ProtectedKeyword) ?? false for (const clause of cls.heritageClauses ?? []) { @@ -198,24 +202,50 @@ function heritageExemption( const decls = prop.declarations ?? [] if (decls.length > 0 && decls.every(isProtected)) continue // public override of a protected base: new surface let baseParams: Set | null = null + let baseVoidReturn: boolean | null = null for (const d of decls) { let params: readonly ts.ParameterDeclaration[] | undefined - if (ts.isMethodDeclaration(d) || ts.isMethodSignature(d)) params = d.parameters - else if ((ts.isPropertySignature(d) || ts.isPropertyDeclaration(d)) && d.type !== undefined && ts.isFunctionTypeNode(d.type)) { + let returnType: ts.TypeNode | undefined + if (ts.isMethodDeclaration(d) || ts.isMethodSignature(d)) { + params = d.parameters + returnType = d.type + } else if ((ts.isPropertySignature(d) || ts.isPropertyDeclaration(d)) && d.type !== undefined && ts.isFunctionTypeNode(d.type)) { params = d.type.parameters + returnType = d.type.type } else continue baseParams ??= new Set() // Leading underscores are the deliberately-unused marker (eslint // argsIgnorePattern), not a rename: `_cwd` overriding `cwd` is the // same parameter, so compare underscore-stripped on both sides. for (const p of params) if (ts.isIdentifier(p.name)) baseParams.add(p.name.text.replace(/^_+/, '')) + if (returnType !== undefined) { + const voidish = /^(void|Promise)$/.test(returnType.getText(d.getSourceFile()).replace(/\s+/g, ' ')) + baseVoidReturn = (baseVoidReturn ?? true) && voidish + } } - return { baseParams } + return { baseParams, baseVoidReturn } } } return null } +/** + * True when a method's INFERRED return type is void-like (void, undefined, + * never, or a promise of one) — the one return the walk asks the checker to + * classify: an unannotated override above a void heritage member, where + * demanding an annotation just to prove faithfulness would be boilerplate. + * @param m - a method declaration with no return type annotation. + * @param checker - the program's type checker. + * @returns true when the inferred result carries nothing to document. + */ +function inferredReturnIsVoidish(m: ts.MethodDeclaration, checker: ts.TypeChecker): boolean { + const sig = checker.getSignatureFromDeclaration(m) + if (sig === undefined) return true // no callable signature: nothing classifiable to document + const returned = checker.getReturnTypeOfSignature(sig) + const awaited = checker.getAwaitedType(returned) ?? returned + return (awaited.flags & (ts.TypeFlags.Void | ts.TypeFlags.Undefined | ts.TypeFlags.Never)) !== 0 +} + /** * Check description-prose presence for one labeled declaration: JSDoc must * exist and carry prose above its block tags. @@ -285,17 +315,29 @@ function checkClass(cls: ts.ClassDeclaration, name: string, w: Walk): void { if (m.body && overloadSigs.has(mname)) continue // overload implementation: the signatures carry the docs const where = `exported class method '${name}.${mname}' (${pointer(w.rel, w.sf, m)})` if (exemption !== null) { - // The heritage declaration owns prose and @returns; parameters the - // base never names — including binding patterns, which no base - // declaration can name — are new surface and keep their @param duty. + const raw = rawJsDoc(w.text, m) + // The heritage declaration owns the prose; parameters the base never + // names — including binding patterns, which no base declaration can + // name — are new surface and keep their @param duty. const base = exemption.baseParams const inBase = (p: ts.ParameterDeclaration): boolean => base !== null && ts.isIdentifier(p.name) && base.has(p.name.text.replace(/^_+/, '')) if (base !== null && m.parameters.some(p => !thisReceiver(p) && !inBase(p))) { - const { params } = parseTags(rawJsDoc(w.text, m)) - checkParams(where, 'export', m.parameters, params, w.sf, + checkParams(where, 'export', m.parameters, parseTags(raw).params, w.sf, p => thisReceiver(p) || inBase(p), w.violations) } + // A void base return carried no @returns duty, so an override growing + // a concrete result documents it itself. An annotated override runs + // the standard check; an inferred one is classified by the checker + // (this branch is already the checker's domain), so a faithful void + // override stays exempt without a boilerplate annotation. + if (exemption.baseVoidReturn === true) { + if (m.type !== undefined) { + checkReturns(where, m.type, parseTags(raw).returns, w.sf, w.violations) + } else if (!inferredReturnIsVoidish(m, w.checker)) { + w.violations.push(`${where} returns a non-void result its heritage declaration does not document; annotate the return type and add @returns.`) + } + } continue } checkFunctionLike(where, rawJsDoc(w.text, m), m.parameters, m.type, false, w)