@dsh-workbench/module-tools 0.3.11 → 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +223 -177
  3. package/bin/dsh-wb-module.mjs +89 -34
  4. package/package.json +22 -3
  5. package/src/arch.mjs +158 -129
  6. package/src/build.mjs +6 -30
  7. package/src/changelog.mjs +166 -0
  8. package/src/check-flags.mjs +0 -7
  9. package/src/check.mjs +115 -53
  10. package/src/checks/alias.mjs +0 -9
  11. package/src/checks/arch.mjs +3 -25
  12. package/src/checks/bundles.mjs +2 -34
  13. package/src/checks/cards.mjs +26 -30
  14. package/src/checks/contracts.mjs +72 -219
  15. package/src/checks/kit.mjs +2 -42
  16. package/src/checks/lint.mjs +9 -82
  17. package/src/checks/markers.mjs +48 -17
  18. package/src/checks/primitives.mjs +0 -39
  19. package/src/checks/protocol.mjs +0 -18
  20. package/src/checks/source.mjs +103 -51
  21. package/src/checks/standalone.mjs +4 -14
  22. package/src/checks/style.mjs +84 -230
  23. package/src/css-injection.mjs +0 -11
  24. package/src/css-shell.mjs +3 -31
  25. package/src/datasource-file.mjs +104 -0
  26. package/src/declared-tools.mjs +0 -24
  27. package/src/design.mjs +116 -38
  28. package/src/discover.mjs +2 -10
  29. package/src/font-scale.mjs +1 -61
  30. package/src/init.mjs +2 -17
  31. package/src/lint.mjs +24 -144
  32. package/src/market-index.mjs +569 -0
  33. package/src/market-init.mjs +80 -0
  34. package/src/primitives-surface.mjs +2 -28
  35. package/src/publish.mjs +11 -25
  36. package/src/release-changelog.mjs +29 -0
  37. package/src/release-journal.mjs +131 -0
  38. package/src/release-report.mjs +99 -0
  39. package/src/release-util.mjs +87 -0
  40. package/src/release.mjs +1250 -0
  41. package/src/seed.mjs +0 -25
  42. package/src/shell-type-files.mjs +3 -30
  43. package/src/sync.mjs +41 -24
  44. package/template/AGENTS.md.tpl +33 -0
  45. package/template/CHANGELOG.md.tpl +7 -0
  46. package/template/README.md.tpl +6 -33
  47. package/template/package.json.tpl +2 -2
  48. package/template/src/client.tsx.tpl +1 -18
  49. package/template/src/host.ts.tpl +2 -15
  50. package/template/types/globals.d.ts +1 -14
  51. package/template-diy/AGENTS.md.tpl +33 -0
  52. package/template-diy/CHANGELOG.md.tpl +7 -0
  53. package/template-diy/README.md.tpl +4 -133
  54. package/template-diy/package.json.tpl +2 -2
  55. package/template-diy/src/client.tsx.tpl +1 -29
  56. package/template-diy/src/host.ts.tpl +1 -15
  57. package/template-diy/src/plugin.test.ts.tpl +1 -1
  58. package/template-diy/types/globals.d.ts +1 -14
  59. package/template-diy/types/styles.d.ts +0 -10
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dsh-workbench contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,246 +1,284 @@
1
1
  # @dsh-workbench/module-tools
2
2
 
3
- 工作台**模块的工具箱**:建骨架、构建、过门禁、管三张表、发货前核清单。
4
- 模块作者(以及替用户造模块的 agent)只跟这几条命令打交道。
3
+ 工作台**模块**的工具箱:建骨架、构建、过门禁、管设计底稿、发货前核清单。
4
+ 模块作者只跟这几条命令打交道 —— 模块包里不再各带一份构建脚本。
5
5
 
6
- ## 命令
6
+ ## 六条命令走一遍
7
7
 
8
8
  ```bash
9
- dsh-wb-module init <unit> [--dir <目录>] [--template minimal|diy] [--description "一句话"]
10
- dsh-wb-module build # 出 dist/host.js 与套了 loader 壳的 client.js
11
- dsh-wb-module check [--only <判据>] # 十三条判据;--only 可以单跑
12
- dsh-wb-module lint [--fix] # 跑模块自己那份 eslint 配置;--fix 写回
13
- dsh-wb-module design <init|check|render|accept>
14
- dsh-wb-module publish # 发货前的清单:门禁 + 三张表 + npm pack 清单(只 dry run)
9
+ pnpm exec dsh-wb-module init my-unit --template diy # 建骨架(minimal 是默认)
10
+ cd my-unit && pnpm install
11
+ pnpm build # dist/host.js + 套了 loader 壳的 client.js
12
+ pnpm check # 十三条判据
13
+ pnpm design check # 设计底稿有没有空格、README 有没有落后
15
14
  ```
16
15
 
17
- `--cwd <目录>` 指定包目录(默认当前目录)。模块目录里更省事的是骨架自带的脚本:
18
- `pnpm build` / `pnpm typecheck` / `pnpm test` / `pnpm selfcheck` / `pnpm check` / `pnpm lint` / `pnpm design`。
16
+ 要发货再加一步 `pnpm publish`:跑门禁、核 `files` 与 `npm pack` 清单,**只 dry run,不真发**。
17
+ 维护者确认要真发时跑 `pnpm exec dsh-wb-module release`:全门禁(build / check / design / typecheck / test / selfcheck)、
18
+ 验收必须全勾、预检(登录 / license / git 远端)、按 `--target` 发到团队源 / 公网 npm / 安装包 / git,
19
+ 最后从出口侧核包(`--dry-run` 只预览,`--verify` 只核已发布的包)。配了可写 git 索引源时,
20
+ 发完还会更新 `market.json`(`--index auto|none|<目录|git 地址>`,`artifacts-in-repo` 连产物一起入库,
21
+ `catalog-only` 只更索引);索引没更上不影响包已发布这件事,结果里会如实写。
22
+ 空仓库先跑 `dsh-wb-module market-init <git 地址>`:生成 `market.json` 与 README 并推首个 commit
23
+ (用仓库声明的默认分支,读不到时用 `--ref` 指定)。在工作台里配了可写索引源之后,
24
+ `release` 默认出口就是 `market`:产物与索引一起进那个仓库。
25
+ 发布过程中每一步都写进 `~/.dsh/workbench/releases/<unit>.json`;中断后带 `--resume` 接着发
26
+ (沿用记录里的出口、版本与索引计划,产物没变就不重跑门禁)。
27
+ 这一版在 `CHANGELOG.md` 里没有记录、或 `package.json` 缺 `files` 里的 `CHANGELOG.md` 时,
28
+ `release` 直接拦下并给出补的命令;`author` 没写时从本机 `git config` 补进 manifest,索引与产物都带上。
29
+
30
+ 骨架自带脚本,日常不用记长命令:`build` · `typecheck` · `test` · `selfcheck` · `check` · `lint` · `design`。
19
31
 
