Use explicit ts specifiers for declarations

Restore explicit .ts relative specifiers in source and enable rewriteRelativeImportExtensions so emitted JS uses .js while declarations keep explicit .ts specifiers. Add a NodeNext declaration-consumer gate to prevent extensionless declaration regressions.
This commit is contained in:
Tianyi Cui
2026-06-22 06:11:00 +08:00
parent 94e7355449
commit 07f4047ff0
56 changed files with 323 additions and 147 deletions

View File

@@ -69,12 +69,13 @@ jobs:
run: pnpm run test:snapshot
# Before hygiene: publint validates the packed artifacts (lib/index.js),
# which only the tsdown bundling step emits.
# which only the tsdown bundling step emits, and verify-node-next-types
# validates the built declarations.
- name: Build (tsc -b + tsdown bundles)
run: pnpm run build
- name: Hygiene (knip + publint)
run: pnpm run knip && pnpm run publint
- name: Hygiene (knip + publint + constraints + NodeNext types)
run: pnpm run hygiene
- name: Demo smoke test
run: |

View File

@@ -145,7 +145,7 @@ pnpm run lint:fix # eslint . --fix
pnpm run build # tsc emits lib/types, then tsdown bundles runtime lib/index.*
pnpm run knip # dead-code / unused-dependency check
pnpm run publint # package.json publish-correctness check (every packages/*/* package)
pnpm run hygiene # knip + publint + workspace constraints
pnpm run hygiene # knip + publint + workspace constraints + NodeNext type-consumer check
pnpm run doc-typecheck # typecheck every ```ts block in README.md, docs/**/*.md,
# packages/*/*.md + packages/*/*/*.md (doc/code drift gate)
pnpm run gen-cordis-catalog # regenerate docs/cordis-catalog/events-and-services.md
@@ -160,6 +160,8 @@ pnpm run verify-package-paths # assert every packages/<path> cited in Markdown
pnpm run verify-rfc-classification # assert every RFC lives in a valid
# {lifecycle}/{class}/ folder and docs/rfc/README.md lists it
# under the matching heading (closed class set + index completeness)
pnpm run verify-node-next-types # assert built declarations typecheck for a
# standard external NodeNext ESM TypeScript consumer
pnpm run doc-sync # doc-typecheck + verify-cordis-catalog + verify-md-wrap + verify-md-links + verify-doc-refs + verify-package-paths + verify-rfc-classification + verify-type-equiv (CI runs this)
pnpm run demo:echo # run examples/echo-agent (no API key; type "echo hi" to
# see a tool call) — the mock skeleton
@@ -188,7 +190,7 @@ Dev/test/demo run **unbuilt** via tsx + the source `paths` map in the root `tsco
## Conventions
- **Package naming**: every npm package in this repo is `@deepseek-ai/dsh-<name>` (vendored packages keep their upstream names and are `private: true`).
- **ESM everywhere** (`"type": "module"`); imports between workspace packages use package names, never relative paths across package boundaries. In-package relative imports are extensionless so generated `.d.ts` files stay extensionless; `lib/types/**/*.js` is a bundler-only intermediate, not a Node ESM entrypoint.
- **ESM everywhere** (`"type": "module"`); imports between workspace packages use package names, never relative paths across package boundaries. In-package relative imports use explicit `.ts` extensions; `rewriteRelativeImportExtensions` turns those into `.js` in emitted JS, while declarations keep explicit `.ts` specifiers that NodeNext/Node16 TypeScript consumers can resolve to sibling `.d.ts` files. `lib/types/**/*.js` is a bundler-only intermediate, not a Node ESM entrypoint.
- **`cordis` is a peerDependency** (+ devDependency) of every harness package, mirroring upstream convention.
- **Registrations are effects**: anything a plugin contributes (adapter, tool, section, agent, event listener) goes through `ctx.effect()` / `ctx.on()` so disposal and HMR work. If you write a registry, `register()` must return the disposer.
- **Typed events via declaration merging**: services declare their events in `declare module 'cordis' { interface Events { … } }`, and their ctx key in `interface Context`. Extensible unions use the merge-extensible-map pattern (see `ContentBlockMap`, `MessageSourceMap`).

View File

@@ -17,6 +17,8 @@ packages/<name>/
package.json invariants (enforced by `pnpm run constraints` / `scripts/check-workspace-constraints.ts`): `private: true`, `version: 0.0.1`, `type: module`, `main: "lib/index.js"`, `types: "lib/types/index.d.ts"`, `exports["."].types: "./lib/types/index.d.ts"`, `exports["."].default: "./lib/index.js"`, `cordis` in BOTH peerDependencies and devDependencies (same range). Mirror every dsh peer dependency in devDependencies. `schemastery` goes in `dependencies` (it is a runtime validator), matching agent-loop. The `files` list is precise: `lib/index.js`, `lib/types/**/*.d.ts`, `lib/types/**/*.d.ts.map`, and `src`; do not publish `lib/types` JS or JS-map intermediates or stale root declaration files. CLI app packages with a package `bin` include `lib/bin.js` immediately after `lib/index.js` in `files`.
In-package relative imports use explicit `.ts` specifiers in source (for example, `export * from './types.ts'`). The compiler rewrites those to `.js` in emitted JS and leaves explicit `.ts` specifiers in declarations, which standard NodeNext/Node16 TypeScript consumers resolve to the sibling `.d.ts` files.
## 2. Register it in the root configs
| File | Change |

View File

@@ -29,6 +29,8 @@ vendor/<dir>/
`package.json` invariants: `"private": true` (vendored packages are never published), keep upstream's `name`/`version`/`exports`/`type`, point declaration metadata at `lib/types`, publish `.d.ts` and `.d.ts.map` declaration outputs, and list its cordis deps in `peerDependencies` (matching the upstream manifest). Transitive upstream deps must themselves be vendored or already present — vendoring one package often means vendoring its dependency tree (e.g. `@cordisjs/plugin-http` pulls `@cordisjs/fetch-file`).
Local relative imports/exports in vendored TypeScript source use explicit `.ts` specifiers after copying. This is a repo-local build-shape divergence from upstream: `rewriteRelativeImportExtensions` emits `.js` runtime imports while declarations keep explicit `.ts` specifiers that NodeNext/Node16 TypeScript consumers can resolve.
## 2. Register it in the root configs
| File | Change |

View File

@@ -39,7 +39,7 @@ If you are preparing to push from a fresh clone or worktree, also build once:
pnpm run build
```
`pnpm run hygiene` includes `publint`, which validates package entrypoints against the built `lib/*.js` files. A fresh worktree has no bundled JS until `pnpm run build` runs.
`pnpm run hygiene` includes `publint`, which validates package entrypoints against the built `lib/*.js` files, and `verify-node-next-types`, which validates built declarations against a temporary NodeNext consumer. A fresh worktree has no bundled JS or declarations until `pnpm run build` runs.
## Environment variables
@@ -101,7 +101,8 @@ pnpm run doc-sync # doc-typecheck, cordis-catalog freshness, markdown wrap
pnpm run gen-module-graph # regenerate docs/module-graph.md from package peerDeps
pnpm run verify-module-graph # fail if docs/module-graph.md is stale
pnpm run build # emit lib/types intermediates, then bundle lib/index.* runtime files
pnpm run hygiene # knip, publint, and workspace constraints
pnpm run verify-node-next-types # fail if built declarations are not NodeNext-consumable
pnpm run hygiene # knip, publint, workspace constraints, and NodeNext declaration check
```
When changing package public behavior, update the relevant README or JSDoc in the same change. `pnpm run doc-sync` catches checked TypeScript snippets, cordis events/services catalog drift, and hard-wrapped markdown prose, but broader prose/API sync still needs review.

View File

@@ -15,7 +15,7 @@ Every AGENTS.md promise gets a command that exits non-zero, wired into git hooks
- Max-strict TypeScript (`noUncheckedIndexedAccess`, `exactOptionalPropertyTypes`, …); examples, tests, and scripts typecheck in CI via the root no-emit `tsconfig.json` while package/vendor code stays behind its own project-reference boundary.
- ESLint strict-type-checked + @stylistic (the house style, enforced); vendored code excluded.
- Per-file 100% coverage on `packages/*/src` (v8); unreachable defensive guards carry `/* v8 ignore */ ` with stated reasons instead of deletion.
- knip (dead code/deps), publint (package correctness), workspace constraints (workspace rules: private, cordis peer+dev, uniform version, ESM).
- knip (dead code/deps), publint (package correctness), workspace constraints (workspace rules: private, cordis peer+dev, uniform version, ESM), and a NodeNext consumer typecheck for built package declarations.
- lefthook pre-commit (lint staged, typecheck, vendor-manifest guard) and pre-push (tests, hygiene); CI runs the full matrix on node 24/26 plus a demo smoke test driving the echo-agent end to end.
## Consequences

View File

@@ -17,7 +17,7 @@ Validation found several concrete technical issues and possible routes:
- `tsdown` uses `oxc` to transform TypeScript, which is not the same behavior as `tsc`.
- Bundled `.d.ts` emitted by `tsdown` conflicts with Cordis' internal relative module augmentation shape.
- The tsc output is affected by `allowImportingTsExtensions`, so we need to ensure that generated `.js` files do not import `.ts` files and generated `.d.ts` files do not import `.js` files. Therefore, we need to adjust the import specifiers to extensionless in the TypeScript source.
- The tsc output is affected by `allowImportingTsExtensions`, so we need to ensure that generated `.js` files do not import `.ts` files and generated `.d.ts` files do not contain extensionless relative imports. Therefore, in-package relative imports use explicit `.ts` specifiers in TypeScript source and `rewriteRelativeImportExtensions` rewrites those specifiers to `.js` in emitted JS.
- Bundled `.js` emitted by `tsdown` is not the same behavior as per-file `.js` emitted by `tsc -b`, such as decorator transform behavior.
- `vendor/*/src`, examples, tests, and scripts cannot all be plain-included in one root strict program.
- Directly typechecking `vendor/*/src` under the root strict config triggers many type errors outside this project's ownership.
@@ -26,7 +26,7 @@ Validation found several concrete technical issues and possible routes:
## Decision
In-package relative imports are extensionless.
In-package relative imports use explicit `.ts` specifiers.
`pnpm run build` is a two-stage build:
@@ -47,6 +47,9 @@ pnpm run build:
tsc -b tsconfig.build.json
tsdown
pnpm run verify-node-next-types:
tsx scripts/verify-node-next-types.ts
pnpm run typecheck:
tsc -b tsconfig.json
```
@@ -60,8 +63,10 @@ Build responsibilities are clearer:
- Each module under `packages/<group>/<pkg>` and `vendor/*` has one local tsconfig for build, typecheck, and tools that run source directly, such as `tsx` and `vitest`.
- The `build` command uses `tsconfig.build.json`. `tsc -b` owns the publishable per-module `.js` and `.d.ts` output, and the bundler owns only `lib/index.*`.
- `lib/types/*.d.ts` and `.d.ts.map` are the publish declaration output.
- `lib/types/*.d.ts` uses explicit `.ts` relative specifiers, which TypeScript's NodeNext/Node16 resolver maps to sibling `.d.ts` files.
- `lib/types/*.js` is only a bundler input and must not be used as a runtime entry or public import target.
- `lib/index.*` is the publish runtime output and is generated by the bundler, currently `tsdown`.
- `pnpm run verify-node-next-types` scans built declarations for extensionless relative specifiers, then typechecks a temporary external ESM consumer with `moduleResolution: "NodeNext"` against the built `types`/`exports` surface, so declaration specifier regressions fail before publish.
- The `typecheck` command uses `tsconfig.json`. Examples, tests, and scripts are checked by the root no-emit project, while packages and vendor modules keep the same emit behavior as `build`. Package and vendor source stays behind project-reference boundaries.
The Cordis vendor copy now has one more type-structure divergence from upstream. During upstream sync, that divergence must be reapplied or explicitly retired.

