fix(dependencies): check experimental packages under the published dependency policy

verify-package-dependencies now covers every packages/*/* manifest, experimental included, and reads each package's tsdown build entries so Worker and child-process entries count as Host runtime sources. Experimental Client packages no longer declare browser and type relationships as peers.
This commit is contained in:
imccyu committed 2026-10-15 03:56:26 +08:00
1 parent b77cc1c0f2
commit 22c51728ed
19 files changed
+436 -256

No files matched your search

@@ -12,8 +12,8 @@
en: a15c7c13f167e5ac
zh: 63bcc2ef60c2f344
/agent-note-published-dependency-faces-and-bounded-peer-relays/decision/package-selection:
en: 5ea13b09ae774437
zh: ef9dbccada39107a
en: 931b678bcdb42dbe
zh: 95dbebdb03c50d1f
/agent-note-published-dependency-faces-and-bounded-peer-relays/decision/dependency-sections:
en: d34ebac6c1c7642b
zh: 2cf3a60df14b072a
@@ -14,7 +14,7 @@ The package that chooses a Client build input is the shipped profile, while a Ho
### Package selection
[`verify-package-dependencies`](../../../../scripts/verify-package-dependencies.ts) owns dependency-section policy. It always covers packages under `packages/client/` and every non-experimental package that declares `dsh.client`. Inside the directory, `dsh.client` marks a Client/Host package whose Host entry is scanned; a package without that declaration is a Client-only static build input. Outside the directory, `dsh.client` selects the same Client/Host scan. A `"./client"` export alone is an API and does not select npm dependency policy.
[`verify-package-dependencies`](../../../../scripts/verify-package-dependencies.ts) owns dependency-section policy. It always covers packages under `packages/client/` and every package that declares `dsh.client`, experimental packages included. Inside the directory, `dsh.client` marks a Client/Host package whose Host entry is scanned; a package without that declaration is a Client-only static build input. Outside the directory, `dsh.client` selects the same Client/Host scan. A `"./client"` export alone is an API and does not select npm dependency policy. [Experimental packages follow the same dependency declaration rules](2026-10-14-experimental-dependency-policy.md) records why the selection includes experimental packages.
[`package-dependency-policy.ts`](../../../../scripts/package-dependency-policy.ts) provides explicit Client-face include and exclude lists. An include handles an exceptional package without `dsh.client`, while an exclude removes an automatically discovered dual-face package outside `packages/client/`. The verifier rejects unknown, stale, redundant, duplicate, overlapping, and ineffective entries. The include list is empty; the exclude list contains `@deepseek-ai/dsh-api-session-controller` and `@deepseek-ai/dsh-api-workspace-controller`. Adding Session Controller back would migrate nine more Host edges while its five-run candidate retest improved median resolution by only 0.15 seconds.
@@ -14,7 +14,7 @@ Client 构建输入由发布 profile 选择,而 Host value import 由导入它
### 包选择
[`verify-package-dependencies`](../../../../scripts/verify-package-dependencies.ts) 统一负责依赖区段策略。它始终覆盖 `packages/client/` 下的包,以及声明 `dsh.client` 的每个非实验包。在该目录内,`dsh.client` 标记需要扫描 Host 入口的 Client/Host 包;没有该声明的包是仅供 Client 编译的静态输入。在目录外,`dsh.client` 选择相同的 Client/Host 扫描。仅有 `"./client"` export 只是 API,不参与 npm 依赖策略选包。
[`verify-package-dependencies`](../../../../scripts/verify-package-dependencies.ts) 统一负责依赖区段策略。它始终覆盖 `packages/client/` 下的包,以及声明 `dsh.client` 的每个包,实验包也包括在内。在该目录内,`dsh.client` 标记需要扫描 Host 入口的 Client/Host 包;没有该声明的包是仅供 Client 编译的静态输入。在目录外,`dsh.client` 选择相同的 Client/Host 扫描。仅有 `"./client"` export 只是 API,不参与 npm 依赖策略选包。[实验包遵守同一套依赖声明规则](2026-10-14-experimental-dependency-policy.zh.md)记录了选包为何包括实验包。
[`package-dependency-policy.ts`](../../../../scripts/package-dependency-policy.ts) 提供显式 Client 门面 include 与 exclude 列表。include 用于没有 `dsh.client` 的例外包,exclude 用于移除 `packages/client/` 之外自动发现的双面包。验证器拒绝未知、失效、冗余、重复、相互重叠和无法生效的配置项。include 列表为空;exclude 列表包含 `@deepseek-ai/dsh-api-session-controller` 和 `@deepseek-ai/dsh-api-workspace-controller`。把 Session Controller 加回会多迁移九条 Host 边,而五次候选复测的 resolver 中位数仅改善 0.15 秒。
@@ -0,0 +1,22 @@
# Bilingual-pair consistency record for 2026-10-14-experimental-dependency-policy.md (docs/i18n/README.md): per heading
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
# After editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-10-14-experimental-dependency-policy.md
/agent-note-experimental-packages-follow-the-same-dependency-declaration-rules:
en: ec44b9c40ce4bb25
zh: 2e5731c96fa82ba8
/agent-note-experimental-packages-follow-the-same-dependency-declaration-rules/problem:
en: ee78aec260872b62
zh: 005e4c3c2b54a9ef
/agent-note-experimental-packages-follow-the-same-dependency-declaration-rules/decision:
en: 081508647fade940
zh: 496cffe0fba8544f
/agent-note-experimental-packages-follow-the-same-dependency-declaration-rules/alternatives-considered:
en: 28c320fb3547502d
zh: 9563fd1709ed8b54
/agent-note-experimental-packages-follow-the-same-dependency-declaration-rules/consequences:
en: cc50e617e4893a30
zh: c2d2b9efd2955127
/agent-note-experimental-packages-follow-the-same-dependency-declaration-rules/testing:
en: 9542331ca97ac00a
zh: 34857e7b85128c7b
@@ -0,0 +1,40 @@
# Agent Note: Experimental packages follow the same dependency declaration rules
Status: implemented
English | [中文](2026-10-14-experimental-dependency-policy.zh.md)
## Problem
A [verify-package-dependencies](../../../../scripts/verify-package-dependencies.ts) scope of `packages/!(experimental)/*/package.json` exempts the whole experimental group from the rule that browser and type relationships belong only in devDependencies ([client dependency rules](../../../../packages/client/AGENTS.md#dependency-declaration)), and [published dependency faces](2026-08-26-published-dependency-faces.md) stated its coverage as "non-experimental packages". Under that scope, 4 experimental client packages declared client, api, session, and similar packages as peerDependencies. The webworker packer follows workspace peers into the image, so browser static libraries became sweep roots, and packing agent-team-profile produced 82 unresolvable requests. The [experimental package rules](../../../../packages/experimental/AGENTS.md) state that experimental status does not relax engineering requirements, which that exemption contradicts.
Checking the 6 experimental packages that declare `dsh.client` under the same rules found 46 violations across the full run, all in experimental packages. In addition, a verifier that traces runtime imports only from the Host entry misclassifies inspector's `ws`, which is used only in its worker_threads entry, as devDependencies-only.
## Decision
- The check reads `packages/*/*/package.json`, so experimental packages follow exactly the release rules with no group-level exemption, and the dependency-section check carries no other experimental exception.
- The verifier also scans each package's own Host-side worker and child-process entries, identified from the build entries the package's `tsdown.config.ts` declares. `readHostBuildEntries` imports that config, evaluates its default export once under the `host` build face and once under the `client` build face (`DSH_BUILD_FACE`), combines the results, and keeps entries whose platform is not browser and whose source is outside `src/client/`. The verifier does not guess from source patterns. The configurations can only be imported asynchronously, so the verifier's state reading is async.
- The affected experimental manifests and the lockfile follow the policy; every entry moved out of `peerDependencies` or `dependencies` was confirmed not to be a runtime requirement. cot-translation applies `instanceof` to translator's `TranslationError`, which requires the same class instance, so `peerRequiredHostExports` classifies that export.
- Host-only experimental packages, like release packages, are checked only when listed in `hostPackages`. Whether to extend the check to every Host-only package is a separate decision.
- This decision partially supersedes the package-selection coverage of [published dependency faces](2026-08-26-published-dependency-faces.md), whose package-selection paragraph links back here; that note's other decisions remain in force.
## Alternatives considered
**Fix only these manifests without changing the check scope.** The same declarations would soon return; a rule without an enforcement point is not a rule.
**Skip browser static libraries as sweep roots in the packer.** This only works around the symptom; the incorrect manifests still affect npm installation and other consumers.
## Consequences
`pnpm run verify-package-dependencies` covers experimental packages that declare `dsh.client`, so the manifests it accepts let the webworker packer pack agent-team-profile and voice-input-bundle, and inspector's published manifest keeps its worker runtime dependencies.
The trade-off costs:
- Published experimental manifests list browser and type relationships only as dev dependencies, so users who install these packages individually from npm receive fewer peer hints.
- The verifier cannot see imports injected by a tsdown `banner`, such as the inspector worker's `@deepseek-ai/dsh-app-boot/worker/profile-resolution-bootstrap`. Such a dependency stays only because it is declared; if it is removed by mistake, the verifier does not detect it.
- `readHostBuildEntries` imports the `tsdown.config.ts` of every covered package with a Host face and evaluates its default export under both build faces, so the gate depends on every such config module importing cleanly, and the modules' import side effects run in the gate process; for example, the inspector's config imports its package-local [`scripts/build-devtools.ts`](../../../../packages/experimental/inspector/scripts/build-devtools.ts).
- Measured: loading the 127 additional tsdown configurations takes about 2 s, and the full-run duration stays essentially the same (4:27 without the scan, 4:21 with it).
## Testing
[verify-package-dependencies.spec.ts](../../../../scripts/verify-package-dependencies.spec.ts) pins three cases: an experimental client package that declares a browser package as a peer is rejected, an experimental package listed in `hostPackages` is checked as a configured Host package, and a runtime dependency used only in a worker entry named by `tsdown.config.ts` stays in dependencies. The same spec pins the peer-required classification of translator's `TranslationError`.
@@ -0,0 +1,40 @@
# Agent Note: 实验包遵守同一套依赖声明规则
Status: implemented
[English](2026-10-14-experimental-dependency-policy.md) | 中文
## 问题
如果 [verify-package-dependencies](../../../../scripts/verify-package-dependencies.ts) 的检查范围是 `packages/!(experimental)/*/package.json`,整个 experimental 组就不受“浏览器和类型关系只放 devDependencies”([client 依赖规则](../../../../packages/client/AGENTS.md#dependency-declaration))的约束,[发布依赖面](2026-08-26-published-dependency-faces.zh.md)也曾把覆盖范围写成“非实验包”。在这一范围下,有 4 个实验 client 包把 client、api、session 等包写成 peerDependencies。webworker packer 会沿 workspace peer 把这些包收进镜像,浏览器静态库因此成了扫描起点,agent-team-profile 打包时出现 82 条无法解析的请求。[实验包规则](../../../../packages/experimental/AGENTS.md)写明实验身份不放宽工程要求,这一豁免与之矛盾。
按同一套规则检查 6 个带 `dsh.client` 的实验包,全量共 46 条违规,全部出在实验包。另外,只从 Host 入口追踪运行时导入的 verifier 会把 inspector 的 `ws` 误判为只该放 devDependencies,因为它只在 worker_threads 入口里使用。
## 决策
- 检查读取 `packages/*/*/package.json`,experimental 包与正式包遵守完全相同的规则,不设组级豁免,依赖区段检查也没有其他针对实验包的例外。
- verifier 同时扫描包自己的 Host 侧 worker 和子进程入口,识别依据是包 `tsdown.config.ts` 声明的构建入口。`readHostBuildEntries` import 这份配置,在 `host` 和 `client` 两个构建 face(`DSH_BUILD_FACE`)下各求值一次默认导出并取并集,保留 platform 不是 browser、源码不在 `src/client/` 下的 entry。不靠猜源码写法。配置只能异步 import,所以 verifier 的状态读取是 async。
- 受影响的实验包清单和 lockfile 符合策略;从 `peerDependencies` 或 `dependencies` 移走的每一项都确认过不是运行时必需。cot-translation 对 translator 的 `TranslationError` 做 `instanceof` 判断,需要同一个类实例,因此 `peerRequiredHostExports` 收录了这个导出。
- 纯 Host 的实验包与正式包一样,只有列入 `hostPackages` 才受检查。是否扩大到全部纯 Host 包,另行决定。
- 本决定部分取代[发布依赖面](2026-08-26-published-dependency-faces.zh.md)的选包覆盖范围,该 Note 的选包段落链接回本 Note;它的其余决定保持有效。
## 考虑过的替代方案
**只修这几个清单,不改检查范围。** 很快会再出现;规则没有执行点等于没有规则。
**在 packer 里跳过浏览器静态库这类扫描起点。** 只是绕开症状,错误的清单还会影响 npm 安装和其他消费方。
## 后果
`pnpm run verify-package-dependencies` 覆盖带 `dsh.client` 的实验包,它接受的清单让 webworker packer 能打包 agent-team-profile 和 voice-input-bundle,inspector 的发布清单也保留它的 worker 运行时依赖。
这一取舍的代价:
- 发布的实验包清单只把浏览器和类型关系列为 dev 依赖,通过 npm 单独安装这些包的用户得到的 peer 提示更少。
- verifier 看不到 tsdown `banner` 注入的导入,例如 inspector worker 的 `@deepseek-ai/dsh-app-boot/worker/profile-resolution-bootstrap`。这类依赖只因为已经声明而被保留;如果被误删,verifier 发现不了。
- `readHostBuildEntries` 会 import 每个受覆盖、带 Host face 的包的 `tsdown.config.ts`,并在两个构建 face 下求值其默认导出,所以门禁依赖每个这样的配置模块都能干净地 import,这些模块的 import 副作用也会在门禁进程里执行;例如 inspector 的配置会 import 包内的 [`scripts/build-devtools.ts`](../../../../packages/experimental/inspector/scripts/build-devtools.ts)。
- 实测:新增的 127 个 tsdown 配置加载约 2 s,全量耗时基本不变(不扫描时 4:27,扫描时 4:21)。
## 测试
[verify-package-dependencies.spec.ts](../../../../scripts/verify-package-dependencies.spec.ts) 锁定三种情形:实验 client 包把浏览器包写成 peer 时会被拒绝;列入 `hostPackages` 的实验包按 configured Host 包检查;只在 `tsdown.config.ts` 指定的 worker 入口里使用的运行时依赖保留在 dependencies。同一份 spec 还锁定 translator 的 `TranslationError` 属于 peer-required 分类。
+6 -49
View File
@@ -493,9 +493,6 @@ flowchart TD
pkg_hmr --> pkg_app_boot
pkg_hmr --> pkg_cmdline
pkg_experimental_computer_use_cua_driver_mcp --> pkg_computer_use
pkg_experimental_inspector --> pkg_client_connection
pkg_experimental_inspector --> pkg_client_modules
pkg_experimental_inspector --> pkg_host_webserver
pkg_experimental_webworker_runtime --> pkg_client_connection
pkg_experimental_webworker_runtime --> pkg_client_modules
pkg_experimental_webworker_runtime --> pkg_host_webserver
@@ -604,6 +601,7 @@ flowchart TD
pkg_api_workspace_controller --> pkg_workspace
pkg_experimental_api_speech_to_text --> pkg_experimental_speech_to_text
pkg_experimental_api_speech_to_text --> pkg_typert_protocol
pkg_experimental_client_ui_cot_translation --> pkg_experimental_translator
pkg_experimental_speech_to_text_sensevoice --> pkg_experimental_speech_to_text
pkg_experimental_speech_to_text_sensevoice --> pkg_subprocess
pkg_hook_protocol --> pkg_session
@@ -1353,47 +1351,6 @@ flowchart TD
pkg_workflow_ptc --> pkg_tools
pkg_workflow_ptc --> pkg_workflow
pkg_workflow_ptc --> pkg_working_directory
pkg_experimental_client_ui_agent_team --> pkg_api_session_controller
pkg_experimental_client_ui_agent_team --> pkg_client_locale
pkg_experimental_client_ui_agent_team --> pkg_client_ui_conversation
pkg_experimental_client_ui_agent_team --> pkg_client_ui_primitives
pkg_experimental_client_ui_agent_team --> pkg_client_ui_renderer
pkg_experimental_client_ui_agent_team --> pkg_client_ui_session
pkg_experimental_client_ui_agent_team --> pkg_client_ui_slots
pkg_experimental_client_ui_agent_team --> pkg_client_ui_workspace
pkg_experimental_client_ui_agent_team --> pkg_experimental_agent_team
pkg_experimental_client_ui_agent_team --> pkg_session
pkg_experimental_client_ui_claude_code_mods --> pkg_api_remotes
pkg_experimental_client_ui_claude_code_mods --> pkg_api_session_controller
pkg_experimental_client_ui_claude_code_mods --> pkg_client_locale
pkg_experimental_client_ui_claude_code_mods --> pkg_client_ui_conversation
pkg_experimental_client_ui_claude_code_mods --> pkg_client_ui_primitives
pkg_experimental_client_ui_claude_code_mods --> pkg_client_ui_renderer
pkg_experimental_client_ui_claude_code_mods --> pkg_client_ui_session
pkg_experimental_client_ui_claude_code_mods --> pkg_client_ui_slots
pkg_experimental_client_ui_claude_code_mods --> pkg_experimental_claude_code_mods
pkg_experimental_client_ui_claude_code_mods --> pkg_session
pkg_experimental_client_ui_claude_code_mods --> pkg_typert_protocol
pkg_experimental_client_ui_cot_translation --> pkg_api_session_controller
pkg_experimental_client_ui_cot_translation --> pkg_experimental_translator
pkg_experimental_client_ui_cot_translation --> pkg_session
pkg_experimental_client_ui_cot_translation --> pkg_settings
pkg_experimental_client_ui_cot_translation --> pkg_typert_protocol
pkg_experimental_client_ui_voice_input --> pkg_api_gateway
pkg_experimental_client_ui_voice_input --> pkg_api_remotes
pkg_experimental_client_ui_voice_input --> pkg_api_session_controller
pkg_experimental_client_ui_voice_input --> pkg_client_locale
pkg_experimental_client_ui_voice_input --> pkg_client_store
pkg_experimental_client_ui_voice_input --> pkg_client_ui_conversation
pkg_experimental_client_ui_voice_input --> pkg_client_ui_plugin_manager
pkg_experimental_client_ui_voice_input --> pkg_client_ui_primitives
pkg_experimental_client_ui_voice_input --> pkg_client_ui_renderer
pkg_experimental_client_ui_voice_input --> pkg_client_ui_session
pkg_experimental_client_ui_voice_input --> pkg_client_ui_slots
pkg_experimental_client_ui_voice_input --> pkg_experimental_api_speech_to_text
pkg_experimental_client_ui_voice_input --> pkg_experimental_speech_to_text
pkg_experimental_client_ui_voice_input --> pkg_session
pkg_experimental_client_ui_voice_input --> pkg_typert_protocol
pkg_experimental_tool_agent_team --> pkg_agent
pkg_experimental_tool_agent_team --> pkg_experimental_agent_team
pkg_experimental_tool_agent_team --> pkg_session
@@ -1553,7 +1510,11 @@ flowchart TD
| [`office-to-pdf`](../packages/document/office-to-pdf) | `document` | — |
| [`experimental-agent-team-profile`](../packages/experimental/agent-team-profile) | `experimental` | — |
| [`experimental-badge-skill-bundle`](../packages/experimental/badge-skill-bundle) | `experimental` | — |
| [`experimental-client-ui-agent-team`](../packages/experimental/client-ui-agent-team) | `experimental` | — |
| [`experimental-client-ui-claude-code-mods`](../packages/experimental/client-ui-claude-code-mods) | `experimental` | — |
| [`experimental-client-ui-voice-input`](../packages/experimental/client-ui-voice-input) | `experimental` | — |
| [`experimental-cot-translation-bundle`](../packages/experimental/cot-translation-bundle) | `experimental` | — |
| [`experimental-inspector`](../packages/experimental/inspector) | `experimental` | — |
| [`experimental-inspector-profile`](../packages/experimental/inspector-profile) | `experimental` | — |
| [`experimental-ralph-bundle`](../packages/experimental/ralph-bundle) | `experimental` | — |
| [`experimental-session-inspector`](../packages/experimental/session-inspector) | `experimental` | — |
@@ -1610,7 +1571,6 @@ flowchart TD
| [`attachment-local`](../packages/attachment/attachment-local) | `attachment` | [`attachment`](../packages/attachment/attachment), [`home-paths`](../packages/util/home-paths) |
| [`hmr`](../packages/boot/hmr) | `boot` | [`app-boot`](../packages/boot/app-boot), [`cmdline`](../packages/boot/cmdline) |
| [`experimental-computer-use-cua-driver-mcp`](../packages/experimental/computer-use-cua-driver-mcp) | `experimental` | [`computer-use`](../packages/computer-use/computer-use) |
| [`experimental-inspector`](../packages/experimental/inspector) | `experimental` | [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver) |
| [`experimental-webworker-runtime`](../packages/experimental/webworker-runtime) | `experimental` | [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver) |
| [`skill-badge`](../packages/experimental/skill-badge) | `experimental` | [`skill`](../packages/skill/skill) |
| [`host-directory-picker-auto`](../packages/host/directory-picker-auto) | `host` | [`client-connection`](../packages/client/connection), [`client-ui-directory-picker-browse`](../packages/client/ui-directory-picker-browse), [`client-ui-directory-picker-native`](../packages/client/ui-directory-picker-native), [`host-directory-picker-browse`](../packages/host/directory-picker-browse), [`host-directory-picker-native`](../packages/host/directory-picker-native), [`host-webserver`](../packages/host/webserver) |
@@ -1649,6 +1609,7 @@ flowchart TD
| [`skill-filesystem`](../packages/skill/skill-filesystem) | `skill` | [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`skill`](../packages/skill/skill) |
| [`api-workspace-controller`](../packages/api/workspace-controller) | `api` | [`api-gateway`](../packages/api/gateway), [`client-connection`](../packages/client/connection), [`host-directory-picker`](../packages/host/directory-picker), [`session`](../packages/core/session), [`storage-domain`](../packages/storage/storage-domain), [`typert-protocol`](../packages/typert/protocol), [`workspace`](../packages/workspace/workspace) |
| [`experimental-api-speech-to-text`](../packages/experimental/api-speech-to-text) | `experimental` | [`experimental-speech-to-text`](../packages/experimental/speech-to-text), [`typert-protocol`](../packages/typert/protocol) |
| [`experimental-client-ui-cot-translation`](../packages/experimental/client-ui-cot-translation) | `experimental` | [`experimental-translator`](../packages/experimental/translator) |
| [`experimental-speech-to-text-sensevoice`](../packages/experimental/speech-to-text-sensevoice) | `experimental` | [`experimental-speech-to-text`](../packages/experimental/speech-to-text), [`subprocess`](../packages/subprocess/subprocess) |
| [`hook-protocol`](../packages/experimental/hook-protocol) | `experimental` | [`session`](../packages/core/session), [`shell`](../packages/shell/shell) |
| [`lsp-stdio`](../packages/lsp/lsp-stdio) | `lsp` | [`brand`](../packages/util/brand), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
@@ -1782,10 +1743,6 @@ flowchart TD
| [`tool-ralph`](../packages/experimental/tool-ralph) | `experimental` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
| [`sdk-protocol`](../packages/sdk/protocol) | `sdk` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
| [`workflow-ptc`](../packages/workflow/workflow-ptc) | `workflow` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow), [`working-directory`](../packages/session/working-directory) |
| [`experimental-client-ui-agent-team`](../packages/experimental/client-ui-agent-team) | `experimental` | [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`experimental-agent-team`](../packages/experimental/agent-team), [`session`](../packages/core/session) |
| [`experimental-client-ui-claude-code-mods`](../packages/experimental/client-ui-claude-code-mods) | `experimental` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-slots`](../packages/client/ui-slots), [`experimental-claude-code-mods`](../packages/experimental/claude-code-mods), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
| [`experimental-client-ui-cot-translation`](../packages/experimental/client-ui-cot-translation) | `experimental` | [`api-session-controller`](../packages/api/session-controller), [`experimental-translator`](../packages/experimental/translator), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`typert-protocol`](../packages/typert/protocol) |
| [`experimental-client-ui-voice-input`](../packages/experimental/client-ui-voice-input) | `experimental` | [`api-gateway`](../packages/api/gateway), [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-store`](../packages/client/store), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-plugin-manager`](../packages/client/ui-plugin-manager), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-slots`](../packages/client/ui-slots), [`experimental-api-speech-to-text`](../packages/experimental/api-speech-to-text), [`experimental-speech-to-text`](../packages/experimental/speech-to-text), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
| [`experimental-tool-agent-team`](../packages/experimental/tool-agent-team) | `experimental` | [`agent`](../packages/core/agent), [`experimental-agent-team`](../packages/experimental/agent-team), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`schedule`](../packages/schedule/schedule) | `schedule` | [`agent`](../packages/core/agent), [`api-session-controller`](../packages/api/session-controller), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`storage-domain`](../packages/storage/storage-domain), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`workspace`](../packages/workspace/workspace) |
| [`sdk-client`](../packages/sdk/client) | `sdk` | [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session) |
+6 -49
View File
@@ -495,9 +495,6 @@ flowchart TD
pkg_hmr --> pkg_app_boot
pkg_hmr --> pkg_cmdline
pkg_experimental_computer_use_cua_driver_mcp --> pkg_computer_use
pkg_experimental_inspector --> pkg_client_connection
pkg_experimental_inspector --> pkg_client_modules
pkg_experimental_inspector --> pkg_host_webserver
pkg_experimental_webworker_runtime --> pkg_client_connection
pkg_experimental_webworker_runtime --> pkg_client_modules
pkg_experimental_webworker_runtime --> pkg_host_webserver
@@ -606,6 +603,7 @@ flowchart TD
pkg_api_workspace_controller --> pkg_workspace
pkg_experimental_api_speech_to_text --> pkg_experimental_speech_to_text
pkg_experimental_api_speech_to_text --> pkg_typert_protocol
pkg_experimental_client_ui_cot_translation --> pkg_experimental_translator
pkg_experimental_speech_to_text_sensevoice --> pkg_experimental_speech_to_text
pkg_experimental_speech_to_text_sensevoice --> pkg_subprocess
pkg_hook_protocol --> pkg_session
@@ -1355,47 +1353,6 @@ flowchart TD
pkg_workflow_ptc --> pkg_tools
pkg_workflow_ptc --> pkg_workflow
pkg_workflow_ptc --> pkg_working_directory
pkg_experimental_client_ui_agent_team --> pkg_api_session_controller
pkg_experimental_client_ui_agent_team --> pkg_client_locale
pkg_experimental_client_ui_agent_team --> pkg_client_ui_conversation
pkg_experimental_client_ui_agent_team --> pkg_client_ui_primitives
pkg_experimental_client_ui_agent_team --> pkg_client_ui_renderer
pkg_experimental_client_ui_agent_team --> pkg_client_ui_session
pkg_experimental_client_ui_agent_team --> pkg_client_ui_slots
pkg_experimental_client_ui_agent_team --> pkg_client_ui_workspace
pkg_experimental_client_ui_agent_team --> pkg_experimental_agent_team
pkg_experimental_client_ui_agent_team --> pkg_session
pkg_experimental_client_ui_claude_code_mods --> pkg_api_remotes
pkg_experimental_client_ui_claude_code_mods --> pkg_api_session_controller
pkg_experimental_client_ui_claude_code_mods --> pkg_client_locale
pkg_experimental_client_ui_claude_code_mods --> pkg_client_ui_conversation
pkg_experimental_client_ui_claude_code_mods --> pkg_client_ui_primitives
pkg_experimental_client_ui_claude_code_mods --> pkg_client_ui_renderer
pkg_experimental_client_ui_claude_code_mods --> pkg_client_ui_session
pkg_experimental_client_ui_claude_code_mods --> pkg_client_ui_slots
pkg_experimental_client_ui_claude_code_mods --> pkg_experimental_claude_code_mods
pkg_experimental_client_ui_claude_code_mods --> pkg_session
pkg_experimental_client_ui_claude_code_mods --> pkg_typert_protocol
pkg_experimental_client_ui_cot_translation --> pkg_api_session_controller
pkg_experimental_client_ui_cot_translation --> pkg_experimental_translator
pkg_experimental_client_ui_cot_translation --> pkg_session
pkg_experimental_client_ui_cot_translation --> pkg_settings
pkg_experimental_client_ui_cot_translation --> pkg_typert_protocol
pkg_experimental_client_ui_voice_input --> pkg_api_gateway
pkg_experimental_client_ui_voice_input --> pkg_api_remotes
pkg_experimental_client_ui_voice_input --> pkg_api_session_controller
pkg_experimental_client_ui_voice_input --> pkg_client_locale
pkg_experimental_client_ui_voice_input --> pkg_client_store
pkg_experimental_client_ui_voice_input --> pkg_client_ui_conversation
pkg_experimental_client_ui_voice_input --> pkg_client_ui_plugin_manager
pkg_experimental_client_ui_voice_input --> pkg_client_ui_primitives
pkg_experimental_client_ui_voice_input --> pkg_client_ui_renderer
pkg_experimental_client_ui_voice_input --> pkg_client_ui_session
pkg_experimental_client_ui_voice_input --> pkg_client_ui_slots
pkg_experimental_client_ui_voice_input --> pkg_experimental_api_speech_to_text
pkg_experimental_client_ui_voice_input --> pkg_experimental_speech_to_text
pkg_experimental_client_ui_voice_input --> pkg_session
pkg_experimental_client_ui_voice_input --> pkg_typert_protocol
pkg_experimental_tool_agent_team --> pkg_agent
pkg_experimental_tool_agent_team --> pkg_experimental_agent_team
pkg_experimental_tool_agent_team --> pkg_session
@@ -1555,7 +1512,11 @@ flowchart TD
| [`office-to-pdf`](../packages/document/office-to-pdf) | `document` | — |
| [`experimental-agent-team-profile`](../packages/experimental/agent-team-profile) | `experimental` | — |
| [`experimental-badge-skill-bundle`](../packages/experimental/badge-skill-bundle) | `experimental` | — |
| [`experimental-client-ui-agent-team`](../packages/experimental/client-ui-agent-team) | `experimental` | — |
| [`experimental-client-ui-claude-code-mods`](../packages/experimental/client-ui-claude-code-mods) | `experimental` | — |
| [`experimental-client-ui-voice-input`](../packages/experimental/client-ui-voice-input) | `experimental` | — |
| [`experimental-cot-translation-bundle`](../packages/experimental/cot-translation-bundle) | `experimental` | — |
| [`experimental-inspector`](../packages/experimental/inspector) | `experimental` | — |
| [`experimental-inspector-profile`](../packages/experimental/inspector-profile) | `experimental` | — |
| [`experimental-ralph-bundle`](../packages/experimental/ralph-bundle) | `experimental` | — |
| [`experimental-session-inspector`](../packages/experimental/session-inspector) | `experimental` | — |
@@ -1612,7 +1573,6 @@ flowchart TD
| [`attachment-local`](../packages/attachment/attachment-local) | `attachment` | [`attachment`](../packages/attachment/attachment), [`home-paths`](../packages/util/home-paths) |
| [`hmr`](../packages/boot/hmr) | `boot` | [`app-boot`](../packages/boot/app-boot), [`cmdline`](../packages/boot/cmdline) |
| [`experimental-computer-use-cua-driver-mcp`](../packages/experimental/computer-use-cua-driver-mcp) | `experimental` | [`computer-use`](../packages/computer-use/computer-use) |
| [`experimental-inspector`](../packages/experimental/inspector) | `experimental` | [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver) |
| [`experimental-webworker-runtime`](../packages/experimental/webworker-runtime) | `experimental` | [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver) |
| [`skill-badge`](../packages/experimental/skill-badge) | `experimental` | [`skill`](../packages/skill/skill) |
| [`host-directory-picker-auto`](../packages/host/directory-picker-auto) | `host` | [`client-connection`](../packages/client/connection), [`client-ui-directory-picker-browse`](../packages/client/ui-directory-picker-browse), [`client-ui-directory-picker-native`](../packages/client/ui-directory-picker-native), [`host-directory-picker-browse`](../packages/host/directory-picker-browse), [`host-directory-picker-native`](../packages/host/directory-picker-native), [`host-webserver`](../packages/host/webserver) |
@@ -1651,6 +1611,7 @@ flowchart TD
| [`skill-filesystem`](../packages/skill/skill-filesystem) | `skill` | [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`skill`](../packages/skill/skill) |
| [`api-workspace-controller`](../packages/api/workspace-controller) | `api` | [`api-gateway`](../packages/api/gateway), [`client-connection`](../packages/client/connection), [`host-directory-picker`](../packages/host/directory-picker), [`session`](../packages/core/session), [`storage-domain`](../packages/storage/storage-domain), [`typert-protocol`](../packages/typert/protocol), [`workspace`](../packages/workspace/workspace) |
| [`experimental-api-speech-to-text`](../packages/experimental/api-speech-to-text) | `experimental` | [`experimental-speech-to-text`](../packages/experimental/speech-to-text), [`typert-protocol`](../packages/typert/protocol) |
| [`experimental-client-ui-cot-translation`](../packages/experimental/client-ui-cot-translation) | `experimental` | [`experimental-translator`](../packages/experimental/translator) |
| [`experimental-speech-to-text-sensevoice`](../packages/experimental/speech-to-text-sensevoice) | `experimental` | [`experimental-speech-to-text`](../packages/experimental/speech-to-text), [`subprocess`](../packages/subprocess/subprocess) |
| [`hook-protocol`](../packages/experimental/hook-protocol) | `experimental` | [`session`](../packages/core/session), [`shell`](../packages/shell/shell) |
| [`lsp-stdio`](../packages/lsp/lsp-stdio) | `lsp` | [`brand`](../packages/util/brand), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`lsp`](../packages/lsp/lsp), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
@@ -1784,10 +1745,6 @@ flowchart TD
| [`tool-ralph`](../packages/experimental/tool-ralph) | `experimental` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) |
| [`sdk-protocol`](../packages/sdk/protocol) | `sdk` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) |
| [`workflow-ptc`](../packages/workflow/workflow-ptc) | `workflow` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`ptc-runtime`](../packages/ptc-runtime/ptc-runtime), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow), [`working-directory`](../packages/session/working-directory) |
| [`experimental-client-ui-agent-team`](../packages/experimental/client-ui-agent-team) | `experimental` | [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`experimental-agent-team`](../packages/experimental/agent-team), [`session`](../packages/core/session) |
| [`experimental-client-ui-claude-code-mods`](../packages/experimental/client-ui-claude-code-mods) | `experimental` | [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-slots`](../packages/client/ui-slots), [`experimental-claude-code-mods`](../packages/experimental/claude-code-mods), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
| [`experimental-client-ui-cot-translation`](../packages/experimental/client-ui-cot-translation) | `experimental` | [`api-session-controller`](../packages/api/session-controller), [`experimental-translator`](../packages/experimental/translator), [`session`](../packages/core/session), [`settings`](../packages/settings/settings), [`typert-protocol`](../packages/typert/protocol) |
| [`experimental-client-ui-voice-input`](../packages/experimental/client-ui-voice-input) | `experimental` | [`api-gateway`](../packages/api/gateway), [`api-remotes`](../packages/api/remotes), [`api-session-controller`](../packages/api/session-controller), [`client-locale`](../packages/client/locale), [`client-store`](../packages/client/store), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-plugin-manager`](../packages/client/ui-plugin-manager), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-session`](../packages/client/ui-session), [`client-ui-slots`](../packages/client/ui-slots), [`experimental-api-speech-to-text`](../packages/experimental/api-speech-to-text), [`experimental-speech-to-text`](../packages/experimental/speech-to-text), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
| [`experimental-tool-agent-team`](../packages/experimental/tool-agent-team) | `experimental` | [`agent`](../packages/core/agent), [`experimental-agent-team`](../packages/experimental/agent-team), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
| [`schedule`](../packages/schedule/schedule) | `schedule` | [`agent`](../packages/core/agent), [`api-session-controller`](../packages/api/session-controller), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`storage-domain`](../packages/storage/storage-domain), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`workspace`](../packages/workspace/workspace) |
| [`sdk-client`](../packages/sdk/client) | `sdk` | [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session) |
+1 -1
View File
@@ -63,7 +63,7 @@ Non-negotiables across the layers:
Npm sections describe installation and development relationships; each build face independently decides what its artifact contains. [`verify-package-dependencies`](../../scripts/verify-package-dependencies.ts) checks and repairs these rules; [`verify-client-packages`](../../scripts/verify-client-packages.ts) owns Client loading and module requests.
1. **Every client package keeps Cordis in matching `peerDependencies` and `devDependencies`.** This includes the static packages because their Node face participates in the same Cordis plugin contract.
2. **A package under `packages/client/` is always covered; `dsh.client` marks a Client/Host package outside that directory.** Explicit include/exclude entries handle exceptions. Every covered package's Host entry is scanned, while a `./client` export alone does not select dependency policy.
2. **A package under `packages/client/` is always covered; `dsh.client` marks a Client/Host package outside that directory, in every group including `packages/experimental/`.** Explicit include/exclude entries handle exceptions. Every covered package's Host entries are scanned, including the non-browser entries its `tsdown.config.ts` builds outside `src/client/`, such as Worker and child-process entries, while a `./client` export alone does not select dependency policy.
3. **Browser and type relationships are development-only.** Workspace and third-party Client imports, type-only imports, module augmentations, TypeScript project references, `dsh.client.inject`, and metadata-only peers belong only in `devDependencies`. Configuration-only entries absent from imports are listed in the dependency policy; Node companion value imports follow the Host rules below.
4. **Host value imports require classified exports.** A workspace value reached from the package's Host entry belongs only in `dependencies` when its exact module specifier and runtime export appear in `safeHostDependencyExports`. Exports whose identity or module state must be shared appear in `peerRequiredHostExports` and keep the whole package edge in matching `peerDependencies` and `devDependencies`. The verifier rejects unclassified exports before `--fix` writes manifests.
5. **Only Host runtime libraries require installation dependencies.** Browser-only third-party implementations belong in `devDependencies`, whether inlined into `lib/client.js` or retained as bare imports in a static browser library. Static libraries are Web-shell build inputs; the final Vite build merges their imports and styles. Distributed browser code remains subject to runtime license disclosure ([reference](../../.agents/notes/implemented/process/2026-08-26-published-dependency-faces.md)).
@@ -44,16 +44,6 @@
},
"license": "MIT",
"peerDependencies": {
"@deepseek-ai/dsh-api-session-controller": "workspace:*",
"@deepseek-ai/dsh-client-locale": "workspace:*",
"@deepseek-ai/dsh-client-ui-conversation": "workspace:*",
"@deepseek-ai/dsh-client-ui-primitives": "workspace:*",
"@deepseek-ai/dsh-client-ui-renderer": "workspace:*",
"@deepseek-ai/dsh-client-ui-session": "workspace:*",
"@deepseek-ai/dsh-client-ui-slots": "workspace:*",
"@deepseek-ai/dsh-client-ui-workspace": "workspace:*",
"@deepseek-ai/dsh-experimental-agent-team": "workspace:*",
"@deepseek-ai/dsh-session": "workspace:*",
"@deepseek-ai/cordis": "workspace:~"
},
"devDependencies": {
@@ -48,18 +48,7 @@
],
"license": "MIT",
"peerDependencies": {
"@deepseek-ai/cordis": "workspace:~",
"@deepseek-ai/dsh-api-remotes": "workspace:*",
"@deepseek-ai/dsh-api-session-controller": "workspace:*",
"@deepseek-ai/dsh-client-locale": "workspace:*",
"@deepseek-ai/dsh-client-ui-conversation": "workspace:*",
"@deepseek-ai/dsh-client-ui-primitives": "workspace:*",
"@deepseek-ai/dsh-client-ui-renderer": "workspace:*",
"@deepseek-ai/dsh-client-ui-session": "workspace:*",
"@deepseek-ai/dsh-client-ui-slots": "workspace:*",
"@deepseek-ai/dsh-experimental-claude-code-mods": "workspace:*",
"@deepseek-ai/dsh-session": "workspace:*",
"@deepseek-ai/dsh-typert-protocol": "workspace:*"
"@deepseek-ai/cordis": "workspace:~"
},
"devDependencies": {
"@deepseek-ai/cordis": "workspace:~",
@@ -56,19 +56,15 @@
"license": "MIT",
"peerDependencies": {
"@deepseek-ai/cordis": "workspace:~",
"@deepseek-ai/dsh-typert-protocol": "workspace:*",
"@deepseek-ai/dsh-settings": "workspace:*",
"@deepseek-ai/dsh-experimental-translator": "workspace:*",
"@deepseek-ai/dsh-session": "workspace:*",
"@deepseek-ai/dsh-api-session-controller": "workspace:*"
"@deepseek-ai/dsh-experimental-translator": "workspace:*"
},
"dependencies": {
"@deepseek-ai/schemastery": "workspace:~",
"zod": "^4.4.3"
"zod": "^4.4.3",
"@deepseek-ai/dsh-typert-protocol": "workspace:*"
},
"devDependencies": {
"@deepseek-ai/cordis": "workspace:~",
"@deepseek-ai/dsh-typert-protocol": "workspace:*",
"@deepseek-ai/dsh-settings": "workspace:*",
"@deepseek-ai/dsh-experimental-translator": "workspace:*",
"@deepseek-ai/dsh-api-remotes": "workspace:*",
@@ -81,7 +77,6 @@
"@deepseek-ai/dsh-client-ui-renderer": "workspace:*",
"@deepseek-ai/dsh-client-ui-settings": "workspace:*",
"@deepseek-ai/dsh-client-ui-slots": "workspace:*",
"@deepseek-ai/schemastery": "workspace:~",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"@testing-library/react": "^16.3.3",
@@ -35,22 +35,7 @@
"license": "MIT",
"dependencies": {},
"peerDependencies": {
"@deepseek-ai/cordis": "workspace:~",
"@deepseek-ai/dsh-api-remotes": "workspace:*",
"@deepseek-ai/dsh-api-session-controller": "workspace:*",
"@deepseek-ai/dsh-client-locale": "workspace:*",
"@deepseek-ai/dsh-client-ui-conversation": "workspace:*",
"@deepseek-ai/dsh-client-ui-primitives": "workspace:*",
"@deepseek-ai/dsh-client-ui-renderer": "workspace:*",
"@deepseek-ai/dsh-client-ui-session": "workspace:*",
"@deepseek-ai/dsh-client-ui-slots": "workspace:*",
"@deepseek-ai/dsh-session": "workspace:*",
"@deepseek-ai/dsh-typert-protocol": "workspace:*",
"@deepseek-ai/dsh-experimental-api-speech-to-text": "workspace:*",
"@deepseek-ai/dsh-experimental-speech-to-text": "workspace:*",
"@deepseek-ai/dsh-api-gateway": "workspace:*",
"@deepseek-ai/dsh-client-store": "workspace:*",
"@deepseek-ai/dsh-client-ui-plugin-manager": "workspace:*"
"@deepseek-ai/cordis": "workspace:~"
},
"devDependencies": {
"@deepseek-ai/cordis": "workspace:~",
+4 -7
View File
@@ -47,7 +47,6 @@
"license": "MIT",
"dependencies": {
"@deepseek-ai/dsh-app-boot": "workspace:*",
"@deepseek-ai/dsh-brand": "workspace:*",
"@deepseek-ai/dsh-util-crypto": "workspace:*",
"@deepseek-ai/schemastery": "workspace:~",
"open": "^11.0.2",
@@ -55,11 +54,7 @@
"ws": "^8.21.3"
},
"peerDependencies": {
"@deepseek-ai/cordis": "workspace:~",
"@deepseek-ai/cordis-plugin-loader": "workspace:~",
"@deepseek-ai/dsh-client-modules": "workspace:*",
"@deepseek-ai/dsh-client-connection": "workspace:*",
"@deepseek-ai/dsh-host-webserver": "workspace:*"
"@deepseek-ai/cordis": "workspace:~"
},
"devDependencies": {
"chrome-devtools-frontend": "1.0.1638082",
@@ -82,6 +77,8 @@
"@deepseek-ai/dsh-client-connection": "workspace:*",
"@types/ws": "^8.18.1",
"playwright": "^1.49.0",
"tsx": "^4.23.13"
"tsx": "^4.23.13",
"@deepseek-ai/dsh-brand": "workspace:*",
"@deepseek-ai/dsh-client-modules": "workspace:*"
}
}
+9 -9
View File
@@ -7972,6 +7972,9 @@ importers:
packages/experimental/client-ui-cot-translation:
dependencies:
'@deepseek-ai/dsh-typert-protocol':
specifier: workspace:*
version: link:../../typert/protocol
'@deepseek-ai/schemastery':
specifier: link:../../../vendor/schemastery
version: link:../../../vendor/schemastery
@@ -8036,9 +8039,6 @@ importers:
'@deepseek-ai/dsh-settings':
specifier: workspace:*
version: link:../../settings/settings
'@deepseek-ai/dsh-typert-protocol':
specifier: workspace:*
version: link:../../typert/protocol
'@deepseek-ai/dsh-typert-registry':
specifier: workspace:*
version: link:../../typert/registry
@@ -8571,12 +8571,6 @@ importers:
'@deepseek-ai/dsh-app-boot':
specifier: workspace:*
version: link:../../boot/app-boot
'@deepseek-ai/dsh-brand':
specifier: workspace:*
version: link:../../util/brand
'@deepseek-ai/dsh-client-modules':
specifier: workspace:*
version: link:../../client/modules
'@deepseek-ai/dsh-util-crypto':
specifier: workspace:*
version: link:../../util/crypto
@@ -8602,12 +8596,18 @@ importers:
'@deepseek-ai/cordis-plugin-loader':
specifier: workspace:~
version: link:../../../vendor/loader
'@deepseek-ai/dsh-brand':
specifier: workspace:*
version: link:../../util/brand
'@deepseek-ai/dsh-client-connection':
specifier: workspace:*
version: link:../../client/connection
'@deepseek-ai/dsh-client-locale':
specifier: workspace:*
version: link:../../client/locale
'@deepseek-ai/dsh-client-modules':
specifier: workspace:*
version: link:../../client/modules
'@deepseek-ai/dsh-client-shortcuts':
specifier: workspace:*
version: link:../../client/shortcuts
+4 -4
View File
@@ -204,7 +204,7 @@ async function main(): Promise<void> {
const packages = readWorkspacePackageManifests(root)
const workspaceVersions = new Map(packages.all.map(pkg => [pkg.name, currentVersion(pkg)]))
const releaseByName = new Map(packages.release.map(pkg => [pkg.name, pkg]))
const state = readPackageDependencyState(root)
const state = await readPackageDependencyState(root)
if (state.policyViolations.length > 0) throw new Error(state.policyViolations.join('\n'))
const base = cloneIndex(buildRegistryIndex(root))
for (const facts of state.facts) applyFactsToRegistry(base, facts, workspaceVersions)
@@ -216,11 +216,11 @@ async function main(): Promise<void> {
for (const name of candidates) {
if (!discovered.includes(name)) throw new Error(`${name} is not a reachable unconfigured Host candidate`)
}
const candidateFacts = new Map(candidates.map((name) => {
const candidateFacts = new Map(await Promise.all(candidates.map(async (name) => {
const pkg = releaseByName.get(name)
if (pkg === undefined) throw new Error(`release set has no ${name}`)
return [name, readPackageDependencyFacts(root, pkg, 'configured-host', state.workspaceNames)]
}))
return [name, await readPackageDependencyFacts(root, pkg, 'configured-host', state.workspaceNames)] as const
})))
const baselineSeconds = await measure(base, targetVersion, options.finalistRuns, options.timeoutMs)
const baseline = median(baselineSeconds)
+2 -14
View File
@@ -61,6 +61,8 @@ const SAFE_HOST_DEPENDENCY_EXPORTS = {
/** Runtime exports that require every consumer to resolve the provider's shared peer instance. */
const PEER_REQUIRED_HOST_EXPORTS = {
// `instanceof` checks need the translator's own class instance.
'@deepseek-ai/dsh-experimental-translator': ['TranslationError'],
'@deepseek-ai/dsh-scope': ['carrierKeyOf', 'createScope', 'scopeOf', 'scopeTarget'],
'@deepseek-ai/dsh-session': ['SESSION_FORMAT_VERSION'],
'@deepseek-ai/dsh-session-persistence': ['SessionPersistenceNotFoundError'],
@@ -101,17 +103,3 @@ function isRecord(value: unknown): value is Record<string, unknown> {
export function hasClientDeclaration(dshField: unknown): boolean {
return isRecord(dshField) && Object.hasOwn(dshField, 'client')
}
/** Whether the repository policy flattens one package's non-Cordis peers. */
export function usesFlattenedPackageDependencies(
manifestPath: string,
packageName: string,
dshField: unknown,
policy: PackageDependencyPolicy = PACKAGE_DEPENDENCY_POLICY,
): boolean {
if (!manifestPath.startsWith('packages/') || manifestPath.startsWith('packages/experimental/')) return false
if (policy.hostPackages.includes(packageName)) return true
if (manifestPath.startsWith('packages/client/')) return true
const included = hasClientDeclaration(dshField) || policy.clientFaceInclude.includes(packageName)
return included && !policy.clientFaceExclude.includes(packageName)
}
+182 -52
View File
@@ -1,6 +1,6 @@
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { dirname, join, relative, sep } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import {
PACKAGE_DEPENDENCY_POLICY,
@@ -16,6 +16,7 @@ import {
fixPackageDependencies,
formatManagedRuntimeDependencies,
formatPeerRequiredRuntimeDependencies,
readHostBuildEntries,
readPackageDependencyFacts,
readPackageDependencyState,
readWorkspacePackageManifests,
@@ -92,11 +93,11 @@ function facts(manifest: PackageDependencyManifest): PackageDependencyFacts {
}
}
function sourceFacts(
async function sourceFacts(
files: Readonly<Record<string, string>>,
manifest: Partial<PackageDependencyManifest> = {},
role: PackageDependencyRole = 'client-host',
): PackageDependencyFacts {
): Promise<PackageDependencyFacts> {
const root = mkdtempSync(join(tmpdir(), 'dsh-dependency-source-'))
roots.push(root)
const subject = pkg('@f/probe', 'packages/g/probe/package.json', manifest)
@@ -244,6 +245,8 @@ describe('package dependency scope', () => {
'carrierKeyOf', 'createScope', 'scopeOf', 'scopeTarget',
])
expect(PACKAGE_DEPENDENCY_POLICY.peerRequiredHostExports['@deepseek-ai/dsh-typert-protocol']).toBeUndefined()
expect(PACKAGE_DEPENDENCY_POLICY.peerRequiredHostExports['@deepseek-ai/dsh-experimental-translator'])
.toEqual(['TranslationError'])
})
it('discovers the Client directory, dsh.client declarations, and configured Host packages', () => {
@@ -291,7 +294,7 @@ describe('package dependency scope', () => {
expect.stringContaining('clientFaceExclude cannot exempt packages/client package @f/client'),
expect.stringContaining('clientFaceExclude names @f/host, which declares no dsh.client entry'),
expect.stringContaining('hostPackages redundantly names Client-faced package @f/dual'),
expect.stringContaining('unknown release package @f/missing'),
expect.stringContaining('clientFaceInclude names unknown package @f/missing'),
]))
})
@@ -341,11 +344,138 @@ describe('package dependency scope', () => {
})
})
/** Write a temporary repository whose files are JSON-encoded unless given as strings. */
function writeWorkspace(files: Readonly<Record<string, string | object>>): string {
const root = mkdtempSync(join(tmpdir(), 'dsh-dependency-workspace-'))
roots.push(root)
for (const [path, content] of Object.entries(files)) {
mkdirSync(dirname(join(root, path)), { recursive: true })
writeFileSync(join(root, path), typeof content === 'string' ? content : JSON.stringify(content))
}
return root
}
const CORDIS_PEER = { peerDependencies: { [CORDIS]: 'workspace:~' }, devDependencies: { [CORDIS]: 'workspace:~' } }
describe('experimental packages and Host build entries', () => {
it('applies the published policy to an experimental Client package that declares browser packages as peers', async () => {
const root = writeWorkspace({
'packages/client/ui-probe/package.json': { name: '@deepseek-ai/dsh-client-ui-probe', ...CORDIS_PEER },
'packages/experimental/client-ui-probe/package.json': {
name: '@deepseek-ai/dsh-experimental-client-ui-probe',
dsh: { client: {} },
peerDependencies: { [CORDIS]: 'workspace:~', '@deepseek-ai/dsh-client-ui-probe': 'workspace:*' },
devDependencies: { [CORDIS]: 'workspace:~', '@deepseek-ai/dsh-client-ui-probe': 'workspace:*' },
},
'packages/experimental/client-ui-probe/src/index.ts': 'export function apply() {}\n',
'packages/experimental/client-ui-probe/src/client/index.ts':
"import { Button } from '@deepseek-ai/dsh-client-ui-probe'\nexport const control = Button\n",
})
const state = await readPackageDependencyState(root, policy())
expect(state.facts.map(fact => [fact.manifestPath, fact.role])).toContainEqual(
['packages/experimental/client-ui-probe/package.json', 'client-host'],
)
expect(collectPackageDependencyViolations(state)).toEqual([
'packages/experimental/client-ui-probe/package.json: @deepseek-ai/dsh-client-ui-probe (existing non-Cordis peer, '
+ 'packages/experimental/client-ui-probe/src/client/index.ts) must be devDependencies-only at workspace:*; '
+ 'found devDependencies + peerDependencies',
])
})
it('accepts an experimental package in the configured Host roster', async () => {
const root = writeWorkspace({
'packages/experimental/host-probe/package.json': { name: '@deepseek-ai/dsh-experimental-host-probe', ...CORDIS_PEER },
'packages/experimental/host-probe/src/index.ts': 'export function apply() {}\n',
})
const state = await readPackageDependencyState(root, policy({ hostPackages: ['@deepseek-ai/dsh-experimental-host-probe'] }))
expect(state.policyViolations).toEqual([])
expect(state.facts.map(fact => [fact.manifestPath, fact.role])).toEqual(
[['packages/experimental/host-probe/package.json', 'configured-host']],
)
})
it('reads Host Worker entries from the package build configuration', async () => {
const root = writeWorkspace({
'packages/g/inspect/package.json': {
name: '@deepseek-ai/dsh-inspect-probe',
dsh: { client: {} },
dependencies: { ws: '^8.0.0' },
...CORDIS_PEER,
},
'packages/g/inspect/tsdown.config.ts': [
'export default [',
" { entry: ['lib/types/index.js'], platform: 'node' },",
" { entry: { worker: 'lib/types/worker/entry.js' } },",
" { name: 'client', entry: { client: 'lib/types/client/index.js' }, platform: 'browser' },",
']',
'',
].join('\n'),
'packages/g/inspect/src/index.ts': 'export function apply() {}\n',
'packages/g/inspect/src/worker/entry.ts': "import { WebSocketServer } from 'ws'\nexport const server = WebSocketServer\n",
'packages/g/inspect/src/client/index.ts': 'export const client = 1\n',
})
const state = await readPackageDependencyState(root, policy())
const subject = state.facts.find(fact => fact.manifestPath === 'packages/g/inspect/package.json')
if (subject === undefined) throw new Error('the Worker fixture was not classified')
expect(subject.hostRuntimeSourceUses.get('ws')).toEqual(['packages/g/inspect/src/worker/entry.ts'])
expect(expectedPackageDependencies(subject).get('ws')?.section).toBe('dependencies')
expect(collectPackageDependencyViolations(state)).toEqual([])
})
it('evaluates every build face and keeps only Host entries outside the Client source tree', async () => {
const root = writeWorkspace({
'packages/g/faces/package.json': { name: '@deepseek-ai/dsh-faces-probe' },
'packages/g/faces/tsdown.config.ts': [
'export default ({ env }: { env: { DSH_BUILD_FACE?: string } }) => env.DSH_BUILD_FACE === \'host\'',
" ? [{ entry: '' }, { entry: { runner: './lib/types/runner.js' }, platform: 'neutral' }]",
" : [{ entry: ['lib/types/index.js', 'src/helper.ts', 'lib/types/optional/*.js'] },",
" { entry: { 'client.node': 'lib/types/client/index.js' }, platform: 'node' },",
" { entry: { client: 'lib/types/client/index.js' }, platform: 'browser' }]",
'',
].join('\n'),
'packages/g/faces/src/index.ts': 'export {}\n',
'packages/g/faces/src/runner.ts': 'export {}\n',
'packages/g/faces/src/helper.ts': 'export {}\n',
'packages/g/faces/src/client/index.ts': 'export {}\n',
})
const subject = pkg('@deepseek-ai/dsh-faces-probe', 'packages/g/faces/package.json')
expect((await readHostBuildEntries(root, subject)).map(path => relative(root, path).split(sep).join('/'))).toEqual([
'packages/g/faces/src/helper.ts',
'packages/g/faces/src/index.ts',
'packages/g/faces/src/runner.ts',
])
expect(await readHostBuildEntries(root, pkg('@f/none', 'packages/g/none/package.json'))).toEqual([])
})
it.each([
['export default undefined', 'tsdown config has no default export'],
['export default { entry: [1] }', 'tsdown entry [1] is not a path, a path list, or an output-name map'],
["export default { entry: 'dist/index.js' }", 'tsdown entry dist/index.js cannot map to a source entry'],
["export default { entry: 'lib/types/missing.js' }", 'tsdown entry lib/types/missing.js has no source module'],
])('rejects a build configuration Host entry discovery cannot follow: %s', async (config, message) => {
const root = writeWorkspace({
'packages/g/broken/package.json': { name: '@deepseek-ai/dsh-broken-probe' },
'packages/g/broken/tsdown.config.ts': `${config}\n`,
'packages/g/broken/src/index.ts': 'export {}\n',
})
await expect(readHostBuildEntries(root, pkg('@deepseek-ai/dsh-broken-probe', 'packages/g/broken/package.json')))
.rejects.toThrow(`packages/g/broken/tsdown.config.ts: ${message}`)
})
})
describe('face-aware source classification', () => {
it('keeps generated Host schema imports in dependencies without reading or writing lib', () => {
it('keeps generated Host schema imports in dependencies without reading or writing lib', async () => {
const { root, manifestPath, source } = generatedHostFixture('schema')
const before = readFileSync(join(root, manifestPath), 'utf8')
const state = readPackageDependencyState(root, policy())
const state = await readPackageDependencyState(root, policy())
const subject = state.facts[0]
if (subject === undefined) throw new Error('generated Host fixture was not classified')
@@ -354,7 +484,7 @@ describe('face-aware source classification', () => {
packageName: 'zod', specifier: 'zod', exportName: 'z',
sourcePath: 'packages/client/probe/lib/typert.host.js',
}))
const standalone = readPackageDependencyFacts(root, pkg('@fixture/generated', manifestPath, subject.manifest),
const standalone = await readPackageDependencyFacts(root, pkg('@fixture/generated', manifestPath, subject.manifest),
subject.role, state.workspaceNames, policy())
expect(standalone.hostRuntimeExportUses).toEqual(subject.hostRuntimeExportUses)
expect(collectPackageDependencyViolations(state)).toEqual([])
@@ -378,9 +508,9 @@ describe('face-aware source classification', () => {
expect(readFileSync(join(root, 'packages/client/probe/src/index.ts'), 'utf8')).toBe(source)
})
it('does not infer a zod runtime dependency from a metadata-only Typert export', () => {
it('does not infer a zod runtime dependency from a metadata-only Typert export', async () => {
const { root } = generatedHostFixture('object')
const state = readPackageDependencyState(root, policy())
const state = await readPackageDependencyState(root, policy())
const subject = state.facts[0]
if (subject === undefined) throw new Error('generated Host fixture was not classified')
@@ -392,32 +522,32 @@ describe('face-aware source classification', () => {
expect(existsSync(join(root, 'packages/client/probe/lib'))).toBe(false)
})
it('rejects a declared Host Typert module absent from the Host program', () => {
it('rejects a declared Host Typert module absent from the Host program', async () => {
const { root } = generatedHostFixture('schema')
rmSync(join(root, 'tsconfig.host.json'))
expect(() => readPackageDependencyState(root, policy())).toThrow(
await expect(readPackageDependencyState(root, policy())).rejects.toThrow(
'packages/client/probe/package.json: declared Host Typert export has no generated module',
)
expect(existsSync(join(root, 'packages/client/probe/lib'))).toBe(false)
})
it('propagates generator publication errors without writing or repairing manifests', () => {
it('propagates generator publication errors without writing or repairing manifests', async () => {
const { root, manifestPath } = generatedHostFixture('schema')
const manifest = JSON.parse(readFileSync(join(root, manifestPath), 'utf8')) as { files: string[] }
manifest.files = []
const before = JSON.stringify(manifest)
writeFileSync(join(root, manifestPath), before)
expect(() => readPackageDependencyState(root, policy())).toThrow(
await expect(readPackageDependencyState(root, policy())).rejects.toThrow(
'package files must include lib/typert.host.js',
)
expect(readFileSync(join(root, manifestPath), 'utf8')).toBe(before)
expect(existsSync(join(root, 'packages/client/probe/lib'))).toBe(false)
})
it('counts browser imports, JSX, type-only references, and augmentations as development inputs', () => {
const subject = sourceFacts({
it('counts browser imports, JSX, type-only references, and augmentations as development inputs', async () => {
const subject = await sourceFacts({
'src/index.ts': [
"import { readFile } from 'node:fs'",
"import { join } from 'path'",
@@ -453,8 +583,8 @@ describe('face-aware source classification', () => {
])
})
it.each(['client-host', 'configured-host'] as const)('retains Host and shared third-party values in dependencies for %s', (role) => {
const subject = sourceFacts({
it.each(['client-host', 'configured-host'] as const)('retains Host and shared third-party values in dependencies for %s', async (role) => {
const subject = await sourceFacts({
'src/index.ts': "import 'host-only'; export { shared } from './nested.ts'",
'src/nested.ts': "export { shared } from 'shared-runtime'",
'src/client/index.ts': "import 'browser-only'; import 'shared-runtime'",
@@ -466,8 +596,8 @@ describe('face-aware source classification', () => {
expect(expected.get('shared-runtime')?.section).toBe('dependencies')
})
it('uses declared DefinitelyTyped providers only for erased source references', () => {
const subject = sourceFacts({
it('uses declared DefinitelyTyped providers only for erased source references', async () => {
const subject = await sourceFacts({
'src/index.ts': "import type { ParseResult } from 'papaparse'",
'src/client/index.ts': "import type { Root } from 'mdast'; import type { Kind } from '@scope/types'",
}, {
@@ -483,8 +613,8 @@ describe('face-aware source classification', () => {
expect(subject.manifest.devDependencies?.mdast).toBeUndefined()
})
it('never lets a root-provided React type package stand in for react', () => {
const subject = sourceFacts({
it('never lets a root-provided React type package stand in for react', async () => {
const subject = await sourceFacts({
'src/index.ts': "import type { ReactNode } from 'react'",
'src/client/index.ts': "import type { Root } from 'react-dom/client'",
}, { devDependencies: { '@types/react': '~18.3.1', '@types/react-dom': '~18.3.0' } })
@@ -493,9 +623,9 @@ describe('face-aware source classification', () => {
expect(() => { repairPackageDependencyManifest(subject) }).toThrow('undeclared third-party dependency react')
})
it.each(["import 'runtime-library'", 'export const view = <div />'])('does not let type providers satisfy runtime imports or JSX: %s', (source) => {
it.each(["import 'runtime-library'", 'export const view = <div />'])('does not let type providers satisfy runtime imports or JSX: %s', async (source) => {
const name = source.includes('<div') ? 'react' : 'runtime-library'
const subject = sourceFacts({
const subject = await sourceFacts({
'src/index.ts': 'export function apply() {}',
'src/client/index.tsx': source,
}, { devDependencies: { [`@types/${name}`]: '^1.0.0' } })
@@ -504,8 +634,8 @@ describe('face-aware source classification', () => {
expect(() => { repairPackageDependencyManifest(subject) }).toThrow(`undeclared third-party dependency ${name}`)
})
it('does not treat static browser library entries as Host modules', () => {
const subject = sourceFacts({
it('does not treat static browser library entries as Host modules', async () => {
const subject = await sourceFacts({
'src/index.tsx': "import 'static-input'; export const view = <div />",
'src/companion.ts': "import 'browser-companion'",
}, {
@@ -521,8 +651,8 @@ describe('face-aware source classification', () => {
}
})
it('scans published Node companions, conditional entries, and emitted-tree subpaths from source', () => {
const subject = sourceFacts({
it('scans published Node companions, conditional entries, and emitted-tree subpaths from source', async () => {
const subject = await sourceFacts({
'src/index.ts': 'export function apply() {}',
'src/companion.ts': "import 'companion-runtime'; import type { Kind } from 'companion-types'",
'src/node/helper.ts': "export { helper } from 'node-helper'",
@@ -565,28 +695,28 @@ describe('face-aware source classification', () => {
{ types: './lib/types/missing.d.ts', default: './lib/renamed.js' },
['./lib/missing.cjs'],
'./lib/missing/*.js',
])('rejects a published Node entry with no matching source: %j', (target) => {
expect(() => sourceFacts({ 'src/index.ts': 'export function apply() {}' }, {
])('rejects a published Node entry with no matching source: %j', async (target) => {
await expect(sourceFacts({ 'src/index.ts': 'export function apply() {}' }, {
exports: { './node': target },
})).toThrow('Host export ./node has no source entry')
})).rejects.toThrow('Host export ./node has no source entry')
})
it('rejects a Node export outside the source mapping', () => {
expect(() => sourceFacts({ 'src/index.ts': 'export function apply() {}' }, {
it('rejects a Node export outside the source mapping', async () => {
await expect(sourceFacts({ 'src/index.ts': 'export function apply() {}' }, {
exports: { './node': './other/node.js' },
})).toThrow('Host export ./node cannot map ./other/node.js to a source entry')
})).rejects.toThrow('Host export ./node cannot map ./other/node.js to a source entry')
})
it('fails when a managed Host package has no Host entry', () => {
it('fails when a managed Host package has no Host entry', async () => {
const root = mkdtempSync(join(tmpdir(), 'dsh-package-missing-host-'))
roots.push(root)
const subject = pkg('@f/host', 'packages/g/host/package.json')
expect(() => readPackageDependencyFacts(root, subject, 'configured-host', new Set([subject.name])))
.toThrow('packages/g/host/package.json: Host runtime entry packages/g/host/src/index.ts does not exist')
await expect(readPackageDependencyFacts(root, subject, 'configured-host', new Set([subject.name])))
.rejects.toThrow('packages/g/host/package.json: Host runtime entry packages/g/host/src/index.ts does not exist')
})
it('classifies Host values, required services, and Client build inputs separately', () => {
it('classifies Host values, required services, and Client build inputs separately', async () => {
const root = mkdtempSync(join(tmpdir(), 'dsh-package-faces-'))
roots.push(root)
const subject = pkg('@f/dual', 'packages/g/dual/package.json', {
@@ -607,7 +737,7 @@ describe('face-aware source classification', () => {
mkdirSync(dirname(join(root, path)), { recursive: true })
writeFileSync(join(root, path), source)
}
const found = readPackageDependencyFacts(root, subject, 'client-host', new Set([
const found = await readPackageDependencyFacts(root, subject, 'client-host', new Set([
CORDIS, '@f/runtime', '@f/types', '@f/nested', '@f/hidden', '@f/browser', '@f/injected',
]), policy({
configurationOnlyDevDependencies: { '@f/dual': ['@f/injected'] },
@@ -618,14 +748,14 @@ describe('face-aware source classification', () => {
expect([...found.configurationOnlyDevDependencies]).toEqual(['@f/injected'])
expect([...found.peerRequiredHostDependencies]).toEqual(['@f/types'])
expect(expectedPackageDependencies(found).get('@f/types')?.section).toBe('peer-dev')
const unrelated = readPackageDependencyFacts(root, subject, 'client-host', found.workspaceNames, policy({
const unrelated = await readPackageDependencyFacts(root, subject, 'client-host', found.workspaceNames, policy({
requiredServicePeers: { '@f/other': ['@f/types'] },
}))
expect(expectedPackageDependencies(unrelated).get('@f/types')?.section).toBe('devDependencies')
for (const name of ['@f/missing', '@f/injected']) {
expect(() => readPackageDependencyFacts(root, subject, 'client-host', found.workspaceNames, policy({
await expect(readPackageDependencyFacts(root, subject, 'client-host', found.workspaceNames, policy({
requiredServicePeers: { '@f/dual': [name] },
}))).toThrow(`requiredServicePeers must name a referenced workspace package: ${name}`)
}))).rejects.toThrow(`requiredServicePeers must name a referenced workspace package: ${name}`)
}
expect(found.hostRuntimeExportUses).toEqual([
{
@@ -692,8 +822,8 @@ describe('face-aware source classification', () => {
})
describe('dependency sections', () => {
it.each(['client-only', 'client-host'] as const)('moves unused third-party and CSS inputs to development dependencies for %s', (role) => {
const subject = sourceFacts({
it.each(['client-only', 'client-host'] as const)('moves unused third-party and CSS inputs to development dependencies for %s', async (role) => {
const subject = await sourceFacts({
'src/index.ts': "import 'host-runtime'",
}, {
dependencies: { 'unused-browser-dep': '^1.2.3', '@fontsource/test-font': '~2.0.0', 'host-runtime': '^3.0.0' },
@@ -713,8 +843,8 @@ describe('dependency sections', () => {
expect(subject.manifest).toEqual(repaired)
})
it('preserves unreferenced third-party declarations in configured Host packages', () => {
const subject = sourceFacts({ 'src/index.ts': 'export function apply() {}' }, {
it('preserves unreferenced third-party declarations in configured Host packages', async () => {
const subject = await sourceFacts({ 'src/index.ts': 'export function apply() {}' }, {
dependencies: { 'unused-host-dep': '^1.0.0' },
optionalDependencies: { 'unused-host-optional': '^2.0.0' },
}, 'configured-host')
@@ -724,8 +854,8 @@ describe('dependency sections', () => {
expect(subject.manifest.optionalDependencies).toEqual({ 'unused-host-optional': '^2.0.0' })
})
it.each(['peerDependencies', 'optionalDependencies'] as const)('rejects browser-only imports declared in %s', (section) => {
const subject = sourceFacts({
it.each(['peerDependencies', 'optionalDependencies'] as const)('rejects browser-only imports declared in %s', async (section) => {
const subject = await sourceFacts({
'src/index.ts': 'export function apply() {}',
'src/client/index.ts': "import 'external'",
}, {
@@ -747,8 +877,8 @@ describe('dependency sections', () => {
expect(collectPackageDependencyViolations(state)).toEqual([])
})
it('rejects a missing third-party declaration and leaves the in-memory manifest unchanged', () => {
const subject = sourceFacts({
it('rejects a missing third-party declaration and leaves the in-memory manifest unchanged', async () => {
const subject = await sourceFacts({
'src/index.ts': 'export function apply() {}',
'src/client/index.ts': "import 'undeclared'",
})
@@ -1003,11 +1133,11 @@ describe('dependency sections', () => {
])
})
it('rejects required service declarations for unmanaged packages', () => {
it('rejects required service declarations for unmanaged packages', async () => {
const { root } = generatedHostFixture('schema')
expect(readPackageDependencyState(root, policy({
expect((await readPackageDependencyState(root, policy({
requiredServicePeers: { '@fixture/missing': [CORDIS] },
})).policyViolations).toContain('requiredServicePeers names unmanaged package @fixture/missing')
}))).policyViolations).toContain('requiredServicePeers names unmanaged package @fixture/missing')
})
it('reports wrong sections, workspace ranges, and stale peer metadata', () => {
+111 -21
View File
@@ -7,6 +7,7 @@ import { spawnSync } from 'node:child_process'
import { existsSync, globSync, readFileSync, writeFileSync } from 'node:fs'
import { isBuiltin } from 'node:module'
import { dirname, extname, join, normalize, relative, resolve, sep } from 'node:path'
import { pathToFileURL } from 'node:url'
import ts from 'typescript'
import { WorkspaceTypertGenerator } from '../packages/typert/generator/src/workspace.ts'
import { writeModuleGraph } from './gen-module-graph.ts'
@@ -32,6 +33,8 @@ function workspaceRange(name: string): 'workspace:*' | 'workspace:~' {
return name === '@deepseek-ai/dsh' || name.startsWith('@deepseek-ai/dsh-') ? 'workspace:*' : 'workspace:~'
}
const RELEASE_MANIFEST_GLOB = 'packages/!(experimental)/*/package.json'
/** Every package group, experimental included, follows the same published dependency policy. */
const POLICY_MANIFEST_GLOB = 'packages/*/*/package.json'
const WORKSPACE_MANIFEST_GLOBS = [
'apps/*/package.json',
'packages/*/*/package.json',
@@ -113,10 +116,15 @@ function packageNameOf(specifier: string): string | undefined {
return specifier.startsWith('@') ? parts.length >= 2 ? `${parts[0]}/${parts[1]}` : undefined : parts[0]
}
/** Read package manifests used for scope discovery and workspace-name checks. */
/**
* Read package manifests used for scope discovery and workspace-name checks.
* @param root - Repository containing the workspace manifests.
* @returns Every workspace package, the non-experimental release packages, and the packages the policy covers.
*/
export function readWorkspacePackageManifests(root: string): {
all: WorkspacePackageManifest[]
release: WorkspacePackageManifest[]
policy: WorkspacePackageManifest[]
} {
const read = (manifestPath: string): WorkspacePackageManifest => {
const manifest = JSON.parse(readFileSync(resolve(root, manifestPath), 'utf8')) as PackageDependencyManifest
@@ -129,8 +137,11 @@ export function readWorkspacePackageManifests(root: string): {
}
}
const all = globSync(WORKSPACE_MANIFEST_GLOBS, { cwd: root }).map(normalizePath).sort().map(read)
const releasePaths = new Set(globSync(RELEASE_MANIFEST_GLOB, { cwd: root }).map(normalizePath))
return { all, release: all.filter(pkg => releasePaths.has(pkg.manifestPath)) }
const subset = (pattern: string): WorkspacePackageManifest[] => {
const paths = new Set(globSync(pattern, { cwd: root }).map(normalizePath))
return all.filter(pkg => paths.has(pkg.manifestPath))
}
return { all, release: subset(RELEASE_MANIFEST_GLOB), policy: subset(POLICY_MANIFEST_GLOB) }
}
function duplicates(values: readonly string[]): string[] {
@@ -161,7 +172,7 @@ export function discoverPackageDependencyScope(
] as const) {
for (const name of duplicates(values)) violations.push(`${field} lists ${name} more than once`)
for (const name of values) {
if (!byName.has(name)) violations.push(`${field} names unknown release package ${name}`)
if (!byName.has(name)) violations.push(`${field} names unknown package ${name}`)
}
}
for (const name of include) {
@@ -389,7 +400,78 @@ function hostSourceEntries(root: string, pkg: WorkspacePackageManifest): string[
return [...entries].sort()
}
function readHostRuntimeUses(root: string, pkg: WorkspacePackageManifest, generatedHostSource?: string): {
/** tsdown build faces the repository build runs; each package config contributes its configs under both. */
const BUILD_FACES = ['host', 'client'] as const
/** One tsdown configuration as far as Host entry discovery reads it. */
interface BuildEntryConfig {
readonly entry?: unknown
readonly platform?: unknown
}
/** Flatten a tsdown entry declaration to its paths: a string, a list, or an output-name map. */
function buildEntryPaths(entry: unknown, configPath: string): string[] {
if (entry === undefined || entry === '') return []
if (typeof entry === 'string') return [entry]
if (Array.isArray(entry) && entry.every(value => typeof value === 'string')) return entry
if (entry !== null && typeof entry === 'object' && !Array.isArray(entry)
&& Object.values(entry).every(value => typeof value === 'string')) return Object.values(entry) as string[]
throw new Error(`${configPath}: tsdown entry ${JSON.stringify(entry)} is not a path, a path list, or an output-name map`)
}
/**
* Read the Host-side source entries a package-local `tsdown.config.ts` builds, such as Worker-thread and child-process
* entries that no package export names. The config is evaluated under every repository build face; configs for the
* browser platform and entries under `src/client/` (Client faces, including the Node Client half) are not Host
* entries. A package without a local config builds only the workspace default `index` and `startup` entries, which
* its `src/index.ts` and exports already cover.
* @param root - Repository containing the package.
* @param pkg - Package whose build configuration is read.
* @returns Absolute source paths, sorted; empty without a package-local config.
* @throws {Error} when the config has no usable default export or an entry has no source module.
*/
export async function readHostBuildEntries(root: string, pkg: WorkspacePackageManifest): Promise<string[]> {
const configPath = resolve(root, pkg.dir, 'tsdown.config.ts')
if (!existsSync(configPath)) return []
const display = normalizePath(relative(root, configPath))
const loaded = await import(pathToFileURL(configPath).href) as { default?: unknown }
const exported = loaded.default
if (exported === undefined || exported === null) throw new Error(`${display}: tsdown config has no default export`)
const configs: BuildEntryConfig[] = []
for (const face of BUILD_FACES) {
const value: unknown = typeof exported === 'function'
? await (exported as (inline: { env: Record<string, string> }) => unknown)({ env: { DSH_BUILD_FACE: face } })
: exported
configs.push(...(Array.isArray(value) ? value : [value]) as BuildEntryConfig[])
}
const packageDir = resolve(root, pkg.dir)
const clientDir = join(packageDir, 'src', 'client') + sep
const entries = new Set<string>()
for (const config of configs) {
if (config.platform === 'browser') continue
for (const path of buildEntryPaths(config.entry, display)) {
if (!/^(?:\.\/)?(?:lib\/types|src)\//.test(path)) {
throw new Error(`${display}: tsdown entry ${path} cannot map to a source entry`)
}
const source = path.replace(/^(?:\.\/)?(?:lib\/types|src)\//, './src/')
const matched = /[*{]/.test(source)
? globSync(source.replace(/\.[cm]?[jt]sx?$/, '.{ts,tsx,mts,cts}'), { cwd: packageDir }).map(match => resolve(packageDir, match))
: [resolveLocal(resolve(root, pkg.manifestPath), source)].filter(match => match !== undefined)
if (matched.length === 0 && !/[*{]/.test(source)) {
throw new Error(`${display}: tsdown entry ${path} has no source module`)
}
for (const match of matched) if (!match.startsWith(clientDir)) entries.add(match)
}
}
return [...entries].sort()
}
function readHostRuntimeUses(
root: string,
pkg: WorkspacePackageManifest,
hostBuildEntries: readonly string[],
generatedHostSource?: string,
): {
packageUses: Map<string, string[]>
exportUses: HostRuntimeExportUse[]
} {
@@ -417,7 +499,7 @@ function readHostRuntimeUses(root: string, pkg: WorkspacePackageManifest, genera
if (target !== undefined) visit(target)
}
}
for (const entry of hostSourceEntries(root, pkg)) visit(entry)
for (const entry of [...hostSourceEntries(root, pkg), ...hostBuildEntries]) visit(entry)
const generated = generatedHostSource ?? generatedHostSources(root, [pkg]).get(pkg.name)
if (generated !== undefined) collect(`${normalizePath(pkg.dir)}/lib/typert.host.js`, generated)
return {
@@ -465,20 +547,21 @@ function readAllSourceUses(root: string, pkg: WorkspacePackageManifest): Map<str
* @param workspaceNames - Workspace package identities.
* @param policy - Reviewed Host exports, required services, and development-only relationships.
* @param generatedHostSource - Host module already emitted in memory by a batched Typert pass.
* @returns Source-derived dependency facts without writing build artifacts.
* @returns Source-derived dependency facts without writing build artifacts; Host roles include the build entries
* {@link readHostBuildEntries} reads from the package's tsdown config.
*/
export function readPackageDependencyFacts(
export async function readPackageDependencyFacts(
root: string,
pkg: WorkspacePackageManifest,
role: PackageDependencyRole,
workspaceNames: ReadonlySet<string>,
policy: PackageDependencyPolicy = PACKAGE_DEPENDENCY_POLICY,
generatedHostSource?: string,
): PackageDependencyFacts {
): Promise<PackageDependencyFacts> {
const inject = pkg.manifest.dsh?.client?.inject ?? []
const hostRuntime = role === 'client-only'
? { packageUses: new Map<string, string[]>(), exportUses: [] }
: readHostRuntimeUses(root, pkg, generatedHostSource)
: readHostRuntimeUses(root, pkg, await readHostBuildEntries(root, pkg), generatedHostSource)
const servicePeers = policy.requiredServicePeers?.[pkg.name] ?? []
const allSourceUses = readAllSourceUses(root, pkg)
for (const name of servicePeers) {
@@ -574,21 +657,28 @@ export function collectHostDependencyExportPolicyViolations(
return violations.sort()
}
/** Read every package covered by the current dependency policy. */
export function readPackageDependencyState(
/**
* Read every package covered by the current dependency policy.
* @param root - Repository containing the workspace manifests, sources, and package build configs.
* @param policy - Reviewed scope overrides and Host export classifications.
* @returns Facts for every covered package, the covered manifests, and policy configuration violations.
*/
export async function readPackageDependencyState(
root: string,
policy: PackageDependencyPolicy = PACKAGE_DEPENDENCY_POLICY,
): PackageDependencyState {
): Promise<PackageDependencyState> {
const packages = readWorkspacePackageManifests(root)
const workspaceNames = new Set(packages.all.map(pkg => pkg.name))
const discovered = discoverPackageDependencyScope(packages.release, policy)
const discovered = discoverPackageDependencyScope(packages.policy, policy)
const generated = generatedHostSources(root, discovered.selected.filter(pkg => pkg.role !== 'client-only'))
const facts = discovered.selected.map(pkg =>
readPackageDependencyFacts(root, pkg, pkg.role, workspaceNames, policy, generated.get(pkg.name)))
const facts: PackageDependencyFacts[] = []
for (const pkg of discovered.selected) {
facts.push(await readPackageDependencyFacts(root, pkg, pkg.role, workspaceNames, policy, generated.get(pkg.name)))
}
const selectedNames = new Set(facts.map(fact => fact.manifest.name))
return {
facts,
packages: packages.release,
packages: packages.policy,
policyViolations: [
...discovered.violations,
...collectHostDependencyExportPolicyViolations(facts, workspaceNames, policy),
@@ -901,9 +991,9 @@ function refreshPnpmLockfile(root: string): void {
if (result.status !== 0) throw new Error(`pnpm lockfile refresh exited with status ${String(result.status)}`)
}
function main(): void {
async function main(): Promise<void> {
const root = resolve(import.meta.dirname, '..')
let state = readPackageDependencyState(root)
let state = await readPackageDependencyState(root)
const fix = process.argv.includes('--fix')
if (fix) {
if (state.policyViolations.length > 0) {
@@ -916,7 +1006,7 @@ function main(): void {
console.log(
`${GATE}: refreshed pnpm-lock.yaml and wrote ${String(graphChanges.length)} module-graph artifact(s).`,
)
state = readPackageDependencyState(root)
state = await readPackageDependencyState(root)
}
}
const violations = [
@@ -945,4 +1035,4 @@ function main(): void {
}
}
if (import.meta.main) main()
if (import.meta.main) await main()