20
- ## 十三条判据(`check`)
32
+ ## 命令
21
33
 
22
- | 判据 | 盯什么 |
34
+ | 命令 | 作用 |
23
35
  |---|---|
24
- | `contracts` | 模块契约:单元 id 唯一、两个面、四个脚本、`files`、peer 下限;以及**清单 ↔ 代码**(真正注册的工具)与**底稿 ↔ 清单**(`design.json` 的能力名)两处对账(见下) |
25
- | `protocol` | 模块声明协议:`dsh.workbench` 的 `apiVersion` 要在、静态半形状要对、不许有协议不认识的键(`scope` 现在**就是**不认识的键之一 —— 见下) |
26
- | `bundles` | 产物边界:宿主面里不许出现浏览器侧的东西;浏览器产物里不许出现**内联的外壳 CSS**(构建会把来自 `@dsh-workbench/shared` / `core` 的 CSS 丢掉,这里是兜底:漏进来一条就说明构建那条闸被绕过了);**产物里出现夹具标记(`wbMock` / `MockStrip` / `useFixturePick` / `-mock'` / `SCENES`…)判红** —— 模块不带夹具:夹具只服务工作台开发者的调试,模块自己的 vitest 把几态各喂一遍就够 |
27
- | `source` | 源码边界:不直接引 dsh 的包、不引不是种子词的 `@deepseek-ai/*`;**共享包的两个入口按运行时分开** —— 根入口(浏览器 barrel)**只许 `import type`**(取运行值:浏览器侧会打进第二份实例、宿主侧会让 `dist/host.js` 在 Node 里 import 不进来);`/host` 只许**宿主侧**取运行值(里面有 `node:crypto` / `node:fs`) |
28
- | `primitives` | dsh 原语只从模块表(`kit`)拿 |
29
- | `kit` | 这份外壳的 kit 成员到底有哪些(缺什么会当场点名) |
30
- | `cards` | 卡片渲染:**八态 × zh/en 各渲一遍**(外壳侧输入 idle / loading / loading-刷新保留旧值(含旧时间戳 `at`)/ ready / ready-空容器 / unreadable-error / unreadable-能力闸缺插件 / unreadable-能力闸缺命令;`useApiResource` 与 `useCapabilityGate` 由判据喂,`useUnitBoard` 用模块仓里装的那份**真壳**)—— 不许抛、不许空白、文字里不许出现 `undefined` / `NaN` / `[object Object]` / `null`、不许裸键名、要套外壳的卡壳、清单 `titleKey` 在两语字典里都有落点。**不断言"画的是哪一态那一屏"**(那归模块自己的逐态测试与 `design.json` 状态矩阵);模块没装外壳时逐态那半跳过、单次全桩渲染照旧跑 |
31
- | `standalone` | 独立构建:产物不依赖工坊里跑 |
32
- | `markers` | 公开包无内部标识(内网域名、企业内部名) |
33
- | `style` | 样式归属:模块不许定义外壳类(`wb-*`)的规则(判据看**复合选择器**,`:global(.wb-x)` 也算,只有同一个复合选择器里带模块自己的类才放行);**裸 `<a>` 与裸交互控件(`<button>` / `<input>` / `<select>`)都判红** —— 前者用 kit 的 `Link`(**只判「正文里的文字链」那一种**;三种 `Link` 表达不了的形态不判、逐文件收在 `ANCHOR_EXEMPT` 里:**图标链**(`<a className="wb-iconButton">` 包一枚图标 —— `Link` 会多画一颗箭头)· **按钮皮的链**(`.anchorButton` 那类"看起来像按钮、实际是链接",ADR-0054 §11 判过:必须是链接的动作留真 `<a>`,不许塞成 `window.open` 按钮)· **整行链**(`.issueLink` 那类 `flex: 1` / `color: inherit` 的行的布局槽)。`<a` **写在行尾**(属性在下一行)也算 —— 这条是**全文**匹配、不逐行,别改回去),后者用 `Button` / `TextField` / `SelectField` / `ChoiceGroup`;只有 `<textarea>` 与 `input[type=hidden\|file]` 例外(两处都没有那种形态);**手写的「标签 + 控件」行(标签元素紧邻 `SelectField` / `TextField` / `ChoiceGroup` / `Input`)与模块自己给 `field`/`form` 容器设的行距(`gap` / 上下 `margin`)也判红** —— 这一层归 kit 的 `FormRow(s)`(面 → 行 → 控件);现存违规收进 `FORM_ROW_EXEMPT`(棘轮:迁一处删一行,且表里已经不再命中的行会当场报红);**手写的操作区(借了 `wb-formActions` 槽类手摆按钮)也判红** —— 操作区归 kit 的 `FormActions`(顺序 / 一个区一颗 `primary` / `danger` 的位置 / 间距 / 窄容器换行都由它定,调用点只给数据与语义),现存违规收进 `FORM_ACTIONS_EXEMPT`(同一套棘轮:迁一处删一行,表里不再命中的行当场报红);**手写的整张表单也判红** —— 模块源码(`.tsx` / `.ts`,测试文件除外)里的**裸 `<form>` 元素**与**手写行的槽类 `wb-formRow` / `wb-formRows` / `wb-field`** 都算,整张表单的面(行的排布 / 校验的时机 / 错误摆哪 / 提交怎么被挡住 / 第一处错误怎么找 / 操作区怎么排)归 kit 的 `Form`(`fields` 进、其余全归它,调用点只给数据与语义),现存违规收进 `FORM_EXEMPT`(同一套棘轮:迁一处删一行,表里不再命中的行当场报红);那一族判据**故意窄**(`wb-field` 后面跟着 `Label`/`Hint` 这些**兄弟**类的不算,测试文件也算在射程外),模块自己用 div 拼一张既没有 `<form>`、也没借槽类的表单**不判** —— 判宽了就是"用户每次改界面都先撞门禁",已知漏报见 `style.mjs` 里那一段注释 |
34
- | `alias` | 模块不指外壳:源码里没有指向外壳 `@/` 的引用 |
35
- | `arch` | 模块自己的**分层形状**(下面单开一节):层序表外的目录 / 反向依赖 / 跨运行时 / 巨文件 / 模型层没测试 / 样式里手搓数值与 `!important`。**只跑带 `arch.config.mjs` 的模块** —— 没有这份文件就是 `○ 不适用`,不是红 |
36
- | `lint` | 模块自己的**语法卫生**(下面单开一节):`as any` / 残留 `console` / `debugger` / 内联颜色字面量 / 字符串里的 `!important` / react-hooks。**只跑带 `eslint.config.mjs` 的模块** —— 没有那份文件就是 `○ 不适用`,不是红;规则住在 module-tools 的方言里、随工具链同代 |
37
-
38
- 三态输出:`✔ 通过` / `○ 不适用` / `✖ 不过`;有任何不过就退出 1。
39
-
40
- ### `arch`:模块包的架构方言
41
-
42
- 新加的一条(十三条判据里的第十二条)。此前那十一条全在盯**边界**(声明 / 产物 / 可达性 / 卡片渲染 / 样式归属),
43
- 没有一条在看**代码结构** —— 卡片一复杂就往 `src/client.tsx` 里堆,宿主侧顺手 import 一个组件
44
- (`dist/host.js` 在 Node 里起不来)也没人拦。`arch` 把 arch-guard 套到模块包上补这一层。
45
-
46
- 模板里那份 `arch.config.mjs` **只有一行**:
36
+ | `dsh-wb-module init <unit> [--dir <目录>] [--template minimal\|diy] [--description "一句话"]` | 建模块骨架 |
37
+ | `dsh-wb-module market-init <git 地址\|目录> [--ref <分支>] [--path <索引路径>]` | 初始化市场索引仓库:写 `market.json` + README,推首个 commit |
38
+ | `dsh-wb-module build` | 出 `dist/host.js`(单文件 ESM)与 `client.js`(套 loader 壳) |
39
+ | `dsh-wb-module check [--only <判据>]` | 十三条判据;`--only` 单跑,也当查询口用;`--list [--json\|--markdown]` 只列判据元数据 |
40
+ | `dsh-wb-module lint [--fix]` | 用模块自己那份 `eslint.config.mjs`;`--fix` 写回文件 |
41
+ | `dsh-wb-module sync` | 按清单生成 `src/declared-tools.ts`,按 `design.json` 的 `key + recipe` 生成 `src/datasource.ts` |
42
+ | `dsh-wb-module design <init\|check\|render\|accept>` | 设计底稿:填、查空格、生成 README、验收打勾 |
43
+ | `dsh-wb-module changelog <init\|add\|check>` | 更新日志:建 `CHANGELOG.md`、记一版、查这一版有没有;`release` 要求当前版本必须有记录 |
44
+ | `dsh-wb-module publish` | 发货前清单:门禁 + `files` + `npm pack`(只 dry run) |
45
+ | `dsh-wb-module release` | 真发布:team / public / tarball / git;全门禁 + 验收闸 + 预检接管 + 发后核包 + 更新市场索引(`--dry-run` / `--verify`) |
46
+
47
+ `--cwd <目录>` 指定包目录,默认当前目录。
48
+
49
+ ## 十三条判据
50
+
51
+ `dsh-wb-module check` 一次跑完,三态输出:`✔ 通过` / `○ 不适用` / `✖ 不过`;有任何不过就退出 1。
52
+
53
+ 表由 `dsh-wb-module check --list --markdown` 生成;`层` 是 manifest / source / build / render / meta。
54
+
55
+ <!-- dsh-wb-module:begin checks -->
56
+ | 判据 | 层 | 摘要 |
57
+ |---|---|---|
58
+ | `contracts` | manifest | 单元 id、两个面、四个脚本、files、peer 下限,以及清单与代码注册对账 |
59
+ | `protocol` | manifest | dsh.workbench 的 apiVersion、静态半形状与未知键 |
60
+ | `bundles` | build | 宿主产物不含浏览器侧、浏览器产物不内联外壳 CSS 与夹具 |
61
+ | `source` | source | 零注释、包与运行时入口分开、不手写进程/锁/原子写 |
62
+ | `arch` | source | 模块自己的层序、运行时边界与设计系统配置 |
63
+ | `lint` | source | 模块自己的 eslint.config.mjs 与规则集 |
64
+ | `primitives` | build | dsh 包只从 kit 取,模块自己声明 external |
65
+ | `kit` | build | 模块产物对得上所装外壳的 kit 面与 peer 下限 |
66
+ | `cards` | render | 八态 × zh/en 渲染:不抛、不空白、不冒裸键名 |
67
+ | `standalone` | build | 外壳源码不在场时仍能 build + typecheck |
68
+ | `markers` | meta | 公开包不含私有源、私网 IP、内部域名后缀 |
69
+ | `style` | source | 不定义 wb-* 规则;裸标签控件改走 kit;令牌名对现读的清单校验 |
70
+ | `alias` | source | 模块源码里没有指向外壳 @/ 的引用 |
71
+ <!-- dsh-wb-module:end checks -->
72
+
73
+ `style` 还查**令牌名**:模块源码里每个 `var(--dsw-* | --wb-*)` 都要在现读的清单里 ——
74
+ `--dsw-*` 从装着的 dsh 主题读、`--wb-*` 从外壳出货的 `client.js` 读;读不到的那一份不判红,只记一条
75
+ 「这次没验」。拼错的名字按最近的几个候选点名(`workbench_diy_components` 的 `scope: 'tokens'` 是同一个来源)。
76
+
77
+ `style` 还有两族更窄的判据:**手写的「标签 + 控件」行与手写的操作区**判红(归 kit 的
78
+ `FormRow(s)` 与 `FormActions`),**裸 `<form>` 与手写整张表单**也判红(归 `Form`)。
79
+ 现存违规收在各自的豁免表里,是棘轮:迁一处删一行,表里已经不再命中的行也会当场报红。
80
+ 判据**故意窄** —— 模块自己用 div 拼一张不借槽类的表单不判,判宽了就是每次改界面都先撞门禁。
81
+
82
+ ### `arch`:模块包的分层方言
83
+
84
+ 卡片一复杂就往 `src/client.tsx` 里堆,宿主侧顺手 import 一个组件(`dist/host.js` 在 Node 里
85
+ 根本起不来)—— 边界判据拦不住这些,`arch` 补这一层。模板里的配置只有一行:
47
86
 
