docs: profile scheme across guides, notes, and generated catalogs; Agent Note

Update every doc referencing base.cordis.yml/web.cordis.yml, --config, -p, or
$DSH_HOME/config.yaml to the profile vocabulary with bilingual counterparts
re-recorded; regenerate the catalogs and graphs; add the
profile-plugin-bundles Agent Note recording the design and its rejected
alternatives.
This commit is contained in:
Turtle
2026-08-06 04:40:40 +08:00
parent cd6b4ee3c9
commit 07d24b005f
37 changed files with 213 additions and 64 deletions
@@ -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 .agents/notes/implemented/architecture/2026-08-05-profile-plugin-bundles.md
2026-08-05-profile-plugin-bundles.md: d35a8d7e3976e3dfc40a3574f216bc0344d1283b
2026-08-05-profile-plugin-bundles.zh.md: 5bfe28c19d3d14921ef76a84aacfbc31fa8d8b0e
@@ -0,0 +1,33 @@
# Agent Note: Profile plugin bundles replace the fixed surface overlays
Status: implemented
English | [中文](2026-08-05-profile-plugin-bundles.zh.md)
## Problem
The `dsh` launcher hardcoded its compositions: `base.cordis.yml` + `web.cordis.yml` shipped inside `apps/cli`, three bespoke entry modes (`--config`, `web`, `-p`) each with its own layer stack, and a single global personal overlay (`$DSH_HOME/config.yaml`). There was no way to install an out-of-tree plugin (a TUI, a provider pack) into a shipped surface without editing the repository, and no place where a third-party package could contribute a default composition.
## Decision
Everything becomes a **profile**: a directory `$DSH_HOME/profiles/<name>` with a `package.json` (pnpm-managed out-of-tree plugin `dependencies` plus the ordered `dsh.plugins` bundle-layer list) and a user `cordis.patch.yml`. A **bundle** is an npm package declaring `"dsh": { "patch": "./cordis.patch.yml" }`; the tree composes over an empty root by applying each bundle's patch in `dsh.plugins` order, then the user layer, then `--patch` overlays, then flag patches — one `applyEntryPatches` call, identical for boot, flag derivation, and `--dump-config`.
The shipped compositions became bundles: `@deepseek-ai/dsh-base` (the former base rows as one insert), `@deepseek-ai/dsh-web-app` (the former web overlay plus a runtime glue plugin that owns what used to be launcher code — frontend-dist resolution, the web-surface prompt section, bash runtime variables, the URL line), and `@deepseek-ai/dsh-headless` (a one-shot runner plugin over base + web-app). `dsh web` stays as an alias for `--profile web` carrying the Web flag family; `dsh --profile headless "task"` replaces `-p`; `dsh --config` is removed (its uses migrate to `--patch`). `dsh plugin --profile <name> <args...>` is a thin pnpm forwarder that initializes the profile and reconciles `dsh.plugins` after `add`/`remove` (a patch-less package warns and stays a plain dependency).
Resolution is two-anchored by construction: `dsh.plugins` names resolve from the dsh installation first, then the profile directory — so in-box bundles always come from the same installation as the running `dsh` and pnpm never manages them — while bare plugin names in patch rows resolve through the profile directory's Node parent-walk into the maintained flat fallback `$DSH_HOME/profiles/node_modules` (one symlink per package the installation's app and bundles depend on, healed on every launch).
Two supporting refactors: the webserver's built-in static dist serving became the single-owner **fallback seat** (`registerFallback`/`applyIndexTaps`), with the SPA server extracted to `@deepseek-ai/dsh-frontend-static` so the web bundle owns its dist as composition, not launcher code; and the personal-overlay machinery (`loadPersonalPatches`, `$DSH_HOME/config.yaml`) was retargeted to per-profile `cordis.patch.yml` files (`loadOptionalPatches`, `watchPersonalPatches` taking a filename).
## Alternatives considered
- **Dependency-scan plus partial `patchOrder`** (the original sketch): scanning `dependencies` for bundles and ordering unlisted ones alphabetically has two sources of truth and an implicit tie-break; one explicit ordered `dsh.plugins` list is smaller and fully deterministic. A raw `pnpm add` inside the profile installs a library without activating any patch — explicit, no spooky scan.
- **`link:` entries for in-box bundles**: pnpm cannot version, install, or update a `link:` into the installation, it embeds a machine path in a user file, and it breaks when the installation moves. The two-anchor resolution plus healed symlink fallback gives the same guarantee ("bundles come from the installation") without ceremony.
- **A pre-boot `context` module in the bundle manifest** for boot-time values (dist path, flag facts): rejected in favor of pure plugins — the glue is ordinary rows the launcher patches, so the composition stays fully dumpable and the manifest stays data-only. The launcher-owned `ctx.headlessIo` seam is the one host-provided slot, and it is provided in `boot()`'s `prepare` hook, before any config-tree entry mounts.
- **Transitive bundle auto-application**: only direct `dsh.plugins` entries contribute layers; a meta-bundle wanting to re-export another bundle's patch must do so explicitly in its own patch file.
## Consequences
- New composition surfaces (a TUI, provider packs) ship as ordinary npm packages installable per profile; the repository no longer needs a row for every deployment shape.
- `apps/cli` shrank to argv parsing, profile machinery consumption, and the pnpm forwarder; `AppCLIEntry` and the per-surface boot paths are gone.
- The keyless web e2e scaffold boots the same bundle layers over the same empty-root shape as production, including the profiles module fallback, so composition drift between test and product fails loudly.
- Backends reject nothing old on disk (pre-release stance): `$DSH_HOME/config.yaml` is simply no longer read.
@@ -0,0 +1,33 @@
# Agent Note: profile 插件组合包取代固定的表层 overlay
Status: implemented
[English](2026-08-05-profile-plugin-bundles.md) | 中文
## Problem
`dsh` 启动器硬编码了自己的组合:`base.cordis.yml` + `web.cordis.yml` 随 `apps/cli` 一起交付,三种各自定制的入口模式(`--config`、`web`、`-p`)各带一套层栈,外加一个全局的个人 overlay(`$DSH_HOME/config.yaml`)。想把树外插件(一个 TUI、一个提供方扩展包)装进已交付的表层,只能修改仓库;第三方包也没有任何位置可以贡献默认组合。
## Decision
一切都变成 **profile**:即目录 `$DSH_HOME/profiles/<name>`,其中包含一个 `package.json`(pnpm 管理的树外插件 `dependencies`,加上有序的 `dsh.plugins` 组合包层列表)和一份用户 `cordis.patch.yml`。**组合包**(bundle)是声明了 `"dsh": { "patch": "./cordis.patch.yml" }` 的 npm 包;配置树在空的根之上组合:按 `dsh.plugins` 顺序应用每个组合包的 patch,然后是用户层,然后是 `--patch` overlay,最后是 flag patch——全部收敛为一次 `applyEntryPatches` 调用,启动、flag 派生与 `--dump-config` 使用完全相同的路径。
已交付的组合改造成了组合包:`@deepseek-ai/dsh-base`(原有基础行合并为一次插入)、`@deepseek-ai/dsh-web-app`(原 web overlay,外加一个接管原启动器代码的运行时粘合插件——前端 dist 解析、web 表层提示词段落、bash 运行时变量、URL 行)、`@deepseek-ai/dsh-headless`(叠加在 base + web-app 之上的一次性 runner 插件)。`dsh web` 保留为携带 Web flag 家族的 `--profile web` 别名;`dsh --profile headless "task"` 取代 `-p`;`dsh --config` 被移除(其用途迁移到 `--patch`)。`dsh plugin --profile <name> <args...>` 是一层薄薄的 pnpm 转发器,负责初始化 profile,并在 `add`/`remove` 后调和 `dsh.plugins`(没有 patch 声明的包会给出警告,保持为普通依赖)。
解析在构造上就是双锚点的:`dsh.plugins` 中的名称先从 dsh 安装目录解析,再从 profile 目录解析——因此内置组合包始终来自与运行中 `dsh` 相同的安装,pnpm 从不管理它们——而 patch 行中的裸插件名称经 profile 目录的 Node 父目录逐级查找,落到受维护的扁平回退目录 `$DSH_HOME/profiles/node_modules`(安装目录的应用与各组合包所依赖的每个包各一个符号链接,每次启动时修复)。
两项配套重构:webserver 内置的静态 dist 服务改为单一所有者的**回退席位**(`registerFallback`/`applyIndexTaps`),SPA 服务器提取到 `@deepseek-ai/dsh-frontend-static`,使 web 组合包以组合的方式持有自己的 dist,而不是靠启动器代码;个人 overlay 机制(`loadPersonalPatches`、`$DSH_HOME/config.yaml`)改为面向每个 profile 的 `cordis.patch.yml` 文件(`loadOptionalPatches`、接受文件名的 `watchPersonalPatches`)。
## Alternatives considered
- **依赖扫描加部分 `patchOrder`**(最初的草案):扫描 `dependencies` 找出组合包、未列出者按字母序排列,会产生两个真源和一条隐式决胜规则;一份显式有序的 `dsh.plugins` 列表更小、完全确定。在 profile 内直接 `pnpm add` 只会安装一个库,不激活任何 patch——行为显式,没有暗中扫描。
- **内置组合包使用 `link:` 条目**:pnpm 无法对指向安装目录的 `link:` 做版本管理、安装或更新,它会把机器路径嵌进用户文件,并且在安装目录移动后失效。双锚点解析加上每次启动修复的符号链接回退提供了同样的保证(「组合包来自安装目录」),且没有这些繁文缛节。
- **在组合包 manifest(元数据清单)中放一个启动前 `context` 模块**承载启动期取值(dist 路径、flag 事实):否决,改用纯插件——粘合逻辑就是启动器 patch 的普通配置行,因此组合始终可完整 dump,manifest 保持纯数据。启动器持有的 `ctx.headlessIo` seam 是唯一由宿主提供的 slot,且在任何配置树条目挂载之前,于 `boot()` 的 `prepare` 钩子中提供。
- **组合包的传递式自动应用**:只有直接列在 `dsh.plugins` 中的条目才贡献层;想重新导出另一个组合包 patch 的元组合包,必须在自己的 patch 文件中显式完成。
## Consequences
- 新的组合表层(TUI、提供方扩展包)以普通 npm 包形式交付,可按 profile 安装;仓库不再需要为每种部署形态各留一行。
- `apps/cli` 收缩为 argv 解析、profile 机制的消费方和 pnpm 转发器;`AppCLIEntry` 与各表层专属的启动路径全部移除。
- 无密钥 web e2e 脚手架以与生产相同的空根形态启动相同的组合包层,包括 profiles 模块回退,因此测试与产品之间的组合漂移会大声失败。
- 后端不拒绝磁盘上的任何旧格式(发布前姿态):`$DSH_HOME/config.yaml` 只是不再被读取。