diff --git a/.agents/notes/implemented/architecture/2026-06-20-package-hierarchy.md b/.agents/notes/implemented/architecture/2026-06-20-package-hierarchy.md index 85c62b7e17..1ba3b94e8b 100644 --- a/.agents/notes/implemented/architecture/2026-06-20-package-hierarchy.md +++ b/.agents/notes/implemented/architecture/2026-06-20-package-hierarchy.md @@ -53,9 +53,9 @@ packages/ The package list had been enumerated in five places. The uniform depth-2 layout lets most of them be derived instead: -- `tsconfig.base.json` maps every package through a single `@deepseek-ai/dsh-*` `paths` wildcard listing one candidate per group, in place of per-package entries. Root `tsconfig.json` reuses that source map and carries the explicit project references that keep package/vendor typecheck boundaries intact. (One subtlety this introduced: a path candidate contains `/*/`, which a naive regex comment-stripper mistakes for a block comment — `scripts/doc-typecheck.ts` reads the JSONC config through TypeScript's parser rather than stripping comments by hand for exactly this reason.) +- `tsconfig.base.json` maps every package through a single `@deepseek-ai/dsh-*` `paths` wildcard listing one candidate per group, in place of per-package entries. The aggregate configs (`tsconfig.host.json`, `tsconfig.client.json`) reuse that source map and carry the explicit project references that keep package/vendor typecheck boundaries intact. (One subtlety this introduced: a path candidate contains `/*/`, which a naive regex comment-stripper mistakes for a block comment — `scripts/doc-typecheck.ts` reads the JSONC config through TypeScript's parser rather than stripping comments by hand for exactly this reason.) - `scripts/publint-all.ts` derives its list by reading the hierarchy (`packages//`), resolving the `TODO(package-inventory)`. -- `tsconfig.build.json`'s project `references` stay an explicit list — TypeScript project references have no wildcard form. Generating these from a manifest is left to a follow-up (see [discover package inventories](../../proposed/process/2026-06-20-discover-package-inventory.md)). +- The aggregates' project `references` stay explicit lists — TypeScript project references have no wildcard form. Generating these from a manifest is left to a follow-up (see [discover package inventories](../../proposed/process/2026-06-20-discover-package-inventory.md)). ### Guardrails added diff --git a/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.i18n.yaml index d3e5608c22..c2f039b148 100644 --- a/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -2026-07-19-gui-layering-and-rpc-protocol.md: ebe21a6060ec69ba9807ab9fbf9906ae24b07823 -2026-07-19-gui-layering-and-rpc-protocol.zh.md: 0c256b60ce44a8e16ec6edfba146c776c4ae2129 +2026-07-19-gui-layering-and-rpc-protocol.md: 65fb01f44698c61e6bf6958e332e1854fbb77fa9 +2026-07-19-gui-layering-and-rpc-protocol.zh.md: e8b15789846ea124fb6a90f2afef437184d4348a diff --git a/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md b/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md index ebe21a6060..65fb01f446 100644 --- a/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md +++ b/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md @@ -53,7 +53,7 @@ Direction discipline (every rule auditable from package deps): - `webserver` does not depend on `runtime`: it provides a `{ fetch }`-shaped implementation — "webserver ← runtime" is a runtime injection relationship, not a package dependency. - Cross-package client imports use the `/client` subpath for plugin packages (a bare package name would inline a second runtime instance into a browser bundle; the tsdown purity gate rewrites or rejects it). -TypeScript checks in **two aggregate programs** (`tsconfig.json` = host side + tests, excluding `packages/client`; `tsconfig.client.json` = client packages and their tests): both sides merge the cordis `Context` interface under the same keys (`sessions`, `loader`) with different services, so one program would see both declaration merges and report a collision. Shared leaves (session/llm/tools/apiproxy…) build once and are referenced by both programs. +TypeScript checks in **two aggregate programs** referenced by a solution root (`tsconfig.json` = solution; `tsconfig.host.json` = host side + tests, excluding `packages/client`; `tsconfig.client.json` = client packages and their tests): both sides merge the cordis `Context` interface under the same keys (`sessions`, `loader`) with different services, so one program would see both declaration merges and report a collision. Shared leaves (session/llm/tools/apiproxy…) build once and are referenced by both programs ([topology](../process/2026-07-22-tsconfig-solution-root-two-aggregates.md)). On the protocol side: TS interfaces (`packages/host/apiproxy/src/api/`, zero Node dependencies, browser-importable); wire messages unify under a **bidirectional model** — each logical message is shaped by "who initiates × request/response" (two axes, four cells, called the four quadrants below), decoupled from the physical channel; clients all inherit `AbstractApiClient` (protocol invariants live entirely in the base class, platform differences are just the `doFetch` transport aspect). diff --git a/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.zh.md b/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.zh.md index 0c256b60ce..e8b1578984 100644 --- a/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.zh.md @@ -51,7 +51,7 @@ harness core packages ──────────────────┘ - `webserver` 不依赖 `runtime`:它提供 `{ fetch }` 特定实现 ——「webserver ← runtime」只是运行时注入关系,不是包依赖。 - client 侧跨包 import 插件包一律走 `/client` 子路径(裸包名会把第二份运行时实例内联进浏览器 bundle;tsdown 纯度门禁会改写或拒收)。 -TypeScript 以**两个聚合 program** 检查(`tsconfig.json` = host 侧 + 测试,排除 `packages/client`;`tsconfig.client.json` = client 各包及其测试):两侧在相同键(`sessions`、`loader`)下以不同服务合并 cordis `Context` 接口,单一 program 会同时看到两份声明合并而报冲突。共享叶子包(session/llm/tools/apiproxy 等)只构建一次,由两个 program 共同引用。 +TypeScript 以 solution 根引用的**两个聚合 program** 检查(`tsconfig.json` = solution;`tsconfig.host.json` = host 侧 + 测试,排除 `packages/client`;`tsconfig.client.json` = client 各包及其测试):两侧在相同键(`sessions`、`loader`)下以不同服务合并 cordis `Context` 接口,单一 program 会同时看到两份声明合并而报冲突。共享叶子包(session/llm/tools/apiproxy 等)只构建一次,由两个 program 共同引用([拓扑](../process/2026-07-22-tsconfig-solution-root-two-aggregates.md))。 协议侧:TS interface(`packages/host/apiproxy/src/api/`,零 Node 依赖,浏览器可 import);wire 消息统一为**双向模型**——每条逻辑消息由「谁发起 × request/response」定形(两轴四格,后文称四象限),与物理通道解耦;客户端统一继承 `AbstractApiClient`(协议不变量全在基类,平台差异只是 `doFetch` 传输切面)。 diff --git a/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.i18n.yaml index b3b5462ecf..1868da83b3 100644 --- a/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -2026-07-19-gui-web-client-architecture.md: f704a00c3b38f5a3a62d66cb37b121f0f21770df -2026-07-19-gui-web-client-architecture.zh.md: 3aa8fdbe82102d8c920a9f77635320c432d8b95d +2026-07-19-gui-web-client-architecture.md: f21b840493b83c02d7abc3ba1c1bf90635166ec1 +2026-07-19-gui-web-client-architecture.zh.md: a50dc556cfc96b6d35feea6ef2b1aadae9f31c44 diff --git a/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md b/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md index f704a00c3b..f21b840493 100644 --- a/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md +++ b/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md @@ -39,7 +39,7 @@ The loading chain, end to end: **The dual-instance ban**: a module-table package inlined into a plugin bundle would duplicate runtime identity (two React copies, two store registries — the root cause of an actual white-screen P0). The tsdown client preset enforces purity at build time: a bare-name import of a module-table package must resolve external (rewritten to its `/client` form where applicable), and any other workspace leak that is not an inline-safe wire/type layer fails the build (`packages/client/tsdown.client.ts`, pinned by `scripts/client-bundle-purity.spec.ts`). -Dev equals prod: plugins rebuild under `tsdown --watch`, refresh reloads the same chain; vite serves only the shell (`apps/web`). Type universes stay split at the aggregate level — the root `tsconfig.json` is the host program, `tsconfig.client.json` the client program, because both sides merge cordis `Context` under the same keys (`sessions`, `loader`) with different services; client packages consume the wire vocabulary through pure type subpaths (`@deepseek-ai/dsh-session/types` and kin) so no host augmentation rides into the client program. +Dev equals prod: plugins rebuild under `tsdown --watch`, refresh reloads the same chain; vite serves only the shell (`apps/web`). Type universes stay split at the aggregate level — `tsconfig.host.json` is the host program and `tsconfig.client.json` the client program, both referenced by the solution root `tsconfig.json` — because both sides merge cordis `Context` under the same keys (`sessions`, `loader`) with different services; client packages consume the wire vocabulary through pure type subpaths (`@deepseek-ai/dsh-session/types` and kin) so no host augmentation rides into the client program. ## The slot system: how the page composes diff --git a/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.zh.md b/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.zh.md index 3aa8fdbe82..a50dc556cf 100644 --- a/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.zh.md @@ -39,7 +39,7 @@ Status: implemented **双实例禁令**:模块表包若被内联进插件 bundle,会复制运行时身份(两份 React、两套 store 注册表——一次真实白屏 P0 的根因)。tsdown client 预设在构建期把守纯度:模块表包的裸名 import 必须解析为 external(适用时改写为其 `/client` 形态),其余任何非 inline 安全 wire/类型层的 workspace 泄漏都令构建大声失败(`packages/client/tsdown.client.ts`,由 `scripts/client-bundle-purity.spec.ts` 钉住)。 -dev 与 prod 同链:插件在 `tsdown --watch` 下重编译,刷新即重走同一条链;vite 只管壳(`apps/web`)。类型宇宙在聚合层拆分——根 `tsconfig.json` 是 host program,`tsconfig.client.json` 是 client program,因为两侧都在相同键(`sessions`、`loader`)上对 cordis `Context` 做声明合并且服务不同;client 包经纯类型子路径(`@deepseek-ai/dsh-session/types` 等)消费协议词汇,host 侧的声明合并不会搭车进入 client program。 +dev 与 prod 同链:插件在 `tsdown --watch` 下重编译,刷新即重走同一条链;vite 只管壳(`apps/web`)。类型宇宙在聚合层拆分——`tsconfig.host.json` 是 host program、`tsconfig.client.json` 是 client program,二者由 solution 根 `tsconfig.json` 引用,因为两侧都在相同键(`sessions`、`loader`)上对 cordis `Context` 做声明合并且服务不同;client 包经纯类型子路径(`@deepseek-ai/dsh-session/types` 等)消费协议词汇,host 侧的声明合并不会搭车进入 client program。 ## slot 体系:页面怎么拼 diff --git a/.agents/notes/implemented/process/2026-06-11-tsdown-over-dumble.md b/.agents/notes/implemented/process/2026-06-11-tsdown-over-dumble.md index 4075d4738a..0629acabb4 100644 --- a/.agents/notes/implemented/process/2026-06-11-tsdown-over-dumble.md +++ b/.agents/notes/implemented/process/2026-06-11-tsdown-over-dumble.md @@ -15,7 +15,7 @@ Replace dumble with **tsdown** (rolldown-based, ~2.5M downloads/week, VoidZero-b - Root `tsdown.config.ts` with `workspace: ['vendor/*', 'packages/*/*']` (explicit globs keep bundling to vendored Cordis and the TypeScript package tree; `workspace: true` would also discover example manifests and non-bundled workspace members). - Shared shape: entry `lib/types/index.js`, `outDir: 'lib'`, ESM, `platform: node`, `target: es2024`, `fixedExtension: false` (keeps `.js` for `"type": "module"` packages), `dts: false` (tsc -b owns declarations), `clean: false` (lib/ also holds TSC's `lib/types` intermediate tree). The entry was originally `src/index.ts`; the [TSC-first build Agent Note](2026-06-17-ts-build-config.md) later moved tsdown to bundling TSC-emitted JS so TypeScript transform behavior comes from one compiler. - Two per-package overrides in vendor/ (ours, like the regenerated tsconfigs; logged in vendor/README.md): schemastery (dual `.mjs`/`.cjs` via `outExtensions`), logger-console (two single-entry passes so the shared base class is inlined into each entry instead of a hash-named chunk, matching upstream's published shape). -- `scripts/build.ts` deleted; `pnpm run build` = `tsc -b tsconfig.build.json && tsdown`. +- `scripts/build.ts` deleted; `pnpm run build` = `tsc -b && tsdown` (the root solution owns the emit graph). ## Alternatives considered diff --git a/.agents/notes/implemented/process/2026-06-17-ts-build-config.md b/.agents/notes/implemented/process/2026-06-17-ts-build-config.md index 8f67b6fc2f..529400b8d7 100644 --- a/.agents/notes/implemented/process/2026-06-17-ts-build-config.md +++ b/.agents/notes/implemented/process/2026-06-17-ts-build-config.md @@ -1,7 +1,9 @@ -# Agent Note: TSC-first build and one tsconfig +# Agent Note: TSC-first build and one compiler ownership Status: implemented +> Root project topology (which tsconfig owns which graph) has since moved to a solution root over two aggregate programs; see the [solution-root note](2026-07-22-tsconfig-solution-root-two-aggregates.md). The tsc-first pipeline decided here is unchanged. + ## Problem The current TypeScript build and typecheck setup had these issues: @@ -28,29 +30,29 @@ In-package relative imports use explicit `.ts` specifiers. `pnpm run build` is a two-stage build: -- Stage 1: `tsc -b tsconfig.build.json` emits per-module `.js`, declarations `.d.ts`, JS sourcemaps `.js.map`, and declaration sourcemaps `.d.ts.map` into each package's `lib/types`. This is the authoritative TypeScript compilation result. For publish we keep `.d.ts` / `.d.ts.map` and ignore `.js` / `.js.map`. - - The build project uses the project-reference graph that `tsc -b` compiles. For example, root `tsconfig.build.json` references package and vendor tsconfigs. It validates and emits package/vendor build results. +- Stage 1: `tsc -b` over the root solution emits per-module `.js`, declarations `.d.ts`, JS sourcemaps `.js.map`, and declaration sourcemaps `.d.ts.map` into each package's `lib/types`. This is the authoritative TypeScript compilation result. For publish we keep `.d.ts` / `.d.ts.map` and ignore `.js` / `.js.map`. + - The graph is the project-reference graph reachable from the root solution `tsconfig.json` through the two aggregates ([topology](2026-07-22-tsconfig-solution-root-two-aggregates.md)). It validates and emits package/vendor build results. - Stage 2: a bundler reads the emitted JS under `lib/types` and writes the bundled runtime entry as `lib/index.js` or `lib/index.mjs` (follow current behavior). This stage is bundling only. It must not read TypeScript source or emit declarations. `tsdown` is no longer the owner of TypeScript compilation or declaration output. -`pnpm run typecheck` runs build mode over the root `tsconfig.json`. -- The root `tsconfig.json` is the single development/typecheck project. It typechecks examples, tests, and scripts with `noEmit`, and validates package/vendor source through references. -- Referenced package/vendor projects keep the same emit behavior as build, so typecheck can refresh their `lib/types` outputs instead of using a separate no-emit graph. Project-specific strictness changes live in the owning `packages/*/*/tsconfig.json` or `vendor/*/tsconfig.json`. -- The root no-emit project disables `rewriteRelativeImportExtensions`; it emits nothing and includes tests that import helpers across project-reference boundaries. Package/vendor emit projects keep the rewrite enabled. +`pnpm run typecheck` runs the same `tsc -b` graph. +- The aggregates (`tsconfig.host.json`, `tsconfig.client.json`) typecheck examples, tests, and scripts with `noEmit`, and validate package/vendor source through references. +- Referenced package/vendor projects keep the same emit behavior as build, so typecheck refreshes their `lib/types` outputs instead of using a separate no-emit graph. Project-specific strictness changes live in the owning `packages/*/*/tsconfig.json` or `vendor/*/tsconfig.json`. +- The no-emit aggregates disable `rewriteRelativeImportExtensions`; they emit nothing and include tests that import helpers across project-reference boundaries. Package/vendor emit projects keep the rewrite enabled. The command orchestration shape is: ```sh pnpm run build: -tsc -b tsconfig.build.json +tsc -b tsdown pnpm run verify-node-next-types: tsx scripts/verify-node-next-types.ts pnpm run typecheck: -tsc -b tsconfig.json +tsc -b ``` `pnpm run demo:*` still runs `src` directly through tsx and root paths, without a compile step. @@ -65,7 +67,7 @@ tsc -b tsconfig.json Build responsibilities are clearer: - Each module under `packages//` 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.*`. +- The `build` command drives the root solution graph. `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. diff --git a/.agents/notes/implemented/process/2026-07-22-cordis-tutorial-docs.i18n.yaml b/.agents/notes/implemented/process/2026-07-22-cordis-tutorial-docs.i18n.yaml new file mode 100644 index 0000000000..b2aa854cf3 --- /dev/null +++ b/.agents/notes/implemented/process/2026-07-22-cordis-tutorial-docs.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write +2026-07-22-cordis-tutorial-docs.md: 45abb8f524218ce0b2678606ae62c7bf2dbca00b +2026-07-22-cordis-tutorial-docs.zh.md: cd1a62e2a6e7f2e28bc32109dd67746840f8b8b7 diff --git a/.agents/notes/implemented/process/2026-07-22-cordis-tutorial-docs.md b/.agents/notes/implemented/process/2026-07-22-cordis-tutorial-docs.md new file mode 100644 index 0000000000..45abb8f524 --- /dev/null +++ b/.agents/notes/implemented/process/2026-07-22-cordis-tutorial-docs.md @@ -0,0 +1,32 @@ +# Agent Note: Tutorial-style Cordis docs under docs/cordis-tutorial + +Status: implemented + +English | [中文](2026-07-22-cordis-tutorial-docs.zh.md) + +## Problem + +The repo documents Cordis at two levels: the condensed [cordis-primer](../../../../docs/cordis-primer.md) states the concepts, and the `docs/user/develop/` pages teach harness plugin authoring against harness services. Neither serves a developer meeting Cordis itself for the first time: the primer assumes the reader already writes plugins, and the develop pages jump straight to `defineTool` without showing how contexts, fibers, services, and dispatch actually behave. There was no path where a reader runs bare Cordis, watches a fiber go PENDING, or sees a waterfall veto happen. + +## Decision + +`docs/cordis-tutorial/` holds a seven-chapter hands-on tutorial (first plugin → lifecycle/effects → services → events → config → composition/HMR → harness tool). Its properties, in decreasing order of load-bearing-ness: + +- **Every transcript is real.** Each chapter's files run in the gitignored `tmp/cordis-tutorial/` scratch directory via `node --import tsx ../../vendor/cordis/bin.js`, and the shown output is what those commands print. The chapter that uses harness packages (`@deepseek-ai/dsh-tools` and `@deepseek-ai/dsh-llm`) runs keylessly. +- **dsh-flavored, not pure Cordis**: later chapters use real harness services and events (`ctx.tools`, `tools/result`) so the tutorial lands the reader inside this repo's actual composition model, per the requesting user's choice. +- **English-only, published to both website locales** through `mirroredPages()` in [website/docs.ts](../../../../website/docs.ts) under a `Cordis 教程` / `Cordis tutorial` section of the develop sidebar — the same pattern as the reference pages, so a Chinese pair can ratchet in later without route changes. +- Code fences compile under `doc-typecheck` except the two fences that import scratch-relative files (`./stats.ts`) or intentionally throw, which carry `ignore-check`. + +## Alternatives considered + +**Under `docs/user/develop/` as paired product docs.** That tier requires en+zh+i18n records in the same PR, roughly doubling the change and coupling every future tutorial edit to a translation. Rejected for the first landing; the mirrored projection keeps the same public visibility. + +**Pure-Cordis tutorial with no harness packages.** Cleaner as framework documentation, but the audience is agent developers extending this harness; ending at `ctx.tools.execute` and `tools/result` teaches the composition they will actually work in. The user chose this explicitly. + +**Extending the primer instead of a new directory.** The primer is a 600-word budgeted concept reference; a multi-chapter walkthrough inside it would break its tier's job (and its budget) rather than complement it. + +## Consequences + +- A runnable introduction to Cordis exercises the loader, fiber states, effects, service injection, all five dispatch-mode contracts, Schemastery validation, and HMR. It demonstrates PENDING dependencies and validation failure; it explains the loader's logged unresolved-entry failure because that boot-time log may not reach a console exporter. +- The tutorial's transcripts pin behavior informally but are not snapshot-gated; if loader or HMR behavior changes, the transcripts drift until a human replays the chapters. The compile gate covers only the code fences. +- The chapters name concrete harness APIs (`ctx.tools.execute`, `CallId`, `tools/result`); renames must update the tutorial like any other doc reference (`verify-md-links` catches file moves, not API prose). diff --git a/.agents/notes/implemented/process/2026-07-22-cordis-tutorial-docs.zh.md b/.agents/notes/implemented/process/2026-07-22-cordis-tutorial-docs.zh.md new file mode 100644 index 0000000000..cd1a62e2a6 --- /dev/null +++ b/.agents/notes/implemented/process/2026-07-22-cordis-tutorial-docs.zh.md @@ -0,0 +1,32 @@ +# Agent Note: `docs/cordis-tutorial` 下的 Cordis 实操教程文档 + +Status: implemented + +[English](2026-07-22-cordis-tutorial-docs.md) | 中文 + +## 问题 + +本仓库从两个层面介绍 Cordis:精简的 [cordis-primer](../../../../docs/cordis-primer.md) 阐述概念,`docs/user/develop/` 下的页面则讲解如何基于 harness 服务编写 harness 插件。但二者都不适合初次接触 Cordis 的开发者:primer 假定读者已经会编写插件,开发页面则直接从 `defineTool` 讲起,没有展示上下文、fiber、服务和 dispatch 的实际行为。此前没有一条学习路径让读者运行原生 Cordis、观察 fiber 进入 PENDING 状态,或看到 waterfall(瀑布式事件)否决实际发生。 + +## 决策 + +`docs/cordis-tutorial/` 包含一套七章实操教程(第一个插件 → 生命周期与 effect → 服务 → 事件 → 配置 → 组合与 HMR(热模块替换)→ harness 工具)。以下是教程的特性,按重要性从高到低排列: + +- **每段 transcript(文本记录)都真实可复现。** 每章文件都通过 `node --import tsx ../../vendor/cordis/bin.js` 在 git 忽略的 `tmp/cordis-tutorial/` 临时目录中运行,展示的输出就是这些命令实际打印的内容。使用 harness 包(package)(`@deepseek-ai/dsh-tools` 和 `@deepseek-ai/dsh-llm`)的章节无需密钥即可运行。 +- **采用 dsh 风格,而非纯 Cordis**:后续章节使用真实的 harness 服务和事件(`ctx.tools`、`tools/result`),使读者最终进入本仓库实际采用的组合模型,这遵循了提出请求的用户所作的选择。 +- **仅提供英文版,但发布到网站的两个语言区域**:通过 [website/docs.ts](../../../../website/docs.ts) 中的 `mirroredPages()`,发布到开发侧边栏的 `Cordis 教程` / `Cordis tutorial` 分区。该方式与参考页面采用的模式相同,因此日后可以逐步纳入中文配对,而无需更改路由。 +- 除两个围栏代码块外,其余代码块均通过 `doc-typecheck` 编译;这两个例外分别导入临时目录中的相对路径文件(`./stats.ts`)或有意抛出异常,因此标有 `ignore-check`。 + +## 考虑过的替代方案 + +**作为双语产品文档放在 `docs/user/develop/` 下。** 该层级要求在同一个 PR(Pull Request)中同时提供英文、中文和 i18n 记录,这会使变更量大致翻倍,并要求未来每次修改教程时都同步翻译。首次落地不采用此方案;镜像投影仍可保持同等的公开可见性。 + +**不使用任何 harness 包的纯 Cordis 教程。** 作为框架文档会更简洁,但目标读者是扩展此 harness 的 agent(智能体)开发者;以 `ctx.tools.execute` 和 `tools/result` 收尾,能讲清他们实际使用的组合方式。用户明确选择了此方案。 + +**扩充 primer,而非新建目录。** primer 是一份预算上限为 600 词的精简概念参考;在其中加入多章演练会破坏该文档层级的职责及其篇幅预算,而非形成补充。 + +## 结果 + +- 现在有了一份可运行的 Cordis 入门教程,涵盖 loader、fiber 状态、effect、服务注入、全部五种 dispatch 模式的契约、Schemastery 校验和 HMR。教程实际展示了依赖处于 PENDING 状态和配置校验失败;对于 loader 记录的配置项解析失败,教程只作说明,因为启动阶段的日志可能无法到达控制台导出器。 +- 教程中的 transcript 以非正式方式固定了行为,但没有快照门禁;如果 loader 或 HMR 的行为发生变化,transcript 会逐渐偏离实际结果,直到有人重新运行各章。编译门禁只覆盖围栏代码块。 +- 各章写明了具体的 harness API(`ctx.tools.execute`、`CallId`、`tools/result`);这些 API 重命名时,必须像更新其他文档引用一样同步修改教程(`verify-md-links` 能发现文件移动,但无法发现 API 文字引用变化)。 diff --git a/.agents/notes/implemented/process/2026-07-22-tsconfig-solution-root-two-aggregates.i18n.yaml b/.agents/notes/implemented/process/2026-07-22-tsconfig-solution-root-two-aggregates.i18n.yaml new file mode 100644 index 0000000000..d191386725 --- /dev/null +++ b/.agents/notes/implemented/process/2026-07-22-tsconfig-solution-root-two-aggregates.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# side as of the last confirmed-consistent state. Both languages carry equal authority; +# after editing either side, bring the other along and re-record with: +# pnpm run verify-translation-pairing --write +2026-07-22-tsconfig-solution-root-two-aggregates.md: 19c229693b98ff3825caf935fa647ab85aff0f56 +2026-07-22-tsconfig-solution-root-two-aggregates.zh.md: becc43de1ef2f6a53b0f6c2285eb64d9b42604f1 diff --git a/.agents/notes/implemented/process/2026-07-22-tsconfig-solution-root-two-aggregates.md b/.agents/notes/implemented/process/2026-07-22-tsconfig-solution-root-two-aggregates.md new file mode 100644 index 0000000000..19c229693b --- /dev/null +++ b/.agents/notes/implemented/process/2026-07-22-tsconfig-solution-root-two-aggregates.md @@ -0,0 +1,45 @@ +# Agent Note: Solution root over two aggregate programs + +Status: implemented + +English | [中文](2026-07-22-tsconfig-solution-root-two-aggregates.zh.md) + +## Problem + +The GUI split introduced a second aggregate program (`tsconfig.client.json`, [layering RFC](../architecture/2026-07-19-gui-layering-and-rpc-protocol.md)) while the root `tsconfig.json` kept doubling as the host aggregate, and `tsconfig.build.json` remained a third, hand-maintained full emit graph. That triple bookkeeping produced four concrete asymmetries: + +- The typecheck and build references lists drifted apart (`packages/goal/command-goal` was in the typecheck graph but missing from the build graph). +- The lefthook pre-push hook ran `tsc -b tsconfig.json` only, so client-side type breakage passed the local checkpoint and surfaced in CI. +- tsserver discovers only configs named `tsconfig.json`, so client test files sat on no discoverable config chain and fell back to inferred projects (no paths, wrong lib/jsx). +- The vitest configs pointed at three different resolution sources (`tsconfig.vitest.json`, the root config, and one hand-written alias). + +## Decision + +One solution root, two check units, one shared base pair, no separate build or vitest config: + +| File | Role | Forms a program? | +|---|---|---| +| `tsconfig.json` | Solution root: `extends` base, `files: []`, two references; the whole-repo `tsc -b tsconfig.json` graph, the tsserver entry, and the nearest config for get-tsconfig consumers (tsx running `examples/`, `scripts/`, doc fences) whose bare workspace imports resolve through the inherited `paths` | No | +| `tsconfig.base.json` | Shared compilerOptions and the source `paths` map; doubles as the resolution facade for vite-tsconfig-paths (no `include`, so it applies to every importer) | No | +| `tsconfig.base.client.json` | Browser compiler shape (`jsx: react-jsx`, DOM libs, `types: []`) shared by the client aggregate and every `packages/client/*` package | No | +| `tsconfig.host.json` | The former root aggregate, moved verbatim: host packages, examples, tests, scripts, website; excludes `packages/client` | Yes | +| `tsconfig.client.json` | Client packages and their tests; extends `tsconfig.base.client.json` | Yes | + +The load-bearing principle: **cordis `Context` declaration-merge collisions exist only inside a `ts.Program`, never in module resolution.** A solution file forms no program, so referencing both aggregates from one root cannot collide the merges; vite-tsconfig-paths reads only `paths` and `include` and discards types, so one facade may span both sides. The only way to explode is to flatten both sides into a single program — hence two derived disciplines: `tsconfig.base.json` never gains `include`/`files` (it would leak into every extending package and narrow the facade), and every repo-wide `ts.Program` consumer (`scripts/ts-project.ts`, doc-typecheck standalone mode) seeds `tsconfig.host.json` or `tsconfig.client.json` explicitly, never the root solution. Program-backed generators and semantic gates intentionally stay host-only; the client side gets program-backed gates only when a real need arrives. + +Commands collapse to one graph and keep the config name explicit: `typecheck` = `tsc -b tsconfig.json`, `build` = `tsc -b tsconfig.json && tsdown`, lefthook pre-push stays `tsc -b tsconfig.json --pretty false` unchanged (the same line now covers both sides through the solution). `tsconfig.build.json` and `tsconfig.vitest.json` are deleted; all vitest configs point vite-tsconfig-paths at `tsconfig.base.json`. + +The solution root `extends` the base deliberately: `examples/` and `scripts/` have no nearer tsconfig, so tsx (get-tsconfig) resolves their workspace imports through the root file. `extends` restores the `paths` map there while `files: []` keeps the file program-less. Their *type checking* is unaffected by this: examples, scripts, and website files are included by the host aggregate. + +## Alternatives considered + +- **Rename `tsconfig.build.json` to `tsconfig.host.json`** — rejected: the build graph was the full emit graph including all client packages, not a host graph; the name that fits the former root aggregate is `tsconfig.host.json`, and the build graph itself is subsumed by the solution. +- **Point vitest at the root solution** — rejected: a solution has neither `paths` nor `include`, so resolution would become a function of how far the plugin walks references, and the client aggregate's include (tests only, no src) would leave transitive src→src imports unmapped, falling through to `exports` and loading a second copy of module singletons. +- **Keep `tsconfig.vitest.json` as a dedicated facade** — retained only as the fallback if vite-tsconfig-paths mishandles an include-less config; the base file already carries the paths map, and an include-less config applies everywhere, which is strictly wider than the facade's hand-kept include list. + +## Consequences + +- `docs/development.md#typescript-project-layout` is the authoritative description; root `AGENTS.md` carries the two disciplines as conventions. +- The [ts-build-config note](2026-06-17-ts-build-config.md) keeps ownership of the tsc-first build pipeline (tsc emits, tsdown bundles, `.ts` specifiers with `rewriteRelativeImportExtensions`); its former "one root typecheck project" shape is superseded by this note. +- Adding a package registers it in exactly one aggregate's references (host packages in `tsconfig.host.json`, client packages in `tsconfig.client.json`); the build graph needs no separate registration. +- The build gate depends on the typecheck gate: both now drive the same `tsc -b` graph, so running them concurrently would race the same `.tsbuildinfo` files. diff --git a/.agents/notes/implemented/process/2026-07-22-tsconfig-solution-root-two-aggregates.zh.md b/.agents/notes/implemented/process/2026-07-22-tsconfig-solution-root-two-aggregates.zh.md new file mode 100644 index 0000000000..becc43de1e --- /dev/null +++ b/.agents/notes/implemented/process/2026-07-22-tsconfig-solution-root-two-aggregates.zh.md @@ -0,0 +1,45 @@ +# Agent Note: 以 solution 根文件统辖两个聚合 program + +Status: implemented + +[English](2026-07-22-tsconfig-solution-root-two-aggregates.md) | 中文 + +## 问题 + +GUI 拆分引入了第二个聚合 program(`tsconfig.client.json`,见[分层 RFC](../architecture/2026-07-19-gui-layering-and-rpc-protocol.md)),根 `tsconfig.json` 则继续兼任宿主侧聚合,`tsconfig.build.json` 还是第三份手工维护的全量 emit 图。三处账本并行,造成四个具体的不对称: + +- 类型检查与构建的 references 列表逐渐脱节(`packages/goal/command-goal` 在类型检查图里,构建图里却没有)。 +- lefthook 的 pre-push 钩子只运行 `tsc -b tsconfig.json`,客户端侧的类型破坏因此通过本地检查点,直到 CI 才暴露。 +- tsserver 只发现名为 `tsconfig.json` 的配置,客户端测试文件不在任何可发现的配置链上,回落到推断项目(inferred project),既没有 paths,lib/jsx 也不对。 +- 各 vitest 配置指向三个不同的解析来源(`tsconfig.vitest.json`、根配置,外加一处手写别名)。 + +## 决策 + +一个 solution 根文件,两个检查单元,一对共享 base,不再单设 build 或 vitest 配置: + +| 文件 | 角色 | 是否构成 program? | +|---|---|---| +| `tsconfig.json` | solution 根文件:`extends` base、`files: []`、两条 references;同时是全仓 `tsc -b tsconfig.json` 图、tsserver 入口,以及 get-tsconfig 消费方(tsx 运行 `examples/`、`scripts/`、文档围栏代码块)就近命中的配置,其裸 workspace 导入经继承来的 `paths` 解析 | 否 | +| `tsconfig.base.json` | 共享 compilerOptions 与源码 `paths` 映射;兼任 vite-tsconfig-paths 的解析门面(不含 `include`,因此对每个导入方都生效) | 否 | +| `tsconfig.base.client.json` | 浏览器侧编译形态(`jsx: react-jsx`、DOM lib、`types: []`),由客户端聚合与每个 `packages/client/*` 包共享 | 否 | +| `tsconfig.host.json` | 原根聚合原样迁入:宿主各包、examples、测试、scripts、website;排除 `packages/client` | 是 | +| `tsconfig.client.json` | 客户端各包及其测试;通过 `extends` 继承 `tsconfig.base.client.json` | 是 | + +整个方案立足的原则:**cordis `Context` 的声明合并冲突只存在于同一个 `ts.Program` 内部,从不发生在模块解析中。** solution 文件不构成 program,因此从一个根文件同时引用两个聚合不会让两侧的声明合并相撞;vite-tsconfig-paths 只读取 `paths` 与 `include`、丢弃全部类型信息,因此一个门面可以横跨两侧。唯一会爆炸的做法是把两侧压平进同一个 program,由此推出两条派生纪律:`tsconfig.base.json` 永远不得添加 `include`/`files`(否则会泄漏进每个继承它的包,并收窄门面范围);每个全仓级 `ts.Program` 消费方(`scripts/ts-project.ts`、doc-typecheck 独立模式)都显式以 `tsconfig.host.json` 或 `tsconfig.client.json` 为种子,绝不使用根 solution。基于 program 的生成器与语义门禁有意只留在宿主侧;客户端侧只有在真实需求出现时才引入基于 program 的门禁。 + +各命令收敛到一张图,且显式写出配置名:`typecheck` = `tsc -b tsconfig.json`,`build` = `tsc -b tsconfig.json && tsdown`,lefthook pre-push 保持 `tsc -b tsconfig.json --pretty false` 不变(经由 solution,这同一行命令现已覆盖两侧)。`tsconfig.build.json` 与 `tsconfig.vitest.json` 删除;所有 vitest 配置都把 vite-tsconfig-paths 指向 `tsconfig.base.json`。 + +solution 根文件刻意 `extends` base:`examples/` 与 `scripts/` 没有更近的 tsconfig,tsx(get-tsconfig)通过根文件解析它们的 workspace 导入。`extends` 把 `paths` 映射带回根文件,`files: []` 则让它始终不构成 program。这不影响两者的*类型检查*:examples、scripts 与 website 的文件由宿主聚合纳入。 + +## 考虑过的替代方案 + +- **把 `tsconfig.build.json` 改名为 `tsconfig.host.json`**——不予采纳:构建图是包含全部客户端包的全量 emit 图,不是宿主图;`tsconfig.host.json` 这个名字对应的是原根聚合,而构建图本身已被 solution 吸收。 +- **让 vitest 指向根 solution**——不予采纳:solution 既没有 `paths` 也没有 `include`,解析结果将取决于插件沿 references 走多远;且客户端聚合的 include 只收测试、不收 src,传递的 src→src 导入会失去映射,回落到 `exports`,加载出模块单例的第二份副本。 +- **保留 `tsconfig.vitest.json` 作为专用门面**——仅保留为后备方案:若 vite-tsconfig-paths 处理不了无 include 的配置再启用;base 文件已经携带 paths 映射,而无 include 的配置处处生效,严格宽于该门面手工维护的 include 列表。 + +## 后果 + +- `docs/development.md#typescript-project-layout` 是权威描述;根 `AGENTS.md` 以约定形式收录上述两条纪律。 +- [ts-build-config Agent Note](2026-06-17-ts-build-config.md) 继续拥有 tsc 先行的构建流水线(tsc 负责输出,tsdown 负责打包,`.ts` 说明符配合 `rewriteRelativeImportExtensions`);其原先「单一根类型检查项目」的形态由本文取代。 +- 新增一个包只登记进恰好一个聚合的 references(宿主包进 `tsconfig.host.json`,客户端包进 `tsconfig.client.json`);构建图无需另行登记。 +- 构建门禁依赖类型检查门禁:两者现在驱动同一张 `tsc -b` 图,并发运行会在同一批 `.tsbuildinfo` 文件上竞态。 diff --git a/.agents/notes/proposed/process/2026-06-20-discover-package-inventory.md b/.agents/notes/proposed/process/2026-06-20-discover-package-inventory.md index fa544d6ddb..f9286aeefd 100644 --- a/.agents/notes/proposed/process/2026-06-20-discover-package-inventory.md +++ b/.agents/notes/proposed/process/2026-06-20-discover-package-inventory.md @@ -6,13 +6,13 @@ Status: proposed Package and gate inventories are repeated across TypeScript project references, package docs, CI prose, and Knip overrides. Most restate package layout, manifest data, or aggregate command contents. Each new package therefore creates avoidable synchronization points. -The [package hierarchy](../../implemented/architecture/2026-06-20-package-hierarchy.md) already removed several of these by hand: `scripts/publint-all.ts` now derives its list from the `packages//` layout, and the two `tsconfig` `paths` maps collapsed to one `@deepseek-ai/dsh-*` wildcard. What remains is the inventory that cannot be globbed away — chiefly `tsconfig.build.json`'s project `references`, which TypeScript requires as an explicit array (no wildcard form). +The [package hierarchy](../../implemented/architecture/2026-06-20-package-hierarchy.md) already removed several of these by hand: `scripts/publint-all.ts` now derives its list from the `packages//` layout, and the two `tsconfig` `paths` maps collapsed to one `@deepseek-ai/dsh-*` wildcard. What remains is the inventory that cannot be globbed away — chiefly the aggregate configs' (`tsconfig.host.json`, `tsconfig.client.json`) project `references`, which TypeScript requires as explicit arrays (no wildcard form). Static lists are appropriate when they encode policy; they are needless friction when they duplicate manifest data or layout facts that already exist in `package.json`, workspace globs, or the package hierarchy. ## Proposal -Make the remaining package/gate inventories discoverable. A single canonical source — the `packages//` hierarchy plus package manifests — should drive `tsconfig.build.json`'s `references`, the module graph, and any other full-package list, with a generate-and-verify step (the existing `gen-module-graph` / `gen-cordis-catalog` pattern: a generator writes the artifact, a `--check` mode in `hygiene`/`doc-sync` fails on a stale committed copy). Module graph generation already reads package manifests. `doc-sync` should be the one command that defines and prints its sub-gates, with docs linking to that command rather than restating a second list. +Make the remaining package/gate inventories discoverable. A single canonical source — the `packages//` hierarchy plus package manifests — should drive the aggregates' `references`, the module graph, and any other full-package list, with a generate-and-verify step (the existing `gen-module-graph` / `gen-cordis-catalog` pattern: a generator writes the artifact, a `--check` mode in `hygiene`/`doc-sync` fails on a stale committed copy). Module graph generation already reads package manifests. `doc-sync` should be the one command that defines and prints its sub-gates, with docs linking to that command rather than restating a second list. The hierarchy does not need to encode every fact about a package, but it should encode the broad maintenance policy: core/product packages, integrations, capability seams, and support/test/example packages should not all require a hand-maintained exception list before scripts can tell them apart. @@ -20,7 +20,7 @@ One cataloged item needs no generator at all: folding the e2e entry glob into kn ## Acceptance criteria -- `tsconfig.build.json` project `references` are generated from the hierarchy (a generator emits them; a `--check` gate fails when the committed copy is stale), rather than hand-maintained. +- Aggregate-config project `references` are generated from the hierarchy (a generator emits them; a `--check` gate fails when the committed copy is stale), rather than hand-maintained. - Adding a package does not require editing a static package list for any gate. - Docs describe the source of truth rather than repeating generated inventories. - CI invokes the aggregate commands and lets those commands own their sub-gate lists. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f52274cddd..2c6036968f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,7 +58,7 @@ jobs: - uses: actions/cache/restore@v4 with: path: .cache/eslint - key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'tsconfig.json', 'packages/*/*/tsconfig.json', 'examples/*/tsconfig.json') }} + key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'tsconfig.json', 'tsconfig.base.json', 'tsconfig.base.client.json', 'tsconfig.host.json', 'tsconfig.client.json', 'packages/*/*/tsconfig.json', 'examples/*/tsconfig.json') }} restore-keys: | ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full- @@ -173,7 +173,7 @@ jobs: - uses: actions/cache/restore@v4 with: path: .cache/eslint - key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'tsconfig.json', 'packages/*/*/tsconfig.json', 'examples/*/tsconfig.json') }} + key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'tsconfig.json', 'tsconfig.base.json', 'tsconfig.base.client.json', 'tsconfig.host.json', 'tsconfig.client.json', 'packages/*/*/tsconfig.json', 'examples/*/tsconfig.json') }} restore-keys: | ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full- @@ -237,7 +237,7 @@ jobs: - uses: actions/cache@v4 with: path: .cache/eslint - key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'tsconfig.json', 'packages/*/*/tsconfig.json', 'examples/*/tsconfig.json') }} + key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'tsconfig.json', 'tsconfig.base.json', 'tsconfig.base.client.json', 'tsconfig.host.json', 'tsconfig.client.json', 'packages/*/*/tsconfig.json', 'examples/*/tsconfig.json') }} restore-keys: | ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full- @@ -309,7 +309,7 @@ jobs: - uses: actions/cache@v4 with: path: .cache/eslint - key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'tsconfig.json', 'packages/*/*/tsconfig.json', 'examples/*/tsconfig.json') }} + key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'tsconfig.json', 'tsconfig.base.json', 'tsconfig.base.client.json', 'tsconfig.host.json', 'tsconfig.client.json', 'packages/*/*/tsconfig.json', 'examples/*/tsconfig.json') }} restore-keys: | ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full- @@ -525,7 +525,7 @@ jobs: if: matrix.platform == 'linux' with: path: .cache/eslint - key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'tsconfig.json', 'packages/*/*/tsconfig.json', 'examples/*/tsconfig.json') }} + key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full-${{ hashFiles('pnpm-lock.yaml', 'eslint.config.mjs', 'tsconfig.json', 'tsconfig.base.json', 'tsconfig.base.client.json', 'tsconfig.host.json', 'tsconfig.client.json', 'packages/*/*/tsconfig.json', 'examples/*/tsconfig.json') }} restore-keys: | ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-eslint-full- diff --git a/AGENTS.md b/AGENTS.md index 8dcdc059f4..8007f32b90 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -100,6 +100,8 @@ Real-API tests and demos read `DEEPSEEK_API_KEY`, optional `DEEPSEEK_BASE_URL`, - **Misconfiguration fails loud** at load when self-contained, otherwise at the earliest resolvable point; never silently skip a missing referent. - **Opaque cross-boundary ids are branded** (`Branded` from `dsh-brand`), never bare `string`. - **Trust TypeScript at typed same-process seams.** Do not add runtime validation, fallback behavior, or hostile-input tests solely for values the static interface requires; validate at parser/config, queued, model/tool JSON, durable/file, worker, process, and wire boundaries. +- **Source plane vs artifact plane, never mixed.** Static gates and tests resolve workspace imports through tsconfig `paths` to `src` and pass on a clean tree; gates consuming built `lib/` declare that dependency ([layout](docs/development.md#typescript-project-layout)). +- **`ts.Program` consumers seed `tsconfig.host.json` or `tsconfig.client.json`, never the root solution** — one program holding both sides collides the cordis `Context` merges ([layout](docs/development.md#typescript-project-layout)). - **An empty `catch` names what it swallows** and why nothing else can reach it; keep the `try` to one statement. - **Prefer symmetry for parallel values**; unexplained asymmetry usually signals a missed extraction. - **Tests describe behavior, not correctness.** Change obsolete behavior with its tests; explain why in the PR. diff --git a/README.i18n.yaml b/README.i18n.yaml index 020593bf20..41a7a06404 100644 --- a/README.i18n.yaml +++ b/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -README.md: 8b34d6177834e1c410b2c3ecaf32154de42520b4 -README.zh.md: cf029cc0bb8c5aa527d14753803eac4c28ab9de7 +README.md: 27774fd3e0ffc821e7287f6153906a5d21530dc2 +README.zh.md: b7d08f2bc948d0a6d388dd672c5a702bc7da6f6f diff --git a/README.md b/README.md index 8b34d61778..27774fd3e0 100644 --- a/README.md +++ b/README.md @@ -2,32 +2,76 @@ English | [中文](README.zh.md) -The **DeepSeek Harness SDK** is a plugin-based SDK for building agent harnesses. +DeepSeek Harness (`dsh`) is an open-source coding agent built on the DeepSeek Harness SDK. + +It uses an architecture where **everything is a plugin**. ## Install -Install the `dsh` coding agent with one line — it needs `git` and Node `^22.19 || >=24`, and offers to install `pnpm` if it is missing: +Install `dsh` with one command: ```sh curl -fsSL https://raw.githubusercontent.com/deepseek-harness/deepseek-harness/master/scripts/install.sh | sh ``` -It clones the harness to `~/.dsh/source`, runs `pnpm install`, symlinks `dsh` into `~/.local/bin` (offering to add it to your PATH), prompts once for your `DEEPSEEK_API_KEY`, and launches `dsh`; re-running it updates an existing checkout. Run from inside a checkout (`sh scripts/install.sh`) it reuses that checkout and skips the clone. The overridable `DSH_*` variables are documented in [`scripts/install.sh`](scripts/install.sh). +The installer requires `git` and Node `^22.19 || >=24`, offers to install `pnpm` when it is missing, and prompts for a DeepSeek API key. + +The installer clones DeepSeek Harness to `~/.dsh/source`, links `dsh` into `~/.local/bin`, and launches it. Re-running the command updates the checkout. See [`scripts/install.sh`](scripts/install.sh) for alternate install locations and other options. + +## Use DeepSeek Harness + +### Web UI + +For the recommended local interface, build the frontend after installation and after each update, then start the Web UI: + +```sh +pnpm --dir ~/.dsh/source run build:web +dsh web +``` + +The Web UI is served at `http://127.0.0.1:3080` by default. + +### TUI + +Start the full-screen terminal interface: + +```sh +dsh +``` + +### Headless + +Run one task, print the final answer, and exit: + +```sh +dsh -p "summarize this workspace" +``` + +## Why DeepSeek Harness + +Built-in capabilities cover file reading, editing, and search; shell execution; reusable skills; task tracking; subagents and workflows; persistent sessions; and context compaction. The TUI also includes Plan Mode. + +- **Everything is a plugin.** Models, tools, policies, storage, context management, and interfaces are composable [Cordis plugins](docs/user/develop/basic/index.md), so deployments can extend or replace behavior without forking the agent loop. See the [architecture](docs/architecture.md) for the underlying design. +- **Code Mode (opt-in).** It exposes a `run_code` tool and a generated TypeScript SDK; only program output re-enters model context. See [Code Mode](packages/core/tools/README.md#code-mode). +- **Self-referential Cordis tools are opt-in.** They let the agent inspect its live runtime and mount or unmount plugins while it runs. See the [Cordis tools](packages/cordis/tool-cordis/README.md). + +## Community + +Follow DeepSeek Harness on Twitter for project updates. ## Development -This monorepo is built on the [Cordis](https://github.com/cordiverse/cordis) framework (vendored as source under `vendor/`), microkernel-style: everything is a plugin. - ```sh pnpm install -pnpm run test # vitest -# Agent demos require DEEPSEEK_API_KEY. -pnpm run demo:tui # full-screen TUI coding agent -pnpm run demo:headless "task" # one-shot coding agent -pnpm run demo:cordis # self-referential agent demo -pnpm run demo:acp # ACP server agent demo +pnpm run test:coverage ``` -For humans, start with the [development guide](docs/development.md) for local setup, hooks, environment variables, and quality gates, then read the [architecture design](docs/architecture.md) and [documentation graph index](docs/graph-atlas.md) before package work. Local context lives in [packages/](packages/) and [vendor/](vendor/). +Start with the [development guide](docs/development.md) and read the [architecture](docs/architecture.md) before changing packages. For agents, follow [AGENTS.md](AGENTS.md). + +DeepSeek Harness is currently pre-release. + +## License + +[BSD 3-Clause](LICENSE) diff --git a/README.zh.md b/README.zh.md index cf029cc0bb..b7d08f2bc9 100644 --- a/README.zh.md +++ b/README.zh.md @@ -2,32 +2,80 @@ [English](README.md) | 中文 -**DeepSeek Harness SDK** 是用于构建 agent harness(智能体框架)的 SDK,采取基于插件的设计。 +DeepSeek Harness(`dsh`)是一款基于 DeepSeek Harness SDK 构建的开源 coding agent(编程智能体)。 + +它采用了**一切皆插件**的架构。 ## 安装 -一行命令即可安装 `dsh` 编码智能体——需要 `git` 和 Node `^22.19 || >=24`,缺少 `pnpm` 时会询问是否代为安装: +使用一条命令安装 `dsh`: ```sh curl -fsSL https://raw.githubusercontent.com/deepseek-harness/deepseek-harness/master/scripts/install.sh | sh ``` -脚本会把 harness 克隆到 `~/.dsh/source`,运行 `pnpm install`,把 `dsh` 软链接到 `~/.local/bin`(并询问是否加入 PATH),提示输入一次 `DEEPSEEK_API_KEY`,随后启动 `dsh`;再次运行会更新已有的检出。若在检出目录内运行(`sh scripts/install.sh`),脚本会复用当前检出并跳过克隆。可覆盖的 `DSH_*` 变量见 [`scripts/install.sh`](scripts/install.sh)。 +安装器要求系统已安装 `git` 和 Node `^22.19 || >=24`,缺少 `pnpm` 时可代为安装,并会提示输入 DeepSeek API 密钥。 + +安装器会将 DeepSeek Harness 克隆到 `~/.dsh/source`,把 `dsh` 链接到 `~/.local/bin`,然后启动它。再次运行该命令会更新源码目录。其他安装位置和选项见 [`scripts/install.sh`](scripts/install.sh)。 + +## 使用 DeepSeek Harness + +### Web UI + +推荐在本地使用 Web UI。安装完成后以及每次更新后,请先构建前端,再启动 Web UI: + +```sh +pnpm --dir ~/.dsh/source run build:web +dsh web +``` + +Web UI 默认通过 `http://127.0.0.1:3080` 提供服务。 + +### TUI + +启动全屏终端界面: + +```sh +dsh +``` + +### Headless + +运行一项任务,打印最终答案后退出: + +```sh +dsh -p "summarize this workspace" +``` + +## 为什么选择 DeepSeek Harness + +内置功能涵盖文件读取、编辑与搜索、shell 执行、可复用 skill(技能)、任务跟踪、subagent 与工作流、持久化会话,以及上下文压缩(context compaction)。TUI 还包含 Plan Mode。 + +- **一切皆插件。** 模型、工具、策略、存储、上下文管理和界面均可组合为 [Cordis 插件](docs/user/develop/basic/index.md),部署方无需 fork agent loop(智能体循环)即可扩展或替换行为。底层设计见[架构文档](docs/architecture.md)。 +- **Code Mode(需显式启用)。** 它会提供 `run_code` 工具和生成的 TypeScript SDK,只有程序输出会重新进入模型上下文。参见 [Code Mode](packages/core/tools/README.md#code-mode)。 +- **自指 Cordis 工具需显式启用。** 这些工具可让 agent 检查自身的实时运行时,并在运行中挂载或卸载插件。参见 [Cordis 工具](packages/cordis/tool-cordis/README.md)。 + +## 社区 + +扫描二维码,或打开 DeepSeek Harness 微信社区申请页面 申请加入。 + +