48
87
  ```js
49
88
  import { moduleArch } from '@dsh-workbench/module-tools/arch';
50
89
  export default moduleArch();
51
90
  ```
52
91
 
53
- 方言(层序表、两个运行时、阈值、允许的依赖)**只有一份**,住在
54
- `@dsh-workbench/module-tools/arch` 的 `moduleArch()` 里,随工具链与外壳同代更新 —— 不要求每个模块
55
- 抄一份配置,也不认 arch-guard 的「应用 / 库 / FSD」范式(模块是一个插件包 + 两个面的小库,
56
- `src/messages.ts` 这种根文件在库范式下会报 S01「无处安放」)。
57
-
58
- 层序(数字越小越底层,S21:只许依赖层号 ≤ 自己的文件):
92
+ 方言只有一份,随工具链与外壳同代,不要求每个模块抄一份。层序:
59
93
 
60
94
  | 落点 | 层 | 是什么 |
61
95
  |---|---|---|
62
- | `src/model/**` | 1 | 纯逻辑:类型 / 换算 / 状态机。两个运行时都可达,不碰 react / kit / dsh / `node:*` |
96
+ | `src/model/**` | 1 | 纯逻辑:类型 / 换算 / 状态机。不碰 react / kit / dsh / `node:*` |
63
97
  | `src/messages.ts` | 1 | 文案(zh / en) |
