# RFC:dsh-external-stent — 仓库目的、架构与决策记录 [English](README.md) | 中文 - 状态:**活文档**(每节记录决策及其历史) - 范围:本独立 Stent 扩展仓库 - 上游锚点:deepseek-harness 快照 `7b9644f2`(0812)/ `9f9e2782a4`(0813)、fork tip `65bcaf9902`(`feat-stent`) 本文解释这个仓库**为什么**长成现在的样子。下面每一处非常规安排都来自提交历史中一次具体的事故;各节按仓库演化顺序组织,而不是按文件布局。 --- ## 1. 目的:外部化的 Stent 扩展,而非 fork deepseek-harness 是私有 monorepo。Stent/Mixin 扩展层在其中以三个实现包 存在,但消费者无法从 registry 安装它们。本仓库外部化这三个包,并发布 `@oh-my-dsh/stent-pack` carrier,使消费者能通过官方插件通道安装完整 bundle: ``` dsh plugin --profile
add @oh-my-dsh/stent-pack
```
**边界(硬规则):** 工作区包含恰好三个完整实现包——`stent`(纯转换服务)、
`stent-api`(纯 compat facade)、`stent-dsh`(DSH 面 facade、invariant、
profile bootstrap)。根包 `@oh-my-dsh/stent-pack` 是单独发布的 carrier,不是第四个实现包。
官方 `@deepseek-ai/dsh-tool-cordis` 保持为上游依赖,不在此重新发布。
## 2. Host 集成:由 import 启用 loader
三个包通过编译后的 loader 安装 hooks 并挂载 facade。`src/stent-dsh.ts`
编译为 `lib/stent-dsh.js`,`src/stent-loader.ts` 编译为
`lib/stent-loader.js`。bin 是薄命令层:选择命令、通过
`NODE_OPTIONS=--import ...` 注入 loader 并转发执行。import loader 本身就是
activation 边界;loader 为导入它的进程安装 hooks,只有识别为官方 DSH CLI
入口时才执行 profile 组合、依赖修复、argv 规范化和环境设置。
它通过 DSH 提供的 `@deepseek-ai/dsh-app-boot` peer 静态导入官方 profile 组合和修复
API;carrier 不会再捆绑另一份 app-boot,也不需要 host patch checkout。preload 还会记录进程内的
Stent 启用状态,因此即使其他路径安装了底层 hooks,未 import loader 的 `dsh` 下
Stent 依赖插件仍不可用。`getStent(ctx)` 也使用同一能力门控:漏写
`inject: ['stent']` 的插件在普通 `dsh` 下无法通过 accessor 挂载 registry,
而会 loud failure。
官方通道已经覆盖的内容被刻意排除:安装 trio(`dsh plugin add`)、bundle 行名册与依赖、catalog 生成、trio-in-workspace 的 invariant/gate 豁免、以及全部文档(`README*`、`docs/`、`.agents/`)。剩下的是任何通道都提供不了的:import-owned loader/bootstrap、`clientBundle` 源码 transform 构建接缝、编译进官方 `tool-cordis` 包的 catalog 条目、它们的测试、以及 pnpm 策略接缝。### 2.1 disabled opt-in 行
web-app bundle 层把 `stent` / `stent-dsh` 行插入为 **disabled opt-in**。动态
patch plugin 的 row 只需要 activation marker(例如 `config: { stent: true }`);
launcher 通过生成的 overlay 自动启用它和 `stent-dsh` integration row。YAML
不会再提供 patch metadata。plugin code 在 preload 调用 `installStentHooks()` 后,
通过 `ctx.stent.register()` 注册 metadata 与 handler。普通 `dsh` 仍保持这些
行 disabled。
### 2.2 完全动态的 patch 注册
`installStentHooks()` 在官方 CLI 导入目标 plugin 前安装。
新的注册会刷新 loader matcher;尚未加载的模块直接使用新 matcher,已加载的
CJS/ESM 模块在同步 Node hooks 可用时会调度 cache re-transform。handler 只
保存在进程内,enable/disable 通过 live bridge 立即生效。`required: true`
由 boot 后的 live runtime registry 检查,而不是 YAML descriptor 列表。
### 2.3 TSX 死胡同(已记录并撤销)
`dsh` 的 source 启动一度看起来需要 `TSX_TSCONFIG_PATH` 或 register preload:`FiberState`(const enum,只在 `vendor/cordis/src` 存在)解析失败。两个 workaround 都曾发布,后来**全部撤销**——真正原因是 shell 里一个指向旧 staging checkout 的过期 `TSX_TSCONFIG_PATH`。干净环境下 tsx 自动发现入口的 tsconfig(继承 base)并把别名解析到 `src`。官方脚本原样运行;patch 中不存在相关接缝。
### 2.4 迁移到 0.2.0
- 使用 `stent-dsh --dsh <命令或源码目录>`,默认执行 PATH 上的 `dsh`。目录通过自身的 `pnpm run --dir <目录> dsh` 脚本启动;薄命令层不再搜索项目依赖或解释 shim 注释。
- 直接 `node --import /absolute/path/to/lib/stent-loader.js <入口>` 即可启用 Stent,不需要 launcher 握手或可继承的完成标志。loader 自读模块 URL、当前入口和 cwd;非 DSH Node 进程安装 hooks,但不改写 profile/argv。
- 将 `markStentDshLaunch`、`isStentDshLaunch`、`STENT_DSH_LAUNCH_KEY` 替换为 `activateStent`、`isStentActive`、`STENT_ACTIVATION_KEY`。底层 `@oh-my-dsh/stent/loader` API 与此 import 启用入口仍然分开。
- carrier 和三个实现包一起升级到 0.2.0;不保留旧 API 别名,不涉及持久化数据迁移。
## 3. 安装模型:npm bundle
可发布的根 bundle `@oh-my-dsh/stent-pack` 声明三个已发布的 npm 实现包:
```
@oh-my-dsh/stent@^0.2.0
@oh-my-dsh/stent-api@^0.2.0
@oh-my-dsh/stent-dsh@^0.2.0
```
同一个 tag workflow 会在这三个包之后发布根 carrier,确保它的 semver 依赖已经存在于 npm。
这样安装只需一个 npm 包:
```sh
dsh plugin --profile web add @oh-my-dsh/stent-pack
```
安装时由 pnpm 解析这些 npm semver 依赖;启动时 `stent-dsh` 调用 DSH 的 module-fallback healer,把 bundle 的依赖闭包映射到 `$DSH_HOME/profiles/node_modules`,使 Profile 和 preload 解析到同一套 trio 副本。
- host 源码安装在 `apps/cli/package.json` 中声明 bundle;先执行 harness workspace 的 `pnpm install` 和 `pnpm run pack:build`,再通过插件通道安装已发布的 npm bundle(把 `@oh-my-dsh/stent-pack` 并入 `dsh.profile.bundles`)。通过编译后的 `lib/stent-dsh.js` 启动 profile 时,launcher 会通过生成的 overlay 自动启用 integration row。
- 消费侧构建使用根目录显式的 `pack:build` 脚本;trio 与 launcher 在打包前分别由各包自己的 tsdown 命令构建,不需要安装期 `prepare`。
### 3.1 pnpm 11 供应链接缝
npm bundle 不需要在 Profile 中设置 `blockExoticSubdeps: false`,也不需要 Git prepare allowlist 或 `dangerouslyAllowAllBuilds`。workspace 仍允许原生 `esbuild` 构建,并排除快速发布的 DSH rc 序列的 minimum-release-age 检查:
- 本 workspace 的 `allowBuilds: esbuild`;
- `minimumReleaseAgeExclude: ['@deepseek-ai/dsh-*']`——dsh-* rc 序列总在 24h 窗口内发布,仅写包名豁免所有版本。
## 4. Registry 依赖策略
dsh-* host 包以快速 rc 序列发布;本仓库通过 registry 范围跟踪它们,每条教训都来自一次真实事故。
### 4.1 dsh-compact 陷阱
`@deepseek-ai/dsh-client-runtime@0.0.1-rc.1` 依赖 `@deepseek-ai/dsh-compact`,后者**从未发布**(上游发布该 runtime 之后删除了这个包)。`0.1.0-rc.x` 系列去掉了该依赖;已端到端验证可安装。
### 4.2 缺失的 rc.5
上游代码版本是 `0.1.0-rc.5`,但 registry 从 `rc.3` 直接跳到 `rc.6`——rc.5 从未发布。因此范围写作 `^0.1.0-rc.0`(解析最新发布的 rc,且 `rc.0` 使未来的稳定版也在范围内)。peer 使用相同范围,host workspace 的 rc.5 满足它——host 安装复用 workspace 包而非 registry 副本。
### 4.3 真实 host 类型,而非本地契约
trio 一度声明 `host-contracts.ts` facade 加一个全局 `@deepseek-ai/cordis` Events 注入。它破坏了 host 各包的类型检查,已删除,改为直接导入真实 `@deepseek-ai/dsh-*` 类型(声明为 peer + devDeps)——与上游形状一致。`ctx.slots` 的类型来自 `dsh-client-runtime` 的声明,与上游相同。
### 4.4 已发布 lib 的运行时 peer
在 `autoInstallPeers: false` 下,已发布 `dsh-*` lib 的加载期导入(`dsh-scope`、`dsh-llm`、`dsh-timeout`、`dsh-typert-protocol`)必须显式列入 devDependencies——每一个都是在测试加载时报 "Cannot find package" 后补上的。
## 5. 浏览器 client 格式:closure factory
web shell 以 classic script 加载 `/plugins/