View File

@@ -31,13 +31,14 @@
"verify-package-paths": "tsx scripts/verify-package-paths.ts",
"verify-rfc-classification": "tsx scripts/verify-rfc-classification.ts",
"verify-type-equiv": "tsx scripts/verify-type-equiv.ts",
"verify-node-next-types": "tsx scripts/verify-node-next-types.ts",
"gen-cordis-catalog": "tsx scripts/gen-cordis-catalog.ts",
"verify-cordis-catalog": "tsx scripts/gen-cordis-catalog.ts --check",
"gen-module-graph": "tsx scripts/gen-module-graph.ts",
"verify-module-graph": "tsx scripts/gen-module-graph.ts --check",
"constraints": "tsx scripts/check-workspace-constraints.ts",
"doc-sync": "pnpm run doc-typecheck && pnpm run verify-cordis-catalog && pnpm run verify-md-wrap && pnpm run verify-md-links && pnpm run verify-doc-refs && pnpm run verify-package-paths && pnpm run verify-rfc-classification && pnpm run verify-type-equiv",
"hygiene": "pnpm run knip && pnpm run publint && pnpm run constraints",
"hygiene": "pnpm run knip && pnpm run publint && pnpm run constraints && pnpm run verify-node-next-types",
"demo:echo": "node --expose-internals --import tsx packages/ui/stdio-agent/src/bin.ts examples/echo-agent/cordis.yml",
"demo:coding": "node --expose-internals --import tsx packages/ui/stdio-agent/src/bin.ts examples/coding-agent/cordis.yml",
"demo:acp": "node --import tsx packages/ui/acp-agent/src/bin.ts examples/acp-agent/cordis.yml",