64
- | `src/components/**` | 2 | 浏览器侧的视图件;样式与它同目录(`X.module.css`) |
65
- | `src/host.ts` | 10 | 宿主面 |
66
- | `src/client.tsx` | 10 | 浏览器面 |
67
- | `**/*.test.ts(x)` | 99 | 测试(S21 跳过) |
68
-
69
- 目录**按需要出现**(空壳模块可以只有 `components/`),但多一个目录就是多一处 S01 —— 要加层就在
70
- 自己那份配置里明写。两个运行时靠 S23 隔离:**`src/components/**` 整片算 client 面**,所以宿主侧
71
- import 一个组件是硬红(层号拦不住它:components 第 2 层、host 第 10 层,S21 认为合法)。
98
+ | `src/components/**` | 2 | 浏览器侧视图件;样式与它同目录(`X.module.css`) |
99
+ | `src/host.ts` · `src/client.tsx` | 10 | 两个面 |
100
+ | `**/*.test.ts(x)` | 99 | 测试 |
72
101
 
73
- 内置口径:阈值 `fileLines 200` / `viewLines 200` / `functionLines 80`(严于 arch-guard 默认的
74
- 500 / 500 / 150);允许的运行时依赖只有 `react` · `@dsh-workbench/core` · `@dsh-workbench/shared`;
75
- 样式目录 `src/components` + CSS Modules;`src/model/**` 有文件就必须有测试;模块**不拥有**主题
76
- (不声明令牌表,`--dsw-*` 由 dsh 运行时给)。
102
+ 两个运行时靠另一条规则隔离:`src/components/**` 整片算浏览器面,宿主侧 import 一个组件是硬红
103
+ (层号拦不住它:components 第 2 层、host 第 10 层)。内置阈值比 arch-guard 默认更严:
104
+ 文件与视图各 200 行、函数 80 行;`src/model/**` 有文件就必须有测试。
77
105
 
78
106
  要开口子**从参数进**(别改判据;每条口子都有名字,评审看得见):
79
107
 
80
108
  ```js
81
- import { moduleArch } from '@dsh-workbench/module-tools/arch';
82
109
  export default moduleArch({
83
- fileLines: 300, // 放宽体积阈值(fileLines / viewLines / functionLines)
84
- deps: ['some-sdk'], // 追加放行的运行时依赖
85
- layers: { adapters: 2 }, // 追加 / 覆盖内部目录表
86
- roles: [{ id: 'lib:version', pattern: 'src/version.ts', layer: 1 }], // 追加角色
87
- // structure: { … }, // 整体替换结构声明(面在别处 / 只有一个运行时)
110
+ fileLines: 300,
111
+ deps: ['some-sdk'],
112
+ layers: { adapters: 2 },
113
+ roles: [{ id: 'lib:version', pattern: 'src/version.ts', layer: 1 }],
88
114
  exceptions: [{ rule: 'S16', glob: 'src/components/Legacy.tsx', reason: '待拆,见 #123' }],
89
115
  });
90
116
  ```
91
117
 
92
- `exceptions` 每条**必须写 `reason`**(arch-guard 的硬要求)。红了怎么读、常见红、怎么从参数开口子,
93
- 见随外壳包出货的模块作者指引 `skills/workbench-module-diy/REVIEW.md`。
118
+ `exceptions` 每条必须写 `reason`。怎么读红、常见红、还有哪些口子,见
119
+ [REVIEW.md](../core/skills/workbench-module-diy/REVIEW.md)。
94
120
 
95
121
  ### `lint`:模块包的语法卫生方言
96
122
 
97
- 第十三条。`arch` 管**结构**(层序 / 反向依赖 / 跨运行时 / 巨文件),`lint` 管**单文件语法卫生**:
98
- `as any` 绕过契约、残留的 `console.log` / `debugger`、TS 内联样式里的颜色字面量与 `!important`、
99
- react-hooks 那几条(渲染期写 ref / 漏依赖 / 条件调用)。两半合起来"手搓外观、绕过契约"才闭合 ——
100
- `*.module.css` 那一半归 arch-guard 的 D 族,TS 里的这一半归这里。
101
-
102
- 模板里那份 `eslint.config.mjs` **只有两行**:
123
+ `arch` 管结构,`lint` 管单文件:`as any` 绕过契约、残留的 `console.log` / `debugger`、
124
+ TS 内联样式里的颜色字面量与 `!important`、react-hooks 那几条。模板里的配置只有两行:
103
125
 
