@dsh-workbench/module-tools 0.3.10 → 1.0.0-beta.0

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 (60) 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 +18 -3
  5. package/src/arch.mjs +158 -129
  6. package/src/build.mjs +6 -30
  7. package/src/changelog.mjs +167 -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 +61 -222
  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 +163 -29
  21. package/src/checks/standalone.mjs +4 -14
  22. package/src/checks/style.mjs +71 -241
  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 +6 -24
  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 +80 -0
  40. package/src/release.mjs +1251 -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/messages.ts.tpl +4 -4
  58. package/template-diy/src/plugin.test.ts.tpl +1 -1
  59. package/template-diy/types/globals.d.ts +1 -14
  60. package/template-diy/types/styles.d.ts +0 -10
@@ -1,25 +1,3 @@
1
- // 模块产物从外壳 kit 里取的成员,必须真的在**它承诺给用户的那份外壳**里存在。
2
- //
3
- // 起因(2026-10-10 现场):`module-cmdb@0.1.7` 的产物用了 0.1.8 才有的
4
- // `kit.components.StateBlock` / `Note` / `Activity*`,而 peer 写的是 `^0.1.4`。
5
- // 装到旧外壳上:顶层解构不抛(`undefined` 也照样解出来),卡片**首帧渲染**时才炸
6
- // `Element type is invalid`;外壳里没有错误边界 ⇒ 整块面板被 React 卸掉。
7
- // 维护者本机复现不了,因为本机 profile 把外壳 `link:` 到本仓(永远 HEAD)。
8
- //
9
- // 两条规则:
10
- // ① **面差**:产物 bind 的 kit 成员 ⊆ 指定外壳 `client.js` 里那份 kit。
11
- // 默认用模块包自己装的那份外壳(devDependency);`DSH_WB_CORE_CLIENT=<路径>`
12
- // 可以指定别的一份 —— 用它验"对 peer 下限那份外壳会不会红"(发布前该跑一遍)。
13
- // ② **下限自洽**:`peerDependencies['@dsh-workbench/core']` 的下限不许低于
14
- // `devDependencies` 的下限。你对用户承诺的版本比你自己构建/测试用的还旧时,
15
- // 产物的面到底够不够,没人验过 —— 这一条就是把"没人验过"变成红灯。
16
- // 用 `DSH_WB_CORE_CLIENT` 指着下限那份外壳跑(且面差通过)= 验过了,规则②不再报。
17
- //
18
- // 判据跟着 `dsh.workbench` 自述清单走;没有 client.js 的包这里没事可查(由 build/契约判据管)。
19
- //
20
- // 形状(所有判据统一):导出 `title` 与 `run()`,`run()` 返回三态结果;
21
- // 打印与退出码归 `../check.mjs` 那一层。
22
-
23
1
  import { existsSync, readFileSync } from 'node:fs';
24
2
  import { join } from 'node:path';
25
3
  import { flagValue, flagsOf } from '../check-flags.mjs';
