@dsh-workbench/module-tools 0.1.16 → 0.1.17
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.
- package/README.md +57 -3
- package/package.json +3 -2
- package/src/check.mjs +3 -3
- package/src/checks/alias.mjs +1 -1
- package/src/checks/bundles.mjs +1 -1
- package/src/checks/cards.mjs +1 -1
- package/src/checks/contracts.mjs +216 -35
- package/src/checks/kit.mjs +1 -1
- package/src/checks/markers.mjs +1 -1
- package/src/checks/primitives.mjs +1 -1
- package/src/checks/protocol.mjs +57 -0
- package/src/checks/source.mjs +14 -8
- package/src/checks/standalone.mjs +1 -1
- package/src/checks/style.mjs +1 -1
- package/src/design.mjs +3 -14
- package/src/publish.mjs +1 -1
- package/template/package.json.tpl +6 -2
- package/template/scripts/smoke.mjs.tpl +4 -6
- package/template/src/client.tsx.tpl +2 -22
- package/template/src/host.ts.tpl +5 -25
- package/template/src/plugin.test.ts.tpl +2 -1
- package/template-diy/README.md.tpl +1 -1
- package/template-diy/package.json.tpl +6 -2
- package/template-diy/scripts/smoke.mjs.tpl +4 -6
- package/template-diy/src/client.tsx.tpl +2 -22
- package/template-diy/src/host.ts.tpl +5 -25
- package/template-diy/src/plugin.test.ts.tpl +4 -4
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
```bash
|
|
9
9
|
dsh-wb-module init <unit> [--dir <目录>] [--template minimal|diy] [--description "一句话"]
|
|
10
10
|
dsh-wb-module build # 出 dist/host.js 与套了 loader 壳的 client.js
|
|
11
|
-
dsh-wb-module check [--only <判据>] #
|
|
11
|
+
dsh-wb-module check [--only <判据>] # 十一条判据;--only 可以单跑
|
|
12
12
|
dsh-wb-module design <init|check|render|accept>
|
|
13
13
|
dsh-wb-module publish # 发货前的清单:门禁 + 三张表 + npm pack 清单(只 dry run)
|
|
14
14
|
```
|
|
@@ -16,11 +16,12 @@ dsh-wb-module publish # 发货前的清单:门禁 + 三张表 +
|
|
|
16
16
|
`--cwd <目录>` 指定包目录(默认当前目录)。模块目录里更省事的是骨架自带的脚本:
|
|
17
17
|
`pnpm build` / `pnpm typecheck` / `pnpm test` / `pnpm selfcheck` / `pnpm design`。
|
|
18
18
|
|
|
19
|
-
##
|
|
19
|
+
## 十一条判据(`check`)
|
|
20
20
|
|
|
21
21
|
| 判据 | 盯什么 |
|
|
22
22
|
|---|---|
|
|
23
|
-
| `contracts` | 模块契约:单元 id 唯一、两个面、四个脚本、`files`、peer
|
|
23
|
+
| `contracts` | 模块契约:单元 id 唯一、两个面、四个脚本、`files`、peer 下限;以及**清单 ↔ 代码**(真正注册的工具)与**底稿 ↔ 清单**(`design.json` 的能力名)两处对账(见下) |
|
|
24
|
+
| `protocol` | 模块声明协议:`dsh.workbench` 的 `apiVersion` 要在、静态半形状要对、不许有协议不认识的键(`scope` 现在**就是**不认识的键之一 —— 见下) |
|
|
24
25
|
| `bundles` | 产物边界:宿主面里不许出现浏览器侧的东西 |
|
|
25
26
|
| `source` | 源码边界:不直接引 dsh 的包、不引不是种子词的 `@deepseek-ai/*` |
|
|
26
27
|
| `primitives` | dsh 原语只从模块表(`kit`)拿 |
|
|
@@ -38,12 +39,65 @@ dsh-wb-module publish # 发货前的清单:门禁 + 三张表 +
|
|
|
38
39
|
三张表(面板 / 能力 / 联动)+ 状态矩阵 + 数据源记录 + 验收清单住在模块根目录的 `design.json`,
|
|
39
40
|
README 里那一段由它生成(夹在 `<!-- dsh-wb:design:start/end -->` 之间)。
|
|
40
41
|
|
|
42
|
+
`design.json` 是**设计底稿**:只装人话(`oneLine` / `shape` / `approvalText` / 面板那几张表的文字 /
|
|
43
|
+
`datasource`),**不装机器字段**。**"这条能力在哪些工作区出现"不在底稿、也不在清单**:
|
|
44
|
+
那是**用户**的事(他的面板板),模块作者声明不了(用户口径「模块开发者不能定义 scope,
|
|
45
|
+
scope 永远是用户来决定的」,见 [ADR-0038](../../docs/adr/0038-module-declaration-protocol.md)
|
|
46
|
+
§2.4 / §6.16)。
|
|
47
|
+
`contracts` 会把底稿的能力名与清单的工具名做**键集合对账**(不重不漏,只对名字):
|
|
48
|
+
底稿多一条、清单多一把都点名判红;没有 `design.json` 的模块不查。
|
|
49
|
+
|
|
41
50
|
```bash
|
|
42
51
|
dsh-wb-module design check # 空着的格一格格点名叫出来 + README 有没有落后
|
|
43
52
|
dsh-wb-module design render # 用 design.json 重新生成 README 那一段
|
|
44
53
|
dsh-wb-module design accept # 用户逐条点头之后,把验收清单打勾
|
|
45
54
|
```
|
|
46
55
|
|
|
56
|
+
## 协议:`dsh` 字段里的五段
|
|
57
|
+
|
|
58
|
+
一个模块包在 `package.json` 里声明五件事,缺哪一段在哪一层炸都不一样:
|
|
59
|
+
|
|
60
|
+
| 段 | 是什么 | 谁读 |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| `dsh.bundle.patch` | 把自己插进 profile 组合图的 cordis 补丁 | dsh 的组合器(**装载**) |
|
|
63
|
+
| `dsh.client{platform,inject,external}` | 浏览器侧接线 | dsh 的客户端组合器(**dsh 自己不校验**,错的声明要等浏览器 `require` 才炸 —— 所以 `contracts` 替你查) |
|
|
64
|
+
| `dsh.workbench` | **模块声明协议**(下详) | 门禁 · 市场 · 外壳 |
|
|
65
|
+
| `dsh.workbenchFaces{host,client}` | **工具链元数据**(**不在协议里**):两个 half 的**源码**入口 | 门禁:`source` 拿它算可达性、`contracts` 查它在位;运行期与装机预检都不读 |
|
|
66
|
+
| `peerDependencies['@dsh-workbench/core']` | 服务面:外壳 provide 的服务在不在 | dsh 的 `evaluatePluginCompatibility` |
|
|
67
|
+
|
|
68
|
+
### `dsh.workbench`(协议 v1)
|
|
69
|
+
|
|
70
|
+
```jsonc
|
|
71
|
+
"workbench": {
|
|
72
|
+
"apiVersion": 1, // 必填。形状契约,缺了外壳不认这个模块
|
|
73
|
+
"unit": "hello", // 单元 id(小写连字符),进持久化配置
|
|
74
|
+
"card": { "titleKey": "hello.cardTitle", "order": 90 }, // 没有卡片写 null
|
|
75
|
+
"tools": [{ "name": "hello_read", "writes": false }], // 没工具写 []
|
|
76
|
+
"host": { "inject": [] }, // 这个单元要外壳的哪几个服务(判"能不能挂")
|
|
77
|
+
"env": { "command": "opencli", "plugins": [], "libs": [] } // 可选:靠什么才跑得起来
|
|
78
|
+
},
|
|
79
|
+
"workbenchFaces": { "host": "src/host.ts", "client": "src/client.tsx" } // 工具链那一格(不在协议里):两半的源码入口
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
五条口径:
|
|
83
|
+
|
|
84
|
+
1. **声明了 `apiVersion` = 宣布静态半写全了** —— `unit` / `card` / `tools` / `host`
|
|
85
|
+
四个键都必须有(`card` 没有卡片写 `null`),`env` 没有外部供给时省略。
|
|
86
|
+
2. **不许有协议不认识的键**:生字段会被外壳**静默忽略**(拼错一个键就等于那句话没写),
|
|
87
|
+
所以 `protocol` 判据先拦。**`scope` 不在协议里**:它曾经是(`card.scope` / `tools[].scope`),
|
|
88
|
+
现已删除 —— 一个能力在哪些工作区出现**只由用户的板决定**,作者写它 = 写了一个生字段、当场红
|
|
89
|
+
(`protocol` 会点名 `"scope"`)。见 [ADR-0038](../../docs/adr/0038-module-declaration-protocol.md) §2.4 / §6.16。
|
|
90
|
+
3. **清单声明的工具 = 代码真正注册的工具**(U1b 起):清单是工具面的**唯一**来源,
|
|
91
|
+
`contracts` 拿 `mount()` 里 `registerTools()` 真正注册的名字对账,两边漂了就红。
|
|
92
|
+
4. **底稿 ↔ 清单只对账键集合**(U1c 起):`design.json` 的 `capabilities[].name` 与清单的
|
|
93
|
+
`tools[].name` 不重不漏,**只对名字**(内容各写一处,没有可比的东西);没有 `design.json`
|
|
94
|
+
的模块不查 —— 底稿是可选的设计材料。见 [ADR-0038](../../docs/adr/0038-module-declaration-protocol.md) §2.5 / §6.3。
|
|
95
|
+
5. **`dsh.workbenchFaces` 不是协议的一部分,但门禁必填**(A-2 起):它是 `dsh.workbench` 的
|
|
96
|
+
**兄弟键**,写两半的**源码**入口;`source` 判据靠它算"这段源码属于哪一半"。
|
|
97
|
+
写进 `dsh.workbench` 里会被判**生字段**(不留兼容),它也不能指向出货产物
|
|
98
|
+
—— 从 `dist/host.js` 反推不出是哪份源码把 dsh 的包拖进来的。见
|
|
99
|
+
[ADR-0038](../../docs/adr/0038-module-declaration-protocol.md) §7 第 7 条。
|
|
100
|
+
|
|
47
101
|
## 两个模板
|
|
48
102
|
|
|
49
103
|
| 模板 | 是什么 |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dsh-workbench/module-tools",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.17",
|
|
4
4
|
"description": "模块包的构建与门禁工具:build(单文件 ESM 宿主侧 + 套 loader 壳的浏览器侧)、check(契约/产物边界/源码边界/卡片渲染)。模块包不再各带一份构建脚本。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"access": "public"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"esbuild": "^0.28.2"
|
|
23
|
+
"esbuild": "^0.28.2",
|
|
24
|
+
"@dsh-workbench/shared": "0.1.17"
|
|
24
25
|
},
|
|
25
26
|
"scripts": {
|
|
26
27
|
"selfcheck": "node scripts/selfcheck.mjs"
|
package/src/check.mjs
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
1
|
+
// 判据的**聚合层**:跑、收集三态、打印、决定退出码。有几条以 `CHECKS` 表为准。
|
|
3
2
|
// ── 为什么判据改成"导出函数",打印与退出码都搬到这里 ──────────────────────────
|
|
4
3
|
//
|
|
5
4
|
// 在这之前,每条 `checks/*.mjs` 都是"import 即执行 + 自己 console.log + 自己 process.exit"
|
|
@@ -21,9 +20,10 @@
|
|
|
21
20
|
// 失败 ✖ <title> <N> 条不过: 然后逐条 ` · <problem>`
|
|
22
21
|
// 最后一行是汇总;有任何 fail 就 exit 1。
|
|
23
22
|
|
|
24
|
-
//
|
|
23
|
+
// 名字与 title 一字不变 —— selfcheck 会把它与各判据模块导出的 `title` 逐个对一遍。
|
|
25
24
|
export const CHECKS = {
|
|
26
25
|
contracts: { title: '模块契约', load: () => import('./checks/contracts.mjs') },
|
|
26
|
+
protocol: { title: '模块声明协议', load: () => import('./checks/protocol.mjs') },
|
|
27
27
|
bundles: { title: '模块产物边界', load: () => import('./checks/bundles.mjs') },
|
|
28
28
|
source: { title: '模块源码边界', load: () => import('./checks/source.mjs') },
|
|
29
29
|
primitives: { title: 'dsh 原语只从模块表拿', load: () => import('./checks/primitives.mjs') },
|
package/src/checks/alias.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// 解析得到 —— 装到别人那里就是构建期找不到文件(`check:standalone` 会把它变成红灯,
|
|
5
5
|
// 这条判据则直接指到源码那一行)。
|
|
6
6
|
//
|
|
7
|
-
//
|
|
7
|
+
// 形状(所有判据统一):导出 `title` 与 `run()`,`run()` 返回三态结果;
|
|
8
8
|
// 打印与退出码归 `../check.mjs` 那一层。
|
|
9
9
|
|
|
10
10
|
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
package/src/checks/bundles.mjs
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// 覆盖范围跟着**自述清单**走:凡 `package.json` 里声明了 `dsh.workbench` 的包都被检查,
|
|
15
15
|
// 新加一个模块不用改这个脚本。
|
|
16
16
|
//
|
|
17
|
-
//
|
|
17
|
+
// 形状(所有判据统一):导出 `title` 与 `run()`,`run()` 返回三态结果;
|
|
18
18
|
// 打印与退出码归 `../check.mjs` 那一层。
|
|
19
19
|
|
|
20
20
|
import { existsSync, readFileSync } from 'node:fs';
|
package/src/checks/cards.mjs
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// `window.__ModuleLoader__.load({id, factory})`,用假的 loader / require 把它拉起来。
|
|
13
13
|
// 覆盖范围跟着 `dsh.workbench` 自述清单走 —— 新模块不用改这里。
|
|
14
14
|
//
|
|
15
|
-
//
|
|
15
|
+
// 形状(所有判据统一):导出 `title` 与 `run()`,`run()` 返回三态结果;
|
|
16
16
|
// 打印与退出码归 `../check.mjs` 那一层。
|
|
17
17
|
|
|
18
18
|
import { existsSync, readFileSync } from 'node:fs';
|
package/src/checks/contracts.mjs
CHANGED
|
@@ -10,17 +10,20 @@
|
|
|
10
10
|
// · `dsh.client` 的 inject / external 每一条都能落到图里的一行(seed 词 / 平台包 / workspace 包);
|
|
11
11
|
// · `dsh.workbench`(**这是个模块**):
|
|
12
12
|
// - `unit` 形状合法、全局唯一(撞了就是装上去当场抛);
|
|
13
|
-
// - `
|
|
13
|
+
// - `dsh.workbenchFaces.host` / `.client` 指的源码入口文件真的在 ——
|
|
14
|
+
// 它是**工具链字段**,不在 `dsh.workbench` 协议里(ADR-0038 §7 第 7 条):
|
|
15
|
+
// `source` 判据靠这两个入口做可达性分析,所以它必须是源码路径、必须声明;
|
|
14
16
|
// - 对壳有 peer 依赖(它跑在外壳交出来的服务上);
|
|
15
17
|
// - 有自己的接线自检脚本(模块侧也必须机器判据,不能只靠人眼)。
|
|
16
18
|
//
|
|
17
|
-
//
|
|
19
|
+
// 形状(所有判据统一):导出 `title` 与 `run()`,`run()` 返回三态结果;
|
|
18
20
|
// 打印与退出码归 `../check.mjs` 那一层。
|
|
19
21
|
|
|
20
22
|
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
21
23
|
import { createRequire } from 'node:module';
|
|
22
24
|
import { join, relative } from 'node:path';
|
|
23
25
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
26
|
+
import { readDeclaration } from '@dsh-workbench/shared/host';
|
|
24
27
|
import { discoverPackages, workspaceRootOf } from '../discover.mjs';
|
|
25
28
|
import { SEED_SHORT } from '../seed.mjs';
|
|
26
29
|
|
|
@@ -241,15 +244,59 @@ function fakeStore() {
|
|
|
241
244
|
return { for: () => one(), machine: () => one(), scan: () => [one()] };
|
|
242
245
|
}
|
|
243
246
|
|
|
244
|
-
|
|
247
|
+
// mount 也一定拿得到 shell 与 workspaceRegistry(外壳给的是 dsh 交出来的服务)。
|
|
248
|
+
// 少给这两样,模块的**降级写法**就会生效 —— 而"拿不到 shell 就只交 routes + registerTools"
|
|
249
|
+
// 是合法写法(实测 pduiv2 的 coding-issues / coding-analysis 就这么写),降级后的面本来就不带
|
|
250
|
+
// 审批钩子,判据却会把这读成"没认领审批":红的是判据自己的环境,不是模块。
|
|
251
|
+
// (与上面 `store` 那笔账同一条纪律:假 deps 少给一样,模块就在这条判据上被冤枉。)
|
|
252
|
+
function fakeShell() {
|
|
253
|
+
return {
|
|
254
|
+
resolve: () => undefined,
|
|
255
|
+
execute: async () => ({
|
|
256
|
+
result: async () => ({
|
|
257
|
+
exitCode: null,
|
|
258
|
+
timedOut: false,
|
|
259
|
+
aborted: false,
|
|
260
|
+
stdout: { text: '', truncated: false },
|
|
261
|
+
stderr: { text: '', truncated: false },
|
|
262
|
+
}),
|
|
263
|
+
}),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function fakeWorkspaceRegistry() {
|
|
268
|
+
return {
|
|
269
|
+
get: () => undefined,
|
|
270
|
+
resolveByPath: async () => undefined,
|
|
271
|
+
create: async () => undefined,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function mountOf(face, created, manifest, tools) {
|
|
245
276
|
if (face === undefined || typeof face.mount !== 'function') return undefined;
|
|
277
|
+
// 假服务**一次 mount 内是同一个对象**(真外壳给的就是同一个)—— 有的模块会拿两次再比。
|
|
278
|
+
const shell = fakeShell();
|
|
279
|
+
const workspaceRegistry = fakeWorkspaceRegistry();
|
|
280
|
+
const toolRegistry = {
|
|
281
|
+
register(tool) {
|
|
282
|
+
tools.push(tool);
|
|
283
|
+
return () => {};
|
|
284
|
+
},
|
|
285
|
+
};
|
|
246
286
|
try {
|
|
247
287
|
return face.mount({
|
|
288
|
+
manifest,
|
|
248
289
|
ctx: {
|
|
249
|
-
get: () =>
|
|
290
|
+
get: (key) => {
|
|
291
|
+
if (key === 'tools') return toolRegistry;
|
|
292
|
+
if (key === 'shell') return shell;
|
|
293
|
+
if (key === 'workspaceRegistry') return workspaceRegistry;
|
|
294
|
+
return undefined;
|
|
295
|
+
},
|
|
250
296
|
effect: (fn) => {
|
|
251
297
|
fn();
|
|
252
298
|
},
|
|
299
|
+
provide: () => {},
|
|
253
300
|
logger: { warn: () => {}, info: () => {} },
|
|
254
301
|
},
|
|
255
302
|
manifests: () => [],
|
|
@@ -266,6 +313,39 @@ const LONG_TASK_METHODS = ['start', 'snapshot', 'active', 'current', 'onChange']
|
|
|
266
313
|
|
|
267
314
|
const LONG_TASK_RENDERERS = ['useLongTask', 'LongTaskRow', 'longTaskDot'];
|
|
268
315
|
|
|
316
|
+
// 审批探针要用的**合成调用**。
|
|
317
|
+
//
|
|
318
|
+
// 为什么不是 `{ arguments: {} }`:外壳调 needsApproval 时拿到的是 agent 真传的参数,
|
|
319
|
+
// 而"这次要不要点头"本来就允许依赖参数 —— "带 confirm: true 才真的写"是最常见的合法口径。
|
|
320
|
+
// 空参数探会把这类模块判成"没认领审批"(实测 pduiv2 那 4 条审批报错就是这么来的,
|
|
321
|
+
// ADR-0038 §6.15)。所以能拿到工具的 `parameters`(JSON Schema)时,按**必填项**造一个
|
|
322
|
+
// 形状合法的调用:枚举取第一个值,string / number / boolean / array / object 各给一个占位值。
|
|
323
|
+
//
|
|
324
|
+
// 拿不到(registerTools() 没注册出这把工具、或注册时没带 parameters)就退回空参数,
|
|
325
|
+
// 并在判红时把这件事写进那句话 —— 那种情况下判据只能验到"钩子在不在"这一层。
|
|
326
|
+
function placeholderOf(schema) {
|
|
327
|
+
if (typeof schema !== 'object' || schema === null) return null;
|
|
328
|
+
if (Array.isArray(schema.enum) && schema.enum.length > 0) return schema.enum[0];
|
|
329
|
+
if (schema.type === 'string') return 'probe';
|
|
330
|
+
if (schema.type === 'number' || schema.type === 'integer') return 1;
|
|
331
|
+
if (schema.type === 'boolean') return true;
|
|
332
|
+
if (schema.type === 'array') return [];
|
|
333
|
+
if (schema.type === 'object') return {};
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function probeCallOf(tool) {
|
|
338
|
+
const schema = tool?.parameters;
|
|
339
|
+
if (typeof schema !== 'object' || schema === null) {
|
|
340
|
+
return { arguments: {}, shaped: false };
|
|
341
|
+
}
|
|
342
|
+
const properties = typeof schema.properties === 'object' && schema.properties !== null ? schema.properties : {};
|
|
343
|
+
const required = Array.isArray(schema.required) ? schema.required : [];
|
|
344
|
+
const args = {};
|
|
345
|
+
for (const key of required) args[key] = placeholderOf(properties[key]);
|
|
346
|
+
return { arguments: args, shaped: true };
|
|
347
|
+
}
|
|
348
|
+
|
|
269
349
|
function installed(specifier, from) {
|
|
270
350
|
try {
|
|
271
351
|
createRequire(join(from, 'package.json')).resolve(`${packageOf(specifier)}/package.json`);
|
|
@@ -289,12 +369,28 @@ function packageOf(specifier) {
|
|
|
289
369
|
return specifier.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0];
|
|
290
370
|
}
|
|
291
371
|
|
|
372
|
+
/** 深比较两份 JSON:对象**键序无关**,数组**顺序有关**(工具那一串的顺序是声明的一部分)。 */
|
|
373
|
+
function reconcileTools(name, manifest, toolNames) {
|
|
374
|
+
const declared = manifest.tools.map((tool) => tool.name).sort();
|
|
375
|
+
const actual = [...new Set(toolNames.filter((one) => one !== ''))].sort();
|
|
376
|
+
if (declared.join('\n') === actual.join('\n')) return [];
|
|
377
|
+
return [
|
|
378
|
+
`${name}: dsh.workbench 声明的能力与代码真正注册的对不上 —— ` +
|
|
379
|
+
`清单 [${declared.join(', ')}]、代码 [${actual.join(', ')}]。` +
|
|
380
|
+
`清单是工具面的**唯一**来源(外壳按它算收窄、市场按它画花名册):` +
|
|
381
|
+
`漏声明 = 那个工具不会被收起也不会进花名册;多声明 = 花名册里有个永远不存在的工具`,
|
|
382
|
+
];
|
|
383
|
+
}
|
|
384
|
+
|
|
292
385
|
export async function run() {
|
|
293
386
|
const packages = discoverPackages({ root: process.cwd() });
|
|
294
387
|
const known = new Set(packages.map((pkg) => pkg.manifest.name));
|
|
295
388
|
const problems = [];
|
|
296
389
|
const unresolvedIn = new Set();
|
|
297
390
|
const units = new Map();
|
|
391
|
+
// 判据"看到了但下不了结论"的那些,写进 notes(红绿两边都印)——
|
|
392
|
+
// 与其给一个撑不住的 verdict,不如把作者该自己确认的那一件事说清楚(ADR-0038 §6.15)。
|
|
393
|
+
const probeNotes = [];
|
|
298
394
|
let declared = 0;
|
|
299
395
|
let modules = 0;
|
|
300
396
|
|
|
@@ -360,11 +456,14 @@ export async function run() {
|
|
|
360
456
|
}
|
|
361
457
|
|
|
362
458
|
for (const face of ['host', 'client']) {
|
|
363
|
-
const rel = dsh.
|
|
459
|
+
const rel = dsh.workbenchFaces?.[face];
|
|
364
460
|
if (typeof rel !== 'string') {
|
|
365
|
-
problems.push(
|
|
461
|
+
problems.push(
|
|
462
|
+
`${name}: dsh.workbenchFaces.${face} 没声明 —— 这两个源码入口是工具链字段` +
|
|
463
|
+
`(不在 dsh.workbench 协议里):source 判据靠它们判"哪一半",缺了它判不了`,
|
|
464
|
+
);
|
|
366
465
|
} else if (!existsSync(join(pkg.dir, rel))) {
|
|
367
|
-
problems.push(`${name}: dsh.
|
|
466
|
+
problems.push(`${name}: dsh.workbenchFaces.${face} 指的文件不存在 —— ${rel}`);
|
|
368
467
|
}
|
|
369
468
|
}
|
|
370
469
|
|
|
@@ -396,10 +495,37 @@ export async function run() {
|
|
|
396
495
|
// 外部供给:清单里声明了 env,就要把"靠什么跑起来"和"没装好时怎么办"都说清。
|
|
397
496
|
// 外壳只按 env 去装(app/host.ts 把 plugins/libs 交给隔离运行时跑 probe/install),
|
|
398
497
|
// 它不知道这个能力叫什么 —— 所以"照做"那句安装命令必须由模块自己给。
|
|
498
|
+
const read = readDeclaration(name, dsh.workbench);
|
|
499
|
+
const manifest = read.manifest;
|
|
399
500
|
const created = [];
|
|
501
|
+
// 注册出来的**工具对象**(不只是名字):审批探针要读它的 `parameters` 造形状合法的调用。
|
|
502
|
+
const tools = [];
|
|
400
503
|
const face = await unitFaceOf(pkg.dir);
|
|
401
|
-
const mounted = face === undefined ? undefined : mountOf(face, created);
|
|
402
|
-
const env =
|
|
504
|
+
const mounted = face === undefined ? undefined : mountOf(face, created, manifest, tools);
|
|
505
|
+
const env = manifest?.env;
|
|
506
|
+
|
|
507
|
+
if (manifest !== undefined) {
|
|
508
|
+
if (face === undefined) {
|
|
509
|
+
problems.push(
|
|
510
|
+
`${name}: 声明了 ${manifest.tools.length} 个能力,但宿主面挂不起来(dist/host.js 缺、或 apply 抛了)` +
|
|
511
|
+
`—— 声明的能力对不了账,先把面修好`,
|
|
512
|
+
);
|
|
513
|
+
} else {
|
|
514
|
+
try {
|
|
515
|
+
mounted?.registerTools();
|
|
516
|
+
} catch (error) {
|
|
517
|
+
problems.push(`${name}: registerTools() 抛了 —— ${String(error)}`);
|
|
518
|
+
}
|
|
519
|
+
problems.push(
|
|
520
|
+
...reconcileTools(
|
|
521
|
+
name,
|
|
522
|
+
manifest,
|
|
523
|
+
tools.map((tool) => (typeof tool?.name === 'string' ? tool.name : '')),
|
|
524
|
+
),
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
403
529
|
if (env !== undefined) {
|
|
404
530
|
problems.push(...envProblemsOf(name, env));
|
|
405
531
|
const products = productsTextOf(pkg.dir);
|
|
@@ -438,7 +564,8 @@ export async function run() {
|
|
|
438
564
|
// 设计表里写了哪一种就得真的接上哪一种 —— 否则用户改完刷新回到默认,而表上写着"能配"。
|
|
439
565
|
// (**与 env 无关**:没有 env 的模块一样要查,所以放在 env 块外面。)
|
|
440
566
|
const configRows = designRowsOf(pkg.dir, 'config');
|
|
441
|
-
const
|
|
567
|
+
const hostProduct = hostProductOf(pkg.dir);
|
|
568
|
+
const wiredPrefs = /\bprefs\s*:/.test(hostProduct);
|
|
442
569
|
const declaredPrefs = (configRows ?? []).some((one) => text(one?.['persistence']) === 'prefs');
|
|
443
570
|
if (declaredPrefs && !wiredPrefs) {
|
|
444
571
|
problems.push(
|
|
@@ -453,18 +580,27 @@ export async function run() {
|
|
|
453
580
|
);
|
|
454
581
|
}
|
|
455
582
|
|
|
456
|
-
//
|
|
457
|
-
//
|
|
458
|
-
//
|
|
459
|
-
// 查反向会把这种合法用法判红。
|
|
583
|
+
// 底稿与清单只对账**键集合**:`design.json` 的 `capabilities[].name` 与清单的 `tools[].name`
|
|
584
|
+
// 不重不漏(ADR-0038 §2.5)。**只对名字**:内容各写一处(人话在底稿、机器字段在清单),没有可比的东西。
|
|
585
|
+
// 没有 `design.json` 的模块不查(底稿是可选的,契约不要求它);清单形状坏了的也不查(protocol 已经说话了)。
|
|
460
586
|
const capabilityRows = designRowsOf(pkg.dir, 'capabilities');
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
587
|
+
if (capabilityRows !== undefined && manifest !== undefined) {
|
|
588
|
+
const inDesign = capabilityRows.map((one) => text(one?.name)).filter((one) => one !== '');
|
|
589
|
+
const inDeclaration = manifest.tools.map((tool) => tool.name);
|
|
590
|
+
for (const name0 of inDesign.filter((one) => !inDeclaration.includes(one))) {
|
|
591
|
+
problems.push(
|
|
592
|
+
`${name}: design.json 的能力行里有「${name0}」,清单的 tools[] 里却没有它 —— ` +
|
|
593
|
+
'能力只有清单那一处能出货(装机预检 / 市场 / 外壳都读它),底稿写了也是一句空话:' +
|
|
594
|
+
`要么把它补进 \`dsh.workbench.tools\`,要么从底稿里删掉`,
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
for (const name0 of inDeclaration.filter((one) => !inDesign.includes(one))) {
|
|
598
|
+
problems.push(
|
|
599
|
+
`${name}: 清单的 tools[] 里声明了「${name0}」,design.json 的能力行里却没有它 —— ` +
|
|
600
|
+
'底稿是给人读的那半(它干嘛、要不要点头,`design render` 拿它出 README):' +
|
|
601
|
+
`要么在 design.json 的 capabilities[] 里为「${name0}」补一行,要么把它从清单里删掉`,
|
|
602
|
+
);
|
|
603
|
+
}
|
|
468
604
|
}
|
|
469
605
|
|
|
470
606
|
// 长任务:跑一会儿的事,注册了就得让用户看得见那四盏灯。
|
|
@@ -580,10 +716,18 @@ export async function run() {
|
|
|
580
716
|
//
|
|
581
717
|
// 外壳每次调用前都会问套件:writes:true 的工具,needsApproval 不给 verdict.kind==='ask'
|
|
582
718
|
// 就**当场 deny**(原文:"在套件的 needsApproval 里补一条审批(含"这次会发生什么"),别让写静默过去")——
|
|
583
|
-
//
|
|
584
|
-
//
|
|
585
|
-
//
|
|
586
|
-
|
|
719
|
+
// 那条错误只有真跑一次才看得见,用户看到的是一次莫名其妙的拒绝。所以门禁在出包前先看一遍。
|
|
720
|
+
//
|
|
721
|
+
// **这条判据只能验到哪一层**(原样写给以后改它的人):
|
|
722
|
+
// · 硬事实:一个 writes:true 的工具,mount() 有没有把 needsApproval 交出来;
|
|
723
|
+
// · 判据能造出合成调用时:对那次调用给的 verdict 形状对不对;verdict 是 ask 时
|
|
724
|
+
// approvalDetail 说没说清"这次会发生什么";
|
|
725
|
+
// · **验不了的**:真实写入调用会不会真的 ask。答案依赖参数的模块("带 confirm: true 才真的写")
|
|
726
|
+
// 在合成调用上依法回 `{ kind: 'read' }` —— 外壳那条路(shared/host/tool-approval.ts)在 read
|
|
727
|
+
// 时**根本不会问 approvalDetail**,所以这里也不再逼它给。这类模块列进 notes 让作者自己确认。
|
|
728
|
+
// · 空参数探会误报:合成调用按声明里的 `parameters` 必填项造(见 probeCallOf),
|
|
729
|
+
// 造不出来时退回空参数并把这件事写进判红那句。
|
|
730
|
+
const writeTools = (manifest?.tools ?? []).filter((one) => one?.writes === true);
|
|
587
731
|
if (writeTools.length > 0) {
|
|
588
732
|
if (mounted === undefined) {
|
|
589
733
|
problems.push(
|
|
@@ -592,13 +736,32 @@ export async function run() {
|
|
|
592
736
|
);
|
|
593
737
|
continue;
|
|
594
738
|
}
|
|
739
|
+
const registered = new Map(tools.map((tool) => [text(tool?.name), tool]));
|
|
740
|
+
const readOnProbe = [];
|
|
595
741
|
for (const tool of writeTools) {
|
|
596
|
-
const
|
|
742
|
+
const probe = probeCallOf(registered.get(tool.name));
|
|
743
|
+
const call = { name: tool.name, arguments: probe.arguments, workspace: null };
|
|
744
|
+
const cannotShape = probe.shaped
|
|
745
|
+
? ''
|
|
746
|
+
: `(registerTools() 没注册出 ${tool.name} 的 parameters,判据只能用空参数探 —— ` +
|
|
747
|
+
`你的判断依赖参数的话,注册时把 parameters 那个 JSON Schema 一起交出来)`;
|
|
748
|
+
if (typeof mounted.needsApproval !== 'function') {
|
|
749
|
+
problems.push(
|
|
750
|
+
`${name}: 工具 ${tool.name} 的清单里写着 writes:true,但 mount() 的返回值里没有 needsApproval —— ` +
|
|
751
|
+
`外壳每次调用前都问它"这次要不要点头",没有它就只有一句"没认领",**每一次调用都当场被拒**。` +
|
|
752
|
+
`做法:mount() 返回的对象里加 needsApproval(call),认出这把工具再给 verdict:` +
|
|
753
|
+
`{ kind: 'read' }(这次不用问)或 { kind: 'ask', reason, question: {zh,en}, fallback: {zh,en} }(要问,且问得清)`,
|
|
754
|
+
);
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
597
757
|
let verdict;
|
|
598
758
|
try {
|
|
599
|
-
verdict = mounted
|
|
600
|
-
} catch {
|
|
601
|
-
|
|
759
|
+
verdict = mounted.needsApproval(call);
|
|
760
|
+
} catch (error) {
|
|
761
|
+
problems.push(
|
|
762
|
+
`${name}: 工具 ${tool.name} 的 needsApproval 在合成调用 ${JSON.stringify(call.arguments)} 上抛了 —— ${String(error)}`,
|
|
763
|
+
);
|
|
764
|
+
continue;
|
|
602
765
|
}
|
|
603
766
|
// 合法口径有两种:`{ kind: 'read' }`(这件事不用问 —— 例:没什么可做的)与
|
|
604
767
|
// `{ kind: 'ask', reason, question{zh,en}, fallback{zh,en} }`(要问,且问得清)。
|
|
@@ -613,15 +776,23 @@ export async function run() {
|
|
|
613
776
|
text(verdict.fallback?.en) !== '';
|
|
614
777
|
if (!read && !ask) {
|
|
615
778
|
problems.push(
|
|
616
|
-
`${name}: 工具 ${tool.name} 的清单里写着 writes:true,但 needsApproval
|
|
779
|
+
`${name}: 工具 ${tool.name} 的清单里写着 writes:true,但 needsApproval 对合成调用 ` +
|
|
780
|
+
`${JSON.stringify(call.arguments)} 没给出合法口径 —— ` +
|
|
617
781
|
`要么 { kind: 'read' }(这件事不用问),要么 { kind: 'ask', reason, question: {zh,en}, ` +
|
|
618
|
-
`fallback: {zh,en} }(要问,且问得清)。别的返回值外壳会当场 deny`,
|
|
782
|
+
`fallback: {zh,en} }(要问,且问得清)。别的返回值外壳会当场 deny${cannotShape}`,
|
|
619
783
|
);
|
|
620
784
|
continue;
|
|
621
785
|
}
|
|
786
|
+
// read:这次调用不用点头。**不查 approvalDetail** —— 外壳在 read 时也不会去问它
|
|
787
|
+
// (`approvalFor` 只在 verdict 是 ask 时才调 detailOf)。合成调用本来就不是一次真实写入,
|
|
788
|
+
// 拿它逼模块给"这次会发生什么"就是让判据替外壳问一个外壳不会问的问题。
|
|
789
|
+
if (read) {
|
|
790
|
+
readOnProbe.push(tool.name);
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
622
793
|
let facts;
|
|
623
794
|
try {
|
|
624
|
-
facts = await mounted
|
|
795
|
+
facts = await mounted.approvalDetail?.(call);
|
|
625
796
|
} catch {
|
|
626
797
|
facts = undefined;
|
|
627
798
|
}
|
|
@@ -632,11 +803,20 @@ export async function run() {
|
|
|
632
803
|
: text(facts.zh) !== '' && text(facts.en) !== '');
|
|
633
804
|
if (!described) {
|
|
634
805
|
problems.push(
|
|
635
|
-
`${name}: 写工具 ${tool.name}
|
|
636
|
-
|
|
806
|
+
`${name}: 写工具 ${tool.name} 的 needsApproval 对合成调用回了 ask(要点头),` +
|
|
807
|
+
`但 approvalDetail 没说清"这次会发生什么"(zh/en 或 {blocked})—— ` +
|
|
808
|
+
`审批屏上用户只能看到一句通用问句,看不到会动什么${cannotShape}`,
|
|
637
809
|
);
|
|
638
810
|
}
|
|
639
811
|
}
|
|
812
|
+
if (readOnProbe.length > 0) {
|
|
813
|
+
probeNotes.push(
|
|
814
|
+
`写工具 ${readOnProbe.join(' / ')} 按合成调用回了"这次不用问"(kind: 'read')—— ` +
|
|
815
|
+
`判据只能验到这一步:它是不是"参数里带 confirm 才真的写"那种**合法**口径,` +
|
|
816
|
+
`合成调用看不出来(必填项给的是占位值,不是真实写入参数)。作者自己确认一次:` +
|
|
817
|
+
`真的调用会不会 ask、ask 时 approvalDetail 说没说清`,
|
|
818
|
+
);
|
|
819
|
+
}
|
|
640
820
|
}
|
|
641
821
|
}
|
|
642
822
|
|
|
@@ -669,7 +849,7 @@ export async function run() {
|
|
|
669
849
|
}
|
|
670
850
|
|
|
671
851
|
if (problems.length > 0) {
|
|
672
|
-
const notes = [`${declared} 个带 dsh 声明的包,其中 ${modules}
|
|
852
|
+
const notes = [`${declared} 个带 dsh 声明的包,其中 ${modules} 个模块`, ...probeNotes];
|
|
673
853
|
// 落不到图里最常见的成因不是"声明写错",而是"这个包不在任何 workspace 里"(裸目录里 init
|
|
674
854
|
// 出来的骨架)。不点出来的话,屏幕上的话会把责任推给那两条声明,而真正该做的是换到工坊里跑。
|
|
675
855
|
const bare = [...unresolvedIn].filter(barePackage);
|
|
@@ -690,6 +870,7 @@ export async function run() {
|
|
|
690
870
|
`inject/external 都能落到图里 · 单元 id 唯一 · 面文件与 peer 都在 · 各有接线自检`,
|
|
691
871
|
'(test 不在这儿跑:状态级的东西在模块自己的 vitest 里 —— 四个脚本自己跑一遍)',
|
|
692
872
|
`单元:${unitList}`,
|
|
873
|
+
...probeNotes,
|
|
693
874
|
],
|
|
694
875
|
problems: [],
|
|
695
876
|
};
|
package/src/checks/kit.mjs
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
//
|
|
18
18
|
// 判据跟着 `dsh.workbench` 自述清单走;没有 client.js 的包这里没事可查(由 build/契约判据管)。
|
|
19
19
|
//
|
|
20
|
-
//
|
|
20
|
+
// 形状(所有判据统一):导出 `title` 与 `run()`,`run()` 返回三态结果;
|
|
21
21
|
// 打印与退出码归 `../check.mjs` 那一层。
|
|
22
22
|
|
|
23
23
|
import { existsSync, readFileSync } from 'node:fs';
|
package/src/checks/markers.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// 不能照搬"本 workspace 里所有 access=public 的包" —— 在外壳仓里等价,但把这个工具拿到
|
|
5
5
|
// 别人的 monorepo(例如 pdv2)里跑时,那条会把人家二十几个自家 SDK 包一起判了。
|
|
6
6
|
//
|
|
7
|
-
//
|
|
7
|
+
// 形状(所有判据统一):导出 `title` 与 `run()`,`run()` 返回三态结果;
|
|
8
8
|
// 打印与退出码归 `../check.mjs` 那一层。
|
|
9
9
|
|
|
10
10
|
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
//
|
|
13
13
|
// 判据跟着 `dsh.workbench` 自述清单走:没用 dsh 原语的模块直接跳过(0 命中 = 通过)。
|
|
14
14
|
//
|
|
15
|
-
//
|
|
15
|
+
// 形状(所有判据统一):导出 `title` 与 `run()`,`run()` 返回三态结果;
|
|
16
16
|
// 打印与退出码归 `../check.mjs` 那一层。
|
|
17
17
|
|
|
18
18
|
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// 模块声明协议:`dsh.workbench` 的**版本**与**静态半形状**。
|
|
2
|
+
//
|
|
3
|
+
// 这一条是**薄壳**:真正的形状判定在 `@dsh-workbench/shared` 的 `declarationProblemsOf()`。
|
|
4
|
+
// 三个调用方共用那一份实现,所以协议只有一处定义:
|
|
5
|
+
// · 门禁(本判据)
|
|
6
|
+
// · 外壳(装配期把声明读成运行期清单,ADR-0038 §2.2)
|
|
7
|
+
// · 装机预检(拿 registry 元数据里的 package.json 就能调,ADR-0038 §2.6)
|
|
8
|
+
// 在 U1b 之前本文件自己带了一套形状校验 —— 那意味着外壳与门禁会各判各的,
|
|
9
|
+
// 正是"两份同类真相"。现在它只剩"把当前目录的包扫一遍、把问题倒出来"。
|
|
10
|
+
//
|
|
11
|
+
// 分工:contracts 管"这个包长得对不对"(unit 唯一、`dsh.workbenchFaces` 在、peer 在、审批认领了没),
|
|
12
|
+
// 本判据管"这份声明读不读得懂"(版本认不认、静态半形状对不对、有没有生字段)。
|
|
13
|
+
//
|
|
14
|
+
// `faces` 自 A-2 起**不在协议里**(ADR-0038 §7 第 7 条):它指的是**源码**入口
|
|
15
|
+
// (`src/host.ts` / `src/client.tsx`),而协议是"装的人手上有什么"那份清单 —— 装机预检与外壳
|
|
16
|
+
// 都不用它。它搬到 `dsh.workbench` 的**兄弟键** `dsh.workbenchFaces`,由 contracts / source 读;
|
|
17
|
+
// 于是它既不是协议里的合法键(写回 `dsh.workbench` 里会被判生字段),也不会被协议"顺手校验"。
|
|
18
|
+
|
|
19
|
+
import { declarationProblemsOf } from '@dsh-workbench/shared/host';
|
|
20
|
+
|
|
21
|
+
export const title = '模块声明协议';
|
|
22
|
+
|
|
23
|
+
export async function run() {
|
|
24
|
+
const { discoverPackages } = await import('../discover.mjs');
|
|
25
|
+
const packages = discoverPackages({ root: process.cwd() });
|
|
26
|
+
const problems = [];
|
|
27
|
+
const versions = new Set();
|
|
28
|
+
let declared = 0;
|
|
29
|
+
|
|
30
|
+
for (const pkg of packages) {
|
|
31
|
+
const raw = pkg.manifest.dsh?.workbench;
|
|
32
|
+
if (raw === undefined) continue;
|
|
33
|
+
problems.push(...declarationProblemsOf(pkg.manifest.name, raw));
|
|
34
|
+
if (typeof raw === 'object' && raw !== null && Number.isInteger(raw.apiVersion)) {
|
|
35
|
+
declared += 1;
|
|
36
|
+
versions.add(raw.apiVersion);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (problems.length > 0) {
|
|
41
|
+
return {
|
|
42
|
+
status: 'fail',
|
|
43
|
+
notes: [`${packages.length} 个包里带 dsh.workbench 声明的已逐个读过`],
|
|
44
|
+
problems,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
status: 'pass',
|
|
50
|
+
notes: [
|
|
51
|
+
declared === 0
|
|
52
|
+
? '这个工作区里还没有模块(没有带 dsh.workbench 的包)'
|
|
53
|
+
: `${declared} 个模块都声明了协议 v${[...versions].sort().join(' / v')},静态半形状齐、没有生字段`,
|
|
54
|
+
],
|
|
55
|
+
problems: [],
|
|
56
|
+
};
|
|
57
|
+
}
|
package/src/checks/source.mjs
CHANGED
|
@@ -5,11 +5,15 @@
|
|
|
5
5
|
// 模块包自己要守的是另外几条,写在下面。判据全部来自源码里的 import 说明符,覆盖范围跟着
|
|
6
6
|
// `dsh.workbench` 自述清单走(新加模块不用改这里)。
|
|
7
7
|
//
|
|
8
|
-
// 关于 dsh 的包:「哪一半」**不靠目录名猜**,而是从两个入口(`
|
|
9
|
-
//
|
|
10
|
-
//
|
|
8
|
+
// 关于 dsh 的包:「哪一半」**不靠目录名猜**,而是从两个入口(`dsh.workbenchFaces.host` /
|
|
9
|
+
// `dsh.workbenchFaces.client`)做一次本包内可达性分析。种子词(模块表里天生就有的那几个)在
|
|
10
|
+
// **只被客户端入口可达**的文件里可以用(ADR-0028);宿主侧一个都不许碰 —— 那些包只在浏览器侧存在。
|
|
11
11
|
//
|
|
12
|
-
//
|
|
12
|
+
// 这两个入口是 dsh.workbenchFaces 的**唯一**用途(工具链字段,不是协议的一部分,见 ADR-0038 §7 第 7 条):
|
|
13
|
+
// 装机预检与外壳都不读它。所以这里必须拿**源码**入口 —— 从产物 dist/host.js 反推不出是哪一份源码
|
|
14
|
+
// 把 dsh 的包拖进来的。
|
|
15
|
+
//
|
|
16
|
+
// 形状(所有判据统一):导出 `title` 与 `run()`,`run()` 返回三态结果;
|
|
13
17
|
// 打印与退出码归 `../check.mjs` 那一层。
|
|
14
18
|
|
|
15
19
|
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
@@ -102,7 +106,7 @@ export async function run() {
|
|
|
102
106
|
for (const pkg of modulePackages) {
|
|
103
107
|
const name = pkg.manifest.name;
|
|
104
108
|
const unit = pkg.manifest.dsh.workbench.unit;
|
|
105
|
-
const
|
|
109
|
+
const workbenchFaces = pkg.manifest.dsh.workbenchFaces ?? {};
|
|
106
110
|
const srcDir = join(pkg.dir, 'src');
|
|
107
111
|
const viaPlatform = hasPlatformPackage(pkg.manifest);
|
|
108
112
|
|
|
@@ -112,8 +116,10 @@ export async function run() {
|
|
|
112
116
|
);
|
|
113
117
|
}
|
|
114
118
|
|
|
115
|
-
const hostReach =
|
|
116
|
-
|
|
119
|
+
const hostReach =
|
|
120
|
+
workbenchFaces.host === undefined ? new Set() : reachableFrom(pkg.dir, unit, workbenchFaces.host);
|
|
121
|
+
const clientReach =
|
|
122
|
+
workbenchFaces.client === undefined ? new Set() : reachableFrom(pkg.dir, unit, workbenchFaces.client);
|
|
117
123
|
const sideOf = (file) => {
|
|
118
124
|
const host = hostReach.has(file);
|
|
119
125
|
const client = clientReach.has(file);
|
|
@@ -147,7 +153,7 @@ export async function run() {
|
|
|
147
153
|
: '这个文件在宿主侧可达';
|
|
148
154
|
problems.push(
|
|
149
155
|
`${name}: ${rel} 里 import 了 ${specifier}(dsh 的浏览器包,是平台种子词)—— ${why},` +
|
|
150
|
-
`而它只在浏览器侧存在。把它挪到只被
|
|
156
|
+
`而它只在浏览器侧存在。把它挪到只被 dsh.workbenchFaces.client 可达的文件里`,
|
|
151
157
|
);
|
|
152
158
|
continue;
|
|
153
159
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// 最后无论成败都还原。藏起来还过不了 = 这条模块在用"只有本仓才有"的相对路径 / 别名 ——
|
|
5
5
|
// 发布出去会散架。
|
|
6
6
|
//
|
|
7
|
-
//
|
|
7
|
+
// 形状(所有判据统一):导出 `title` 与 `run()`,`run()` 返回三态结果;
|
|
8
8
|
// 打印与退出码归 `../check.mjs` 那一层。所以这里**不许**自己退出:
|
|
9
9
|
// 仓外工作区(没有外壳源码)以前就是"提前成功退出"(退出码 0)—— 那会把排在后面的判据静默吞掉。
|
|
10
10
|
|
package/src/checks/style.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// CSS Modules 的哈希类名靠"同一份文件、同一个导出"配对,调用点拿到 undefined 时真机渲染成
|
|
6
6
|
// `class="undefined"` —— 元素整片没样式,而且控制台一声不响。
|
|
7
7
|
//
|
|
8
|
-
//
|
|
8
|
+
// 形状(所有判据统一):导出 `title` 与 `run()`,`run()` 返回三态结果;
|
|
9
9
|
// 打印与退出码归 `../check.mjs` 那一层。
|
|
10
10
|
|
|
11
11
|
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
package/src/design.mjs
CHANGED
|
@@ -41,8 +41,7 @@ const ROWS = {
|
|
|
41
41
|
mayBeEmpty: true,
|
|
42
42
|
},
|
|
43
43
|
capabilities: {
|
|
44
|
-
fields: ['name', 'oneLine', 'shape', 'approval'
|
|
45
|
-
choices: { scope: ['always', 'workspace'] },
|
|
44
|
+
fields: ['name', 'oneLine', 'shape', 'approval'],
|
|
46
45
|
optional: ['approvalText'],
|
|
47
46
|
mayBeEmpty: true,
|
|
48
47
|
},
|
|
@@ -68,15 +67,6 @@ function persistenceLabel(value) {
|
|
|
68
67
|
return cell(raw);
|
|
69
68
|
}
|
|
70
69
|
|
|
71
|
-
/** 一条能力在哪些项目里出现:`always` = 每个项目都该看见;`workspace` = 只在某些项目里有意义,
|
|
72
|
-
* 代码里必须真的用 `gate.deniedTools(workspace)` 把它收窄,否则表上写着窄、实际到处都在。 */
|
|
73
|
-
function scopeLabel(value) {
|
|
74
|
-
const raw = text(value);
|
|
75
|
-
if (raw === 'always') return '哪个项目都能用';
|
|
76
|
-
if (raw === 'workspace') return '只在某些项目里出现(按项目收窄)';
|
|
77
|
-
return cell(raw);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
70
|
function datasourceRowsOf(read) {
|
|
81
71
|
return [
|
|
82
72
|
'| 项 | 值(照抄,别转述) |',
|
|
@@ -264,16 +254,15 @@ export function renderDesign(design) {
|
|
|
264
254
|
'### 表 2 agent:他能替用户做什么、哪几件要点头',
|
|
265
255
|
'',
|
|
266
256
|
tableOf(
|
|
267
|
-
'能力 | 一句话 | 传什么 / 拿回什么(贴真实调用与返回) | 要点头吗 | 点头时他看到的"会发生什么"
|
|
257
|
+
'能力 | 一句话 | 传什么 / 拿回什么(贴真实调用与返回) | 要点头吗 | 点头时他看到的"会发生什么"',
|
|
268
258
|
capabilities.length === 0
|
|
269
|
-
? [['(这份模块故意没有能力)', '—', '—', '—', '—'
|
|
259
|
+
? [['(这份模块故意没有能力)', '—', '—', '—', '—']]
|
|
270
260
|
: capabilities.map((one) => [
|
|
271
261
|
cell(one?.name),
|
|
272
262
|
cell(one?.oneLine),
|
|
273
263
|
cell(one?.shape),
|
|
274
264
|
cell(one?.approval),
|
|
275
265
|
soft(one?.approvalText),
|
|
276
|
-
scopeLabel(one?.scope),
|
|
277
266
|
]),
|
|
278
267
|
),
|
|
279
268
|
'',
|
package/src/publish.mjs
CHANGED
|
@@ -69,7 +69,7 @@ export async function publish({ root }) {
|
|
|
69
69
|
if (existsSync(join(root, DESIGN_FILE))) runCli(root, ['design', 'check']);
|
|
70
70
|
else console.log('○ 没有 design.json(三张表还没落成机器形状)—— 建议先 dsh-wb-module design init');
|
|
71
71
|
} else {
|
|
72
|
-
console.log('○ 这不是一个 wb 模块包(没有 dsh.workbench.unit)——
|
|
72
|
+
console.log('○ 这不是一个 wb 模块包(没有 dsh.workbench.unit)—— 跳过判据,只查打包清单');
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
const packed = packList(root);
|
|
@@ -24,9 +24,13 @@
|
|
|
24
24
|
"external": ["@dsh-workbench/core/client"]
|
|
25
25
|
},
|
|
26
26
|
"workbench": {
|
|
27
|
+
"apiVersion": 1,
|
|
27
28
|
"unit": "__UNIT__",
|
|
28
|
-
"
|
|
29
|
-
|
|
29
|
+
"card": { "titleKey": "__UNIT__.cardTitle", "order": 90 },
|
|
30
|
+
"tools": [],
|
|
31
|
+
"host": { "inject": [] }
|
|
32
|
+
},
|
|
33
|
+
"workbenchFaces": { "host": "src/host.ts", "client": "src/client.tsx" }
|
|
30
34
|
},
|
|
31
35
|
"peerDependencies": { "@dsh-workbench/core": "^0.1.0", "react": "*" },
|
|
32
36
|
"peerDependenciesMeta": { "react": { "optional": true } },
|
|
@@ -108,14 +108,12 @@ const ctx = {
|
|
|
108
108
|
|
|
109
109
|
pluginExports.apply(ctx);
|
|
110
110
|
check('卡片真的注册进了外壳', faces.length === 1, `注册了 ${faces.length} 个`);
|
|
111
|
-
check(`注册的是 ${UNIT_ID} 单元`, faces[0]?.
|
|
111
|
+
check(`注册的是 ${UNIT_ID} 单元`, faces[0]?.unit === UNIT_ID, faces[0]?.unit);
|
|
112
112
|
check('卡片是个能画的组件', typeof faces[0]?.card === 'function');
|
|
113
113
|
check(
|
|
114
|
-
'
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
typeof faces[0]?.manifest.card?.order === 'number',
|
|
118
|
-
JSON.stringify(faces[0]?.manifest.card),
|
|
114
|
+
'面只交 unit(标题与顺序住在 package.json 的 dsh.workbench 里,由外壳按 unit 交给浏览器)',
|
|
115
|
+
faces[0]?.manifest === undefined,
|
|
116
|
+
JSON.stringify(faces[0]?.manifest),
|
|
119
117
|
);
|
|
120
118
|
check(
|
|
121
119
|
'卡片在空数据下也画得出来(不许抛)',
|
|
@@ -4,23 +4,11 @@ import { messages } from './messages';
|
|
|
4
4
|
|
|
5
5
|
const UNIT_ID = '__UNIT__';
|
|
6
6
|
|
|
7
|
-
interface UnitManifest {
|
|
8
|
-
readonly id: string;
|
|
9
|
-
readonly kind: string;
|
|
10
|
-
readonly card: {
|
|
11
|
-
readonly titleKey: string;
|
|
12
|
-
readonly order: number;
|
|
13
|
-
readonly enabledByDefault: boolean;
|
|
14
|
-
} | null;
|
|
15
|
-
readonly tools: readonly unknown[];
|
|
16
|
-
readonly host: { readonly inject: readonly string[] };
|
|
17
|
-
}
|
|
18
|
-
|
|
19
7
|
type Rendered = ReturnType<typeof h>;
|
|
20
8
|
type Translate = (key: string, params?: Record<string, unknown>) => string;
|
|
21
9
|
|
|
22
10
|
interface UnitClientFace {
|
|
23
|
-
readonly
|
|
11
|
+
readonly unit: string;
|
|
24
12
|
readonly messages: { readonly zh: Readonly<Record<string, string>>; readonly en: Readonly<Record<string, string>> };
|
|
25
13
|
readonly card: (props: { readonly t: Translate; readonly onRefresh: () => void }) => Rendered;
|
|
26
14
|
}
|
|
@@ -34,16 +22,8 @@ interface ClientContext {
|
|
|
34
22
|
effect(effect: () => void | (() => void), label?: string): () => void;
|
|
35
23
|
}
|
|
36
24
|
|
|
37
|
-
const manifest: UnitManifest = {
|
|
38
|
-
id: UNIT_ID,
|
|
39
|
-
kind: 'board',
|
|
40
|
-
card: { titleKey: `${UNIT_ID}.cardTitle`, order: 90, enabledByDefault: true },
|
|
41
|
-
tools: [],
|
|
42
|
-
host: { inject: [] },
|
|
43
|
-
};
|
|
44
|
-
|
|
45
25
|
const face: UnitClientFace = {
|
|
46
|
-
|
|
26
|
+
unit: UNIT_ID,
|
|
47
27
|
messages,
|
|
48
28
|
card: (props) => {
|
|
49
29
|
const { Card } = kit.components;
|
package/template/src/host.ts.tpl
CHANGED
|
@@ -4,25 +4,13 @@ export const name = '__NAME__';
|
|
|
4
4
|
|
|
5
5
|
export const inject = ['workbenchUnits'];
|
|
6
6
|
|
|
7
|
-
interface
|
|
8
|
-
readonly
|
|
9
|
-
readonly kind: string;
|
|
10
|
-
readonly card: {
|
|
11
|
-
readonly titleKey: string;
|
|
12
|
-
readonly order: number;
|
|
13
|
-
readonly enabledByDefault: boolean;
|
|
14
|
-
} | null;
|
|
15
|
-
readonly tools: readonly unknown[];
|
|
16
|
-
readonly host: { readonly inject: readonly string[] };
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
interface UnitHostFace {
|
|
20
|
-
readonly manifest: UnitManifest;
|
|
7
|
+
interface UnitModuleFace {
|
|
8
|
+
readonly unit: string;
|
|
21
9
|
mount(): { routes: Record<string, never>; registerTools(): Array<() => void> };
|
|
22
10
|
}
|
|
23
11
|
|
|
24
12
|
interface UnitRegistry {
|
|
25
|
-
register(face:
|
|
13
|
+
register(face: UnitModuleFace): () => void;
|
|
26
14
|
}
|
|
27
15
|
|
|
28
16
|
interface HostContext {
|
|
@@ -31,16 +19,8 @@ interface HostContext {
|
|
|
31
19
|
logger?: { warn?(message: string): void } | undefined;
|
|
32
20
|
}
|
|
33
21
|
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
kind: 'board',
|
|
37
|
-
card: { titleKey: `${UNIT_ID}.cardTitle`, order: 90, enabledByDefault: true },
|
|
38
|
-
tools: [],
|
|
39
|
-
host: { inject: [] },
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
const face: UnitHostFace = {
|
|
43
|
-
manifest,
|
|
22
|
+
const face: UnitModuleFace = {
|
|
23
|
+
unit: UNIT_ID,
|
|
44
24
|
mount: () => ({ routes: {}, registerTools: () => [] }),
|
|
45
25
|
};
|
|
46
26
|
|
|
@@ -39,7 +39,7 @@ describe('__NAME__ 的面', () => {
|
|
|
39
39
|
});
|
|
40
40
|
apply(ctx as never);
|
|
41
41
|
expect(faces).toHaveLength(1);
|
|
42
|
-
expect((faces[0] as {
|
|
42
|
+
expect((faces[0] as { unit: string }).unit).toBe('__UNIT__');
|
|
43
43
|
});
|
|
44
44
|
|
|
45
45
|
it('浏览器半侧把自己的卡片注册进外壳', () => {
|
|
@@ -51,5 +51,6 @@ describe('__NAME__ 的面', () => {
|
|
|
51
51
|
});
|
|
52
52
|
applyClient(ctx as never);
|
|
53
53
|
expect(faces).toHaveLength(1);
|
|
54
|
+
expect((faces[0] as { unit: string }).unit).toBe('__UNIT__');
|
|
54
55
|
});
|
|
55
56
|
});
|
|
@@ -58,7 +58,7 @@ pnpm selfcheck # 构建 + 把 client.js 当浏览器加载器跑一遍 + 调
|
|
|
58
58
|
|
|
59
59
|
| 能力 | 一句话 | 传什么 / 拿回什么(贴真实调用与返回) | 要点头吗 | 点头时他看到的"会发生什么" |
|
|
60
60
|
|---|---|---|---|---|
|
|
61
|
-
| (这份骨架故意没有能力:`
|
|
61
|
+
| (这份骨架故意没有能力:`dsh.workbench.tools` 是空的。要加就照这张表填,只读的不点头、动东西的要点头) | | | | |
|
|
62
62
|
|
|
63
63
|
### 表 3 联动:agent 动完,这个模块哪一格变
|
|
64
64
|
|
|
@@ -25,9 +25,13 @@
|
|
|
25
25
|
"external": ["@dsh-workbench/core/client"]
|
|
26
26
|
},
|
|
27
27
|
"workbench": {
|
|
28
|
+
"apiVersion": 1,
|
|
28
29
|
"unit": "__UNIT__",
|
|
29
|
-
"
|
|
30
|
-
|
|
30
|
+
"card": { "titleKey": "__UNIT__.cardTitle", "order": 90 },
|
|
31
|
+
"tools": [],
|
|
32
|
+
"host": { "inject": [] }
|
|
33
|
+
},
|
|
34
|
+
"workbenchFaces": { "host": "src/host.ts", "client": "src/client.tsx" }
|
|
31
35
|
},
|
|
32
36
|
"peerDependencies": { "@dsh-workbench/core": "^0.1.0", "react": "*" },
|
|
33
37
|
"peerDependenciesMeta": { "react": { "optional": true } },
|
|
@@ -115,14 +115,12 @@ const ctx = {
|
|
|
115
115
|
|
|
116
116
|
pluginExports.apply(ctx);
|
|
117
117
|
check('卡片真的注册进了外壳', faces.length === 1, `注册了 ${faces.length} 个`);
|
|
118
|
-
check(`注册的是 ${UNIT_ID} 单元`, faces[0]?.
|
|
118
|
+
check(`注册的是 ${UNIT_ID} 单元`, faces[0]?.unit === UNIT_ID, faces[0]?.unit);
|
|
119
119
|
check('卡片是个能画的组件', typeof faces[0]?.card === 'function');
|
|
120
120
|
check(
|
|
121
|
-
'
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
typeof faces[0]?.manifest.card?.order === 'number',
|
|
125
|
-
JSON.stringify(faces[0]?.manifest.card),
|
|
121
|
+
'面只交 unit(标题与顺序住在 package.json 的 dsh.workbench 里,由外壳按 unit 交给浏览器)',
|
|
122
|
+
faces[0]?.manifest === undefined,
|
|
123
|
+
JSON.stringify(faces[0]?.manifest),
|
|
126
124
|
);
|
|
127
125
|
check(
|
|
128
126
|
'卡片在空数据下也画得出来(不许抛)',
|
|
@@ -10,18 +10,6 @@ const KB = 1024;
|
|
|
10
10
|
const { Card, MockStrip, CenteredState, EmptyState, StateBlock, IconPanelScreen } = kit.components;
|
|
11
11
|
const { useApiResource, useFixturePick } = kit.hooks;
|
|
12
12
|
|
|
13
|
-
interface UnitManifest {
|
|
14
|
-
readonly id: string;
|
|
15
|
-
readonly kind: string;
|
|
16
|
-
readonly card: {
|
|
17
|
-
readonly titleKey: string;
|
|
18
|
-
readonly order: number;
|
|
19
|
-
readonly enabledByDefault: boolean;
|
|
20
|
-
} | null;
|
|
21
|
-
readonly tools: readonly unknown[];
|
|
22
|
-
readonly host: { readonly inject: readonly string[] };
|
|
23
|
-
}
|
|
24
|
-
|
|
25
13
|
type Rendered = ReturnType<typeof h>;
|
|
26
14
|
type Translate = (key: string, params?: Record<string, unknown>) => string;
|
|
27
15
|
|
|
@@ -37,7 +25,7 @@ interface BoardProps {
|
|
|
37
25
|
}
|
|
38
26
|
|
|
39
27
|
interface UnitClientFace {
|
|
40
|
-
readonly
|
|
28
|
+
readonly unit: string;
|
|
41
29
|
readonly messages: { readonly zh: Readonly<Record<string, string>>; readonly en: Readonly<Record<string, string>> };
|
|
42
30
|
readonly card: (props: BoardProps) => Rendered;
|
|
43
31
|
}
|
|
@@ -146,14 +134,6 @@ export function overviewStateOf(input: OverviewInput): OverviewState {
|
|
|
146
134
|
return { kind: 'loading' };
|
|
147
135
|
}
|
|
148
136
|
|
|
149
|
-
const manifest: UnitManifest = {
|
|
150
|
-
id: UNIT_ID,
|
|
151
|
-
kind: 'board',
|
|
152
|
-
card: { titleKey: `${UNIT_ID}.cardTitle`, order: 90, enabledByDefault: true },
|
|
153
|
-
tools: [],
|
|
154
|
-
host: { inject: [] },
|
|
155
|
-
};
|
|
156
|
-
|
|
157
137
|
function bodyOf(state: OverviewState, t: Translate): Rendered {
|
|
158
138
|
if (state.kind === 'idle') {
|
|
159
139
|
return h(CenteredState, {
|
|
@@ -207,7 +187,7 @@ function bodyOf(state: OverviewState, t: Translate): Rendered {
|
|
|
207
187
|
}
|
|
208
188
|
|
|
209
189
|
const face: UnitClientFace = {
|
|
210
|
-
|
|
190
|
+
unit: UNIT_ID,
|
|
211
191
|
messages,
|
|
212
192
|
card: (props) => {
|
|
213
193
|
const workspace = props.world.current.workspace;
|
|
@@ -56,25 +56,13 @@ function limitOf(search: URLSearchParams): number {
|
|
|
56
56
|
return Math.min(MAX_LIMIT, Math.max(1, Math.trunc(parsed)));
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
interface
|
|
60
|
-
readonly
|
|
61
|
-
readonly kind: string;
|
|
62
|
-
readonly card: {
|
|
63
|
-
readonly titleKey: string;
|
|
64
|
-
readonly order: number;
|
|
65
|
-
readonly enabledByDefault: boolean;
|
|
66
|
-
} | null;
|
|
67
|
-
readonly tools: readonly unknown[];
|
|
68
|
-
readonly host: { readonly inject: readonly string[] };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
interface UnitHostFace {
|
|
72
|
-
readonly manifest: UnitManifest;
|
|
59
|
+
interface UnitModuleFace {
|
|
60
|
+
readonly unit: string;
|
|
73
61
|
mount(): { routes: Record<string, RouteHandler>; registerTools(): Array<() => void> };
|
|
74
62
|
}
|
|
75
63
|
|
|
76
64
|
interface UnitRegistry {
|
|
77
|
-
register(face:
|
|
65
|
+
register(face: UnitModuleFace): () => void;
|
|
78
66
|
}
|
|
79
67
|
|
|
80
68
|
interface HostContext {
|
|
@@ -83,16 +71,8 @@ interface HostContext {
|
|
|
83
71
|
logger?: { warn?(message: string): void } | undefined;
|
|
84
72
|
}
|
|
85
73
|
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
kind: 'board',
|
|
89
|
-
card: { titleKey: `${UNIT_ID}.cardTitle`, order: 90, enabledByDefault: true },
|
|
90
|
-
tools: [],
|
|
91
|
-
host: { inject: [] },
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
const face: UnitHostFace = {
|
|
95
|
-
manifest,
|
|
74
|
+
const face: UnitModuleFace = {
|
|
75
|
+
unit: UNIT_ID,
|
|
96
76
|
mount: () => ({
|
|
97
77
|
routes: {
|
|
98
78
|
[OVERVIEW_ROUTE]: async (search: URLSearchParams) => {
|
|
@@ -51,12 +51,12 @@ import {
|
|
|
51
51
|
} from './client';
|
|
52
52
|
|
|
53
53
|
interface HostFace {
|
|
54
|
-
readonly
|
|
54
|
+
readonly unit: string;
|
|
55
55
|
mount(): { readonly routes: Record<string, (search: URLSearchParams) => Promise<unknown>> };
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
interface ClientFace {
|
|
59
|
-
readonly
|
|
59
|
+
readonly unit: string;
|
|
60
60
|
readonly card: (props: {
|
|
61
61
|
readonly world: { readonly current: { readonly workspace?: { readonly path: string } | null } };
|
|
62
62
|
readonly refreshToken: number;
|
|
@@ -163,8 +163,8 @@ describe('__NAME__ 的面', () => {
|
|
|
163
163
|
});
|
|
164
164
|
|
|
165
165
|
it('宿主半侧与浏览器半侧都把自己的面注册进外壳', () => {
|
|
166
|
-
expect(hostFace().
|
|
167
|
-
expect(clientFace().
|
|
166
|
+
expect(hostFace().unit).toBe('__UNIT__');
|
|
167
|
+
expect(clientFace().unit).toBe('__UNIT__');
|
|
168
168
|
});
|
|
169
169
|
});
|
|
170
170
|
|