104
126
  ```js
105
127
  import { moduleLint } from '@dsh-workbench/module-tools/lint';
106
128
  export default moduleLint({ rootDir: import.meta.dirname });
107
129
  ```
108
130
 
109
- `moduleLint(options)` 返回一份**标准 flat config 数组**(不是私有格式)—— 所以它同时是"判据跑的那份
110
- 配置"与"编辑器 / `eslint` 认的那份配置",没有第二份真相。方言(规则、ignores、那一组 react 与
111
- react-hooks 取舍)**只有一份**,住在 `@dsh-workbench/module-tools/lint`,随工具链与外壳同代更新;
112
- 依赖(`eslint` / `@eslint/js` / `typescript-eslint` / 两个 react 插件)装在 **module-tools 一家**,
113
- 模块与工坊根的清单**一个字都不用改**。
114
-
115
- `rootDir` 把 typescript-eslint 的 `tsconfigRootDir` 钉在这份配置所在的目录。**不传也能跑**(一个
116
- 进程里只有一份配置时 parser 自己猜得对);但本仓根上还有一份 `eslint.config.mjs`,ESLint 10 的扁平
117
- 配置按目录就近选,一个进程里同时出现多份候选时它猜不出来(每个文件一条
118
- `No tsconfigRootDir was set, and multiple candidate TSConfigRootDirs are present`),所以模板与骨架
119
- 都带着它。
131
+ `moduleLint()` 返回一份**标准 flat config**:它同时是判据跑的那份与编辑器 / `eslint` 认的那份,
132
+ 没有第二份真相。依赖(`eslint`、`typescript-eslint`、两个 react 插件)装在 module-tools 一家,
133
+ 模块与工坊根的清单一个字都不用改。
120
134
 
121
- 要开口子**从命名参数进**(`ignores` / `rules` / `rootDir`),也可以展开 `moduleLint()` 再追自己的一块:
135
+ 要开口子从命令参数进:`moduleLint({ rootDir, ignores, rules })`,或者展开它再追自己的一块:
122
136
 
123
137
  ```js
124
- import { moduleLint } from '@dsh-workbench/module-tools/lint';
125
138
  export default [
126
139
  ...moduleLint({ rootDir: import.meta.dirname, rules: { 'no-console': 'off' } }),
127
140
  { files: ['src/legacy/**'], rules: { '@typescript-eslint/no-explicit-any': 'off' } },
128
141
  ];
129
142
  ```
130
143
 
131
- **指纹是逐块比整份方言**:`moduleLint()`(不带 options)产出里除 `/overrides` 与 `/root`(options
132
- 驱动的那两块)之外**全部 8 块**,在模块那份配置里必须一块不少,且名字 / `files` / `rules` 与
133
- `moduleLint()` 产出的**逐项一致** —— `@eslint/js/recommended` · `typescript-eslint/base` ·
134
- `typescript-eslint/eslint-recommended` · `typescript-eslint/recommended`,加 `dsh-wb/module-lint`
135
- 那四块。全局 ignores 单列按**超集**比:多忽略几条可以(那是 `moduleLint({ ignores })` 这个口子),
136
- 少一条就点名缺哪条 glob。少一块、同名出现两次、删一条规则、改一个规则值都当场红,主句是
137
- `… 里的方言与 moduleLint() 产出的不一致 —— <明细>`。所以**只抄一个名字不行**(`export default
138
- [{ name: 'dsh-wb/module-lint', rules: {} }]` 也红),`export default []`、语法错的配置、以及
139
- `moduleLint().filter((b) => b.name !== undefined)` 这类"外科手术式"筛法(会筛掉全局 ignores 或
140
- 基础块)都红;要开口子走 `moduleLint({ rules, ignores })` 或展开 `moduleLint()` 再追加自己的一块。
141
- **残余(只剩这一条)**:模块自己 import 这个方言、再把整份照抄一遍,判据分不出来(它比的是形状,
142
- 不是"这段代码从哪来")—— 这条是**抬高门槛 + 让改动必须明着写**,不是"防住蓄意伪造"。
143
-
144
- `scripts/**` 与 `*.config.*` **不判 `no-console`**:模块自己的 `scripts/smoke.mjs` 那几处
145
- `console.log` 是它的本职工作,不是 debug 残留。`src/components/vendor/**` 是**唯一**允许
146
- `!important` 的地方(方言里写成 `**/components/vendor/**`,与 arch 那份 `vendorDir` 同一处)。
147
-
148
- `dsh-wb-module lint [--fix]` 单独跑:`--fix` 先写回再跑一遍看还剩什么。**`check` 永远不动手改文件**
149
- —— 门禁是判据,不是修复器。
150
-
151
- 为什么依赖装在 module-tools 一家、为什么用"文件在不在"做 opt-in、`warn` 为什么不判红:见 ADR-0060。
152
-
153
- ## 三张表:`design.json`
154
-
155
- 三张表(面板 / 能力 / 联动)+ 状态矩阵 + 数据源记录 + 写通道记录 + 验收清单住在模块根目录的 `design.json`,
156
- README 里那一段由它生成(夹在 `<!-- dsh-wb:design:start/end -->` 之间);验收清单由 agent 自己在真卡片上逐条走通后打勾。
144
+ 指纹是逐块比整份方言:方言那 8 块在模块配置里必须一块不少、逐项一致;少一块、同名两次、
145
+ 删一条规则或改一个值都红。只写一个同名块、`export default []`、语法错的配置都会红。
146
+ `scripts/**` 与 `*.config.*` 不判 `no-console`(脚本打印是本职工作)。
147
+
148
+ `dsh-wb-module lint --fix` 先写回再跑一遍看还剩什么。**`check` 永远不动手改文件** ——
149
+ 门禁是判据,不是修复器。
150
+
151
+ ## `design.json`:三张表
152
+
153
+ 面板 / 能力 / 联动三张表、状态矩阵、数据源记录、写通道记录、验收清单都住在模块根目录的
154
+ `design.json`,README 里那一段由它生成(夹在 `<!-- dsh-wb:design:start/end -->` 之间)。
155
+
156
+ 它是**设计底稿**:三张表与探明记录装人话(`oneLine` / `shape` / `approvalText` / 三张表的文字);
157
+ 读源那一条可以多带 `key` 与 `recipe` 两个机器字段 —— 它们只用于生成 `src/datasource.ts`,
158
+ 不改变"人话那一份"(`read` / `params` / `credentials` / `notLoggedIn` / `timeout`)照写。「这条能力在哪些工作区出现」不在底稿、也不在清单 —— 那是用户的面板板说了算。
159
+ `contracts` 会把底稿的能力名与清单的工具名做键集合对账(不重不漏,只对名字);没有
160
+ `design.json` 的模块不查。
161
+
162
+ 有写能力的模块另在 `writes[]` 里落命令 / 入参 / 出参 / 预览档 / 回读 / 失败形状,
163
+ 渲染成 README 里的「写通道探明记录」。没有写通道就不用写,不会因此变红。
164
+ 写也能落配方:`writes[]` 里加一对 `key` + `recipe`,`sync` 把它和读一起生成进
165
+ `DATASOURCE`(`data.submit(DATASOURCE.<key>)`);配方盖不住的写写 `custom: { why }`(warn)。
166
+
167
+ ### 读源配方
168
+
169
+ 一条读可以带 `key`(lowerCamelCase)和 `recipe`:
170
+
171
+ ```json
172
+ "datasource": {
173
+ "key": "balance",
174
+ "recipe": {
175
+ "origin": "https://x.example.com",
176
+ "request": { "method": "GET", "path": "/api/balance" },
177
+ "response": { "kind": "json", "path": "data" }
178
+ },
179
+ "read": "GET /api/balance(登录态 cookie)",
180
+ "params": "无",
181
+ "credentials": "同源 cookie",
182
+ "notLoggedIn": "401 → auth",
183
+ "timeout": "25 秒"
184
+ }
185
+ ```
157
186
 