@@ -37,7 +15,7 @@ function kitUsageOf(text) {
37
15
  const BINDING = /(?:var|const|let)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:__toESM\(\s*)?require\(\s*["']@dsh-workbench\/core\/client["']\s*\)/g;
38
16
  for (const match of text.matchAll(BINDING)) bindings.add(match[1]);
39
17
  if (bindings.size === 0) {
40
- // 认不出绑定名时不静默放过:退化成"全文里任何 `<id>.kit.` 的 id"。
18
+
41
19
  for (const match of text.matchAll(/([A-Za-z_$][\w$]*)\.kit\./g)) bindings.add(match[1]);
42
20
  }
43
21
 
@@ -75,21 +53,6 @@ function below(a, b) {
75
53
  return false;
76
54
  }
77
55
 
78
- // `--only kit` 顺带当"这份外壳的 kit 里有什么"的查询口。
79
- //
80
- // 为什么放在这条判据里:它本来就要把外壳真实产物 `client.js` 跑一遍、取出那份 kit ——
81
- // 这已经是**唯一可信**的取数方式(手抄一份清单就是第二份真相,迟早跟外壳漂)。
82
- // 造模块的人(尤其 DIY 工坊里那位)要的正是"有哪些成员能用",于是单跑这一条时把它打出来。
83
- //
84
- // 只在单跑时打:外壳把整份 `@dsh-workbench/shared` barrel 摊进了每个桶,所以这份名单
85
- // 有一百多个名字 —— 聚合跑时它只是噪音,查询时才值钱。
86
- //
87
- // 名字还要能落到**文件**("想问哪个直接去看"):默认打两棵类型树的绝对根 + 取法,
88
- // `--files` 打逐条 `名字 → 绝对路径`(190 条 / ~9k 字符,会被 tool-result-pruner 裁中间,
89
- // 所以它得配 `| grep <名字>` 用,默认档不内联)。`--json` 给机器档。
90
- //
91
- // 返回的是 `{ notes, payload }`(notes 每条一行,交给聚合层加缩进;payload 归 `--json`)——
92
- // 打印归 check.mjs。
93
56
  function faceNotes(asked, shells) {
94
57
  const faceShell = shells[shells.length - 1] ?? shellClientPathOf(process.cwd());
95
58
  if (!asked) {
@@ -231,10 +194,7 @@ export async function run() {
231
194
  const payload = face.payload === undefined ? {} : { payload: face.payload };
232
195
 
233
196
  if (problems.length > 0) {
234
- // 清单是**事实**、红是**判据**:判据红不该把清单一起扣下。`--json` 的消费者(工坊那把
235
- // `workbench_diy_components`)要靠这份机器档回答"这份外壳有哪些件",而判据红经常是**别的
236
- // 模块**的事 —— 2026-10-20 真机:三个模块用了 MockStrip / useFixturePick,工具整条查询直接
237
- // 报错、agent 什么也没拿到。退出码照旧 1(红没被藏起来),problems 也一起进机器档。
197
+
238
198
  const extra = face.payload === undefined ? {} : { payload: { ...face.payload, problems } };
239
199
  return { status: 'fail', notes: [], problems, ...extra };
240
200
  }
@@ -1,47 +1,17 @@
1
- // 模块包的**语法卫生判据**(`check` 的第十三条):把 eslint 跑在**模块自己的方言**上。
2
- //
3
- // 为什么模块包需要这一条:另外十二条判据里,结构那一半归 arch-guard(`./arch.mjs`),
4
- // 而"单文件语法卫生"那一半一条都没有 —— `as any` / `console` 残留 / `debugger` / 内联颜色 /
5
- // 手写 `!important` / 渲染期写 ref / 漏依赖,这些都不改变文件形状,结构判据永远看不见。
6
- // arch-guard 自己的 README 把这一半明确委派给了 eslint。
7
- //
8
- // 判据的口径(三条,与第十二条 `arch` 逐条对齐 —— 同一套承诺):
9
- // ① **只跑带 `eslint.config.mjs` 的模块**:没有那份文件就 `skip`(不适用),不是红。
10
- // 工坊里已经造好的模块(以及别的仓里已经发出去的模块)不会被这次升级炸掉 —— 补两行就上车。
11
- // ② **判据不自己编规则**:它 import 模块那份配置,**逐块核对整份方言**(`../lint.mjs` 的
12
- // `DIALECT_BLOCKS` 八块:名字 / `files` / `rules` 一处不一致就红;全局 ignores 按 `DIALECT_IGNORES`
13
- // 的**超集**比)再跑;规则与阈值都在 `../lint.mjs` 那一处。
14
- // **残余**:模块自己 import 方言再照抄整份,判据分不出来 —— 这条的目标是"抬高门槛 +
15
- // 让改动必须明着写",不是防住蓄意伪造(详见 `../lint.mjs` 文件头那段)。
16
- // ③ **只把 `error` 算红**:`warn` 照外壳口径计数、不挡门禁(`eslint .` 也只对 error 退 1);
17
- // 单跑(`--only lint`)时把 warn 逐条列出来(与 `--only kit` 列 kit 成员同一个约定)。
18
- //
19
- // ESLint 的**唯一构造处**也在这里(`lintPackage`):判据与 `dsh-wb-module lint [--fix]` 共用它,
20
- // 于是"门禁跑的那份"与"命令跑的那份"在形状上不可能漂。**判据永远不动手改文件**:只有
21
- // `fix: true` 才会写回,而 `fix` 只从命令那条路进来(自检有负例钉着这一点)。
22
- //
23
- // 形状(所有判据统一):导出 `title` 与 `run()`,`run()` 返回三态结果;
24
- // 打印与退出码归 `../check.mjs` 那一层。
25
-
26
1
  import { existsSync } from 'node:fs';
27
2
  import { join, relative } from 'node:path';
28
3
  import { pathToFileURL } from 'node:url';
4
+ import { isDeepStrictEqual } from 'node:util';
29
5
  import { ESLint } from 'eslint';
30
6
  import { discoverModulePackages } from '../discover.mjs';
31
7
  import { DIALECT_BLOCKS, DIALECT_IGNORES, FINGERPRINT, dialectShapeOf, isGlobalIgnoresBlock } from '../lint.mjs';
32
8
 
33
9
  export const title = '模块代码卫生(eslint)';
34
10
 
35
- /** 上车标记:模块根有这份文件才被这条判据管(opt-in,与第十二条同一形状)。 */
36
11
  export const CONFIG_FILE = 'eslint.config.mjs';
37
12
 
38
- // 聚合跑时逐条列出的上限:一条模块可能带几百条 error,门禁那一屏不该被它刷掉、也不该只剩
39
- // 最后几条 —— 截断 + 一句"还有 N 条"(单跑 `--only lint` 时不截断:那时你就是来看全的)。
40
13
  const MAX_PROBLEMS = 40;
41
14
 
42
- // "怎么上车"那句话只写一遍:skip 的提示与方言对不上的人都该看见它。
43
- // 不提本仓内部路径(`src/lint.mjs` 那句):这份提示会随 skill 文档发给用户,用户手上只有
44
- // 装出来的那个包。方言与阈值随 `@dsh-workbench/module-tools` 同代。
45
15
  const HOW_TO_JOIN =
46
16
  `在模块包里放一份 ${CONFIG_FILE},内容两行就够(与模板里那份逐字一致):\n` +
47
17
  ` import { moduleLint } from '@dsh-workbench/module-tools/lint';\n` +
@@ -52,16 +22,12 @@ const HOW_TO_JOIN =
52
22
 
53
23
  const messageOf = (error) => (error instanceof Error ? error.message : String(error));
54
24
 
55
- // 一行一条:把消息压成单行再截断。react-hooks 那几条的 message 里带**整段代码框**(多行),
56
- // 原样倒出来会把"逐条"变成几十行、`·` 前缀也只挂在第一行;前面那句结论("Cannot call impure
57
- // function during render")才是要看的东西,所以压平 + 截断。
58
25
  const TEXT_LIMIT = 200;
59
26
  function oneLine(text) {
60
27
  const flat = text.replace(/\s+/g, ' ').trim();
61
28
  return flat.length > TEXT_LIMIT ? `${flat.slice(0, TEXT_LIMIT)}…` : flat;
62
29
  }
63
30
 
64
- /** 一行人话:`名字: 文件:行:列 规则 —— 一句话`(规则缺了就是解析错误 —— 那也是要立刻看见的)。 */
65
31
  function lineOf(name, dir, result, message) {
66
32
  const line = message.line ?? 1;
67
33
  const column = message.column ?? 1;
@@ -70,7 +36,6 @@ function lineOf(name, dir, result, message) {
70
36
  return `${where} ${rule} —— ${oneLine(message.message)}`;
71
37
  }
72
38
 
73
- /** 方言那几块的名字(基础块 + 四块 `dsh-wb/module-lint*`)+ 全局 ignores 那一块 —— 认"有没有方言痕迹"。 */
74
39
  const DIALECT_NAMES = new Set(DIALECT_BLOCKS.map((block) => block.name));
75
40
 
76
41
  function isDialectNamed(block) {
@@ -82,12 +47,10 @@ function isDialectNamed(block) {
82
47
  );
83
48
  }
84
49
 
85
- /** 配置里有没有一点方言的痕迹(全局 ignores 那一块 / 方言那几块)—— 用来区分"没上车"与"改坏了"。 */
86
50
  function hasDialectTrace(config) {
87
51
  return config.some((block) => isGlobalIgnoresBlock(block) || isDialectNamed(block));
88
52
  }
89
53
 
90
- /** 配置里所有全局 ignores 块的 glob 并集(方言那一块 + 模块自己追加的,可能有不止一块)。 */
91
54
  function globalIgnoresOf(config) {
92
55
  const globs = new Set();
93
56
  for (const block of config) {
@@ -98,12 +61,11 @@ function globalIgnoresOf(config) {
98
61
  return globs;
99
62
  }
100
63
 
101
- /** 两份 rules 差在哪:少了哪些 / 多了哪些 / 哪些值被改了(形状已键排序,逐键比即可)。 */
102
64
  function rulesDivergenceOf(expected, actual) {
103
65
  const gone = Object.keys(expected).filter((rule) => !(rule in actual));
104
66
  const extra = Object.keys(actual).filter((rule) => !(rule in expected));
105
67
  const changed = Object.keys(expected).filter(
106
- (rule) => rule in actual && JSON.stringify(expected[rule]) !== JSON.stringify(actual[rule]),
68
+ (rule) => rule in actual && !isDeepStrictEqual(expected[rule], actual[rule]),
107
69
  );
108
70
  const parts = [];
109
71
  if (gone.length > 0) parts.push(`少了 ${gone.join('、')}`);
@@ -112,22 +74,6 @@ function rulesDivergenceOf(expected, actual) {
112
74
  return parts.join(';');
113
75
  }
114
76
 
115
- /**
116
- * **逐块比**方言(这一期 hardening 的核心):`moduleLint()` 产出的**整份方言**,模块那份配置里
117
- * 必须一块不少、且名字 / `files` / `rules` 逐项一致;全局 ignores 那一块按**超集**比(多忽略
118
- * 可以、少忽略点名)。
119
- *
120
- * 为什么不能只比"有没有那个名字":2026-10-19 verifier 造出
121
- * `export default [{ name: 'dsh-wb/module-lint', rules: {} }]` —— 只比名字时它给 ✔ 0 error,
122
- * 而"方言只有一份、下限不能被悄悄删"正是这条判据的立论。同名多块也判红:扁平配置里**只有
123
- * 最后一块算数**,"留一块好的、再追加一块掏空的"同样是把下限删掉。
124
- *
125
- * 为什么后来把基础块也收进来:只比 `dsh-wb/module-lint*` 那 4 块时,verifier 量到的残余有**两种
126
- * 筛法**:`moduleLint().filter((b) => b.name !== undefined)` 筛掉的是**唯一没有名字**的那块(全局
127
- * ignores 不能带 `name`);基础块(`@eslint/js/recommended` 与三块 `typescript-eslint/*`,**都带
128
- * 名字**)则要**显式点名**(`(b) => b.name !== '@eslint/js/recommended'` 这种)才筛得掉 ——
129
- * 两种都是"下限被悄悄删"。
130
- */
131
77
  function dialectDivergences(config) {
132
78
  const blocks = config.filter((block) => block !== null && typeof block === 'object');
133
79
  const problems = [];
@@ -146,7 +92,7 @@ function dialectDivergences(config) {
146
92
  continue;
147
93
  }
148
94
  const actual = dialectShapeOf(same[0]);
149
- if (JSON.stringify(actual.files) !== JSON.stringify(expected.files)) {
95
+ if (!isDeepStrictEqual(actual.files, expected.files)) {
150
96
  problems.push(`改了「${expected.name}」那一块的 files`);
151
97
  continue;
152
98
  }
@@ -198,14 +144,6 @@ async function loadConfig(pkg) {
198
144
  return { config };
199
145
  }
200
146
 
201
- /**
202
- * 跑一个模块包的 lint。**ESLint 只在这里构造**(判据与 `dsh-wb-module lint` 共用)。
203
- *
204
- * @param {{ dir: string, manifest: { name: string } }} pkg
205
- * @param {{ fix?: boolean }} [options] `fix` 会写回文件;判据那条路永远不传它。
206
- * @returns {Promise<object>} `{ name, files, errors, warnings, problems, warnLines, fixed }`
207
- * 或 `{ name, error }`(配置跑不起来 / 不是 moduleLint 来的)
208
- */
209
147
  export async function lintPackage(pkg, { fix = false } = {}) {
210
148
  const name = pkg.manifest.name;
211
149
  const loaded = await loadConfig(pkg);
@@ -214,33 +152,22 @@ export async function lintPackage(pkg, { fix = false } = {}) {
214
152
  const runOnce = (fixMode) =>
215
153
  new ESLint({
216
154
  cwd: pkg.dir,
217
- // 不读目录里那份配置,用**这里 import 进来的**:指纹已经核过,`files` 相对 cwd 解析
218
- // (Lead 探针实测这条路可行)。这样"判据跑的那份"与"模块文件里写的那份"是同一个对象。
155
+
219
156
  overrideConfigFile: true,
220
- // 头一块**只**补 `tsconfigRootDir`:typescript-eslint 的 parser 拿不到它时会去"猜",
221
- // 猜法是看调用 `tseslint.configs` 的那段栈 —— 一次进程里跑两个模块包就累积出两个候选
222
- // 根目录,于是**第二个包**每个文件都报 "No tsconfigRootDir was set, and multiple
223
- // candidate TSConfigRootDirs are present"(实测:两个模板一起跑,mini 全红)。
224
- // 判据知道自己在跑哪个包,就把这件事钉死;模块自己写的那份配置若声明了
225
- // parserOptions,它在后面,仍然覆盖这一条。
157
+
226
158
  overrideConfig: [
227
159
  {
228
- // 不写 `files`:typescript-eslint 的 base 块不带 `files`,于是**所有**文件(含
229
- // `scripts/*.mjs` 与两份配置)都走 TS parser —— 只给 `*.ts` 补根目录,`.mjs` 那三个
230
- // 照样报同一个错(实测)。
160
+
231
161
  languageOptions: { parserOptions: { tsconfigRootDir: pkg.dir } },
232
162
  },
233
163
  ...loaded.config,
234
164
  ],
235
165
  errorOnUnmatchedPattern: false,
166
+ cache: true,
167
+ cacheLocation: join(pkg.dir, 'node_modules/.cache/eslint'),
236
168
  fix: fixMode,
237
169
  }).lintFiles(['.']);
238
170
 
239
- // `--fix` 的报数要"修了 N 条":fix 那一趟返回的结果是**修完之后**的(实测:`output` 有值、
240
- // messages 只剩修不动的),所以前后各跑一趟普通轮,差值才是真修掉的那批。
241
- //
242
- // 整段包 try:一份"长得像方言但少了 plugin / 塞了坏东西"的配置会让 ESLint 在**建配置**那一步
243
- // 抛(`Could not find plugin …`)—— 那是"跑不起来"这一类,要落成一行人话,不是把判据整条崩掉。
244
171
  let results;
245
172
  let before;
246
173
  try {
@@ -323,7 +250,7 @@ export async function run() {
323
250
  }
324
251
 
325
252
  if (problems.length > 0) {
326
- // 单跑时列全(你就是来看全的);聚合跑截断,把"还有 N 条"说清楚。
253
+
327
254
  const only = process.env.DSH_WB_ONLY === 'lint';
328
255
  const shown = only ? problems : problems.slice(0, MAX_PROBLEMS);
329
256
  const notes = [
@@ -1,22 +1,13 @@
1
- // 公开包里不许出现公司内部标识(内网域名 / 内网 npm 源 / 企业内部名)。
2
- //
3
- // 只扫**工作台自己会发出去的**包:声明了 `dsh.workbench` 的模块,或 `@dsh-workbench/*` 名下的。
4
- // 不能照搬"本 workspace 里所有 access=public 的包" —— 在外壳仓里等价,但把这个工具拿到
5
- // 别人的 monorepo(例如 pdv2)里跑时,那条会把人家二十几个自家 SDK 包一起判了。
6
- //
7
- // 形状(所有判据统一):导出 `title` 与 `run()`,`run()` 返回三态结果;
8
- // 打印与退出码归 `../check.mjs` 那一层。
9
-
10
1
  import { readdirSync, readFileSync, statSync } from 'node:fs';
11
2
  import { join, relative } from 'node:path';
12
3
  import { discoverPackages } from '../discover.mjs';
13
4
 
14
- const MARKERS = [
15
- ['内网 cmdb 域名', /pagoda\.com\.cn/],
16
- ['内网 coding.net 企业名', /\bbaiguokeji\b/i],
17
- ['内网 coding.net 团队名', new RegExp('UED' + 'YHSJ')],
18
- ['公司内部叫法', new RegExp('\u767e' + '\u679c')],
19
- ['内网 npm 源', new RegExp('[a-z0-9-]+\\.pkg\\.coding\\.net', 'i')],
5
+ const MARKERS_ENV = 'DSH_WB_INTERNAL_MARKERS';
6
+
7
+ const BUILT_IN = [
8
+ ['私有 npm 源', /[a-z0-9-]+\.pkg\.[a-z0-9-]+\.(?:net|com|cn)/i],
9
+ ['私网 IP', /\b(?:10\.\d{1,3}|172\.(?:1[6-9]|2\d|3[01])|192\.168)\.\d{1,3}\.\d{1,3}\b/],
10
+ ['内部域名后缀', /\.(?:internal|corp|local)(?:[/:?#]|$)/i],
20
11
  ];
21
12
 
22
13
  const SKIP = new Set(['node_modules', 'dist', 'coverage', '.git', '.turbo']);
@@ -27,6 +18,34 @@ const isOurs = (pkg) =>
27
18
  pkg.manifest.dsh?.workbench !== void 0 ||
28
19
  String(pkg.manifest.name ?? '').startsWith(OWN_SCOPE);
29
20
 
21
+ function configured() {
22
+ const raw = process.env[MARKERS_ENV];
23
+ if (raw === undefined || raw.trim() === '') return { markers: [], problems: [] };
24
+ let parsed;
25
+ try {
26
+ parsed = JSON.parse(raw);
27
+ } catch {
28
+ return { markers: [], problems: [`${MARKERS_ENV} 不是合法 JSON`] };
29
+ }
30
+ if (!Array.isArray(parsed)) {
31
+ return { markers: [], problems: [`${MARKERS_ENV} 必须是数组:每项 [名字, 正则源码]`] };
32
+ }
33
+ const markers = [];
34
+ const problems = [];
35
+ for (const [index, entry] of parsed.entries()) {
36
+ if (!Array.isArray(entry) || entry.length !== 2 || typeof entry[0] !== 'string' || typeof entry[1] !== 'string') {
37
+ problems.push(`${MARKERS_ENV}[${index}] 必须是 [名字, 正则源码]`);
38
+ continue;
39
+ }
40
+ try {
41
+ markers.push([entry[0], new RegExp(entry[1], 'i')]);
42
+ } catch {
43
+ problems.push(`${MARKERS_ENV}[${index}] 的正则源码编译不了:${entry[1]}`);
44
+ }
45
+ }
46
+ return { markers, problems };
47
+ }
48
+
30
49
  function walk(dir, acc = []) {
31
50
  for (const entry of readdirSync(dir)) {
32
51
  if (SKIP.has(entry)) continue;
@@ -40,6 +59,15 @@ function walk(dir, acc = []) {
40
59
  export const title = '公开包无内部标识';
41
60
 
42
61
  export async function run() {
62
+ const { markers: extra, problems: configProblems } = configured();
63
+ if (configProblems.length > 0) {
64
+ return {
65
+ status: 'fail',
66
+ notes: [`(${MARKERS_ENV} 的形状不对,这一轮没有扫)`],
67
+ problems: configProblems,
68
+ };
69
+ }
70
+ const markers = [...BUILT_IN, ...extra];
43
71
  const publicPackages = discoverPackages({ root: process.cwd() }).filter(
44
72
  (pkg) => isOurs(pkg) && pkg.manifest.publishConfig?.access === 'public',
45
73
  );
@@ -53,7 +81,7 @@ export async function run() {
53
81
  continue;
54
82
  }
55
83
  text.split('\n').forEach((line, index) => {
56
- for (const [label, pattern] of MARKERS) {
84
+ for (const [label, pattern] of markers) {
57
85
  if (pattern.test(line)) {
58
86
  problems.push(`${pkg.manifest.name}: ${relative(pkg.dir, file)}:${index + 1} 命中${label} —— ${line.trim().slice(0, 120)}`);
59
87
  }
@@ -78,7 +106,10 @@ export async function run() {
78
106
  }
79
107
  return {
80
108
  status: 'pass',
81
- notes: [`${publicPackages.length} 个 access=public 的包扫过`],
109
+ notes: [
110
+ `${publicPackages.length} 个 access=public 的包扫过` +
111
+ (extra.length === 0 ? `(组织特有标识由 ${MARKERS_ENV} 提供;这次没设)` : `(另有 ${extra.length} 条来自 ${MARKERS_ENV})`),
112
+ ],
82
113
  problems: [],
83
114
  };
84
115
  }
@@ -1,31 +1,3 @@
1
- // 模块用 dsh 的 UI 原语时,**只从模块表拿** —— 这条判据守的是"声明"与"产物"对不对得上。
2
- //
3
- // ADR-0028 决定:浏览器半侧直接用 dsh 的客户端 UI 原语(它们是平台种子词)。但"直接用"要付
4
- // 两笔账,任何一笔错了都不会当场报错,只会**悄悄多一份实例**或**等浏览器 require 那一刻才炸**:
5
- //
6
- // ① 源码 import 了某个种子词 ⇒ 它必须出现在 `dsh.client.external` 里
7
- // (漏声明 ⇒ 构建把那个包**内联**进模块产物 = 第二份 React 组件树、两套样式);
8
- // ② 同一条 ⇒ 产物 `client.js` 里必须有 `require("<说明符>")`
9
- // (内联了就不会有 require —— 这是"到底外部化没有"唯一不靠猜的物证);
10
- // ③ 宿主产物里不许出现种子词的引用(那些包只在浏览器侧存在;宿主侧源码 import 它们
11
- // 由 `checks/source.mjs` 拦,这里对着产物再确认一次)。
12
- //
13
- // 判据跟着 `dsh.workbench` 自述清单走:没用 dsh 原语的模块直接跳过(0 命中 = 通过)。
14
- //
15
- // ── 查询口:`check --only primitives` 单跑时顺带当"dsh 原语里有什么"的取数处 ──────────────
16
- //
17
- // 与 `--only kit` 同一个惯例(`kit.mjs` 的 faceNotes):单跑时把**能用的东西**列出来,
18
- // 聚合跑时只留一句提示。这份清单的真相只有一处 —— 装机包里那份类型面
19
- // (`lib/types/**/*.d.ts`;源码树不随包发布),详见 `../primitives-surface.mjs` 与
20
- // `.scratch/dsh-primitives-face/spec.md`。
21
- //
22
- // 为什么放在这条判据里:它本来就要按模块逐个解析种子词的声明与产物,而"有哪些名字可用"正是
23
- // 造模块的人(尤其 DIY 工坊里那位)要的那件事;单开一个顶层命令要多一处分发、一处用法文案、
24
- // 一处文档,而这里的机制(`DSH_WB_ONLY`)已经在了。
25
- //
26
- // 形状(所有判据统一):导出 `title` 与 `run()`,`run()` 返回三态结果;
27
- // 打印与退出码归 `../check.mjs` 那一层。`--json` 时聚合层打 `payload`(见 `check.mjs` 头注释)。
28
-
29
1
  import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
30
2
  import { createRequire } from 'node:module';
31
3
  import { join, relative } from 'node:path';
@@ -44,7 +16,6 @@ const IMPORT = /from\s*'([^']+)'|import\s*\(?\s*'([^']+)'/g;
44
16
  const SEED = new Set(SEED_SPECIFIERS);
45
17
  const SHELL = '@dsh-workbench/core';
46
18
 
47
- /** 摘要档每个桶最多列几个名字;`--all` 去掉截断。 */
48
19
  const SUMMARY_PER_BUCKET = 12;
49
20
  const BUCKETS = [
50
21
  ['components', 'components'],
@@ -53,7 +24,6 @@ const BUCKETS = [
53
24
  ['lib', 'lib'],
54
25
  ];
55
26
 
56
- /** 短名写法(`ui-primitives`)也算声明过 —— 老包里有这种写法,不该因为写法短被判红。 */
57
27
  function declares(declared, specifier) {
58
28
  if (declared.has(specifier)) return true;
59
29
  const short = specifier.replace(/^@deepseek-ai\/(dsh-client-)?/, '');
@@ -76,7 +46,6 @@ function sourceFiles(dir, acc = []) {
76
46
 
77
47
  export const title = 'dsh 原语只从模块表拿';
78
48
 
79
- /** 从 `from` 找到一份能解析到的原语包:先看模块包自己装的,再看当前 cwd。 */
80
49
  function primitivesFrom(modulePackages) {
81
50
  for (const pkg of modulePackages) {
82
51
  const found = installedPrimitives(pkg.dir);
@@ -86,7 +55,6 @@ function primitivesFrom(modulePackages) {
86
55
  return here === undefined ? undefined : { ...here, from: process.cwd() };
87
56
  }
88
57
 
89
- /** 外壳 manifest 的路径(那份 `devDependencies` 里钉着原语版本)。 */
90
58
  function shellManifestOf(from) {
91
59
  try {
92
60
  return createRequire(join(from, 'package.json')).resolve(`${SHELL}/package.json`);
@@ -95,7 +63,6 @@ function shellManifestOf(from) {
95
63
  }
96
64
  }
97
65
 
98
- /** 外壳 kit 的成员表:`名字 → 桶`(取不到就空表 —— 求交只是锦上添花,不该把查询口弄崩)。 */
99
66
  function kitMembersOf(from) {
100
67
  const client = shellClientPathOf(from);
101
68
  if (client === undefined || !existsSync(client)) return new Map();
@@ -117,10 +84,6 @@ function truncated(names, all) {
117
84
  return `${names.slice(0, SUMMARY_PER_BUCKET).join(', ')} …(还有 ${names.length - SUMMARY_PER_BUCKET} 个,加 --all 看全表)`;
118
85
  }
119
86
 
120
- /**
121
- * 单跑这一条时的清单。返回 `{ notes, payload }`:
122
- * notes 是人话(聚合层逐行打),payload 是 `--json` 的机器档。
123
- */
124
87
  function faceOf(modulePackages) {
125
88
  const flags = flagsOf();
126
89
  const all = flags.includes('--all');
@@ -297,8 +260,6 @@ export async function run() {
297
260
  if (dshSeeds.length === 0) modulesUsingReactOnly += 1;
298
261
  else modulesUsingPrimitives += 1;
299
262
 
300
- // ① 只管 **dsh 的那些浏览器包**:react / react-dom 是构建一直默认外部化的(从第一天就是),
301
- // 要求每个模块去声明它只会制造噪音。dsh 的包不一样 —— 旧版 module-tools 的默认表里没有它们。
302
263
  const needsDeclaration = new Set(BROWSER_SEED_SPECIFIERS);
303
264
  for (const [specifier, where] of used) {
304
265
  if (needsDeclaration.has(specifier) && !declares(declared, specifier)) {
@@ -1,21 +1,3 @@
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
1
  import { declarationProblemsOf } from '@dsh-workbench/shared/host';
20
2
 
21
3
  export const title = '模块声明协议';