+ DeepSeek Harness 微信社区二维码 +

## 开发 -本 monorepo 基于 [Cordis](https://github.com/cordiverse/cordis) 框架构建(以源码形式收录在 `vendor/` 下),采用微内核风格:所有功能都以插件形式提供。 - ```sh pnpm install -pnpm run test # vitest -# Agent demos require DEEPSEEK_API_KEY. -pnpm run demo:tui # full-screen TUI coding agent -pnpm run demo:headless "task" # one-shot coding agent -pnpm run demo:cordis # self-referential agent demo -pnpm run demo:acp # ACP server agent demo +pnpm run test:coverage ``` -面向开发者:先读[开发指南](docs/development.md),了解本地环境搭建、钩子、环境变量与质量门禁,动手改 package 之前再读[架构设计](docs/architecture.md)和[文档关系图索引](docs/graph-atlas.md)。局部上下文见 [packages/](packages/) 与 [vendor/](vendor/)。 +请先阅读[开发指南](docs/development.md);修改包之前,请阅读[架构文档](docs/architecture.md)。 面向 agent:遵循 [AGENTS.md](AGENTS.md)。 + +DeepSeek Harness 目前处于预发布阶段。 + +## 许可证 + +[BSD 3-Clause](LICENSE) diff --git a/assets/community-wecom-survey.png b/assets/community-wecom-survey.png new file mode 100644 index 0000000000..d6f6040ba7 Binary files /dev/null and b/assets/community-wecom-survey.png differ diff --git a/docs/cookbook/adding-a-package.i18n.yaml b/docs/cookbook/adding-a-package.i18n.yaml index 6fd3feebbd..234db6273e 100644 --- a/docs/cookbook/adding-a-package.i18n.yaml +++ b/docs/cookbook/adding-a-package.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -adding-a-package.md: 556a48493af4452c178634c0abb4e23e2419dd8e -adding-a-package.zh.md: 5f7e4692233448c746d25e4808c078c390cf39e6 +adding-a-package.md: 1859310965538b35a353ee05c94b01d1093a3e43 +adding-a-package.zh.md: 22f574a0469609e44f5c55957560ff0f04b9a053 diff --git a/docs/cookbook/adding-a-package.md b/docs/cookbook/adding-a-package.md index 556a48493a..1859310965 100644 --- a/docs/cookbook/adding-a-package.md +++ b/docs/cookbook/adding-a-package.md @@ -32,10 +32,11 @@ In-package relative imports use explicit `.ts` specifiers in source (for example | File | Change | |---|---| | `tsconfig.base.json` | no edit for an existing group; for a new group, add a `./packages//*/src` candidate to the `@deepseek-ai/dsh-*` wildcard | -| `tsconfig.json` | add `{ "path": "./packages//" }` to `references` | -| `tsconfig.build.json` | add `{ "path": "./packages//" }` to `references` | +| `tsconfig.host.json` (host-side package) or `tsconfig.client.json` (client-side package) | add `{ "path": "./packages//" }` to `references` — exactly one aggregate, never both ([layout](../development.md#typescript-project-layout)) | | `knip.json` | only if the package has non-`*.spec.ts` entries (e.g. `*.e2e.ts` → add a per-workspace override like `packages/llm/llm-deepseek`) | +A `packages/client/*` package additionally extends `tsconfig.base.client.json` instead of `tsconfig.base.json`, and a client plugin package declares `dshClient` in package.json, exports `./client`, and calls the shared tsdown preset (`packages/client/tsdown.client.ts`) — see [packages/client/AGENTS.md](../../packages/client/AGENTS.md) for the client-side contract. + Covered automatically by globs or package-manifest discovery — no edits needed: root `package.json` workspaces, `scripts/publint-all.ts`, `tsdown.config.ts`, `vitest.config.ts`, `eslint.config.mjs`, `scripts/check-workspace-constraints.ts`. ## 3. Decide the package topology diff --git a/docs/cookbook/adding-a-package.zh.md b/docs/cookbook/adding-a-package.zh.md index 5f7e469223..22f574a046 100644 --- a/docs/cookbook/adding-a-package.zh.md +++ b/docs/cookbook/adding-a-package.zh.md @@ -32,10 +32,11 @@ package.json 不变式(由 `pnpm run constraints` / `scripts/check-workspace-c | 文件 | 变更 | |---|---| | `tsconfig.base.json` | 已有分组无需编辑;新分组需为 `@deepseek-ai/dsh-*` 通配符添加 `./packages//*/src` 候选路径 | -| `tsconfig.json` | 在 `references` 中添加 `{ "path": "./packages//" }` | -| `tsconfig.build.json` | 在 `references` 中添加 `{ "path": "./packages//" }` | +| `tsconfig.host.json`(host 侧包)或 `tsconfig.client.json`(client 侧包) | 在 `references` 中添加 `{ "path": "./packages//" }`——恰好一个聚合,绝不两个都加([布局](../development.md#typescript-project-layout)) | | `knip.json` | 仅当包有非 `*.spec.ts` 入口时需要(如 `*.e2e.ts` → 添加 per-workspace override,参照 `packages/llm/llm-deepseek`) | +`packages/client/*` 包改为 extends `tsconfig.base.client.json`(而非 `tsconfig.base.json`);client 插件包还需在 package.json 声明 `dshClient`、导出 `./client`、调用共享 tsdown preset(`packages/client/tsdown.client.ts`)——client 侧见 [packages/client/AGENTS.md](../../packages/client/AGENTS.md)。 + 以下内容由 glob 或包 manifest 发现机制自动覆盖,无需手动编辑:根 `package.json` workspaces、`scripts/publint-all.ts`、`tsdown.config.ts`、`vitest.config.ts`、`eslint.config.mjs`、`scripts/check-workspace-constraints.ts`。 ## 3. 确定包拓扑 diff --git a/docs/cookbook/adding-a-vendored-package.i18n.yaml b/docs/cookbook/adding-a-vendored-package.i18n.yaml index b3ca7ca791..98c5ee7696 100644 --- a/docs/cookbook/adding-a-vendored-package.i18n.yaml +++ b/docs/cookbook/adding-a-vendored-package.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -adding-a-vendored-package.md: 1b82f2e582ca5cd040a7f3237505848dbb304fae -adding-a-vendored-package.zh.md: 7245682ef8b7d85ace2c626f8d47aa36f739506b +adding-a-vendored-package.md: 71ca9fccc9418348784dbb6668127242e4fb45d2 +adding-a-vendored-package.zh.md: c340630aebeda0ec293a835cdfc8d15d71cd7801 diff --git a/docs/cookbook/adding-a-vendored-package.md b/docs/cookbook/adding-a-vendored-package.md index 1b82f2e582..71ca9fccc9 100644 --- a/docs/cookbook/adding-a-vendored-package.md +++ b/docs/cookbook/adding-a-vendored-package.md @@ -38,8 +38,7 @@ Local relative imports/exports in vendored TypeScript source use explicit `.ts` | File | Change | |---|---| | `tsconfig.base.json` | add `"": ["./vendor//src"]` to `paths` | -| `tsconfig.json` | add `{ "path": "./vendor/" }` to `references` | -| `tsconfig.build.json` | add `{ "path": "./vendor/" }` to `references` (before the `packages/*` entries) | +| `tsconfig.host.json` | add `{ "path": "./vendor/" }` to `references` (before the `packages/*` entries; vendored code enters the graph through the host aggregate only) | | `vendor/README.md` | add a manifest table row (dir, npm name, version, upstream repo, commit SHA) and log any local modifications | | `scripts/publint-all.ts` | only if the vendored package is itself published from here (vendored deps normally are not — skip) | @@ -57,4 +56,4 @@ pnpm run typecheck pnpm run build && pnpm run test && pnpm run constraints ``` -The source `paths` map is shared by build and root typecheck configs. The important isolation boundary is the project-reference graph: vendored source must be referenced through its own `vendor//tsconfig.json`, not pulled into a root strict program. +The source `paths` map lives once in `tsconfig.base.json` and serves every graph. The important isolation boundary is the project-reference graph: vendored source must be referenced through its own `vendor//tsconfig.json`, not pulled into an aggregate's strict program ([layout](../development.md#typescript-project-layout)). diff --git a/docs/cookbook/adding-a-vendored-package.zh.md b/docs/cookbook/adding-a-vendored-package.zh.md index 7245682ef8..c340630aeb 100644 --- a/docs/cookbook/adding-a-vendored-package.zh.md +++ b/docs/cookbook/adding-a-vendored-package.zh.md @@ -38,8 +38,7 @@ vendored TypeScript 源码中的本地相对导入/导出在复制后使用显 | 文件 | 修改内容 | |---|---| | `tsconfig.base.json` | 在 `paths` 中添加 `"": ["./vendor//src"]` | -| `tsconfig.json` | 在 `references` 中添加 `{ "path": "./vendor/" }` | -| `tsconfig.build.json` | 在 `references` 中添加 `{ "path": "./vendor/" }`(置于 `packages/*` 条目之前) | +| `tsconfig.host.json` | 在 `references` 中添加 `{ "path": "./vendor/" }`(置于 `packages/*` 条目之前;vendored 代码只经 host 聚合进图) | | `vendor/README.md` | 添加一行 manifest 表格行(dir、npm name、version、upstream repo、commit SHA)并记录所有本地修改 | | `scripts/publint-all.ts` | 仅当该 vendored 包本身从此仓库发布时才需要(vendored 依赖通常不发布——跳过) | @@ -57,4 +56,4 @@ pnpm run typecheck pnpm run build && pnpm run test && pnpm run constraints ``` -源码 `paths` 映射由构建配置和根类型检查配置共享。重要的隔离边界是 project-reference 图:vendored 源码必须通过其自身的 `vendor//tsconfig.json` 被引用,而非被拉入根目录的严格程序中。 +源码 `paths` 映射只在 `tsconfig.base.json` 存在一份,服务所有图。重要的隔离边界是 project-reference 图:vendored 源码必须通过其自身的 `vendor//tsconfig.json` 被引用,而非被拉入某个聚合的严格程序中([布局](../development.md#typescript-project-layout))。 diff --git a/docs/cordis-primer.md b/docs/cordis-primer.md index d8e92dcb67..64230564b7 100644 --- a/docs/cordis-primer.md +++ b/docs/cordis-primer.md @@ -1,6 +1,6 @@ # Cordis Primer -Cordis is the vendored plugin framework underneath the DeepSeek Harness SDK. This primer teaches the Cordis ideas a harness plugin author needs before reading the generated [events](cordis-catalog/events.md) and [services](cordis-catalog/services.md) catalogs. The vendored source and sync procedure live in [vendor/README.md](../vendor/README.md). +Cordis is the vendored plugin framework underneath the DeepSeek Harness SDK. This primer teaches the Cordis ideas a harness plugin author needs before reading the generated [events](cordis-catalog/events.md) and [services](cordis-catalog/services.md) catalogs; the [Cordis tutorial](cordis-tutorial/index.md) walks the same ideas hands-on. The vendored source and sync procedure live in [vendor/README.md](../vendor/README.md). ## Cordis In Five Ideas diff --git a/docs/cordis-tutorial/01-first-plugin.md b/docs/cordis-tutorial/01-first-plugin.md new file mode 100644 index 0000000000..084f9964e1 --- /dev/null +++ b/docs/cordis-tutorial/01-first-plugin.md @@ -0,0 +1,93 @@ +# 1. Your first plugin + +In the loader configuration used here, a Cordis plugin module named-exports an `apply` function. When Cordis loads it, it calls `apply` with a **context** — the `ctx` object through which the plugin registers everything it contributes. + +## Write the plugin + +In your `tmp/cordis-tutorial` directory (see [setup](index.md#setup)), create `hello.ts`: + +```ts +import type { Context } from 'cordis' + +export const name = 'hello' + +export function apply(ctx: Context) { + console.log('hello from my first plugin') +} +``` + +The `name` export is optional display metadata; it labels the plugin in diagnostics. + +## Compose the app + +This tutorial's launcher assembles the application from configuration. Create `cordis.yml`: + +```yaml +- name: './hello.ts' +``` + +The file is a list of plugin entries. `name` is a module specifier — a relative path or an npm package name — and the loader mounts every entry. Entries start concurrently, so list position guarantees nothing about which plugin loads first; ordering comes from service dependencies (`inject`, [chapter 3](03-services.md)), not from position in the file. + +## Run it + +```sh +node --import tsx ../../vendor/cordis/bin.js +``` + +Expected output: + +``` +hello from my first plugin +``` + +The process exits on its own once nothing is left running. What happened: + +1. The launcher created a root `Context` and mounted the **Loader** plugin. +2. The Loader read `cordis.yml`, resolved `./hello.ts`, and mounted it as a child plugin. +3. Cordis called your `apply(ctx)`. + +There is no framework bootstrap code in your file: a plugin describes what it contributes, and `cordis.yml` composes the application. The [TUI agent](../../examples/tui-agent/cordis.yml), for example, is a longer plugin composition. + +## The two other plugin shapes + +A function is the most common shape, but Cordis accepts three: + +```ts +import { Service, type Context } from 'cordis' + +// 1. Function plugin (what you just wrote). +export function apply(ctx: Context) {} + +// 2. Object plugin: an object with an `apply` method. +export const objectPlugin = { + name: 'object-plugin', + apply(ctx: Context) {}, +} + +// 3. Class plugin: a Service subclass (covered in chapter 3). +export class MyService extends Service { + constructor(ctx: Context) { + super(ctx, 'myTutorialService') + } +} +``` + +Use the function form until you need to expose a service; [chapter 3](03-services.md) covers when the class form earns its place. + +## Try breaking it + +Make `apply` throw: + +```ts ignore-check +export function apply(ctx: Context) { + throw new Error('apply exploded') +} +``` + +Run again: the process dies with your error. A plugin that fails to load is a loud failure, not a skipped entry. + +One caveat worth knowing early: a config entry whose module cannot be **resolved** — a typo'd path or package name — is reported through the Cordis logger service instead of crashing the process, and at boot that report can be lost before a console exporter is watching. If a freshly added entry seems to do nothing, check the spelling first. + +Next: [Lifecycle and effects](02-lifecycle-and-effects.md) — what happens when a plugin unloads. + +[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-harness/deepseek-harness) diff --git a/docs/cordis-tutorial/02-lifecycle-and-effects.md b/docs/cordis-tutorial/02-lifecycle-and-effects.md new file mode 100644 index 0000000000..68a0f12ec8 --- /dev/null +++ b/docs/cordis-tutorial/02-lifecycle-and-effects.md @@ -0,0 +1,96 @@ +# 2. Lifecycle and effects + +A Cordis plugin can be unloaded by a config edit, hot reload, explicit disposal, or loss of a required service. Registrations made through Cordis APIs are effects and are undone when their owning plugin unloads; resources managed outside those APIs must be wrapped in `ctx.effect()`. + +## Effects + +For a resource Cordis does not already manage — a timer, a connection, a watcher — wrap it in `ctx.effect()` and return a disposer: + +Create `lifecycle.ts` in `tmp/cordis-tutorial`: + +```ts +import type { Context } from 'cordis' + +export const name = 'lifecycle-demo' + +function heartbeat(ctx: Context) { + console.log('heartbeat plugin loading') + ctx.effect(() => { + const timer = setInterval(() => console.log('tick'), 200) + return () => { + clearInterval(timer) + console.log('heartbeat cleaned up') + } + }) +} + +export function apply(ctx: Context) { + // Mount a child plugin and keep its fiber to dispose it later. + const fiber = ctx.plugin(heartbeat) + // The demo timer is itself an effect: if THIS plugin is unloaded first, + // the pending callback is cancelled instead of firing on a dead app. + ctx.effect(() => { + const timer = setTimeout(async () => { + await fiber.dispose() + console.log('disposed') + process.exit(0) + }, 700) + return () => clearTimeout(timer) + }) +} +``` + +Point `cordis.yml` at it: + +```yaml +- name: './lifecycle.ts' +``` + +Run (`node --import tsx ../../vendor/cordis/bin.js`) and you get: + +``` +heartbeat plugin loading +tick +tick +tick +heartbeat cleaned up +disposed +``` + +Three things to notice: + +- `ctx.plugin(heartbeat)` mounts a function **from code** as a plugin — the same operation the YAML loader performs for each config entry. A function plugin needs no `apply` method: Cordis calls the function directly and uses its name only for diagnostics. An `apply` method is required only for the object form, `ctx.plugin({ apply(ctx) { /* ... */ } })`. The call returns a **fiber**, the runtime handle for one loaded plugin instance. +- The effect body runs during load; the disposer it returns runs during unload. You never call the disposer yourself for a plugin-lifetime resource. +- `fiber.dispose()` resolves after all of the plugin's cleanup — including async disposers — has finished, and recursively unloads any child plugins it mounted. + +## The fiber state machine + +Every loaded plugin instance owns a fiber that moves through these states: + +``` +PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED + ↘ FAILED +``` + +- **PENDING** — declared, but a required service (chapter 3) is not available yet. +- **LOADING / ACTIVE** — `apply` is running / has completed. +- **FAILED** — `apply` or config validation threw. +- **UNLOADING / DISPOSED** — disposers are running / everything is torn down. + +You will meet PENDING again in [chapter 6](06-composition-and-hmr.md), where it is the usual answer to "why does my plugin print nothing?". + +## What is already an effect + +You rarely write `ctx.effect()` yourself, because the built-in registration APIs are effects already: + +- `ctx.on(event, listener)` — the listener is removed on unload ([chapter 4](04-events.md)). +- `ctx.plugin(child)` — the child is disposed with its parent. +- Service registrations are effects. Harness registries such as `ctx.tools.register(...)` also attach their returned disposers to the calling plugin, so they unwind automatically ([chapter 7](07-into-the-harness.md)). + +For a resource Cordis does not manage, acquire it inside `ctx.effect()` and return a disposer that releases it. Cordis then invokes that release during unloading, including hot reload. + +One ordering caveat: disposers start in reverse registration order, but multiple **async** disposers run concurrently. If teardown steps must run in sequence, keep them in one disposer and await them there. + +Next: [Services](03-services.md) — how plugins share capabilities. + +[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-harness/deepseek-harness) diff --git a/docs/cordis-tutorial/03-services.md b/docs/cordis-tutorial/03-services.md new file mode 100644 index 0000000000..9f62003e99 --- /dev/null +++ b/docs/cordis-tutorial/03-services.md @@ -0,0 +1,96 @@ +# 3. Services + +A **service** is a named capability one plugin provides and other plugins consume through `ctx`. In the harness, `ctx.tools`, `ctx.llm`, and `ctx.agents` are services. A consumer names the capability, such as `'tools'`, rather than importing its provider, so configuration can select a provider without changing the consumer. + +## Provide a service + +Create `greeter.ts` in `tmp/cordis-tutorial`: + +```ts +import { Service, type Context } from 'cordis' + +declare module 'cordis' { + interface Context { + greeter: GreeterService + } +} + +export class GreeterService extends Service { + constructor(ctx: Context) { + super(ctx, 'greeter') + } + + greet(who: string) { + return `Hello, ${who}!` + } +} + +export const name = 'greeter' + +export function apply(ctx: Context) { + ctx.plugin(GreeterService) +} +``` + +Two pieces work together: + +- **Runtime**: `super(ctx, 'greeter')` registers the instance under the name `greeter`. From then on, any plugin can reach it as `ctx.greeter`. The registration is an effect — unloading the provider removes the service. +- **Compile time**: the `declare module 'cordis'` block is TypeScript declaration merging. It adds `greeter` to the `Context` interface so `ctx.greeter` typechecks everywhere. It generates no code; without it the service still works at runtime, but consumers lose type safety. + +A `Service` subclass is itself a plugin (the class form from chapter 1), so `ctx.plugin(GreeterService)` mounts it like any other. + +## Consume a service with `inject` + +Create `consumer.ts`: + +```ts +import type { Context } from 'cordis' + +export const name = 'consumer' +export const inject = ['greeter'] + +export function apply(ctx: Context) { + console.log(ctx.greeter.greet('world')) +} +``` + +`inject` lists the services this plugin requires. Cordis holds the plugin in PENDING until every listed service exists, so inside `apply`, `ctx.greeter` is guaranteed ready. Load order in `cordis.yml` does not matter — dependencies, not file order, decide when plugins start. + +Compose and run: + +```yaml +- name: './greeter.ts' +- name: './consumer.ts' +``` + +``` +Hello, world! +``` + +Swap the two lines in `cordis.yml` and rerun: same output. Try removing `./greeter.ts` entirely: the consumer stays PENDING and prints nothing — no crash, no partial run. A PENDING fiber does not keep Node's event loop alive either, so a composition with nothing else running exits 0 silently. [Chapter 6](06-composition-and-hmr.md) shows how to diagnose that state. + +## Dependencies are tracked after load + +`inject` is not a one-shot boot check. If a required service disappears while the app runs — its provider was unloaded or hot-replaced — every dependent plugin is unloaded too, and loads again when the service returns. Combined with effects ([chapter 2](02-lifecycle-and-effects.md)), this prevents a running consumer from retaining a reference to an unavailable service: its own registrations are unwound when the dependency disappears. + +This is also why service replacement works in config: unload the `dsh-bash-local` entry, mount a different `bash` provider, and every plugin injecting `'bash'` cleanly restarts against the new implementation. + +## Optional dependencies + +`inject` is for hard requirements. For a capability the plugin can live without, skip `inject` and probe at the use site: + +```ts ignore-check +export function apply(ctx: Context) { + // undefined when no provider is loaded; the plugin still runs. + const greeter = ctx.get('greeter') + console.log(greeter?.greet('maybe') ?? 'no greeter available') +} +``` + +## Naming + +Service names live in one flat namespace per application. Prefix or namespace your own services distinctively (the harness claims plain names like `tools` and `llm`); the generated [services catalog](../cordis-catalog/services.md) lists every name the harness registers. + +Next: [Events](04-events.md) — communication without a shared service. + +[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-harness/deepseek-harness) diff --git a/docs/cordis-tutorial/04-events.md b/docs/cordis-tutorial/04-events.md new file mode 100644 index 0000000000..4c14ba5313 --- /dev/null +++ b/docs/cordis-tutorial/04-events.md @@ -0,0 +1,142 @@ +# 4. Events + +Services support direct calls; **events** let a plugin announce something without knowing which plugins listen. The harness uses events for interactions such as tool results, model requests, and approval decisions. + +## Declare, emit, listen + +Create `stats.ts` in `tmp/cordis-tutorial` — a service that counts things and announces each change: + +```ts +import { Service, type Context } from 'cordis' + +declare module 'cordis' { + interface Context { + stats: StatsService + } + interface Events { + 'stats/report'(name: string, count: number): void + } +} + +export class StatsService extends Service { + private counts = new Map() + + constructor(ctx: Context) { + super(ctx, 'stats') + } + + bump(name: string) { + const next = (this.counts.get(name) ?? 0) + 1 + this.counts.set(name, next) + this.ctx.emit('stats/report', name, next) + } +} + +export const name = 'stats' + +export function apply(ctx: Context) { + ctx.plugin(StatsService) +} +``` + +The `interface Events` merge is the event-system twin of the `interface Context` merge from chapter 3: it declares the event name and its listener signature, so `ctx.emit` and `ctx.on` are fully typed. The `namespace/action` naming convention keeps the flat event namespace readable. + +Create `reporter.ts`: + +```ts ignore-check +import type { Context } from 'cordis' +import type {} from './stats.ts' + +export const name = 'reporter' +export const inject = ['stats'] + +export function apply(ctx: Context) { + ctx.on('stats/report', (name, count) => { + console.log(`[stats] ${name} -> ${count}`) + }) + ctx.stats.bump('tool_call') + ctx.stats.bump('tool_call') + ctx.stats.bump('prompt') +} +``` + +The `import type {} from './stats.ts'` line imports nothing at runtime; it exists so TypeScript sees the declaration merges. Compose and run: + +```yaml +- name: './stats.ts' +- name: './reporter.ts' +``` + +``` +[stats] tool_call -> 1 +[stats] tool_call -> 2 +[stats] prompt -> 1 +``` + +Because `ctx.on()` is an effect, the listener disappears with the plugin — no manual `removeListener` bookkeeping, ever. + +## Dispatch modes + +`emit` is one of five dispatch modes. Which one an event uses is part of its contract — it decides whether listeners can return values, run concurrently, or short-circuit each other: + +| Mode | Call | Semantics | +|---|---|---| +| emit | `ctx.emit(name, ...args)` | Synchronous broadcast; returned promises and values are not awaited or collected. | +| parallel | `await ctx.parallel(name, ...args)` | All listeners run concurrently; awaited together. | +| serial | `await ctx.serial(name, ...args)` | Listeners run in order, awaited; the first non-`null`/`false`/`undefined` return wins and stops the rest. | +| bail | `ctx.bail(name, ...args)` | Synchronous version of serial. | +| waterfall | `ctx.waterfall(name, ...args, next)` | Around-middleware; see below. | + +Every harness event documents its mode in the generated [events catalog](../cordis-catalog/events.md). + +## Waterfall: transform or short-circuit + +Waterfall is the mode that powers interception. Each listener receives the arguments plus a `next()` continuation; it can transform what `next()` returns, or return without calling `next()` and short-circuit the rest of the chain — what the Cordis docs call the veto. Create `waterfall-demo.ts`: + +```ts +import type { Context } from 'cordis' + +declare module 'cordis' { + interface Events { + 'demo/transform'(input: string, next: () => Promise): Promise + } +} + +export const name = 'waterfall-demo' + +export function apply(ctx: Context) { + // Listener 1: wrap the downstream result. + ctx.on('demo/transform', async (input, next) => { + const downstream = await next() + return downstream.toUpperCase() + }) + + // Listener 2: short-circuit when it owns the decision. + ctx.on('demo/transform', async (input, next) => { + if (input.includes('blocked')) return '** blocked **' + return next() + }) + + void (async () => { + console.log(await ctx.waterfall('demo/transform', 'hello', async () => 'hello')) + console.log(await ctx.waterfall('demo/transform', 'blocked words', async () => 'blocked words')) + })() +} +``` + +Point `cordis.yml` at just this file and run: + +``` +HELLO +** BLOCKED ** +``` + +Walk through the second line: listener 1 runs first, calls `next()`, which invokes listener 2; listener 2 sees `blocked` and returns without calling `next()` — the innermost default (the function passed to `ctx.waterfall`) never runs — and listener 1 uppercases the replacement message on the way out. + +The discipline that follows: **a waterfall listener that only observes or annotates must call `next()`**; returning without it is a deliberate short-circuit. Forgetting `next()` in a logging listener silently swallows the default behavior for everyone downstream. This is important enough that it is a standing rule of this repository ([waterfall semantics](../cordis-primer.md#cordis-waterfall-semantics)). + +The harness uses waterfalls for decisions that cooperating plugins may wrap or answer: [`agent/request`](../cordis-catalog/events.md#agentrequest--waterfall) lets a plugin replace the model-call config, and [`approval/request`](../cordis-catalog/events.md#approvalrequest--waterfall) lets a policy answer instead of the user. + +Next: [Configuration](05-config.md) — plugin options from `cordis.yml`. + +[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-harness/deepseek-harness) diff --git a/docs/cordis-tutorial/05-config.md b/docs/cordis-tutorial/05-config.md new file mode 100644 index 0000000000..09aaf8971d --- /dev/null +++ b/docs/cordis-tutorial/05-config.md @@ -0,0 +1,82 @@ +# 5. Configuration + +Each `cordis.yml` entry can carry a `config` block, and the plugin declares a schema that validates it before `apply` runs. Bad config fails the load with a precise error — the plugin never starts half-configured. + +## A configurable plugin + +Create `config-demo.ts` in `tmp/cordis-tutorial`: + +```ts +import type { Context } from 'cordis' +import Schema from 'schemastery' + +export const name = 'config-demo' + +export interface Config { + greeting: string + targets: string[] +} + +export const Config: Schema = Schema.object({ + greeting: Schema.string().default('Hello'), + targets: Schema.array(String).default(['world']), +}) + +export function apply(ctx: Context, config: Config) { + for (const target of config.targets) { + console.log(`${config.greeting}, ${target}!`) + } +} +``` + +The exported `Config` is both a TypeScript interface and a runtime schema with the same name — consumers get the type, Cordis gets the validator. This repo uses [Schemastery](https://github.com/shigma/schemastery) for schemas; Cordis itself accepts any [Standard Schema](https://standardschema.dev/) validator, so a plain object exported as `Config` will not work. + +Configure it: + +```yaml +- name: './config-demo.ts' + config: + targets: ['alpha', 'beta'] +``` + +Run: + +``` +Hello, alpha! +Hello, beta! +``` + +`greeting` was omitted, so the schema default filled it in — `apply` always receives complete, validated config. + +## Fail loud + +Now feed it something invalid: + +```yaml +- name: './config-demo.ts' + config: + targets: 'not-an-array' +``` + +``` +ValidationError: invalid config: + - $.targets expected array but got not-an-array (at targets) +``` + +The plugin's fiber goes to FAILED, and this tutorial's launcher exits with status 1 after printing the error. A plugin should also reject schema-valid config that names an unavailable resource or provider as soon as it can resolve that reference. + +## Computed config values + +The loader used in this repo supports a `!!js` tag for config values that must be computed at load time, such as reading an API key from the environment: + +```yaml +- name: '@deepseek-ai/dsh-llm-deepseek' + config: + apiKey: !!js process.env.DEEPSEEK_API_KEY +``` + +`!!js` works **only inside `config`**. Entry metadata (`name`, `id`, `disabled`, `inject`, ...) is static; `disabled: !!js ...` produces a truthy expression object that always disables the entry. See [loader configuration](../cordis-primer.md#loader-configuration). + +Next: [Composition and HMR](06-composition-and-hmr.md) — treating `cordis.yml` as the application. + +[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-harness/deepseek-harness) diff --git a/docs/cordis-tutorial/06-composition-and-hmr.md b/docs/cordis-tutorial/06-composition-and-hmr.md new file mode 100644 index 0000000000..b11fdbe45c --- /dev/null +++ b/docs/cordis-tutorial/06-composition-and-hmr.md @@ -0,0 +1,111 @@ +# 6. Composition and HMR + +Every capability built so far is a plugin, and `cordis.yml` selects the application's plugin tree. This chapter changes that composition, hot-reloads a plugin, and diagnoses a plugin that never loads. + +## Entries are more than a name + +A config entry accepts metadata beyond `name` and `config`: + +```yaml +- id: greeter # stable identity for this entry + name: './greeter.ts' +- id: consumer + name: './consumer.ts' + disabled: true # keep the entry, skip mounting it +``` + +`id` gives the entry a stable identity so the loader can tell an edit to an existing entry apart from a removal plus an addition. `disabled: true` unmounts a plugin without deleting its entry — flip it back and the plugin (and everything PENDING on its services) loads again. + +Groups nest a sub-list of entries that load and unload as one unit, and `isolate` gives a group its own instance of a service name — two groups can each see a differently-configured `bash` without affecting each other. Those are worth knowing about before you need them; the [Cordis primer](../cordis-primer.md) and the [service isolation example](../user/develop/framework/service.md#service-isolation) cover the details. + +## Hot module replacement + +Because unloading releases effects ([chapter 2](02-lifecycle-and-effects.md)) and loading follows dependencies ([chapter 3](03-services.md)), HMR can replace a running plugin by unloading and loading it. The `@cordisjs/plugin-hmr` plugin watches your files and does exactly that on save. + +In `tmp/cordis-tutorial`, write `cordis.yml`: + +```yaml +- id: logger + name: '@cordisjs/plugin-logger-console' +- id: timer + name: '@cordisjs/plugin-timer' +- id: hmr + name: '@cordisjs/plugin-hmr' + config: + root: ['.'] +- id: hello + name: './hello.ts' +``` + +Two support plugins joined the list: HMR logs through the Cordis logger service, so without a console exporter you would not see its messages, and it `inject`s the `timer` service for debouncing — without `@cordisjs/plugin-timer` it sits in PENDING forever, silently. That silence is the subject of the next section. + +HMR also needs Node's loader internals: + +```sh +node --expose-internals --import tsx ../../vendor/cordis/bin.js +``` + +Now edit `hello.ts` — change the log message — and save: + +``` +hello from my first plugin +2026-07-22 15:44:36 [I] hmr watching [ '.' ] +2026-07-22 15:44:39 [I] hmr reload plugin at hello.ts +hello from my EDITED plugin +``` + +The old instance unloaded (all its effects unwound), the new code loaded, `apply` ran again. Stop the process with Ctrl-C. Editing `cordis.yml` itself is also picked up: the loader diffs entries by `id` and mounts, unmounts, or reconfigures only what changed. This is why the entries above carry explicit `id`s — an entry without one gets a generated id on every read, so after any config-file edit it counts as removed-plus-added and remounts even if its own lines did not change. + +## Diagnosing a plugin that never loads + +The flip side of dependency-driven loading: a plugin whose `inject` names a service nobody provides waits forever, printing nothing. No error — PENDING is a legitimate state, since the provider may be mounted later. + +You can see the states directly. Every context can enumerate the plugin registry; create `diagnose.ts`: + +```ts +import { FiberState, type Context } from 'cordis' + +export const name = 'diagnose' + +export function apply(ctx: Context) { + setTimeout(() => { + for (const runtime of ctx.registry.values()) { + for (const fiber of runtime.fibers) { + if (fiber.state === FiberState.PENDING) { + console.log(`${fiber.name} is PENDING — a required service is missing`) + } + } + } + }, 500) +} +``` + +And a plugin with an unsatisfiable dependency, `needs-timer.ts`: + +```ts +import type { Context } from 'cordis' + +export const name = 'needs-timer' +export const inject = ['timer'] + +export function apply(ctx: Context) { + console.log('needs-timer loaded') +} +``` + +```yaml +- name: './needs-timer.ts' +- name: './diagnose.ts' +``` + +Run it (plain `node --import tsx ../../vendor/cordis/bin.js`; stop with Ctrl-C): + +``` +needs-timer is PENDING — a required service is missing +``` + +`inject: ['timer']` has no provider. Add `- name: '@cordisjs/plugin-timer'` to the list and the plugin loads. When a plugin does nothing and reports nothing, inspect its fiber state. Iterating without the PENDING filter also shows the loader's own plugins (Loader, Include) as ACTIVE fibers because plugins mount the config file itself. + +Next: [Into the harness](07-into-the-harness.md) — the same patterns against real harness services. + +[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-harness/deepseek-harness) diff --git a/docs/cordis-tutorial/07-into-the-harness.md b/docs/cordis-tutorial/07-into-the-harness.md new file mode 100644 index 0000000000..d25419afd5 --- /dev/null +++ b/docs/cordis-tutorial/07-into-the-harness.md @@ -0,0 +1,101 @@ +# 7. Into the harness + +This chapter registers a model-callable tool with the harness's `tools` service, executes it through the harness tool pipeline, and observes the result event. It remains keyless and does not call a model. + +## A tool plugin + +Create `greet-tool.ts` in `tmp/cordis-tutorial`: + +```ts +import type { Context } from 'cordis' +import { defineTool } from '@deepseek-ai/dsh-tools' +import { CallId } from '@deepseek-ai/dsh-llm' + +export const name = 'greet-tool' +export const inject = ['tools'] + +export function apply(ctx: Context) { + ctx.tools.register(defineTool({ + name: 'greet', + description: 'Greet the named person.', + parameters: { + name: { type: 'string', required: true, description: 'Who to greet' }, + }, + async execute(args) { + return [{ type: 'text', text: `Hello, ${args.name}!` }] + }, + })) + + // Drive one call through the real execution pipeline, standing in for + // the model. CallId brands the correlation id a provider would issue. + void (async () => { + const result = await ctx.tools.execute({ + callId: CallId('demo-1'), + name: 'greet', + arguments: { name: 'Cordis' }, + signal: new AbortController().signal, + }) + console.log('tool replied:', JSON.stringify(result.content)) + })() +} +``` + +Every pattern here is from the earlier chapters: `inject: ['tools']` ([chapter 3](03-services.md)) holds the plugin until the tool registry exists; `ctx.tools.register(...)` attaches the registration disposer to the plugin ([chapter 2](02-lifecycle-and-effects.md)), so unloading unregisters the tool. `defineTool` converts the `parameters` spec to the JSON Schema shown to the model, infers the type of `args`, and validates model-supplied arguments before `execute` runs. + +## An observer plugin + +Create `tool-logger.ts` — a separate plugin that watches every tool call in the app through the harness's `tools/result` event: + +```ts +import type { Context } from 'cordis' +import type {} from '@deepseek-ai/dsh-tools' + +export const name = 'tool-logger' +export const inject = ['tools'] + +export function apply(ctx: Context) { + ctx.on('tools/result', (exec, result) => { + const text = result.content + .map(block => (block.type === 'text' ? block.text : '')) + .join('') + console.log(`[tool-logger] ${exec.name} -> ${text}`) + }) +} +``` + +The `import type {} from '@deepseek-ai/dsh-tools'` line pulls in the package's declaration merges so `'tools/result'` and its payload are typed — the same move as chapter 4's `stats.ts` import, at package scale. + +## Compose and run + +```yaml +- name: '@deepseek-ai/dsh-system-prompt' +- name: '@deepseek-ai/dsh-tools' +- name: './tool-logger.ts' +- name: './greet-tool.ts' +``` + +`@deepseek-ai/dsh-tools` injects the `systemPrompt` service because tools contribute schemas to the system prompt, so the composition lists its provider too. Without it, the tools plugin remains PENDING as described in [chapter 6](06-composition-and-hmr.md). + +```sh +node --import tsx ../../vendor/cordis/bin.js +``` + +``` +[tool-logger] greet -> Hello, Cordis! +tool replied: [{"type":"text","text":"Hello, Cordis!"}] +``` + +The logger fired first: `tools/result` is emitted as part of result materialization, before `execute`'s promise resolves to the caller. Neither of your plugins knows the other exists — the registry service and the event connect them. + +## From here to a full agent + +A real agent is this composition plus more plugins: an LLM adapter, the agent loop, persistence, a front end. Compare [examples/headless-agent/cordis.yml](../../examples/headless-agent/cordis.yml) — you can read every entry in it now. Add your `greet-tool.ts` to a copy of that file. + +Where to go next: + +- [Build a tool](../user/develop/basic/tool.md) — more of `defineTool`, including presentation and richer schemas. +- [Three-layer capability design](../user/develop/practice/index.md) — how the harness structures replaceable capabilities. +- The generated [services](../cordis-catalog/services.md) and [events](../cordis-catalog/events.md) catalogs — everything you can inject and listen to. +- [Architecture](../architecture.md) — the system map these plugins live in. + +[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-harness/deepseek-harness) diff --git a/docs/cordis-tutorial/index.md b/docs/cordis-tutorial/index.md new file mode 100644 index 0000000000..9cf3966117 --- /dev/null +++ b/docs/cordis-tutorial/index.md @@ -0,0 +1,54 @@ +# Cordis tutorial + +Cordis is the plugin framework underneath the DeepSeek Harness SDK: a small runtime where every capability — tools, LLM adapters, file access, the agent loop itself — is a plugin mounted into a shared context. This tutorial teaches Cordis hands-on: each chapter is a runnable example you build in a scratch directory inside this repository, ending with a plugin wired into real harness services. + +The audience is agent developers. You do not need deep TypeScript experience; the [TypeScript notes](#typescript-notes) below explain the syntax that may be unfamiliar, and every chapter shows the exact commands and expected output. + +If you want the condensed concept reference instead of a walkthrough, read the [Cordis primer](../cordis-primer.md). The exhaustive API reference lives in the generated [events](../cordis-catalog/events.md) and [services](../cordis-catalog/services.md) catalogs and the [Cordis core API](../cordis-catalog/core/context.md) pages. + +## Setup + +You need a clone of this repository with dependencies installed — the [quick start](../user/guide/quickstart.md) covers prerequisites. No API key is needed for this tutorial; every example runs keylessly. + +```sh +git clone https://github.com/deepseek-harness/deepseek-harness.git +cd deepseek-harness +pnpm install +``` + +Create the scratch directory the chapters work in. `tmp/` is gitignored, so nothing you write there touches version control: + +```sh +mkdir -p tmp/cordis-tutorial +cd tmp/cordis-tutorial +``` + +Every chapter runs the same command from this directory: + +```sh +node --import tsx ../../vendor/cordis/bin.js +``` + +That one-file launcher (see [vendor/cordis/bin.js](../../vendor/cordis/bin.js)) creates a root `Context`, mounts the Loader plugin, and tells it to load `./cordis.yml` from the current directory. Everything else — which plugins exist, how they are configured — comes from that YAML file, which you will write in a moment. The `--import tsx` flag lets Node run the TypeScript files the config points at without a build step. + +## Chapters + +1. [Your first plugin](01-first-plugin.md) — a plugin is a function; the loader mounts it. +2. [Lifecycle and effects](02-lifecycle-and-effects.md) — Cordis-managed registrations are undone when their plugin unloads. +3. [Services](03-services.md) — expose a capability on `ctx` and depend on it with `inject`. +4. [Events](04-events.md) — typed events, broadcast dispatch, and the waterfall short-circuit. +5. [Configuration](05-config.md) — validated config from `cordis.yml`, failing loud on bad input. +6. [Composition and HMR](06-composition-and-hmr.md) — the config file as a plugin tree, hot reload, and diagnosing a plugin that never loads. +7. [Into the harness](07-into-the-harness.md) — register a model-callable tool against real harness services. + +## TypeScript notes + +The examples use three TypeScript features beyond ordinary modern JavaScript: + +- **Type annotations** describe values without changing runtime behavior: `ctx: Context` says that `ctx` has the Cordis context API, `who: string` accepts text, and `string[]` means an array of strings. +- **`import type { Context } from 'cordis'`** imports only type information. It vanishes at runtime, so a plugin file that needs `Context` solely for annotations adds no runtime dependency. +- **Declaration merging** (`declare module 'cordis' { ... }`) adds your entries to interfaces that Cordis already declares — for example the type of a new `ctx.greeter` property or event name. It generates no runtime wiring; the plugin separately provides the service or emits the event. Chapter 3 shows the pattern in full. + +Chapter 5 also uses an `interface` to describe a configuration object's fields and a generic type such as `Schema` to say which object shape a schema validates. You can copy those declarations as shown; the surrounding text explains what each one connects. + +[![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-harness/deepseek-harness) diff --git a/docs/development.i18n.yaml b/docs/development.i18n.yaml index 4ea0bafef3..40828094ce 100644 --- a/docs/development.i18n.yaml +++ b/docs/development.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -development.md: 3559b09d86395707f222aad0a281c9db1246c24f -development.zh.md: 8664a3291c04a5338fdadbce8f24b160cc9ec0a8 +development.md: 4294038e40aa774a006874e6641ca63eea44beeb +development.zh.md: 1f07c95dd60d0554b945c29e6e3ba8bc6ca9841a diff --git a/docs/development.md b/docs/development.md index 3559b09d86..4294038e40 100644 --- a/docs/development.md +++ b/docs/development.md @@ -33,7 +33,26 @@ Run typecheck once after a fresh clone: pnpm run typecheck ``` -That first typecheck runs the package/vendor build graph and the root no-emit `tsconfig.json` graph for examples, tests, and scripts. The root graph uses the same source `paths` map but relies on project references so vendored code is checked under its own tsconfig settings. +That first typecheck runs the whole-repo `tsc -b` graph: it emits every package/vendor `lib/types` and checks examples, tests, and scripts through the two no-emit aggregates described below. + +## TypeScript project layout + +The repository's TypeScript configuration has exactly three roles; every tsconfig file plays one of them. + +| File | Role | Forms a program? | +|---|---|---| +| `tsconfig.json` | Solution root: `extends` base, `files: []`, references to the two aggregates. The whole-repo `tsc -b tsconfig.json` graph, the tsserver discovery entry, and — through the inherited `paths` — the resolution config for tsx running `examples/` and `scripts/` (their nearest tsconfig is this file). | No | +| `tsconfig.host.json` | Host aggregate: host-side packages (via references), examples, tests, scripts, website. Excludes `packages/client`. | Yes | +| `tsconfig.client.json` | Client aggregate: `packages/client/*` packages and their tests, `apps/web`. | Yes | +| `tsconfig.base.json` | Shared compilerOptions and the source `paths` map. Also the resolution facade the vitest configs point vite-tsconfig-paths at: it has no `include`, so its `paths` apply to every importer. | No | +| `tsconfig.base.client.json` | Browser compiler shape (`jsx`, DOM libs, `types: []`) extended by the client aggregate and every `packages/client/*` package. | No | + +Host and client stay two aggregate programs because both sides declaration-merge the cordis `Context` interface under the same keys with different services; one program seeing both merges reports a collision. The collision exists only inside a `ts.Program` — module resolution never triggers it — which is why the solution may reference both aggregates and one paths facade may span both sides. Two disciplines follow: + +- `tsconfig.base.json` never gains `include` or `files`: they would leak into every extending package project and narrow the facade's match-all scope. +- A script that builds a repo-wide `ts.Program` seeds `tsconfig.host.json` or `tsconfig.client.json` explicitly — never the root solution, because flattening both aggregates into one program collides the `Context` merges. Program-backed generators and gates (`scripts/ts-project.ts` consumers, doc-typecheck standalone mode) are host-only by decision; the client side gains program-backed tooling only with a concrete need. + +Static analysis and tests resolve workspace imports through the base `paths` map to `src` and must pass on a clean tree; gates that consume built `lib/` output declare that dependency explicitly. Decision record: [solution-root note](../.agents/notes/implemented/process/2026-07-22-tsconfig-solution-root-two-aggregates.md); the tsc-first emit pipeline is the [ts-build-config note](../.agents/notes/implemented/process/2026-06-17-ts-build-config.md). If a relevant local check consumes built package output, build once first: @@ -59,7 +78,7 @@ DEEPSEEK_BASE_URL=https://... # optional lefthook is configured in `lefthook.yml` as a fast local checkpoint: - `pre-commit` runs staged-file ESLint fixes, checks the staged diff for whitespace errors, and runs the vendor manifest guard. -- `pre-push` runs only the incremental repository typecheck. +- `pre-push` runs only the incremental repository typecheck (`tsc -b` over the root solution, covering both the host and client aggregates). The vendor manifest guard checks that changes under `vendor/*/src` are staged with the matching `vendor/README.md` manifest update. See `vendor/README.md` before editing vendored code. @@ -80,7 +99,7 @@ pnpm run test # unit tests pnpm run test:coverage # unit tests with per-file coverage gates pnpm run test:e2e # real-API tests; self-skips without DEEPSEEK_API_KEY pnpm run check:all # comprehensive opt-in gate set; not wired to Git hooks -pnpm run typecheck # build package/vendor outputs, then typecheck examples, tests, and scripts +pnpm run typecheck # tsc -b over the root solution: emits package/vendor lib/types, checks both aggregates pnpm run lint # eslint . pnpm run lint:fix # eslint . --fix pnpm run doc-typecheck # compile checked TypeScript snippets in Markdown docs diff --git a/docs/development.zh.md b/docs/development.zh.md index 8664a3291c..1f07c95dd6 100644 --- a/docs/development.zh.md +++ b/docs/development.zh.md @@ -33,7 +33,26 @@ pnpm exec lefthook install --force pnpm run typecheck ``` -首次类型检查会执行 package/vendor 的构建图,以及根目录下用于示例、测试和脚本的 no-emit `tsconfig.json` 项目图。根图使用同一份源码 `paths` 映射,但依赖 project references,因此 vendor 代码在它自己的 tsconfig 设置下被检查。 +首次类型检查会执行全仓 `tsc -b tsconfig.json` 图:发射每个 package/vendor 的 `lib/types`,并通过下述两个 no-emit 聚合检查示例、测试和脚本。 + +## TypeScript 项目布局 + +仓库的 TypeScript 配置只有三种角色;每个 tsconfig 文件恰好扮演其中一种。 + +| 文件 | 角色 | 是否构成 program? | +|---|---|---| +| `tsconfig.json` | solution 根:`extends` base、`files: []`、引用两个聚合。全仓 `tsc -b tsconfig.json` 图、tsserver 发现入口,并经继承的 `paths` 充当 tsx 运行 `examples/` 与 `scripts/` 时的解析配置(它们最近的 tsconfig 就是此文件)。 | 否 | +| `tsconfig.host.json` | host 聚合:host 侧各包(经 references)、示例、测试、脚本、website。排除 `packages/client`。 | 是 | +| `tsconfig.client.json` | client 聚合:`packages/client/*` 各包及其测试、`apps/web`。 | 是 | +| `tsconfig.base.json` | 共享 compilerOptions 与源码 `paths` 映射。同时是各 vitest 配置让 vite-tsconfig-paths 指向的解析门面:它没有 `include`,因此其 `paths` 适用于任何 importer。 | 否 | +| `tsconfig.base.client.json` | 浏览器编译形状(`jsx`、DOM lib、`types: []`),由 client 聚合和每个 `packages/client/*` 包 extends。 | 否 | + +host 与 client 保持两个聚合 program,是因为两侧在相同键下以不同服务对 cordis `Context` 接口做声明合并;单一 program 同时看到两份合并会报冲突。这种冲突只存在于 `ts.Program` 内部——模块解析永远不会触发它——所以 solution 可以同时引用两个聚合,一个 paths 门面也可以横跨两侧。由此推出两条纪律: + +- `tsconfig.base.json` 永不添加 `include` 或 `files`:它们会泄漏进每个 extends 它的包项目,并收窄门面的全匹配范围。 +- 构造全仓 `ts.Program` 的脚本显式种子 `tsconfig.host.json` 或 `tsconfig.client.json`——永不种子根 solution,因为把两个聚合展平进一个 program 会撞上 `Context` 合并冲突。基于 program 的生成器与门禁(`scripts/ts-project.ts` 的消费者、doc-typecheck standalone 模式)按决策仅覆盖 host 侧;client 侧只在出现真实需求时再获得基于 program 的工具。 + +静态分析和测试通过 base 的 `paths` 映射把工作区 import 解析到 `src`,且必须在干净树上通过;消费构建产物 `lib/` 的门禁显式声明该依赖。决策记录:[solution-root note](../.agents/notes/implemented/process/2026-07-22-tsconfig-solution-root-two-aggregates.md);tsc-first 发射管线见 [ts-build-config note](../.agents/notes/implemented/process/2026-06-17-ts-build-config.md)。 如果相关的本地检查需要使用构建后的包产物,请先构建一次: @@ -59,7 +78,7 @@ DEEPSEEK_BASE_URL=https://... # optional lefthook 在 `lefthook.yml` 中配置,作为快速的本地检查点: - `pre-commit` 运行对暂存文件的 ESLint 修复,检查暂存 diff 中的空白错误,并运行 vendor manifest(元数据清单)守卫; -- `pre-push` 只运行仓库增量类型检查。 +- `pre-push` 只运行仓库增量类型检查(对根 solution 执行 `tsc -b`,覆盖 host 与 client 两个聚合)。 vendor manifest 守卫检查 `vendor/*/src` 下的改动是否连同对应的 `vendor/README.md` manifest 更新一起暂存。请在编辑 vendor 代码前先阅读 `vendor/README.md`。 @@ -80,7 +99,7 @@ pnpm run test # unit tests pnpm run test:coverage # unit tests with per-file coverage gates pnpm run test:e2e # real-API tests; self-skips without DEEPSEEK_API_KEY pnpm run check:all # comprehensive opt-in gate set; not wired to Git hooks -pnpm run typecheck # build package/vendor outputs, then typecheck examples, tests, and scripts +pnpm run typecheck # tsc -b over the root solution: emits package/vendor lib/types, checks both aggregates pnpm run lint # eslint . pnpm run lint:fix # eslint . --fix pnpm run doc-typecheck # compile checked TypeScript snippets in Markdown docs diff --git a/docs/testing.md b/docs/testing.md index 85601ab6c9..85798cee3e 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -29,6 +29,10 @@ An e2e assertion re-runs the command or re-reads the file externally; a keyword - A guard only guards if the regression actually fails it. For a plugin without `inject` (bundle/composition plugins), a Loader smoke stays green under a broken export shape — add an explicit `expect('default' in mod).toBe(false)` plus an `unwrapExports` round-trip assertion, and prove it: introduce the regression, watch red, revert. - "Real entry path" means the published artifact: a package `bin` runs built `lib/bin.js` under plain `node`, exposing failures tsx masks (settle races, module resolution, swallowed load failures). The same applies to non-index runtime entries (the worker-thread sibling `lib/worker.cjs`) and singleton modules shared across bundles (`packages/ui/jsonrpc/tests/built-scope-carrier.e2e.ts`). Keep the built-artifact smokes green (`packages/ui/*/tests/built-bin.e2e.ts`, `packages/code-runtime/code-runtime-worker/tests/built-lib.e2e.ts`), and assert a genuinely-missing config exits non-zero. +## Test resolution: source plane only + +- Every vitest config points vite-tsconfig-paths at `tsconfig.base.json`; bare workspace imports resolve to `src` ([layout](development.md#typescript-project-layout)), never through package `exports` to built `lib/` — stale artifacts there load a second copy of module singletons. Built artifacts are consumed only explicitly: `lib`-mode subprocesses and the built smokes below. + ## Test subprocess launch modes - CI and build-having test lanes run every example or Cordis-config subprocess from built `lib/` through the shared dual-mode launcher. Do not hand-write `--import tsx` for these subprocesses. diff --git a/eslint.config.mjs b/eslint.config.mjs index 334d3799af..eec63181df 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -86,7 +86,8 @@ export default tseslint.config( languageOptions: { parserOptions: { // Same shared project service as the src block: test files resolve - // through the root tsconfig (its include covers every tests/ tree). + // through the root solution to tsconfig.host.json (its include covers + // every host tests/ tree). projectService: true, tsconfigRootDir: import.meta.dirname, }, diff --git a/lefthook.yml b/lefthook.yml index 49a1499976..cf2e6bb11d 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -20,4 +20,4 @@ pre-commit: pre-push: jobs: - name: typecheck - run: node_modules/.bin/tsc -b tsconfig.json --pretty false + run: node_modules/.bin/tsc -b --pretty false diff --git a/package.json b/package.json index 611e1d49e2..38db3a2ac7 100644 --- a/package.json +++ b/package.json @@ -14,10 +14,10 @@ "website" ], "scripts": { - "build": "tsc -b tsconfig.build.json && tsdown", + "build": "tsc -b && tsdown", "build:web": "pnpm --filter @deepseek-ai/dsh-frontend run build", "clean:build": "rm -rf .typecheck packages/*/*/lib vendor/*/lib *.tsbuildinfo", - "typecheck": "tsc -b tsconfig.json tsconfig.client.json", + "typecheck": "tsc -b", "lint": "eslint .", "lint:fix": "eslint . --fix", "duplication": "jscpd --config .jscpd.json packages scripts", diff --git a/packages/AGENTS.md b/packages/AGENTS.md index 4fc767173f..bb7fdfa839 100644 --- a/packages/AGENTS.md +++ b/packages/AGENTS.md @@ -19,6 +19,7 @@ These package-specific rules supplement the repo-wide [conventions](../AGENTS.md Naming notes: +- **Package tsconfig shape:** extends `tsconfig.base.json` (client: `tsconfig.base.client.json`), `rootDir: src`, `outDir: lib/types`, a `references` entry per workspace dependency plus `support/invariants`; registered in exactly one aggregate — host packages in `tsconfig.host.json`, client in `tsconfig.client.json` ([layout](../docs/development.md#typescript-project-layout)). - `src/types.ts` contains only types — no runtime code. - Tests live at package level under `tests/`, not `src/__tests__/`. - A package's README and JSDoc are part of the change: altered behavior (config keys, defaults, error codes, wire fields) updates them in the same commit. `doc-sync` gates what it can; apply [dsh-prose-standard](../.agents/skills/dsh-prose-standard/SKILL.md) for complete, concise prose and verify accuracy against code. diff --git a/packages/client/connection/tsconfig.json b/packages/client/connection/tsconfig.json index 3f45ca0231..8b0357cf97 100644 --- a/packages/client/connection/tsconfig.json +++ b/packages/client/connection/tsconfig.json @@ -1,14 +1,8 @@ { - "extends": "../../../tsconfig.base.json", + "extends": "../../../tsconfig.base.client.json", "compilerOptions": { "rootDir": "src", - "outDir": "lib/types", - "lib": [ - "ES2024", - "DOM", - "DOM.Iterable" - ], - "types": [] + "outDir": "lib/types" }, "include": [ "src" diff --git a/packages/client/i18n/tsconfig.json b/packages/client/i18n/tsconfig.json index 76dd83e8d6..63eb9779ca 100644 --- a/packages/client/i18n/tsconfig.json +++ b/packages/client/i18n/tsconfig.json @@ -1,14 +1,8 @@ { - "extends": "../../../tsconfig.base.json", + "extends": "../../../tsconfig.base.client.json", "compilerOptions": { "rootDir": "src", - "outDir": "lib/types", - "lib": [ - "ES2024", - "DOM", - "DOM.Iterable" - ], - "types": [] + "outDir": "lib/types" }, "include": [ "src" diff --git a/packages/client/runtime/src/client/store/index.ts b/packages/client/runtime/src/client/contract/store.ts similarity index 100% rename from packages/client/runtime/src/client/store/index.ts rename to packages/client/runtime/src/client/contract/store.ts diff --git a/packages/client/runtime/src/client/index.ts b/packages/client/runtime/src/client/index.ts index a403484280..77b51a68b5 100644 --- a/packages/client/runtime/src/client/index.ts +++ b/packages/client/runtime/src/client/index.ts @@ -12,7 +12,7 @@ import type { Context } from 'cordis' import type { ConnectionHandle, SessionId } from '@deepseek-ai/dsh-client-connection/client' import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots' -import type { SnapshotStore } from './store/index.ts' +import type { SnapshotStore } from './contract/store.ts' import { SlotsService } from './slots.ts' import { SessionsService } from './sessions/service.ts' import type { SessionListState } from './sessions/service.ts' @@ -28,10 +28,10 @@ export type { SessionBinding, SessionListState, SessionSummary } from './session // The snapshot-store engine lives here since the store migration (the data // layer owns its substrate; web-react is React glue only). The './client' // main export is the single serving door — no store subpath. -export { createSnapshotStore, defineStore, shallowEqual } from './store/index.ts' +export { createSnapshotStore, defineStore, shallowEqual } from './contract/store.ts' export type { EngineStoreHandle, EngineStoreInstance, ObservableSnapshot, SnapshotStore, -} from './store/index.ts' +} from './contract/store.ts' export type { AssistantBlock, AssistantMessageNode, ContextMessageNode, ConversationNode, ConversationSnapshot, PendingInteraction, RunningToolCall, SteeringMessageNode, diff --git a/packages/client/runtime/src/client/loader/index.ts b/packages/client/runtime/src/client/loader/index.ts index 1afa4b5eb5..81148a7dfc 100644 --- a/packages/client/runtime/src/client/loader/index.ts +++ b/packages/client/runtime/src/client/loader/index.ts @@ -17,7 +17,7 @@ * load one by one in inject topology. */ import type { Context } from 'cordis' -import { createSnapshotStore } from '../store/index.ts' +import { createSnapshotStore } from '../contract/store.ts' import type { BootPluginEntry, ClientLoader, LoaderStatus } from '../index.ts' export type { BootPluginEntry, ClientLoader, LoaderStatus } from '../index.ts' diff --git a/packages/client/runtime/src/client/sessions/service.ts b/packages/client/runtime/src/client/sessions/service.ts index eb4983c839..ec63b8f70d 100644 --- a/packages/client/runtime/src/client/sessions/service.ts +++ b/packages/client/runtime/src/client/sessions/service.ts @@ -16,8 +16,8 @@ import type { Context, Fiber } from 'cordis' import type { IApiClient, SessionId } from '@deepseek-ai/dsh-client-connection/client' import type { SessionCell } from '@deepseek-ai/dsh-client-ui-slots' -import type { SnapshotStore } from '../store/index.ts' -import { createSnapshotStore } from '../store/index.ts' +import type { SnapshotStore } from '../contract/store.ts' +import { createSnapshotStore } from '../contract/store.ts' import { SessionManager } from './manager.ts' import type { Session } from './session.ts' diff --git a/packages/client/runtime/src/client/sessions/session.ts b/packages/client/runtime/src/client/sessions/session.ts index 7330738de8..761aca96c2 100644 --- a/packages/client/runtime/src/client/sessions/session.ts +++ b/packages/client/runtime/src/client/sessions/session.ts @@ -7,7 +7,7 @@ import type { ContentBlock } from '@deepseek-ai/dsh-llm/types' import type { SessionEvent } from '@deepseek-ai/dsh-session/types' import type { HistoryEntry, IApiClient, MuxFrame, RpcError, RpcId, RpcResult, SessionId, ToolEventView } from '@deepseek-ai/dsh-client-connection/client' import { transportError } from '@deepseek-ai/dsh-client-connection/client' -import type { ObservableSnapshot } from '../store/index.ts' +import type { ObservableSnapshot } from '../contract/store.ts' import type { ConversationNode, ConversationSnapshot, OpenState, PendingInteraction, PromptError, RunningToolCall, } from './conversation.ts' diff --git a/packages/client/runtime/tests/store.spec.ts b/packages/client/runtime/tests/store.spec.ts index 22de90c0cf..3abd0483f2 100644 --- a/packages/client/runtime/tests/store.spec.ts +++ b/packages/client/runtime/tests/store.spec.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, it, vi } from 'vitest' -import { createSnapshotStore, defineStore, shallowEqual } from '../src/client/store/index.ts' +import { createSnapshotStore, defineStore, shallowEqual } from '../src/client/contract/store.ts' interface State { a: { n: number } diff --git a/packages/client/runtime/tsconfig.json b/packages/client/runtime/tsconfig.json index 82ebf4f989..9584ecc857 100644 --- a/packages/client/runtime/tsconfig.json +++ b/packages/client/runtime/tsconfig.json @@ -1,14 +1,8 @@ { - "extends": "../../../tsconfig.base.json", + "extends": "../../../tsconfig.base.client.json", "compilerOptions": { "rootDir": "src", - "outDir": "lib/types", - "lib": [ - "ES2024", - "DOM", - "DOM.Iterable" - ], - "types": [] + "outDir": "lib/types" }, "include": [ "src" diff --git a/packages/client/ui-conversation/tsconfig.json b/packages/client/ui-conversation/tsconfig.json index ed0065fa04..7755337093 100644 --- a/packages/client/ui-conversation/tsconfig.json +++ b/packages/client/ui-conversation/tsconfig.json @@ -1,15 +1,8 @@ { - "extends": "../../../tsconfig.base.json", + "extends": "../../../tsconfig.base.client.json", "compilerOptions": { "rootDir": "src", - "outDir": "lib/types", - "jsx": "react-jsx", - "lib": [ - "ES2024", - "DOM", - "DOM.Iterable" - ], - "types": [] + "outDir": "lib/types" }, "include": [ "src" diff --git a/packages/client/ui-layout/tsconfig.json b/packages/client/ui-layout/tsconfig.json index f2c03d8d12..c82482294b 100644 --- a/packages/client/ui-layout/tsconfig.json +++ b/packages/client/ui-layout/tsconfig.json @@ -1,15 +1,8 @@ { - "extends": "../../../tsconfig.base.json", + "extends": "../../../tsconfig.base.client.json", "compilerOptions": { "rootDir": "src", - "outDir": "lib/types", - "jsx": "react-jsx", - "lib": [ - "ES2024", - "DOM", - "DOM.Iterable" - ], - "types": [] + "outDir": "lib/types" }, "include": [ "src" diff --git a/packages/client/ui-primitives/tsconfig.json b/packages/client/ui-primitives/tsconfig.json index fa9b40f9e6..e2650b47b6 100644 --- a/packages/client/ui-primitives/tsconfig.json +++ b/packages/client/ui-primitives/tsconfig.json @@ -1,15 +1,8 @@ { - "extends": "../../../tsconfig.base.json", + "extends": "../../../tsconfig.base.client.json", "compilerOptions": { "rootDir": "src", - "outDir": "lib/types", - "jsx": "react-jsx", - "lib": [ - "ES2024", - "DOM", - "DOM.Iterable" - ], - "types": [] + "outDir": "lib/types" }, "include": [ "src" diff --git a/packages/client/ui-sidebar/tsconfig.json b/packages/client/ui-sidebar/tsconfig.json index a560748366..c48fe2567f 100644 --- a/packages/client/ui-sidebar/tsconfig.json +++ b/packages/client/ui-sidebar/tsconfig.json @@ -1,15 +1,8 @@ { - "extends": "../../../tsconfig.base.json", + "extends": "../../../tsconfig.base.client.json", "compilerOptions": { "rootDir": "src", - "outDir": "lib/types", - "jsx": "react-jsx", - "lib": [ - "ES2024", - "DOM", - "DOM.Iterable" - ], - "types": [] + "outDir": "lib/types" }, "include": [ "src" diff --git a/packages/client/ui-slots/tsconfig.json b/packages/client/ui-slots/tsconfig.json index fc829c7a83..b8ccb71310 100644 --- a/packages/client/ui-slots/tsconfig.json +++ b/packages/client/ui-slots/tsconfig.json @@ -1,14 +1,8 @@ { - "extends": "../../../tsconfig.base.json", + "extends": "../../../tsconfig.base.client.json", "compilerOptions": { "rootDir": "src", - "outDir": "lib/types", - "lib": [ - "ES2024", - "DOM", - "DOM.Iterable" - ], - "types": [] + "outDir": "lib/types" }, "include": [ "src" diff --git a/packages/client/ui-theme/tsconfig.json b/packages/client/ui-theme/tsconfig.json index 076aa22e9f..51f9171643 100644 --- a/packages/client/ui-theme/tsconfig.json +++ b/packages/client/ui-theme/tsconfig.json @@ -1,14 +1,8 @@ { - "extends": "../../../tsconfig.base.json", + "extends": "../../../tsconfig.base.client.json", "compilerOptions": { "rootDir": "src", - "outDir": "lib/types", - "lib": [ - "ES2024", - "DOM", - "DOM.Iterable" - ], - "types": [] + "outDir": "lib/types" }, "include": [ "src" diff --git a/packages/client/ui-trajectory/tsconfig.json b/packages/client/ui-trajectory/tsconfig.json index e3b1abe266..f525474d9d 100644 --- a/packages/client/ui-trajectory/tsconfig.json +++ b/packages/client/ui-trajectory/tsconfig.json @@ -1,15 +1,8 @@ { - "extends": "../../../tsconfig.base.json", + "extends": "../../../tsconfig.base.client.json", "compilerOptions": { "rootDir": "src", - "outDir": "lib/types", - "jsx": "react-jsx", - "lib": [ - "ES2024", - "DOM", - "DOM.Iterable" - ], - "types": [] + "outDir": "lib/types" }, "include": [ "src" diff --git a/packages/client/web-react/tsconfig.json b/packages/client/web-react/tsconfig.json index 57b533b2a6..90b7619fed 100644 --- a/packages/client/web-react/tsconfig.json +++ b/packages/client/web-react/tsconfig.json @@ -1,15 +1,8 @@ { - "extends": "../../../tsconfig.base.json", + "extends": "../../../tsconfig.base.client.json", "compilerOptions": { "rootDir": "src", - "outDir": "lib/types", - "jsx": "react-jsx", - "lib": [ - "ES2024", - "DOM", - "DOM.Iterable" - ], - "types": [] + "outDir": "lib/types" }, "include": [ "src" diff --git a/packages/client/web/tsconfig.json b/packages/client/web/tsconfig.json index 87a6b0dcab..f974787045 100644 --- a/packages/client/web/tsconfig.json +++ b/packages/client/web/tsconfig.json @@ -1,15 +1,8 @@ { - "extends": "../../../tsconfig.base.json", + "extends": "../../../tsconfig.base.client.json", "compilerOptions": { "rootDir": "src", - "outDir": "lib/types", - "jsx": "react-jsx", - "lib": [ - "ES2024", - "DOM", - "DOM.Iterable" - ], - "types": [] + "outDir": "lib/types" }, "include": [ "src" diff --git a/scripts/doc-budgets.manifest.json b/scripts/doc-budgets.manifest.json index 010f767c3e..7e4d154174 100644 --- a/scripts/doc-budgets.manifest.json +++ b/scripts/doc-budgets.manifest.json @@ -1,11 +1,11 @@ { - "AGENTS.md": 1600, + "AGENTS.md": 1680, "docs/AGENTS.md": 1150, "docs/architecture.md": 1800, "docs/cordis-primer.md": 600, "docs/defensive-patterns.md": 550, - "docs/testing.md": 960, + "docs/testing.md": 1020, "examples/AGENTS.md": 310, - "packages/AGENTS.md": 650, + "packages/AGENTS.md": 660, "packages/README.md": 760 } diff --git a/scripts/doc-typecheck.ts b/scripts/doc-typecheck.ts index 5034d65cae..9a9e007758 100644 --- a/scripts/doc-typecheck.ts +++ b/scripts/doc-typecheck.ts @@ -55,15 +55,20 @@ const configHost: ts.ParseConfigFileHost = { }, } -/** Load root settings and redirect workspace aliases to declarations from the coordinated build. */ +/** + * Load host-aggregate settings and redirect workspace aliases to declarations + * from the coordinated build. Doc fragments speak the host vocabulary; the host + * aggregate (never the root solution — it has no compilerOptions) carries the + * workspace paths via tsconfig.base.json. + */ function builtTypeCompilerOptions(): ts.CompilerOptions { - const configPath = join(root, 'tsconfig.json') + const configPath = join(root, 'tsconfig.host.json') const parsed = ts.getParsedCommandLineOfConfigFile(configPath, {}, configHost) if (!parsed) throw new Error(`doc-typecheck: cannot parse ${configPath}`) if (parsed.errors.length > 0) { throw new Error(parsed.errors.map(error => ts.flattenDiagnosticMessageText(error.messageText, '\n')).join('\n')) } - if (parsed.options.paths === undefined) throw new Error('doc-typecheck: root tsconfig has no workspace paths') + if (parsed.options.paths === undefined) throw new Error('doc-typecheck: host tsconfig has no workspace paths') const paths = Object.fromEntries(Object.entries(parsed.options.paths).map(([specifier, candidates]) => [ specifier, candidates.map(builtDeclarationPath), @@ -125,9 +130,14 @@ function formatDiagnostics(diagnostics: readonly ts.Diagnostic[], blocks: Block[ return remapBlockPaths(formatted, blocks) } -/** Reuse the repo typecheck graph references from a temp project one directory below root. */ +/** + * Reuse the host-aggregate references from a temp project one directory below + * root. Doc fragments speak the host vocabulary, so the standalone project + * seeds tsconfig.host.json (never the root solution: flattening host+client + * into one program collides the cordis Context merges). + */ function workspaceReferences(): { path: string }[] { - const file = join(root, 'tsconfig.json') + const file = join(root, 'tsconfig.host.json') // Parse with TypeScript's own JSONC reader: a regex comment stripper corrupts the `/*/` path // candidate in the workspace wildcard. const result = ts.readConfigFile(file, path => readFileSync(path, 'utf8')) @@ -144,7 +154,7 @@ function workspaceReferences(): { path: string }[] { /** The standalone temp project used when no coordinated build owns declaration freshness. */ function tempTsconfig(): string { return JSON.stringify({ - extends: '../tsconfig.json', + extends: '../tsconfig.host.json', compilerOptions: { noUnusedLocals: false, noUnusedParameters: false, diff --git a/scripts/gen-scoped-events.ts b/scripts/gen-scoped-events.ts index 4aa6637934..ead001a79b 100644 --- a/scripts/gen-scoped-events.ts +++ b/scripts/gen-scoped-events.ts @@ -370,7 +370,7 @@ function quote(value: string): string { /** * Render the generated scoped-event resolver module for one repository root. - * @param projectRoot - repository root carrying tsconfig.json. + * @param projectRoot - repository root carrying tsconfig.host.json. * @returns complete generated TypeScript source. */ export function renderScopedEvents(projectRoot: string = root): string { diff --git a/scripts/run-gates.ts b/scripts/run-gates.ts index 1c3958434c..896ffacfd1 100644 --- a/scripts/run-gates.ts +++ b/scripts/run-gates.ts @@ -228,7 +228,10 @@ function ciPrimaryGates(): Gate[] { ...docSyncLeafGates(), pnpmScript('module-graph', 'verify-module-graph', { label: 'module graph' }), pnpmScript('knip', 'knip'), - pnpmScript('build', 'build'), + // typecheck and build now drive the same root solution graph; without the + // dependency two concurrent `tsc -b` runs race the same tsbuildinfo files. + // The tsc step is an incremental no-op after typecheck. + pnpmScript('build', 'build', { needs: ['typecheck'] }), pnpmScript('publint', 'publint', { needs: ['build'] }), pnpmScript('node-next-types', 'verify-node-next-types', { label: 'node-next types', diff --git a/scripts/ts-project.ts b/scripts/ts-project.ts index 0b59aee873..930c055d4f 100644 --- a/scripts/ts-project.ts +++ b/scripts/ts-project.ts @@ -22,9 +22,13 @@ const configHost: ts.ParseConfigFileHost = { }, } -/** Parse a root tsconfig and flatten all referenced projects into one semantic graph. */ +/** + * Parse the host aggregate tsconfig and flatten all referenced projects into one + * semantic graph. Never seed the root solution: flattening host+client into one + * program collides the cordis Context merges. + */ function loadProjectGraph(projectRoot: string): ProjectGraph { - const rootConfigPath = resolve(projectRoot, 'tsconfig.json') + const rootConfigPath = resolve(projectRoot, 'tsconfig.host.json') const rootConfig = parseConfig(rootConfigPath) const rootNames = new Set() const visited = new Set() diff --git a/scripts/verify-cordis-config.ts b/scripts/verify-cordis-config.ts index c9820a67a8..916db4a168 100644 --- a/scripts/verify-cordis-config.ts +++ b/scripts/verify-cordis-config.ts @@ -152,12 +152,13 @@ function localPackageDirectories(): Map { } function rootProjectReferences(): Set { - // Typecheck runs two sibling aggregates (root = host program, - // tsconfig.client.json = client program; the two sides merge cordis Context - // under the same keys, so one program cannot see both). Seed both and follow - // any nested aggregate references to collect the covered leaf project set. + // The root solution references the host and client aggregates (the two + // sides merge cordis Context under the same keys, so one program cannot see + // both — but this BFS only collects reference paths, it never forms a + // program). Seed the solution and follow nested aggregate references to + // collect the covered leaf project set. const collected = new Set() - const queue = [resolve(root, 'tsconfig.json'), resolve(root, 'tsconfig.client.json')] + const queue = [resolve(root, 'tsconfig.json')] const seen = new Set() for (let file = queue.pop(); file !== undefined; file = queue.pop()) { if (seen.has(file)) continue diff --git a/tsconfig.base.client.json b/tsconfig.base.client.json new file mode 100644 index 0000000000..411c006da9 --- /dev/null +++ b/tsconfig.base.client.json @@ -0,0 +1,11 @@ +{ + // Client-side compiler shape shared by tsconfig.client.json and every + // packages/client/* package: browser lib surface, React JSX, no ambient + // node types (packages that need them override locally). + "extends": "./tsconfig.base.json", + "compilerOptions": { + "jsx": "react-jsx", + "lib": ["ES2024", "DOM", "DOM.Iterable"], + "types": [] + } +} diff --git a/tsconfig.base.json b/tsconfig.base.json index 28f7e2d0f3..2018a1c747 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1,4 +1,7 @@ { + // Doubles as the resolution facade for vite-tsconfig-paths (vitest configs + // point here). NEVER add include/files to this file: it would leak into + // every extending package project and narrow the facade's match-all scope. "compilerOptions": { "target": "es2024", "module": "esnext", @@ -87,8 +90,9 @@ // One wildcard maps every @deepseek-ai/dsh- to its source. Package // dir names are unique across groups, so first-on-disk-wins resolution is // unambiguous; adding a package under an existing group needs no edit - // here. The build graph's project references (tsconfig.build.json) stay - // explicit — TS project references have no wildcard form. + // here. The aggregates' project references (tsconfig.host.json / + // tsconfig.client.json) stay explicit — TS project references have no + // wildcard form. "@deepseek-ai/dsh-host-apiproxy": ["./packages/host/apiproxy/src"], "@deepseek-ai/dsh-host-apiproxy/client": ["./packages/host/apiproxy/src/fetch/client.ts"], "@deepseek-ai/dsh-host-apiproxy/*": ["./packages/host/apiproxy/src/*"], diff --git a/tsconfig.client.json b/tsconfig.client.json index ceb498a1a9..d7ee9090e7 100644 --- a/tsconfig.client.json +++ b/tsconfig.client.json @@ -4,14 +4,13 @@ // under the same keys (sessions, loader) with different services; shared // leaves (session/llm/tools/apiproxy/...) build once and are referenced by // both programs through each client package's own references. - "extends": "./tsconfig.base.json", + "extends": "./tsconfig.base.client.json", "compilerOptions": { "noEmit": true, "rewriteRelativeImportExtensions": false, - "jsx": "react-jsx", - "lib": ["ES2024", "DOM", "DOM.Iterable"], // Tests execute under vitest on node (e2e files spawn processes); browser - // purity of package src is each package's own tsconfig + verify-client-closure. + // purity of package src is each package's own tsconfig plus + // scripts/client-bundle-purity.spec.ts. "types": ["node"] }, "include": [ diff --git a/tsconfig.build.json b/tsconfig.host.json similarity index 88% rename from tsconfig.build.json rename to tsconfig.host.json index fc0050f34b..f340f235da 100644 --- a/tsconfig.build.json +++ b/tsconfig.host.json @@ -1,5 +1,25 @@ { - "files": [], + // Host aggregate (one of the two check units; see tsconfig.json). packages/client + // type-checks in tsconfig.client.json: the two sides merge cordis Context under + // the same keys, one program cannot see both. + "extends": "./tsconfig.base.json", + "compilerOptions": { + "noEmit": true, + "rewriteRelativeImportExtensions": false + }, + "include": [ + "examples/*/src/**/*.ts", + "examples/*/start.ts", + "examples/*/tests/**/*.ts", + "packages/*/*/tests/**/*.ts", + "scripts/**/*.ts", + "website/**/*.ts", + "website/.vitepress/**/*.ts" + ], + "exclude": [ + "packages/client/**", + "scripts/client-bundle-purity.spec.ts" + ], "references": [ { "path": "./vendor/cosmokit" }, { "path": "./vendor/schemastery" }, @@ -33,7 +53,9 @@ { "path": "./packages/goal/goal" }, { "path": "./packages/goal/tool-goal" }, { "path": "./packages/goal/goal-session" }, + { "path": "./packages/goal/command-goal" }, { "path": "./packages/context/time-context" }, + { "path": "./packages/context/session-reference" }, { "path": "./packages/ui/user-interaction" }, { "path": "./packages/ui/user-approval" }, { "path": "./packages/ui/permission" }, @@ -53,9 +75,6 @@ { "path": "./packages/pty/tool-pty" }, { "path": "./packages/code-runtime/code-runtime" }, { "path": "./packages/code-runtime/code-runtime-worker" }, - { "path": "./packages/compact/compact" }, - { "path": "./packages/compact/compact-basic" }, - { "path": "./packages/compact/compact-tool-result-prune" }, { "path": "./packages/llm/llm-deepseek" }, { "path": "./packages/llm/llm-pi-ai" }, { "path": "./packages/bash/bash-local" }, @@ -70,6 +89,9 @@ { "path": "./packages/fs/fs-sandbox" }, { "path": "./packages/fs/tool-fs" }, { "path": "./packages/fs/tool-fs-search" }, + { "path": "./packages/compact/compact" }, + { "path": "./packages/compact/compact-basic" }, + { "path": "./packages/compact/compact-tool-result-prune" }, { "path": "./packages/web/web" }, { "path": "./packages/web/web-search-exa" }, { "path": "./packages/web/web-search-perplexity" }, @@ -116,18 +138,6 @@ { "path": "./packages/host/apiproxy" }, { "path": "./packages/host/runtime" }, { "path": "./packages/host/webserver" }, - { "path": "./packages/client/ui-slots" }, - { "path": "./packages/client/ui-primitives" }, - { "path": "./packages/client/web-react" }, - { "path": "./packages/client/connection" }, - { "path": "./packages/client/runtime" }, - { "path": "./packages/client/ui-layout" }, - { "path": "./packages/client/ui-sidebar" }, - { "path": "./packages/client/ui-conversation" }, - { "path": "./packages/client/ui-trajectory" }, - { "path": "./packages/client/ui-theme" }, - { "path": "./packages/client/i18n" }, - { "path": "./packages/client/web" }, { "path": "./packages/sdk/helper" }, { "path": "./packages/sdk/scripts" }, { "path": "./packages/sdk/create-sdk" }, diff --git a/tsconfig.json b/tsconfig.json index c42fda4147..a98f88aae0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,149 +1,15 @@ { - // Host aggregate. packages/client type-checks in tsconfig.client.json: the two - // sides merge cordis Context under the same keys, one program cannot see both. + // Solution file: the whole-repo graph for `tsc -b tsconfig.json` and the + // tsserver entry. `extends` carries the base paths for get-tsconfig + // consumers — tsx running examples/ and scripts/ (no nearer tsconfig) + // resolves workspace imports through this file. `files: []` keeps it + // program-less, so the host/client cordis Context merges never meet. + // NEVER add include/files entries, and NEVER flatten this solution into a + // single ts.Program (scripts seed tsconfig.host.json or tsconfig.client.json). "extends": "./tsconfig.base.json", - "compilerOptions": { - "noEmit": true, - "rewriteRelativeImportExtensions": false - }, - "include": [ - "examples/*/src/**/*.ts", - "examples/*/start.ts", - "examples/*/tests/**/*.ts", - "packages/*/*/tests/**/*.ts", - "scripts/**/*.ts", - "website/**/*.ts", - "website/.vitepress/**/*.ts" - ], - "exclude": [ - "packages/client/**", - "scripts/client-bundle-purity.spec.ts" - ], + "files": [], "references": [ - { "path": "./vendor/cosmokit" }, - { "path": "./vendor/schemastery" }, - { "path": "./vendor/cordis" }, - { "path": "./vendor/loader" }, - { "path": "./vendor/include" }, - { "path": "./vendor/group" }, - { "path": "./vendor/timer" }, - { "path": "./vendor/hmr" }, - { "path": "./vendor/logger-console" }, - { "path": "./packages/util/brand" }, - { "path": "./packages/util/paths" }, - { "path": "./packages/util/timeout" }, - { "path": "./packages/util/retention" }, - { "path": "./packages/llm/llm" }, - { "path": "./packages/llm/token-meter" }, - { "path": "./packages/core/session" }, - { "path": "./packages/core/scope" }, - { "path": "./packages/session-persistence/session-persistence" }, - { "path": "./packages/session-persistence/session-checkpoint-policy" }, - { "path": "./packages/session-persistence/session-persistence-jsonl" }, - { "path": "./packages/session-persistence/session-persistence-sqlite" }, - { "path": "./packages/session-query/session-query" }, - { "path": "./packages/session-title/session-title" }, - { "path": "./packages/session-title/session-title-llm" }, - { "path": "./packages/session-title/session-title-first-message-llm" }, - { "path": "./packages/session-title/session-title-all-messages-llm" }, - { "path": "./packages/core/system-prompt" }, - { "path": "./packages/core/agent" }, - { "path": "./packages/ui/commands" }, - { "path": "./packages/goal/goal" }, - { "path": "./packages/goal/tool-goal" }, - { "path": "./packages/goal/goal-session" }, - { "path": "./packages/goal/command-goal" }, - { "path": "./packages/context/time-context" }, - { "path": "./packages/context/session-reference" }, - { "path": "./packages/ui/user-interaction" }, - { "path": "./packages/ui/user-approval" }, - { "path": "./packages/ui/permission" }, - { "path": "./packages/core/tools" }, - { "path": "./packages/skill/skill" }, - { "path": "./packages/skill/skill-local" }, - { "path": "./packages/skill/tool-skill" }, - { "path": "./packages/ui/tool-ask-user" }, - { "path": "./packages/context/workspace-context" }, - { "path": "./packages/core/agent-loop" }, - { "path": "./packages/llm/llm-retry" }, - { "path": "./packages/examples/agent-spine-demo" }, - { "path": "./packages/examples/cli-demo" }, - { "path": "./packages/bash/bash" }, - { "path": "./packages/pty/pty" }, - { "path": "./packages/pty/pty-local" }, - { "path": "./packages/pty/tool-pty" }, - { "path": "./packages/code-runtime/code-runtime" }, - { "path": "./packages/code-runtime/code-runtime-worker" }, - { "path": "./packages/llm/llm-deepseek" }, - { "path": "./packages/llm/llm-pi-ai" }, - { "path": "./packages/bash/bash-local" }, - { "path": "./packages/sandbox/sandbox" }, - { "path": "./packages/sandbox/sandbox-local" }, - { "path": "./packages/sandbox/sandbox-policy" }, - { "path": "./packages/bash/bash-sandbox" }, - { "path": "./packages/bash/tool-bash" }, - { "path": "./packages/fs/fs" }, - { "path": "./packages/fs/fs-local" }, - { "path": "./packages/fs/fs-policy" }, - { "path": "./packages/fs/fs-sandbox" }, - { "path": "./packages/fs/tool-fs" }, - { "path": "./packages/fs/tool-fs-search" }, - { "path": "./packages/compact/compact" }, - { "path": "./packages/compact/compact-basic" }, - { "path": "./packages/compact/compact-tool-result-prune" }, - { "path": "./packages/web/web" }, - { "path": "./packages/web/web-search-exa" }, - { "path": "./packages/web/web-search-perplexity" }, - { "path": "./packages/web/web-search-deepseek" }, - { "path": "./packages/web/web-fetch-local" }, - { "path": "./packages/web/tool-web" }, - { "path": "./packages/spill/spill" }, - { "path": "./packages/spill/spill-local" }, - { "path": "./packages/spill/spill-policy" }, - { "path": "./packages/timeout/timeout-policy" }, - { "path": "./packages/support/invariants" }, - { "path": "./packages/support/agent-loop-testkit" }, - { "path": "./packages/ui/acp" }, - { "path": "./packages/examples/acp-demo" }, - { "path": "./packages/ui/app-boot" }, - { "path": "./packages/ui/jsonrpc" }, - { "path": "./packages/examples/jsonrpc-demo" }, - { "path": "./packages/ui/tui" }, - { "path": "./packages/examples/tui-demo" }, - { "path": "./packages/support/llm-replay" }, - { "path": "./packages/support/acp-snapshot" }, - { "path": "./packages/support/loader-smoke" }, - { "path": "./packages/subagent/subagent" }, - { "path": "./packages/subagent/tool-subagent" }, - { "path": "./packages/subagent/subagent-inprocess" }, - { "path": "./packages/subagent/subagent-subprocess" }, - { "path": "./packages/subagent/subagent-spawn" }, - { "path": "./packages/subagent/subagent-fork" }, - { "path": "./packages/subagent/subagent-acp" }, - { "path": "./packages/tasks/tasks" }, - { "path": "./packages/tasks/tool-tasks" }, - { "path": "./packages/workflow/workflow" }, - { "path": "./packages/workflow/workflow-workerthread" }, - { "path": "./packages/workflow/tool-workflow" }, - { "path": "./packages/workflow/tool-ralph" }, - { "path": "./packages/todo/tool-todo" }, - { "path": "./packages/plan/plan-mode" }, - { "path": "./packages/guard/repeat-tool-guard" }, - { "path": "./packages/cordis/tool-cordis" }, - { "path": "./packages/hooks/hook-protocol" }, - { "path": "./packages/hooks/hooks-claude" }, - { "path": "./packages/hooks/hooks-codex" }, - { "path": "./packages/mcp/mcp-client" }, - { "path": "./packages/host/apiproxy" }, - { "path": "./packages/host/runtime" }, - { "path": "./packages/host/webserver" }, - { "path": "./packages/sdk/helper" }, - { "path": "./packages/sdk/scripts" }, - { "path": "./packages/sdk/create-sdk" }, - { "path": "./packages/sdk/telemetry" }, - { "path": "./packages/lsp/lsp" }, - { "path": "./packages/lsp/lsp-local" }, - { "path": "./packages/lsp/tool-lsp" }, - { "path": "./apps/cli" } + { "path": "./tsconfig.host.json" }, + { "path": "./tsconfig.client.json" } ] } diff --git a/tsconfig.vitest.json b/tsconfig.vitest.json deleted file mode 100644 index 9cb8a888c6..0000000000 --- a/tsconfig.vitest.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - // Path-map scope for vite-tsconfig-paths in vitest only (never a tsc -b target): - // widens include to package src and .tsx so bare workspace imports resolve to source. - "extends": "./tsconfig.base.json", - "include": [ - "examples/*/src/**/*.ts", - "examples/*/start.ts", - "examples/*/tests/**/*.ts", - "packages/*/*/src/**/*.ts", - "packages/*/*/src/**/*.tsx", - "packages/*/*/tests/**/*.ts", - "packages/*/*/tests/**/*.tsx", - "scripts/**/*.ts" - ] -} diff --git a/tsdown.config.ts b/tsdown.config.ts index 8ad1a5c41c..41a490436f 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -2,7 +2,7 @@ import { defineConfig } from 'tsdown' /** * JS bundling for vendored Cordis and Harness TypeScript packages. - * TypeScript source is compiled first by `tsc -b tsconfig.build.json`; tsdown + * TypeScript source is compiled first by `tsc -b` (the root solution); tsdown * reads only the emitted JS under lib/types and writes the package root and * invariant companion runtime bundles. Declarations are NOT produced here, * hence `dts: false`. diff --git a/vitest.config.ts b/vitest.config.ts index 408f100130..1177782a8a 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,6 +1,12 @@ import tsconfigPaths from 'vite-tsconfig-paths' import { defineConfig } from 'vitest/config' +// Resolution facade shared by every plugin instance below: tsconfig.base.json +// has no include, which vite-tsconfig-paths treats as match-all, so its paths +// map applies to every test file. paths must win over package exports so built +// lib/ never loads a second module-singleton copy. +const pathsPlugin = (): ReturnType => tsconfigPaths({ projects: ['./tsconfig.base.json'] }) + const windowsUnsupportedPackages = process.platform === 'win32' ? [ 'packages/bash/*', @@ -40,12 +46,7 @@ const processBoundTests = [ ] export default defineConfig({ - // Native path resolution reads each package's nearest tsconfig, but only the root defines - // workspace paths. Keep this plugin pinned to the root map so bare package imports resolve - // to source — with built lib/ present, manifest-exports fallthrough would load a second - // copy of module singletons. tsconfig.vitest.json widens include to .tsx specs (the root - // include stops at .ts for tsc -b; the plugin scopes applicability by include). - plugins: [tsconfigPaths({ projects: ['./tsconfig.vitest.json'] })], + plugins: [pathsPlugin()], test: { setupFiles: ['./scripts/test-invariants.ts'], // .tsx: client component specs (jsdom via per-file @vitest-environment pragma). @@ -55,7 +56,7 @@ export default defineConfig({ // for lower startup/IPC overhead; only explicit process-bound suites fork. projects: [ { - plugins: [tsconfigPaths({ projects: ['./tsconfig.vitest.json'] })], + plugins: [pathsPlugin()], test: { name: 'thread-safe', pool: 'threads', @@ -68,7 +69,7 @@ export default defineConfig({ }, }, { - plugins: [tsconfigPaths({ projects: ['./tsconfig.vitest.json'] })], + plugins: [pathsPlugin()], test: { name: 'process-bound', pool: 'forks', diff --git a/vitest.e2e.config.ts b/vitest.e2e.config.ts index 97be1395b3..2e2221b6e8 100644 --- a/vitest.e2e.config.ts +++ b/vitest.e2e.config.ts @@ -29,13 +29,13 @@ const e2eMaxWorkers = positiveIntFromEnv('DSH_E2E_MAX_WORKERS', DEFAULT_E2E_MAX_ export default defineConfig({ // Same resolution note as vitest.config.ts: bare workspace names resolve - // through the vitest-scoped tsconfig paths map (its include spans package - // src, so client-package sources get mapping too — the root tsconfig - // excludes packages/client, which would drop /client subpath imports onto - // package exports and load browser dist bundles into node). Built-artifact - // e2e suites are unaffected: their built-ness lives in subprocesses and - // createRequire lookups, which bypass vite resolution entirely. - plugins: [tsconfigPaths({ projects: ['./tsconfig.vitest.json'] })], + // through the tsconfig.base.json paths facade (no include = match-all, so + // client-package sources get mapping too — dropping /client subpath imports + // onto package exports would load browser dist bundles into node). + // Built-artifact e2e suites are unaffected: their built-ness lives in + // subprocesses and createRequire lookups, which bypass vite resolution + // entirely. + plugins: [tsconfigPaths({ projects: ['./tsconfig.base.json'] })], test: { setupFiles: ['./scripts/test-invariants.ts'], include: ['packages/*/*/tests/**/*.e2e.ts', 'examples/*/tests/**/*.e2e.ts'], diff --git a/vitest.snapshot.config.ts b/vitest.snapshot.config.ts index 142528e604..24764cf6c7 100644 --- a/vitest.snapshot.config.ts +++ b/vitest.snapshot.config.ts @@ -36,8 +36,9 @@ if (process.env.DSH_SNAPSHOT === 'record') { export default defineConfig({ // Same resolution note as vitest.config.ts: bare workspace names resolve - // through the root tsconfig paths map; the native option cannot do this. - plugins: [tsconfigPaths({ projects: ['./tsconfig.json'] })], + // through the tsconfig.base.json paths facade; the native option cannot do + // this (the root tsconfig is a solution file with no paths). + plugins: [tsconfigPaths({ projects: ['./tsconfig.base.json'] })], test: { setupFiles: ['./scripts/test-invariants.ts'], include: [ diff --git a/vitest.web.config.ts b/vitest.web.config.ts index 765fb2e4a9..de220c9f12 100644 --- a/vitest.web.config.ts +++ b/vitest.web.config.ts @@ -1,4 +1,4 @@ -import { fileURLToPath } from 'node:url' +import tsconfigPaths from 'vite-tsconfig-paths' import { defineConfig } from 'vitest/config' // Web smoke lane (GUI, gate-exempt — not part of the CI sequence yet): built @@ -13,13 +13,10 @@ try { } export default defineConfig({ - resolve: { - alias: { - // apps/web/tests is outside the root tsconfig include, so the shared - // tsconfig-paths plugin never maps it; alias the one bare import to source. - '@deepseek-ai/dsh-host-webserver': fileURLToPath(new URL('packages/host/webserver/src/index.ts', import.meta.url)), - }, - }, + // Same resolution note as vitest.config.ts: the tsconfig.base.json paths + // facade has no include (match-all), so apps/web/tests resolves bare + // workspace imports to source like every other lane. + plugins: [tsconfigPaths({ projects: ['./tsconfig.base.json'] })], test: { include: ['apps/web/tests/**/*.e2e.ts'], // Browser boot + real-model turns are slow; files share one browser, run serial. diff --git a/website/.vitepress/config.ts b/website/.vitepress/config.ts index 3ea513a0bb..da3892eaa2 100644 --- a/website/.vitepress/config.ts +++ b/website/.vitepress/config.ts @@ -13,6 +13,7 @@ const sectionOrder = [ '基础', '框架能力', '实战', + 'Cordis 教程', '概念', '生成参考', 'Cordis API', @@ -22,6 +23,7 @@ const sectionOrder = [ 'Basics', 'Framework', 'Practice', + 'Cordis tutorial', 'Concepts', 'Generated reference', 'Cordis Core API', diff --git a/website/docs.ts b/website/docs.ts index 9346670806..ee7ad64061 100644 --- a/website/docs.ts +++ b/website/docs.ts @@ -206,6 +206,26 @@ const develop = pairedPages([ }, ]) +const cordisTutorial = mirroredPages(([ + ['index.md', 'Cordis 教程', 'Cordis tutorial'], + ['01-first-plugin.md', '1. 第一个插件', '1. Your first plugin'], + ['02-lifecycle-and-effects.md', '2. 生命周期与副作用', '2. Lifecycle and effects'], + ['03-services.md', '3. 服务', '3. Services'], + ['04-events.md', '4. 事件', '4. Events'], + ['05-config.md', '5. 配置', '5. Configuration'], + ['06-composition-and-hmr.md', '6. 组合与热重载', '6. Composition and HMR'], + ['07-into-the-harness.md', '7. 进入 Harness', '7. Into the harness'], +] as const).map(([file, rootLabel, enLabel], order): MirroredPage => ({ + source: `docs/cordis-tutorial/${file}`, + route: `develop/cordis-tutorial/${file}`, + contentLocale: 'en-US', + label: { root: rootLabel, en: enLabel }, + sidebar: { root: 'zh-develop', en: 'en-develop' }, + section: { root: 'Cordis 教程', en: 'Cordis tutorial' }, + order, + ...(file === 'index.md' ? { sourceAliases: ['docs/cordis-tutorial'] } : {}), +}))) + const reference = mirroredPages([ ...([ ['docs/architecture.md', 'reference/index.md', '架构', 'Architecture'], @@ -304,5 +324,6 @@ const reference = mirroredPages([ export const docsPages: DocsPage[] = [ ...homeAndGuide, ...develop, + ...cordisTutorial, ...reference, ]