# 装载机与模块
本仓库是 Laurel 套件的**单一开发仓库**,形状是**一个装载机 + N 个模块**:
```text
laurel-suite/
├── package.json ← 装载机 laurel-center(唯一挂进 DSH profile 的包)
├── cordis.patch.yml ← 总闸:往 loader 树插「自己 + 每个模块」各一行
├── lib/ ← 装载机宿主半边 + 总控台客户端半边(client/ 是源码,client.js 是构建产物)
├── packages/ ← ★ 模块的完整源码(每个模块一个目录、一个独立包、独立版本)
│ ├── laurel/ ← 通知
│ ├── dsh-claude-parchment/ ← Claude 主题
│ ├── dsh-strata/ ← 记忆与上下文
│ └── dsh-loom/ ← 可视化规划模式
├── scripts/ ← release / 安装事务 / 装配校验 / 模块落点工具 / 离线探针
├── docs/ ← 装载机的契约文档
└── MODULES.md ← 本文
```
**为什么是这个形状**:装载机和每个模块都是**独立的 npm 包**,各自有自己的版本号、
自己的 `package.json`、自己的 `LICENSE`。所以「只更新某一个」不会牵动其它;
增删模块是**加/删一个目录 + 一处注册表条目**。
**唯一真相是 `lib/registry.js`**。客户端清单在构建期从同一处内联
(`scripts/build-client.mjs` 的清单里有 `lib/registry.js`),装配校验也从同一处推导
(`scripts/assembly-check.mjs` 不再手抄一份)。这就是「两份不一致」这个老坑的堵法:
**加模块只需要登记一次。**
---
## 1. 当前清单
版本以各 `package.json` 为准(下表写的是本文件落笔时的值,实跑 `node scripts/module.mjs list` 看实时值)。
| 角色 | id(loader 行 id) | 目录 | 包名 | 版本 | 出厂默认 |
|---|---|---|---|---|---|
| 装载机 | `laurel-center` | `.`(本仓库根) | `laurel-center` | 1.1.2 | 自启(它自己不参与开关) |
| 模块 | `laurel` | `packages/laurel` | `laurel` | 0.1.0 | 关 |
| 模块 | `claude-parchment` | `packages/dsh-claude-parchment` | `dsh-claude-parchment` | 1.2.0 | 关 |
| 模块 | `strata` | `packages/dsh-strata` | `dsh-strata` | 0.2.0 | 关 |
| 模块 | `loom` | `packages/dsh-loom` | `dsh-loom` | 0.8.6 | 关(客户端 bundle 大,开着拖首屏) |
出厂默认全关:装载机自己在跑,四个模块待总控台(设置 → 功能管理)逐个打开。
**宿主侧的形状**:装载机在 profile 的 `dependencies`,四个模块在 `devDependencies`
(`link:` 指向 `<装载机>/packages/<目录>`)。**每个包在两段依赖里只能出现一处** ——
两段都有同名时,`assembly-check` 的合并口径是 `devDependencies` 盖掉 `dependencies`,
于是「以为改了、其实读的是另一处」。`scripts/install-profile.mjs` 会规范化成一处。
---
## 2. 一个模块的落点(增删改都必须一起动的地方)
| # | 落点 | 作用 | 漏改的后果 |
|---|---|---|---|
| 1 | `lib/registry.js` → `SUB_PLUGINS` | **唯一真相**:id / dir / entry(纯包名)/ clientId / label / desc / defaultOn | 什么都不发生(模块不存在) |
| 2 | `packages/
/` | 模块实体源码 + `package.json`(name / main / exports["./client"] / version) | 宿主解析不到入口 |
| 3 | `cordis.patch.yml` 的 `insert:` | 往 loader 树插一行:`id` + **纯包名** `name` + `disabled: true` | 前端永远看不到它(`name` 带斜杠会**静默**失效) |
| 4 | `lib/client/index.mjs` 的 locale | `plugin.` 与 `plugin..desc`,zh 与 en 各一条 | 界面上显示成裸键名 |
| 5 | profile 的 `package.json` | 子包 link 指向 `<装载机>/packages/` | 宿主按裸包名解析不到那个模块 |
| 6 | ~~`scripts/assembly-check.mjs`~~ | **已不需要**:它由落点 1 推导 | —— |
装载机自己另有一处:`package.json` 的 `dsh.bundle.patch`、`exports`、`version`。
**落点 2–4 由 `node scripts/module.mjs check` 逐条核对**(只读,不改文件)。
落点 5 给它一个 profile 目录就会一起查(形状 + realpath):
```bash
node scripts/module.mjs list # 看实时清单
node scripts/module.mjs check # 只查仓库内落点
node scripts/module.mjs check /path/to/profile # 连 profile 指向一起查
```
> 为什么 `module.mjs` **不去自动改** `cordis.patch.yml`:那个文件里的注释是这个项目
> 防止重犯 `uiSettings` / 带斜杠包名那类事故的主要手段,脚本改写会毁掉它们。
> 工具只负责说清「还差哪一处」,真正动手改的是人。
---
## 3. 只更新装载机
1. 改 `lib/`(宿主半边 `index.js`/`host.js`/…;客户端半边改 `lib/client/index.mjs`,**不要手改构建产物 `lib/client.js`**)。
2. 改了客户端源码就跑 `node scripts/build-client.mjs`(它会把 `lib/client.js` 重建;`--check` 只校验)。
3. `package.json` 的 `version` +1(装载机自己的版本)。
4. 跑门禁:`node scripts/check.mjs && node scripts/integration-probe.mjs && node scripts/bootgraph-probe.mjs`
(端到端另跑 `node scripts/e2e-real-cordis.mjs`)。
5. `node scripts/module.mjs check` 必须通过。
6. 提交 + 打标签(见 §6):`laurel-center@1.1.2`。
**四个模块一个字都不用动。**
---
## 4. 只更新某一个模块
以 `packages/dsh-strata/` 为例:
1. 只改 `packages/dsh-strata/` 里的源码(它的 `lib/client.js` 是构建产物:改 `lib/client/index.mjs` 后跑它自己的 `npm run build`)。
2. 改 `packages/dsh-strata/package.json` 的 `version`。
3. 跑该模块自己的测试(`cd packages/dsh-strata && npm test`)。
4. 回仓库根跑 `node scripts/check.mjs` 与 `node scripts/module.mjs check`(确认装载机那边没被带坏)。
5. 提交 + 打标签:`dsh-strata@0.2.1`。
**装载机和其它三个模块一个字都不用动。** 模块的版本号和装载机的版本号互不绑定 ——
这是「单独更新」能成立的前提。
> 真实机器上的生效边界:模块的**客户端半边**刷新页面即生效;**宿主半边**(`lib/index.js`)
> 必须**完全退出 Harness 再打开**。改了源码还要让 profile 指向的安装面拿到新文件:
> `node scripts/release.mjs --to <安装目录>`(不要手改安装目录)。
---
## 5. 增加 / 删除模块
### 增加
1. 把模块源码放进 `packages//`,`package.json` 里 `name` 用**纯包名**(不带斜杠、不带 scope 前缀的包名规则见 `docs/PLUGIN-CONTRACT.md`)。
2. `lib/registry.js` 的 `SUB_PLUGINS` 加一条:`{ id, dir, entry: <包名>, clientId: <包名>, label, desc, defaultOn: false }`。
- `id` 一旦发布就**不要再改**:它是 profile 用户层 patch 定位那一行的键。
3. `cordis.patch.yml` 的 `insert:` 里加一行:`- id: ` / `name: <纯包名>` / `disabled: true`。
4. `lib/client/index.mjs` 的 zh 与 en 两个 locale 段各加 `plugin.` 与 `plugin..desc`。
5. `node scripts/build-client.mjs` 重建装载机的客户端 bundle。
6. `node scripts/module.mjs check` 应零问题;`node scripts/check.mjs` 应全绿。
7. 安装:在 profile 的 `package.json` 里给新包加一个 `link:<装载机>/packages/`,
再用宿主自己的安装事务(或 `node scripts/install-profile.mjs `)落链接。
8. 提交 + 打标签 `<包名>@<版本>`。
### 删除
1. `lib/registry.js` 删掉那条。
2. `cordis.patch.yml` 删掉那一行(**必须删**:留下的孤立行会让它的 id 永远不可达)。
3. `lib/client/index.mjs` 删掉两段 locale 的四个键。
4. 删 `packages//` 整个目录。
5. profile 里删掉那个依赖(否则用户层还指着不存在的路径,宿主启动会报加载失败)。
6. `node scripts/build-client.mjs` 重建 bundle。
7. `node scripts/module.mjs check` 应零问题(它会报「孤儿目录」当夹层漏删)。
> 删模块**不要**只删目录:`packages/` 少了目录但 patch/注册表还在,装载机会去找一个不存在的包。
> 反过来,只删注册表条目会在 `module.mjs check` 里报孤儿目录。
---
## 6. 标签约定(单独更新的可追溯身份)
每个包**独立打标签**,标签名就是「包名@版本」:
```bash
git tag laurel-center@1.1.1
git tag laurel@0.1.0
git tag dsh-claude-parchment@1.2.0
git tag dsh-strata@0.2.0
git tag dsh-loom@0.8.6
```
这样「哪个模块什么时候到的哪一版」在仓库里可查,也不需要给整仓编一个统一的版本号。
`package.json` 的 `version` 与之同步;`release.manifest.json` 记录本次生成的安装面来自哪个 commit。
---
## 7. 与安装面的关系
- 本仓库是**完整开发输入**(含各模块的 `src/`、`tests/`、`scripts/`)。
- **安装面**只该有运行需要的文件,由 `node scripts/release.mjs --to <目录>` 生成,逐文件 hash 记在 `release.manifest.json`。
- 安装目录**不要手改**:下次 release 会覆盖它。
- profile 的注册走安装事务:`node scripts/install-profile.mjs `(写依赖 → 一次 pnpm install → 校验,失败整体回滚)。
- ⚠ Windows profile 不要在 WSL 里跑 pnpm(会写出 Windows 解析不了的链接);用宿主插件页的安装流程。