158
- `design.json` 是**设计底稿**:只装人话(`oneLine` / `shape` / `approvalText` / 面板那几张表的文字 /
159
- `datasource`),**不装机器字段**。**"这条能力在哪些工作区出现"不在底稿、也不在清单**:
160
- 那是**用户**的事(他的面板板),模块作者声明不了(用户口径「模块开发者不能定义 scope,
161
- scope 永远是用户来决定的」)。
162
- `contracts` 会把底稿的能力名与清单的工具名做**键集合对账**(不重不漏,只对名字):
163
- 底稿多一条、清单多一把都点名判红;没有 `design.json` 的模块不查。
187
+ `sync` 会把它们生成成 `src/datasource.ts`(`export const DATASOURCE = { balance: … } as const`),
188
+ 模块代码只写 `data.request(DATASOURCE.balance)`。一条读要一次打多条同源接口时,再加一格 `calls`:
189
+
190
+ ```json
191
+ "datasource": {
192
+ "key": "wallet",
193
+ "recipe": { "origin": "https://x.example.com", "request": { "method": "GET", "path": "/api/subscription/self" } },
194
+ "calls": {
195
+ "balance": { "path": "/api/wallet/balance", "response": { "path": "data.balance" } },
196
+ "usage": { "query": { "days": 7 } }
197
+ }
198
+ }
199
+ ```
164
200
 
165
- 有写能力的模块,还要在 `writes[]` 里落它的**命令 / 入参 / 出参列 / 预览档 / 回读 / 失败形状**
166
- (一条写一行;只有一条写时写成 `write: { … }`)—— 与 `datasource` 那张表对称,渲染成 README 里
167
- 「写通道探明记录」那一节。**没有写通道的模块不用写**,`design check` 不会因此变红。
201
+ 生成的 `src/datasource.ts` 多一个 `export const DATASOURCE_CALLS = { wallet: { … } } as const`,
202
+ 代码写 `data.requestMany(DATASOURCE.wallet, DATASOURCE_CALLS.wallet)`:一次换登录态、并发打,
203
+ 每格各自成败。`calls` 的键就是结果名;`path` / `query` / `headers` / `response` 覆盖配方同名默认值。
204
+ 几条规矩:
205
+
206
+ - `key` 与 `recipe` **要么都写、要么都不写**;只写一个不生成、也不校验(旧条目一字不动)。
207
+ - 写走同一份生成物:`writes[]` 的 `key + recipe` 也进 `DATASOURCE`;读写的 `key` 不许重复,
208
+ `design check` 点名。
209
+ - `recipe` 与 `custom` 二选一。`custom: { why }` 表示"配方盖不住,仍用旧的 `browser` 脚本",
210
+ `design check` 记 warn,脚本留在代码里。
211
+ - `design check` 校验配方与批量:枚举、`origin` / `path`、`request.headers` 里不许写凭据头、
212
+ `maxBytes`、`GET` 不带 body;`calls` 非空、≤ 8 条、逐格同一套校验,且必须配 `recipe`。
213
+ 生成物过期会被 `contracts` 点名,跑 `sync` 刷新。
214
+ - 形状与六种配方见模块 DIY 的 [`DATASOURCE.md`](../core/skills/workbench-module-diy/DATASOURCE.md)。
168
215
 
169
216
  ```bash
170
217
  dsh-wb-module design check # 空着的格一格格点名叫出来 + README 有没有落后
171
218
  dsh-wb-module design render # 用 design.json 重新生成 README 那一段
172
- dsh-wb-module design accept # 自己在真卡片上逐条走通后,把验收清单打勾
219
+ dsh-wb-module design accept # 在真卡片上逐条走通后打勾
173
220
  ```
174
221
 
175
- ## 协议:`dsh` 字段里的五段
176
-
177
- 一个模块包在 `package.json` 里声明五件事,缺哪一段在哪一层炸都不一样:
222
+ ## 协议:`package.json` 里的五段
178
223
 
179
224
  | 段 | 是什么 | 谁读 |
180
225
  |---|---|---|