View File

@@ -79,5 +79,5 @@ Each package has its own `README.md` with purpose, service API, events, extensio
- **Declaration merging for events and ctx**: services declare their events in `declare module 'cordis' { interface Events { ... } }` and their ctx key in `interface Context`.
- **Waterfall semantics**: `ctx.waterfall` listeners receive `(...args, next)` and MUST call `next()` to delegate; returning without it short-circuits (the veto mechanism).
- **Extensible unions**: `ContentBlockMap`, `MessageSourceMap`, `FinishReasonMap`, `TurnTriggerMap`, `TurnEndReasonMap`, and `SessionEventMap` use the merge-extensible-map pattern so plugins can add variants via declaration merging.
- **ESM everywhere**; imports use package names across package boundaries and extensionless relative specifiers within a package.
- **ESM everywhere**; imports use package names across package boundaries and explicit `.ts` relative specifiers within a package.
- **Tests**: vitest, colocated under `packages/<group>/<pkg>/tests/*.spec.ts`. Every registry needs an HMR-safety test. Err on the side of more tests.

View File

@@ -17,11 +17,11 @@ import { Context } from 'cordis'
import z from 'schemastery'
import { BashExecutor, BashTaskId } from '@deepseek-ai/dsh-bash'
import type { BashExecRequest, BashExecSpec, BashRunResult, BashTask, BashTaskRead, OwnerToken } from '@deepseek-ai/dsh-bash'
import { runBash } from './run'
import type { RunInternals, RunningBash } from './run'
import { runBash } from './run.ts'
import type { RunInternals, RunningBash } from './run.ts'
export { DEFAULT_GRACE_MS, ENV_OVERRIDES, killGroup, OutputCollector, runBash } from './run'
export type { RunInternals, RunningBash, SpawnOutcome, SpawnSpec } from './run'
export { DEFAULT_GRACE_MS, ENV_OVERRIDES, killGroup, OutputCollector, runBash } from './run.ts'
export type { RunInternals, RunningBash, SpawnOutcome, SpawnSpec } from './run.ts'
/** Plugin config (all optional — `static Config` supplies the defaults). */
export interface Config {

View File

@@ -15,9 +15,9 @@
*/
import { Context, Service } from 'cordis'
import type { BashExecRequest, BashExecSpec, BashRunResult, BashTask, BashTaskId, BashTaskListener, BashTaskRead, OwnerToken } from './types'
import type { BashExecRequest, BashExecSpec, BashRunResult, BashTask, BashTaskId, BashTaskListener, BashTaskRead, OwnerToken } from './types.ts'
export { BashTaskId, OwnerToken } from './types'
export { BashTaskId, OwnerToken } from './types.ts'
export type {
BashExecRequest,
BashExecSpec,
@@ -27,7 +27,7 @@ export type {
BashTaskRead,
BashTaskStatus,
CollectedOutput,
} from './types'
} from './types.ts'
declare module 'cordis' {
interface Context {

View File

@@ -11,8 +11,8 @@ import type { AgentId, AgentOptions, AgentStatus, SendOptions } from '@deepseek-
import type { Agent } from '@deepseek-ai/dsh-agent'
import type { ContentBlock, MessageSource } from '@deepseek-ai/dsh-llm'
import type { Session } from '@deepseek-ai/dsh-session'
import { Inbox } from './inbox'
import { isTurnOpen, lastTurnNumber, runLoop } from './loop'
import { Inbox } from './inbox.ts'
import { isTurnOpen, lastTurnNumber, runLoop } from './loop.ts'
/**
* The concrete {@link Agent} implementation owned by the agent-loop plugin.

View File

@@ -17,11 +17,11 @@ import type { Session } from '@deepseek-ai/dsh-session'
import type {} from '@deepseek-ai/dsh-system-prompt'
import type {} from '@deepseek-ai/dsh-tools'
import type { SessionPersistence } from '@deepseek-ai/dsh-session-persistence'
import { ReactLoopAgent } from './agent'
import { ReactLoopAgent } from './agent.ts'
export { ReactLoopAgent } from './agent'
export { Inbox, type InboxMessage } from './inbox'
export { runLoop } from './loop'
export { ReactLoopAgent } from './agent.ts'
export { Inbox, type InboxMessage } from './inbox.ts'
export { runLoop } from './loop.ts'
declare module 'cordis' {
interface Context {

View File

@@ -13,7 +13,7 @@ import { BlockAssembler, HarnessError } from '@deepseek-ai/dsh-llm'
import type { Session, TurnEndReason, TurnTrigger } from '@deepseek-ai/dsh-session'
import { renderPrompt } from '@deepseek-ai/dsh-system-prompt'
import type {} from '@deepseek-ai/dsh-tools'
import type { ReactLoopAgent } from './agent'
import type { ReactLoopAgent } from './agent.ts'
/** An Error with an optional machine-readable code (e.g., from LlmError or a throwing plugin). */
type CodedError = Error & { code?: string }

View File

@@ -7,9 +7,9 @@
import { Context, Service } from 'cordis'
import type { SessionId } from '@deepseek-ai/dsh-session'
import type { Agent, AgentId, AgentOptions } from './types'
import type { Agent, AgentId, AgentOptions } from './types.ts'
export * from './types'
export * from './types.ts'
declare module 'cordis' {
interface Context {

View File

@@ -9,13 +9,13 @@
import { Context, Service } from 'cordis'
import { isAbsolute } from 'node:path'
import type { ContentBlock, Message, MessageSource } from '@deepseek-ai/dsh-llm'
import { SESSION_FORMAT_VERSION, SessionId } from './types'
import type { CreateSessionOptions, SessionEvent, SessionEventMap, SessionEventType, SessionHeader } from './types'
import { isJsonValue } from './json'
import { SESSION_FORMAT_VERSION, SessionId } from './types.ts'
import type { CreateSessionOptions, SessionEvent, SessionEventMap, SessionEventType, SessionHeader } from './types.ts'
import { isJsonValue } from './json.ts'
export * from './types'
export { isJsonValue } from './json'
export { interruptedTurnClosers } from './repair'
export * from './types.ts'
export { isJsonValue } from './json.ts'
export { interruptedTurnClosers } from './repair.ts'
declare module 'cordis' {
interface Context {

View File

@@ -36,7 +36,7 @@
*/
import type { CallId } from '@deepseek-ai/dsh-llm'
import type { SessionEvent } from './types'
import type { SessionEvent } from './types.ts'
/**
* Scan `events` for an open turn/step at the tail and return the synthetic

View File

@@ -24,7 +24,7 @@ export {
type InferArgs,
type DefineToolOptions,
type JsonSchemaObject,
} from './schema'
} from './schema.ts'
declare module 'cordis' {
interface Context {

View File

@@ -21,7 +21,7 @@
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import { assertNever, HarnessError } from '@deepseek-ai/dsh-llm'
import type { ToolCallPresentation, ToolDefinition, ToolExecution, ToolResult, ToolResultPresentation } from './index'
import type { ToolCallPresentation, ToolDefinition, ToolExecution, ToolResult, ToolResultPresentation } from './index.ts'
// ---------------------------------------------------------------------------
// SchemaSpec — the author-facing per-property type

View File

@@ -7,11 +7,11 @@
import { LlmAdapter, LlmError } from '@deepseek-ai/dsh-llm'
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
import { serializeRequest } from './serialize'
import type { RequestDefaults } from './serialize'
import { parseSse } from './sse'
import { translate } from './translate'
import type { WireError } from './types'
import { serializeRequest } from './serialize.ts'
import type { RequestDefaults } from './serialize.ts'
import { parseSse } from './sse.ts'
import { translate } from './translate.ts'
import type { WireError } from './types.ts'
export interface DeepSeekAdapterOptions {
apiKey: string

View File

@@ -21,15 +21,15 @@
import type { Context } from 'cordis'
import z from 'schemastery'
import type {} from '@deepseek-ai/dsh-llm'
import { DeepSeekAdapter } from './adapter'
import { DeepSeekAdapter } from './adapter.ts'
export { DeepSeekAdapter, httpErrorCode } from './adapter'
export type { DeepSeekAdapterOptions } from './adapter'
export { serializeMessages, serializeRequest } from './serialize'
export type { RequestDefaults } from './serialize'
export { DONE, parseSse } from './sse'
export { mapFinishReason, mapUsage, translate } from './translate'
export type * from './types'
export { DeepSeekAdapter, httpErrorCode } from './adapter.ts'
export type { DeepSeekAdapterOptions } from './adapter.ts'
export { serializeMessages, serializeRequest } from './serialize.ts'
export type { RequestDefaults } from './serialize.ts'
export { DONE, parseSse } from './sse.ts'
export { mapFinishReason, mapUsage, translate } from './translate.ts'
export type * from './types.ts'
export const name = 'llm-deepseek'
export const inject = ['llm']

View File

@@ -18,7 +18,7 @@
import { LlmError } from '@deepseek-ai/dsh-llm'
import type { ContentBlock, GenerateOptions, Message } from '@deepseek-ai/dsh-llm'
import type { WireMessage, WireRequest, WireTool } from './types'
import type { WireMessage, WireRequest, WireTool } from './types.ts'
/** Adapter-level request defaults (from plugin config). */
export interface RequestDefaults {

View File

@@ -16,8 +16,8 @@
import { CallId, LlmError } from '@deepseek-ai/dsh-llm'
import type { ContentBlock, FinishReason, StreamChunk, TokenUsage } from '@deepseek-ai/dsh-llm'
import { DONE } from './sse'
import type { WireChunk, WireUsage } from './types'
import { DONE } from './sse.ts'
import type { WireChunk, WireUsage } from './types.ts'
/** One open block under assembly. */
interface OpenBlock {

View File

@@ -16,7 +16,7 @@ import type { Model } from '@earendil-works/pi-ai'
import { LlmAdapter, LlmError } from '@deepseek-ai/dsh-llm'
import { CallId } from '@deepseek-ai/dsh-llm'
import type { GenerateOptions, StreamChunk, ToolSchema } from '@deepseek-ai/dsh-llm'
import { toPiContext, toStreamChunks } from './convert'
import { toPiContext, toStreamChunks } from './convert.ts'
/** Reasoning levels surfaced by this adapter (DeepSeek wire: high|max). */
export type PiAiReasoning = 'off' | 'high' | 'xhigh'

View File

@@ -19,12 +19,12 @@
import type { Context } from 'cordis'
import z from 'schemastery'
import type {} from '@deepseek-ai/dsh-llm'
import { PiAiAdapter } from './adapter'
import type { PiAiReasoning } from './adapter'
import { PiAiAdapter } from './adapter.ts'
import type { PiAiReasoning } from './adapter.ts'
export { buildModel, PiAiAdapter } from './adapter'
export type { PiAiAdapterOptions, PiAiReasoning } from './adapter'
export { mapStopReason, mapUsage, toPiContext, toStreamChunks } from './convert'
export { buildModel, PiAiAdapter } from './adapter.ts'
export type { PiAiAdapterOptions, PiAiReasoning } from './adapter.ts'
export { mapStopReason, mapUsage, toPiContext, toStreamChunks } from './convert.ts'
export const name = 'llm-pi-ai'
export const inject = ['llm']

View File

@@ -6,9 +6,9 @@
* @module @deepseek-ai/dsh-llm/assembler
*/
import { CallId } from './brand'
import { assertNever } from './never'
import type { ContentBlock, FinishReason, Message, StreamChunk, TokenUsage } from './types'
import { CallId } from './brand.ts'
import { assertNever } from './never.ts'
import type { ContentBlock, FinishReason, Message, StreamChunk, TokenUsage } from './types.ts'
interface PartialBlock {
blockType: string

View File

@@ -7,14 +7,14 @@
*/
import { Context, Service } from 'cordis'
import type { GenerateOptions, StreamChunk } from './types'
import { HarnessError } from './error'
import type { GenerateOptions, StreamChunk } from './types.ts'
import { HarnessError } from './error.ts'
export * from './brand'
export * from './never'
export * from './error'
export * from './types'
export { BlockAssembler } from './assembler'
export * from './brand.ts'
export * from './never.ts'
export * from './error.ts'
export * from './types.ts'
export { BlockAssembler } from './assembler.ts'
declare module 'cordis' {
interface Context {

View File

@@ -19,7 +19,7 @@
* ```
*/
import type { CallId } from './brand'
import type { CallId } from './brand.ts'
/** Cache hint attached to a content block (provider-interpreted). */
export type CacheHint = 'ephemeral'

View File

@@ -29,7 +29,7 @@ import {
import type { Session, SessionEvent, SessionId, SessionHeader } from '@deepseek-ai/dsh-session'
import {
encodeSegment, eventLine, logPath, parseHeaderMeta, scanLog, sessionDir, toHeaderLine,
} from './format'
} from './format.ts'
export interface Config {
/**

View File

@@ -29,9 +29,9 @@ import {
import type { Session, SessionEvent, SessionId, SessionHeader } from '@deepseek-ai/dsh-session'
import {
openDatabase, rowToMeta, scanRows, type EventRow, type SessionRow,
} from './schema'
} from './schema.ts'
export { SCHEMA_VERSION } from './schema'
export { SCHEMA_VERSION } from './schema.ts'
/** Plugin configuration. */
export interface Config {

View File

@@ -27,7 +27,7 @@
import { Context } from 'cordis'
import { interruptedTurnClosers, SESSION_FORMAT_VERSION } from '@deepseek-ai/dsh-session'
import type { Session, SessionEvent, SessionId, SessionHeader } from '@deepseek-ai/dsh-session'
import { assertSerializable, seedCoversPrefix } from './index'
import { assertSerializable, seedCoversPrefix } from './index.ts'
/**
* A stored session's durable prefix as read back from a backend: its

View File

@@ -29,8 +29,8 @@ import type { SessionEvent, SessionId, SessionHeader } from '@deepseek-ai/dsh-se
export type { SessionHeader } from '@deepseek-ai/dsh-session'
// The backend-agnostic write-path orchestration first-party backends compose.
export { PersistenceCoordinator } from './coordinator'
export type { PersistenceBackend, StoredPrefix } from './coordinator'
export { PersistenceCoordinator } from './coordinator.ts'
export type { PersistenceBackend, StoredPrefix } from './coordinator.ts'
declare module 'cordis' {
interface Context {

View File

@@ -74,7 +74,7 @@ import {
harnessBlockToAcpContent,
promptHasUnsupportedContent,
turnEndToStopReason,
} from './codec'
} from './codec.ts'
export const name = 'acp'
// The bridge programs against the interface packages only (architecture rule:

View File

@@ -0,0 +1,160 @@
/**
* Verify that built package declarations are consumable by a standard external
* TypeScript ESM project using NodeNext resolution.
*
* Run after `pnpm run build` has emitted declaration files under package
* `lib/types` directories.
*/
import { execFileSync } from 'node:child_process'
import { existsSync, globSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
import { dirname, resolve } from 'node:path'
const root = resolve(import.meta.dirname, '..')
interface ExportTarget {
types?: string
}
interface PackageManifest {
name?: string
types?: string
exports?: Record<string, ExportTarget | string | null>
}
interface WorkspacePackage {
dir: string
name: string
manifest: PackageManifest
}
function readPackage(path: string): WorkspacePackage | null {
const manifest = JSON.parse(readFileSync(path, 'utf8')) as PackageManifest
if (!manifest.name) return null
return { dir: dirname(path), name: manifest.name, manifest }
}
function workspacePackages(): WorkspacePackage[] {
return [
...globSync('vendor/*/package.json', { cwd: root }),
...globSync('packages/*/*/package.json', { cwd: root }),
]
.map(path => readPackage(resolve(root, path)))
.filter(pkg => pkg !== null)
.sort((a, b) => a.name.localeCompare(b.name))
}
const declarationSpecifierPattern = /(?:from\s*|import\s*\(\s*|import\s+|declare\s+module\s*)["'](\.{0,2}(?:\/[^"']*)?)["']/g
const hasExtension = /\.[^/.]+$/
function extensionlessRelativeSpecifiers(): string[] {
const errors: string[] = []
const files = [
...globSync('vendor/*/lib/types/**/*.d.ts', { cwd: root }),
...globSync('packages/*/*/lib/types/**/*.d.ts', { cwd: root }),
].sort()
for (const file of files) {
const text = readFileSync(resolve(root, file), 'utf8')
for (const match of text.matchAll(declarationSpecifierPattern)) {
const specifier = match[1]
if (!specifier) continue
const isRelative = specifier === '.' || specifier.startsWith('./') || specifier.startsWith('../')
if (isRelative && !hasExtension.test(specifier)) errors.push(`${file}: ${specifier}`)
}
}
return errors
}
function publicSpecifiers(pkg: WorkspacePackage): string[] {
const specifiers = new Set<string>()
if (pkg.manifest.types) specifiers.add(pkg.name)
for (const [key, target] of Object.entries(pkg.manifest.exports ?? {})) {
if (key.includes('*') || key === './package.json') continue
if (typeof target !== 'object' || target === null || !target.types) continue
specifiers.add(key === '.' ? pkg.name : `${pkg.name}/${key.slice(2)}`)
}
return [...specifiers].sort()
}
function linkPackage(pkg: WorkspacePackage, nodeModules: string): void {
const parts = pkg.name.split('/')
const link = resolve(nodeModules, ...parts)
mkdirSync(dirname(link), { recursive: true })
symlinkSync(pkg.dir, link, 'dir')
}
const packages = workspacePackages()
const badSpecifiers = extensionlessRelativeSpecifiers()
if (badSpecifiers.length > 0) {
console.error('verify-node-next-types: declaration files still contain extensionless relative specifiers.')
console.error(badSpecifiers.join('\n'))
process.exit(1)
}
const missingOutputs = packages
.filter(pkg => pkg.manifest.types && !existsSync(resolve(pkg.dir, pkg.manifest.types)))
.map(pkg => `${pkg.name}: missing ${pkg.manifest.types}`)
if (missingOutputs.length > 0) {
console.error('verify-node-next-types: build outputs are missing; run `pnpm run build` first.')
console.error(missingOutputs.join('\n'))
process.exit(1)
}
const tmp = mkdtempSync(resolve(root, '.node-next-types-'))
let failed = false
try {
const nodeModules = resolve(tmp, 'node_modules')
mkdirSync(nodeModules, { recursive: true })
for (const pkg of packages) linkPackage(pkg, nodeModules)
const rootTypes = resolve(root, 'node_modules/@types/node')
if (existsSync(rootTypes)) {
const typesDir = resolve(nodeModules, '@types')
mkdirSync(typesDir, { recursive: true })
symlinkSync(rootTypes, resolve(typesDir, 'node'), 'dir')
}
writeFileSync(resolve(tmp, 'package.json'), `${JSON.stringify({ type: 'module', private: true }, null, 2)}\n`)
writeFileSync(resolve(tmp, 'tsconfig.json'), `${JSON.stringify({
compilerOptions: {
target: 'es2024',
module: 'NodeNext',
moduleResolution: 'NodeNext',
strict: true,
// Third-party SDK declarations can have their own lib-check noise under a
// symlinked temp install. The explicit scan above owns our regression:
// extensionless relative specifiers in built declarations.
skipLibCheck: true,
preserveSymlinks: true,
noEmit: true,
types: ['node'],
},
include: ['index.ts'],
}, null, 2)}\n`)
const imports = packages.flatMap(publicSpecifiers)
.map((specifier, index) => `import * as mod${index} from ${JSON.stringify(specifier)};\nvoid mod${index};`)
.join('\n')
writeFileSync(resolve(tmp, 'index.ts'), `${imports}\n`)
execFileSync(resolve(root, 'node_modules/.bin/tsc'), ['-p', resolve(tmp, 'tsconfig.json'), '--pretty', 'false'], {
cwd: root,
stdio: 'pipe',
})
console.log(`verify-node-next-types: ${packages.length} workspace package declaration surface(s) compile under NodeNext.`)
} catch (error: unknown) {
failed = true
const output = error as { stdout?: Buffer; stderr?: Buffer }
console.error('verify-node-next-types: NodeNext consumer typecheck failed.\n')
console.error(`${output.stdout?.toString() ?? ''}${output.stderr?.toString() ?? ''}`)
} finally {
rmSync(tmp, { recursive: true, force: true })
}
if (failed) process.exit(1)

View File

@@ -10,6 +10,8 @@
"incremental": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowImportingTsExtensions": true,
"rewriteRelativeImportExtensions": true,
"verbatimModuleSyntax": false,
"strict": true,
"noUncheckedIndexedAccess": true,

2
vendor/README.md vendored
View File

@@ -33,7 +33,7 @@ Keep this log exhaustive — every divergence from upstream must be listed.
1. **`hmr/src/index.ts`**: removed the `./locales/en-US.yml` / `./locales/zh-CN.yml` imports, the `.i18n({...})` call on the `Config` schema, and the `src/locales/` directory. Rationale: those imports require a runtime YAML loader hook (`@cordisjs/unyaml`) that we do not vendor; the i18n texts only localize config descriptions.
2. **All `package.json` files**: regenerated — added `private: true`, added precise `files` entries for bundled runtime files and `lib/types/**/*.d.ts` / `.d.ts.map`, preserved `src` in `files` only for packages whose previous file list already shipped it, added a `./src/*` export where missing, pointed declaration metadata at `lib/types`, and removed upstream `devDependencies`/`scripts`/`repository` fields. Dependency and peer-dependency ranges preserved, except `hmr` declares `esbuild` as a direct dev dependency because its source imports the `BuildFailure` type and pnpm's strict workspace resolution requires the owner package to name that dependency.
3. **All `tsconfig.json` files**: regenerated to extend the repo-root `tsconfig.base.json`, emit TypeScript intermediates to `lib/types`, and declare project references.
4. **Vendored TypeScript source internal specifiers**: changed local relative imports/exports from explicit `.ts` / `.js` specifiers to extensionless specifiers so generated `.js` and `.d.ts` intermediates are extensionless and no declaration postprocess is needed. This includes `loader/src/config/isolate.ts` changing `declare module './entry.ts'` to `declare module './entry'`.
4. **Vendored TypeScript source internal specifiers**: changed local relative imports/exports from upstream's specifier shape to explicit `.ts` specifiers so TypeScript rewrites emitted JS to `.js` while declarations keep explicit, NodeNext-safe `.ts` specifiers. This includes `loader/src/config/isolate.ts` using `declare module './entry.ts'`.
5. **`schemastery/tsdown.config.ts` and `logger-console/tsdown.config.ts`**: ours, not upstream files — per-package build-shape overrides (dual ESM+CJS output; separate node/browser entries) for the repo-root tsdown build. They read the JS emitted under `lib/types` and then write the publish runtime entries under `lib/`. Like the regenerated tsconfigs, they are not part of the upstream sync surface.
## Sync procedure

View File

@@ -1,10 +1,10 @@
import { Dict } from 'cosmokit'
import { EventsService } from './events'
import { LoggerService } from './logger'
import { ReflectService } from './reflect'
import { InjectKey, RegistryService } from './registry'
import { getTraceable, symbols } from './utils'
import { Fiber } from './fiber'
import { EventsService } from './events.ts'
import { LoggerService } from './logger.ts'
import { ReflectService } from './reflect.ts'
import { InjectKey, RegistryService } from './registry.ts'
import { getTraceable, symbols } from './utils.ts'
import { Fiber } from './fiber.ts'
/**
* Public shape of a Cordis context.

View File

@@ -1,7 +1,7 @@
import { defineProperty, Promisify } from 'cosmokit'
import { Context } from './context'
import { Fiber, FiberState } from './fiber'
import { DisposableList, symbols } from './utils'
import { Context } from './context.ts'
import { Fiber, FiberState } from './fiber.ts'
import { DisposableList, symbols } from './utils.ts'
/** Return whether an event result should stop a bail-style dispatch. */
export function isBailed(value: any) {
@@ -25,7 +25,7 @@ export type ThisType<F> = F extends (this: infer T, ...args: any) => any ? T : n
*/
export type DispatchMode = 'emit' | 'parallel' | 'serial' | 'bail' | 'waterfall'
declare module './context' {
declare module './context.ts' {
export interface Context {
/* eslint-disable max-len */
parallel<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): Promise<void>

View File

@@ -1,11 +1,11 @@
import { Awaitable, defineProperty, Dict, isNullable } from 'cosmokit'
import { Context } from './context'
import { Plugin } from './registry'
import { buildOuterStack, composeError, DisposableList, getTraceable, isConstructor, isObject, symbols } from './utils'
import { Impl } from './reflect'
import { Context } from './context.ts'
import { Plugin } from './registry.ts'
import { buildOuterStack, composeError, DisposableList, getTraceable, isConstructor, isObject, symbols } from './utils.ts'
import { Impl } from './reflect.ts'
import { StandardSchemaV1 } from '@standard-schema/spec'
declare module './context' {
declare module './context.ts' {
export interface Context extends Pick<Fiber, 'effect'> {
fiber: Fiber
}

View File

@@ -1,14 +1,14 @@
/** Core context type and root context implementation. */
export * from './context'
export * from './context.ts'
/** Event bus, dispatch modes, and event augmentation types. */
export * from './events'
export * from './events.ts'
/** Plugin fiber lifecycle, effects, and config validation helpers. */
export * from './fiber'
export * from './fiber.ts'
/** Logger facade, logger service, message, exporter, and formatting types. */
export * from './logger'
export * from './logger.ts'
/** Plugin registry, dependency injection, and plugin entrypoint types. */
export * from './registry'
export * from './registry.ts'
/** Base service class and service lifecycle symbols. */
export * from './service'
export * from './service.ts'
/** Shared internal helpers used by context, services, and plugin fibers. */
export * from './utils'
export * from './utils.ts'

View File

@@ -1,9 +1,9 @@
import { defineProperty, hyphenate } from 'cosmokit'
import { Context } from './context'
import { Fiber } from './fiber'
import { createCallable, joinPrototype, symbols, Tracker } from './utils'
import { Context } from './context.ts'
import { Fiber } from './fiber.ts'
import { createCallable, joinPrototype, symbols, Tracker } from './utils.ts'
declare module './context' {
declare module './context.ts' {
interface Intercept {
logger: LoggerService.Intercept
}

View File

@@ -1,9 +1,9 @@
import { defineProperty, Dict, isNullable } from 'cosmokit'
import { Context } from './context'
import { getTraceable, symbols, withProps } from './utils'
import { Fiber, FiberState } from './fiber'
import { Context } from './context.ts'
import { getTraceable, symbols, withProps } from './utils.ts'
import { Fiber, FiberState } from './fiber.ts'
declare module './context' {
declare module './context.ts' {
interface Context {
get<K extends string & keyof this>(name: K, strict?: boolean): undefined | this[K]
get(name: string, strict?: boolean): any

View File

@@ -1,8 +1,8 @@
import { defineProperty, Dict } from 'cosmokit'
import { StandardSchemaV1 } from '@standard-schema/spec'
import { Context } from './context'
import { Fiber } from './fiber'
import { buildOuterStack, DisposableList, symbols, withProps } from './utils'
import { Context } from './context.ts'
import { Fiber } from './fiber.ts'
import { buildOuterStack, DisposableList, symbols, withProps } from './utils.ts'
function isApplicable(object: Plugin) {
return object && typeof object === 'object' && typeof object.apply === 'function'
@@ -140,7 +140,7 @@ type GetPluginConfig<P> =
? S
: GetPluginParameters<P>[0]
declare module './context' {
declare module './context.ts' {
export interface Context {
inject(deps: Inject, callback: Plugin.Function<void>): Fiber & PromiseLike<Fiber>
plugin<P extends Plugin>(plugin: P, ...args: Spread<GetPluginConfig<P>>): Fiber & PromiseLike<Fiber>

View File

@@ -1,6 +1,6 @@
import { defineProperty } from 'cosmokit'
import { Context } from './context'
import { createCallable, joinPrototype, symbols, Tracker } from './utils'
import { Context } from './context.ts'
import { createCallable, joinPrototype, symbols, Tracker } from './utils.ts'
/**
* Base class for services that expose a named API on `ctx`.

View File

@@ -1,5 +1,5 @@
import { defineProperty } from 'cosmokit'
import type { Context, Service } from '.'
import type { Context, Service } from './index.ts'
/** Ordered collection of disposable values with O(1) deletion by value. */
export class DisposableList<T extends WeakKey> {

View File

@@ -1,4 +1,4 @@
import { isNullable } from './misc'
import { isNullable } from './misc.ts'
/** Return true when every item in `array2` is present in `array1`. */
export function contain(array1: readonly any[], array2: readonly any[]) {

View File

@@ -1,10 +1,10 @@
/** Array set and normalization helpers. */
export * from './array'
export * from './array.ts'
/** Runtime type, binary, clone, and equality helpers. */
export * from './types'
export * from './types.ts'
/** Shared utility types and object helpers. */
export * from './misc'
export * from './misc.ts'
/** String case, path, and property formatting helpers. */
export * from './string'
export * from './string.ts'
/** Time constants, parsing, and formatting helpers. */
export * from './time'
export * from './time.ts'

View File

@@ -1,4 +1,4 @@
import { isNullable } from './misc'
import { isNullable } from './misc.ts'
type GlobalConstructorNames = keyof {
[K in keyof typeof globalThis as typeof globalThis[K] extends abstract new (...args: any) => any ? K : never]: K

View File

@@ -4,7 +4,7 @@ import { ModuleJob, ModuleLoader, ResolveResult } from '@cordisjs/plugin-loader'
import type { Include } from '@cordisjs/plugin-include'
import { ChokidarOptions, FSWatcher, watch } from 'chokidar'
import { relative, resolve } from 'node:path'
import { handleError } from './error'
import { handleError } from './error.ts'
import type {} from '@cordisjs/plugin-timer'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { createRequire } from 'node:module'

View File

@@ -1,9 +1,9 @@
import { Context, Fiber, Inject } from 'cordis'
import { deepEqual, isNullable } from 'cosmokit'
import { Loader } from '../index'
import { EntryGroup } from './group'
import { EntryTree } from './tree'
import { evaluate, interpolate } from './utils'
import { Loader } from '../index.ts'
import { EntryGroup } from './group.ts'
import { EntryTree } from './tree.ts'
import { evaluate, interpolate } from './utils.ts'
/** Serialized plugin entry options stored in loader config files. */
export interface EntryOptions {

View File

@@ -1,6 +1,6 @@
import { Context, Service } from 'cordis'
import { Entry, EntryOptions } from './entry'
import { EntryTree } from './tree'
import { Entry, EntryOptions } from './entry.ts'
import { EntryTree } from './tree.ts'
/** Runtime owner for a list of child loader entries. */
export class EntryGroup {

View File

@@ -1,8 +1,8 @@
import { Context } from 'cordis'
import { Dict } from 'cosmokit'
import { Entry } from './entry'
import { Entry } from './entry.ts'
declare module './entry' {
declare module './entry.ts' {
interface EntryOptions {
intercept?: Dict | null
isolate?: Dict<true | string> | null

View File

@@ -1,7 +1,7 @@
import { composeError, Context } from 'cordis'
import { Dict, isNonNullable } from 'cosmokit'
import { Entry, EntryOptions } from './entry'
import { EntryGroup } from './group'
import { Entry, EntryOptions } from './entry.ts'
import { EntryGroup } from './group.ts'
/** Mutable tree of loader entries. Persistence is supplied by subclasses. */
export abstract class EntryTree {

View File

@@ -1,22 +1,22 @@
import { Context, Inject, Service } from 'cordis'
import { defineProperty, Dict, isNullable } from 'cosmokit'
import { ModuleLoader } from './internal'
import { Entry, EntryOptions } from './config/entry'
import isolate from './config/isolate'
import { EntryTree } from './config/tree'
import { ModuleLoader } from './internal.ts'
import { Entry, EntryOptions } from './config/entry.ts'
import isolate from './config/isolate.ts'
import { EntryTree } from './config/tree.ts'
/** Re-export entry node APIs. */
export * from './config/entry'
export * from './config/entry.ts'
/** Re-export nested entry group APIs. */
export * from './config/group'
export * from './config/group.ts'
/** Re-export service isolation helpers. */
export * from './config/isolate'
export * from './config/isolate.ts'
/** Re-export entry tree persistence APIs. */
export * from './config/tree'
export * from './config/tree.ts'
/** Re-export loader config expression helpers. */
export * from './config/utils'
export * from './config/utils.ts'
/** Re-export Node internal module loader compatibility types. */
export * from './internal'
export * from './internal.ts'
declare module 'cordis' {
interface Events {

View File

@@ -1,8 +1,8 @@
import { Message } from 'cordis'
import { ConsoleExporter as Base } from './shared'
import { ConsoleExporter as Base } from './shared.ts'
/** Re-export shared console exporter config and base implementation. */
export * from './shared'
export * from './shared.ts'
/** Browser console exporter that dispatches to native console methods. */
export class ConsoleExporter extends Base {

View File

@@ -1,10 +1,10 @@
import { Formatter } from 'cordis'
import { inspect } from 'node:util'
import supportsColor from 'supports-color'
import { ConsoleExporter as Base } from './shared'
import { ConsoleExporter as Base } from './shared.ts'
/** Re-export shared console exporter config and base implementation. */
export * from './shared'
export * from './shared.ts'
const inspectFormatter: Formatter = (value, target) => {
return inspect(value, { colors: !!target.colors, depth: Infinity, compact: true, breakLength: Infinity })