Gate JSDoc completeness on every package export

New doc-sync gate verify-export-jsdoc walks every module-level exported
name under packages/*/*/src and requires description prose everywhere,
plus @param per parameter and @returns on non-void annotated returns for
function-like exports, public class methods, properties, and accessors.
The parsing + check helpers move out of gen-cordis-catalog.ts into a
shared scripts/jsdoc.ts so 'documented' means one thing on both gated
surfaces.

Deliberate exemptions (documented in the RFC): heritage-declared class
members (the seam declaration is the doc's one home — the one checker
query in an otherwise pure-AST walk), cordis plugin-protocol slots
(name/inject/reusable/Config/apply, top-level and static), constructors,
overload implementations, declare-module augmentation bodies, and
re-export statements (checked at the defining module).

The 203 under-documented exports the gate found at adoption are filled
in this change, so the gate lands green; generated catalogs/graphs are
regenerated for the shifted line pointers.

RFC: docs/rfc/implemented/process/2026-07-06-export-surface-jsdoc-gate.md
This commit is contained in:
Tianyi Cui
2026-07-06 22:09:30 +08:00
parent 1c999804d8
commit cd9737d569
92 changed files with 1802 additions and 289 deletions

View File

@@ -129,6 +129,9 @@ export interface LocalDirEntry {
* and intermediate directories are created by the write. Two input paths
* reaching the same file via symlinks share one key. Falls back to the absolute
* path only when no ancestor (not even the filesystem root) can be resolved.
* @param cwd - base directory a relative `path` resolves against.
* @param path - absolute or relative path; empty/whitespace-only throws `FS_NOT_FOUND`.
* @returns the absolute display path plus the realpath-derived stable target key.
*/
export async function resolveLocalTarget(cwd: string, path: string): Promise<LocalTarget> {
if (path.trim().length === 0) throw new FsError('file_path must be a non-empty string', 'FS_NOT_FOUND')
@@ -165,7 +168,11 @@ export async function resolveLocalTarget(cwd: string, path: string): Promise<Loc
}
}
/** Probe a path for its version, mode, type, and size. Null if absent. */
/**
* Probe a path for its version, mode, type, and size. Null if absent.
* @param absolutePath - the path to stat (typically a target key; symlinks are followed).
* @returns the metadata, or null when the path — or a parent segment — does not exist.
*/
export async function probe(absolutePath: string): Promise<PathInfo | null> {
try {
const info = await stat(absolutePath)
@@ -200,6 +207,9 @@ async function resolveListedChildTarget(parent: LocalTarget, name: string): Prom
* List direct children of a directory in stable name order. Each child includes
* a resolved target plus stat metadata when still available; file contents are
* never read.
* @param target - the resolved directory to list; a missing or non-directory target throws.
* @param signal - aborts the listing, checked between children (`FS_ABORTED`).
* @returns one entry per direct child, sorted by name.
*/
export async function listDirectory(target: LocalTarget, signal?: AbortSignal): Promise<LocalDirEntry[]> {
throwIfAborted(signal, 'list')
@@ -290,6 +300,9 @@ async function statRegularFile(target: LocalTarget, verb: 'read', signal?: Abort
/**
* Read a whole regular UTF-8 text file into a single decoded string. Rejects
* non-regular files, invalid UTF-8, and NUL-byte binary samples.
* @param target - the resolved file to read.
* @param signal - aborts the read (`FS_ABORTED`).
* @returns the full decoded text, byte-for-byte (no normalization).
*/
export async function readWholeText(target: LocalTarget, signal?: AbortSignal): Promise<string> {
await statRegularFile(target, 'read', signal)
@@ -305,6 +318,9 @@ export async function readWholeText(target: LocalTarget, signal?: AbortSignal):
* Stream a whole regular UTF-8 text file as decoded text chunks. Same text
* semantics as {@link readWholeText} (regular-file check, binary/NUL rejection,
* cross-chunk UTF-8 decoding), but never holds the whole file in memory.
* @param target - the resolved file to stream.
* @param signal - aborts the stream, including between chunks (`FS_ABORTED`).
* @returns decoded text chunks in file order; chunk boundaries carry no meaning.
*/
export async function* streamWholeText(target: LocalTarget, signal?: AbortSignal): AsyncIterable<string> {
await statRegularFile(target, 'read', signal)
@@ -352,6 +368,11 @@ async function removeStagingDirOrThrow(stagingDir: string, originalError: unknow
* (`0o700`) staging directory, fsync, optionally chmod to the final mode while
* still private, then rename over the target. `mode` (when given) preserves an
* existing file's permissions across the replace.
* @param absolutePath - the final destination (typically a target key); missing parent dirs are created.
* @param content - the full UTF-8 text to write.
* @param mode - final file mode applied before the rename (an existing file's, to preserve permissions); undefined leaves `0o600`.
* @param signal - aborts the write (`FS_ABORTED`); checked before the rename, so the target is never left torn.
* @param internals - test seam for pinning temp names and observing the staged file.
*/
export async function writeFileAtomic(
absolutePath: string,
@@ -409,6 +430,12 @@ export async function writeFileAtomic(
/** Line ending style detected before LF normalization. */
export type LineEndings = 'LF' | 'CRLF'
/**
* Collapse CRLF to LF — the canonical in-memory form every edit/diff basis
* uses. Lone `\r` bytes (not followed by `\n`) are left untouched.
* @param content - decoded text in whatever line-ending style the file had.
* @returns the text with every `\r\n` pair replaced by `\n`.
*/
function normalizeLineEndings(content: string): string {
return content.replaceAll('\r\n', '\n')
}
@@ -420,6 +447,14 @@ function detectLineEndings(raw: string): LineEndings {
return crlfCount > lfCount ? 'CRLF' : 'LF'
}
/**
* Convert LF-normalized content back to the line-ending style detected at read
* time, for write-back. `LF` returns the content unchanged; `CRLF` re-normalizes
* first so an already-CRLF sequence is never doubled to `\r\r\n`.
* @param content - the LF-normalized (edited) text.
* @param lineEndings - the original file's style, as detected by {@link readForEdit}.
* @returns the text in the original file's line-ending style.
*/
function restoreLineEndings(content: string, lineEndings: LineEndings): string {
return lineEndings === 'LF' ? content : normalizeLineEndings(content).split('\n').join('\r\n')
}
@@ -438,6 +473,10 @@ function countOccurrences(content: string, needle: string): number {
/**
* Read and decode a file for editing: rejects binaries, returns LF-normalized
* content plus the original line-ending style for write-back.
* @param absolutePath - the file to read (typically a target key).
* @param displayPath - the caller-facing path used in error messages.
* @param signal - aborts the read (`FS_ABORTED`).
* @returns the LF-normalized content and the detected style to restore on write-back.
*/
export async function readForEdit(
absolutePath: string,
@@ -459,6 +498,9 @@ export async function readForEdit(
* prior bytes, so an undiffable prior file simply yields no contextual-hunk basis
* (the caller treats `null` the same as an absent file: the result renders a
* whole-file diff rather than an applied hunk).
* @param absolutePath - the file to read (typically a target key); it must exist.
* @param signal - aborts the read (`FS_ABORTED`).
* @returns the LF-normalized text, or null for a binary or non-UTF-8 file.
*/
export async function readTextForDiff(absolutePath: string, signal?: AbortSignal): Promise<string | null> {
const buffer = await readFileAbortable(absolutePath, 'read', signal)
@@ -477,6 +519,12 @@ export async function readTextForDiff(absolutePath: string, signal?: AbortSignal
* `FS_EDIT_NOT_FOUND` on empty `oldString` or zero matches and
* `FS_AMBIGUOUS_EDIT` on multiple matches when `replaceAll` is false. Returns
* the edited content (still LF-normalized) and the replacement count.
* @param content - the current file content, already LF-normalized.
* @param oldString - literal text to find; CRLF inside it is normalized to LF before matching.
* @param newString - literal replacement text, normalized the same way.
* @param replaceAll - replace every match instead of requiring exactly one.
* @param displayPath - the caller-facing path used in error messages.
* @returns the edited LF-normalized content plus how many occurrences were replaced.
*/
export function applyLiteralEdit(
content: string,

View File

@@ -73,6 +73,7 @@ export class LocalFileSystem extends FileSystem {
cwd: z.string().default(process.cwd()),
})
/** Validated config (schemastery applied the defaults before construction). */
readonly config: ResolvedConfig
/** Test seam forwarded to fsio (force streaming path, pin temp names). */
internals: FsIoInternals = {}

View File

@@ -29,7 +29,12 @@ import type { Branded } from '@deepseek-ai/dsh-brand'
*/
export type FsTargetKey = Branded<'FsTargetKey'>
/** Brand a string as an {@link FsTargetKey}. */
/**
* Brand a string as an {@link FsTargetKey}. For backend use only — a consumer
* never manufactures a key, it receives one from `resolve()`.
* @param key - the backend's raw key string (the local backend passes a realpath).
* @returns the same string, branded; no validation is performed.
*/
export function FsTargetKey(key: string): FsTargetKey {
return key as FsTargetKey
}
@@ -42,7 +47,12 @@ export function FsTargetKey(key: string): FsTargetKey {
*/
export type FsVersion = Branded<'FsVersion'>
/** Brand a string as an {@link FsVersion}. */
/**
* Brand a string as an {@link FsVersion}. For backend use only — a consumer
* never manufactures a version, it receives one from `stat`/write/edit outcomes.
* @param v - the backend's raw version string (the local backend derives it from mtime+size).
* @returns the same string, branded; no validation is performed.
*/
export function FsVersion(v: string): FsVersion {
return v as FsVersion
}

View File

@@ -40,6 +40,10 @@ export type FsDiffMeta = { diffs: FileDiff[] }
* (a pure insertion) reports `oldText: null` (nothing to diff against), mirroring
* the call-time card's new-file convention. The unified-diff "\ No newline at end
* of file" markers are dropped — they annotate the patch, not file content.
* @param path - the path stamped on every produced diff (the model-facing `file_path`; the bridge relativizes it).
* @param before - the file text before the change (the backend's LF-normalized diff basis).
* @param after - the file text after the change, on the same basis.
* @returns one diff per applied hunk, in file order; empty when the texts are identical.
*/
export function computeHunkDiffs(path: string, before: string, after: string): FileDiff[] {
const patch = structuredPatch('', '', before, after, undefined, undefined, { context: DIFF_CONTEXT })
@@ -83,6 +87,8 @@ function isFileDiff(value: unknown): value is FileDiff {
* it validates defensively rather than trusting the payload — a bad `meta` yields
* `undefined`, and the caller decides the fallback (edit → the generic result
* rendering; write → an args-derived whole-file diff), never a thrown presenter.
* @param meta - the opaque `tool/result` meta payload (live or replayed from the session log).
* @returns the validated non-empty hunk list, or undefined for an absent/empty/malformed payload.
*/
export function diffsFromMeta(meta: unknown): FileDiff[] | undefined {
if (typeof meta !== 'object' || meta === null || Array.isArray(meta)) return undefined

View File

@@ -29,7 +29,13 @@ interface EditInput {
replaceAll: boolean
}
/** Validate value constraints the schema DSL can't express. */
/**
* Validate value constraints the schema DSL can't express: a non-blank
* `file_path`, a non-empty `old_string`, and `old_string !== new_string`
* (an equal pair would be a guaranteed no-op edit).
* @param args - the schema-validated raw tool arguments.
* @returns the camelCased input with `replace_all` defaulted to false.
*/
export function parseEditArgs(args: { file_path: string; old_string: string; new_string: string; replace_all?: boolean }): EditInput {
if (args.file_path.trim().length === 0) throw new Error('file_path must be a non-empty string')
if (args.old_string.length === 0) throw new Error('old_string must be a non-empty string')
@@ -42,14 +48,22 @@ export function parseEditArgs(args: { file_path: string; old_string: string; new
}
}
/** Format an edit success (single-match or replace-all) as a Claude-style model-facing message. */
/**
* Format an edit success (single-match or replace-all) as a Claude-style model-facing message.
* @param displayPath - the backend-resolved path shown to the model.
* @param replaceAll - selects the all-occurrences wording over the single-replacement one.
* @returns the confirmation sentence the model sees as the tool result.
*/
export function formatEditOutput(displayPath: string, replaceAll: boolean): string {
return replaceAll
? `The file ${displayPath} has been updated. All occurrences were successfully replaced.`
: `The file ${displayPath} has been updated successfully.`
}
/** Register the `edit` tool and its system-prompt guidance. */
/**
* Register the `edit` tool and its system-prompt guidance.
* @param ctx - the plugin context; registrations are effects scoped to it, and execution uses its `fs` service.
*/
export function applyEditTool(ctx: Context): void {
ctx.systemPrompt.section({
name: 'tool:edit',

View File

@@ -119,6 +119,10 @@ function finish(acc: WindowAccumulator, request: ReadWindow, displayPath: string
* path serves both. Scans for newlines with a capped line buffer (a newline-free
* giant line is truncated, never buffered past `request.maxLineLength`),
* enforces the byte cap, and throws `FS_NOT_FOUND` for an offset past EOF.
* @param chunks - decoded text chunks in file order; chunk boundaries carry no meaning.
* @param request - the resolved window; the caller has already applied its defaults and caps.
* @param displayPath - the caller-facing path used in the offset-out-of-range error.
* @returns the numbered window lines, the total line count seen, and the byte-cap truncation flag.
*/
export async function buildWindow(
chunks: AsyncIterable<string> | Iterable<string>,
@@ -156,7 +160,12 @@ export async function buildWindow(
return finish(acc, request, displayPath)
}
/** Format a read outcome as one OpenCode-style line-numbered text block body. */
/**
* Format a read outcome as one OpenCode-style line-numbered text block body.
* @param displayPath - the backend-resolved path rendered in the envelope's `<path>` element.
* @param outcome - the windowed read to render.
* @returns the model-facing envelope: numbered lines plus a continuation or end-of-file footer.
*/
export function formatReadOutput(displayPath: string, outcome: FileReadOutcome): string {
const endLine = outcome.lines.at(-1)?.number ?? Math.max(0, outcome.offset - 1)
let footer: string

View File

@@ -58,7 +58,12 @@ function parsePositiveInteger(value: number, name: string): number {
return value
}
/** Validate value constraints the schema DSL can't express. `maxLimit` is the deployment's line cap. */
/**
* Validate value constraints the schema DSL can't express. `maxLimit` is the deployment's line cap.
* @param args - the schema-validated raw tool arguments; `offset`/`limit` must be positive integers when given.
* @param maxLimit - the configured line cap: both the default `limit` and the largest one accepted.
* @returns the validated input with `offset` defaulted to 1 and `limit` to `maxLimit`.
*/
export function parseReadArgs(args: { file_path: string; offset?: number; limit?: number }, maxLimit: number): ReadInput {
if (args.file_path.trim().length === 0) throw new Error('file_path must be a non-empty string')
const offset = args.offset === undefined ? 1 : parsePositiveInteger(args.offset, 'offset')
@@ -67,7 +72,11 @@ export function parseReadArgs(args: { file_path: string; offset?: number; limit?
return { filePath: args.file_path, offset, limit }
}
/** Register the `read` tool and its system-prompt guidance. */
/**
* Register the `read` tool and its system-prompt guidance.
* @param ctx - the plugin context; registrations are effects scoped to it, and execution uses its `fs` service.
* @param caps - the deployment's resolved read caps (plugin config after defaulting).
*/
export function applyReadTool(ctx: Context, caps: ReadToolCaps): void {
ctx.systemPrompt.section({
name: 'tool:read',

View File

@@ -18,7 +18,11 @@
import type { ToolExecution } from '@deepseek-ai/dsh-tools'
/** The session workspace cwd for this call, or `undefined` when none applies. */
/**
* The session workspace cwd for this call, or `undefined` when none applies.
* @param exec - the tool-execution context; only its optional `agent` is read.
* @returns the calling agent's session cwd, or undefined for a non-agent caller (the backend then applies its own default).
*/
export function sessionCwd(exec: ToolExecution): string | undefined {
return exec.agent?.session.header.cwd
}

View File

@@ -21,13 +21,23 @@ import type {} from '@deepseek-ai/dsh-system-prompt'
import { computeHunkDiffs, diffsFromMeta, type FsDiffMeta } from './diff.ts'
import { sessionCwd } from './session-cwd.ts'
/** Validate value constraints the schema DSL can't express. */
/**
* Validate value constraints the schema DSL can't express: only a non-blank
* `file_path` — an empty `content` is legitimate (it writes an empty file).
* @param args - the schema-validated raw tool arguments.
* @returns the camelCased input; `content` passes through untouched.
*/
export function parseWriteArgs(args: { file_path: string; content: string }): { filePath: string; content: string } {
if (args.file_path.trim().length === 0) throw new Error('file_path must be a non-empty string')
return { filePath: args.file_path, content: args.content }
}
/** Format a write outcome as one model-facing text block body. */
/**
* Format a write outcome as one model-facing text block body.
* @param displayPath - the backend-resolved path rendered in the envelope's `<path>` element.
* @param outcome - the write outcome; its `operation` selects the Created/Updated wording.
* @returns the model-facing confirmation envelope (no file content is echoed back).
*/
export function formatWriteOutput(displayPath: string, outcome: FsWriteOutcome): string {
const verb = outcome.operation === 'create' ? 'Created' : 'Updated'
return `<path>${displayPath}</path>
@@ -37,7 +47,10 @@ ${verb} file
</content>`
}
/** Register the `write` tool and its system-prompt guidance. */
/**
* Register the `write` tool and its system-prompt guidance.
* @param ctx - the plugin context; registrations are effects scoped to it, and execution uses its `fs` service.
*/
export function applyWriteTool(ctx: Context): void {
ctx.systemPrompt.section({
name: 'tool:write',