181
- | `dsh.bundle.patch` | 把自己插进 profile 组合图的 cordis 补丁 | dsh 的组合器(**装载**) |
182
- | `dsh.client{platform,inject,external}` | 浏览器侧接线 | dsh 的客户端组合器(**dsh 自己不校验**,错的声明要等浏览器 `require` 才炸 —— 所以 `contracts` 替你查) |
183
- | `dsh.workbench` | **模块声明协议**(下详) | 门禁 · 市场 · 外壳 |
184
- | `dsh.workbenchFaces{host,client}` | **工具链元数据**(**不在协议里**):两个 half 的**源码**入口 | 门禁:`source` 拿它算可达性、`contracts` 查它在位;运行期与装机预检都不读 |
185
- | `peerDependencies['@dsh-workbench/core']` | 服务面:外壳 provide 的服务在不在 | dsh 的 `evaluatePluginCompatibility` |
186
-
187
- ### `dsh.workbench`(协议 v1)
226
+ | `dsh.bundle.patch` | 把自己插进 profile 组合图的 cordis 补丁 | dsh 的组合器(装载) |
227
+ | `dsh.client{platform,inject,external}` | 浏览器侧接线 | dsh 的客户端组合器(dsh 不校验,错的声明要等浏览器 `require` 才炸 —— 所以 `contracts` 替你查) |
228
+ | `dsh.workbench` | **模块声明协议** | 门禁 · 市场 · 外壳 |
229
+ | `dsh.workbenchFaces{host,client}` | **工具链元数据**(不在协议里):两个面的源码入口 | 门禁:`source` 拿它算可达性、`contracts` 查它在位;运行期不读 |
230
+ | `peerDependencies['@dsh-workbench/core']` | 服务面:外壳 provide 的服务在不在 | dsh 的兼容性预检 |
188
231
 
189
232
  ```jsonc
190
233
  "workbench": {
191
- "apiVersion": 1, // 必填。形状契约,缺了外壳不认这个模块
192
- "unit": "hello", // 单元 id(小写连字符),进持久化配置
193
- "card": { "titleKey": "hello.cardTitle", "order": 90 }, // 没有卡片写 null
194
- "tools": [{ "name": "hello_read", "writes": false }], // 没工具写 []
195
- "host": { "inject": [] }, // 这个单元要外壳的哪几个服务(判"能不能挂")
196
- "env": { "command": "opencli", "plugins": [], "libs": [] } // 可选:靠什么才跑得起来
234
+ "apiVersion": 2,
235
+ "unit": "hello",
236
+ "card": { "titleKey": "hello.cardTitle", "order": 90 },
237
+ "tools": [{ "name": "hello_read", "writes": false }],
238
+ "host": { "inject": [] },
239
+ "needs": [
240
+ {
241
+ "kind": "extension",
242
+ "runtime": "opencli",
243
+ "name": "acme-workflow",
244
+ "commands": ["acme-read"],
245
+ "purpose": "读写工单",
246
+ "sources": [{ "kind": "npm", "package": "opencli-plugin-acme", "registryEnv": "ACME_REGISTRY" }]
247
+ }
248
+ ]
197
249
  },
198
- "workbenchFaces": { "host": "src/host.ts", "client": "src/client.tsx" } // 工具链那一格(不在协议里):两半的源码入口
250
+ "workbenchFaces": { "host": "src/host.ts", "client": "src/client.tsx" }
199
251
  ```
200
252
 
201
253
  五条口径:
202
254
 
203
- 1. **声明了 `apiVersion` = 宣布静态半写全了** —— `unit` / `card` / `tools` / `host`
204
- 四个键都必须有(`card` 没有卡片写 `null`),`env` 没有外部供给时省略。
205
- 2. **不许有协议不认识的键**:生字段会被外壳**静默忽略**(拼错一个键就等于那句话没写),
206
- 所以 `protocol` 判据先拦。**`scope` 不在协议里**:它曾经是(`card.scope` / `tools[].scope`),
207
- 现已删除 —— 一个能力在哪些工作区出现**只由用户的板决定**,作者写它 = 写了一个生字段、当场红
208
- (`protocol` 会点名 `"scope"`)。
209
- 3. **清单声明的工具 = 代码真正注册的工具**(U1b 起):清单是工具面的**唯一**来源,
210
- `contracts` 拿 `mount()` 里 `registerTools()` 真正注册的名字对账,两边漂了就红。
211
- 4. **底稿 ↔ 清单只对账键集合**(U1c 起):`design.json` 的 `capabilities[].name` 与清单的
212
- `tools[].name` 不重不漏,**只对名字**(内容各写一处,没有可比的东西);没有 `design.json`
213
- 的模块不查 —— 底稿是可选的设计材料。
214
- 5. **`dsh.workbenchFaces` 不是协议的一部分,但门禁必填**(A-2 起):它是 `dsh.workbench` 的
215
- **兄弟键**,写两半的**源码**入口;`source` 判据靠它算"这段源码属于哪一半"。
216
- 写进 `dsh.workbench` 里会被判**生字段**(不留兼容),它也不能指向出货产物
217
- —— 从 `dist/host.js` 反推不出是哪份源码把 dsh 的包拖进来的。
255
+ 1. **声明了 `apiVersion` = 静态半写全了**:`unit` / `card` / `tools` / `host` 四个键都要有
256
+ (没有卡片写 `null`),`needs` 没有外部需要时省略。
257
+ `needs` 四种 `kind`:`bin` / `extension` / `lib` / `env`;每条必写 `purpose`;
258
+ 未知 `kind` 或 `sources[].kind` 判红,need 里未知字段只算警告(`x-` 前缀忽略)。
259
+ 2. **不许有协议不认识的键**:生字段会被外壳静默忽略(拼错一个键就等于那句话没写),
260
+ `protocol` 先拦。`scope` 不在协议里 —— 能力出现在哪些工作区只由用户的板决定。
261
+ 3. **清单声明的工具 = 代码真正注册的工具**:清单是工具面的唯一来源,两边漂了就红。
262
+ 4. **底稿 ↔ 清单只对账键集合**,只对名字,内容各写一处。
263
+ 5. **`dsh.workbenchFaces` 不是协议的一部分,但门禁必填**:它指向两半的**源码**,不能指向出货
264
+ 产物 —— 从 `dist/host.js` 反推不出是哪份源码把 dsh 的包拖进来的。
218
265
 
