fix review findings: close export-form and heritage-exemption gaps

Codex round-1 review found three fail-open paths in the new gate:

- Unhandled export forms passed silently. checkDecl now fails CLOSED on
  unrecognized exported statement kinds, 'export =' is refused outright,
  'export import X = N.member' is an explicit documented skip (alias;
  definition site owns the doc), and ambient 'declare namespace' bodies
  recurse with implicit export semantics.
- Function-like exports escaped the function contract: non-identifier
  default exports and consts with INLINE function-type annotations now
  get full @param/@returns checks (the named-type waiver stays for
  reference annotations only).
- The heritage exemption was name-only: it no longer exempts a public
  override of a protected-only base member, and parameters the base
  never names keep their @param duty (underscore-prefixed renames of a
  base parameter count as the same parameter).

Eight new negative-path tests pin the closed gaps; RFC and module doc
updated to the refined contract.
This commit is contained in:
Tianyi Cui
2026-07-06 23:25:33 +08:00
parent 54bb9e5ec7
commit 9ff010cbef
3 changed files with 249 additions and 53 deletions

View File

@@ -13,15 +13,16 @@ A new gate, `scripts/verify-export-jsdoc.ts` (`pnpm run verify-export-jsdoc`, wi
The contract by declaration kind:
- Every exported name needs JSDoc with non-empty description prose.
- Function-like exports (function declarations; consts with function initializers) follow the full function contract. A const whose declarator is type-annotated (`export const f: Handler = …`) defers the return contract to the named type; `@returns` stays optional there.
- Function-like exports (function declarations; consts with function initializers or an INLINE function-type annotation; non-identifier function default exports) follow the full function contract. A const whose declarator is annotated with a NAMED type (`export const f: Handler = …`) defers the signature contract to that type's own declaration and `@returns` stays optional; an inline `(x: T) => U` annotation is the surface signature itself and gets the full contract.
- Exported classes need class-level prose; public methods (statics included — reachable on the exported name) follow the function contract; public properties and accessors need prose (a get/set pair is covered by the getter). Overload implementations are exempt — the signatures carry the docs.
- Exported interfaces, type aliases, and enums need prose on the declaration; member-level enforcement is deliberately deferred (the highest-value member surface — seam service classes — is already under the cordis gate).
- Exported namespaces recurse; the namespace itself needs prose only when it does not merge with a documented same-name declaration (the Config-namespace idiom documents the plugin once).
- `declare module` / `declare global` bodies and `export … from` re-export statements are skipped: an augmentation is not an export of the package, and a re-exported definition is checked where it is defined.
- Exported namespaces recurse (inside an ambient `declare` namespace every member exports implicitly); the namespace itself needs prose only when it does not merge with a documented same-name declaration (the Config-namespace idiom documents the plugin once).
- `declare module` / `declare global` bodies, `export … from` re-export statements, and `export import X = N.member` aliases are skipped: an augmentation is not an export of the package, and a re-exported or aliased definition is checked where it is defined.
- Everything else fails CLOSED: `export =` is refused outright, and an exported statement kind the dispatch does not recognize is itself a violation — no export form can pass unchecked by omission.
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. 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, 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).
- **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.