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:
@@ -13,7 +13,9 @@ import { parseSse } from './sse.ts'
|
||||
import { translate } from './translate.ts'
|
||||
import type { WireError } from './types.ts'
|
||||
|
||||
/** Constructor options for {@link DeepSeekAdapter}; the plugin's `apply` resolves them from Config + environment. */
|
||||
export interface DeepSeekAdapterOptions {
|
||||
/** Bearer token sent in the `authorization` header on every request. */
|
||||
apiKey: string
|
||||
/** Endpoint base; `/chat/completions` is appended. */
|
||||
baseURL: string
|
||||
@@ -21,7 +23,11 @@ export interface DeepSeekAdapterOptions {
|
||||
defaults?: RequestDefaults
|
||||
}
|
||||
|
||||
/** Map an HTTP status to a stable LlmError code. */
|
||||
/**
|
||||
* Map an HTTP status to a stable LlmError code.
|
||||
* @param status - status of a non-2xx provider response.
|
||||
* @returns `AUTH` (401/403), `RATE_LIMIT` (429), `INVALID_REQUEST` (400), `SERVER` (5xx), or `HTTP_<status>` for anything else.
|
||||
*/
|
||||
export function httpErrorCode(status: number): string {
|
||||
if (status === 401 || status === 403) return 'AUTH'
|
||||
if (status === 429) return 'RATE_LIMIT'
|
||||
|
||||
@@ -34,6 +34,12 @@ export type * from './types.ts'
|
||||
export const name = 'llm-deepseek'
|
||||
export const inject = ['llm']
|
||||
|
||||
/**
|
||||
* Plugin config, validated by the same-named schemastery schema. Every field
|
||||
* is optional in yml: credentials/endpoint fall back to the environment (a
|
||||
* missing API key fails plugin load, not the first call), and omitted
|
||||
* thinking fields send nothing on the wire, so the provider default applies.
|
||||
*/
|
||||
export interface Config {
|
||||
/** API key; falls back to $DEEPSEEK_API_KEY. Required one way or the other. */
|
||||
apiKey?: string
|
||||
|
||||
@@ -66,6 +66,8 @@ function serializeAssistant(message: Message): WireMessage {
|
||||
* `{role: 'tool'}` messages; the harness puts each tool result in its own
|
||||
* user-role message, so a mixed user message contributes its text first and
|
||||
* its tool results as separate wire messages after.
|
||||
* @param messages - the harness conversation, in order.
|
||||
* @returns the wire messages; order preserved, each tool result expanded into its own entry.
|
||||
*/
|
||||
export function serializeMessages(messages: Message[]): WireMessage[] {
|
||||
const wire: WireMessage[] = []
|
||||
@@ -97,7 +99,14 @@ export function serializeMessages(messages: Message[]): WireMessage[] {
|
||||
return wire
|
||||
}
|
||||
|
||||
/** Build the full wire request. */
|
||||
/**
|
||||
* Build the full wire request. Always streaming (`stream: true`, usage
|
||||
* reporting on); optional fields are omitted rather than sent as null, so
|
||||
* provider defaults apply.
|
||||
* @param options - the harness request (model, history, system, tools, sampling).
|
||||
* @param defaults - adapter-level thinking defaults; undefined fields put nothing on the wire.
|
||||
* @returns the chat-completions request body.
|
||||
*/
|
||||
export function serializeRequest(options: GenerateOptions, defaults: RequestDefaults = {}): WireRequest {
|
||||
const messages: WireMessage[] = []
|
||||
if (options.system !== undefined) {
|
||||
|
||||
@@ -37,6 +37,8 @@ function eventData(block: string): string | undefined {
|
||||
* Parse a byte stream into SSE data payloads. Yields `[DONE]` as the final
|
||||
* value and returns; throws `LlmError('STREAM_CLOSED')` when the stream ends
|
||||
* without it (truncated response — the model call cannot be trusted).
|
||||
* @param stream - raw SSE bytes; reads may split anywhere, including mid-UTF-8 sequence.
|
||||
* @returns each event's data payload in arrival order, the `[DONE]` sentinel last.
|
||||
*/
|
||||
export async function* parseSse(stream: AsyncIterable<Uint8Array>): AsyncGenerator<string> {
|
||||
const decoder = new TextDecoder()
|
||||
|
||||
@@ -29,7 +29,11 @@ interface OpenBlock {
|
||||
name?: string
|
||||
}
|
||||
|
||||
/** Map the wire finish_reason vocabulary to the harness FinishReason. */
|
||||
/**
|
||||
* Map the wire finish_reason vocabulary to the harness FinishReason.
|
||||
* @param reason - the wire `finish_reason` string.
|
||||
* @returns the mapped reason; unrecognized values (content_filter, …) become `{kind: 'error'}` with the uppercased value as `code`.
|
||||
*/
|
||||
export function mapFinishReason(reason: string): FinishReason {
|
||||
switch (reason) {
|
||||
case 'stop': return { kind: 'stop' }
|
||||
@@ -46,6 +50,8 @@ export function mapFinishReason(reason: string): FinishReason {
|
||||
* (`prompt_tokens = prompt_cache_hit_tokens + prompt_cache_miss_tokens`,
|
||||
* api/create-chat-completion); the harness TokenUsage convention is
|
||||
* DISJOINT counts, so cache reads are subtracted out of `inputTokens`.
|
||||
* @param usage - wire usage from the finish chunk or the trailing usage-only chunk.
|
||||
* @returns disjoint harness counts; cache/reasoning fields present only when the wire reported them.
|
||||
*/
|
||||
export function mapUsage(usage: WireUsage): TokenUsage {
|
||||
const cacheRead = usage.prompt_tokens_details?.cached_tokens ?? usage.prompt_cache_hit_tokens
|
||||
@@ -75,6 +81,8 @@ function closeBlock(block: OpenBlock): ContentBlock {
|
||||
/**
|
||||
* Consume SSE data payloads (ending with `[DONE]`) and yield StreamChunks.
|
||||
* Malformed JSON payloads abort the stream with `MALFORMED_RESPONSE`.
|
||||
* @param payloads - SSE data payloads from {@link parseSse}, `[DONE]`-terminated.
|
||||
* @returns deltas as they arrive; `block-end`s, `usage`, and `finish` are all deferred to the `[DONE]` sentinel.
|
||||
*/
|
||||
export async function* translate(payloads: AsyncIterable<string>): AsyncGenerator<StreamChunk> {
|
||||
let nextIndex = 0
|
||||
|
||||
@@ -48,12 +48,18 @@ export interface WireToolMessage {
|
||||
content: string
|
||||
}
|
||||
|
||||
/** One entry of the request `messages` array, discriminated on `role`. */
|
||||
export type WireMessage =
|
||||
| WireSystemMessage
|
||||
| WireUserMessage
|
||||
| WireAssistantMessage
|
||||
| WireToolMessage
|
||||
|
||||
/**
|
||||
* Assistant-role history message. The harness replays `content: ""` (never
|
||||
* null) on tool-call-only turns — some gateways reject null — and sends null
|
||||
* only when the turn carried neither text nor tool calls.
|
||||
*/
|
||||
export interface WireAssistantMessage {
|
||||
role: 'assistant'
|
||||
content: string | null
|
||||
@@ -66,12 +72,14 @@ export interface WireAssistantMessage {
|
||||
tool_calls?: WireToolCall[]
|
||||
}
|
||||
|
||||
/** A completed tool call replayed on an assistant history message; `arguments` is the raw JSON string. */
|
||||
export interface WireToolCall {
|
||||
id: string
|
||||
type: 'function'
|
||||
function: { name: string; arguments: string }
|
||||
}
|
||||
|
||||
/** One entry of the request `tools` array; `parameters` is a JSON Schema object. */
|
||||
export interface WireTool {
|
||||
type: 'function'
|
||||
function: {
|
||||
@@ -88,11 +96,13 @@ export interface WireChunk {
|
||||
usage?: WireUsage | null
|
||||
}
|
||||
|
||||
/** One streamed choice (requests always ask for a single one); `finish_reason` is non-null only on its terminal chunk. */
|
||||
export interface WireChoice {
|
||||
delta?: WireDelta
|
||||
finish_reason?: string | null
|
||||
}
|
||||
|
||||
/** The incremental content of one streamed choice; any subset of fields may be present per chunk. */
|
||||
export interface WireDelta {
|
||||
role?: string
|
||||
/** Visible text. Null/empty on reasoning/tool-call chunks. */
|
||||
@@ -105,6 +115,7 @@ export interface WireDelta {
|
||||
tool_calls?: WireToolCallDelta[]
|
||||
}
|
||||
|
||||
/** A streamed fragment of one tool call; fragments sharing an `index` concatenate into one call. */
|
||||
export interface WireToolCallDelta {
|
||||
/** Disambiguates parallel tool calls; stable across a call's deltas. */
|
||||
index: number
|
||||
@@ -119,6 +130,13 @@ export interface WireToolCallDelta {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wire token accounting. `prompt_tokens` INCLUDES cache hits (it equals
|
||||
* `prompt_cache_hit_tokens + prompt_cache_miss_tokens`); `mapUsage` subtracts
|
||||
* them to keep the harness convention of disjoint counts.
|
||||
* `prompt_tokens_details.cached_tokens` is the OpenAI-compat spelling of the
|
||||
* hit count.
|
||||
*/
|
||||
export interface WireUsage {
|
||||
prompt_tokens: number
|
||||
completion_tokens: number
|
||||
|
||||
@@ -21,14 +21,22 @@ import { toPiContext, toStreamChunks } from './convert.ts'
|
||||
/** Reasoning levels surfaced by this adapter (DeepSeek wire: high|max). */
|
||||
export type PiAiReasoning = 'off' | 'high' | 'xhigh'
|
||||
|
||||
/** Constructor options for {@link PiAiAdapter}; the plugin's `apply` resolves them from Config + environment. */
|
||||
export interface PiAiAdapterOptions {
|
||||
/** Bearer token pi-ai sends on every request. */
|
||||
apiKey: string
|
||||
/** Endpoint base; `/chat/completions` is appended. */
|
||||
baseURL: string
|
||||
/** Thinking level applied to every request ('off' disables thinking). */
|
||||
reasoning?: PiAiReasoning | undefined
|
||||
}
|
||||
|
||||
/** Build the inline pi-ai model descriptor for one DeepSeek model name. */
|
||||
/**
|
||||
* Build the inline pi-ai model descriptor for one DeepSeek model name.
|
||||
* @param modelId - harness model name; sent verbatim on the wire.
|
||||
* @param options - adapter options; only `baseURL` is read here (key and reasoning apply per request, not per descriptor).
|
||||
* @returns a descriptor with every DeepSeek compat flag explicit — pi-ai's URL-based auto-detection is never relied on.
|
||||
*/
|
||||
export function buildModel(modelId: string, options: PiAiAdapterOptions): Model<'openai-completions'> {
|
||||
return {
|
||||
id: modelId,
|
||||
|
||||
@@ -55,6 +55,8 @@ function parseArguments(raw: string): Record<string, unknown> {
|
||||
* NAME (pi-ai's `toolName`), which the harness doesn't carry on the result
|
||||
* block — it is recovered from the preceding assistant tool-call with the
|
||||
* same id.
|
||||
* @param options - the harness request; `options.system` maps to pi-ai's single `systemPrompt` slot.
|
||||
* @returns the pi-ai context; `tools` is omitted entirely when the request declares none.
|
||||
*/
|
||||
export function toPiContext(options: GenerateOptions): PiContext {
|
||||
const toolNames = new Map<CallId, string>()
|
||||
@@ -159,7 +161,11 @@ function emptyPiUsage(): PiUsage {
|
||||
}
|
||||
}
|
||||
|
||||
/** Map pi-ai usage (reasoning folded into output by pi-ai). */
|
||||
/**
|
||||
* Map pi-ai usage (reasoning folded into output by pi-ai).
|
||||
* @param usage - cumulative usage from the terminal pi-ai event.
|
||||
* @returns harness counts; cache fields appear only when non-zero (pi-ai reports zeros, not absence).
|
||||
*/
|
||||
export function mapUsage(usage: PiUsage): TokenUsage {
|
||||
return {
|
||||
inputTokens: usage.input,
|
||||
@@ -177,7 +183,11 @@ function classifyPiAiError(message: string): string {
|
||||
return 'PI_AI_ERROR'
|
||||
}
|
||||
|
||||
/** Map a terminal pi-ai event to the harness finish reason. */
|
||||
/**
|
||||
* Map a terminal pi-ai event to the harness finish reason.
|
||||
* @param message - the assistant message carried by the `done` or `error` event.
|
||||
* @returns the harness reason; `error` yields `{kind: 'error'}` with a code classified from the error text.
|
||||
*/
|
||||
export function mapStopReason(message: AssistantMessage): FinishReason {
|
||||
switch (message.stopReason) {
|
||||
case 'stop': return { kind: 'stop' }
|
||||
@@ -195,6 +205,9 @@ export function mapStopReason(message: AssistantMessage): FinishReason {
|
||||
* Translate the pi-ai event stream into StreamChunks. pi-ai never throws
|
||||
* mid-stream — failures arrive as `error` events, which become error/aborted
|
||||
* `finish` chunks (the harness protocol's other error-delivery style).
|
||||
* @param events - one assistant turn's pi-ai event stream.
|
||||
* @returns the harness chunks, ending with `usage` then `finish`; throws
|
||||
* `LlmError` (`STREAM_CLOSED`) if the source ends without a terminal event.
|
||||
*/
|
||||
export async function* toStreamChunks(events: AsyncIterable<AssistantMessageEvent>): AsyncGenerator<StreamChunk> {
|
||||
// pi-ai contentIndex ↔ our block index map 1:1 (both count blocks from 0
|
||||
|
||||
@@ -29,6 +29,11 @@ export { mapStopReason, mapUsage, toPiContext, toStreamChunks } from './convert.
|
||||
export const name = 'llm-pi-ai'
|
||||
export const inject = ['llm']
|
||||
|
||||
/**
|
||||
* Plugin config, validated by the same-named schemastery schema. Every field
|
||||
* is optional in yml: credentials/endpoint fall back to the environment (a
|
||||
* missing API key fails plugin load, not the first call).
|
||||
*/
|
||||
export interface Config {
|
||||
/** API key; falls back to $DEEPSEEK_API_KEY. Required one way or the other. */
|
||||
apiKey?: string
|
||||
|
||||
@@ -40,6 +40,8 @@ export class BlockAssembler {
|
||||
/**
|
||||
* Feed one chunk. Returns the completed block when the chunk closes one
|
||||
* (an explicit `block-end`), otherwise undefined.
|
||||
* @param chunk - the next raw chunk, in stream order.
|
||||
* @returns the authoritative block from the first `block-end` at its index; undefined for every other chunk.
|
||||
*/
|
||||
push(chunk: StreamChunk): ContentBlock | undefined {
|
||||
switch (chunk.type) {
|
||||
@@ -123,20 +125,29 @@ export class BlockAssembler {
|
||||
return partial
|
||||
}
|
||||
|
||||
/** Assemble all blocks seen so far, in stream order. */
|
||||
/**
|
||||
* Assemble all blocks seen so far, in stream order.
|
||||
* @returns one block per seen index; an open block assembles from its
|
||||
* accumulated deltas (an unknown block type never closed by `block-end` throws).
|
||||
*/
|
||||
blocks(): ContentBlock[] {
|
||||
return this.order.map(index => this.assemble(this.mustGet(index), index))
|
||||
}
|
||||
|
||||
/** Usage from the `usage` chunk; undefined until one arrives. */
|
||||
get usage(): TokenUsage | undefined {
|
||||
return this._usage
|
||||
}
|
||||
|
||||
/** Finish reason from the `finish` chunk; `{kind: 'stop'}` when the stream ended without one. */
|
||||
get finish(): FinishReason {
|
||||
return this._finish ?? { kind: 'stop' }
|
||||
}
|
||||
|
||||
/** The assembled assistant message. */
|
||||
/**
|
||||
* The assembled assistant message.
|
||||
* @returns an assistant-role message over `blocks()` (same open-block assembly rules).
|
||||
*/
|
||||
message(): Message {
|
||||
return { role: 'assistant', content: this.blocks() }
|
||||
}
|
||||
|
||||
@@ -54,6 +54,8 @@ export const APP_IDENTITY: AppIdentity = {
|
||||
* The standard `User-Agent` value: `product/version (+url)`. The
|
||||
* parenthesized `+url` comment is the conventional self-identification form
|
||||
* (RFC 9110 §10.1.5 product + comment syntax).
|
||||
* @param identity - the identity to render; defaults to {@link APP_IDENTITY}.
|
||||
* @returns the ready-to-send header value.
|
||||
*/
|
||||
export function userAgent(identity: AppIdentity = APP_IDENTITY): string {
|
||||
return `${identity.product}/${identity.version} (+${identity.url})`
|
||||
@@ -63,6 +65,8 @@ export function userAgent(identity: AppIdentity = APP_IDENTITY): string {
|
||||
* Build the attribution headers an adapter must send on every provider
|
||||
* request. Header names are lowercase (HTTP field names are case-insensitive
|
||||
* on the wire).
|
||||
* @param identity - the identity to send; defaults to {@link APP_IDENTITY} — omission cannot suppress attribution.
|
||||
* @returns headers to merge into the provider request (currently just `user-agent`).
|
||||
*/
|
||||
export function attributionHeaders(
|
||||
identity: AppIdentity = APP_IDENTITY,
|
||||
|
||||
@@ -17,7 +17,11 @@ import type { Branded } from '@deepseek-ai/dsh-brand'
|
||||
*/
|
||||
export type CallId = Branded<'CallId'>
|
||||
|
||||
/** Brand a string as a {@link CallId}. */
|
||||
/**
|
||||
* Brand a string as a {@link CallId}.
|
||||
* @param id - the provider-issued (or synthesized) call id.
|
||||
* @returns the same string, branded; no validation is performed.
|
||||
*/
|
||||
export function CallId(id: string): CallId {
|
||||
return id as CallId
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
* `ErrorOptions`. `name` defaults to the subclass constructor name.
|
||||
*/
|
||||
export class HarnessError extends Error {
|
||||
/** Stable machine-routable failure class (e.g. `RATE_LIMIT`); route on this, never by parsing `message`. */
|
||||
readonly code: string
|
||||
|
||||
constructor(message: string, code: string, options?: ErrorOptions) {
|
||||
@@ -27,7 +28,11 @@ export class HarnessError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
/** Narrow an arbitrary thrown value to a HarnessError (for `instanceof` at seams). */
|
||||
/**
|
||||
* Narrow an arbitrary thrown value to a HarnessError (for `instanceof` at seams).
|
||||
* @param value - the caught value (`unknown` in catch clauses).
|
||||
* @returns true only for real instances; duck-typed or cross-realm errors do not narrow.
|
||||
*/
|
||||
export function isHarnessError(value: unknown): value is HarnessError {
|
||||
return value instanceof HarnessError
|
||||
}
|
||||
|
||||
@@ -73,7 +73,11 @@ export class LlmError extends HarnessError {
|
||||
* same value to the wire.
|
||||
*/
|
||||
export abstract class LlmAdapter {
|
||||
/** Stream one model call as raw chunks. The only required method. */
|
||||
/**
|
||||
* Stream one model call as raw chunks. The only required method.
|
||||
* @param options - the fully-assembled request; implementations must honor `options.signal`.
|
||||
* @returns the chunk stream, obeying the adapter contract documented on `StreamChunk`.
|
||||
*/
|
||||
abstract stream(options: GenerateOptions): AsyncIterable<StreamChunk>
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,9 @@
|
||||
* variant was added without updating the switch (compile error at the call
|
||||
* site — the desired outcome) or a value escaped its type (runtime throw
|
||||
* with diagnostics — the safety net).
|
||||
* @param value - the impossible value; typed `never` so an unhandled variant fails compilation at the call site.
|
||||
* @param context - optional label (e.g. the switch site) prefixed into the throw message.
|
||||
* @returns never — it always throws, with the offending value JSON-rendered in the message.
|
||||
*/
|
||||
export function assertNever(value: never, context?: string): never {
|
||||
// JSON.stringify is typed string but returns undefined for undefined input;
|
||||
|
||||
@@ -70,7 +70,9 @@ export interface ContentBlockMap {
|
||||
'tool-result': ToolResultBlock
|
||||
}
|
||||
|
||||
/** The block `type` tag vocabulary; widens as plugins merge new shapes into {@link ContentBlockMap}. */
|
||||
export type ContentBlockType = keyof ContentBlockMap
|
||||
/** Any known content block, derived from {@link ContentBlockMap}; switch on `type` and fall through unknowns (merge-extensible). */
|
||||
export type ContentBlock = ContentBlockMap[ContentBlockType]
|
||||
|
||||
/** A single message in a conversation history. */
|
||||
@@ -88,6 +90,7 @@ export interface MessageSourceMap {
|
||||
plugin: { kind: 'plugin'; plugin: string }
|
||||
}
|
||||
|
||||
/** Any known message source, derived from {@link MessageSourceMap}; switch on `kind` and fall through unknowns (merge-extensible). */
|
||||
export type MessageSource = MessageSourceMap[keyof MessageSourceMap]
|
||||
|
||||
/**
|
||||
@@ -102,6 +105,7 @@ export interface FinishReasonMap {
|
||||
'error': { kind: 'error'; message: string; code?: string }
|
||||
}
|
||||
|
||||
/** Any known finish reason, derived from {@link FinishReasonMap}; switch on `kind` and fall through unknowns (merge-extensible). */
|
||||
export type FinishReason = FinishReasonMap[keyof FinishReasonMap]
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user