219
266
  ## 两个模板
220
267
 
221
268
  | 模板 | 是什么 |
222
269
  |---|---|
223
- | `minimal`(默认) | 空壳:卡壳、文案、测试接好了,没有数据源 |
224
- | `diy` | 一份真能跑的样板:宿主侧一条只读路由真读盘、卡片五态,**三张表也是填好的** |
225
-
226
- 两个模板都是**分层多文件**骨架,不是单文件:纯逻辑(类型 / 状态机 / 换算)住 `src/model/**`、
227
- 视图件与它同目录的样式住 `src/components/**`、文案住 `src/messages.ts`,`src/host.ts` /
228
- `src/client.tsx` 只剩两个面(`client.tsx` 只注册、把 props 交给组件)。目录按需要出现 ——
229
- `minimal` 没有纯逻辑就不硬造 `model/`。两个模板各自带一份一行工厂的 `arch.config.mjs` 与一份两行的
230
- `eslint.config.mjs`,`package.json` 里多 `check`(方言里的 M09 要求有这个脚本名)与 `lint` 两条脚本。
231
- 为什么这么分、怎么给自己的模块加配置,见 `## 十三条判据` 里的 `arch` 与 `lint` 那两节。
232
-
233
- `init` 会连模板里的 `*.tpl` 一起拷过去、按**已知 token 名**逐个盖章
234
- (`__UNIT__` / `__NAME__` / `__DESC__` / `__SHELL_RANGE__`)、剥掉 `.tpl` 后缀,
235
- 再用 `design.json` 生成 README 那一段。
236
-
237
- - `__UNIT__` / `__NAME__` / `__DESC__`:单元 id / 包名 / 一句描述。
238
- - `__SHELL_RANGE__`:模板里那三处 `@dsh-workbench/*` 声明的**范围**(peer 与 devDependency 用同一个值,
239
- 否则 `check:kit` 的"peer 下限不许比 devDependency 旧"当场红)。取 **init 所在工程里实装的那份**
240
- `@dsh-workbench/core` 的版本(`node_modules/@dsh-workbench/core/package.json`);读不到就退回
241
- `module-tools` 自己的版本(同一次发布同版本);两者都读不到才保持 `^0.1.0`,并在 stderr 上说一句。
242
- - 按 token 名盖章而不是按 `__[A-Z]+__` 的形状:名字里带下划线的 `__SHELL_RANGE__` 也要认,
243
- DIY 模板 README 里真写着的 `workbench___UNIT___rebuild` 则仍只认内层那一个。
270
+ | `minimal`(默认) | 空壳:卡壳、文案、测试都接好了,没有数据源 |
271
+ | `diy` | 一份真能跑的样板:宿主侧一条只读路由真读盘、卡片五态、三张表也是填好的 |
272
+
273
+ 两个模板都是**分层多文件**骨架:纯逻辑住 `src/model/**`、视图件与同目录样式住
274
+ `src/components/**`、文案住 `src/messages.ts`,两个面只剩注册与交 props。目录按需要出现 ——
275
+ `minimal` 没有纯逻辑就不硬造 `model/`。各自带一份一行工厂的 `arch.config.mjs` 与两行的
276
+ `eslint.config.mjs`。
277
+
278
+ `init` 连模板里的 `*.tpl` 一起拷、按已知 token 名逐个盖章(`__UNIT__` / `__NAME__` / `__DESC__` /
279
+ `__SHELL_RANGE__`)、剥掉后缀,再用 `design.json` 生成 README 那一段。
280
+ `__SHELL_RANGE__` 取 init 所在工程实装的那份外壳版本;读不到就退回 module-tools 自己的版本;
281
+ 两者都读不到才保持 `^0.1.0` 并在 stderr 上说一句。
244
282
 
245
283
  ## 自己的自检
246
284
 
@@ -248,11 +286,19 @@ dsh-wb-module design accept # 自己在真卡片上逐条走通后,把验收
248
286
  node scripts/selfcheck/index.mjs
249
287
  ```
250
288
 
251
- 工具箱的回归网:两个模板各 `init` 一遍、各跑全套(门禁 + 骨架自带的脚本);判据的**正例与负例**都要对;
252
- 打包清单不含 `.tpl` 残留;`design` 的空格、README 同步、`accept`;`publish` 的清单。
289
+ 工具箱的回归网:两个模板各 `init` 一遍、各跑全套;判据的正例与负例都要对;打包清单不含
290
+ `.tpl` 残留;`design` 的空格、README 同步、`accept`;`publish` 的清单;`release` 的验收闸、
291
+ dry-run、真发后的元数据与发布失败回滚。
253
292
 
254
- ## 去哪看"为什么"
293
+ ## 相关文档
255
294
 
256
- 模块怎么写、DIY 那套流程怎么走,见外壳包里的指引:`@dsh-workbench/core` 的
257
- `skills/workbench-module-diy/**`(`SKILL.md` / `BUILD.md` / `WIRING.md` / `DESIGN.md` / `PANEL.md` /
258
- `TOOLS.md` / `LINKAGE.md` / `DATASOURCE.md` / `REVIEW.md` / `TOOLING.md` / `STEPS.md`)。
295
+ | 想了解 | 读 |
296
+ |---|---|
297
+ | 模块怎么写、DIY 整套流程 | [workbench-module-diy](../core/skills/workbench-module-diy/SKILL.md) |
298
+ | 构建骨架、接线(bundle / client / 注册) | [BUILD.md](../core/skills/workbench-module-diy/BUILD.md) · [WIRING.md](../core/skills/workbench-module-diy/WIRING.md) |
299
+ | kit 组件与 hooks | [KIT.md](../core/skills/workbench-module-diy/KIT.md) |
300
+ | 面板、状态矩阵、界面规则 | [PANEL.md](../core/skills/workbench-module-diy/PANEL.md) |
301
+ | 工具、审批、长任务 | [TOOLS.md](../core/skills/workbench-module-diy/TOOLS.md) |
302
+ | 判据红了怎么读 | [REVIEW.md](../core/skills/workbench-module-diy/REVIEW.md) |
303
+ | 最小可安装样本 | [module-hello](../module-hello/README.md) |
304
+ | 契约类型与共享组件 | [shared](../shared/README.md) |